From cfbc6c36a519d74a1970a177f09a8f4065995bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 5 Sep 2026 11:10:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=B1=E4=BA=AB=20CanvasWorld=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=B6=85=E9=87=87=E6=A0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 默认以 2 倍内容分辨率渲染并保持 viewport.scale 的最终视觉比例 为 world 内 SVG 几何元素启用非缩放描边 补充超采样 transform 与 inverse-scale 契约测试 --- .../image-canvas-react/src/CanvasWorld.tsx | 16 ++++++++++++-- .../src/sharedCanvasReact.test.tsx | 21 ++++++++++++++++++- packages/image-canvas-react/src/styles.css | 10 +++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) 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;