子画布展开改用真实坐标与视口承载

资源卡拖拽使用模型坐标换算,消除阻力和回弹

子画布空白区域支持画布平移,保持进入退出动画连贯
This commit is contained in:
2026-09-04 16:31:50 +08:00
parent 9c85a9501e
commit 069248b296
2 changed files with 96 additions and 75 deletions
+15
View File
@@ -5184,6 +5184,14 @@ iframe.preview-frame {
pointer-events: none;
}
.game-resource-book-scene--child {
pointer-events: auto;
}
.game-resource-book-scene--child.is-measuring {
pointer-events: none;
}
.game-resource-book-scene.is-measuring {
opacity: 0;
}
@@ -5197,6 +5205,7 @@ iframe.preview-frame {
#fffdfa;
content: '';
opacity: 0;
pointer-events: none;
transition: opacity 180ms ease;
}
@@ -5338,6 +5347,12 @@ iframe.preview-frame {
box-shadow 180ms ease;
}
.game-resource-book-scene--idle
.game-resource-book-scene-card
.game-resource-card {
transition: box-shadow 180ms ease;
}
.game-resource-book-scene--main
.game-resource-book-scene-card
.game-resource-card {
@@ -326,15 +326,6 @@ function defaultResourceCanvasViewports(): ResourceCanvasViewportBySortMode {
const RESOURCE_CANVAS_FALLBACK_WIDTH = 900;
const RESOURCE_CANVAS_FALLBACK_HEIGHT = 640;
const RESOURCE_CANVAS_MIN_DRAG_COORDINATE = -1_000_000;
const RESOURCE_CANVAS_MAX_DRAG_COORDINATE = 1_000_000;
function normalizeResourceCanvasDragCoordinate(value: number) {
return Math.min(
RESOURCE_CANVAS_MAX_DRAG_COORDINATE,
Math.max(RESOURCE_CANVAS_MIN_DRAG_COORDINATE, Math.round(value)),
);
}
function resourceCanvasViewportElement(element: HTMLElement | null) {
const pageCanvas = element?.querySelector<HTMLElement>(
@@ -979,9 +970,13 @@ function ResourceBookScene({
positions,
cardSizes,
visibleResourceIds,
sceneSize,
viewport,
renderCard,
onCollapse,
onPointerDown,
onPointerMove,
onPointerUp,
onPointerCancel,
}: {
resources: ProjectResource[];
visibleCategoryOrder: readonly ResourceCategory[];
@@ -992,7 +987,7 @@ function ResourceBookScene({
positions: Map<string, { x: number; y: number; section?: ResourceCategory }>;
cardSizes: Map<string, ResourceCanvasCardSize>;
visibleResourceIds: Set<string>;
sceneSize: { width: number; height: number };
viewport: CanvasViewport;
renderCard: (
resource: ProjectResource,
layout: {
@@ -1001,9 +996,15 @@ function ResourceBookScene({
width: number;
height: number;
rotation: number;
dragX: number;
dragY: number;
},
) => ReactNode;
onCollapse: () => void;
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
onPointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
onPointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
onPointerCancel: (event: ReactPointerEvent<HTMLDivElement>) => void;
}) {
const resourcesByCategory = new Map<ResourceCategory, ProjectResource[]>();
resources.forEach((resource) => {
@@ -1019,6 +1020,10 @@ function ResourceBookScene({
return (
<div
className={`game-resource-book-scene game-resource-book-scene--${resourceBookView} game-resource-book-scene--${resourceBookTransition}${overviewReady ? '' : ' is-measuring'}`}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerCancel}
>
{visibleCategoryOrder.map((category) => {
const rect = overviewRects.get(category);
@@ -1083,60 +1088,18 @@ function ResourceBookScene({
const overviewTop =
(rect?.top ?? 0) + 58 + Math.min(stackIndex, 2) * 5;
const overflow = stackIndex >= 3;
const positionedItems = categoryResources.map((item) => ({
resource: item,
position: positions.get(item.id),
size: cardSizes.get(item.id) ?? {
width: 180,
height: 128,
},
}));
const positioned = positionedItems.filter(
(item) => item.position,
) as Array<{
resource: ProjectResource;
position: { x: number; y: number };
size: ResourceCanvasCardSize;
}>;
const minX = positioned.length
? Math.min(...positioned.map((item) => item.position.x))
: 0;
const minY = positioned.length
? Math.min(...positioned.map((item) => item.position.y))
: 0;
const maxX = positioned.length
? Math.max(
...positioned.map(
(item) => item.position.x + item.size.width,
),
)
: 720;
const maxY = positioned.length
? Math.max(
...positioned.map(
(item) => item.position.y + item.size.height,
),
)
: 480;
const availableWidth = Math.max(320, sceneSize.width - 96);
const availableHeight = Math.max(240, sceneSize.height - 96);
const expandedScale = Math.min(
1,
availableWidth / Math.max(1, maxX - minX),
availableHeight / Math.max(1, maxY - minY),
);
const expandedLeft = position
? 48 + (position.x - minX) * expandedScale
: 48 + (stackIndex % 4) * 180;
const expandedTop = position
? 48 + (position.y - minY) * expandedScale
: 48 + Math.floor(stackIndex / 4) * 150;
const expanded = active;
const layout = {
x: expanded ? expandedLeft : overviewLeft,
y: expanded ? expandedTop : overviewTop,
width: expanded ? size.width * expandedScale : 92,
height: expanded ? size.height * expandedScale : 64,
x: expanded
? (position?.x ?? 0) * viewport.scale + viewport.x
: overviewLeft,
y: expanded
? (position?.y ?? 0) * viewport.scale + viewport.y
: overviewTop,
width: expanded ? size.width * viewport.scale : 92,
height: expanded ? size.height * viewport.scale : 64,
dragX: expanded ? (position?.x ?? 0) : 0,
dragY: expanded ? (position?.y ?? 0) : 0,
rotation: expanded
? 0
: (stackIndex % 2 ? -1 : 1) * (stackIndex + 0.5),
@@ -2443,6 +2406,15 @@ export default function ProjectDevelopmentView({
}
if (resourceBookView === 'child') {
const switchFitBounds = resourceCanvasContentBounds(
resourcePositionsByCategory.get(category) ?? [],
resourceCardSizeByResourceId,
);
const switchFitViewport = fitResourceCanvasViewportToContent({
bounds: switchFitBounds,
canvasSize: resourceBookSceneSize,
padding: RESOURCE_CANVAS_FIT_PADDING,
});
setResourceBookTransition('returning');
scheduleResourceBookTransition(() => {
setActiveResourceCategory(category);
@@ -2450,6 +2422,7 @@ export default function ProjectDevelopmentView({
setResourceBookView('main');
setResourceBookTransition('idle');
window.requestAnimationFrame(() => {
setResourceCanvasViewport(category, switchFitViewport);
setResourceBookView('child');
setResourceBookTransition('entering');
scheduleResourceBookTransition(() => {
@@ -2460,6 +2433,18 @@ export default function ProjectDevelopmentView({
return;
}
const fitBounds = resourceCanvasContentBounds(
resourcePositionsByCategory.get(category) ?? [],
resourceCardSizeByResourceId,
);
setResourceCanvasViewport(
category,
fitResourceCanvasViewportToContent({
bounds: fitBounds,
canvasSize: resourceBookSceneSize,
padding: RESOURCE_CANVAS_FIT_PADDING,
}),
);
setActiveResourceCategory(category);
setResourceBookCategory(category);
setResourceBookView('child');
@@ -2474,8 +2459,12 @@ export default function ProjectDevelopmentView({
cancelResourceCardDrag,
resourceBookCategory,
resourceBookManagerRef,
resourceBookSceneSize,
resourceBookTransition,
resourceBookView,
resourceCardSizeByResourceId,
resourcePositionsByCategory,
setResourceCanvasViewport,
scheduleResourceBookTransition,
],
);
@@ -3497,7 +3486,7 @@ export default function ProjectDevelopmentView({
}
drag.changed = true;
const coordinate = (start: number, delta: number) =>
normalizeResourceCanvasDragCoordinate(start + delta / drag.startScale);
start + delta / drag.startScale;
const preview = {
resourceId: drag.resourceId,
x: coordinate(drag.startX, deltaX),
@@ -3528,14 +3517,12 @@ export default function ProjectDevelopmentView({
const preview = drag.changed
? {
resourceId: drag.resourceId,
x: normalizeResourceCanvasDragCoordinate(
x:
drag.startX +
(event.clientX - drag.startClientX) / drag.startScale,
),
y: normalizeResourceCanvasDragCoordinate(
(event.clientX - drag.startClientX) / drag.startScale,
y:
drag.startY +
(event.clientY - drag.startClientY) / drag.startScale,
),
(event.clientY - drag.startClientY) / drag.startScale,
}
: null;
setResourceCardDragPreview(null);
@@ -3577,7 +3564,7 @@ export default function ProjectDevelopmentView({
const target = event.target as HTMLElement;
if (
target.closest(
'button, input, textarea, select, a, .game-resource-card',
'button, input, textarea, select, a, .game-resource-card, .game-resource-book-scene-titlebar',
)
) {
return;
@@ -4780,6 +4767,8 @@ export default function ProjectDevelopmentView({
width: number;
height: number;
rotation: number;
dragX: number;
dragY: number;
},
) => {
const previewIdentity = resourceCardPreviews.identityByResourceId.get(
@@ -4792,6 +4781,13 @@ export default function ProjectDevelopmentView({
resourceCardDragPreview?.resourceId === resource.id
? resourceCardDragPreview
: null;
const viewport = activeResourceCanvasViewport;
const previewX = dragPreview
? dragPreview.x * viewport.scale + viewport.x
: undefined;
const previewY = dragPreview
? dragPreview.y * viewport.scale + viewport.y
: undefined;
return (
<ResourceCard
resource={resource}
@@ -4801,8 +4797,8 @@ export default function ProjectDevelopmentView({
? 'version-binding'
: null
}
x={dragPreview?.x ?? layout.x}
y={dragPreview?.y ?? layout.y}
x={previewX ?? layout.x}
y={previewY ?? layout.y}
rotation={layout.rotation}
dragging={dragPreview !== null}
cardSize={{ width: layout.width, height: layout.height }}
@@ -4815,7 +4811,12 @@ export default function ProjectDevelopmentView({
onSelect={handleResourceSelect}
onOpenEditor={handleResourceCardOpenEditor}
onPointerDown={(event) =>
handleResourceCardPointerDown(event, resource, layout.x, layout.y)
handleResourceCardPointerDown(
event,
resource,
layout.dragX,
layout.dragY,
)
}
onPointerMove={handleResourceCardPointerMove}
onPointerUp={handleResourceCardPointerUp}
@@ -4831,6 +4832,7 @@ export default function ProjectDevelopmentView({
},
[
activeCardMediaIdentity,
activeResourceCanvasViewport,
handleCardMediaPlaybackChange,
handleCardMediaReady,
handleCardMediaToggle,
@@ -5379,9 +5381,13 @@ export default function ProjectDevelopmentView({
positions={resourcePositionById}
cardSizes={resourceCardSizeByResourceId}
visibleResourceIds={visibleResourceIds}
sceneSize={resourceBookSceneSize}
viewport={activeResourceCanvasViewport}
renderCard={renderResourceBookCard}
onCollapse={returnToResourceBookMain}
onPointerDown={handleResourceCanvasPointerDown}
onPointerMove={handleResourceCanvasPointerMove}
onPointerUp={stopResourceCanvasPan}
onPointerCancel={stopResourceCanvasPan}
/>
<label className="game-resource-search">
<Search size={16} aria-hidden="true" />