共享 CanvasWorld 统一超采样
Project CI / Frontend tests (pull_request) Failing after 2m16s
Project CI / Repository checks (pull_request) Successful in 2m45s
Project CI / Native shell tests (pull_request) Failing after 4m47s
Project CI / Backend tests (pull_request) Successful in 5m58s

默认以 2 倍内容分辨率渲染并保持 viewport.scale 的最终视觉比例

为 world 内 SVG 几何元素启用非缩放描边

补充超采样 transform 与 inverse-scale 契约测试
This commit is contained in:
2026-09-05 11:10:33 +08:00
parent 4e7b4c3e2c
commit cfbc6c36a5
3 changed files with 44 additions and 3 deletions
@@ -4,6 +4,8 @@ import {
} from '@genarrative/image-canvas-core'; } from '@genarrative/image-canvas-core';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'; import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
const CANVAS_RENDER_SUPERSAMPLE = 2;
export function CanvasWorld({ export function CanvasWorld({
children, children,
className = '', className = '',
@@ -28,12 +30,22 @@ export function CanvasWorld({
...style, ...style,
width: worldSize, width: worldSize,
height: 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), '--genarrative-image-canvas-inverse-scale': String(1 / viewportScale),
} as CSSProperties } as CSSProperties
} }
> >
{children} <div
className="genarrative-image-canvas__world-content"
style={{
position: 'relative',
width: worldSize,
height: worldSize,
zoom: CANVAS_RENDER_SUPERSAMPLE,
}}
>
{children}
</div>
</div> </div>
); );
} }
@@ -1,10 +1,11 @@
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
import type { CanvasLayer } from '@genarrative/image-canvas-core'; 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 { createRef } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { CanvasWorld } from './CanvasWorld';
import { useImageCanvasViewportControls } from './useImageCanvasViewportControls'; import { useImageCanvasViewportControls } from './useImageCanvasViewportControls';
function layer(id: string, x: number): CanvasLayer { function layer(id: string, x: number): CanvasLayer {
@@ -52,6 +53,24 @@ afterEach(() => {
}); });
describe('shared canvas React lifecycle', () => { describe('shared canvas React lifecycle', () => {
it('renders a supersampled world while preserving the public viewport scale', () => {
render(
<CanvasWorld viewport={{ x: 12, y: 24, scale: 2 }}>
<span>content</span>
</CanvasWorld>,
);
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', () => { it('removes every native listener across repeated mounts', () => {
const viewport = createViewport(); const viewport = createViewport();
const addListener = vi.spyOn(viewport, 'addEventListener'); const addListener = vi.spyOn(viewport, 'addEventListener');
@@ -29,6 +29,16 @@
transform-origin: 0 0; 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 { .genarrative-image-canvas__layer {
position: absolute; position: absolute;
display: block; display: block;