Replace crop zoom with image selection dialog

This commit is contained in:
wuyanwanwu
2026-08-14 02:04:36 +08:00
parent e028ecc9c8
commit d53f8f4585
15 changed files with 214 additions and 73 deletions
+34 -4
View File
@@ -48,12 +48,10 @@ button { color: inherit; }
.upload-card { width: 100%; min-height: 100px; max-height: 260px; display: block; position: relative; overflow: hidden; border: 1px dashed #a5a59e; background: #ebe7dd; }
.upload-card canvas { width: 100%; height: 100%; display: block; pointer-events: none; }
.upload-card .file-input { display: none; }
.crop-preview { cursor: grab; touch-action: none; }
.crop-preview:active { cursor: grabbing; }
.crop-frame { position: absolute; inset: 0; border: 2px solid rgba(255,255,255,.85); box-shadow: inset 0 0 0 1px rgba(20,30,25,.22); pointer-events: none; }
.crop-preview { cursor: default; }
.upload-overlay { position: absolute; z-index: 3; right: 10px; bottom: 10px; padding: 7px 10px; border: 0; color: white; background: rgba(22, 75, 57, .9); cursor: pointer; font-size: 12px; }
.file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 9px 0 24px; color: var(--muted); font-size: 11px; }
.crop-zoom { margin-top: 0; }
.crop-select-button { width: 100%; margin-top: 0; padding: 10px; border: 1px solid var(--ink); background: var(--lime); cursor: pointer; font-size: 12px; font-weight: 700; }
.reset-crop { width: 100%; margin: 7px 0 20px; padding: 7px; border: 1px solid var(--line); background: white; cursor: pointer; color: var(--muted); font-size: 11px; }
.field-row { display: grid; grid-template-columns: 1fr auto 1fr; align-items: end; gap: 8px; }
.field-row label, .field { display: flex; flex-direction: column; gap: 8px; }
@@ -140,6 +138,30 @@ input[type="range"] { accent-color: var(--green); }
.history-actions button:first-child { background: var(--lime); font-weight: 700; }
.history-empty { margin-top: 18px; padding: 44px 20px; color: var(--muted); text-align: center; border: 1px dashed var(--line); background: rgba(255,253,248,.5); font-size: 13px; }
.crop-dialog-backdrop { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 24px; background: rgba(17,24,21,.76); backdrop-filter: blur(5px); }
.crop-dialog { width: min(1280px, 96vw); max-height: 94vh; display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--ink); background: var(--card); box-shadow: 10px 10px 0 rgba(0,0,0,.32); }
.crop-dialog-header { flex: 0 0 auto; padding: 18px 22px; display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; border-bottom: 1px solid var(--line); }
.crop-dialog-header .section-kicker { margin-bottom: 5px; }
.crop-dialog-header h2 { margin: 0; font-size: 22px; }
.crop-dialog-header p:not(.section-kicker) { margin: 6px 0 0; color: var(--muted); font-size: 12px; }
.crop-dialog-header > button { width: 38px; height: 38px; flex: 0 0 auto; border: 1px solid var(--line); background: white; cursor: pointer; font-size: 25px; line-height: 1; }
.crop-dialog-scroll { min-height: 0; flex: 1 1 auto; overflow: auto; padding: 18px; display: grid; place-items: center; background-color: #e9e5db; background-image: radial-gradient(#c8c3b8 .7px, transparent .7px); background-size: 12px 12px; }
.crop-dialog-stage { position: relative; width: max-content; max-width: 100%; line-height: 0; cursor: crosshair; touch-action: none; user-select: none; box-shadow: 0 0 0 1px var(--ink); }
.crop-dialog-stage canvas { display: block; max-width: 100%; height: auto; pointer-events: none; }
.crop-selection-mask { position: absolute; z-index: 1; background: rgba(10,15,13,.55); pointer-events: none; }
.crop-selection-top { inset: 0 0 auto; }
.crop-selection-left { left: 0; }
.crop-selection-bottom { inset: auto 0 0; bottom: 0; }
.crop-selection-box { position: absolute; z-index: 2; border: 2px solid white; outline: 1px solid rgba(0,0,0,.75); box-shadow: inset 0 0 0 1px rgba(0,0,0,.18); pointer-events: none; }
.crop-selection-box::before, .crop-selection-box::after { content: ""; position: absolute; background: rgba(255,255,255,.62); }
.crop-selection-box::before { left: 33.333%; top: 0; bottom: 0; width: 1px; box-shadow: calc(33.333% * 1) 0 0 rgba(255,255,255,.62); }
.crop-selection-box::after { left: 0; right: 0; top: 33.333%; height: 1px; box-shadow: 0 calc(33.333% * 1) 0 rgba(255,255,255,.62); }
.crop-dialog-actions { flex: 0 0 auto; min-height: 66px; padding: 12px 20px; display: flex; align-items: center; justify-content: flex-end; gap: 10px; border-top: 1px solid var(--line); }
.crop-dialog-actions span { margin-right: auto; color: var(--muted); font-size: 11px; }
.crop-dialog-actions button { min-height: 40px; padding: 0 16px; border: 1px solid var(--ink); background: white; cursor: pointer; font-size: 12px; }
.crop-dialog-actions .confirm { background: var(--green); color: white; font-weight: 700; }
.crop-dialog-actions .confirm:disabled { opacity: .42; cursor: not-allowed; }
@media (max-width: 1160px) {
.workspace { grid-template-columns: 260px minmax(500px, 1fr); }
.color-panel { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--ink); max-height: none; }
@@ -175,6 +197,14 @@ input[type="range"] { accent-color: var(--green); }
.history-section { margin: 52px auto; }
.history-heading .search-box { width: 100%; }
.history-grid { grid-template-columns: 1fr; }
.crop-dialog-backdrop { padding: 0; }
.crop-dialog { width: 100vw; max-height: 100vh; height: 100vh; border: 0; box-shadow: none; }
.crop-dialog-header { padding: 14px 16px; }
.crop-dialog-header h2 { font-size: 18px; }
.crop-dialog-scroll { padding: 10px; }
.crop-dialog-actions { padding: 10px 12px; flex-wrap: wrap; }
.crop-dialog-actions span { width: 100%; order: -1; }
.crop-dialog-actions button { flex: 1; padding: 0 10px; }
input, select, button { font-size: 16px; }
.search-box input, .color-item, .selection-tools button { font-size: 12px; }
}
+163 -52
View File
@@ -28,6 +28,15 @@ type ColorCluster = {
type SamplingStrategy = "smooth" | "dominant";
type CropRect = {
x: number;
y: number;
width: number;
height: number;
};
const FULL_CROP: CropRect = { x: 0, y: 0, width: 1, height: 1 };
function logUsage(event: Record<string, unknown>) {
void fetch("/api/usage", {
method: "POST",
@@ -400,9 +409,7 @@ function sampleDominantRegions(
width: number,
height: number,
fitMode: "cover" | "contain",
cropZoom: number,
cropX: number,
cropY: number,
crop: CropRect,
) {
// A mildly center-weighted 3x3 vote keeps hard object boundaries clean.
// Broad gradients are preserved later when the MARD palette is selected,
@@ -420,7 +427,7 @@ function sampleDominantRegions(
const ctx = sample.getContext("2d", { willReadFrequently: true })!;
ctx.clearRect(0, 0, sample.width, sample.height);
ctx.imageSmoothingEnabled = false;
drawFittedImage(ctx, image, sample.width, sample.height, fitMode, cropZoom, cropX, cropY);
drawFittedImage(ctx, image, sample.width, sample.height, fitMode, crop);
const source = ctx.getImageData(0, 0, sample.width, sample.height).data;
const result = new Uint8ClampedArray(width * height * 4);
const confidence = new Float32Array(width * height);
@@ -613,11 +620,13 @@ function drawFittedImage(
width: number,
height: number,
fitMode: "cover" | "contain",
cropZoom: number,
cropX: number,
cropY: number,
crop: CropRect,
) {
const imageRatio = image.naturalWidth / image.naturalHeight;
const sourceX = crop.x * image.naturalWidth;
const sourceY = crop.y * image.naturalHeight;
const sourceWidth = crop.width * image.naturalWidth;
const sourceHeight = crop.height * image.naturalHeight;
const imageRatio = sourceWidth / sourceHeight;
const boxRatio = width / height;
let drawWidth = width;
let drawHeight = height;
@@ -628,15 +637,9 @@ function drawFittedImage(
drawWidth = width;
drawHeight = width / imageRatio;
}
drawWidth *= cropZoom;
drawHeight *= cropZoom;
// Allow an object at the source-image edge to be dragged into the crop
// center. The exposed area intentionally becomes transparent/no-bead.
const panRangeX = drawWidth / 2;
const panRangeY = drawHeight / 2;
const drawX = (width - drawWidth) / 2 + cropX * panRangeX;
const drawY = (height - drawHeight) / 2 + cropY * panRangeY;
ctx.drawImage(image, drawX, drawY, drawWidth, drawHeight);
const drawX = (width - drawWidth) / 2;
const drawY = (height - drawHeight) / 2;
ctx.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, drawX, drawY, drawWidth, drawHeight);
}
function makeDemoImage() {
@@ -677,9 +680,9 @@ export default function Home() {
const [colorLimit, setColorLimit] = useState(64);
const [fitMode, setFitMode] = useState<"cover" | "contain">("contain");
const [samplingStrategy, setSamplingStrategy] = useState<SamplingStrategy>("dominant");
const [cropZoom, setCropZoom] = useState(1);
const [cropX, setCropX] = useState(0);
const [cropY, setCropY] = useState(0);
const [cropRect, setCropRect] = useState<CropRect>(FULL_CROP);
const [draftCropRect, setDraftCropRect] = useState<CropRect>(FULL_CROP);
const [cropDialogOpen, setCropDialogOpen] = useState(false);
const [pixels, setPixels] = useState<Pixel[]>([]);
const [selectedCodes, setSelectedCodes] = useState<Set<string>>(new Set());
const [onlySelected, setOnlySelected] = useState(false);
@@ -694,10 +697,11 @@ export default function Home() {
const sourceImageRef = useRef<HTMLImageElement | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const cropCanvasRef = useRef<HTMLCanvasElement | null>(null);
const cropDialogCanvasRef = 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 cropDialogStageRef = useRef<HTMLDivElement | null>(null);
const cropSelectionDragRef = useRef<{ pointerId: number; startX: number; startY: number } | null>(null);
const patternDragRef = useRef<{
pointerId: number;
x: number;
@@ -734,7 +738,7 @@ export default function Home() {
const convertImage = (
image = sourceImageRef.current,
crop = { zoom: cropZoom, x: cropX, y: cropY },
crop = cropRect,
recordUsage = true,
) => {
if (!image) return;
@@ -743,7 +747,7 @@ export default function Home() {
let data: Uint8ClampedArray;
let dominantConfidence: Float32Array | null = null;
if (samplingStrategy === "dominant") {
const sampled = sampleDominantRegions(image, width, height, fitMode, crop.zoom, crop.x, crop.y);
const sampled = sampleDominantRegions(image, width, height, fitMode, crop);
data = sampled.data;
dominantConfidence = sampled.confidence;
} else {
@@ -752,7 +756,7 @@ export default function Home() {
sample.height = height;
const ctx = sample.getContext("2d", { willReadFrequently: true })!;
ctx.clearRect(0, 0, width, height);
drawFittedImage(ctx, image, width, height, fitMode, crop.zoom, crop.x, crop.y);
drawFittedImage(ctx, image, width, height, fitMode, crop);
data = ctx.getImageData(0, 0, width, height).data;
}
const { pixelKeys, clusters, binClusters } = mergePerceptualColors(data);
@@ -803,7 +807,7 @@ export default function Home() {
image.onload = () => {
sourceImageRef.current = image;
setSourceUrl(demo);
convertImage(image, undefined, false);
convertImage(image, FULL_CROP, false);
};
image.src = demo;
// This intentionally runs once to create the initial example.
@@ -824,8 +828,37 @@ export default function Home() {
const ctx = canvas.getContext("2d")!;
ctx.fillStyle = "#f7f5ed";
ctx.fillRect(0, 0, previewWidth, previewHeight);
drawFittedImage(ctx, image, previewWidth, previewHeight, fitMode, cropZoom, cropX, cropY);
}, [sourceUrl, requestedWidth, requestedHeight, fitMode, cropZoom, cropX, cropY]);
drawFittedImage(ctx, image, previewWidth, previewHeight, fitMode, cropRect);
}, [sourceUrl, requestedWidth, requestedHeight, fitMode, cropRect]);
useEffect(() => {
if (!cropDialogOpen) return;
const canvas = cropDialogCanvasRef.current;
const image = sourceImageRef.current;
if (!canvas || !image) return;
const maximumWidth = 1200;
const maximumHeight = 760;
const scale = Math.min(maximumWidth / image.naturalWidth, maximumHeight / image.naturalHeight, 1);
canvas.width = Math.max(1, Math.round(image.naturalWidth * scale));
canvas.height = Math.max(1, Math.round(image.naturalHeight * scale));
const ctx = canvas.getContext("2d")!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
}, [cropDialogOpen, sourceUrl]);
useEffect(() => {
if (!cropDialogOpen) return;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") setCropDialogOpen(false);
};
window.addEventListener("keydown", closeOnEscape);
return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener("keydown", closeOnEscape);
};
}, [cropDialogOpen]);
const usedColors = useMemo(() => {
const counts = new Map<string, number>();
@@ -940,33 +973,81 @@ export default function Home() {
sourceImageRef.current = image;
setSourceUrl(url);
setSourceName(file.name);
setCropZoom(1);
setCropX(0);
setCropY(0);
setCropRect(FULL_CROP);
setDraftCropRect(FULL_CROP);
setStatus("图片已载入,点击“重新转换”生成图纸");
convertImage(image, { zoom: 1, x: 0, y: 0 });
convertImage(image, FULL_CROP);
};
image.src = url;
};
const handleCropPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
const openCropDialog = () => {
setDraftCropRect(cropRect);
setCropDialogOpen(true);
};
const cropPoint = (event: React.PointerEvent<HTMLDivElement>) => {
const stage = cropDialogStageRef.current;
if (!stage) return { x: 0, y: 0 };
const rect = stage.getBoundingClientRect();
return {
x: Math.max(0, Math.min(1, (event.clientX - rect.left) / Math.max(1, rect.width))),
y: Math.max(0, Math.min(1, (event.clientY - rect.top) / Math.max(1, rect.height))),
};
};
const updateDraftCrop = (startX: number, startY: number, endX: number, endY: number) => {
const targetRatio = Math.max(8, requestedWidth || 8) / Math.max(8, requestedHeight || 8);
const image = sourceImageRef.current;
if (!image) return;
const normalizedRatio = targetRatio * image.naturalHeight / image.naturalWidth;
const directionX = endX >= startX ? 1 : -1;
const directionY = endY >= startY ? 1 : -1;
let width = Math.abs(endX - startX);
let height = Math.abs(endY - startY);
if (width / Math.max(height, 0.0001) > normalizedRatio) width = height * normalizedRatio;
else height = width / Math.max(normalizedRatio, 0.0001);
const availableWidth = directionX > 0 ? 1 - startX : startX;
const availableHeight = directionY > 0 ? 1 - startY : startY;
const scale = Math.min(1, availableWidth / Math.max(width, 0.0001), availableHeight / Math.max(height, 0.0001));
width *= scale;
height *= scale;
setDraftCropRect({
x: directionX > 0 ? startX : startX - width,
y: directionY > 0 ? startY : startY - height,
width: Math.max(0.01, width),
height: Math.max(0.01, height),
});
};
const handleCropSelectionDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
const point = cropPoint(event);
event.currentTarget.setPointerCapture(event.pointerId);
cropDragRef.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, cropX, cropY };
cropSelectionDragRef.current = { pointerId: event.pointerId, startX: point.x, startY: point.y };
setDraftCropRect({ x: point.x, y: point.y, width: 0.01, height: 0.01 });
};
const handleCropPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
const drag = cropDragRef.current;
const preview = cropPreviewRef.current;
if (!drag || drag.pointerId !== event.pointerId || !preview) return;
const rect = preview.getBoundingClientRect();
const nextX = drag.cropX + ((event.clientX - drag.x) / Math.max(1, rect.width)) * 2;
const nextY = drag.cropY + ((event.clientY - drag.y) / Math.max(1, rect.height)) * 2;
setCropX(Math.max(-1, Math.min(1, nextX)));
setCropY(Math.max(-1, Math.min(1, nextY)));
const handleCropSelectionMove = (event: React.PointerEvent<HTMLDivElement>) => {
const drag = cropSelectionDragRef.current;
if (!drag || drag.pointerId !== event.pointerId) return;
const point = cropPoint(event);
updateDraftCrop(drag.startX, drag.startY, point.x, point.y);
};
const handleCropPointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
if (cropDragRef.current?.pointerId === event.pointerId) cropDragRef.current = null;
const handleCropSelectionEnd = (event: React.PointerEvent<HTMLDivElement>) => {
if (cropSelectionDragRef.current?.pointerId === event.pointerId) cropSelectionDragRef.current = null;
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
};
const confirmCrop = () => {
if (draftCropRect.width < 0.03 || draftCropRect.height < 0.03) {
setStatus("框选区域太小,请在大图上拖出更大的选框");
return;
}
setCropRect(draftCropRect);
setCropDialogOpen(false);
setStatus("取景区域已更新,点击“重新转换”生成图纸");
};
const toggleColor = (code: string) => {
@@ -1151,21 +1232,19 @@ export default function Home() {
<section className="workspace" aria-label="拼豆图纸转换工具">
<aside className="control-panel">
<div className="panel-heading"><span>01</span><div><h2></h2><p></p></div></div>
<div className="upload-card crop-preview" style={{ aspectRatio: `${Math.max(8, requestedWidth || 8)} / ${Math.max(8, requestedHeight || 8)}` }} ref={cropPreviewRef} onPointerDown={handleCropPointerDown} onPointerMove={handleCropPointerMove} onPointerUp={handleCropPointerUp} onPointerCancel={handleCropPointerUp}>
<canvas ref={cropCanvasRef} aria-label="转换区域预览,可拖动调整取景" />
<span className="crop-frame" aria-hidden="true" />
<div className="upload-card crop-preview" style={{ aspectRatio: `${Math.max(8, requestedWidth || 8)} / ${Math.max(8, requestedHeight || 8)}` }}>
<canvas ref={cropCanvasRef} aria-label="当前取景区域预览" />
<button
type="button"
className="upload-overlay"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => { event.stopPropagation(); fileInputRef.current?.click(); }}
onClick={() => fileInputRef.current?.click()}
></button>
<input ref={fileInputRef} className="file-input" type="file" accept="image/png,image/jpeg,image/webp" onChange={handleUpload} />
</div>
<p className="file-name" title={sourceName}>{sourceName}</p>
<label className="field crop-zoom"><span> <b>{cropZoom.toFixed(1)}×</b></span><input type="range" min="1" max="5" step="0.1" value={cropZoom} onChange={(e) => setCropZoom(Number(e.target.value))} /></label>
<button className="reset-crop" onClick={() => { setCropZoom(1); setCropX(0); setCropY(0); }}></button>
<button className="crop-select-button" onClick={openCropDialog}></button>
<button className="reset-crop" onClick={() => { setCropRect(FULL_CROP); setDraftCropRect(FULL_CROP); setStatus("已恢复使用整张图片"); }}>使</button>
<div className="field-row">
<label><span></span><input type="number" min="8" max="256" value={requestedWidth} onChange={(e) => setRequestedWidth(Number(e.target.value))} /></label>
@@ -1259,6 +1338,38 @@ export default function Home() {
</aside>
</section>
{cropDialogOpen && <div className="crop-dialog-backdrop" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget) setCropDialogOpen(false); }}>
<section className="crop-dialog" role="dialog" aria-modal="true" aria-labelledby="crop-dialog-title">
<div className="crop-dialog-header">
<div><p className="section-kicker">IMAGE CROP</p><h2 id="crop-dialog-title"></h2><p> {Math.max(8, requestedWidth || 8)} : {Math.max(8, requestedHeight || 8)}</p></div>
<button aria-label="关闭取景窗口" onClick={() => setCropDialogOpen(false)}>×</button>
</div>
<div className="crop-dialog-scroll">
<div
className="crop-dialog-stage"
ref={cropDialogStageRef}
onPointerDown={handleCropSelectionDown}
onPointerMove={handleCropSelectionMove}
onPointerUp={handleCropSelectionEnd}
onPointerCancel={handleCropSelectionEnd}
>
<canvas ref={cropDialogCanvasRef} aria-label="可框选的原始大图" />
<div className="crop-selection-mask crop-selection-top" style={{ height: `${draftCropRect.y * 100}%` }} />
<div className="crop-selection-mask crop-selection-left" style={{ top: `${draftCropRect.y * 100}%`, width: `${draftCropRect.x * 100}%`, height: `${draftCropRect.height * 100}%` }} />
<div className="crop-selection-mask crop-selection-right" style={{ top: `${draftCropRect.y * 100}%`, left: `${(draftCropRect.x + draftCropRect.width) * 100}%`, right: 0, height: `${draftCropRect.height * 100}%` }} />
<div className="crop-selection-mask crop-selection-bottom" style={{ top: `${(draftCropRect.y + draftCropRect.height) * 100}%` }} />
<div className="crop-selection-box" style={{ left: `${draftCropRect.x * 100}%`, top: `${draftCropRect.y * 100}%`, width: `${draftCropRect.width * 100}%`, height: `${draftCropRect.height * 100}%` }} />
</div>
</div>
<div className="crop-dialog-actions">
<button onClick={() => setDraftCropRect(FULL_CROP)}></button>
<span></span>
<button onClick={() => setCropDialogOpen(false)}></button>
<button className="confirm" disabled={draftCropRect.width < 0.03 || draftCropRect.height < 0.03} onClick={confirmCrop}>使</button>
</div>
</section>
</div>}
<section className="history-section" aria-labelledby="history-title">
<div className="history-heading"><div><p className="section-kicker">LOCAL PATTERN ARCHIVE</p><h2 id="history-title"></h2><p> 30 </p></div>
<div className="search-box"><span></span><input value={historyQuery} onChange={(e) => setHistoryQuery(e.target.value)} placeholder="按名称或 32x32 查询" /></div>
+3 -3
View File
@@ -11,7 +11,7 @@
"name": "rolldown-runtime"
},
"app/page.tsx": {
"file": "_next/static/chunks/page-5ZnMML26.js",
"file": "_next/static/chunks/page-CNQA87Lx.js",
"name": "page",
"src": "app/page.tsx",
"isDynamicEntry": true,
@@ -21,7 +21,7 @@
]
},
"node_modules/vinext/dist/shims/layout-segment-context.js": {
"file": "_next/static/chunks/layout-segment-context-Bq1X1g2W.js",
"file": "_next/static/chunks/layout-segment-context-Bf1cBuVr.js",
"name": "layout-segment-context",
"src": "node_modules/vinext/dist/shims/layout-segment-context.js",
"isDynamicEntry": true,
@@ -32,7 +32,7 @@
]
},
"virtual:vinext-app-browser-entry": {
"file": "_next/static/chunks/index-ByP43Bdw.js",
"file": "_next/static/chunks/index-DAoL2pdt.js",
"name": "index",
"src": "virtual:vinext-app-browser-entry",
"isEntry": true,
+2 -2
View File
@@ -1,2 +1,2 @@
<!DOCTYPE html><html lang="zh-CN"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/index.Dq42BzaZ.css" data-rsc-css-href="/_next/static/css/index.Dq42BzaZ.css" data-precedence="vite-rsc/importer-resources"/><link rel="modulepreload" fetchPriority="low" href="/_next/static/chunks/index-ByP43Bdw.js"/><script src="/_next/static/chunks/rolldown-runtime-C60lm6uB.js" type="module" async=""></script><script src="/_next/static/chunks/framework-BgSIrAUN.js" type="module" async=""></script><meta name="robots" content="noindex"/><title>豆格工坊|图片转拼豆图纸</title><meta name="description" content="在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"/><title>404: This page could not be found.</title><script>Object.assign(((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}),{params:{},nav:{"pathname":"/__vinext_nonexistent_for_404__","searchParams":[]}})</script><link rel="modulepreload" href="/_next/static/chunks/index-ByP43Bdw.js" />
</head><body><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script type="module" src="/_next/static/chunks/index-ByP43Bdw.js" id="_R_" async=""></script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("1:\"$Sreact.fragment\"\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push(":HL[\"/_next/static/css/index.Dq42BzaZ.css\",\"style\" ]\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("0:{\"__route\":\"route:/__vinext_nonexistent_for_404__\",\"__interceptionContext\":null,\"__layoutIds\":[],\"__rootLayout\":null,\"route:/__vinext_nonexistent_for_404__\":[[[\"$\",\"link\",\"css:/_next/static/css/index.Dq42BzaZ.css\",{\"rel\":\"stylesheet\",\"precedence\":\"vite-rsc/importer-resources\",\"href\":\"/_next/static/css/index.Dq42BzaZ.css\",\"data-rsc-css-href\":\"/_next/static/css/index.Dq42BzaZ.css\"}],\"$undefined\"],[\"$\",\"html\",null,{\"lang\":\"zh-CN\",\"children\":[\"$\",\"body\",null,{\"children\":[[\"$\",\"meta\",\"charset\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"robots\",{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$1\",\"metadata\",{\"children\":[[\"$\",\"title\",\"0\",{\"children\":\"豆格工坊|图片转拼豆图纸\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。\"}]]}],[\"$\",\"$1\",\"viewport\",{\"children\":[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]}],[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]]}]}]]}\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).done=true</script></body></html>
<!DOCTYPE html><html lang="zh-CN"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/index.BD45GRB0.css" data-rsc-css-href="/_next/static/css/index.BD45GRB0.css" data-precedence="vite-rsc/importer-resources"/><link rel="modulepreload" fetchPriority="low" href="/_next/static/chunks/index-DAoL2pdt.js"/><script src="/_next/static/chunks/rolldown-runtime-C60lm6uB.js" type="module" async=""></script><script src="/_next/static/chunks/framework-BgSIrAUN.js" type="module" async=""></script><meta name="robots" content="noindex"/><title>豆格工坊|图片转拼豆图纸</title><meta name="description" content="在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"/><title>404: This page could not be found.</title><script>Object.assign(((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}),{params:{},nav:{"pathname":"/__vinext_nonexistent_for_404__","searchParams":[]}})</script><link rel="modulepreload" href="/_next/static/chunks/index-DAoL2pdt.js" />
</head><body><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script type="module" src="/_next/static/chunks/index-DAoL2pdt.js" id="_R_" async=""></script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("1:\"$Sreact.fragment\"\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push(":HL[\"/_next/static/css/index.BD45GRB0.css\",\"style\" ]\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).rsc.push("0:{\"__route\":\"route:/__vinext_nonexistent_for_404__\",\"__interceptionContext\":null,\"__layoutIds\":[],\"__rootLayout\":null,\"route:/__vinext_nonexistent_for_404__\":[[[\"$\",\"link\",\"css:/_next/static/css/index.BD45GRB0.css\",{\"rel\":\"stylesheet\",\"precedence\":\"vite-rsc/importer-resources\",\"href\":\"/_next/static/css/index.BD45GRB0.css\",\"data-rsc-css-href\":\"/_next/static/css/index.BD45GRB0.css\"}],\"$undefined\"],[\"$\",\"html\",null,{\"lang\":\"zh-CN\",\"children\":[\"$\",\"body\",null,{\"children\":[[\"$\",\"meta\",\"charset\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"robots\",{\"name\":\"robots\",\"content\":\"noindex\"}],[\"$\",\"$1\",\"metadata\",{\"children\":[[\"$\",\"title\",\"0\",{\"children\":\"豆格工坊|图片转拼豆图纸\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。\"}]]}],[\"$\",\"$1\",\"viewport\",{\"children\":[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]}],[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]]}]}]]}\n")</script><script>((self[Symbol.for("vinext.navigationRuntime")]??={bootstrap:{routeManifest:null},functions:{}}).bootstrap.rsc??={rsc:[]}).done=true</script></body></html>
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as e}from"./rolldown-runtime-C60lm6uB.js";import{r as t}from"./framework-BgSIrAUN.js";import{t as n}from"./index-ByP43Bdw.js";var r=e(t(),1),i=new Map;function a(e,t){return e?{...e,...t}:t}function o({providerId:e,segmentMap:t,children:o}){let s=(0,r.useRef)(null),c=n(),l=a(s.current??(e?i.get(e)??null:null),t);return(0,r.useEffect)(()=>{s.current=l,e&&i.set(e,l)},[l,e]),c?(0,r.createElement)(c.Provider,{value:l},o):o}export{o as LayoutSegmentProvider,a as mergeLayoutSegmentMap};
import{r as e}from"./rolldown-runtime-C60lm6uB.js";import{r as t}from"./framework-BgSIrAUN.js";import{t as n}from"./index-DAoL2pdt.js";var r=e(t(),1),i=new Map;function a(e,t){return e?{...e,...t}:t}function o({providerId:e,segmentMap:t,children:o}){let s=(0,r.useRef)(null),c=n(),l=a(s.current??(e?i.get(e)??null:null),t);return(0,r.useEffect)(()=>{s.current=l,e&&i.set(e,l)},[l,e]),c?(0,r.createElement)(c.Provider,{value:l},o):o}export{o as LayoutSegmentProvider,a as mergeLayoutSegmentMap};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
7:I["8c0f216c4604",[],"Slot",1]
8:I["9276801271d6",[],"AppRouterScrollTarget",1]
9:I["593f344dc510",[],"RedirectBoundary",1]
:HL["/_next/static/css/index.Dq42BzaZ.css","style" ]
0:{"__route":"route:/","__interceptionContext":null,"__layoutIds":["layout:/"],"__rootLayout":"/","__sourcePage":"/page","page:/":"$L1","layout:/":[[[["$","link","css:/_next/static/css/index.Dq42BzaZ.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/_next/static/css/index.Dq42BzaZ.css","data-rsc-css-href":"/_next/static/css/index.Dq42BzaZ.css"}],"$undefined"],["$","html",null,{"lang":"zh-CN","children":["$","body",null,{"children":["$","$L2",null,{}]}]}]],null],"route:/":[[["$","meta",null,{"charSet":"utf-8"}],[["$","title","0",{"children":"豆格工坊|图片转拼豆图纸"}],["$","meta","1",{"name":"description","content":"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"}]],[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]],["$","$L3",null,{"fallback":"$4","children":["$","$L5",null,{"fallback":"$4","children":["$","$L6",null,{"providerId":"layout:/","segmentMap":{"children":[]},"children":["$","$L7",null,{"id":"layout:/","parallelSlots":"$undefined","children":["$","$L8",null,{"children":["$","$L9",null,{"children":[["$","$L6",null,{"providerId":"page:/","segmentMap":{"children":["__PAGE__"]},"children":["$","$L7",null,{"id":"page:/"}]}],null]}]}]}]}]}]}],null,null],"__layoutFlags":{"layout:/":"s"},"__artifactCompatibility":{"schemaVersion":1,"graphVersion":"app-route-graph:1177fd80f83fa7b0","deploymentVersion":"91d6b0b4-aeca-4c4d-9c3c-27cc5139b142","appElementsSchemaVersion":1,"rscPayloadSchemaVersion":1,"rootBoundaryId":"/","renderEpoch":null},"__renderObservation":{"schemaVersion":1,"output":{"kind":"app-rsc","mountedSlotsFingerprint":null,"renderEpoch":null,"rootBoundaryId":"/","routeId":"route:/"},"completeness":"partial","boundaryOutcome":{"kind":"unknown"},"requestApis":[{"kind":"connection","status":"unknown"},{"kind":"cookies","status":"unknown"},{"kind":"draftMode","status":"unknown"},{"kind":"headers","status":"unknown"},{"kind":"params","status":"unknown"},{"kind":"searchParams","status":"unknown"}],"dynamicFetches":[],"cacheTags":["/","_N_T_/","_N_T_/index","_N_T_/layout","_N_T_/page"],"pathTags":["/"],"cacheability":"unknown","downgrade":{"target":"freshRender","reasons":[{"code":"CP_DOWNGRADE_CACHEABILITY_UNKNOWN","target":"freshRender"},{"code":"CP_DOWNGRADE_INCOMPLETE_OBSERVATION","completeness":"partial","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"connection","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"cookies","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"draftMode","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"headers","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"params","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"searchParams","target":"freshRender"}],"fallback":{"kind":"breakerFallback","code":"CP_PRIVATE_DYNAMIC_DOWNGRADE","mode":"renderFresh","scope":"affectedOutput","fields":{"reasonCodes":["CP_DOWNGRADE_CACHEABILITY_UNKNOWN","CP_DOWNGRADE_INCOMPLETE_OBSERVATION","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API"],"target":"freshRender"}},"isPublicCacheCandidate":false}}}
:HL["/_next/static/css/index.BD45GRB0.css","style" ]
0:{"__route":"route:/","__interceptionContext":null,"__layoutIds":["layout:/"],"__rootLayout":"/","__sourcePage":"/page","page:/":"$L1","layout:/":[[[["$","link","css:/_next/static/css/index.BD45GRB0.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/_next/static/css/index.BD45GRB0.css","data-rsc-css-href":"/_next/static/css/index.BD45GRB0.css"}],"$undefined"],["$","html",null,{"lang":"zh-CN","children":["$","body",null,{"children":["$","$L2",null,{}]}]}]],null],"route:/":[[["$","meta",null,{"charSet":"utf-8"}],[["$","title","0",{"children":"豆格工坊|图片转拼豆图纸"}],["$","meta","1",{"name":"description","content":"在浏览器中把图片转换为拼豆像素图纸,按色号、名称或像素格筛选颜色。"}]],[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]],["$","$L3",null,{"fallback":"$4","children":["$","$L5",null,{"fallback":"$4","children":["$","$L6",null,{"providerId":"layout:/","segmentMap":{"children":[]},"children":["$","$L7",null,{"id":"layout:/","parallelSlots":"$undefined","children":["$","$L8",null,{"children":["$","$L9",null,{"children":[["$","$L6",null,{"providerId":"page:/","segmentMap":{"children":["__PAGE__"]},"children":["$","$L7",null,{"id":"page:/"}]}],null]}]}]}]}]}]}],null,null],"__layoutFlags":{"layout:/":"s"},"__artifactCompatibility":{"schemaVersion":1,"graphVersion":"app-route-graph:1177fd80f83fa7b0","deploymentVersion":"976d8e63-a1e8-4c46-b57a-876eef7ec109","appElementsSchemaVersion":1,"rscPayloadSchemaVersion":1,"rootBoundaryId":"/","renderEpoch":null},"__renderObservation":{"schemaVersion":1,"output":{"kind":"app-rsc","mountedSlotsFingerprint":null,"renderEpoch":null,"rootBoundaryId":"/","routeId":"route:/"},"completeness":"partial","boundaryOutcome":{"kind":"unknown"},"requestApis":[{"kind":"connection","status":"unknown"},{"kind":"cookies","status":"unknown"},{"kind":"draftMode","status":"unknown"},{"kind":"headers","status":"unknown"},{"kind":"params","status":"unknown"},{"kind":"searchParams","status":"unknown"}],"dynamicFetches":[],"cacheTags":["/","_N_T_/","_N_T_/index","_N_T_/layout","_N_T_/page"],"pathTags":["/"],"cacheability":"unknown","downgrade":{"target":"freshRender","reasons":[{"code":"CP_DOWNGRADE_CACHEABILITY_UNKNOWN","target":"freshRender"},{"code":"CP_DOWNGRADE_INCOMPLETE_OBSERVATION","completeness":"partial","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"connection","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"cookies","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"draftMode","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"headers","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"params","target":"freshRender"},{"code":"CP_DOWNGRADE_UNKNOWN_REQUEST_API","requestApi":"searchParams","target":"freshRender"}],"fallback":{"kind":"breakerFallback","code":"CP_PRIVATE_DYNAMIC_DOWNGRADE","mode":"renderFresh","scope":"affectedOutput","fields":{"reasonCodes":["CP_DOWNGRADE_CACHEABILITY_UNKNOWN","CP_DOWNGRADE_INCOMPLETE_OBSERVATION","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API","CP_DOWNGRADE_UNKNOWN_REQUEST_API"],"target":"freshRender"}},"isPublicCacheCandidate":false}}}
a:I["6efdf509a785",[],"default",1]
1:["$","$La",null,{"params":"$@b","searchParams":"$@c"}]
b:{}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"appBrowserEntry": "_next/static/chunks/index-ByP43Bdw.js"
"appBrowserEntry": "_next/static/chunks/index-DAoL2pdt.js"
}