diff --git a/examples/assets/svg-icons/cast.svg b/examples/assets/svg-icons/cast.svg new file mode 100644 index 0000000..afc4b4f --- /dev/null +++ b/examples/assets/svg-icons/cast.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/forward_10.svg b/examples/assets/svg-icons/forward_10.svg new file mode 100644 index 0000000..24e1708 --- /dev/null +++ b/examples/assets/svg-icons/forward_10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/fullscreen.svg b/examples/assets/svg-icons/fullscreen.svg new file mode 100644 index 0000000..e5eb6a2 --- /dev/null +++ b/examples/assets/svg-icons/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/home.svg b/examples/assets/svg-icons/home.svg new file mode 100644 index 0000000..6bd84cf --- /dev/null +++ b/examples/assets/svg-icons/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/info.svg b/examples/assets/svg-icons/info.svg new file mode 100644 index 0000000..22ef137 --- /dev/null +++ b/examples/assets/svg-icons/info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/live_tv.svg b/examples/assets/svg-icons/live_tv.svg new file mode 100644 index 0000000..fa41bd8 --- /dev/null +++ b/examples/assets/svg-icons/live_tv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/movie.svg b/examples/assets/svg-icons/movie.svg new file mode 100644 index 0000000..632ecd4 --- /dev/null +++ b/examples/assets/svg-icons/movie.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/pause.svg b/examples/assets/svg-icons/pause.svg new file mode 100644 index 0000000..c20e156 --- /dev/null +++ b/examples/assets/svg-icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/play_arrow.svg b/examples/assets/svg-icons/play_arrow.svg new file mode 100644 index 0000000..c5e1a4c --- /dev/null +++ b/examples/assets/svg-icons/play_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/replay_10.svg b/examples/assets/svg-icons/replay_10.svg new file mode 100644 index 0000000..db36103 --- /dev/null +++ b/examples/assets/svg-icons/replay_10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/search.svg b/examples/assets/svg-icons/search.svg new file mode 100644 index 0000000..cd9fd53 --- /dev/null +++ b/examples/assets/svg-icons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/settings.svg b/examples/assets/svg-icons/settings.svg new file mode 100644 index 0000000..f255a58 --- /dev/null +++ b/examples/assets/svg-icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/skip_next.svg b/examples/assets/svg-icons/skip_next.svg new file mode 100644 index 0000000..150159d --- /dev/null +++ b/examples/assets/svg-icons/skip_next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/skip_previous.svg b/examples/assets/svg-icons/skip_previous.svg new file mode 100644 index 0000000..0d2ec25 --- /dev/null +++ b/examples/assets/svg-icons/skip_previous.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/subtitles.svg b/examples/assets/svg-icons/subtitles.svg new file mode 100644 index 0000000..ea10bfd --- /dev/null +++ b/examples/assets/svg-icons/subtitles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/svg-icons/volume_up.svg b/examples/assets/svg-icons/volume_up.svg new file mode 100644 index 0000000..958838a --- /dev/null +++ b/examples/assets/svg-icons/volume_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tests/svg-icons.ts b/examples/tests/svg-icons.ts new file mode 100644 index 0000000..8d04c7d --- /dev/null +++ b/examples/tests/svg-icons.ts @@ -0,0 +1,284 @@ +import type { ExampleSettings } from '../common/ExampleSettings.js'; + +import playArrow from '../assets/svg-icons/play_arrow.svg'; +import pause from '../assets/svg-icons/pause.svg'; +import skipNext from '../assets/svg-icons/skip_next.svg'; +import skipPrevious from '../assets/svg-icons/skip_previous.svg'; +import replay10 from '../assets/svg-icons/replay_10.svg'; +import forward10 from '../assets/svg-icons/forward_10.svg'; +import volumeUp from '../assets/svg-icons/volume_up.svg'; +import fullscreen from '../assets/svg-icons/fullscreen.svg'; +import subtitles from '../assets/svg-icons/subtitles.svg'; +import settings from '../assets/svg-icons/settings.svg'; +import cast from '../assets/svg-icons/cast.svg'; +import info from '../assets/svg-icons/info.svg'; +import liveTv from '../assets/svg-icons/live_tv.svg'; +import movie from '../assets/svg-icons/movie.svg'; +import home from '../assets/svg-icons/home.svg'; +import search from '../assets/svg-icons/search.svg'; + +import rocko2 from '../assets/rocko2.svg'; + +/** + * Visual regression test for SVG loading. + * + * Exercises: + * - SVGs rasterized at their natural intrinsic size (24x24 MDI icons) + * - SVGs upscaled 4x and 16x to test DPR-aware rasterization — without + * DPR scaling these are visibly soft/blurry on HiDPI displays + * - Source-region crop via srcX/srcY/srcWidth/srcHeight + * - Cross-origin SVG load from jsdelivr (verifies img.crossOrigin path) + */ + +const ICONS: Array<{ src: string; name: string }> = [ + { src: playArrow, name: 'play_arrow' }, + { src: pause, name: 'pause' }, + { src: skipPrevious, name: 'skip_prev' }, + { src: skipNext, name: 'skip_next' }, + { src: replay10, name: 'replay_10' }, + { src: forward10, name: 'forward_10' }, + { src: volumeUp, name: 'volume_up' }, + { src: fullscreen, name: 'fullscreen' }, + { src: subtitles, name: 'subtitles' }, + { src: settings, name: 'settings' }, + { src: cast, name: 'cast' }, + { src: info, name: 'info' }, + { src: liveTv, name: 'live_tv' }, + { src: movie, name: 'movie' }, + { src: home, name: 'home' }, + { src: search, name: 'search' }, +]; + +const CROSS_ORIGIN_SVG = + 'https://cdn.jsdelivr.net/npm/@material-design-icons/svg@latest/filled/favorite.svg'; + +function waitForLoaded( + node: { once: (event: string, cb: () => void) => void }, + timeoutMs = 3000, +): Promise { + return new Promise((resolve) => { + const timer = setTimeout(() => resolve(false), timeoutMs); + node.once('loaded', () => { + clearTimeout(timer); + resolve(true); + }); + node.once('failed', () => { + clearTimeout(timer); + resolve(false); + }); + }); +} + +export async function automation(settings: ExampleSettings) { + await test(settings); + await settings.snapshot(); +} + +export default async function test({ renderer, testRoot }: ExampleSettings) { + // Light background panel so the black MDI icons are visible + renderer.createNode({ + x: 0, + y: 0, + w: 1920, + h: 1080, + color: 0xf5f5f5ff, + parent: testRoot, + }); + + renderer.createTextNode({ + x: 30, + y: 20, + text: 'SVG Icons — DPR sharpness, crop, cross-origin', + fontFamily: 'Ubuntu', + fontSize: 36, + color: 0x202020ff, + parent: testRoot, + }); + + // Row 1: natural size (24x24) + renderer.createTextNode({ + x: 30, + y: 80, + text: '1. Natural size (24×24)', + fontFamily: 'Ubuntu', + fontSize: 22, + color: 0x404040ff, + parent: testRoot, + }); + + const ICON_NATURAL = 24; + const ICON_NATURAL_GAP = 80; + let i = 0; + for (const icon of ICONS) { + renderer.createNode({ + x: 30 + i * ICON_NATURAL_GAP, + y: 120, + w: ICON_NATURAL, + h: ICON_NATURAL, + src: icon.src, + parent: testRoot, + }); + i++; + } + + // Row 2: 4x upscale (96x96) — DPR sharpness test + renderer.createTextNode({ + x: 30, + y: 170, + text: '2. Upscaled 4× (96×96) — should be crisp with DPR-aware raster', + fontFamily: 'Ubuntu', + fontSize: 22, + color: 0x404040ff, + parent: testRoot, + }); + + const ICON_LARGE = 96; + const ICON_LARGE_GAP = 110; + i = 0; + for (const icon of ICONS) { + renderer.createNode({ + x: 30 + i * ICON_LARGE_GAP, + y: 210, + w: ICON_LARGE, + h: ICON_LARGE, + src: icon.src, + parent: testRoot, + }); + i++; + } + + // Row 3: extreme upscale (400x400) — most visible DPR test + renderer.createTextNode({ + x: 30, + y: 330, + text: '3. Extreme upscale (400×400) — sharpness at high zoom', + fontFamily: 'Ubuntu', + fontSize: 22, + color: 0x404040ff, + parent: testRoot, + }); + + renderer.createNode({ + x: 30, + y: 370, + w: 400, + h: 400, + src: playArrow, + parent: testRoot, + }); + + renderer.createNode({ + x: 460, + y: 370, + w: 400, + h: 400, + src: liveTv, + parent: testRoot, + }); + + renderer.createNode({ + x: 890, + y: 370, + w: 400, + h: 400, + src: search, + parent: testRoot, + }); + + // Row 4: source-region crop (sx/sy/sw/sh) + renderer.createTextNode({ + x: 30, + y: 790, + text: '4. Source-region crop (srcX/srcY/srcWidth/srcHeight on a larger SVG)', + fontFamily: 'Ubuntu', + fontSize: 22, + color: 0x404040ff, + parent: testRoot, + }); + + // Full rocko2 for reference (181x218) + renderer.createNode({ + x: 30, + y: 830, + w: 181, + h: 218, + src: rocko2, + parent: testRoot, + }); + + // Left half crop + renderer.createNode({ + x: 240, + y: 830, + w: 90, + h: 218, + src: rocko2, + srcX: 0, + srcY: 0, + srcWidth: 90, + srcHeight: 218, + parent: testRoot, + }); + + // Right half crop + renderer.createNode({ + x: 360, + y: 830, + w: 91, + h: 218, + src: rocko2, + srcX: 90, + srcY: 0, + srcWidth: 91, + srcHeight: 218, + parent: testRoot, + }); + + // Centered crop stretched to 2x + renderer.createNode({ + x: 480, + y: 830, + w: 200, + h: 218, + src: rocko2, + srcX: 40, + srcY: 0, + srcWidth: 100, + srcHeight: 218, + parent: testRoot, + }); + + // Row 5: cross-origin SVG load (jsdelivr CDN) + renderer.createTextNode({ + x: 720, + y: 790, + text: '5. Cross-origin SVG (jsdelivr CDN)', + fontFamily: 'Ubuntu', + fontSize: 22, + color: 0x404040ff, + parent: testRoot, + }); + + const crossOrigin = renderer.createNode({ + x: 720, + y: 830, + w: 200, + h: 200, + src: CROSS_ORIGIN_SVG, + parent: testRoot, + }); + + const crossOriginStatus = renderer.createTextNode({ + x: 940, + y: 870, + text: 'loading…', + fontFamily: 'Ubuntu', + fontSize: 22, + color: 0x404040ff, + parent: testRoot, + }); + + const ok = await waitForLoaded(crossOrigin); + crossOriginStatus.text = ok ? 'cross-origin: OK' : 'cross-origin: FAILED'; + crossOriginStatus.color = ok ? 0x008800ff : 0xcc0000ff; +} diff --git a/examples/vite.config.ts b/examples/vite.config.ts index 0b8dc6b..49640ec 100644 --- a/examples/vite.config.ts +++ b/examples/vite.config.ts @@ -60,6 +60,11 @@ export default defineConfig(({ command, mode, isSsrBuild }) => { headers: { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', + // Required under COEP=require-corp: without CORP, no-CORS subresource + // requests (e.g. without a crossorigin attribute) are blocked + // even when same-origin. 'cross-origin' matches the existing + // Access-Control-Allow-Origin: * the dev server already sends. + 'Cross-Origin-Resource-Policy': 'cross-origin', }, }, define: {