共享 CanvasWorld 统一超采样
默认以 2 倍内容分辨率渲染并保持 viewport.scale 的最终视觉比例 为 world 内 SVG 几何元素启用非缩放描边 补充超采样 transform 与 inverse-scale 契约测试
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user