diff --git a/packages/image-canvas-react/src/CanvasWorld.tsx b/packages/image-canvas-react/src/CanvasWorld.tsx
index 4dd6bd889..d5ab20e97 100644
--- a/packages/image-canvas-react/src/CanvasWorld.tsx
+++ b/packages/image-canvas-react/src/CanvasWorld.tsx
@@ -4,6 +4,8 @@ import {
} from '@genarrative/image-canvas-core';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
+const CANVAS_RENDER_SUPERSAMPLE = 2;
+
export function CanvasWorld({
children,
className = '',
@@ -28,12 +30,22 @@ export function CanvasWorld({
...style,
width: worldSize,
height: worldSize,
- transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewportScale})`,
+ transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewportScale / CANVAS_RENDER_SUPERSAMPLE})`,
'--genarrative-image-canvas-inverse-scale': String(1 / viewportScale),
} as CSSProperties
}
>
- {children}
+
+ {children}
+
);
}
diff --git a/packages/image-canvas-react/src/sharedCanvasReact.test.tsx b/packages/image-canvas-react/src/sharedCanvasReact.test.tsx
index 81304b756..1be34a166 100644
--- a/packages/image-canvas-react/src/sharedCanvasReact.test.tsx
+++ b/packages/image-canvas-react/src/sharedCanvasReact.test.tsx
@@ -1,10 +1,11 @@
/* @vitest-environment jsdom */
import type { CanvasLayer } from '@genarrative/image-canvas-core';
-import { act, renderHook } from '@testing-library/react';
+import { act, render, renderHook, screen } from '@testing-library/react';
import { createRef } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
+import { CanvasWorld } from './CanvasWorld';
import { useImageCanvasViewportControls } from './useImageCanvasViewportControls';
function layer(id: string, x: number): CanvasLayer {
@@ -52,6 +53,24 @@ afterEach(() => {
});
describe('shared canvas React lifecycle', () => {
+ it('renders a supersampled world while preserving the public viewport scale', () => {
+ render(
+
+ content
+ ,
+ );
+
+ const world = screen.getByText('content').parentElement?.parentElement;
+ const content = screen.getByText('content').parentElement;
+ expect(world?.style.width).toBe('12000px');
+ expect(world?.style.height).toBe('12000px');
+ expect(world?.style.transform).toBe('translate(12px, 24px) scale(1)');
+ expect(
+ world?.style.getPropertyValue('--genarrative-image-canvas-inverse-scale'),
+ ).toBe('0.5');
+ expect(content?.style.zoom).toBe('2');
+ });
+
it('removes every native listener across repeated mounts', () => {
const viewport = createViewport();
const addListener = vi.spyOn(viewport, 'addEventListener');
diff --git a/packages/image-canvas-react/src/styles.css b/packages/image-canvas-react/src/styles.css
index e9c0cbbd8..e696336b0 100644
--- a/packages/image-canvas-react/src/styles.css
+++ b/packages/image-canvas-react/src/styles.css
@@ -29,6 +29,16 @@
transform-origin: 0 0;
}
+.genarrative-image-canvas__world svg path,
+.genarrative-image-canvas__world svg line,
+.genarrative-image-canvas__world svg polyline,
+.genarrative-image-canvas__world svg polygon,
+.genarrative-image-canvas__world svg circle,
+.genarrative-image-canvas__world svg rect,
+.genarrative-image-canvas__world svg ellipse {
+ vector-effect: non-scaling-stroke;
+}
+
.genarrative-image-canvas__layer {
position: absolute;
display: block;