From adf74b2cbaa3de914df07e6e7cb56dac5c87d1ab Mon Sep 17 00:00:00 2001 From: "Yifeng \"Evan\" Wang" <7312949+doodlewind@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:53:35 +0800 Subject: [PATCH] feat: polish PSP UI demos and rendering --- DESIGN.md | 18 +- README.md | 6 +- assets/images/spinner-00.svg | 10 + assets/images/spinner-01.svg | 10 + assets/images/spinner-02.svg | 10 + assets/images/spinner-03.svg | 10 + assets/images/spinner-04.svg | 10 + assets/images/spinner-05.svg | 10 + assets/images/spinner-06.svg | 10 + assets/images/spinner-07.svg | 10 + compiler/bake-font.ts | 72 ++-- compiler/bake-svg.ts | 134 +++++++ core/src/draw.rs | 370 +++++++++++++++++- core/src/spec.rs | 2 +- core/src/tests.rs | 65 ++- core/src/text.rs | 8 +- demos/cards.tsx | 46 +-- demos/hero-main.tsx | 3 +- demos/hero.tsx | 53 ++- demos/library.tsx | 72 ++-- demos/music.tsx | 32 +- demos/notifications.tsx | 26 +- demos/settings.tsx | 226 +++++++---- demos/stats.tsx | 134 ++++--- native/src/ge.rs | 62 ++- package.json | 2 +- scripts/build.ts | 16 +- scripts/psp.ts | 20 +- spec/spec.ts | 21 +- src/jsx.d.ts | 2 + test/e2e-ppsspp.ts | 21 +- test/golden.ts | 24 +- test/goldens-psp/cards.detail.png | Bin 5811 -> 19872 bytes test/goldens-psp/cards.focused.png | Bin 4973 -> 15870 bytes test/goldens-psp/cards.layout.png | Bin 4721 -> 14325 bytes test/goldens-psp/cards.opening.png | Bin 0 -> 19836 bytes test/goldens-psp/hero.boot.png | Bin 6003 -> 18011 bytes test/goldens-psp/hero.focused.png | Bin 5975 -> 17911 bytes test/goldens-psp/hero.pressed.png | Bin 5956 -> 17800 bytes test/goldens-psp/library.detail.png | Bin 5446 -> 17172 bytes test/goldens-psp/library.focused.png | Bin 5044 -> 16086 bytes test/goldens-psp/library.grid.png | Bin 5023 -> 15461 bytes test/goldens-psp/library.loading.png | Bin 0 -> 7746 bytes test/goldens-psp/music.paused.png | Bin 5842 -> 19871 bytes test/goldens-psp/music.playing.png | Bin 6203 -> 20109 bytes test/goldens-psp/music.skipped.png | Bin 6203 -> 20324 bytes test/goldens-psp/notifications.boot.png | Bin 3978 -> 10756 bytes test/goldens-psp/notifications.dismissing.png | Bin 5612 -> 19022 bytes test/goldens-psp/notifications.settled.png | Bin 5612 -> 19287 bytes test/goldens-psp/settings.boot.png | Bin 4750 -> 18836 bytes test/goldens-psp/settings.themed.png | Bin 4542 -> 19731 bytes test/goldens-psp/settings.toggled.png | Bin 4548 -> 19280 bytes test/goldens-psp/stats.midcount.png | Bin 6223 -> 19225 bytes test/goldens-psp/stats.overview.png | Bin 6401 -> 19967 bytes test/goldens-psp/stats.systems.png | Bin 6439 -> 23235 bytes test/goldens/cards-main.12.png | Bin 7934 -> 14634 bytes test/goldens/cards-main.2.png | Bin 7665 -> 12685 bytes test/goldens/cards-main.24.png | Bin 0 -> 15631 bytes test/goldens/cards-main.78.png | Bin 8496 -> 15490 bytes test/goldens/hero-main.10.png | Bin 7349 -> 13956 bytes test/goldens/hero-main.2.png | Bin 7224 -> 13862 bytes test/goldens/hero-main.80.png | Bin 7947 -> 14917 bytes test/goldens/library-main.105.png | Bin 7109 -> 13002 bytes test/goldens/library-main.150.png | Bin 7662 -> 13973 bytes test/goldens/library-main.2.png | Bin 8187 -> 12818 bytes test/goldens/library-main.30.png | Bin 5978 -> 7523 bytes test/goldens/music-main.2.png | Bin 7650 -> 16233 bytes test/goldens/music-main.20.png | Bin 7516 -> 15627 bytes test/goldens/music-main.60.png | Bin 7825 -> 16248 bytes test/goldens/music-main.90.png | Bin 7875 -> 16149 bytes test/goldens/notifications-main.2.png | Bin 5568 -> 10105 bytes test/goldens/notifications-main.34.png | Bin 6811 -> 14120 bytes test/goldens/notifications-main.60.png | Bin 6816 -> 14292 bytes test/goldens/settings-main.2.png | Bin 6126 -> 16033 bytes test/goldens/settings-main.26.png | Bin 6201 -> 16052 bytes test/goldens/settings-main.90.png | Bin 6247 -> 16120 bytes test/goldens/stats-main.2.png | Bin 7258 -> 15378 bytes test/goldens/stats-main.20.png | Bin 8428 -> 16090 bytes test/goldens/stats-main.85.png | Bin 7692 -> 17096 bytes test/svg-bake.test.ts | 24 ++ wasm/src/raster.rs | 8 +- 81 files changed, 1180 insertions(+), 367 deletions(-) create mode 100644 assets/images/spinner-00.svg create mode 100644 assets/images/spinner-01.svg create mode 100644 assets/images/spinner-02.svg create mode 100644 assets/images/spinner-03.svg create mode 100644 assets/images/spinner-04.svg create mode 100644 assets/images/spinner-05.svg create mode 100644 assets/images/spinner-06.svg create mode 100644 assets/images/spinner-07.svg create mode 100644 compiler/bake-svg.ts create mode 100644 test/goldens-psp/cards.opening.png create mode 100644 test/goldens-psp/library.loading.png create mode 100644 test/goldens/cards-main.24.png create mode 100644 test/svg-bake.test.ts diff --git a/DESIGN.md b/DESIGN.md index 20fcd75f..bce655b4 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -62,10 +62,10 @@ artifacts: `$JOB_TMP/map-*.json`). - **Native animation**: tweens/springs tick in Rust per vblank with **fixed dt = 1/60 s** (frame content is a pure function of frame index — this is what makes byte-exact goldens possible **[R]**). JS only declares motion. -- **Baked text**: opentype.js atlas baker → 1-bit row-bitmask cells (MSB=left) - + proportional advances + cmap; native draw = run-length extraction into - batched GE sprites (WA2-proven at 60 fps). Font: **Inter** (OFL), vendored - in `assets/fonts/`. +- **Baked text**: opentype.js atlas baker → horizontally-supersampled 8-bit + coverage cells + proportional advances + cmap; native draw = alpha run-length + extraction into batched GE sprites. Font: **Inter** (OFL), vendored in + `assets/fonts/`. ## Repository layout @@ -262,10 +262,12 @@ Utilities (Tailwind default scales; `w-[123]` arbitrary px supported): `absolute|relative`, `inset/top/right/bottom/left-N`, `hidden`, `overflow-hidden` (scissor), `z-N` - **visual**: `bg-{palette}`, `bg-gradient-to-t|b|l|r` + `from-{c}`/`to-{c}` - (per-vertex gouraud), `rounded|-sm|-md|-lg|-xl` (pre-baked corner arcs, - PSM4444; **`rounded-full` only on nodes whose w/h are build-time known** - from `w-N h-N`, compiler bakes the exact radius, else compile error **[R]**), - `opacity-N`, `shadow|-md|-lg` (pre-baked blurred 9-slice), `border`+`border-{c}` + (per-vertex gouraud for square boxes; alpha-covered RECT spans for rounded + boxes), `rounded|-sm|-md|-lg|-xl` (axis-aligned boxes get deterministic + subpixel edge coverage; **`rounded-full` only on nodes whose w/h are + build-time known** from `w-N h-N`, compiler bakes the exact radius, else + compile error **[R]**), `opacity-N`, `shadow|-md|-lg` (layered rounded alpha + spans), `border`+`border-{c}` - **text**: `text-{palette}`, `text-xs|sm|base|lg|xl|2xl|4xl` → baked slots **12/14/16/18/20/24/36 px** (slots derived from the utility list, both weights **[R]**), `font-bold`, `text-left|center|right`, `leading-N`, diff --git a/README.md b/README.md index 5ae8e1c6..2cda2688 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ and everything is packed into `dist/.dcpak`. Pass 2 bundles with Bun ```tsx // demos/hero.tsx — the only intrinsics are , , - - Count: {count()} - + Count: {count()} + setCount(count() + 1)} /> ``` diff --git a/assets/images/spinner-00.svg b/assets/images/spinner-00.svg new file mode 100644 index 00000000..217c70f4 --- /dev/null +++ b/assets/images/spinner-00.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/spinner-01.svg b/assets/images/spinner-01.svg new file mode 100644 index 00000000..8a01408e --- /dev/null +++ b/assets/images/spinner-01.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/spinner-02.svg b/assets/images/spinner-02.svg new file mode 100644 index 00000000..4a73010f --- /dev/null +++ b/assets/images/spinner-02.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/spinner-03.svg b/assets/images/spinner-03.svg new file mode 100644 index 00000000..5bdd2de5 --- /dev/null +++ b/assets/images/spinner-03.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/spinner-04.svg b/assets/images/spinner-04.svg new file mode 100644 index 00000000..9b21b023 --- /dev/null +++ b/assets/images/spinner-04.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/spinner-05.svg b/assets/images/spinner-05.svg new file mode 100644 index 00000000..81f1e88e --- /dev/null +++ b/assets/images/spinner-05.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/spinner-06.svg b/assets/images/spinner-06.svg new file mode 100644 index 00000000..7028e772 --- /dev/null +++ b/assets/images/spinner-06.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/spinner-07.svg b/assets/images/spinner-07.svg new file mode 100644 index 00000000..483d31a6 --- /dev/null +++ b/assets/images/spinner-07.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/compiler/bake-font.ts b/compiler/bake-font.ts index 0316ab2a..01356a57 100644 --- a/compiler/bake-font.ts +++ b/compiler/bake-font.ts @@ -7,14 +7,14 @@ // out — the core resolves cmap misses to gid 0 (tofu) at runtime. // // Rasterization: opentype.js outlines, flattened to polylines, scanline -// even-odd fill with 3x3 supersampling, 1-bit threshold at coverage >= 0.5 -// (the WA2-proven approach). Cells are tight: cellW = max inked width over -// the slot's glyphs. Row bits MSB = LEFTMOST pixel. Proportional advances -// (font units * px/upm, rounded) live in the cmap entries. Glyphs with ink -// LEFT of the pen origin (negative left side bearing: î ï ĥ ǰ) are shifted -// right at bake and carry the shift in cmap byte +7 (xoff) so no ink is -// clipped; renderers place the cell at penX - xoff. gid 0 is a drawn hollow -// "tofu" box and is also mapped from U+FFFD so it has a discoverable advance. +// even-odd fill with horizontally-biased supersampling into 8-bit coverage +// cells. Cells are tight: cellW = max inked width over the slot's glyphs. +// Proportional advances (font units * px/upm, rounded) live in the cmap +// entries. Glyphs with ink LEFT of the pen origin (negative left side bearing: +// î ï ĥ ǰ) are shifted right at bake and carry the shift in cmap byte +7 +// (xoff) so no ink is clipped; renderers place the cell at penX - xoff. gid 0 +// is a drawn hollow "tofu" box and is also mapped from U+FFFD so it has a +// discoverable advance. import { parse as parseFont, type Font, type Path } from "opentype.js"; import { @@ -53,7 +53,7 @@ export interface BakeOptions { } // --------------------------------------------------------------------------- -// Outline -> 1-bit cell rasterizer +// Outline -> coverage cell rasterizer // --------------------------------------------------------------------------- type Pt = { x: number; y: number }; @@ -122,23 +122,25 @@ function flatten(path: Path): Contour[] { return contours; } -const SS = 3; // supersample factor (3x3 grid per pixel) +const SS_X = 9; // horizontal subpixel samples per pixel +const SS_Y = 3; // vertical samples per pixel /** - * Rasterize contours into a 1-bit cell bitmap (MSB = leftmost pixel). - * Coverage per pixel = covered subsamples / 9; ink at >= 0.5. + * Rasterize contours into a grayscale coverage cell. Horizontal sampling is + * intentionally denser than vertical sampling so thin glyph stems can land on + * subpixel boundaries without collapsing to a hard 1-bit edge. */ function rasterize(contours: Contour[], cellW: number, cellH: number): Uint8Array { - const bpr = (cellW + 7) >> 3; - const out = new Uint8Array(cellH * bpr); + const out = new Uint8Array(cellH * cellW); if (contours.length === 0) return out; - const sw = cellW * SS; - const counts = new Uint8Array(cellW); // covered subsamples per pixel column, one row at a time + const sw = cellW * SS_X; + const samplesPerPixel = SS_X * SS_Y; + const counts = new Uint16Array(cellW); // covered subsamples per pixel column, one row at a time const xs: number[] = []; for (let row = 0; row < cellH; row++) { counts.fill(0); - for (let sub = 0; sub < SS; sub++) { - const y = row + (sub + 0.5) / SS; + for (let sub = 0; sub < SS_Y; sub++) { + const y = row + (sub + 0.5) / SS_Y; xs.length = 0; for (const c of contours) { for (let i = 0; i < c.length - 1; i++) { @@ -153,27 +155,26 @@ function rasterize(contours: Contour[], cellW: number, cellH: number): Uint8Arra xs.sort((a, b) => a - b); for (let k = 0; k + 1 < xs.length; k += 2) { // subcolumn centers inside [xs[k], xs[k+1]) - let s0 = Math.ceil(xs[k] * SS - 0.5); - let s1 = Math.floor(xs[k + 1] * SS - 0.5); + let s0 = Math.ceil(xs[k] * SS_X - 0.5); + let s1 = Math.floor(xs[k + 1] * SS_X - 0.5); if (s0 < 0) s0 = 0; if (s1 >= sw) s1 = sw - 1; for (let s = s0; s <= s1; s++) { - const center = (s + 0.5) / SS; - if (center >= xs[k] && center < xs[k + 1]) counts[(s / SS) | 0]++; + const center = (s + 0.5) / SS_X; + if (center >= xs[k] && center < xs[k + 1]) counts[(s / SS_X) | 0]++; } } } for (let x = 0; x < cellW; x++) { - if (counts[x] >= 5) out[row * bpr + (x >> 3)] |= 0x80 >> (x & 7); // 5/9 >= 0.5 + out[row * cellW + x] = Math.round((counts[x] * 255) / samplesPerPixel); } } return out; } -/** Hollow tofu box bitmap for gid 0. Returns [bitmap rows, advance]. */ +/** Hollow tofu box coverage cell for gid 0. Returns [coverage rows, advance]. */ function tofu(cellW: number, cellH: number, baseline: number, px: number): [Uint8Array, number] { - const bpr = (cellW + 7) >> 3; - const out = new Uint8Array(cellH * bpr); + const out = new Uint8Array(cellH * cellW); const w = Math.max(4, Math.min(cellW, Math.round(px * 0.55))); const h = Math.max(5, Math.round(px * 0.7)); const y1 = Math.min(cellH - 1, baseline - 1); @@ -181,7 +182,7 @@ function tofu(cellW: number, cellH: number, baseline: number, px: number): [Uint const x0 = 0; const x1 = w - 1; const set = (x: number, y: number) => { - out[y * bpr + (x >> 3)] |= 0x80 >> (x & 7); + out[y * cellW + x] = 255; }; for (let x = x0; x <= x1; x++) { set(x, y0); @@ -258,13 +259,12 @@ export function bakeSlot(font: Font, slot: number, px: number, bold: boolean, ch let cellW = tofuW; for (const g of glyphs) cellW = Math.max(cellW, Math.ceil(g.maxX)); cellW = Math.min(255, Math.max(1, cellW)); - const bpr = (cellW + 7) >> 3; // gid 0 = tofu; gid k+1 = k-th glyph (chars arrive sorted ascending) const glyphCount = glyphs.length + 1; if (glyphCount > 0xffff) throw new Error("psp-ui bake-font: too many glyphs"); - const bitmapBytes = glyphCount * cellH * bpr; - const size = FONT_HEADER_SIZE + glyphCount * FONT_CMAP_ENTRY_SIZE + bitmapBytes; + const coverageBytes = glyphCount * cellH * cellW; + const size = FONT_HEADER_SIZE + glyphCount * FONT_CMAP_ENTRY_SIZE + coverageBytes; const out = new Uint8Array(size); const dv = new DataView(out.buffer); @@ -289,7 +289,7 @@ export function bakeSlot(font: Font, slot: number, px: number, bold: boolean, ch xoff: g.xoff, }), ); - const [tofuBits, tofuAdvance] = tofu(cellW, cellH, baseline, px); + const [tofuCoverage, tofuAdvance] = tofu(cellW, cellH, baseline, px); entries.push({ cp: TOFU_CODEPOINT, gid: 0, advance: tofuAdvance, xoff: 0 }); entries.sort((a, b) => a.cp - b.cp); let o = FONT_HEADER_SIZE; @@ -301,12 +301,12 @@ export function bakeSlot(font: Font, slot: number, px: number, bold: boolean, ch o += FONT_CMAP_ENTRY_SIZE; } - // bitmap region, indexed by gid - const bitmapOff = FONT_HEADER_SIZE + glyphCount * FONT_CMAP_ENTRY_SIZE; - const cellBytes = cellH * bpr; - out.set(tofuBits, bitmapOff); // gid 0 + // coverage region, indexed by gid + const coverageOff = FONT_HEADER_SIZE + glyphCount * FONT_CMAP_ENTRY_SIZE; + const cellBytes = cellH * cellW; + out.set(tofuCoverage, coverageOff); // gid 0 glyphs.forEach((g, i) => { - out.set(rasterize(g.contours, cellW, cellH), bitmapOff + (i + 1) * cellBytes); + out.set(rasterize(g.contours, cellW, cellH), coverageOff + (i + 1) * cellBytes); }); return { slot, px, bold, bytes: out, glyphCount, cellW, cellH }; diff --git a/compiler/bake-svg.ts b/compiler/bake-svg.ts new file mode 100644 index 00000000..91b085a0 --- /dev/null +++ b/compiler/bake-svg.ts @@ -0,0 +1,134 @@ +// compiler/bake-svg.ts — tiny offline SVG rasterizer for baked UI icons. +// +// This is deliberately scoped to the assets we want to ship in dcpak today: +// pow2-sized SVGs containing filled elements. Edges are supersampled +// before being written as straight-alpha RGBA, so icons keep subpixel coverage +// on both the wasm rasterizer and the PSP texture path. + +import type { DecodedImage } from "./dcpak.ts"; + +export const SVG_SUPERSAMPLE = 4; + +interface Circle { + cx: number; + cy: number; + r: number; + rgba: [number, number, number, number]; +} + +function attrs(tag: string): Record { + const out: Record = {}; + const re = /([A-Za-z_:][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g; + let m: RegExpExecArray | null; + while ((m = re.exec(tag))) out[m[1]] = m[2] ?? m[3] ?? ""; + return out; +} + +function num(v: string | undefined, name: string): number { + if (v === undefined) throw new Error(`svg bake: missing ${name}`); + const n = Number(v.replace(/px$/, "")); + if (!Number.isFinite(n)) throw new Error(`svg bake: invalid ${name}="${v}"`); + return n; +} + +function parseColor(value: string | undefined): [number, number, number, number] { + const s = value ?? "#000000"; + if (/^#[0-9a-fA-F]{3}$/.test(s)) { + return [ + parseInt(s[1] + s[1], 16), + parseInt(s[2] + s[2], 16), + parseInt(s[3] + s[3], 16), + 255, + ]; + } + if (/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(s)) { + return [ + parseInt(s.slice(1, 3), 16), + parseInt(s.slice(3, 5), 16), + parseInt(s.slice(5, 7), 16), + s.length === 9 ? parseInt(s.slice(7, 9), 16) : 255, + ]; + } + throw new Error(`svg bake: unsupported fill "${s}"`); +} + +function parseOpacity(v: string | undefined): number { + if (v === undefined) return 1; + const n = Number(v); + if (!Number.isFinite(n)) throw new Error(`svg bake: invalid opacity="${v}"`); + return Math.max(0, Math.min(1, n)); +} + +function sourceOver(px: Uint8Array, i: number, src: [number, number, number, number], srcA01: number): void { + if (srcA01 <= 0) return; + const dstA01 = px[i + 3] / 255; + const outA = srcA01 + dstA01 * (1 - srcA01); + if (outA <= 0) return; + px[i] = Math.round((src[0] * srcA01 + px[i] * dstA01 * (1 - srcA01)) / outA); + px[i + 1] = Math.round((src[1] * srcA01 + px[i + 1] * dstA01 * (1 - srcA01)) / outA); + px[i + 2] = Math.round((src[2] * srcA01 + px[i + 2] * dstA01 * (1 - srcA01)) / outA); + px[i + 3] = Math.round(outA * 255); +} + +export function bakeSvg(svg: string): DecodedImage { + const svgTag = svg.match(/]*>/i)?.[0]; + if (!svgTag) throw new Error("svg bake: missing "); + const root = attrs(svgTag); + const width = Math.round(num(root.width, "width")); + const height = Math.round(num(root.height, "height")); + if (width <= 0 || height <= 0) throw new Error(`svg bake: bad dimensions ${width}x${height}`); + const viewBox = (root.viewBox ?? `0 0 ${width} ${height}`).trim().split(/\s+/).map(Number); + if (viewBox.length !== 4 || viewBox.some((n) => !Number.isFinite(n))) { + throw new Error(`svg bake: invalid viewBox="${root.viewBox}"`); + } + const [vbX, vbY, vbW, vbH] = viewBox; + const sx = width / vbW; + const sy = height / vbH; + + const circles: Circle[] = []; + const circleRe = /]*\/?>/gi; + let m: RegExpExecArray | null; + while ((m = circleRe.exec(svg))) { + const a = attrs(m[0]); + const rgba = parseColor(a.fill); + const op = parseOpacity(a.opacity) * parseOpacity(a["fill-opacity"]); + rgba[3] = Math.round(rgba[3] * op); + circles.push({ + cx: (num(a.cx, "cx") - vbX) * sx, + cy: (num(a.cy, "cy") - vbY) * sy, + r: num(a.r, "r") * Math.min(sx, sy), + rgba, + }); + } + if (circles.length === 0) throw new Error("svg bake: no supported shapes"); + + const rgba = new Uint8Array(width * height * 4); + const ss = SVG_SUPERSAMPLE; + const samples = ss * ss; + for (const c of circles) { + const x0 = Math.max(0, Math.floor(c.cx - c.r - 1)); + const y0 = Math.max(0, Math.floor(c.cy - c.r - 1)); + const x1 = Math.min(width, Math.ceil(c.cx + c.r + 1)); + const y1 = Math.min(height, Math.ceil(c.cy + c.r + 1)); + const rr = c.r * c.r; + for (let y = y0; y < y1; y++) { + for (let x = x0; x < x1; x++) { + let covered = 0; + for (let yy = 0; yy < ss; yy++) { + const py = y + (yy + 0.5) / ss; + for (let xx = 0; xx < ss; xx++) { + const px = x + (xx + 0.5) / ss; + const dx = px - c.cx; + const dy = py - c.cy; + if (dx * dx + dy * dy <= rr) covered++; + } + } + if (covered > 0) { + sourceOver(rgba, (y * width + x) * 4, c.rgba, (c.rgba[3] / 255) * (covered / samples)); + } + } + } + } + + return { width, height, rgba }; +} diff --git a/core/src/draw.rs b/core/src/draw.rs index f9048816..6f5ccaa4 100644 --- a/core/src/draw.rs +++ b/core/src/draw.rs @@ -14,10 +14,9 @@ //! - glyph cells position along the rotated/scaled frame but stay upright //! and unscaled (bitmap cells); glyphs whose cell top-left leaves the //! screen range or whose cell leaves the clip rect are dropped; -//! - rounded corners and shadows need pre-baked atlas textures which no -//! host registers yet, so `radius`/`shadow` are skipped gracefully -//! (backgrounds draw square, shadows don't draw) rather than emitting -//! TexQuads against unregistered atlas handles; +//! - rounded corners and shadows are emitted for axis-aligned boxes as +//! deterministic alpha-covered RECT spans; rotated rounded boxes degrade +//! to square fills; //! - opacity multiplies vertex alpha down the subtree (wrong on overlap, //! per DESIGN.md punt list). @@ -187,6 +186,22 @@ fn alpha(color: u32) -> u32 { color >> 24 } +#[inline] +fn with_alpha_abgr(color: u32, a: u32) -> u32 { + (color & 0x00ff_ffff) | ((a & 0xff) << 24) +} + +#[inline] +fn scale_alpha_coverage(color: u32, coverage: u32) -> u32 { + let a = ((alpha(color) * coverage.min(255) + 127) / 255).min(255); + with_alpha_abgr(color, a) +} + +#[inline] +fn ceilf(x: f32) -> f32 { + -floorf(-x) +} + // ---- fills --------------------------------------------------------------------- #[derive(Clone, Copy)] @@ -221,6 +236,74 @@ fn lerp_color(a: u32, b: u32, f: f32) -> u32 { crate::anim::interp(a, b, f, true) } +#[inline] +fn sqrtf(x: f32) -> f32 { + if x <= 0.0 { + return 0.0; + } + let mut y = f32::from_bits((x.to_bits() >> 1) + 0x1fc0_0000); + y = 0.5 * (y + x / y); + y = 0.5 * (y + x / y); + y = 0.5 * (y + x / y); + y +} + +#[inline] +fn coverage_from_unit(x: f32) -> u32 { + let v = clampf(x, 0.0, 1.0); + (v * 255.0 + 0.5) as u32 +} + +#[inline] +fn coverage_mul(a: u32, b: u32) -> u32 { + ((a.min(255) * b.min(255) + 127) / 255).min(255) +} + +fn pixel_interval_coverage(pixel: i32, start: f32, end: f32) -> u32 { + let a = (pixel as f32).max(start); + let b = ((pixel + 1) as f32).min(end); + if b <= a { + 0 + } else { + coverage_from_unit(b - a) + } +} + +fn gradient_run_limit(fill: &Fill) -> i32 { + match *fill { + Fill::Grad { dir, .. } if dir == spec::GradDir::ToLeft as u32 || dir == spec::GradDir::ToRight as u32 => 4, + _ => 1_000_000, + } +} + +fn vertical_gradient(fill: &Fill) -> bool { + match *fill { + Fill::Grad { dir, .. } => dir == spec::GradDir::ToTop as u32 || dir == spec::GradDir::ToBottom as u32, + _ => false, + } +} + +fn fill_color_at(fill: &Fill, x0: f32, y0: f32, x1: f32, y1: f32, sx0: i32, sy: i32, sx1: i32, coverage: u32) -> u32 { + let color = match *fill { + Fill::Flat(color) => color, + Fill::Grad { from, to, dir } => { + let horizontal = dir == spec::GradDir::ToLeft as u32 || dir == spec::GradDir::ToRight as u32; + let (p, denom) = if horizontal { + (((sx0 + sx1) as f32 * 0.5) - x0, x1 - x0) + } else { + (sy as f32 + 0.5 - y0, y1 - y0) + }; + let f = if denom <= 0.0 { 0.0 } else { clampf(p / denom, 0.0, 1.0) }; + if dir == spec::GradDir::ToTop as u32 || dir == spec::GradDir::ToLeft as u32 { + lerp_color(to, from, f) + } else { + lerp_color(from, to, f) + } + } + }; + scale_alpha_coverage(color, coverage) +} + // ---- the walker ------------------------------------------------------------------ struct Walker<'a> { @@ -274,24 +357,55 @@ impl<'a> Walker<'a> { return; } - // -- background (rect / gradrect; `radius`/`shadow` skipped: no baked - // corner/shadow atlases are registered by any v1 host) ------------ + // -- background + shadow -------------------------------------------- let has_grad = r.grad_dir != NO_GRADIENT && r.grad_dir <= spec::GradDir::ToRight as u32; - if has_grad { + let bg_color = scale_alpha(r.bg_color, op); + let border_color = scale_alpha(r.border_color, op); + let rounded_ring = + r.radius > 0.0 && r.border_width > 0.0 && alpha(border_color) > 0 && (has_grad || alpha(bg_color) > 0); + + if r.shadow > 0 && (alpha(bg_color) > 0 || has_grad) { + self.emit_shadow(dl, &world, l.w, l.h, r.radius, r.shadow, op, &clip); + } + + if rounded_ring { + self.emit_rounded_box(dl, &world, 0.0, 0.0, l.w, l.h, r.radius, Fill::Flat(border_color), &clip); + let bw = r.border_width.min(l.w * 0.5).min(l.h * 0.5); + if has_grad { + let fill = Fill::Grad { + from: scale_alpha(r.grad_from, op), + to: scale_alpha(r.grad_to, op), + dir: r.grad_dir, + }; + self.emit_rounded_box(dl, &world, bw, bw, l.w - bw, l.h - bw, (r.radius - bw).max(0.0), fill, &clip); + } else { + self.emit_rounded_box( + dl, + &world, + bw, + bw, + l.w - bw, + l.h - bw, + (r.radius - bw).max(0.0), + Fill::Flat(bg_color), + &clip, + ); + } + } else if has_grad { let fill = Fill::Grad { from: scale_alpha(r.grad_from, op), to: scale_alpha(r.grad_to, op), dir: r.grad_dir, }; - self.emit_box(dl, &world, 0.0, 0.0, l.w, l.h, fill, &clip); - } else if alpha(scale_alpha(r.bg_color, op)) > 0 { - self.emit_box(dl, &world, 0.0, 0.0, l.w, l.h, Fill::Flat(scale_alpha(r.bg_color, op)), &clip); + self.emit_rounded_box(dl, &world, 0.0, 0.0, l.w, l.h, r.radius, fill, &clip); + } else if alpha(bg_color) > 0 { + self.emit_rounded_box(dl, &world, 0.0, 0.0, l.w, l.h, r.radius, Fill::Flat(bg_color), &clip); } // -- border: 4 inset strips ------------------------------------------ let bw = r.border_width; - if bw > 0.0 && alpha(scale_alpha(r.border_color, op)) > 0 { - let bc = Fill::Flat(scale_alpha(r.border_color, op)); + if !rounded_ring && bw > 0.0 && alpha(border_color) > 0 { + let bc = Fill::Flat(border_color); let bwx = bw.min(l.w * 0.5); let bwy = bw.min(l.h * 0.5); self.emit_box(dl, &world, 0.0, 0.0, l.w, bwy, bc, &clip); // top @@ -366,7 +480,6 @@ impl<'a> Walker<'a> { c.y1 = c.y1.max(y); } // Conservative integer AABB: floor mins, ceil maxes. - let ceilf = |x: f32| -floorf(-x); Clip { x0: floorf(clampf(c.x0, 0.0, SCREEN_W)), y0: floorf(clampf(c.y0, 0.0, SCREEN_H)), @@ -452,6 +565,237 @@ impl<'a> Walker<'a> { } } + #[allow(clippy::too_many_arguments)] + fn emit_rounded_span( + &self, + dl: &mut DrawList, + fill: &Fill, + sx0: f32, + sy0: f32, + sx1: f32, + sy1: f32, + py: i32, + h: i32, + x0: i32, + x1: i32, + coverage: u32, + ) { + if coverage == 0 || h <= 0 || x1 <= x0 { + return; + } + let max_run = gradient_run_limit(fill); + let mut x = x0; + while x < x1 { + let next = (x + max_run).min(x1); + let color = fill_color_at(fill, sx0, sy0, sx1, sy1, x, py, next, coverage); + if alpha(color) > 0 { + dl.words.push(spec::draw_op::RECT); + dl.words.push(xy_word(x as f32, py as f32)); + dl.words.push(wh_word((next - x) as f32, h as f32)); + dl.words.push(color); + } + x = next; + } + } + + #[allow(clippy::too_many_arguments)] + fn emit_rounded_box( + &self, + dl: &mut DrawList, + world: &Affine, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + radius: f32, + fill: Fill, + clip: &Clip, + ) { + if radius <= 0.0 || !world.is_axis_aligned() { + self.emit_box(dl, world, x0, y0, x1, y1, fill, clip); + return; + } + let (sx0, sy0) = world.apply(x0, y0); + let (sx1, sy1) = world.apply(x1, y1); + if sx1 <= sx0 || sy1 <= sy0 { + return; + } + let w = sx1 - sx0; + let h = sy1 - sy0; + let scale = ((world.a + world.d) * 0.5).max(0.0); + let r = (radius * scale).min(w * 0.5).min(h * 0.5); + if r <= 0.5 { + self.emit_box(dl, world, x0, y0, x1, y1, fill, clip); + return; + } + let ix0 = floorf(sx0).max(floorf(clip.x0)).max(0.0) as i32; + let iy0 = floorf(sy0).max(floorf(clip.y0)).max(0.0) as i32; + let ix1 = ceilf(sx1).min(ceilf(clip.x1)).min(SCREEN_W) as i32; + let iy1 = ceilf(sy1).min(ceilf(clip.y1)).min(SCREEN_H) as i32; + if ix1 <= ix0 || iy1 <= iy0 { + return; + } + let rr = r * r; + let tall_middle = !vertical_gradient(&fill); + let mid_y0 = if tall_middle { + (ceilf(sy0 + r) as i32).max(iy0).min(iy1) + } else { + iy0 + }; + let mid_y1 = if tall_middle { + (floorf(sy1 - r) as i32).max(iy0).min(iy1) + } else { + iy0 + }; + if tall_middle && mid_y1 > mid_y0 { + let span_x0 = sx0.max(ix0 as f32); + let span_x1 = sx1.min(ix1 as f32); + if span_x1 > span_x0 { + let left_edge = floorf(span_x0) as i32; + let full_start = ceilf(span_x0) as i32; + let full_end = floorf(span_x1) as i32; + let right_edge = ceilf(span_x1) as i32; + let h = mid_y1 - mid_y0; + let mut emitted_left_edge = false; + if left_edge < full_start && left_edge >= ix0 && left_edge < ix1 { + let x_coverage = pixel_interval_coverage(left_edge, span_x0, span_x1); + self.emit_rounded_span( + dl, + &fill, + sx0, + sy0, + sx1, + sy1, + mid_y0, + h, + left_edge, + left_edge + 1, + x_coverage, + ); + emitted_left_edge = true; + } + let inner_x0 = full_start.max(ix0); + let inner_x1 = full_end.min(ix1); + self.emit_rounded_span(dl, &fill, sx0, sy0, sx1, sy1, mid_y0, h, inner_x0, inner_x1, 255); + if full_end < right_edge + && full_end >= ix0 + && full_end < ix1 + && !(emitted_left_edge && full_end == left_edge) + { + let x_coverage = pixel_interval_coverage(full_end, span_x0, span_x1); + self.emit_rounded_span( + dl, + &fill, + sx0, + sy0, + sx1, + sy1, + mid_y0, + h, + full_end, + full_end + 1, + x_coverage, + ); + } + } + } + for py in iy0..iy1 { + if tall_middle && py >= mid_y0 && py < mid_y1 { + continue; + } + let y_coverage = pixel_interval_coverage(py, sy0, sy1); + if y_coverage == 0 { + continue; + } + + let row_y = clampf(py as f32 + 0.5, sy0, sy1); + let inset = if row_y < sy0 + r { + let dy = sy0 + r - row_y; + r - sqrtf((rr - dy * dy).max(0.0)) + } else if row_y > sy1 - r { + let dy = row_y - (sy1 - r); + r - sqrtf((rr - dy * dy).max(0.0)) + } else { + 0.0 + }; + let span_x0 = (sx0 + inset).max(ix0 as f32); + let span_x1 = (sx1 - inset).min(ix1 as f32); + if span_x1 <= span_x0 { + continue; + } + + let left_edge = floorf(span_x0) as i32; + let full_start = ceilf(span_x0) as i32; + let full_end = floorf(span_x1) as i32; + let right_edge = ceilf(span_x1) as i32; + + let mut emitted_left_edge = false; + if left_edge < full_start && left_edge >= ix0 && left_edge < ix1 { + let x_coverage = pixel_interval_coverage(left_edge, span_x0, span_x1); + self.emit_rounded_span( + dl, + &fill, + sx0, + sy0, + sx1, + sy1, + py, + 1, + left_edge, + left_edge + 1, + coverage_mul(x_coverage, y_coverage), + ); + emitted_left_edge = true; + } + + let inner_x0 = full_start.max(ix0); + let inner_x1 = full_end.min(ix1); + self.emit_rounded_span(dl, &fill, sx0, sy0, sx1, sy1, py, 1, inner_x0, inner_x1, y_coverage); + + if full_end < right_edge && full_end >= ix0 && full_end < ix1 && !(emitted_left_edge && full_end == left_edge) { + let x_coverage = pixel_interval_coverage(full_end, span_x0, span_x1); + self.emit_rounded_span( + dl, + &fill, + sx0, + sy0, + sx1, + sy1, + py, + 1, + full_end, + full_end + 1, + coverage_mul(x_coverage, y_coverage), + ); + } + } + } + + fn emit_shadow(&self, dl: &mut DrawList, world: &Affine, w: f32, h: f32, radius: f32, level: u32, opacity: f32, clip: &Clip) { + if !world.is_axis_aligned() { + return; + } + let layers: &[(f32, f32, f32, u32)] = match level { + 1 => &[(0.0, 1.0, 0.0, 22), (0.0, 2.0, 1.0, 10)], + 2 => &[(0.0, 2.0, 1.0, 24), (0.0, 4.0, 2.0, 12), (0.0, 6.0, 3.0, 6)], + _ => &[(0.0, 2.0, 1.0, 28), (0.0, 5.0, 3.0, 14), (0.0, 9.0, 5.0, 7)], + }; + for &(dx, dy, spread, alpha) in layers { + let color = scale_alpha(with_alpha_abgr(0x0000_0000, alpha), opacity); + self.emit_rounded_box( + dl, + world, + -spread + dx, + -spread + dy, + w + spread + dx, + h + spread + dy, + radius + spread, + Fill::Flat(color), + clip, + ); + } + } + /// Emit an image TEX_QUAD (axis-aligned only; rotated images are /// conservatively culled — no textured-triangle op in the DrawList v1). fn emit_tex_quad(&self, dl: &mut DrawList, world: &Affine, w: f32, h: f32, tex: u32, op: f32, clip: &Clip) { diff --git a/core/src/spec.rs b/core/src/spec.rs index 649f9de0..3b378080 100644 --- a/core/src/spec.rs +++ b/core/src/spec.rs @@ -288,7 +288,7 @@ pub mod style_table { /// FONT ATLAS blob format constants — full layout in spec.ts. pub mod font_atlas { pub const MAGIC: u32 = 0x41464344; // 'DCFA' LE - pub const VERSION: u16 = 1; + pub const VERSION: u16 = 2; pub const HEADER_SIZE: usize = 16; pub const CMAP_ENTRY_SIZE: usize = 8; pub const FLAG_BOLD: u8 = 1; diff --git a/core/src/tests.rs b/core/src/tests.rs index 37628a42..bfe0eb22 100644 --- a/core/src/tests.rs +++ b/core/src/tests.rs @@ -64,7 +64,7 @@ fn encode_styles(styles: &[StyleSpec]) -> Vec { } /// Synthetic font atlas: `glyphs` are (codepoint, gid, advance), sorted by -/// codepoint. Bitmaps are all-zero (metrics are what the tests exercise). +/// codepoint. Coverage cells are all-zero (metrics are what the tests exercise). fn encode_atlas( slot: u8, cell_w: u8, @@ -92,7 +92,7 @@ fn encode_atlas( out.push(adv); out.push(0); } - let bytes_per_row = (cell_w as usize + 7) / 8; + let bytes_per_row = cell_w as usize; out.extend_from_slice(&alloc::vec![0u8; glyph_count as usize * cell_h as usize * bytes_per_row]); out } @@ -472,6 +472,65 @@ fn drawlist_clip_invariant_offscreen_rects() { assert!(found, "gradient rect must survive the clip"); } +#[test] +fn rounded_boxes_emit_subpixel_edge_coverage() { + let mut ui = Ui::new(); + let n = ui.create_node(0); + ui.set_prop(n, spec::prop::WIDTH, 36.0); + ui.set_prop(n, spec::prop::HEIGHT, 20.0); + ui.set_prop(n, spec::prop::POS_TYPE, spec::PosType::Absolute as u32 as f64); + ui.set_prop(n, spec::prop::INSET_T, 10.5); + ui.set_prop(n, spec::prop::INSET_L, 10.5); + ui.set_prop(n, spec::prop::RADIUS, 10.0); + ui.set_prop(n, spec::prop::BG_COLOR, abgr(37, 99, 235, 255) as f64); + ui.insert_before(spec::ROOT_ID, n, 0); + ui.tick(); + let words = ui.draw().words.clone(); + let counts = validate_drawlist(&words); + assert!(counts[spec::draw_op::RECT as usize] > 0); + let mut edge_alpha = false; + let mut i = 0usize; + while i < words.len() { + match words[i] { + spec::draw_op::RECT => { + let a = words[i + 3] >> 24; + edge_alpha |= a > 0 && a < 255; + i += 4; + } + spec::draw_op::GRAD_RECT => i += 6, + spec::draw_op::TRI => i += 7, + spec::draw_op::GLYPH_RUN => i += 3 + 2 * ((words[i + 1] >> 16) as usize), + spec::draw_op::TEX_QUAD => i += 9, + spec::draw_op::SCISSOR => i += 3, + _ => i += 1, + } + } + assert!(edge_alpha, "rounded pill edges should be alpha-covered, not hard 1-bit spans"); +} + +#[test] +fn rounded_gradients_emit_rect_coverage_spans() { + let mut ui = Ui::new(); + let n = ui.create_node(0); + ui.set_prop(n, spec::prop::WIDTH, 120.0); + ui.set_prop(n, spec::prop::HEIGHT, 12.0); + ui.set_prop(n, spec::prop::POS_TYPE, spec::PosType::Absolute as u32 as f64); + ui.set_prop(n, spec::prop::INSET_T, 20.0); + ui.set_prop(n, spec::prop::INSET_L, 20.0); + ui.set_prop(n, spec::prop::RADIUS, 6.0); + ui.set_prop(n, spec::prop::GRAD_FROM, abgr(251, 191, 36, 255) as f64); + ui.set_prop(n, spec::prop::GRAD_TO, abgr(217, 119, 6, 255) as f64); + ui.set_prop(n, spec::prop::GRAD_DIR, spec::GradDir::ToRight as u32 as f64); + ui.insert_before(spec::ROOT_ID, n, 0); + ui.tick(); + let counts = validate_drawlist(&ui.draw().words.clone()); + assert!(counts[spec::draw_op::RECT as usize] > 0); + assert_eq!( + counts[spec::draw_op::GRAD_RECT as usize], 0, + "rounded gradients must not rely on 1px-high GRAD_RECT triangle strips" + ); +} + #[test] fn overflow_hidden_emits_balanced_intersected_scissors() { let mut ui = Ui::new(); @@ -575,7 +634,7 @@ fn text_measurement_against_synthetic_atlas() { // Atlas accessor exposes glyph bitmaps for the backends. let atlas = ui.font_atlas(2).unwrap(); assert_eq!(atlas.lookup('B' as u32), Some((2, 5))); - assert_eq!(atlas.glyph_rows(1).len(), 8); // cellH * 1 byte per row + assert_eq!(atlas.glyph_rows(1).len(), 64); // cellH * cellW coverage bytes } #[test] diff --git a/core/src/text.rs b/core/src/text.rs index 65b0052d..2e3798b0 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -53,7 +53,7 @@ pub struct Atlas { pub flags: u8, pub glyph_count: u16, cmap: Vec, - /// 1-bit row bitmasks: glyphCount x cellH x bytesPerRow, MSB = leftmost. + /// Coverage cells: glyphCount x cellH x cellW alpha bytes, left-to-right. pub bitmap: Vec, } @@ -77,7 +77,7 @@ impl Atlas { } let cmap_off = fa::HEADER_SIZE; let bitmap_off = cmap_off + glyph_count as usize * fa::CMAP_ENTRY_SIZE; - let bytes_per_row = (cell_w as usize + 7) / 8; + let bytes_per_row = cell_w as usize; let bitmap_len = glyph_count as usize * cell_h as usize * bytes_per_row; if bytes.len() < bitmap_off + bitmap_len { return None; @@ -127,10 +127,10 @@ impl Atlas { #[inline] pub fn bytes_per_row(&self) -> usize { - (self.cell_w as usize + 7) / 8 + self.cell_w as usize } - /// The row-bitmask bytes of one glyph (top row first). + /// The row coverage bytes of one glyph (top row first). pub fn glyph_rows(&self, gid: u16) -> &[u8] { let per_glyph = self.cell_h as usize * self.bytes_per_row(); let start = gid as usize * per_glyph; diff --git a/demos/cards.tsx b/demos/cards.tsx index f8810de7..b806bc89 100644 --- a/demos/cards.tsx +++ b/demos/cards.tsx @@ -1,11 +1,11 @@ // demos/cards.tsx — "card carousel" showcase: three feature cards in a row, // LEFT/RIGHT d-pad moves focus (native focus: variants lift + brighten the // focused card — zero JS on focus change), CIRCLE flips a detail panel -// that fades in (mount transition) and springs up into place. Two slow +// that springs up into place without color fades. Two slow // gradient streaks drift behind everything (long native tweens started once // on mount — deterministic fixed-dt, no per-frame JS). // -// v1-aware design notes: no rounded corners/shadows (they don't render), +// Design notes: rounded/shadowed surfaces are emitted by the core itself, // focus emphasis = translate-y lift + bg/border color (never scale — glyphs // don't scale), all text single-line, every class a FULL literal. @@ -30,30 +30,30 @@ const CARDS: Card[] = [ title: "Layout", caption: "Flexbox via Taffy", detail: "Rows, columns, gaps and insets — solved natively in Rust.", - cls: "flex-col gap-1 p-3 w-[136] bg-slate-800 border-slate-700 translate-y-1 focus:bg-slate-700 focus:border-indigo-400 focus:translate-y-0 transition-all duration-150 ease-out", - strip: "h-1 w-full bg-gradient-to-r from-indigo-400 to-indigo-600", - bar: "w-1 h-7 bg-indigo-400", + cls: "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-blue-50 focus:border-blue-500 focus:translate-y-0 transition-all duration-150 ease-out", + strip: "h-1 w-full rounded-sm bg-gradient-to-r from-blue-500 to-blue-600", + bar: "w-1 h-7 bg-blue-500", }, { title: "Motion", caption: "Springs and tweens", detail: "Fixed-dt springs and tweens tick natively at 60 FPS.", - cls: "flex-col gap-1 p-3 w-[136] bg-slate-800 border-slate-700 translate-y-1 focus:bg-slate-700 focus:border-emerald-400 focus:translate-y-0 transition-all duration-150 ease-out", - strip: "h-1 w-full bg-gradient-to-r from-emerald-400 to-emerald-600", - bar: "w-1 h-7 bg-emerald-400", + cls: "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-emerald-50 focus:border-emerald-500 focus:translate-y-0 transition-all duration-150 ease-out", + strip: "h-1 w-full rounded-sm bg-gradient-to-r from-emerald-500 to-emerald-600", + bar: "w-1 h-7 bg-emerald-500", }, { title: "Input", caption: "D-pad and focus", detail: "Native focus variants respond before JS even wakes up.", - cls: "flex-col gap-1 p-3 w-[136] bg-slate-800 border-slate-700 translate-y-1 focus:bg-slate-700 focus:border-amber-400 focus:translate-y-0 transition-all duration-150 ease-out", - strip: "h-1 w-full bg-gradient-to-r from-amber-400 to-amber-600", - bar: "w-1 h-7 bg-amber-400", + cls: "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-amber-50 focus:border-amber-500 focus:translate-y-0 transition-all duration-150 ease-out", + strip: "h-1 w-full rounded-sm bg-gradient-to-r from-amber-500 to-amber-600", + bar: "w-1 h-7 bg-amber-500", }, ]; -/** Detail panel — remounts (keyed ) per card, so the mount fade - * (transition-opacity) and the translate-y spring replay on every open. */ +/** Detail panel — remounts (keyed ) per card, so the translate-y spring + * replays on every open; colors are static on the first visible frame. */ function Detail(props: { card: Card }) { let el: NodeMirror | undefined; onMount(() => { @@ -63,12 +63,12 @@ function Detail(props: { card: Card }) { - {props.card.title} - {props.card.detail} + {props.card.title} + {props.card.detail} ); @@ -88,22 +88,22 @@ export default function Cards() { }); return ( - + - PSP-UI SHOWCASE - Feature Cards + PSP-UI SHOWCASE + Feature Cards 3 MODULES @@ -116,8 +116,8 @@ export default function Cards() { onPress={() => setOpen(open() === i ? -1 : i)} > - {card.title} - {card.caption} + {card.title} + {card.caption} ))} diff --git a/demos/hero-main.tsx b/demos/hero-main.tsx index bc807611..c38ed938 100644 --- a/demos/hero-main.tsx +++ b/demos/hero-main.tsx @@ -1,5 +1,6 @@ // @title psp-ui: Hero import Hero from "./hero.tsx"; +import { heroFrame } from "./hero.tsx"; import { mount } from "../src/index.ts"; -mount(() => ); +mount(() => , { beforeFrame: heroFrame }); diff --git a/demos/hero.tsx b/demos/hero.tsx index 7148357e..dd33ca2d 100644 --- a/demos/hero.tsx +++ b/demos/hero.tsx @@ -2,10 +2,28 @@ // Uses all three intrinsics, class literals, a dynamic style object, focus + // onPress, and a signal in text — the exact surface phase v1 supports. -import { createSignal, onMount, Show } from "solid-js"; +import { createMemo, createSignal, onMount, Show } from "solid-js"; import { animate } from "../src/anim.ts"; import type { NodeMirror } from "../src/renderer.ts"; +const SPINNER_FRAME_STEP = 3; +const SPINNER_FRAMES = [ + "spinner-00.svg", + "spinner-01.svg", + "spinner-02.svg", + "spinner-03.svg", + "spinner-04.svg", + "spinner-05.svg", + "spinner-06.svg", + "spinner-07.svg", +]; + +const [spinnerFrame, setSpinnerFrame] = createSignal(0); + +export function heroFrame(): void { + setSpinnerFrame((spinnerFrame() + 1) % (SPINNER_FRAMES.length * SPINNER_FRAME_STEP)); +} + function Stat(props: { label: string; value: string; cls: string }) { return ( @@ -17,52 +35,59 @@ function Stat(props: { label: string; value: string; cls: string }) { export default function Hero() { const [count, setCount] = createSignal(0); + const spinnerSrc = createMemo(() => { + const i = Math.floor(spinnerFrame() / SPINNER_FRAME_STEP) % SPINNER_FRAMES.length; + return SPINNER_FRAMES[i]; + }); let underline: NodeMirror | undefined; onMount(() => { // Underline sweeps in once on mount — native tween, zero steady-state JS. if (underline) animate(underline, "width", 210, { dur: 700, easing: "out", delay: 150 }); }); return ( - + - + - psp-ui + psp-ui SOLID + RUST + SCEGU - - - + + + - ONE RUST CORE · ONE JSX APP - JSX at 60 FPS. + ONE RUST CORE · ONE JSX APP + + JSX at 60 FPS. + + - + Flexbox, springs and baked type — running on a 2005 handheld. setCount(count() + 1)} > Press Circle - Count: {count()} + Count: {count()} 3}> - Reactive on real hardware. + Reactive on real hardware. diff --git a/demos/library.tsx b/demos/library.tsx index d94e46c5..5bdd3ada 100644 --- a/demos/library.tsx +++ b/demos/library.tsx @@ -2,17 +2,17 @@ // own home menu made real). LEFT/RIGHT move focus (focus:scale-110 + lift — // the icon quad scales, its label stays crisp: draw.rs keeps glyph cells // unscaled even inside a scaled parent frame), CIRCLE opens the selected -// tile — a spinning loading tile (native `rotate` tween, unused by the other -// three demos) auto-advances into a detail screen, TRIANGLE returns to the +// tile — an SVG-baked spinner texture cycles through frames while loading +// auto-advances into a detail screen, TRIANGLE returns to the // grid with focus restored to the tile that opened it (focusNode(), also // unused elsewhere — the other demos rely purely on d-pad-driven focus). // -// v1-aware design notes: text single-line (DESIGN.md: no auto word-wrap — -// the blurb is pre-split into lines), every class a FULL literal (the -// per-tile accent border/gradient is baked per entry, never synthesized). +// Design notes: text single-line (DESIGN.md: no auto word-wrap — the blurb is +// pre-split into lines), every class a FULL literal (the per-tile accent +// border/gradient is baked per entry, never synthesized). -import { createSignal, onMount, Show } from "solid-js"; -import { animate, spring } from "../src/anim.ts"; +import { createMemo, createSignal, onMount, Show } from "solid-js"; +import { spring } from "../src/anim.ts"; import { BTN } from "../spec/spec.ts"; import { focusNode } from "../src/input.ts"; import type { NodeMirror } from "../src/renderer.ts"; @@ -42,7 +42,7 @@ const GAMES: Game[] = [ trophies: "18 / 40", blurb: ["Drift a synthwave coastline at 200 km/h.", "Three circuits — never lift off the gas."], tileCls: - "w-14 h-14 items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-gradient-to-b from-indigo-400 to-indigo-700 border-indigo-300", + "w-14 h-14 rounded-xl shadow-md items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-gradient-to-b from-blue-500 to-blue-700 border-blue-300 focus:border-slate-900", }, { title: "IRON VANGUARD", @@ -51,7 +51,7 @@ const GAMES: Game[] = [ trophies: "27 / 40", blurb: ["Pilot a scrapyard mech at the Vanguard fleet.", "Every boss fight rewrites the arena."], tileCls: - "w-14 h-14 items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-gradient-to-b from-rose-400 to-rose-700 border-rose-300", + "w-14 h-14 rounded-xl shadow-md items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-gradient-to-b from-rose-400 to-rose-700 border-rose-300 focus:border-slate-900", }, { title: "TIDE POOL", @@ -60,7 +60,7 @@ const GAMES: Game[] = [ trophies: "9 / 40", blurb: ["Rearrange the reef before the tide comes in.", "120 hand-made pools, zero timers."], tileCls: - "w-14 h-14 items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-gradient-to-b from-sky-400 to-sky-700 border-sky-300", + "w-14 h-14 rounded-xl shadow-md items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-gradient-to-b from-sky-400 to-sky-700 border-sky-300 focus:border-slate-900", }, { title: "GHOST WATCH", @@ -69,7 +69,7 @@ const GAMES: Game[] = [ trophies: "12 / 40", blurb: ["Something in the lighthouse keeps the log.", "Find out before the batteries do."], tileCls: - "w-14 h-14 items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-gradient-to-b from-fuchsia-400 to-fuchsia-700 border-fuchsia-300", + "w-14 h-14 rounded-xl shadow-md items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-gradient-to-b from-cyan-500 to-cyan-700 border-cyan-300 focus:border-slate-900", }, { title: "ABOUT", @@ -78,12 +78,23 @@ const GAMES: Game[] = [ trophies: "", blurb: ["Solid universal renderer over a no_std Rust core.", "One JSX app — PSP hardware, PPSSPP or a browser."], tileCls: - "w-14 h-14 items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-slate-800 border-slate-600", + "w-14 h-14 rounded-xl shadow-md items-center justify-center translate-y-2 focus:translate-y-0 focus:scale-110 transition-all duration-150 ease-out bg-white border-slate-300 focus:border-slate-900", about: true, }, ]; -const LOADING_FRAMES = 48; // ~0.8s at 60 Hz — spinner completes 2 turns in that window +const LOADING_FRAMES = 48; // ~0.8s at 60 Hz — spinner cycles while mounted +const SPINNER_FRAME_STEP = 3; +const SPINNER_FRAMES = [ + "spinner-00.svg", + "spinner-01.svg", + "spinner-02.svg", + "spinner-03.svg", + "spinner-04.svg", + "spinner-05.svg", + "spinner-06.svg", + "spinner-07.svg", +]; // --------------------------------------------------------------------------- // Frame driver (wired by library-main.tsx): edge-detects TRIANGLE (back) and @@ -143,24 +154,24 @@ function Grid() { - {game.title} + {game.title} ))} ); } -/** Spinning tile (native `rotate` tween) — the loading screen replays it on - * every open, same as cards.tsx's Detail replays its mount spring. */ +/** SVG-baked spinner — frame-cycled instead of rotating an image quad, because + * the v1 texture op is axis-aligned and should stay cheap on PSP. */ function Loading(props: { title: string }) { - let spin: NodeMirror | undefined; - onMount(() => { - if (spin) animate(spin, "rotate", 720, { dur: 800, easing: "linear" }); + const src = createMemo(() => { + const i = Math.floor(loadFrame() / SPINNER_FRAME_STEP) % SPINNER_FRAMES.length; + return SPINNER_FRAMES[i]; }); return ( - - LOADING {props.title}... + + LOADING {props.title}... ); } @@ -168,13 +179,14 @@ function Loading(props: { title: string }) { function DetailStat(props: { label: string; value: string }) { return ( - {props.value} + {props.value} {props.label} ); } -/** Springs up into place on open, same choreography as cards.tsx's Detail. */ +/** Springs up into place on open; colors are static from the first frame so the + * panel never fades through the default dark style. */ function Detail(props: { game: Game }) { let panel: NodeMirror | undefined; onMount(() => { @@ -184,12 +196,12 @@ function Detail(props: { game: Game }) { - {props.game.genre} - {props.game.title} + {props.game.genre} + {props.game.title} @@ -200,7 +212,7 @@ function Detail(props: { game: Game }) { {props.game.blurb.map((line) => ( - {line} + {line} ))} TRIANGLE back to library @@ -214,11 +226,11 @@ function Detail(props: { game: Game }) { export default function Library() { return ( - + - PSP-UI SHOWCASE - Game Library + PSP-UI SHOWCASE + Game Library 5 TITLES diff --git a/demos/music.tsx b/demos/music.tsx index 90b96cff..cedaa64f 100644 --- a/demos/music.tsx +++ b/demos/music.tsx @@ -8,8 +8,8 @@ // tracks (the one button pair none of the other three demos touch); CIRCLE // on the cover toggles play/pause, CIRCLE on a track row selects it. // -// v1-aware design notes: every class a FULL literal (per-track cover accent -// baked per entry); text single-line. +// Design notes: every class a FULL literal (per-track cover accent baked per +// entry); text single-line. import { createSignal } from "solid-js"; import { BTN } from "../spec/spec.ts"; @@ -27,19 +27,19 @@ const TRACKS: Track[] = [ title: "MIDNIGHT REPLAY", artist: "SYNC PULSE", coverCls: - "w-16 h-16 items-center justify-center bg-gradient-to-b from-indigo-400 to-indigo-700 border-indigo-300 focus:border-white transition-colors duration-150", + "w-16 h-16 rounded-xl shadow-md items-center justify-center bg-gradient-to-b from-blue-500 to-blue-700 border-blue-300 focus:border-slate-900 transition-colors duration-150", }, { title: "GLASS HORIZON", artist: "AMBER TIDE", coverCls: - "w-16 h-16 items-center justify-center bg-gradient-to-b from-amber-400 to-amber-700 border-amber-300 focus:border-white transition-colors duration-150", + "w-16 h-16 rounded-xl shadow-md items-center justify-center bg-gradient-to-b from-amber-400 to-amber-700 border-amber-300 focus:border-slate-900 transition-colors duration-150", }, { title: "STATIC BLOOM", artist: "NEON DRIFTERS", coverCls: - "w-16 h-16 items-center justify-center bg-gradient-to-b from-fuchsia-400 to-fuchsia-700 border-fuchsia-300 focus:border-white transition-colors duration-150", + "w-16 h-16 rounded-xl shadow-md items-center justify-center bg-gradient-to-b from-cyan-500 to-cyan-700 border-cyan-300 focus:border-slate-900 transition-colors duration-150", }, ]; @@ -103,11 +103,11 @@ export default function Music() { const pct = () => Math.round((position() / TRACK_FRAMES) * 100); return ( - + - PSP-UI SHOWCASE - Now Playing + PSP-UI SHOWCASE + Now Playing TRACK {trackIndex() + 1} / {TRACKS.length} @@ -118,12 +118,12 @@ export default function Music() { - {track().title} - {track().artist} + {track().title} + {track().artist} - + @@ -133,7 +133,7 @@ export default function Music() { {([0, 1, 2, 3] as const).map((i) => ( - + ))} @@ -143,13 +143,13 @@ export default function Music() { selectTrack(i)} > - {t.title} + {t.title} {t.artist} ))} diff --git a/demos/notifications.tsx b/demos/notifications.tsx index fe2a0b45..335dc354 100644 --- a/demos/notifications.tsx +++ b/demos/notifications.tsx @@ -9,8 +9,8 @@ // driven timer (beforeFrame, same shape as stats.tsx/library.tsx) removes it // from the list once the tween has had time to finish. // -// v1-aware design notes: p-1 rows / p-3 root — 4 cards is already a tight fit -// in 480x272 (DESIGN.md punts kinetic scroll, so the list can't overflow the +// Design notes: p-1 rows / p-3 root — 4 cards is already a tight fit in +// 480x272 (DESIGN.md punts kinetic scroll, so the list can't overflow the // screen); every class a FULL literal. import { createSignal, For, onMount, Show } from "solid-js"; @@ -33,28 +33,28 @@ const INITIAL: Notice[] = [ title: "UPDATE AVAILABLE", message: "Firmware 6.61 is ready to install.", time: "2m ago", - dotCls: "w-2 h-2 rounded-full bg-sky-400", + dotCls: "w-2 h-2 rounded-full bg-sky-500", }, { id: "friend", title: "FRIEND REQUEST", message: "RIDGE_FOX wants to join your session.", time: "14m ago", - dotCls: "w-2 h-2 rounded-full bg-emerald-400", + dotCls: "w-2 h-2 rounded-full bg-emerald-500", }, { id: "battery", title: "LOW BATTERY", message: "12% remaining — plug in soon.", time: "35m ago", - dotCls: "w-2 h-2 rounded-full bg-amber-400", + dotCls: "w-2 h-2 rounded-full bg-amber-500", }, { id: "trophy", title: "TROPHY UNLOCKED", message: '"First Contact" — Iron Vanguard.', time: "1h ago", - dotCls: "w-2 h-2 rounded-full bg-indigo-400", + dotCls: "w-2 h-2 rounded-full bg-blue-500", }, ]; @@ -95,11 +95,11 @@ function dismiss(id: string, el: NodeMirror | undefined): void { export default function Notifications() { return ( - + - PSP-UI SHOWCASE - Notifications + PSP-UI SHOWCASE + Notifications {items().length} UNREAD @@ -118,14 +118,14 @@ export default function Notifications() { dismiss(item.id, el)} > - {item.title} - {item.message} + {item.title} + {item.message} {item.time} @@ -135,7 +135,7 @@ export default function Notifications() { - + ALL CLEAR diff --git a/demos/settings.tsx b/demos/settings.tsx index 61fd46b8..5b6c9fa1 100644 --- a/demos/settings.tsx +++ b/demos/settings.tsx @@ -2,55 +2,161 @@ // toggles (spring-sliding pill knobs — first demo use of `rounded-full`, // build-time-fixed w/h per DESIGN.md so the compiler can bake the exact // corner radius), a brightness control CIRCLE cycles through 5 steps (a -// spring width tween, same mechanism as stats.tsx's bars but driven by -// presses instead of mount), and a row of theme swatches whose SELECTED -// state (a persistent signal) is a separate visual layer from FOCUSED (the -// transient native focus: variant) — pressing one live-recolors the header -// title, the simplest possible demonstration of cross-component reactivity. +// direct style width/translate update, and a row of theme swatches whose +// SELECTED state (a persistent signal) drives one small design-token table: +// page wash, header text, focus colors, switches, slider, and theme panel. // // No custom frame wiring: every interaction is UP/DOWN navigation + CIRCLE // press, entirely covered by the engine's default input pass (src/input.ts) // — unlike stats.tsx/library.tsx this entry needs no beforeFrame. -import { createEffect, createSignal } from "solid-js"; -import { spring } from "../src/anim.ts"; +import { createEffect, createSignal, Show } from "solid-js"; +import { animate } from "../src/anim.ts"; import type { NodeMirror } from "../src/renderer.ts"; type ThemeName = "indigo" | "emerald" | "amber" | "rose"; -function titleCls(t: ThemeName): string { - if (t === "emerald") return "text-2xl text-emerald-300 font-bold"; - if (t === "amber") return "text-2xl text-amber-300 font-bold"; - if (t === "rose") return "text-2xl text-rose-300 font-bold"; - return "text-2xl text-indigo-300 font-bold"; +interface ThemeOption { + name: ThemeName; + pageCls: string; + eyebrowCls: string; + titleCls: string; + optionsCls: string; + rowCls: string; + rowLabelCls: string; + switchOnCls: string; + switchOffCls: string; + knobCls: string; + sliderTrackCls: string; + sliderFillCls: string; + sliderThumbCls: string; + valueCls: string; + panelCls: string; + footerCls: string; + swatchCls: string; + selectedCls: string; +} + +const THEMES: ThemeOption[] = [ + { + name: "indigo", + pageCls: "flex-col w-full h-full p-3 gap-2 bg-gradient-to-b from-indigo-50 to-slate-100", + eyebrowCls: "text-xs text-indigo-600 tracking-wide", + titleCls: "text-2xl text-indigo-700 font-bold", + optionsCls: "text-xs text-indigo-700", + rowCls: "flex-row items-center justify-between px-2 py-1 bg-white border-indigo-200 rounded-lg shadow focus:bg-indigo-50 focus:border-indigo-500 transition-colors duration-150", + rowLabelCls: "text-sm text-indigo-950", + switchOnCls: "w-9 h-5 rounded-full bg-gradient-to-r from-indigo-500 to-indigo-600 border-indigo-500 shadow flex-row items-center", + switchOffCls: "w-9 h-5 rounded-full bg-gradient-to-r from-indigo-100 to-indigo-200 border-indigo-200 shadow flex-row items-center", + knobCls: "w-4 h-4 rounded-full bg-white border-indigo-200 shadow-md m-[2] translate-x-[0.5]", + sliderTrackCls: "relative w-[120] h-3 rounded-full bg-indigo-100 border-indigo-200 shadow overflow-hidden", + sliderFillCls: "absolute left-0 top-0 h-3 w-[72] rounded-full bg-gradient-to-r from-indigo-400 to-indigo-600", + sliderThumbCls: "absolute left-0 top-[2] w-2 h-2 rounded-full bg-white border-indigo-500 shadow-md translate-x-[64]", + valueCls: "text-xs text-indigo-700", + panelCls: "flex-col gap-2 px-2 py-2 bg-white border-indigo-200 rounded-xl shadow-md", + footerCls: "text-xs text-indigo-700", + swatchCls: "w-8 h-6 rounded-lg shadow bg-gradient-to-b from-indigo-500 to-indigo-700 border-indigo-300 focus:border-indigo-950 transition-colors duration-150 items-center justify-center", + selectedCls: "w-8 h-6 rounded-lg shadow-md bg-gradient-to-b from-indigo-500 to-indigo-700 border-indigo-950 transition-colors duration-150 items-center justify-center", + }, + { + name: "emerald", + pageCls: "flex-col w-full h-full p-3 gap-2 bg-gradient-to-b from-emerald-50 to-slate-100", + eyebrowCls: "text-xs text-emerald-600 tracking-wide", + titleCls: "text-2xl text-emerald-700 font-bold", + optionsCls: "text-xs text-emerald-700", + rowCls: "flex-row items-center justify-between px-2 py-1 bg-white border-emerald-200 rounded-lg shadow focus:bg-emerald-50 focus:border-emerald-500 transition-colors duration-150", + rowLabelCls: "text-sm text-emerald-950", + switchOnCls: "w-9 h-5 rounded-full bg-gradient-to-r from-emerald-500 to-emerald-600 border-emerald-500 shadow flex-row items-center", + switchOffCls: "w-9 h-5 rounded-full bg-gradient-to-r from-emerald-100 to-emerald-200 border-emerald-200 shadow flex-row items-center", + knobCls: "w-4 h-4 rounded-full bg-white border-emerald-200 shadow-md m-[2] translate-x-[0.5]", + sliderTrackCls: "relative w-[120] h-3 rounded-full bg-emerald-100 border-emerald-200 shadow overflow-hidden", + sliderFillCls: "absolute left-0 top-0 h-3 w-[72] rounded-full bg-gradient-to-r from-emerald-400 to-emerald-600", + sliderThumbCls: "absolute left-0 top-[2] w-2 h-2 rounded-full bg-white border-emerald-500 shadow-md translate-x-[64]", + valueCls: "text-xs text-emerald-700", + panelCls: "flex-col gap-2 px-2 py-2 bg-white border-emerald-200 rounded-xl shadow-md", + footerCls: "text-xs text-emerald-700", + swatchCls: "w-8 h-6 rounded-lg shadow bg-gradient-to-b from-emerald-400 to-emerald-600 border-emerald-300 focus:border-emerald-950 transition-colors duration-150 items-center justify-center", + selectedCls: "w-8 h-6 rounded-lg shadow-md bg-gradient-to-b from-emerald-400 to-emerald-600 border-emerald-950 transition-colors duration-150 items-center justify-center", + }, + { + name: "amber", + pageCls: "flex-col w-full h-full p-3 gap-2 bg-gradient-to-b from-amber-50 to-slate-100", + eyebrowCls: "text-xs text-amber-600 tracking-wide", + titleCls: "text-2xl text-amber-700 font-bold", + optionsCls: "text-xs text-amber-700", + rowCls: "flex-row items-center justify-between px-2 py-1 bg-white border-amber-200 rounded-lg shadow focus:bg-amber-50 focus:border-amber-500 transition-colors duration-150", + rowLabelCls: "text-sm text-amber-950", + switchOnCls: "w-9 h-5 rounded-full bg-gradient-to-r from-amber-400 to-amber-600 border-amber-500 shadow flex-row items-center", + switchOffCls: "w-9 h-5 rounded-full bg-gradient-to-r from-amber-100 to-amber-200 border-amber-200 shadow flex-row items-center", + knobCls: "w-4 h-4 rounded-full bg-white border-amber-200 shadow-md m-[2] translate-x-[0.5]", + sliderTrackCls: "relative w-[120] h-3 rounded-full bg-amber-100 border-amber-200 shadow overflow-hidden", + sliderFillCls: "absolute left-0 top-0 h-3 w-[72] rounded-full bg-gradient-to-r from-amber-400 to-amber-600", + sliderThumbCls: "absolute left-0 top-[2] w-2 h-2 rounded-full bg-white border-amber-500 shadow-md translate-x-[64]", + valueCls: "text-xs text-amber-700", + panelCls: "flex-col gap-2 px-2 py-2 bg-white border-amber-200 rounded-xl shadow-md", + footerCls: "text-xs text-amber-700", + swatchCls: "w-8 h-6 rounded-lg shadow bg-gradient-to-b from-amber-400 to-amber-600 border-amber-300 focus:border-amber-950 transition-colors duration-150 items-center justify-center", + selectedCls: "w-8 h-6 rounded-lg shadow-md bg-gradient-to-b from-amber-400 to-amber-600 border-amber-950 transition-colors duration-150 items-center justify-center", + }, + { + name: "rose", + pageCls: "flex-col w-full h-full p-3 gap-2 bg-gradient-to-b from-rose-50 to-slate-100", + eyebrowCls: "text-xs text-rose-600 tracking-wide", + titleCls: "text-2xl text-rose-700 font-bold", + optionsCls: "text-xs text-rose-700", + rowCls: "flex-row items-center justify-between px-2 py-1 bg-white border-rose-200 rounded-lg shadow focus:bg-rose-50 focus:border-rose-500 transition-colors duration-150", + rowLabelCls: "text-sm text-rose-950", + switchOnCls: "w-9 h-5 rounded-full bg-gradient-to-r from-rose-400 to-rose-600 border-rose-500 shadow flex-row items-center", + switchOffCls: "w-9 h-5 rounded-full bg-gradient-to-r from-rose-100 to-rose-200 border-rose-200 shadow flex-row items-center", + knobCls: "w-4 h-4 rounded-full bg-white border-rose-200 shadow-md m-[2] translate-x-[0.5]", + sliderTrackCls: "relative w-[120] h-3 rounded-full bg-rose-100 border-rose-200 shadow overflow-hidden", + sliderFillCls: "absolute left-0 top-0 h-3 w-[72] rounded-full bg-gradient-to-r from-rose-400 to-rose-600", + sliderThumbCls: "absolute left-0 top-[2] w-2 h-2 rounded-full bg-white border-rose-500 shadow-md translate-x-[64]", + valueCls: "text-xs text-rose-700", + panelCls: "flex-col gap-2 px-2 py-2 bg-white border-rose-200 rounded-xl shadow-md", + footerCls: "text-xs text-rose-700", + swatchCls: "w-8 h-6 rounded-lg shadow bg-gradient-to-b from-rose-400 to-rose-600 border-rose-300 focus:border-rose-950 transition-colors duration-150 items-center justify-center", + selectedCls: "w-8 h-6 rounded-lg shadow-md bg-gradient-to-b from-rose-400 to-rose-600 border-rose-950 transition-colors duration-150 items-center justify-center", + }, +]; + +function themeByName(name: ThemeName): ThemeOption { + return THEMES.find((t) => t.name === name) ?? THEMES[0]; } // --------------------------------------------------------------------------- // Toggle row // --------------------------------------------------------------------------- -function Toggle(props: { label: string; value: boolean; onToggle: () => void }) { +function Toggle(props: { label: string; value: boolean; theme: ThemeOption; onToggle: () => void }) { let knob: NodeMirror | undefined; + let initialized = false; createEffect(() => { - if (knob) spring(knob, "translateX", props.value ? 16 : 0); + if (!knob) return; + const x = props.value ? 15.5 : 0.5; + animate(knob, "translateX", x, { + dur: initialized ? 160 : 1, + easing: "out", + }); + initialized = true; }); return ( - {props.label} + {props.label} @@ -63,24 +169,22 @@ function Toggle(props: { label: string; value: boolean; onToggle: () => void }) const BRIGHTNESS_TRACK_W = 120; -function Brightness() { +function Brightness(props: { theme: ThemeOption }) { const [level, setLevel] = createSignal(3); - let fill: NodeMirror | undefined; - createEffect(() => { - if (fill) spring(fill, "width", (level() / 5) * BRIGHTNESS_TRACK_W); - }); + const fillW = () => (level() / 5) * BRIGHTNESS_TRACK_W; return ( setLevel(level() >= 5 ? 1 : level() + 1)} > - BRIGHTNESS + BRIGHTNESS - - + + + - {level()}/5 + {level()}/5 ); @@ -90,46 +194,21 @@ function Brightness() { // Theme swatches // --------------------------------------------------------------------------- -interface ThemeOption { - name: ThemeName; - swatchCls: string; - selectedCls: string; -} - -const THEMES: ThemeOption[] = [ - { - name: "indigo", - swatchCls: "w-6 h-6 bg-indigo-500 border-slate-800 focus:border-slate-300 transition-colors duration-150", - selectedCls: "w-6 h-6 bg-indigo-500 border-white transition-colors duration-150", - }, - { - name: "emerald", - swatchCls: "w-6 h-6 bg-emerald-500 border-slate-800 focus:border-slate-300 transition-colors duration-150", - selectedCls: "w-6 h-6 bg-emerald-500 border-white transition-colors duration-150", - }, - { - name: "amber", - swatchCls: "w-6 h-6 bg-amber-500 border-slate-800 focus:border-slate-300 transition-colors duration-150", - selectedCls: "w-6 h-6 bg-amber-500 border-white transition-colors duration-150", - }, - { - name: "rose", - swatchCls: "w-6 h-6 bg-rose-500 border-slate-800 focus:border-slate-300 transition-colors duration-150", - selectedCls: "w-6 h-6 bg-rose-500 border-white transition-colors duration-150", - }, -]; - -function ThemeRow(props: { value: ThemeName; onPick: (t: ThemeName) => void }) { +function ThemeRow(props: { value: ThemeName; theme: ThemeOption; onPick: (t: ThemeName) => void }) { return ( - - THEME - + + THEME + {THEMES.map((t) => ( props.onPick(t.name)} - /> + > + + + + ))} @@ -144,25 +223,26 @@ export default function Settings() { const [sfx, setSfx] = createSignal(true); const [vibration, setVibration] = createSignal(false); const [theme, setTheme] = createSignal("indigo"); + const currentTheme = () => themeByName(theme()); return ( - + - PSP-UI SHOWCASE - Settings + PSP-UI SHOWCASE + Settings - 4 OPTIONS + 4 OPTIONS - - setSfx(!sfx())} /> - setVibration(!vibration())} /> - - + + setSfx(!sfx())} /> + setVibration(!vibration())} /> + + - UP / DOWN move focus · CIRCLE toggle / cycle / select + UP / DOWN move focus · CIRCLE toggle / cycle / select ); } diff --git a/demos/stats.tsx b/demos/stats.tsx index a84beffc..c8fe4991 100644 --- a/demos/stats.tsx +++ b/demos/stats.tsx @@ -1,36 +1,46 @@ // demos/stats.tsx — "animated dashboard" showcase: three stat tiles whose // numbers count up over the first ~1.2 s, horizontal bars that grow to their -// value with a staggered ease-out (native width tweens declared once on -// mount), and UP/DOWN switching two tabs ( panels with mount fades). +// value from the capped frame signal, and LEFT/RIGHT switching two horizontally +// arranged tabs. The SYSTEMS tab uses a short staggered row reveal so rows never +// flash through the style-table default before becoming white. // // Frame driving: statsFrame(buttons) is called once per frame by the -// stats-main entry (it wraps globalThis.frame). It edge-detects UP/DOWN for -// the tab switch and steps a frame-counter signal that the count-up memo -// derives from — CAPPED at COUNT_FRAMES, so after ~1.2 s the signal stops -// changing and steady-state JS work is zero. Content stays a pure function -// of the frame index (byte-exact goldens). - -import { createMemo, createSignal, onMount, Show } from "solid-js"; -import { animate, spring } from "../src/anim.ts"; +// stats-main entry (it wraps globalThis.frame). It edge-detects LEFT/RIGHT for +// the tab switch and steps capped frame-counter signals; after the current +// choreography settles, steady-state JS work is zero. Content stays a pure +// function of the frame index (byte-exact goldens). + +import { createMemo, createSignal, Show } from "solid-js"; import { BTN } from "../spec/spec.ts"; -import type { NodeMirror } from "../src/renderer.ts"; // --------------------------------------------------------------------------- // Frame driver (wired by stats-main.tsx) // --------------------------------------------------------------------------- const COUNT_FRAMES = 75; +const BAR_ANIM_FRAMES = 26; +const BAR_STAGGER_FRAMES = 4; +const SYSTEMS_REVEAL_FRAMES = 12; +const SYSTEMS_STAGGER_FRAMES = 5; +const SYSTEMS_MAX_FRAMES = SYSTEMS_REVEAL_FRAMES + SYSTEMS_STAGGER_FRAMES * 3; const [frameN, setFrameN] = createSignal(0); const [tab, setTab] = createSignal(0); +const [systemsFrame, setSystemsFrame] = createSignal(0); let prevButtons = 0; /** Once per frame, BEFORE the engine's own handler (stats-main wraps frame). */ export function statsFrame(buttons: number): void { const pressed = buttons & ~prevButtons; prevButtons = buttons; - if (pressed & BTN.DOWN) setTab(1); - if (pressed & BTN.UP) setTab(0); + if (pressed & BTN.RIGHT) { + setTab(1); + setSystemsFrame(0); + } + if (pressed & BTN.LEFT) setTab(0); if (frameN() < COUNT_FRAMES) setFrameN(frameN() + 1); // settles, then silence + if (tab() === 1 && systemsFrame() < SYSTEMS_MAX_FRAMES) { + setSystemsFrame(systemsFrame() + 1); + } } // --------------------------------------------------------------------------- @@ -45,9 +55,9 @@ interface Stat { } const STATS: Stat[] = [ - { label: "PLAYERS ONLINE", target: 12480, delta: "+318", valueCls: "text-2xl text-indigo-300 font-bold" }, - { label: "SESSIONS TODAY", target: 3642, delta: "+9%", valueCls: "text-2xl text-emerald-300 font-bold" }, - { label: "DRAW CALLS", target: 268, delta: "-12", valueCls: "text-2xl text-amber-300 font-bold" }, + { label: "PLAYERS ONLINE", target: 12480, delta: "+318", valueCls: "text-2xl text-blue-600 font-bold" }, + { label: "SESSIONS TODAY", target: 3642, delta: "+9%", valueCls: "text-2xl text-emerald-600 font-bold" }, + { label: "DRAW CALLS", target: 268, delta: "-12", valueCls: "text-2xl text-amber-600 font-bold" }, ]; interface Bar { @@ -59,10 +69,10 @@ interface Bar { const BAR_W = 280; // track px — fill animates to pct/100 * BAR_W const BARS: Bar[] = [ - { label: "CPU", pct: 42, fill: "h-2 w-0 bg-gradient-to-r from-indigo-400 to-indigo-600 transition-colors duration-200 ease-out" }, - { label: "GPU", pct: 71, fill: "h-2 w-0 bg-gradient-to-r from-emerald-400 to-emerald-600 transition-colors duration-200 ease-out" }, - { label: "RAM", pct: 63, fill: "h-2 w-0 bg-gradient-to-r from-amber-400 to-amber-600 transition-colors duration-200 ease-out" }, - { label: "I/O", pct: 28, fill: "h-2 w-0 bg-gradient-to-r from-sky-400 to-sky-600 transition-colors duration-200 ease-out" }, + { label: "CPU", pct: 42, fill: "h-2 w-0 rounded-full bg-gradient-to-r from-blue-500 to-blue-600" }, + { label: "GPU", pct: 71, fill: "h-2 w-0 rounded-full bg-gradient-to-r from-emerald-500 to-emerald-600" }, + { label: "RAM", pct: 63, fill: "h-2 w-0 rounded-full bg-gradient-to-r from-amber-500 to-amber-600" }, + { label: "I/O", pct: 28, fill: "h-2 w-0 rounded-full bg-gradient-to-r from-sky-500 to-sky-600" }, ]; interface Sys { @@ -73,10 +83,10 @@ interface Sys { } const SYSTEMS: Sys[] = [ - { name: "GE PIPELINE", status: "ONLINE", led: "w-2 h-2 bg-emerald-400", statusCls: "text-xs text-emerald-400" }, - { name: "AUDIO MIXER", status: "ONLINE", led: "w-2 h-2 bg-emerald-400", statusCls: "text-xs text-emerald-400" }, - { name: "MEMORY ARENA", status: "87% USED", led: "w-2 h-2 bg-amber-400", statusCls: "text-xs text-amber-400" }, - { name: "WIFI LINK", status: "ONLINE", led: "w-2 h-2 bg-emerald-400", statusCls: "text-xs text-emerald-400" }, + { name: "GE PIPELINE", status: "ONLINE", led: "w-2 h-2 rounded-full bg-emerald-500", statusCls: "text-xs text-emerald-600" }, + { name: "AUDIO MIXER", status: "ONLINE", led: "w-2 h-2 rounded-full bg-emerald-500", statusCls: "text-xs text-emerald-600" }, + { name: "MEMORY ARENA", status: "87% USED", led: "w-2 h-2 rounded-full bg-amber-500", statusCls: "text-xs text-amber-600" }, + { name: "WIFI LINK", status: "ONLINE", led: "w-2 h-2 rounded-full bg-emerald-500", statusCls: "text-xs text-emerald-600" }, ]; function fmt(n: number): string { @@ -84,35 +94,31 @@ function fmt(n: number): string { return s.length > 3 ? s.slice(0, -3) + "," + s.slice(-3) : s; } +function easeOutCubic(x: number): number { + const t = Math.max(0, Math.min(1, x)); + return 1 - (1 - t) * (1 - t) * (1 - t); +} + // --------------------------------------------------------------------------- // Panels // --------------------------------------------------------------------------- -/** OVERVIEW tab: bars grow to their value with staggered native ease-out - * width tweens — remounting the tab replays the whole choreography. */ +/** OVERVIEW tab: bars grow from the capped frame signal, not a stack of native + * width tweens, so there is no transition from default dark style values. */ function Overview() { - const fills: (NodeMirror | undefined)[] = []; - onMount(() => { - BARS.forEach((bar, i) => { - const el = fills[i]; - if (el) { - animate(el, "width", (bar.pct / 100) * BAR_W, { - dur: 600, - easing: "out", - delay: i * 90, - }); - } - }); - }); + const fillW = (bar: Bar, i: number) => { + const local = (frameN() - i * BAR_STAGGER_FRAMES) / BAR_ANIM_FRAMES; + return easeOutCubic(local) * (bar.pct / 100) * BAR_W; + }; return ( {BARS.map((bar, i) => ( - {bar.label} + {bar.label} - - + + {bar.pct + "%"} @@ -121,20 +127,20 @@ function Overview() { ); } -/** SYSTEMS tab: status board (square LEDs — v1 renders no rounded corners). - * Slides up on mount (spring) while the row backgrounds fade in natively. */ +/** SYSTEMS tab: status board. Rows appear one after another with short delays; + * opacity starts at 0, so there is no visible flash from default gray. */ function Systems() { - let el: NodeMirror | undefined; - onMount(() => { - if (el) spring(el, "translateY", 0); - }); + const rowT = (i: number) => easeOutCubic((systemsFrame() - i * SYSTEMS_STAGGER_FRAMES) / SYSTEMS_REVEAL_FRAMES); return ( - - {SYSTEMS.map((sys) => ( - + + {SYSTEMS.map((sys, i) => ( + - {sys.name} + {sys.name} {sys.status} @@ -152,22 +158,22 @@ export default function Stats() { // silent once frameN stops at COUNT_FRAMES. const t = createMemo(() => { const x = Math.min(1, frameN() / COUNT_FRAMES); - return 1 - (1 - x) * (1 - x) * (1 - x); // ease-out cubic + return easeOutCubic(x); }); return ( - + - LIVE TELEMETRY - Mission Control + LIVE TELEMETRY + Mission Control {STATS.map((stat) => ( - - {stat.label} + + {stat.label} {fmt(Math.round(stat.target * t()))} - {stat.delta} + {stat.delta} ))} @@ -221,7 +227,7 @@ export default function Stats() { - UP / DOWN switch tab + LEFT / RIGHT switch tab ); } diff --git a/native/src/ge.rs b/native/src/ge.rs index 5cbd9ef5..d9d767c6 100644 --- a/native/src/ge.rs +++ b/native/src/ge.rs @@ -191,18 +191,35 @@ unsafe fn apply_texture(pixels: &[u8], w: u32, h: u32, psm: u32) { sys::sceGuTexFilter(TextureFilter::Linear, TextureFilter::Linear); } -/// Count the horizontal 1-bit runs in one glyph's rows (MSB = leftmost). -fn count_glyph_runs(rows: &[u8], bpr: usize, cell_w: usize, cell_h: usize) -> usize { +#[inline] +fn glyph_alpha(color: u32, coverage: u8) -> u8 { + if coverage == 0 { + return 0; + } + let base = (color >> 24) & 0xff; + let alpha = (base * coverage as u32 + 127) / 255; + // Quantize to 16 levels so antialiased text stays smooth enough while + // keeping PSP sprite batches bounded. + (((alpha + 8) / 17) * 17).min(255) as u8 +} + +#[inline] +fn with_alpha(color: u32, alpha: u8) -> u32 { + (color & 0x00ff_ffff) | ((alpha as u32) << 24) +} + +/// Count horizontal coverage runs in one glyph's rows. +fn count_glyph_runs(rows: &[u8], bpr: usize, cell_w: usize, cell_h: usize, color: u32) -> usize { let mut runs = 0usize; for y in 0..cell_h { let row = &rows[y * bpr..y * bpr + bpr]; - let mut in_run = false; + let mut cur = 0u8; for x in 0..cell_w { - let on = row[x >> 3] & (0x80 >> (x & 7)) != 0; - if on && !in_run { + let a = glyph_alpha(color, row[x]); + if a != 0 && a != cur { runs += 1; } - in_run = on; + cur = a; } } runs @@ -214,7 +231,7 @@ pub unsafe fn render(ui: &Ui, words: &[u32]) { sys::sceGuClearColor(0xff00_0000); sys::sceGuClear(ClearBuffer::COLOR_BUFFER_BIT); - // Pass state: alpha blending on for everything (opacity, text AA-less + // Pass state: alpha blending on for everything (opacity, coverage text // cells with alpha colors, texture alpha). sys::sceGuEnable(GuState::Blend); sys::sceGuBlendFunc(BlendOp::Add, BlendFactor::SrcAlpha, BlendFactor::OneMinusSrcAlpha, 0, 0); @@ -310,8 +327,8 @@ pub unsafe fn render(ui: &Ui, words: &[u32]) { break; // truncated list — bail } if let Some(atlas) = ui.font_atlas(slot) { - // WA2 pattern: bit-scan each glyph cell into horizontal - // solid runs, batched as sprite pairs in ONE draw. + // Scan each glyph coverage cell into horizontal alpha + // runs, batched as sprite pairs in ONE draw. let bpr = atlas.bytes_per_row(); let (cw, ch) = (atlas.cell_w as usize, atlas.cell_h as usize); // Pass 1: exact vertex count for the bump alloc. @@ -319,7 +336,7 @@ pub unsafe fn render(ui: &Ui, words: &[u32]) { for g in 0..count { let gid = (words[body + g * 2 + 1] & 0xffff) as u16; if gid < atlas.glyph_count { - rects += count_glyph_runs(atlas.glyph_rows(gid), bpr, cw, ch); + rects += count_glyph_runs(atlas.glyph_rows(gid), bpr, cw, ch, color); } } if rects > 0 { @@ -338,20 +355,27 @@ pub unsafe fn render(ui: &Ui, words: &[u32]) { let row = &rows[y * bpr..y * bpr + bpr]; let mut x = 0usize; while x < cw { - if row[x >> 3] & (0x80 >> (x & 7)) == 0 { + let a = glyph_alpha(color, row[x]); + if a == 0 { x += 1; continue; } let start = x; - while x < cw && row[x >> 3] & (0x80 >> (x & 7)) != 0 { + while x < cw && glyph_alpha(color, row[x]) == a { x += 1; } let rx = gx as i32 + start as i32; let ry = gy as i32 + y as i32; - *verts.add(vi) = - VertC { color, x: rx as i16, y: ry as i16, z: 0, _pad: 0 }; + let run_color = with_alpha(color, a); + *verts.add(vi) = VertC { + color: run_color, + x: rx as i16, + y: ry as i16, + z: 0, + _pad: 0, + }; *verts.add(vi + 1) = VertC { - color, + color: run_color, x: (rx + (x - start) as i32) as i16, y: (ry + 1) as i16, z: 0, @@ -407,8 +431,12 @@ pub unsafe fn render(ui: &Ui, words: &[u32]) { spec::draw_op::SCISSOR if i + 3 <= n => { let (x, y) = xy(words[i + 1]); let (w, h) = wh(words[i + 2]); - scissors.push((x as i32, y as i32, w, h)); - sys::sceGuScissor(x as i32, y as i32, w, h); + // rust-psp's wrapper names the last two parameters `w`/`h`, + // but forwards them as the absolute scissor end coordinates. + let x1 = x as i32 + w; + let y1 = y as i32 + h; + scissors.push((x as i32, y as i32, x1, y1)); + sys::sceGuScissor(x as i32, y as i32, x1, y1); i += 3; } spec::draw_op::SCISSOR_POP => { diff --git a/package.json b/package.json index 1ee27a0b..7030c650 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "serve": "bun host-web/serve.ts", "golden": "bun test/golden.ts", "e2e": "bun test/e2e-ppsspp.ts", - "test": "bun scripts/build.ts demos/hero.tsx >/dev/null && bun test/contract.ts && bun test --conditions=browser test/tailwind.test.ts test/renderer.test.ts", + "test": "bun scripts/build.ts demos/hero.tsx >/dev/null && bun test/contract.ts && bun test --conditions=browser test/tailwind.test.ts test/renderer.test.ts test/svg-bake.test.ts", "test:tailwind": "bun test test/tailwind.test.ts", "contract": "bun test/contract.ts", "gen": "bun spec/gen-rust.ts" diff --git a/scripts/build.ts b/scripts/build.ts index 1b2ba982..f1f5749b 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -6,7 +6,7 @@ // .tsx/.ts reachable from the app entry (content-hash cached), // collecting candidate class strings + text codepoints from the AST. // compile tailwind.ts -> styles.bin + src/styles.generated.ts; -// bake-font.ts -> font atlas per used slot; demo images (PNG or a +// bake-font.ts -> font atlas per used slot; demo images (PNG/SVG or a // placeholder); dcpak.ts packs it all -> dist/.dcpak. // pass 2 Bun.build (plugin serves the CACHED pass-1 transforms, iife, // target browser, minify false) -> dist/.js. @@ -19,6 +19,7 @@ import { PSM } from "../spec/spec.ts"; import { RENDERER_PATH, solidUniversalPlugin, transformFile } from "../compiler/solid-plugin.ts"; import { compileClasses, generateStylesModule } from "../compiler/tailwind.ts"; import { bakeAtlases } from "../compiler/bake-font.ts"; +import { bakeSvg } from "../compiler/bake-svg.ts"; import { DCPAK_DTYPE, KEY_STYLES, @@ -146,20 +147,25 @@ for (const a of atlases) { ); } -// demo images: any collected literal ending .png is a candidate asset name +// demo images: any collected literal ending .png/.svg is a candidate asset name const blobs: PakBlob[] = [ { key: KEY_STYLES, dtype: DCPAK_DTYPE.u8, data: styles.bin }, ...atlases.map((a) => ({ key: keyFont(a.slot), dtype: DCPAK_DTYPE.u8, data: a.bytes })), ]; const appDir = entry.slice(0, entry.lastIndexOf("/") + 1); -const imageNames = classStrings.filter((s) => /^[\w./-]+\.png$/i.test(s)); +const imageNames = classStrings.filter((s) => /^[\w./-]+\.(?:png|svg)$/i.test(s)); for (const name of imageNames) { const candidates = [appDir + name, ROOT + "assets/images/" + name, ROOT + "assets/" + name]; const found = candidates.find((c) => existsSync(c)); let img; if (found) { - img = decodePng(new Uint8Array(await Bun.file(found).arrayBuffer())); - console.log(` image: ${name} <- ${found} (${img.width}x${img.height})`); + if (/\.svg$/i.test(found)) { + img = bakeSvg(await Bun.file(found).text()); + console.log(` image: ${name} <- ${found} (${img.width}x${img.height}, svg)`); + } else { + img = decodePng(new Uint8Array(await Bun.file(found).arrayBuffer())); + console.log(` image: ${name} <- ${found} (${img.width}x${img.height})`); + } } else { img = placeholderImage(); console.log(` image: ${name} not found (tried ${candidates.join(", ")}) — baking a 32x32 placeholder`); diff --git a/scripts/psp.ts b/scripts/psp.ts index 68cc0f10..e7c1ea3c 100644 --- a/scripts/psp.ts +++ b/scripts/psp.ts @@ -4,8 +4,8 @@ // inside native/, with the exact env block from dreamcart runtime/build.ts // (LLVM PATH, TARGET_CFLAGS, AR_mipsel_sony_psp=llvm-ar, // RUST_PSP_TARGET=native/targets/mipsel-sony-psp.json, RUST_PSP_ABORT_ONLY=1, -// RUSTFLAGS "-A linker-messages …"). Needs the repo-root mipsel-sony-psp/ -// SDK (bun run bootstrap in the dreamcart repo). +// RUSTFLAGS "-A linker-messages …"). Needs a rust-psp SDK: set PSP_SDK or +// keep mipsel-sony-psp next to this checkout / in a sibling dreamcart checkout. // // Demo entries: `bun scripts/psp.ts hero` prefers demos/hero-main.tsx (the // mounting entry — hero.tsx only exports the component) when it exists. @@ -19,8 +19,16 @@ import { existsSync } from "node:fs"; const pspUiDir = new URL("..", import.meta.url).pathname; // psp-ui/ const nativeDir = pspUiDir + "native/"; -const root = new URL("../..", import.meta.url).pathname; // dreamcart repo root -const sdk = root + "mipsel-sony-psp"; +const root = new URL("../..", import.meta.url).pathname; // parent of psp-ui checkout +const sdkCandidates = [ + process.env.PSP_SDK, + root + "mipsel-sony-psp", + root + "dreamcart/mipsel-sony-psp", +].filter((p): p is string => !!p); +const sdk = + sdkCandidates.find((p) => existsSync(`${p}/psp/lib/libc.a`)) ?? + sdkCandidates[0] ?? + root + "mipsel-sony-psp"; const llvm = existsSync("/opt/homebrew/opt/llvm/bin") ? "/opt/homebrew/opt/llvm/bin" : "/usr/local/opt/llvm/bin"; @@ -56,7 +64,9 @@ if (!rustup) { process.exit(1); } if (!existsSync(`${sdk}/psp/lib/libc.a`)) { - console.error(`psp-ui psp: PSP SDK missing at ${sdk} — run \`bun run bootstrap\` in the dreamcart repo`); + console.error( + `psp-ui psp: PSP SDK missing (looked in ${sdkCandidates.join(", ")}) — set PSP_SDK or run \`bun run bootstrap\` in the dreamcart repo`, + ); process.exit(1); } if (!existsSync(`${llvm}/clang`)) { diff --git a/spec/spec.ts b/spec/spec.ts index 64c69bd7..97cf9b0c 100644 --- a/spec/spec.ts +++ b/spec/spec.ts @@ -517,16 +517,18 @@ export function decodeStyleTable(bytes: Uint8Array): StyleRecord[] { } // --------------------------------------------------------------------------- -// FONT ATLAS binary format (version 1) +// FONT ATLAS binary format (version 2) // --------------------------------------------------------------------------- // One blob per font slot, baked by compiler/bake-font.ts, parsed by -// core/src/text.rs. 1-bit glyph bitmaps in fixed-size cells. +// core/src/text.rs. Glyph coverage is stored in fixed-size cells as one +// alpha byte per pixel, generated from horizontally-biased supersampling for +// smoother subpixel positioning. // // Header (16 bytes): // off 0 u32 magic = 0x41464344 bytes 'D','C','F','A' -// off 4 u16 version = 1 +// off 4 u16 version = 2 // off 6 u16 glyphCount (including gid 0 = tofu box) -// off 8 u8 cellW cell width in px (glyph bitmaps are cellW x cellH) +// off 8 u8 cellW cell width in px (coverage cells are cellW x cellH) // off 9 u8 cellH cell height in px // off 10 u8 baseline px from cell TOP to the baseline // off 11 u8 lineHeight default line advance in px @@ -546,16 +548,15 @@ export function decodeStyleTable(bytes: Uint8Array): StyleRecord[] { // penX - xoff. 0 for most glyphs (was reserved; old // atlases with 0 here remain valid). // -// bitmap region at FONT_HEADER_SIZE + glyphCount*FONT_CMAP_ENTRY_SIZE: -// glyphCount x cellH x bytesPerRow bytes, where -// bytesPerRow = ceil(cellW / 8) -// Row bits: MSB = LEFTMOST pixel (bit 7 of byte 0 is x=0). Glyph g's rows -// start at bitmapOffset + g * cellH * bytesPerRow, top row first. +// coverage region at FONT_HEADER_SIZE + glyphCount*FONT_CMAP_ENTRY_SIZE: +// glyphCount x cellH x cellW bytes. Each byte is alpha coverage 0..255 +// for one pixel, left-to-right, top row first. Glyph g's rows start at +// coverageOffset + g * cellH * cellW. // // gid 0 MUST be the tofu box (drawn for unmapped codepoints). export const FONT_MAGIC = 0x41464344; // 'DCFA' LE -export const FONT_VERSION = 1; +export const FONT_VERSION = 2; export const FONT_HEADER_SIZE = 16; export const FONT_CMAP_ENTRY_SIZE = 8; export const FONT_FLAG_BOLD = 1 << 0; diff --git a/src/jsx.d.ts b/src/jsx.d.ts index 334452c6..4d531608 100644 --- a/src/jsx.d.ts +++ b/src/jsx.d.ts @@ -37,6 +37,8 @@ interface TextProps { interface ImageProps { class?: string; src?: string; + style?: Record; + ref?: any; } declare global { diff --git a/test/e2e-ppsspp.ts b/test/e2e-ppsspp.ts index 4d75807f..2245d7ec 100644 --- a/test/e2e-ppsspp.ts +++ b/test/e2e-ppsspp.ts @@ -65,8 +65,8 @@ interface Spec { const SPECS: Spec[] = [ { // hero: underline sweep = 150 ms delay + 700 ms tween ≈ settled by frame - // 52. DOWN@58 focuses the CTA (focus:bg-indigo-300, 150 ms), CIRCLE@76 - // increments Count and nudges the underline (translateX = count*2). + // 52; SVG-baked spinner cycles beside the headline. DOWN@58 focuses the + // CTA, CIRCLE@76 increments Count and nudges the underline. app: "hero", inputScript: "0:0,58:0x40,62:0,76:0x2000,80:0", capStart: 48, @@ -80,8 +80,8 @@ const SPECS: Spec[] = [ { // cards: nothing focused at boot; RIGHT@20 focuses card 1 (enter-from-end), // RIGHT@28 moves to card 2 "Motion" (lift + border transition, 150 ms), - // CIRCLE@44 opens its detail panel (mount fade + translateY spring, - // ~30-40 frames to settle). Ambient gradient streaks drift for 20+ s — + // CIRCLE@44 opens its detail panel (translateY spring, no color fade). + // Ambient gradient streaks drift for 20+ s — // never "settled", but deterministic per frame index. app: "cards", inputScript: "0:0,20:0x20,24:0,28:0x20,32:0,44:0x2000,48:0", @@ -90,27 +90,27 @@ const SPECS: Spec[] = [ shots: [ { name: "layout", frame: 18 }, // three cards, header, no focus { name: "focused", frame: 40 }, // 2nd card lifted + emerald border + { name: "opening", frame: 52 }, // detail just opened: white panel, no gray fade { name: "detail", frame: 88 }, // detail panel sprung into place ], }, { // stats: counters count up over 75 frames from mount (capped signal), - // bars stagger-grow (600 ms + i*90 ms → last settles ≈ frame 53). - // DOWN@84 switches to the SYSTEMS tab (panel translateY spring + row - // mount fades, ~30-40 frames). + // bars stagger-grow from the capped frame signal. RIGHT@84 switches to + // the SYSTEMS tab (short row reveal, no default gray flash). app: "stats", - inputScript: "0:0,84:0x40,88:0", + inputScript: "0:0,84:0x20,88:0", capStart: 28, capN: 100, // window 28..127 shots: [ { name: "midcount", frame: 30 }, // counters mid-count-up, bars part-grown { name: "overview", frame: 80 }, // OVERVIEW settled (count-up + bars done) - { name: "systems", frame: 124 }, // SYSTEMS tab settled after DOWN + { name: "systems", frame: 124 }, // SYSTEMS tab settled after RIGHT ], }, { // library: RIGHT@8/16 walks focus to tile 1, CIRCLE@32 opens it — the - // loading spinner (native `rotate` tween) auto-advances to the detail + // SVG-baked loading spinner auto-advances to the detail // screen after LOADING_FRAMES=48 (at frame 80), which then springs into // place (~30-40 frames to settle). app: "library", @@ -120,6 +120,7 @@ const SPECS: Spec[] = [ shots: [ { name: "grid", frame: 6 }, // icon row settled, nothing focused { name: "focused", frame: 28 }, // tile 1 (IRON VANGUARD) focused: lift + scale-110 + { name: "loading", frame: 44 }, // SVG-baked spinner, not a rotating rectangle { name: "detail", frame: 112 }, // detail panel settled after the loading screen ], }, diff --git a/test/golden.ts b/test/golden.ts index c42f430b..3504e1b7 100644 --- a/test/golden.ts +++ b/test/golden.ts @@ -141,10 +141,9 @@ interface Spec { const SPECS: Spec[] = [ { - // hero: initial layout (f2), mid focus-transition after DOWN at f5 — - // transition-colors 150ms ≈ 9 frames (f10), then five CIRCLE presses - // (edge-detected: 1-frame pulses) -> count 5: "You pressed a lot!" row - // visible, white bar translated 10px, focus color settled (f80). + // hero: initial layout (f2) includes the SVG-baked spinner beside the + // headline; f10 is mid focus transition after DOWN at f5; then five + // CIRCLE presses (edge-detected pulses) settle by f80. name: "hero-main", frames: 90, capture: [2, 10, 80], @@ -159,28 +158,27 @@ const SPECS: Spec[] = [ // cards: f2 = early layout (cards mid mount-fade, nothing focused); // RIGHT at f4 focuses card 0, RIGHT at f8 moves to card 1 — f12 is // mid focus-transition (card 0 settling back down, card 1 lifting); - // CIRCLE at f18 opens card 1's detail panel (mount color-fade + translateY - // spring) — f78 is fully settled, ambient streaks drifted ~1.2 s. + // CIRCLE at f18 opens card 1's detail panel; f24 catches the early spring + // without a gray color fade, and f78 is fully settled. name: "cards-main", frames: 90, - capture: [2, 12, 78], + capture: [2, 12, 24, 78], input: (f) => (f === 4 || f === 8 ? BTN.RIGHT : f === 18 ? BTN.CIRCLE : 0), }, { // stats: f2 = early layout (counters near 0, bar fills barely started — - // staggered 90 ms delays); f20 = mid animation (counters mid ease-out, - // bars part-grown); DOWN at f50 switches OVERVIEW -> SYSTEMS — f85 is - // settled on the second tab (counters capped at f75, rows faded/sprung in). + // staggered frame delays); f20 = mid animation (counters mid ease-out, + // bars part-grown); RIGHT at f50 switches OVERVIEW -> SYSTEMS — f85 is + // settled on the second tab (counters capped at f75, rows revealed). name: "stats-main", frames: 95, capture: [2, 20, 85], - input: (f) => (f === 50 ? BTN.DOWN : 0), + input: (f) => (f === 50 ? BTN.RIGHT : 0), }, { // library: f2 = grid at rest, nothing focused; RIGHT@4 focuses tile 0, // RIGHT@8 moves to tile 1 (IRON VANGUARD) — CIRCLE@20 opens it: loading - // screen mounts and its spinner starts a 720deg/800ms native `rotate` - // tween (f30 is mid-spin, ~135deg in). The loading screen auto-advances + // screen mounts and its SVG-baked spinner cycles frames. The loading screen auto-advances // after LOADING_FRAMES=48 (screen -> "detail" at f68); f105 is the detail // panel's translateY spring fully settled. TRIANGLE@120 (well inside the // detail screen's active window) returns to the grid — f150 shows tile 1 diff --git a/test/goldens-psp/cards.detail.png b/test/goldens-psp/cards.detail.png index 4b045fb3781a01a946f6fee83d788ee0d38bab3a..f3fe5d974b26336854e201750d639093c0bc6f58 100644 GIT binary patch literal 19872 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 LGB>QyCr1ARpYDio literal 5811 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 R;7i(of{dzkm89wC{{~mK@#p{m literal 4973 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 H`tAP!1j$jo literal 4721 zcmeHLdoT8dH`%6(juTjb86FgUqq+!@25az;W#$#oDS*C86WP(oyK zpO|4r$e1w(8BA{Db^d$Tde8aiw|@V;YrSjlz4o&{d#$}b>$9Hy>}T&MRu;zG9HJZm z0C3+jF|Yvuw(p1I)QKa9k;My&Q-{;pdnPvK01zb$01y5IfStq80}23yD+0ih8vuZ_ z0YEq?ztvjnaN+nLX2u4#j9~UhyUr<@zP=UeJM;-rATgBy* zcOFvIJfKw7tDS(guzg<-z3jm*#G6%~YYIAg;;9z9_S|sfPZW7kO$#+9_gq!*1JX{3 zoxUz>Pv0=?AE#fsu31A$7eklD?uI6mUC%x4W_LX7vekRu4zTXU<-PnG1HbZcI1$!b zQi|@9UC84AY$S$J4>NLN>Gqa0zssvU{!a9hc0sdq9&TML63P=*-Q7QJdu`5w5&ctE z*`~>(^c%tH`g$gy*E_7?lcjdKT1?_Gb9RANG36Vp;Eo!ZQZ)_mi%9m){UWaXz$$cs zz#4@DfWex5AYhgB;5hL5gwNjrKdeIK*@Y=@%jJ0~*Emlq8JOhGbhQiSznCO){knuo zobXRD+wx14_>EQ5;UBKO9Wh%5q4R84Q#B|Re^SMJgBQj4fGqqlpYpvAZ^lJtv}Ci` zUS#_wm3XMjJ^2yR#HFTa*7cpS)Ks+`5;z|e50PK(A)?eqM({^*@P<@Hghv3V%S!z^|h)TqAV z50|iYmqi)R+*`bspea-G(32ZA*Vn0;nkrKgxJK6x>{SJSQtWXN-}e3U_P}Qw(osSA zEFGAUk<$^k*n#PubWsU~%o7W{s79oqwxacbu$C}Ty}c3-LUdNIK{v7HxuGyp^by#0 z{$sAQrX$b@juN<6@`hfR5K@juFMiu-c)LnF+%h|1yf!<6;vb>z>{7>W1M_CM82bd# zzV~TGMn;@=S&;7x4-x>KykE!3dH=8&_7M93XpW134by)0+!L#e6eSwAIjAP#$uFu7 z=dq~Z^Xj6h6*d8;Mp9xJuZpi$=t{eR0kPG)%T*-X8RGKqIxDF=7_Ilj=yZp4ti%`r zf&*x?_*>q!El2GG6=^+lpBkQ1-h6vXU5fA$dMR!=-pEGh{&M?N^OI)>I)=A`esp$Q zqjUu2Ed^mtHtY0`hYIrU17_fJ;XYi2ubf2?;nwOwzr$P5Lg8CYP!fm-66r$?Y51Vf{~yV=3R*F^EGt*g-b-Elg)SJl%yOZLT+R&JA_|KqU~P}%*1uixcr5bEC4t*i`eukZs}au!&{~qmS!%F2E?k?m`pX+BsP-EwS*Z5&?(a^qnMg~Icb`?VJWmQ zIZ+vUrAB?KakTb+-D?6S6T{b*wYd~7cTGjNN#=D7DS+@E-$AT~dfHNVX1?BKek_yp z7At=-8IfyL%k((l#?*iI=b5NgtrBf|(d}3F8X7%K#Ej+#`b-2uX=o@aQ<^5)^|h3{ zk`PrFJ2@%Zt!?McUi($)#vVr=pKK1zt%14Y<~VE{J8}#)h&2j@3oWw(#CHz#iKkW!*QEIvdsCzJ%%+521VvhkbO$ z@+sfMSU1N^R_V60;(QNOr00)(f15jVoeeq|M&99_NU%($s0P?;<#;{Q$Xto1v~UWS zVT_T3tRXZ<(?(?v7nz508)R&W(D`f@p~hf?NXnGUIyoF+UfNl^7=1 z1hBLN)ziE%YG&Xc>IRno1343UV`8L-|%s0yh+$%XEniRyLN7=gKf21dQDW z=dPbW(rXyE^2W1Fy(>)i9d51^Fw0RXqC0)Zwq01^AN`c)dAC*L!)qm|-qg>xVr`_y zUB53@hbgz&-VAZ4s3q@JR9r&hf1o8mSGa}RzJeN5!9fG;t*~^4+!&Xsg*n z4n&T};PM>80ma6YqJB>|Rp)>em}85c)Or$e)J*E$|QL;F>ajl=DQ zAit6e6Roc*u|q$5R`7o}dbN%7Ulz>j#z~rwBBGZ;Uo8C`FX!GzO+3-hY2Fw_ma^;2 zY9tjfeNYvWpbC6dw?DNV+qZRW8s9}3Qz4{`*IzAO<1T~+BV!7vq+rYE-K17Q$gk~r zf;Io~MhO$6-M%(|>^!9x;>eVTbVY}7U0rzRSII)9L?cUi?mP-jue?am7?GasLhxoy zI0yea%51)}sIB#?eP?=_J=YvKArig2QD||$Wyk|6^Lb3FJ5){ z5z?Pd=8a1znQY;=_Rcw&_PK0Sq9j=1%iUyB7P)9)8!b0dHD1eA+#V6pu`fkBnln%l zl(yXwZv0hna|0ZlYftm;ZeS17ij$NSitU&a1;U6a{IdOa<(+jTipv+i1;I>t>2k zt|N=}?`jgK!Rio)$SCG}Xw^MFcpYRk7unt>@+B-BT71S6Y zDVZ160gv3EWzLAH{ce2N(=03ls!72@9rs4JdzXd{7_OG9_(JLRZ+@HoRBCy-cVI0K z$yuT_mGU&cNve<-hln`XjBl#i<AKJk|Zrqs2**l8VW$u?!`LP0^lmOot#A9 zzF1yPeyI6qFcJ|FXOcF1Jr9v|(+E|N94l%6_D)!_Ti8l8LetSAX8ew|_e`oN?2pDe zh9+-$U^;s_ED)V)uI$hZkDPAmdx~?9J*w0*LfvG@@f7v;Nu5r(4C=F!g!5OUG*Oq3 z4EZMR#Fh41wH={;tbqg8)R?wxk4aWin>LV~(NRCGIe=d_R3(8tWuvFsWkdQtDsM1p zGBt^aPEf0hM_~I_8yOy%(!`U>nnHL>9vFY)Wyv2ruN^v#pifo>om*ZK5A(}V>-@+C z*5$4Cfz0G2b=)L&?7t+$w(3YN)aVhhlWXs$zd!WLJWruKt>13*G!~F3OpxmM8tUHs zv>$707Uk&T=)I`#00(c-)zD{h-o_MOSRr0tV9#5{CB555wv+PHM?~HnTADbjs{gD` z+zS(8P2Y2N;U}$fJRrqVgVlYJh2!V+E{31?Ih$6m1mgY6N!qCOumOsH|5E=?jsJ%T ai`SqFl>}R?e|7jT1l%&TFsRjcd-N~(4U?4s diff --git a/test/goldens-psp/cards.opening.png b/test/goldens-psp/cards.opening.png new file mode 100644 index 0000000000000000000000000000000000000000..a07a402443ba5a275627c926f80b92c7d9dd9c59 GIT binary patch literal 19836 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 HDWM4f5BvAb literal 6003 zcma)=2RIyGzsE-iRw5E5ItijB5~2lR)o2OPdr!2*23fsDi697quxb!>i5^x-L}#_c z>O^OkUA>pP`9Jr*@6CPx@B7?mo|$>h%$ak}%$)Q4etzGGry9zX6igHV08px`C~5-$ z(J#V&mHYzX$}*SW1mSeUMn(Gx0QhhNKu{xkLc&B{Ho^7&`!1zb<%$3?2j)#dqb zKFgK7FsxY+{O~DJoXX9pib%Sf)NFDpH>R&rIeQhbIJc6HtMSoM1%(EY8_*f43%Qxx zDvYPKqQ9E0rb}%SEgsS6>&<}o@;gI8`y_<7;l9o6Q{l+%<+K?>3Avn6=ZhcecFrV> z*MILVD*$|rC7}e=;%{rTLYR&c;tEuJ;c9AFr7>yvItPj|%$x>?YC<>+LR z!#l;Y$^gA(B_xghDdl&7om)^6=^oph8j+$ zr@-juu&+Y#WOZsIY-=7JZxVCrYnx(}88SV_wk)JYKkc>H!d&*2^&)passSh8C1z8u z{lafJB08CK0y}HFJkX-?MV0)qo+w95z8M5QOb;%zV{tB|ag}z=P;?ZFRy3tb+17D? z9xGJ2(BahacB?bOyS1a+Io!ZS>Y)$AL+;Z8o%MEqO{oUir1TauIT*5Z6m-LA23 z7hz-~t#`ae<7V8}kkOCR&E2NSeSDKDeP`}AY!(UGF4lY>PQcW8GJ7UIn^B3LzJ)Ws z@dFcOA!tCoNs=~tps~AffRWzaHs)lVn9!6@e}9Q-j(ZlyF1XKPe1`5x_QfQBk^IhN zQNL5_I7QE3mY53fpLg(?Z{kTszU_R{x;=|X@TBh_G@2n3Xs{5UfW(}hz%n0H?f+a= zUp{lUO9HRq3|qPp;n70$(|*HKjm9w8ddc@D))u(Z$OTogfFqDiFGk(s%a-R!Yt+59 znFu?N@bCISWzu>q_+d)u8M+GzJ>2n}IXaq(CsljqA8B*E^>}M^EQ8TTW+%rNeB5)) zcF=}R+VAtIQIdMimL6Q9^WrqOu&?2EEt`ip`MU2c$^aglCPOl$J63nN;P^@Z@dxMp z#B@Td!>`;E)8XoF&z`vUdMLuzMZ}QDHa+ZhHuw~%#kk+>(4zXHubRsu`n#Ts^U{hC z@r5H1m!l?0KU~EoSA~0??xUK^t_cIFpt`f8Jk8Td*#6niEwIe~^cddRg3ZoxoWbsB zt_5C3;Vx3hqB3ubq3QR_PkEF7A)Pa4skb#Y4axbm}>If zh7j_Kyw26#wNOtt&Ze@@k8HtD)1>gQeBM$HF70eVfFdflg&9yIu(cN5XFEpJ6klFrtgz2_@A}D<~=4++E|0R`cKi z3s1NDnvX{vx@?d4QotvtbX`g3+edp~@IEduZ6YR6aIwGHOHtccIwWqOD$SoL>rKP8 zojt-YyLSG4b{*WbpDa^}kD1tSy~Kk20a%hN1oqy=LC*uQy(#-Mfq@GeL;z()fQY_d z!iz!}oZj3Ezc4a$++3?zXUeesgpr0;;`oB>wH}G{aNY)sTGlh#j=sLNxwf$g8nEAK zpp-vKxRM1}nz^dO5{GKqO7t}MSslvLI?uZbykwelAHb0x&9$W)nAhzLmzn$RPX%B}I+JlA>2-okOqf>^BD<7Se!E zzEP?9An4KjeJd zUaB}+sNbc;fXdTK)c(uT8+@swL}0!7ljxkiU*91xl?>E{a_-7+Lal>&pc9(6A%wQz7tsZVvE;>?EbBo8)YB-x;em!yI17icD zQ{?>aNXMb}V0}nYOdtN}a)q@woj3lrwDVzvIdF~iv zPM?ftRp^+I`p&~QuA`ZP3-G6WR7wiTD5E7q zxJ{WOglWwSl+urKy<+Qn<|k_Rz;kn|0!=n&Jyy0i)At0VEU8d$O?t4dn_pPLVtNSu`J$+QBZ=`MvQaer+8U7<{X_HreIG@(60@zjOwD zrk}aQo^c+gg}E4XbI~6@WzT8<2uD~>4`;YYf>hz#@K8X|OkF%+W8S6%05&?0H01%= zz~2h4i;iu-0322J@}L1(p1;)eKPWsu7W;?@IJNpi!vBT_{MZR{-$woi9I!`U1o9Yu zvw|O-_5a>=tW)8`TKFJ*D)#CWP+=%A;@0k6Tnu_#<>scf0<7+NJ2N#jCl?gQv@Ipv z{L#zIWq7kd{yDcI`JFEpK;zx9Ef6G!k%`$vd~to<;!(1BF@!#e)9KAbfy$v-f|i>d z#qElr`U!*h`qV~NQ2YA3QZib@7j-%;=)?v>k7&cf-TN+P6RT=2xJ4XqZ=e1G!uc{? zMnbw2%+|Q#(ym^m?3jSo_n>EUB9F%l;Y zXIu=MTPxff;PNuaJL*nv-Xm@Im(MDuo*|RIbT5U`rgvN+9Nl~}o)}iS5<>~0TK^R5 zW1$)yJw?#Yk?aw|wHtYlMsxueC?31& zCKn$!tg2D)YANNmr}57zfgoL%Weo|Rhr5bHcZARhM%gxjD0D%?RhRxVO!d+}9q#$! z8gVfJ>r<18Jv004w4h$CGPSFCN}*{y1mSx4ALqsA7zaq`jYuCXdRr z{A&3=CW}mmi)b9|jQs7njb2#MrLfPiF~(669ym zczqLR8l--&N$^WPkm~e1mHfq>M{do}J-p(`w>1SV8Rj)zcUl`qC;i@g%c)f$p2209 zq!3#~UeP=L1JP>x>Okg}?#g81_h*Yj_&F6rQ88ifa9$WhyOO=@;%+fC|v_(co?BYFFF zvqHF2IvCFM`L$|H@e~J^(UOJ#(}OvKPV}MsaE@iy^D@<9k=hOPukCpU1P6k6Ss{>z zVc@nv=A1qAI#VUwTI8*#Nb7(|_5wAPXtqt`#rnm^SQ27>W9q6nGx^uWyd|)OCfM2J z&trk*yf-wL6LNUv;48EUr4Nvn9~EIb!z(t1Q1?(ihd#v^r%js6D~Y8fW)S2uH}#Cr zIYpW^?slT*PwsnGmP#6eSx_~ampb-8g>ekw#gb0~!|yErkQt`Y<6*GeqItO*8$~xy zS@M&Pp{q~t6>&cP0PbTYn9!+w#7RQI6iDKn7pq;@J|$lDd<0@X3(?M{Q!?l0KK00s zrMb-TI= z9Ho$9*!bMzR3>~D{MK{j+9(zJE=YAkz4}74S zKe4|9WYPU&?f<`$PXV$f#cP1z{ogU>=Y3*(U?Adm;+eRcQy#dXT*hdV)SKwNhUwC- zcG>UJw$ZbM8$E;PO%XL&!uu8vBgCoUtVX)P(+wfycia`8_^e^u8RkjhH4FDj?yraOUW} z%a#4Lij_=9)X45#_O$xSa4&K8`GYHuwn6ih)z%lC%vMp=4#gPCqm>lfaT(P!vfFGti9rh&BWc!*4zBZ zvH2y;3+r6!n8ZHTHnD*Iq$H+NtZoQ{sALn<7JaO~x*y@`jdd6pU(k>zGiEUH^M>Ta zQ&~eS!aQDv#TUX$;L(w>)nU?j>E%o2SX&5en+ z*()@psndJKjBVzITMKQ;NEPez!>8gEc~UD$&t##QAKd8WAv1^)eAHzF z%yzR5PZd{WeTmgqUZs`xtRA9s*3f#nU<3(2KB0m>*fViK5rcnB!!Jyh{t37LR2g2A zP>7p*s)dmHGz?epD$e#=M<6ZcxFjHrPj{1{u)geh}v~ny)?#y2>fGDa_dd>5TEX}pAc?^8%~?_kBB#DAxA5cLBN z&lhK=&vwm(W=uPo7wqmyDCwDq8F=?V5#sq>E@sn3@Kj2t=nhN*TA;Dm7*djCF!(wq zSTNp47&>u_NLeGKf}_LredK-SX`lDSVQ7AAH-#Q+J-;vIgCPksP3ZUGKLwti*HrSvL27>nhQBe!@F4`lEUBv> z$fD6<`CHrUpQPg`VYf^`y})SJ^SL90#p%ZAO<=`9KUl0i?F8xB?$W{`x%c%|xdJdC z^*15sw;?xooW&DGOx7#)uc+m?0zx23*X8-oquR2w|gk|u2bpO z(hsz*l;OKA)T2R`A&>N|H%=CIF95S>VKUb9JIcKM?Uyb;xxOzF%R+VEd_>o_DoRt6 zc;#6YJoHI?C>@in453S(N2B}#6^^EAuX};_%!fKGN;OpeI~+~jbEO!3f(K`lh8Gm& zz`*Sue)UE4315ugE&}ZNQKCXk+bKYI{82Y(v7~ z9TsYw*|$#P*`j)02sf9my6rnNajZh(b$Cj-cTQPdH*ykH6vxj68bMroV|*#85&}1_W}a z0)c!3L7*q#knaHqo8><=k%5zza;&m3@ZJP0J}D=8wV z?7DQC>7k9Ow)Wm7*|Gx}hxLaZ{>Wdndn4ak)GAlEEn3ae#R|;xamh>b%QE6=Q{!sm zT3frSMeZsh-}4dnN_O(m8C{5TbKb9yeNAmKWa}8w-KbZGD_eXL4a`TbUpas$1NN<`T;QdKLtmJdhp#@zL5n236;N=AU z#L%EOX=(lfr+8ps@C4BUi*p3X;r%bLu*`sAS^kibB=9{-|NDb3?RGc1vawwI4ovqH zb%$^S(f-7@Q2fP4LZtL3k?`3}6G#VJ@%fVs{_dmnV@vOt4Y-T4fcbY7PXQy!KVe|` z4z>hhf)mM|c0n;LJer$d8gT_zbHB7{j1dM0E3>9mHLLqm^b8O=N#^j#4U92Oue!&o zGZH-)XkmCtc>G1BbehLII>OKPv@dE#79?_C1KTpxZ()H`S$Mb{p6X)*gbvW0g1xVQ z8FJ1R64-T!rbneUA7-h_LHy3&%oE0kJk-(iu~~ALcL5h_sY4d}fI zJ+5pDqG{6rKV-Mh61sCy?3NQ0aVC30GF0Bhh5IjW~X}MHS^O0DGmktjX0;s{1 z|D;7@@B1(#0*mqG_NVDw3K%C+0v9%`@k_A^iLl^Zce+pES)mqR=g!pzMS!%M94U`ci!t}u)f7xUg9X& z5Wfez^Gr=1pnO&3bZmCde=l0f$9FpX_9=Wq&4vb%g>J=)632s|hV!c^&X!oXnX7Fq z=(BxrLAp|U9ybS})nQUtf>#C9rX5+GQ70=ZQU zvfE%3EzA~=Eex(b;Qa6ieMsD$JA+GWLvfFw3j;CL)yAMU3uKW@q$|{;hIy&+42ka<2%w*hMe~NM zu0PQW)kfU64oy^4Nl8qqf(CqY*bJV7>=2ZW3 zHf9fkPpg;Fb1E3gaaJhO!7zi%p4a#^nb-dmfr$C4HS2Vi&ZwmJK2IPZBGPOVCGi!3hD_^Gg_mZyz^ za%Qy#-@lA-4Frdp6%Wj>r|Z5uxU9SFe=*+fCH8kw5y9xInKg}6>pFT6q{M)Gj0(9Y z<~v+lM1}b6lQNZjUHJdK;dXi?X)ErCP@a!+`;<5QF zQ8JWyq_dpn?T7(=MQzSTzJGv;@XuXTWry-oaRvIH&uE{k=9_-H-$e~k#oB0Ak7%VI z?EBr&|B^Po|D*~2CGGjvIr-9G%*)WVCIr4L_1^zdD3X(U^ir$_NnodP?Z+Lg3hBFk zA=!YsBkxP`s8}g#mP!A$+>sZ`-C2@Ap{S6DYMEry!ow%TfLh z{LPT$BNq+?T^MWZOZiw*E540>crT;6PBzZmIe#L2ldXpW4d;AQCbXhoSx~*oUCfzN zA!@yz--i$&Q3x5JXpO7;{dcn?-{4s`ckAy2MzOwEbmvzC#FdZj&0+y#GFScuUkH6Mf)~?X@q;Oe43)N)S4fN z+a;4z$)j9S@2!__vBMtA*b!=R0p}@{lexNX8gf2t(U|LSK|f?-M)2zV-6zV)b4iu^ zYZ{Xb3z16h>Us93#*8|;{FzU@?ebo80G6PLK|F>j(F8v{C^ldG;|?(ir2EYQg32SM zv8Scr8^qH5s2u~GR&%F}NkGiQGFulE{FW+^d0w(BrNYxcn;uwdu2?2r?Y+C4+o8E^ z@_SCzBu_k`_~5?YB0vO*7I8gbj&q|!Oy>7=pCj3=v$-5{55*i7LjFD=C|uSm+$dxd z@^qS=Ma1ZHGShNh)CUY4{;ok6#-x(VnLfcJIIQ_#W%Fg)8oh4iuF zrZW7m9BV%VJZRM0^ZP>xDM+Bgz91+Sm}+2XF!qV5{c)1{ew2rYgA*+8Y&FuI=`PXa zab)SlxYo_gO+Wq^j9wPyrdk!-**GT#+mD01ViS>Q1pd3-#U5sy!+hmLwcCCaRYk{~ zva6p9A-R{Bd0W&Xtg9`Fi5?=w>|6>?qmE_OY*S4d_&My(%JXdY2{a<#LJFqKI;lT~ z57~tNNnH-Y1mu%=38X1q$o@0&_!1bvI?6!ePa@5;6i`*p?B%p{aqi>sBPmGM#B*Hn0@NSu+< zPP5JFPqs0fF)dtHSfTtvn$Whin*FP`+iF7((tnyc(|{W2_Vz{{C=6yb9!?_fxstffhs;M;9~${Cy$hS3sb2pFal)O zVW(wtqR*BSqwltGK|1cVg;De;QLn7FSxx0J`>L!QO!Z^c;9M#62|8FZYF-_%`}w}|EUO;4}4L;A+(KRQU6@HZRX zC6-45le28IJN{*iRDj_FPMlY_IY@eX=^gqIhv;VOB6)(@w4a8AZa>h z>lxAbuWmw4R)(9Y;iFj)12Hp2)SnH$ujFB9B;n|7JDM}yP?hj&LwTvRdUvun@PN;2bqO}w;^$7;Vakr}xDq>lk-42}E zBoB(&H{ThT?g8BKCJ63x(XBPUODI8sT0B8@ZIbF(ZM_0Ws%L*rc$K?7Gna>EW-CpdgcitxmCFZ{q z)F%!us1T&Kc=(9HaNcD=MsMLEypW_S<79f_>J#luN z!vWHk%=BR-XSR=l!{`G2FH=UqnawA)gSooyKC!d$08TSaiTa`e;4chyJ7M?d$}}ZP zoWrxwF^pDpIE$0(m+FEawcxWFrx)SJ;snh8sY_185DuZ61=}qi6{e%pfQo?XH>zv- zx5EqcTqN8~5hl8)2N!ds{qd5{r9pSbd9@z#52<{VcMC&sRkNKak{YYCdeu62m*-Q1 z#D@?q^ZHT$+$l+I4nX!;*1*rzZQmVRUle6(exTt(LJG{JTpTU+C#goREui!LkN`AN zgg;i`fy2hr!2c6p`&RJYZdXnY1A{@2#pUTNgB(^E4g^gPTgaF$=N!AdG=36bq}H>8 zfaDS7p5o#(_mJ7j{!{t^-y+Fw^i^K=0m1F8dn~WGC|(ksqEv&K`uMiL*{3=9L?(sD zuYo8EgA*lKQg;4ReH%6(tKs-|{5}ugqW;&VaGPqt-U}yk>g>w4ZZpR5z)+tUCtvT9 zwRzG~u%PKXuH_fNr0Ochks=I5POWWCp(m8q2f$Pgz8Qc7hRe$;5Zow2Ud~Xry?(<) zI(dGiVBby|n=bw%lZ|0|{hO1SdLor`ajz(U4JQtba6)&0eg#vQ=h)RK)Q4uKw=V>$cDxE;>{ zB}+Z|iDmNaYPh^ROmKXj4MxCEnoO6)rGNjiSTaPKEQ{CS`}n?I0U!I99rRXNn^Ddi z(VK&si%nr7Bgge7AR|tAc^pPIj#RkZhIgYF@mfo}0`Ocs;mFTp*;;OHZsl>`2sa_+ zY(hu_mgfZW2vaBr;A2XrjN{&(Op+AdLNlezINQ`Bljd!Uu4*2CQ9SU`pf7nyqX&l` zM)f=R>D;R(@)<3*4D*NSEtK!~Eu2wn>QLgeF{tF!7p~V{1lbJ#wmNGJlG`8afNE^d zA~>VvoU6MDG^S(x40{p~fH#u*qAV7TVAytUGLhj~R`A?v26&DCKh3X3zx4zKXVJBO za(b84_nB!DuyOPzjYu%n(jHyov`2M&Do+ObS6Ks?(m%cy{`Y#RqKQ6q6-0fXmT}h|HoQsyrCnJMH8}k)hww4 zze4w9uhO}Pq66Qbrtea0vvKyqhD_3M&%njo)v3_L@>78_k#6D~%6D9e<{( zD`icQOH4n20)G7H5p|$`jO!9q)TbI*c^>;XJ@5#-SLN{p_wD?!)TXR`@O;+Ca{j0^ zvn!!O4%=tlj7mK614P_-BsQG{9Yq%uCFB57kB%4$Hz(pj|F9!Z{_*EMo2nF!BSc?3 zTry~1D>x^k>-zYhk;pyq-i6Zw8Pg%oMZ4BX&(=w!^FJSxoUFKW_E84-UwZkY_yNB^ zC!Dd-W-Q`%=O?7rY|>$OiTiSx&Bxq(A1Q|n#}2)~b60EE`E8TTgLc}(ut#*H>6nc% z*;Z?+D8JK&>D;Zb{kVBs^zdHB1oR1)jI2L{;TR8{O#d#N6Yk_v3gbiL0r*5WMdqEQXJH69uZ|f9MjEEluF*<{)k071 zLg>(TLvUWLlPg6~MLUrn&;dPnwoZ99zIz4{gFKz54z!;FJ=}xyV1}T{g`tV}@s}O3 z78{5XxpiZJcOQZa@Hg_;9a&K025Y58&8Wmk*KCU}4m|d=^RX6;-`3y3K>Qx6tICeT z9LwB!zOUDz0HSIoN{aWuQySd?M+0=$zd3i}CYt0>cOA-p&WlHt#$1XTC7TXoR}(?tyhN{`9;I0;oX#cw zGWvmkfO)G#`Yg>cuL^Jpa(~fe{4uuU3-|EHbo#x>kpe-c?m@Io_F0{|?Kr@c(N__3 zMN3HHXn*jMq1|JYb6C~Y{WN)@C5ho>q+1SBaXVAp`WuYy3cWWfK~a8>>}sRQg(g=m z5j^upiLs=4kt3A&$x@x2KZgG~g8~pn#Bo6OXNm3sj z+2=pn=erq&T-3*lSw!+_%!xYUg2V#hWs`2v%jd7bU=ZBys?habvnI?N%!gM=ZuzclEG+#WY52XOoB(+ zU4XsO5-q0{Y~D29r|rn*oIX)tcC@Kyr0z#t-vD?2wwagF12-@pb^l6Jz3k==?#pnf zb8z;&cYTbMyG_P#Pdql#m>)z7ZkRF=Y$E&$EBQ;1$uGdTp(&m^@o|A;onFR7QDKvU$W11;86b z1Q1vau!x|n)U*4eANcw6t?@4a9A(n6e}hWRnpVSkUUtB(mc#CPsXg0gZp&6J97ngf z?WbPr5WnMNUXA9!77GfQitGF0vtl(gh}Ip2LwIF#qlpGLGFUf#3E8HbN`MQ5j>*7t z+!25i3)f*0?Nz`Hyp>iP-^T=&3VQ19MtPu4!H>&}<3KcggF`HJc(s>x?)*A`IV^_u zhZLI2u(;Vu0LV3#zSZvk{})tZvpG3JVJ_unnsw6y8-1Is4@zw%0sKPN?BN9vfk zI@NO6hscxI-f+uU3+6Cy_`d>ql8r-i>Z!f!F=Z44;v;()n~;$*f%TXI5of>x3F=3@ zzfZW#-kHrv>ZtsMjop=WG~NaRj<}nfsE!vD7#H^&9nc;p@6@cMrDHUoUZJOBgzzo+ zlPY*6csblMIH<4^TmzY{TURr$okEJ(%0oL;KYOk!!Ha{FJ#EHPFcpVkN{4mDN2#c+ zoHu8tEgfjAn2ln<7OjiAZ~gJkHqVcgU)Fk+)xpvoBig*i$@|5pExf+Fog?O&M|0qc-mdrd*GmgN|cl5imlWeTw66OYuDP@*|iu^ ze*_sBnas~Fp7{`RYW{A!N*q>~D|1j#7>dn@xyk0`CM<0p$WCeIvkH)Lg7xj_z{E^s zwzt1?pitD+y%gDci5Rs^Kl%V_ZGFy`$e73s!6~h_866zWD#@rdpFZcL8}0+dmyo6! zS#|#Eh!~NXP&Hk=x?p(TmYo_z2i-|_@v$4NReyCtdKB!#3+JOIn$c>+r9rx8uRdj) zKYO}&`2#yxv(k;su9G*^KtWr(j>FB(dktc)g`3G<*?lQZCcSx@6i;UPsi8S~m$+t} z@$;#bNr*O413UY2yr_xfPiyNW2Vp1cJ_8dt5ZiOm?(WC%_~hky6THZxNOJV7nAlVu zwRJOIJ_ji%VOWYFS;P&&LzW)SpCPYuWny!O^FXHJ0X zZACH<2LTBx1R6E#>5OHnk47`$s|QE8V`FR6KOR&8s^Tfh!jphVaeqpEeetB0{e(|^ zk6FY8_1mhkG{1a+011k9@O00u3r}xGi{M&39X)Lg3fa(`D$Is;WrInYr1UgG~*+mV%hypL#gc$;o=yZO%j=$jHSw zyf_^a{<>)2vwb!4CG}@4`VbblalHLkdcQ`#OB_<#WtrIQMqlCc zZ`b>@eJ{W95$)QNxwzP@*4{3~mmvfy=W5)= zvRtfN!p9<*Ab|ykhpsTWzxTNX1*wvf%z8RqezetneUwg4$PMMClQUFUy_ocN##t$&h3 z1Byk1q=+J-L4$wW4;}ZS^sc<8;V16C`82t5Xt-*fb+1FPTlRg=Px!t9PNn_3E1C2ww z6B)6wwJBv>{78QBFT`J-HrCi9J$YLWs7J%A$RBVe;Gz8mLP;nja^TURQ8AvL?*uX4 zj}=7wrRwgp?%$0nIh?B0$u3>m-PX8>I8o&x^DKl}o94J%*OX5_Gz^4)33&W1_-7V$JRN{H0P zvZNsyUa1d6LPoY?(xsjoQ(VMgs;UFX->BJ}ZEA-2OS?3}&^hlIx4!r}-;WgPgP*|0 zaRqiz@Y7wk!5WTPtM>Q`10!SY{&RQ)!LGi|{LJ(VGjgy;5qv&S$wUB9Wou{m ztnwQm{ZUhV+}tdy<w4diFzee!n&og+DZA4W`qQph(aKPTjQ`_lp3*M?VaT(Ck}`JFB^de>!6AW~oGVoNvj~?07iTTRAhEPgs5*)&vl( zWBmAGaA|2$&~~ylSJfh6SRFX%8ozr*a-51vrvg(S{g`=#k?M^`Oe_vNpYHLbMMY;w zn7#IUO0|_oqOdI?t}baC?aDEMb70&(5(&kRs+EIJC#~1sn$^)-Fd`yiwIW?raqGjA z%RB4W9nwA{SSN0I0SJNiR3^u6`1zy~=Jc`bWO)}BY>=5nv2_!o_=ln(nw6TbrS~v+ zptv}by?w$BZ$p1=Dcsb>Lvss)^OvIH>iR6|^t8H_k81fsq^6zas=B%cUXtP>fy^*V z;A@B(RXMO~VMVSFHT49X@77lRoX#O2$XVLum_03rgqIijePluPNbWyi1wxwl83r`x z8S!SJKiC^mwAypBuF3Fsa&q_l{H4AV=)q~VN35}EZa8kk>DPC{OjXKyi1_EZQAcW*>g4Oj2V=y=roPHoGvzAkkU8KUc7+b(HqE$o-bx-8!v>MC)wY-uns&mofM~F; zK5MO+Tb7JYa05OQWT@x|glH_a&76fb8cQ0RN%FKPutE>`_d>(ry%Dy{e+9N?H*rsZ)a6~P(wdZvEJNi8j;+1jj}(0AHMQ~?u#}7!=RS!j*EhVq@7*pEmI1B#2Fc{ zD@gYh-!&}c90C56&i0yBXy)brq3X19VzFA?!a_u(WlX{C=TTV3%IoNvr`;vz)=ATu zggs86)5zvousc#A(_E}*)XMDdm|!eDbQ(wj8>|$X?%byt_@pFLKT?KRY0 zmYF2TLU7ITifrZ9=T-cuj@zfx!Qr+YlZ9DB6|c zy2$PalZ~xuXE7$jxfy>XrJmC~iU!<`s8>ZC!J=eDGbKoWw=-dxTf4CB&$q5E z?sB5Yv2bc;=J473-2K_Y!pg?Cqwjh+943k10E&Anu+XYu7S)@vk96NS%ax{CY4GTl zQjJ7)?OMf6{N77+<7E_9jztI+JErux`^75AvYURR)7 z^pdpDI7w{cLu|Vp31idaT?fd{@!8;wDmCS}(P|Z&)f?3DP3$H-{o)NvRqJR+&na?6 zNlBpeeD^&n94vPD?wpR)nA1q3){KvJ%GqHX0sq||VJ|r>!ss$1VwYiekR^zX=0Ug(#qrdyD91EyE#4%(?#oA{C?7s8nDiM7Y6k+ z;n-iA7e|st0ywbYPaCmTSWO&szZNSmM8 z&(l~>d42YGdlc{-}H%ex>n+4m!8{Jj}Q4ZmJGkOyD(giCsddaksOLI`D=9<>sRsoI7|BV8y3kk&BzSUr`0WrGpzAJau%0v-xJM zP7iXrG*2bmTE9F1QCJ{6CiwFR(Sz))#e;&6?@X_cnxMkXV;jo;$vub`BI z{x&(|>PnX5VQ2r466syVvu8<}NyOH6ZLRhNip@g0@Tj*U4wxdrt=P{uK-89BtW5SZ zsXktp3VY6HB1B~K^{k9+Uwgz;mk+Ngli?OUiV568VnV#cQEPd~m`9-WZGG)RItJE) zxb+WmMooq-b3YI&_hu4#Y?DE62bYouAr7FhQpy~-i!dGw_eX8Q60;y(aiY3Uw4UsbO+HpILzBg?|g|f7>Vd z&+Vt7 zNCV)c3Kea%{7ABXQ&QUxZgj6T5euRj{od*b7bdy5*>6RIuGu8zr^+}N+>UnymNUFz zh%w0iKWy3+y&1P*YqR$hHFmP{Qkss`y2-Atw(*tLRM3!tfF68$`Cc^*=N^$iAV<3s z@L-+`UUJA&)!{*Z;g2Q?m_Y8wvlopoPiZV~syZC@sp9NBR&|gPRbf>k=n?w>tb6e< zAuum?nr!xm4?1aoJkV)(=%V3qc=Hc?QyL5)io^pOi>m^gGUoR4)iU`0GMg>m5}%DD z$4C-K;+&SSjn?LOWIq~p7Z zRW^4kHEAOzTCc4wDv@9mluobWUszo=EeW#D(nbpz1?Io{3JY%SPuf&nyc~B1b<1^% znbn)6?{(qX>TL^1Ilb*}F@-?Gjr{(r7d?3n?aupAQSj+(DjMqAX|>#NzIb?eA3%Wn zz~or7t}nW@kXU^@lTV?XT2!n)<$f))1Wj#s$)1LK#iv1_Tt#s6fgK z!@})na>!@_-tYRRrDAqKzEa$q1yS|B$UV7p{sNOITfEPKL$E2!{%J3J>~12qg8Ve> zv6;*1Fl8`y=e}}}d(1^M_yY=TPQz*29(U#F$j`xzjj%GjGaZm}yY70`^!d=E=R7qh zn*+sIMNP}hl54Xrd{ceRdQ6*EXD^N#|66M=oX%5a*vbJ&fGYm0NA;5PE8vxSdd*=g z3WkPx-nac8jZQL#WWBoIHJ;}7bbg5z?suv5MMi&5DJzH$!zUKSkW49CdGeks-MR+i zi$bzeyUlk+3K+Y#)Ny=_RK_WZKp?qTuw_5iZc{-@1q_WFYE$#VBm6eSrB`Pm`=cig zroJLRjF4ES2bhbu#dVd|*k`8FhMEH&t9?VIgf91O-cRdNb1d{ewIobr5i3K>PC-&C zvyPqMy^q{{N0lv>M|_@qciK@q6qcS!vTu$zPDLE)0O3$O%1TJa0RA{`FaR70|)k$hg&t$Fi0=UpG5@E!J zYq=-&2?Ng(*qGPRPJ8@eY|N5-KqTUX^%xGFN<>XrwfPwNqsr_@71mMle()YE3(KFZ zOoGDg!ONHCvTGdW>Po3xt;$tQyl+!>QYq5uc?=AUI(7O`|s%oi@syK7`nKd8GsSenU5K zd?sgK12CCkKDxs6n3NnT4vX2uS9b$eDsiveMuM9wL^Hk}pCohP))M|$X z=3d=7XsdSbF5gKC|9Uw5cDn@ zN-?ixGpZ^r+PsQY0Ph#)Cm8#k1razuS9mk~p?cb4)~?yd`e~->j*zjW(#HLMjMY(> zsT9jS?Cq4h5(wf}m)cL5xNKZx5l-|@;oVX5xlUg8>;;`gi^MlJs9Y>8pFpY%I{Me8 zeRG71L)WI6T{&FtFDwqf`z)Qgz}^QN`dqYoge|~i06=q5yi18s5D;V3TA!d}U?jwR ztN?pv(lc2VH|gkU`FOG9D=dws*&f=Do$k%aIlm>bgaf(Tp;_vfYqOX+sSHRN4C`3p|14_eerv}Wb>>Yg0r4E5 ziytv&_S@_oyye=y%8;LJ z)CfdpS?(Q+#1_jaoGoQ)Jyn+7XsPs{X^YoUK}%5Du2;L1xBlYMJ8N`5L<*m-I-8M!>-{X{wDukq=D@>nNqa`YIpNmc7=1h=f8`rv^wRm>f3`B? zbPn>Brt{)7rcM2l>aun7oUVuZAqEy!VxK~0xs5#C67qN4V?97NtUIR>LG<3WF=uGC zaqeELbY}&FqJ8$aD9FfeoW_T9nTtluj8s1_dD==T7e7Y-|8<0**S-{jT%0c?K?!H38Iqptx9}B5og%js^y(fy?Qd z6qhQ%4z`mhCbAoy#P0xnKezGDyph2uqPr(#1R!U03dZAKzOvpgoGH|DF)FU|5T{D) z%xJq&zI=QeggIgVgw3|xMSjS=P<8j-2m%2aAsj^#&{;&r9tCB?@rQ!LIIiO7M1uA; zF9+mR3L!W+=2a@|Zz)c71RZE6v&BxrTN3qoW&Nqlt1i%*UcsjJS3EY>ONl%_Nda8# zck|1tj83`HR~&S@*qfwOj7M9rHUWW zkF^`H?&k5A`yz4#t6xs_{D%^xN$eEp~0j6oR2-wJKV) z2A2@7b}nviZm<;+aD$L|3p+b~1Rm8+Eax-yFs_OhQxLf5&z2;g zo(@~rvx|*0ERGBYn6oQ*iWir>&35eNY@NGN37jT$SSC?6FMBBCs&~!cJ3552mfD(|j3ju>ouCr}d-K*(Sijn{C{AzX}{N zmS06>6CDYx^PE2%THMaRw`@26N2!+HbUGLn85)+3b_zZcL0j955KzJrRbY&qvniw9 zqNjOU z?gWtktAIO@33vLKiZ1jDhk(-;0IPDqDE^3@Ee<>iOvNV)U->JK#&-E8694Cj?JpX6 zGT-Ml-%Z5s76Ty=d;-W!-@f3hLm_`Ck2Yx<6JAyX`s9zcTih<%{PtC<0%js!X$b-< zf_5{=K7p&Hc_o_r?BlcT>EK>3cF}4vXX2}pgFZQKb-Xd1BtAh|zdnOsb4|xUN%?qM zJ^ytZ4qC#Y&Rb5Ao`I3q_Tgd6tzJktAll2W4JZ(7yunmo1vC{@m`SPJZ@m28p7MTP z`fFebh^qV}JqsxZ14TiE4;F&JYie$GJ~jx_x&PW~L_8in5a550bZFuYGBc&Lc!D z;r{RMcD;dr9=~ccl3-*2PVgg>(mh50Hog;tMiV4ZyQ(A2T9A#X`u?}q+(Airb;h<; z+Y(Q5WzO%1;NJIlK!rydoukhXE*ekmW&a9>z#!P=m2#_jVh&Hw$0-h+ zdCsd1!9I)X4Hwq#(Ndv-4PA-uoN?gk(7B#4Nj9ZNh58hgn9AwA$YS0iq(5zJiJC4p zg?VV{{JV8zYJIRkF;&^U@{MbRP+LKR5D%}yVjc=~tzh7*?B3qm_#WYxoM$9Hpsrl>b?_Ug~X=l+N&_;zKDYm)Zi zw8{Jq9$I|Chai`=DDrxauuk^-GA~DQ6^jDYT(f~o+t!W9$4>g9XYC5+bDqiDt>XUY zO`#~A3Wm@#Yl`fEzT9= zOVCI%W6SDFo0T|rY8jxj*R}DJNp$jUDiKs<^Vee(L;MCx z*W&-haREwBI8>6kDYNF&qW``LdCUP^vg>2l1`ux}@ho2V3Ocl%+}|Up2p+pdK}#;< z4fbtvd*6NX3jSDSdbNc+x@LPSW6_`kFc=s06=a5t<+e3Y8$R?lRRw@1ZN4ad@pDDS zTaJ)*S!pIUfj#p2jQpeVUgEUZ+~`#%0qutbNB&2KHcJ!HZAK{{W`Lxlrt6K2E