mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-06 09:00:29 +02:00
Logo: a DualShock 4 mark, and an icon with three lit controllers
The mark is a vector DualShock 4 seen from above (launcher/logo.c), filled with stb_truetype's anti-aliased rasterizer through a small path API in draw.c. The app's top bar and the page's top bar, toast and favicon all use it. The package icon shows three controllers with their light bars glowing in the page's player colours.
This commit is contained in:
1 parent
5e38f1db2d
commit
346b18ba6c
9 files changed
+372
-35
No files matched your search
+5
-5
@@ -9,7 +9,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="description" content="Use this device as a PS4 controller">
|
||||
<title>Control4Free</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230a3c96'/%3E%3Cpath d='M9 11h14a5 5 0 0 1 4.8 6.4l-1 3.4a3 3 0 0 1-5.2 1L19 19h-6l-2.6 2.8a3 3 0 0 1-5.2-1l-1-3.4A5 5 0 0 1 9 11z' fill='%23fff'/%3E%3C/svg%3E">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 1000'%3E%3Crect width='1000' height='1000' rx='220' fill='%230a3c96'/%3E%3Cpath transform='translate(80 228) scale(0.84)' fill='%23fff' d='M500 78C410 78 330 74 262 62C232 46 188 42 146 62C108 80 86 120 78 170C70 230 58 320 60 400C62 470 80 535 118 568C152 598 196 592 222 560C250 525 272 470 300 430C360 418 430 440 500 440C570 440 640 418 700 430C728 470 750 525 778 560C804 592 848 598 882 568C920 535 938 470 940 400C942 320 930 230 922 170C914 120 892 80 854 62C812 42 768 46 738 62C670 74 590 78 500 78ZM366 98L634 98C648.359 98 660 109.641 660 124L660 226C660 240.359 648.359 252 634 252L366 252C351.641 252 340 240.359 340 226L340 124C340 109.641 351.641 98 366 98ZM173 148L219 148L219 189L260 189L260 235L219 235L219 276L173 276L173 235L132 235L132 189L173 189L173 148ZM804 121C818.912 121 831 133.088 831 148C831 162.912 818.912 175 804 175C789.088 175 777 162.912 777 148C777 133.088 789.088 121 804 121ZM804 249C818.912 249 831 261.088 831 276C831 290.912 818.912 303 804 303C789.088 303 777 290.912 777 276C777 261.088 789.088 249 804 249ZM740 185C754.912 185 767 197.088 767 212C767 226.912 754.912 239 740 239C725.088 239 713 226.912 713 212C713 197.088 725.088 185 740 185ZM868 185C882.912 185 895 197.088 895 212C895 226.912 882.912 239 868 239C853.088 239 841 226.912 841 212C841 197.088 853.088 185 868 185ZM338 276C374.451 276 404 305.549 404 342C404 378.451 374.451 408 338 408C301.549 408 272 378.451 272 342C272 305.549 301.549 276 338 276ZM338 298C313.699 298 294 317.699 294 342C294 366.301 313.699 386 338 386C362.301 386 382 366.301 382 342C382 317.699 362.301 298 338 298ZM662 276C698.451 276 728 305.549 728 342C728 378.451 698.451 408 662 408C625.549 408 596 378.451 596 342C596 305.549 625.549 276 662 276ZM662 298C637.699 298 618 317.699 618 342C618 366.301 637.699 386 662 386C686.301 386 706 366.301 706 342C706 317.699 686.301 298 662 298ZM500 319C509.389 319 517 326.611 517 336C517 345.389 509.389 353 500 353C490.611 353 483 345.389 483 336C483 326.611 490.611 319 500 319Z'/%3E%3C/svg%3E">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
@@ -124,7 +124,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-mark svg { width: 26px; height: 26px; }
|
||||
.app-mark svg { width: 36px; height: 23px; }
|
||||
|
||||
.clock {
|
||||
font-size: 17px;
|
||||
@@ -927,7 +927,7 @@
|
||||
transition: opacity .25s, transform .3s var(--ease);
|
||||
}
|
||||
|
||||
.toast svg { width: 26px; height: 26px; flex: none; }
|
||||
.toast svg { width: 34px; height: 22px; flex: none; }
|
||||
.toast.show { opacity: 1; transform: none; }
|
||||
|
||||
.keep-awake { position: fixed; left: 0; bottom: 0; width: 1px; height: 1px; opacity: .01; pointer-events: none; }
|
||||
@@ -962,7 +962,7 @@
|
||||
<header class="topbar">
|
||||
<div class="tb-group">
|
||||
<div class="app-mark">
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="#fff" d="M9 10h14a6 6 0 0 1 5.8 7.6l-1.2 4.6a3.4 3.4 0 0 1-5.9 1.3L18.5 20h-5l-3.2 3.5a3.4 3.4 0 0 1-5.9-1.3l-1.2-4.6A6 6 0 0 1 9 10zm1 4v2h-2v2h2v2h2v-2h2v-2h-2v-2h-2zm11.5 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.5 3.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/></svg>
|
||||
<svg viewBox="56 38 888 566" aria-hidden="true"><path fill="#fff" d="M500 78C410 78 330 74 262 62C232 46 188 42 146 62C108 80 86 120 78 170C70 230 58 320 60 400C62 470 80 535 118 568C152 598 196 592 222 560C250 525 272 470 300 430C360 418 430 440 500 440C570 440 640 418 700 430C728 470 750 525 778 560C804 592 848 598 882 568C920 535 938 470 940 400C942 320 930 230 922 170C914 120 892 80 854 62C812 42 768 46 738 62C670 74 590 78 500 78ZM366 98L634 98C648.359 98 660 109.641 660 124L660 226C660 240.359 648.359 252 634 252L366 252C351.641 252 340 240.359 340 226L340 124C340 109.641 351.641 98 366 98ZM173 148L219 148L219 189L260 189L260 235L219 235L219 276L173 276L173 235L132 235L132 189L173 189L173 148ZM804 121C818.912 121 831 133.088 831 148C831 162.912 818.912 175 804 175C789.088 175 777 162.912 777 148C777 133.088 789.088 121 804 121ZM804 249C818.912 249 831 261.088 831 276C831 290.912 818.912 303 804 303C789.088 303 777 290.912 777 276C777 261.088 789.088 249 804 249ZM740 185C754.912 185 767 197.088 767 212C767 226.912 754.912 239 740 239C725.088 239 713 226.912 713 212C713 197.088 725.088 185 740 185ZM868 185C882.912 185 895 197.088 895 212C895 226.912 882.912 239 868 239C853.088 239 841 226.912 841 212C841 197.088 853.088 185 868 185ZM338 276C374.451 276 404 305.549 404 342C404 378.451 374.451 408 338 408C301.549 408 272 378.451 272 342C272 305.549 301.549 276 338 276ZM338 298C313.699 298 294 317.699 294 342C294 366.301 313.699 386 338 386C362.301 386 382 366.301 382 342C382 317.699 362.301 298 338 298ZM662 276C698.451 276 728 305.549 728 342C728 378.451 698.451 408 662 408C625.549 408 596 378.451 596 342C596 305.549 625.549 276 662 276ZM662 298C637.699 298 618 317.699 618 342C618 366.301 637.699 386 662 386C686.301 386 706 366.301 706 342C706 317.699 686.301 298 662 298ZM500 319C509.389 319 517 326.611 517 336C517 345.389 509.389 353 500 353C490.611 353 483 345.389 483 336C483 326.611 490.611 319 500 319Z"/></svg>
|
||||
<span>Control4Free</span>
|
||||
</div>
|
||||
<span class="conn" id="conn" data-conn="connecting" role="status" aria-live="polite"><span class="dot"></span><span id="connText">Connecting</span></span>
|
||||
@@ -1133,7 +1133,7 @@
|
||||
</aside>
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite">
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true"><path fill="#fff" d="M9 10h14a6 6 0 0 1 5.8 7.6l-1.2 4.6a3.4 3.4 0 0 1-5.9 1.3L18.5 20h-5l-3.2 3.5a3.4 3.4 0 0 1-5.9-1.3l-1.2-4.6A6 6 0 0 1 9 10z"/></svg>
|
||||
<svg viewBox="56 38 888 566" aria-hidden="true"><path fill="#fff" d="M500 78C410 78 330 74 262 62C232 46 188 42 146 62C108 80 86 120 78 170C70 230 58 320 60 400C62 470 80 535 118 568C152 598 196 592 222 560C250 525 272 470 300 430C360 418 430 440 500 440C570 440 640 418 700 430C728 470 750 525 778 560C804 592 848 598 882 568C920 535 938 470 940 400C942 320 930 230 922 170C914 120 892 80 854 62C812 42 768 46 738 62C670 74 590 78 500 78ZM366 98L634 98C648.359 98 660 109.641 660 124L660 226C660 240.359 648.359 252 634 252L366 252C351.641 252 340 240.359 340 226L340 124C340 109.641 351.641 98 366 98ZM173 148L219 148L219 189L260 189L260 235L219 235L219 276L173 276L173 235L132 235L132 189L173 189L173 148ZM804 121C818.912 121 831 133.088 831 148C831 162.912 818.912 175 804 175C789.088 175 777 162.912 777 148C777 133.088 789.088 121 804 121ZM804 249C818.912 249 831 261.088 831 276C831 290.912 818.912 303 804 303C789.088 303 777 290.912 777 276C777 261.088 789.088 249 804 249ZM740 185C754.912 185 767 197.088 767 212C767 226.912 754.912 239 740 239C725.088 239 713 226.912 713 212C713 197.088 725.088 185 740 185ZM868 185C882.912 185 895 197.088 895 212C895 226.912 882.912 239 868 239C853.088 239 841 226.912 841 212C841 197.088 853.088 185 868 185ZM338 276C374.451 276 404 305.549 404 342C404 378.451 374.451 408 338 408C301.549 408 272 378.451 272 342C272 305.549 301.549 276 338 276ZM338 298C313.699 298 294 317.699 294 342C294 366.301 313.699 386 338 386C362.301 386 382 366.301 382 342C382 317.699 362.301 298 338 298ZM662 276C698.451 276 728 305.549 728 342C728 378.451 698.451 408 662 408C625.549 408 596 378.451 596 342C596 305.549 625.549 276 662 276ZM662 298C637.699 298 618 317.699 618 342C618 366.301 637.699 386 662 386C686.301 386 706 366.301 706 342C706 317.699 686.301 298 662 298ZM500 319C509.389 319 517 326.611 517 336C517 345.389 509.389 353 500 353C490.611 353 483 345.389 483 336C483 326.611 490.611 319 500 319Z"/></svg>
|
||||
<span id="toastText"></span>
|
||||
</div>
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ CC := clang-18
|
||||
CXX := clang++-18
|
||||
LD := ld.lld-18
|
||||
CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -O2 -Wall -Wextra -Werror -MMD -MP -D_DEFAULT_SOURCE -isysroot $(SDK) -isystem $(SDK)/include -I../vendor/jsmn -I../vendor/qrcodegen -I../vendor/stb
|
||||
OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
|
||||
OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
|
||||
LIBS := -lc -lkernel -lSceVideoOut -lScePad -lSceUserService -lSceSystemService -lSceNetCtl
|
||||
FONTS := ../vendor/roboto/Roboto-Light.ttf ../vendor/roboto/Roboto-Regular.ttf
|
||||
.PHONY: all package
|
||||
|
||||
+181
@@ -258,6 +258,187 @@ void c4fTriangleOutline(C4fCanvas *c, float cx, float cy, float radius, float wi
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- paths ---- */
|
||||
|
||||
static C4fPathOp *c4fPathPush(C4fPath *p, char op)
|
||||
{
|
||||
if (p->count == p->capacity) {
|
||||
int capacity = p->capacity ? p->capacity * 2 : 64;
|
||||
C4fPathOp *ops = realloc(p->ops, (size_t)capacity * sizeof(*ops));
|
||||
if (!ops) return NULL;
|
||||
p->ops = ops;
|
||||
p->capacity = capacity;
|
||||
}
|
||||
C4fPathOp *o = &p->ops[p->count++];
|
||||
memset(o, 0, sizeof(*o));
|
||||
o->op = op;
|
||||
return o;
|
||||
}
|
||||
|
||||
void c4fPathMove(C4fPath *p, float x, float y)
|
||||
{
|
||||
C4fPathOp *o = c4fPathPush(p, 'M');
|
||||
if (o) { o->v[0] = x; o->v[1] = y; }
|
||||
}
|
||||
|
||||
void c4fPathLine(C4fPath *p, float x, float y)
|
||||
{
|
||||
C4fPathOp *o = c4fPathPush(p, 'L');
|
||||
if (o) { o->v[0] = x; o->v[1] = y; }
|
||||
}
|
||||
|
||||
void c4fPathCubic(C4fPath *p, float x1, float y1, float x2, float y2, float x, float y)
|
||||
{
|
||||
C4fPathOp *o = c4fPathPush(p, 'C');
|
||||
if (o) { o->v[0] = x1; o->v[1] = y1; o->v[2] = x2; o->v[3] = y2; o->v[4] = x; o->v[5] = y; }
|
||||
}
|
||||
|
||||
/* Screen y points down, so "clockwise" is as seen on screen. */
|
||||
void c4fPathCircle(C4fPath *p, float cx, float cy, float r, int clockwise)
|
||||
{
|
||||
const float k = 0.5522847f * r, s = clockwise ? 1.0f : -1.0f;
|
||||
c4fPathMove(p, cx, cy - r);
|
||||
c4fPathCubic(p, cx + s * k, cy - r, cx + s * r, cy - k, cx + s * r, cy);
|
||||
c4fPathCubic(p, cx + s * r, cy + k, cx + s * k, cy + r, cx, cy + r);
|
||||
c4fPathCubic(p, cx - s * k, cy + r, cx - s * r, cy + k, cx - s * r, cy);
|
||||
c4fPathCubic(p, cx - s * r, cy - k, cx - s * k, cy - r, cx, cy - r);
|
||||
}
|
||||
|
||||
void c4fPathRoundRect(C4fPath *p, float x, float y, float w, float h, float r, int clockwise)
|
||||
{
|
||||
const float k = 0.5522847f * r, x1 = x + w, y1 = y + h;
|
||||
if (clockwise) {
|
||||
c4fPathMove(p, x + r, y);
|
||||
c4fPathLine(p, x1 - r, y);
|
||||
c4fPathCubic(p, x1 - r + k, y, x1, y + r - k, x1, y + r);
|
||||
c4fPathLine(p, x1, y1 - r);
|
||||
c4fPathCubic(p, x1, y1 - r + k, x1 - r + k, y1, x1 - r, y1);
|
||||
c4fPathLine(p, x + r, y1);
|
||||
c4fPathCubic(p, x + r - k, y1, x, y1 - r + k, x, y1 - r);
|
||||
c4fPathLine(p, x, y + r);
|
||||
c4fPathCubic(p, x, y + r - k, x + r - k, y, x + r, y);
|
||||
} else {
|
||||
c4fPathMove(p, x + r, y);
|
||||
c4fPathCubic(p, x + r - k, y, x, y + r - k, x, y + r);
|
||||
c4fPathLine(p, x, y1 - r);
|
||||
c4fPathCubic(p, x, y1 - r + k, x + r - k, y1, x + r, y1);
|
||||
c4fPathLine(p, x1 - r, y1);
|
||||
c4fPathCubic(p, x1 - r + k, y1, x1, y1 - r + k, x1, y1 - r);
|
||||
c4fPathLine(p, x1, y + r);
|
||||
c4fPathCubic(p, x1, y + r - k, x1 - r + k, y, x1 - r, y);
|
||||
c4fPathLine(p, x + r, y);
|
||||
}
|
||||
}
|
||||
|
||||
void c4fPathFree(C4fPath *p)
|
||||
{
|
||||
free(p->ops);
|
||||
memset(p, 0, sizeof(*p));
|
||||
}
|
||||
|
||||
/* stb_truetype's rasterizer takes 16-bit vertices: work in 1/16 pixels. */
|
||||
#define C4F_SUBPIXEL 16.0f
|
||||
|
||||
static stbtt_vertex_type c4fVertexUnit(float v)
|
||||
{
|
||||
float u = v * C4F_SUBPIXEL;
|
||||
return (stbtt_vertex_type)(u > 32000 ? 32000 : u < -32000 ? -32000 : u);
|
||||
}
|
||||
|
||||
int c4fPathMask(const C4fCanvas *c, const C4fPath *p, C4fPlacement at, unsigned char **mask, int *x0, int *y0, int *w, int *h)
|
||||
{
|
||||
const float angle = at.angle * 3.14159265f / 180.0f, cs = cosf(angle), sn = sinf(angle);
|
||||
float minX = 1e9f, minY = 1e9f, maxX = -1e9f, maxY = -1e9f;
|
||||
stbtt_vertex *v = calloc((size_t)p->count + 1, sizeof(*v));
|
||||
if (!v || !p->count) { free(v); return 0; }
|
||||
for (int i = 0; i < p->count; i++) {
|
||||
const C4fPathOp *o = &p->ops[i];
|
||||
float placed[6];
|
||||
int points = o->op == 'C' ? 3 : 1;
|
||||
for (int k = 0; k < points; k++) {
|
||||
float dx = (o->v[k * 2] - at.originX) * at.scale, dy = (o->v[k * 2 + 1] - at.originY) * at.scale;
|
||||
placed[k * 2] = at.x + dx * cs - dy * sn;
|
||||
placed[k * 2 + 1] = at.y + dx * sn + dy * cs;
|
||||
minX = fminf(minX, placed[k * 2]); maxX = fmaxf(maxX, placed[k * 2]);
|
||||
minY = fminf(minY, placed[k * 2 + 1]); maxY = fmaxf(maxY, placed[k * 2 + 1]);
|
||||
}
|
||||
const float *end = &placed[(points - 1) * 2];
|
||||
v[i].type = o->op == 'M' ? STBTT_vmove : o->op == 'L' ? STBTT_vline : STBTT_vcubic;
|
||||
v[i].x = c4fVertexUnit(end[0]);
|
||||
v[i].y = c4fVertexUnit(end[1]);
|
||||
if (o->op == 'C') {
|
||||
v[i].cx = c4fVertexUnit(placed[0]); v[i].cy = c4fVertexUnit(placed[1]);
|
||||
v[i].cx1 = c4fVertexUnit(placed[2]); v[i].cy1 = c4fVertexUnit(placed[3]);
|
||||
}
|
||||
}
|
||||
int bx0 = c4fMaxI((int)floorf(minX), 0), by0 = c4fMaxI((int)floorf(minY), 0);
|
||||
int bx1 = c4fMinI((int)ceilf(maxX) + 1, c->w), by1 = c4fMinI((int)ceilf(maxY) + 1, c->h);
|
||||
if (bx0 >= bx1 || by0 >= by1) { free(v); return 0; }
|
||||
stbtt__bitmap bitmap;
|
||||
bitmap.w = bx1 - bx0;
|
||||
bitmap.h = by1 - by0;
|
||||
bitmap.stride = bitmap.w;
|
||||
bitmap.pixels = calloc((size_t)bitmap.w * (size_t)bitmap.h, 1);
|
||||
if (!bitmap.pixels) { free(v); return 0; }
|
||||
stbtt_Rasterize(&bitmap, 0.25f, v, p->count, 1 / C4F_SUBPIXEL, 1 / C4F_SUBPIXEL, 0, 0, bx0, by0, 0, NULL);
|
||||
free(v);
|
||||
*mask = bitmap.pixels; *x0 = bx0; *y0 = by0; *w = bitmap.w; *h = bitmap.h;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void c4fFillPath(C4fCanvas *c, const C4fPath *p, C4fPlacement at, C4fColor color)
|
||||
{
|
||||
unsigned char *mask;
|
||||
int x0, y0, w, h;
|
||||
if (!c4fPathMask(c, p, at, &mask, &x0, &y0, &w, &h)) return;
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
if (mask[y * w + x]) c4fBlend(c, x0 + x, y0 + y, color, mask[y * w + x] / 255.0f);
|
||||
free(mask);
|
||||
}
|
||||
|
||||
/* One box-blur pass over an 8-bit buffer, along rows or columns, edges zero. */
|
||||
static void c4fBlurMask(const unsigned char *src, unsigned char *dst, int w, int h, int radius, int horizontal)
|
||||
{
|
||||
const int n = horizontal ? w : h, lines = horizontal ? h : w, step = horizontal ? 1 : w, lineStep = horizontal ? w : 1;
|
||||
const int divisor = 2 * radius + 1;
|
||||
for (int line = 0; line < lines; line++) {
|
||||
const unsigned char *s = src + (size_t)line * lineStep;
|
||||
unsigned char *d = dst + (size_t)line * lineStep;
|
||||
int sum = 0;
|
||||
for (int i = 0; i <= radius && i < n; i++) sum += s[i * step];
|
||||
for (int i = 0; i < n; i++) {
|
||||
d[i * step] = (unsigned char)(sum / divisor);
|
||||
if (i + radius + 1 < n) sum += s[(i + radius + 1) * step];
|
||||
if (i - radius >= 0) sum -= s[(i - radius) * step];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void c4fShadowPath(C4fCanvas *c, const C4fPath *p, C4fPlacement at, float dx, float dy, int radius, C4fColor color)
|
||||
{
|
||||
unsigned char *mask;
|
||||
int x0, y0, w, h;
|
||||
/* Rasterize on a canvas-sized frame so the blur has room at the edges. */
|
||||
if (radius < 1 || !c4fPathMask(c, p, at, &mask, &x0, &y0, &w, &h)) return;
|
||||
const int margin = 3 * radius, bw = w + 2 * margin, bh = h + 2 * margin;
|
||||
unsigned char *a = calloc((size_t)bw * bh, 1), *b = calloc((size_t)bw * bh, 1);
|
||||
if (a && b) {
|
||||
for (int y = 0; y < h; y++) memcpy(a + (size_t)(y + margin) * bw + margin, mask + (size_t)y * w, (size_t)w);
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
c4fBlurMask(a, b, bw, bh, radius, 1);
|
||||
c4fBlurMask(b, a, bw, bh, radius, 0);
|
||||
}
|
||||
const int ox = x0 - margin + (int)lroundf(dx), oy = y0 - margin + (int)lroundf(dy);
|
||||
for (int y = 0; y < bh; y++)
|
||||
for (int x = 0; x < bw; x++)
|
||||
if (a[(size_t)y * bw + x]) c4fBlend(c, ox + x, oy + y, color, a[(size_t)y * bw + x] / 255.0f);
|
||||
}
|
||||
free(a);
|
||||
free(b);
|
||||
free(mask);
|
||||
}
|
||||
|
||||
/* ---- text ---- */
|
||||
|
||||
static C4fFace *c4fFace(int font, float size)
|
||||
|
||||
@@ -42,6 +42,26 @@ void c4fSegment(C4fCanvas *c, float x0, float y0, float x1, float y1, float widt
|
||||
void c4fSquareOutline(C4fCanvas *c, float cx, float cy, float half, float width, C4fColor color);
|
||||
void c4fTriangleOutline(C4fCanvas *c, float cx, float cy, float radius, float width, C4fColor color);
|
||||
|
||||
/* Vector paths (moves, lines, cubic curves), filled with non-zero winding:
|
||||
* draw holes the opposite way round. Coordinates are in a design space that
|
||||
* a transform places on the canvas. */
|
||||
typedef struct { char op; float v[6]; } C4fPathOp; /* 'M', 'L', 'C' (c1 c2 end) */
|
||||
typedef struct { C4fPathOp *ops; int count, capacity; } C4fPath;
|
||||
typedef struct { float originX, originY, scale, angle, x, y; } C4fPlacement; /* design origin -> canvas x,y */
|
||||
|
||||
void c4fPathMove(C4fPath *p, float x, float y);
|
||||
void c4fPathLine(C4fPath *p, float x, float y);
|
||||
void c4fPathCubic(C4fPath *p, float x1, float y1, float x2, float y2, float x, float y);
|
||||
void c4fPathCircle(C4fPath *p, float cx, float cy, float r, int clockwise);
|
||||
void c4fPathRoundRect(C4fPath *p, float x, float y, float w, float h, float r, int clockwise);
|
||||
void c4fPathFree(C4fPath *p);
|
||||
/* Coverage of the path at a placement, as an 8-bit mask the size of its
|
||||
* bounding box (caller frees *mask). 0 when nothing lands on the canvas. */
|
||||
int c4fPathMask(const C4fCanvas *c, const C4fPath *p, C4fPlacement at, unsigned char **mask, int *x0, int *y0, int *w, int *h);
|
||||
void c4fFillPath(C4fCanvas *c, const C4fPath *p, C4fPlacement at, C4fColor color);
|
||||
/* The mask blurred and spread: a soft halo or drop shadow under a shape. */
|
||||
void c4fShadowPath(C4fCanvas *c, const C4fPath *p, C4fPlacement at, float dx, float dy, int radius, C4fColor color);
|
||||
|
||||
/* Text on a baseline. Sizes are CSS font-size in pixels. ASCII only. */
|
||||
float c4fText(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text);
|
||||
float c4fTextN(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text, int length);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/* The DualShock 4 mark. The outline runs counter-clockwise on screen and the
|
||||
* cut-outs clockwise, so non-zero winding removes them; a stick cap inside
|
||||
* its cut ring runs counter-clockwise again and stays. */
|
||||
#include "logo.h"
|
||||
|
||||
/* One half of the body, top centre to bottom centre down the left side:
|
||||
* cubic segments (c1x c1y c2x c2y x y). The right half mirrors it. */
|
||||
static const float c4fBodyLeft[][6] = {
|
||||
{ 410, 78, 330, 74, 262, 62 }, /* top edge out to the L1 shoulder */
|
||||
{ 232, 46, 188, 42, 146, 62 }, /* over the shoulder */
|
||||
{ 108, 80, 86, 120, 78, 170 }, /* round the upper corner */
|
||||
{ 70, 230, 58, 320, 60, 400 }, /* down the outer side */
|
||||
{ 62, 470, 80, 535, 118, 568 }, /* along the grip */
|
||||
{ 152, 598, 196, 592, 222, 560 }, /* round the grip's end */
|
||||
{ 250, 525, 272, 470, 300, 430 }, /* up its inner edge */
|
||||
{ 360, 418, 430, 440, 500, 440 }, /* under the sticks to the centre */
|
||||
};
|
||||
#define C4F_BODY_START_X 500.0f
|
||||
#define C4F_BODY_START_Y 78.0f
|
||||
|
||||
void c4fLogoPath(C4fPath *p, int cutouts)
|
||||
{
|
||||
const int n = (int)(sizeof(c4fBodyLeft) / sizeof(c4fBodyLeft[0]));
|
||||
c4fPathMove(p, C4F_BODY_START_X, C4F_BODY_START_Y);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const float *s = c4fBodyLeft[i];
|
||||
c4fPathCubic(p, s[0], s[1], s[2], s[3], s[4], s[5]);
|
||||
}
|
||||
/* Back up the right side: each left segment mirrored and reversed. */
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
const float *s = c4fBodyLeft[i];
|
||||
float startX = i ? c4fBodyLeft[i - 1][4] : C4F_BODY_START_X, startY = i ? c4fBodyLeft[i - 1][5] : C4F_BODY_START_Y;
|
||||
c4fPathCubic(p, 1000 - s[2], s[3], 1000 - s[0], s[1], 1000 - startX, startY);
|
||||
}
|
||||
if (!cutouts) return;
|
||||
|
||||
/* Touchpad. */
|
||||
c4fPathRoundRect(p, 340, 98, 320, 154, 26, 1);
|
||||
/* D-pad. */
|
||||
{
|
||||
const float cx = 196, cy = 212, a = 23, l = 64;
|
||||
c4fPathMove(p, cx - a, cy - l);
|
||||
c4fPathLine(p, cx + a, cy - l);
|
||||
c4fPathLine(p, cx + a, cy - a);
|
||||
c4fPathLine(p, cx + l, cy - a);
|
||||
c4fPathLine(p, cx + l, cy + a);
|
||||
c4fPathLine(p, cx + a, cy + a);
|
||||
c4fPathLine(p, cx + a, cy + l);
|
||||
c4fPathLine(p, cx - a, cy + l);
|
||||
c4fPathLine(p, cx - a, cy + a);
|
||||
c4fPathLine(p, cx - l, cy + a);
|
||||
c4fPathLine(p, cx - l, cy - a);
|
||||
c4fPathLine(p, cx - a, cy - a);
|
||||
c4fPathLine(p, cx - a, cy - l);
|
||||
}
|
||||
/* Face buttons. */
|
||||
c4fPathCircle(p, 804, 148, 27, 1);
|
||||
c4fPathCircle(p, 804, 276, 27, 1);
|
||||
c4fPathCircle(p, 740, 212, 27, 1);
|
||||
c4fPathCircle(p, 868, 212, 27, 1);
|
||||
/* Sticks: a cut ring around a standing cap. */
|
||||
c4fPathCircle(p, 338, 342, 66, 1);
|
||||
c4fPathCircle(p, 338, 342, 44, 0);
|
||||
c4fPathCircle(p, 662, 342, 66, 1);
|
||||
c4fPathCircle(p, 662, 342, 44, 0);
|
||||
/* PS button. */
|
||||
c4fPathCircle(p, 500, 336, 17, 1);
|
||||
}
|
||||
|
||||
void c4fLogoLightPath(C4fPath *p)
|
||||
{
|
||||
/* Just above the touchpad, where the light bar shows from above. */
|
||||
c4fPathRoundRect(p, 350, 78, 300, 17, 8.5f, 0);
|
||||
}
|
||||
|
||||
C4fPlacement c4fLogoAt(float x, float y, float width, float angle)
|
||||
{
|
||||
C4fPlacement at = { C4F_LOGO_CENTER_X, C4F_LOGO_CENTER_Y, width / C4F_LOGO_WIDTH, angle, x, y };
|
||||
return at;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef C4F_LAUNCHER_LOGO_H
|
||||
#define C4F_LAUNCHER_LOGO_H
|
||||
/* The Control4Free mark: a DualShock 4 seen from above, in a 1000 x 640
|
||||
* design box (y down). The page's app mark, toast icon and favicon use the
|
||||
* same path: print it with `build/launcher/launcher-art svg` after a change. */
|
||||
#include "draw.h"
|
||||
|
||||
#define C4F_LOGO_CENTER_X 500.0f
|
||||
#define C4F_LOGO_CENTER_Y 320.0f
|
||||
#define C4F_LOGO_WIDTH 880.0f /* body width in design units */
|
||||
|
||||
/* cutouts: 0 for the bare outline, 1 with the buttons, touchpad and sticks. */
|
||||
void c4fLogoPath(C4fPath *p, int cutouts);
|
||||
/* The light bar along the top edge, for an LED colour. */
|
||||
void c4fLogoLightPath(C4fPath *p);
|
||||
|
||||
/* A placement that centres the controller at x,y, `width` pixels wide. */
|
||||
C4fPlacement c4fLogoAt(float x, float y, float width, float angle);
|
||||
|
||||
#endif
|
||||
+46
-28
@@ -8,6 +8,7 @@
|
||||
#include <string.h>
|
||||
#include "autorun.h"
|
||||
#include "draw.h"
|
||||
#include "logo.h"
|
||||
#include "qrcodegen.h"
|
||||
|
||||
/* Page CSS pixels to TV pixels. */
|
||||
@@ -267,32 +268,45 @@ static void c4fBlurUnder(C4fCanvas *c, float x, float y, float w, float h, float
|
||||
|
||||
/* ---- pieces of the page ---- */
|
||||
|
||||
static float c4fCapsuleDistance(float x, float y, float ax, float ay, float bx, float by, float r)
|
||||
/* The DualShock 4 mark, its cut-outs showing what is behind. */
|
||||
static void c4fMark(C4fCanvas *c, float x, float y, float width, C4fColor color)
|
||||
{
|
||||
float vx = bx - ax, vy = by - ay, fx = x - ax, fy = y - ay;
|
||||
float t = c4fClamp((fx * vx + fy * vy) / (vx * vx + vy * vy));
|
||||
return hypotf(fx - t * vx, fy - t * vy) - r;
|
||||
C4fPath path = { 0 };
|
||||
c4fLogoPath(&path, 1);
|
||||
c4fFillPath(c, &path, c4fLogoAt(x, y, width, 0), color);
|
||||
c4fPathFree(&path);
|
||||
}
|
||||
|
||||
/* The page's controller mark (.app-mark, viewBox 0 0 32 32) from simple shapes. */
|
||||
static float c4fMarkDistance(float x, float y)
|
||||
/* A DualShock 4 with its light bar lit: the bar glows over the top edge, and
|
||||
* the cut-outs read as dark recesses. */
|
||||
static void c4fLitController(C4fCanvas *c, float x, float y, float width, float angle,
|
||||
C4fColor body, C4fColor light, int shadow)
|
||||
{
|
||||
float body = c4fRoundBoxDistance(x, y, 16, 15, 12.9f, 5, 5);
|
||||
float grips = fminf(c4fCapsuleDistance(x, y, 6.4f, 16.5f, 7.6f, 21.2f, 3.3f),
|
||||
c4fCapsuleDistance(x, y, 25.6f, 16.5f, 24.4f, 21.2f, 3.3f));
|
||||
float dpad = fminf(c4fRoundBoxDistance(x, y, 11, 17, 1, 3, 0.3f), c4fRoundBoxDistance(x, y, 11, 17, 3, 1, 0.3f));
|
||||
float buttons = fminf(hypotf(x - 21.5f, y - 15.5f), hypotf(x - 19, y - 19)) - 1.5f;
|
||||
return fmaxf(fminf(body, grips), -fminf(dpad, buttons));
|
||||
}
|
||||
|
||||
static void c4fMark(C4fCanvas *c, float cx, float cy, float unit, C4fColor color)
|
||||
{
|
||||
for (int y = (int)floorf(cy - 8 * unit); y <= (int)ceilf(cy + 8 * unit); y++)
|
||||
for (int x = (int)floorf(cx - 14 * unit); x <= (int)ceilf(cx + 14 * unit); x++) {
|
||||
float d = c4fMarkDistance(16 + (x + 0.5f - cx) / unit, 17.3f + (y + 0.5f - cy) / unit) * unit;
|
||||
float cov = c4fClamp(0.5f - d);
|
||||
if (cov > 0) c4fBlend(c, x, y, color, cov);
|
||||
}
|
||||
C4fPath outline = { 0 }, detail = { 0 }, bar = { 0 };
|
||||
C4fPlacement at = c4fLogoAt(x, y, width, angle);
|
||||
const float k = width / C4F_LOGO_WIDTH;
|
||||
c4fLogoPath(&outline, 0);
|
||||
c4fLogoPath(&detail, 1);
|
||||
c4fLogoLightPath(&bar);
|
||||
if (shadow) c4fShadowPath(c, &outline, at, 0, 26 * k, (int)fmaxf(2, 22 * k), C4F_RGBA(0, 4, 18, .75f));
|
||||
/* The light spills up and out over the top edge, like the real bar: a
|
||||
* wide soft source behind the edge, and a tight one at the bar. */
|
||||
C4fPath spill = { 0 };
|
||||
c4fPathRoundRect(&spill, 330, 46, 340, 56, 28, 0);
|
||||
c4fShadowPath(c, &spill, at, 0, -6 * k, (int)fmaxf(3, 42 * k), light);
|
||||
c4fShadowPath(c, &bar, at, 0, -8 * k, (int)fmaxf(2, 22 * k), light);
|
||||
c4fPathFree(&spill);
|
||||
c4fFillPath(c, &outline, at, C4F_RGBA(6, 22, 62, 1));
|
||||
c4fFillPath(c, &detail, at, body);
|
||||
c4fFillPath(c, &bar, at, light);
|
||||
/* A bright core makes it read as a light, not paint. */
|
||||
C4fPath core = { 0 };
|
||||
c4fPathRoundRect(&core, 370, 83, 260, 7, 3.5f, 0);
|
||||
c4fFillPath(c, &core, at, C4F_RGBA((light.r + 255) / 2, (light.g + 255) / 2, (light.b + 255) / 2, .9f));
|
||||
c4fPathFree(&core);
|
||||
c4fPathFree(&outline);
|
||||
c4fPathFree(&detail);
|
||||
c4fPathFree(&bar);
|
||||
}
|
||||
|
||||
static void c4fTextCentered(C4fCanvas *c, int font, float size, float cx, float y, C4fColor color, const char *text)
|
||||
@@ -454,8 +468,8 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
else memset(pixels, 0, (size_t)c.w * (size_t)c.h * sizeof(uint32_t));
|
||||
|
||||
/* .topbar: the app mark, the name and the state. */
|
||||
c4fMark(&c, 64 + 13 * C4F_S, 54, 26 * C4F_S / 32, C4F_WHITE);
|
||||
float x = 64 + 26 * C4F_S + 10 * C4F_S;
|
||||
c4fMark(&c, 64 + 24, 54, 48, C4F_WHITE);
|
||||
float x = 64 + 48 + 16;
|
||||
x += c4fText(&c, C4F_FONT_LIGHT, 17 * C4F_S, x, 63, C4F_TEXT, "Control4Free") + 14 * C4F_S;
|
||||
c4fDot(&c, x + 4 * C4F_S, 55, look.dot, look.glow);
|
||||
c4fText(&c, C4F_FONT_LIGHT, 13 * C4F_S, x + 16 * C4F_S, 62, C4F_MUTED, look.pill);
|
||||
@@ -502,7 +516,7 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
} else {
|
||||
c4fFillRoundRect(&c, cardX, cardY, card, card, 6 * C4F_S, C4F_RGBA(0, 0, 0, .3f));
|
||||
c4fInsetRoundRect(&c, cardX, cardY, card, card, 6 * C4F_S, 1 * C4F_S, C4F_LINE_SOFT);
|
||||
c4fMark(&c, cardX + card / 2, cardY + card / 2, 5, C4F_FAINT);
|
||||
c4fMark(&c, cardX + card / 2, cardY + card / 2, 180, C4F_RGBA(226, 236, 255, .3f));
|
||||
c4fTextCentered(&c, C4F_FONT_LIGHT, 24, qrX + qrWidth / 2, cardY + card + 50, C4F_MUTED,
|
||||
"The code appears once it runs");
|
||||
}
|
||||
@@ -575,9 +589,13 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
void c4fDrawIcon(uint32_t *pixels, int size)
|
||||
{
|
||||
C4fCanvas c = { pixels, size, size };
|
||||
c4fPaintBackdrop(&c, size / 360.0f);
|
||||
c4fMark(&c, size / 2.0f, size * 0.40f, size * 0.60f / 26, C4F_WHITE);
|
||||
c4fTextCentered(&c, C4F_FONT_LIGHT, size * 0.105f, size / 2.0f, size * 0.80f, C4F_TEXT, "Control4Free");
|
||||
const float s = (float)size;
|
||||
c4fPaintBackdrop(&c, s / 360.0f);
|
||||
/* Three players, their light bars in the page's player colours. */
|
||||
c4fLitController(&c, s * 0.31f, s * 0.385f, s * 0.50f, -15, C4F_RGBA(196, 210, 240, 1), C4F_RGBA(255, 48, 64, 1), 0);
|
||||
c4fLitController(&c, s * 0.69f, s * 0.385f, s * 0.50f, 15, C4F_RGBA(196, 210, 240, 1), C4F_RGBA(48, 200, 96, 1), 0);
|
||||
c4fLitController(&c, s * 0.50f, s * 0.50f, s * 0.62f, 0, C4F_WHITE, C4F_RGBA(58, 140, 255, 1), 1);
|
||||
c4fTextCentered(&c, C4F_FONT_LIGHT, s * 0.1f, s / 2.0f, s * 0.89f, C4F_TEXT, "Control4Free");
|
||||
}
|
||||
|
||||
int c4fScreenInit(void)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "autorun.h"
|
||||
#include "logo.h"
|
||||
#include "screen.h"
|
||||
|
||||
static int c4fWrite(const char *path, const uint32_t *pixels, size_t count)
|
||||
@@ -20,8 +21,25 @@ static int c4fWrite(const char *path, const uint32_t *pixels, size_t count)
|
||||
return fclose(out) != 0;
|
||||
}
|
||||
|
||||
/* The mark as an SVG path in its design box (viewBox 0 0 1000 640), for the page. */
|
||||
static int c4fPrintSvg(void)
|
||||
{
|
||||
C4fPath path = { 0 };
|
||||
c4fLogoPath(&path, 1);
|
||||
for (int i = 0; i < path.count; i++) {
|
||||
const C4fPathOp *o = &path.ops[i];
|
||||
if (o->op == 'C') printf("C%g %g %g %g %g %g", o->v[0], o->v[1], o->v[2], o->v[3], o->v[4], o->v[5]);
|
||||
else printf("%c%g %g", o->op, o->v[0], o->v[1]);
|
||||
printf(i + 1 < path.count && path.ops[i + 1].op == 'M' ? "Z" : "");
|
||||
}
|
||||
printf("Z\n");
|
||||
c4fPathFree(&path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc == 2 && !strcmp(argv[1], "svg")) return c4fPrintSvg();
|
||||
if (argc != 4 || c4fScreenInit()) return 2;
|
||||
if (!strcmp(argv[1], "icon")) {
|
||||
int size = atoi(argv[2]);
|
||||
|
||||
@@ -35,7 +35,7 @@ def build_art_tool():
|
||||
stb = OUT / 'stb_truetype-host.o'
|
||||
subprocess.run([*flags, '-w', '-c', str(ROOT / 'launcher/stb_truetype.c'), '-o', str(stb)], check=True)
|
||||
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', str(ROOT / 'tools/launcher_art.c'),
|
||||
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/draw.c'),
|
||||
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/draw.c'), str(ROOT / 'launcher/logo.c'),
|
||||
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', '-o', str(ART)],
|
||||
check=True)
|
||||
|
||||
|
||||
Reference in new issue
Block a user