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: {