修复子画布缩放并支持Ctrl滚轮与右下角缩放控件
美术资源初始适配不再放大过头 scene层支持Ctrl滚轮锚点缩放,普通滚轮保留翻页
This commit is contained in:
@@ -5192,6 +5192,60 @@ iframe.preview-frame {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.game-resource-book-zoom {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
z-index: 40;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 34px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid #ecdcd4;
|
||||
border-radius: 12px;
|
||||
background: rgb(255 253 250 / 94%);
|
||||
box-shadow: 0 6px 18px rgb(112 70 52 / 12%);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.game-resource-book-zoom-button,
|
||||
.game-resource-book-zoom-fit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #8c6252;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.game-resource-book-zoom-button:hover,
|
||||
.game-resource-book-zoom-button:focus-visible,
|
||||
.game-resource-book-zoom-fit:hover,
|
||||
.game-resource-book-zoom-fit:focus-visible {
|
||||
background: #f7ebe4;
|
||||
color: #b5582f;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.game-resource-book-zoom-value {
|
||||
min-width: 42px;
|
||||
color: #6d4b3f;
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.game-resource-book-zoom-fit {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.game-resource-book-scene.is-measuring {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ImageCanvasHostScope } from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
type CanvasViewport,
|
||||
MAX_SCALE,
|
||||
MIN_SCALE,
|
||||
resolveViewportFromWheel,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
@@ -11,9 +13,12 @@ import {
|
||||
Image,
|
||||
Info,
|
||||
ListFilter,
|
||||
Maximize2,
|
||||
Minus,
|
||||
Music2,
|
||||
Pause,
|
||||
Play,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Search,
|
||||
Settings2,
|
||||
@@ -36,6 +41,7 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type WheelEvent as ReactWheelEvent,
|
||||
} from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -326,6 +332,7 @@ function defaultResourceCanvasViewports(): ResourceCanvasViewportBySortMode {
|
||||
|
||||
const RESOURCE_CANVAS_FALLBACK_WIDTH = 900;
|
||||
const RESOURCE_CANVAS_FALLBACK_HEIGHT = 640;
|
||||
const RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE = 1;
|
||||
|
||||
function resourceCanvasViewportElement(element: HTMLElement | null) {
|
||||
const pageCanvas = element?.querySelector<HTMLElement>(
|
||||
@@ -977,6 +984,11 @@ function ResourceBookScene({
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
onPointerCancel,
|
||||
onWheel,
|
||||
zoomPercent,
|
||||
onZoomOut,
|
||||
onZoomIn,
|
||||
onZoomFit,
|
||||
}: {
|
||||
resources: ProjectResource[];
|
||||
visibleCategoryOrder: readonly ResourceCategory[];
|
||||
@@ -1005,6 +1017,11 @@ function ResourceBookScene({
|
||||
onPointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onPointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onPointerCancel: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onWheel: (event: ReactWheelEvent<HTMLDivElement>) => void;
|
||||
zoomPercent: number;
|
||||
onZoomOut: () => void;
|
||||
onZoomIn: () => void;
|
||||
onZoomFit: () => void;
|
||||
}) {
|
||||
const resourcesByCategory = new Map<ResourceCategory, ProjectResource[]>();
|
||||
resources.forEach((resource) => {
|
||||
@@ -1024,7 +1041,42 @@ function ResourceBookScene({
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerCancel}
|
||||
onWheel={onWheel}
|
||||
>
|
||||
{resourceBookView === 'child' ? (
|
||||
<div className="game-resource-book-zoom">
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-book-zoom-button"
|
||||
aria-label="缩小画布"
|
||||
title="缩小画布"
|
||||
onClick={onZoomOut}
|
||||
>
|
||||
<Minus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<span className="game-resource-book-zoom-value" aria-live="polite">
|
||||
{Math.round(zoomPercent)}%
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-book-zoom-button"
|
||||
aria-label="放大画布"
|
||||
title="放大画布"
|
||||
onClick={onZoomIn}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-book-zoom-fit"
|
||||
aria-label="将画布调整到适应内容大小"
|
||||
title="适应内容大小"
|
||||
onClick={onZoomFit}
|
||||
>
|
||||
<Maximize2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{visibleCategoryOrder.map((category) => {
|
||||
const rect = overviewRects.get(category);
|
||||
const categoryResources = resourcesByCategory.get(category) ?? [];
|
||||
@@ -2414,6 +2466,7 @@ export default function ProjectDevelopmentView({
|
||||
bounds: switchFitBounds,
|
||||
canvasSize: resourceBookSceneSize,
|
||||
padding: RESOURCE_CANVAS_FIT_PADDING,
|
||||
maxScale: RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE,
|
||||
});
|
||||
setResourceBookTransition('returning');
|
||||
scheduleResourceBookTransition(() => {
|
||||
@@ -2443,6 +2496,7 @@ export default function ProjectDevelopmentView({
|
||||
bounds: fitBounds,
|
||||
canvasSize: resourceBookSceneSize,
|
||||
padding: RESOURCE_CANVAS_FIT_PADDING,
|
||||
maxScale: RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE,
|
||||
}),
|
||||
);
|
||||
setActiveResourceCategory(category);
|
||||
@@ -3419,10 +3473,109 @@ export default function ProjectDevelopmentView({
|
||||
bounds: resourceCanvasFitBounds,
|
||||
canvasSize,
|
||||
padding: RESOURCE_CANVAS_FIT_PADDING,
|
||||
maxScale: RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE,
|
||||
}),
|
||||
);
|
||||
}, [activePageCategory, resourceCanvasFitBounds, setResourceCanvasViewport]);
|
||||
|
||||
const fitResourceBookScene = useCallback(() => {
|
||||
if (!activePageCategory) {
|
||||
return;
|
||||
}
|
||||
setResourceCanvasViewport(
|
||||
activePageCategory,
|
||||
fitResourceCanvasViewportToContent({
|
||||
bounds: resourceCanvasFitBounds,
|
||||
canvasSize: resourceBookSceneSize,
|
||||
padding: RESOURCE_CANVAS_FIT_PADDING,
|
||||
maxScale: RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE,
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
activePageCategory,
|
||||
resourceBookSceneSize,
|
||||
resourceCanvasFitBounds,
|
||||
setResourceCanvasViewport,
|
||||
]);
|
||||
|
||||
const zoomResourceBookSceneBy = useCallback(
|
||||
(factor: number) => {
|
||||
const category = activePageCategory;
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
const viewport = resourceCanvasViewportRef.current;
|
||||
const scale = Math.min(
|
||||
MAX_SCALE,
|
||||
Math.max(MIN_SCALE, viewport.scale * factor),
|
||||
);
|
||||
if (scale === viewport.scale) {
|
||||
return;
|
||||
}
|
||||
const sceneElement = resourceBookManagerRef.current;
|
||||
const rect = sceneElement?.getBoundingClientRect();
|
||||
const anchorX = rect ? rect.width / 2 : 0;
|
||||
const anchorY = rect ? rect.height / 2 : 0;
|
||||
const ratio = scale / viewport.scale;
|
||||
setResourceCanvasViewport(category, {
|
||||
scale,
|
||||
x: anchorX - (anchorX - viewport.x) * ratio,
|
||||
y: anchorY - (anchorY - viewport.y) * ratio,
|
||||
});
|
||||
},
|
||||
[activePageCategory, setResourceCanvasViewport],
|
||||
);
|
||||
|
||||
const handleResourceBookSceneWheel = useCallback(
|
||||
(event: ReactWheelEvent<HTMLDivElement>) => {
|
||||
const category = activePageCategory;
|
||||
const sceneElement = resourceBookManagerRef.current;
|
||||
if (!category || !sceneElement) {
|
||||
return;
|
||||
}
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
const pageCanvas =
|
||||
resourceCanvasRef.current?.querySelector<HTMLElement>(
|
||||
'[data-resource-section-scroll]',
|
||||
);
|
||||
if (pageCanvas) {
|
||||
pageCanvas.dispatchEvent(
|
||||
new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
deltaX: event.deltaX,
|
||||
deltaY: event.deltaY,
|
||||
deltaMode: event.deltaMode,
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
shiftKey: event.shiftKey,
|
||||
}),
|
||||
);
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const rect = sceneElement.getBoundingClientRect();
|
||||
const result = resolveViewportFromWheel({
|
||||
viewport: resourceCanvasViewportRef.current,
|
||||
deltaX: event.deltaX,
|
||||
deltaY: event.deltaY,
|
||||
shiftKey: event.shiftKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
screenPoint: {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
},
|
||||
});
|
||||
setResourceCanvasViewport(category, result.viewport, true);
|
||||
},
|
||||
[activePageCategory, setResourceCanvasViewport],
|
||||
);
|
||||
|
||||
const handleResourceCardPointerDown = useCallback(
|
||||
(
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
@@ -3564,7 +3717,7 @@ export default function ProjectDevelopmentView({
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
target.closest(
|
||||
'button, input, textarea, select, a, .game-resource-card, .game-resource-book-scene-titlebar',
|
||||
'button, input, textarea, select, a, .game-resource-card, .game-resource-book-scene-titlebar, .game-resource-book-zoom',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -5388,6 +5541,11 @@ export default function ProjectDevelopmentView({
|
||||
onPointerMove={handleResourceCanvasPointerMove}
|
||||
onPointerUp={stopResourceCanvasPan}
|
||||
onPointerCancel={stopResourceCanvasPan}
|
||||
onWheel={handleResourceBookSceneWheel}
|
||||
zoomPercent={activeResourceCanvasViewport.scale * 100}
|
||||
onZoomOut={() => zoomResourceBookSceneBy(1 / 1.2)}
|
||||
onZoomIn={() => zoomResourceBookSceneBy(1.2)}
|
||||
onZoomFit={fitResourceBookScene}
|
||||
/>
|
||||
<label className="game-resource-search">
|
||||
<Search size={16} aria-hidden="true" />
|
||||
|
||||
Reference in New Issue
Block a user