Add draggable preview and keep 30 histories

This commit is contained in:
wuyanwanwu
2026-08-14 00:12:38 +08:00
parent 67683a9de1
commit 28dadddf71
14 changed files with 88 additions and 21 deletions
+4 -3
View File
@@ -76,9 +76,10 @@ input[type="range"] { accent-color: var(--green); }
.zoom-control button { width: 36px; height: 36px; border: 1px solid var(--line); background: white; cursor: pointer; font-size: 20px; }
.zoom-control input { width: 95px; }
.zoom-control output { width: 38px; color: var(--muted); font-size: 11px; text-align: right; }
.canvas-stage { min-height: 610px; flex: 1; overflow: auto; display: grid; place-items: center; padding: 36px; touch-action: pan-x pan-y; }
.canvas-wrap { width: max-content; line-height: 0; position: relative; border: 1px solid var(--ink); box-shadow: 10px 10px 0 rgba(32,39,36,.13); background: white; }
.canvas-wrap canvas { cursor: crosshair; image-rendering: pixelated; }
.canvas-stage { min-height: 610px; flex: 1; overflow: auto; display: block; padding: 36px; cursor: grab; touch-action: none; overscroll-behavior: contain; user-select: none; }
.canvas-stage.is-dragging { cursor: grabbing; }
.canvas-wrap { width: max-content; line-height: 0; position: relative; margin: auto; border: 1px solid var(--ink); box-shadow: 10px 10px 0 rgba(32,39,36,.13); background: white; }
.canvas-wrap canvas { cursor: inherit; image-rendering: pixelated; }
.pixel-tooltip { position: absolute; z-index: 5; width: max-content; max-width: 260px; padding: 9px 11px; display: grid; grid-template-columns: 14px auto; align-items: center; gap: 3px 8px; color: white; background: rgba(22, 30, 27, .94); border: 1px solid rgba(255,255,255,.2); box-shadow: 4px 4px 0 rgba(0,0,0,.2); border-radius: 3px; line-height: 1.35; pointer-events: none; }
.tooltip-swatch { width: 14px; height: 14px; grid-row: 1 / 3; border-radius: 50%; border: 1px solid rgba(255,255,255,.6); }
.pixel-tooltip strong { font-size: 11px; }
+68 -2
View File
@@ -156,8 +156,18 @@ export default function Home() {
const fileInputRef = useRef<HTMLInputElement | null>(null);
const cropCanvasRef = useRef<HTMLCanvasElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const patternStageRef = useRef<HTMLDivElement | null>(null);
const cropPreviewRef = useRef<HTMLDivElement | null>(null);
const cropDragRef = useRef<{ pointerId: number; x: number; y: number; cropX: number; cropY: number } | null>(null);
const patternDragRef = useRef<{
pointerId: number;
x: number;
y: number;
scrollLeft: number;
scrollTop: number;
moved: boolean;
} | null>(null);
const suppressCanvasClickRef = useRef(false);
useEffect(() => {
const saved = localStorage.getItem("bead-pattern-history");
@@ -362,7 +372,50 @@ export default function Home() {
});
};
const handlePatternPointerDown = (event: React.PointerEvent<HTMLCanvasElement>) => {
if (event.button !== 0) return;
const stage = patternStageRef.current;
if (!stage) return;
stage.setPointerCapture(event.pointerId);
patternDragRef.current = {
pointerId: event.pointerId,
x: event.clientX,
y: event.clientY,
scrollLeft: stage.scrollLeft,
scrollTop: stage.scrollTop,
moved: false,
};
};
const handlePatternPointerMove = (event: React.PointerEvent<HTMLCanvasElement>) => {
const stage = patternStageRef.current;
const drag = patternDragRef.current;
if (!stage || !drag || drag.pointerId !== event.pointerId) return;
const deltaX = event.clientX - drag.x;
const deltaY = event.clientY - drag.y;
if (!drag.moved && Math.hypot(deltaX, deltaY) < 5) return;
drag.moved = true;
stage.classList.add("is-dragging");
stage.scrollLeft = drag.scrollLeft - deltaX;
stage.scrollTop = drag.scrollTop - deltaY;
setHoveredPixel(null);
};
const handlePatternPointerUp = (event: React.PointerEvent<HTMLCanvasElement>) => {
const stage = patternStageRef.current;
const drag = patternDragRef.current;
if (!drag || drag.pointerId !== event.pointerId) return;
suppressCanvasClickRef.current = drag.moved;
patternDragRef.current = null;
stage?.classList.remove("is-dragging");
if (stage?.hasPointerCapture(event.pointerId)) stage.releasePointerCapture(event.pointerId);
};
const handleCanvasClick = (event: React.MouseEvent<HTMLCanvasElement>) => {
if (suppressCanvasClickRef.current) {
suppressCanvasClickRef.current = false;
return;
}
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
@@ -499,9 +552,22 @@ export default function Home() {
</div>
</div>
<div className="canvas-stage">
<div
className="canvas-stage"
ref={patternStageRef}
>
<div className="canvas-wrap">
<canvas ref={canvasRef} onClick={handleCanvasClick} onMouseMove={handleCanvasMove} onMouseLeave={() => setHoveredPixel(null)} aria-label="转换后的拼豆像素图,点击格子可选择颜色" />
<canvas
ref={canvasRef}
onPointerDown={handlePatternPointerDown}
onPointerMove={handlePatternPointerMove}
onPointerUp={handlePatternPointerUp}
onPointerCancel={handlePatternPointerUp}
onClick={handleCanvasClick}
onMouseMove={handleCanvasMove}
onMouseLeave={() => setHoveredPixel(null)}
aria-label="转换后的拼豆像素图,可拖动查看,轻点格子选择颜色"
/>
{hoveredPixel && <div className="pixel-tooltip" style={{ left: hoveredPixel.left, top: hoveredPixel.top }}>
<span className="tooltip-swatch" style={{ backgroundColor: hoveredPixel.color.hex }} />
<strong> {hoveredPixel.row} · {hoveredPixel.column} </strong>