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 { 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 canvas { width: 100%; height: 100%; display: block; pointer-events: none; }
.upload-card .file-input { display: none; } .upload-card .file-input { display: none; }
.crop-preview { cursor: grab; touch-action: none; } .crop-preview { cursor: default; }
.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; }
.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; } .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; } .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; } .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 { display: grid; grid-template-columns: 1fr auto 1fr; align-items: end; gap: 8px; }
.field-row label, .field { display: flex; flex-direction: column; 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-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; } .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) { @media (max-width: 1160px) {
.workspace { grid-template-columns: 260px minmax(500px, 1fr); } .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; } .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-section { margin: 52px auto; }
.history-heading .search-box { width: 100%; } .history-heading .search-box { width: 100%; }
.history-grid { grid-template-columns: 1fr; } .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; } input, select, button { font-size: 16px; }
.search-box input, .color-item, .selection-tools button { font-size: 12px; } .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 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>) { function logUsage(event: Record<string, unknown>) {
void fetch("/api/usage", { void fetch("/api/usage", {
method: "POST", method: "POST",
@@ -400,9 +409,7 @@ function sampleDominantRegions(
width: number, width: number,
height: number, height: number,
fitMode: "cover" | "contain", fitMode: "cover" | "contain",
cropZoom: number, crop: CropRect,
cropX: number,
cropY: number,
) { ) {
// A mildly center-weighted 3x3 vote keeps hard object boundaries clean. // A mildly center-weighted 3x3 vote keeps hard object boundaries clean.
// Broad gradients are preserved later when the MARD palette is selected, // Broad gradients are preserved later when the MARD palette is selected,
@@ -420,7 +427,7 @@ function sampleDominantRegions(
const ctx = sample.getContext("2d", { willReadFrequently: true })!; const ctx = sample.getContext("2d", { willReadFrequently: true })!;
ctx.clearRect(0, 0, sample.width, sample.height); ctx.clearRect(0, 0, sample.width, sample.height);
ctx.imageSmoothingEnabled = false; 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 source = ctx.getImageData(0, 0, sample.width, sample.height).data;
const result = new Uint8ClampedArray(width * height * 4); const result = new Uint8ClampedArray(width * height * 4);
const confidence = new Float32Array(width * height); const confidence = new Float32Array(width * height);
@@ -613,11 +620,13 @@ function drawFittedImage(
width: number, width: number,
height: number, height: number,
fitMode: "cover" | "contain", fitMode: "cover" | "contain",
cropZoom: number, crop: CropRect,
cropX: number,
cropY: number,
) { ) {
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; const boxRatio = width / height;
let drawWidth = width; let drawWidth = width;
let drawHeight = height; let drawHeight = height;
@@ -628,15 +637,9 @@ function drawFittedImage(
drawWidth = width; drawWidth = width;
drawHeight = width / imageRatio; drawHeight = width / imageRatio;
} }
drawWidth *= cropZoom; const drawX = (width - drawWidth) / 2;
drawHeight *= cropZoom; const drawY = (height - drawHeight) / 2;
// Allow an object at the source-image edge to be dragged into the crop ctx.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, drawX, drawY, drawWidth, drawHeight);
// 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);
} }
function makeDemoImage() { function makeDemoImage() {
@@ -677,9 +680,9 @@ export default function Home() {
const [colorLimit, setColorLimit] = useState(64); const [colorLimit, setColorLimit] = useState(64);
const [fitMode, setFitMode] = useState<"cover" | "contain">("contain"); const [fitMode, setFitMode] = useState<"cover" | "contain">("contain");
const [samplingStrategy, setSamplingStrategy] = useState<SamplingStrategy>("dominant"); const [samplingStrategy, setSamplingStrategy] = useState<SamplingStrategy>("dominant");
const [cropZoom, setCropZoom] = useState(1); const [cropRect, setCropRect] = useState<CropRect>(FULL_CROP);
const [cropX, setCropX] = useState(0); const [draftCropRect, setDraftCropRect] = useState<CropRect>(FULL_CROP);
const [cropY, setCropY] = useState(0); const [cropDialogOpen, setCropDialogOpen] = useState(false);
const [pixels, setPixels] = useState<Pixel[]>([]); const [pixels, setPixels] = useState<Pixel[]>([]);
const [selectedCodes, setSelectedCodes] = useState<Set<string>>(new Set()); const [selectedCodes, setSelectedCodes] = useState<Set<string>>(new Set());
const [onlySelected, setOnlySelected] = useState(false); const [onlySelected, setOnlySelected] = useState(false);
@@ -694,10 +697,11 @@ export default function Home() {
const sourceImageRef = useRef<HTMLImageElement | null>(null); const sourceImageRef = useRef<HTMLImageElement | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
const cropCanvasRef = useRef<HTMLCanvasElement | null>(null); const cropCanvasRef = useRef<HTMLCanvasElement | null>(null);
const cropDialogCanvasRef = useRef<HTMLCanvasElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null); const canvasRef = useRef<HTMLCanvasElement | null>(null);
const patternStageRef = useRef<HTMLDivElement | null>(null); const patternStageRef = useRef<HTMLDivElement | null>(null);
const cropPreviewRef = useRef<HTMLDivElement | null>(null); const cropDialogStageRef = useRef<HTMLDivElement | null>(null);
const cropDragRef = useRef<{ pointerId: number; x: number; y: number; cropX: number; cropY: number } | null>(null); const cropSelectionDragRef = useRef<{ pointerId: number; startX: number; startY: number } | null>(null);
const patternDragRef = useRef<{ const patternDragRef = useRef<{
pointerId: number; pointerId: number;
x: number; x: number;
@@ -734,7 +738,7 @@ export default function Home() {
const convertImage = ( const convertImage = (
image = sourceImageRef.current, image = sourceImageRef.current,
crop = { zoom: cropZoom, x: cropX, y: cropY }, crop = cropRect,
recordUsage = true, recordUsage = true,
) => { ) => {
if (!image) return; if (!image) return;
@@ -743,7 +747,7 @@ export default function Home() {
let data: Uint8ClampedArray; let data: Uint8ClampedArray;
let dominantConfidence: Float32Array | null = null; let dominantConfidence: Float32Array | null = null;
if (samplingStrategy === "dominant") { 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; data = sampled.data;
dominantConfidence = sampled.confidence; dominantConfidence = sampled.confidence;
} else { } else {
@@ -752,7 +756,7 @@ export default function Home() {
sample.height = height; sample.height = height;
const ctx = sample.getContext("2d", { willReadFrequently: true })!; const ctx = sample.getContext("2d", { willReadFrequently: true })!;
ctx.clearRect(0, 0, width, height); 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; data = ctx.getImageData(0, 0, width, height).data;
} }
const { pixelKeys, clusters, binClusters } = mergePerceptualColors(data); const { pixelKeys, clusters, binClusters } = mergePerceptualColors(data);
@@ -803,7 +807,7 @@ export default function Home() {
image.onload = () => { image.onload = () => {
sourceImageRef.current = image; sourceImageRef.current = image;
setSourceUrl(demo); setSourceUrl(demo);
convertImage(image, undefined, false); convertImage(image, FULL_CROP, false);
}; };
image.src = demo; image.src = demo;
// This intentionally runs once to create the initial example. // This intentionally runs once to create the initial example.
@@ -824,8 +828,37 @@ export default function Home() {
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
ctx.fillStyle = "#f7f5ed"; ctx.fillStyle = "#f7f5ed";
ctx.fillRect(0, 0, previewWidth, previewHeight); ctx.fillRect(0, 0, previewWidth, previewHeight);
drawFittedImage(ctx, image, previewWidth, previewHeight, fitMode, cropZoom, cropX, cropY); drawFittedImage(ctx, image, previewWidth, previewHeight, fitMode, cropRect);
}, [sourceUrl, requestedWidth, requestedHeight, fitMode, cropZoom, cropX, cropY]); }, [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 usedColors = useMemo(() => {
const counts = new Map<string, number>(); const counts = new Map<string, number>();
@@ -940,33 +973,81 @@ export default function Home() {
sourceImageRef.current = image; sourceImageRef.current = image;
setSourceUrl(url); setSourceUrl(url);
setSourceName(file.name); setSourceName(file.name);
setCropZoom(1); setCropRect(FULL_CROP);
setCropX(0); setDraftCropRect(FULL_CROP);
setCropY(0);
setStatus("图片已载入,点击“重新转换”生成图纸"); setStatus("图片已载入,点击“重新转换”生成图纸");
convertImage(image, { zoom: 1, x: 0, y: 0 }); convertImage(image, FULL_CROP);
}; };
image.src = url; 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); 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 handleCropSelectionMove = (event: React.PointerEvent<HTMLDivElement>) => {
const drag = cropDragRef.current; const drag = cropSelectionDragRef.current;
const preview = cropPreviewRef.current; if (!drag || drag.pointerId !== event.pointerId) return;
if (!drag || drag.pointerId !== event.pointerId || !preview) return; const point = cropPoint(event);
const rect = preview.getBoundingClientRect(); updateDraftCrop(drag.startX, drag.startY, point.x, point.y);
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 handleCropPointerUp = (event: React.PointerEvent<HTMLDivElement>) => { const handleCropSelectionEnd = (event: React.PointerEvent<HTMLDivElement>) => {
if (cropDragRef.current?.pointerId === event.pointerId) cropDragRef.current = null; 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) => { const toggleColor = (code: string) => {
@@ -1151,21 +1232,19 @@ export default function Home() {
<section className="workspace" aria-label="拼豆图纸转换工具"> <section className="workspace" aria-label="拼豆图纸转换工具">
<aside className="control-panel"> <aside className="control-panel">
<div className="panel-heading"><span>01</span><div><h2></h2><p></p></div></div> <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}> <div className="upload-card crop-preview" style={{ aspectRatio: `${Math.max(8, requestedWidth || 8)} / ${Math.max(8, requestedHeight || 8)}` }}>
<canvas ref={cropCanvasRef} aria-label="转换区域预览,可拖动调整取景" /> <canvas ref={cropCanvasRef} aria-label="当前取景区域预览" />
<span className="crop-frame" aria-hidden="true" />
<button <button
type="button" type="button"
className="upload-overlay" className="upload-overlay"
onPointerDown={(event) => event.stopPropagation()} onClick={() => fileInputRef.current?.click()}
onClick={(event) => { event.stopPropagation(); fileInputRef.current?.click(); }}
></button> ></button>
<input ref={fileInputRef} className="file-input" type="file" accept="image/png,image/jpeg,image/webp" onChange={handleUpload} /> <input ref={fileInputRef} className="file-input" type="file" accept="image/png,image/jpeg,image/webp" onChange={handleUpload} />
</div> </div>
<p className="file-name" title={sourceName}>{sourceName}</p> <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="crop-select-button" onClick={openCropDialog}></button>
<button className="reset-crop" onClick={() => { setCropZoom(1); setCropX(0); setCropY(0); }}></button> <button className="reset-crop" onClick={() => { setCropRect(FULL_CROP); setDraftCropRect(FULL_CROP); setStatus("已恢复使用整张图片"); }}>使</button>
<div className="field-row"> <div className="field-row">
<label><span></span><input type="number" min="8" max="256" value={requestedWidth} onChange={(e) => setRequestedWidth(Number(e.target.value))} /></label> <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> </aside>
</section> </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"> <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="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> <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" "name": "rolldown-runtime"
}, },
"app/page.tsx": { "app/page.tsx": {
"file": "_next/static/chunks/page-5ZnMML26.js", "file": "_next/static/chunks/page-CNQA87Lx.js",
"name": "page", "name": "page",
"src": "app/page.tsx", "src": "app/page.tsx",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -21,7 +21,7 @@
] ]
}, },
"node_modules/vinext/dist/shims/layout-segment-context.js": { "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", "name": "layout-segment-context",
"src": "node_modules/vinext/dist/shims/layout-segment-context.js", "src": "node_modules/vinext/dist/shims/layout-segment-context.js",
"isDynamicEntry": true, "isDynamicEntry": true,
@@ -32,7 +32,7 @@
] ]
}, },
"virtual:vinext-app-browser-entry": { "virtual:vinext-app-browser-entry": {
"file": "_next/static/chunks/index-ByP43Bdw.js", "file": "_next/static/chunks/index-DAoL2pdt.js",
"name": "index", "name": "index",
"src": "virtual:vinext-app-browser-entry", "src": "virtual:vinext-app-browser-entry",
"isEntry": true, "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" /> <!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-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> </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] 7:I["8c0f216c4604",[],"Slot",1]
8:I["9276801271d6",[],"AppRouterScrollTarget",1] 8:I["9276801271d6",[],"AppRouterScrollTarget",1]
9:I["593f344dc510",[],"RedirectBoundary",1] 9:I["593f344dc510",[],"RedirectBoundary",1]
:HL["/_next/static/css/index.Dq42BzaZ.css","style" ] :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.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}}} 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] a:I["6efdf509a785",[],"default",1]
1:["$","$La",null,{"params":"$@b","searchParams":"$@c"}] 1:["$","$La",null,{"params":"$@b","searchParams":"$@c"}]
b:{} b:{}
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"appBrowserEntry": "_next/static/chunks/index-ByP43Bdw.js" "appBrowserEntry": "_next/static/chunks/index-DAoL2pdt.js"
} }