Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d53f8f4585 | ||
|
|
e028ecc9c8 |
+34
-4
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
+301
-59
@@ -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",
|
||||||
@@ -201,7 +210,9 @@ function mergePerceptualColors(data: Uint8ClampedArray) {
|
|||||||
}).sort((a, b) => b.count - a.count);
|
}).sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
const clusters: ColorCluster[] = [];
|
const clusters: ColorCluster[] = [];
|
||||||
const mergeThreshold = 0.025;
|
// Keep enough source-color resolution for broad gradients. The later MARD
|
||||||
|
// selection still merges close colors globally and rejects tiny edge noise.
|
||||||
|
const mergeThreshold = 0.015;
|
||||||
for (const bin of bins) {
|
for (const bin of bins) {
|
||||||
let nearestIndex = -1;
|
let nearestIndex = -1;
|
||||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||||
@@ -254,28 +265,151 @@ function chooseDistinctMardColors(clusters: ColorCluster[], maximum: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const separated: BeadColor[] = [];
|
const separated: BeadColor[] = [];
|
||||||
const totalPixels = clusters.reduce((total, cluster) => total + cluster.count, 0);
|
|
||||||
const minimumMardDistance = 0.035;
|
const minimumMardDistance = 0.035;
|
||||||
const broadGradientDistance = 0.02;
|
|
||||||
const broadColorThreshold = Math.max(12, totalPixels * 0.008);
|
|
||||||
const ranked = [...candidates.values()].sort((a, b) => b.score - a.score || a.error / a.count - b.error / b.count);
|
const ranked = [...candidates.values()].sort((a, b) => b.score - a.score || a.error / a.count - b.error / b.count);
|
||||||
for (const candidate of ranked) {
|
for (const candidate of ranked) {
|
||||||
const requiredDistance = candidate.count >= broadColorThreshold ? broadGradientDistance : minimumMardDistance;
|
if (separated.every((selected) => oklabDistance(candidate.color.lab, selected.lab) >= minimumMardDistance)) {
|
||||||
if (separated.every((selected) => oklabDistance(candidate.color.lab, selected.lab) >= requiredDistance)) {
|
|
||||||
separated.push(candidate.color);
|
separated.push(candidate.color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return separated.slice(0, Math.max(0, Math.min(maximum, separated.length)));
|
return separated.slice(0, Math.max(0, Math.min(maximum, separated.length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchClustersToPalette(clusters: ColorCluster[], limitedPalette: BeadColor[], colorLimit: number) {
|
||||||
|
const matches = clusters.map((cluster) => nearestColor(cluster.lab, limitedPalette));
|
||||||
|
if (colorLimit < 12) return matches;
|
||||||
|
|
||||||
|
// A large, smooth gradient may contain several useful MARD steps which the
|
||||||
|
// global minimum-distance filter deliberately merges. Let only substantial
|
||||||
|
// source clusters recover their accurate shade; tiny edge colors stay out.
|
||||||
|
const totalPixels = clusters.reduce((total, cluster) => total + cluster.count, 0);
|
||||||
|
const broadClusterThreshold = Math.max(24, totalPixels * 0.012);
|
||||||
|
const maximumExtraShades = Math.min(8, Math.max(0, colorLimit - limitedPalette.length));
|
||||||
|
const extraShades: BeadColor[] = [];
|
||||||
|
const rankedClusters = clusters
|
||||||
|
.map((cluster, index) => ({ cluster, index, exact: nearestColor(cluster.lab, PALETTE) }))
|
||||||
|
.filter(({ cluster, exact, index }) => cluster.count >= broadClusterThreshold && exact.code !== matches[index].code)
|
||||||
|
.sort((a, b) => b.cluster.count - a.cluster.count);
|
||||||
|
|
||||||
|
for (const candidate of rankedClusters) {
|
||||||
|
if (extraShades.length >= maximumExtraShades) break;
|
||||||
|
if (extraShades.some((color) => color.code === candidate.exact.code)) {
|
||||||
|
matches[candidate.index] = candidate.exact;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const nearestSelectedDistance = Math.min(...limitedPalette.map((color) => oklabDistance(color.lab, candidate.exact.lab)));
|
||||||
|
if (nearestSelectedDistance < 0.018) continue;
|
||||||
|
extraShades.push(candidate.exact);
|
||||||
|
matches[candidate.index] = candidate.exact;
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function preserveSmoothGradientSteps(
|
||||||
|
colors: Array<BeadColor | null>,
|
||||||
|
sourceData: Uint8ClampedArray,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
colorLimit: number,
|
||||||
|
) {
|
||||||
|
if (colorLimit < 12) return colors;
|
||||||
|
const result = [...colors];
|
||||||
|
const sourceLabs: Array<Oklab | null> = [];
|
||||||
|
for (let index = 0; index < sourceData.length; index += 4) {
|
||||||
|
sourceLabs.push(sourceData[index + 3] < TRANSPARENT_ALPHA_THRESHOLD
|
||||||
|
? null
|
||||||
|
: rgbToOklab([sourceData[index], sourceData[index + 1], sourceData[index + 2]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const smoothMask = new Uint8Array(width * height);
|
||||||
|
const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
|
||||||
|
for (let row = 1; row < height - 1; row++) {
|
||||||
|
for (let column = 1; column < width - 1; column++) {
|
||||||
|
const index = row * width + column;
|
||||||
|
const current = sourceLabs[index];
|
||||||
|
if (!current) continue;
|
||||||
|
let maximumNeighborDistance = 0;
|
||||||
|
for (const [rowOffset, columnOffset] of directions) {
|
||||||
|
const neighbor = sourceLabs[(row + rowOffset) * width + column + columnOffset];
|
||||||
|
if (!neighbor) { maximumNeighborDistance = Number.POSITIVE_INFINITY; break; }
|
||||||
|
maximumNeighborDistance = Math.max(maximumNeighborDistance, oklabDistance(current, neighbor));
|
||||||
|
}
|
||||||
|
if (maximumNeighborDistance <= 0.018) smoothMask[index] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require a real two-dimensional smooth patch, so a one-cell antialiased
|
||||||
|
// outline can never qualify as a gradient.
|
||||||
|
const gradientPalette = new Set<string>();
|
||||||
|
for (let row = 2; row < height - 2; row++) {
|
||||||
|
for (let column = 2; column < width - 2; column++) {
|
||||||
|
const index = row * width + column;
|
||||||
|
if (!smoothMask[index]) continue;
|
||||||
|
let smoothNeighbors = 0;
|
||||||
|
for (let rowOffset = -1; rowOffset <= 1; rowOffset++) {
|
||||||
|
for (let columnOffset = -1; columnOffset <= 1; columnOffset++) {
|
||||||
|
if (smoothMask[(row + rowOffset) * width + column + columnOffset]) smoothNeighbors += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (smoothNeighbors < 7) continue;
|
||||||
|
const sourceLab = sourceLabs[index]!;
|
||||||
|
const sourceChroma = Math.hypot(sourceLab[1], sourceLab[2]);
|
||||||
|
// Near-neutral pixels should use the neutral MARD series. Otherwise a
|
||||||
|
// barely visible numerical hue can create a conspicuous lavender/pink
|
||||||
|
// stripe inside an otherwise blue-to-cream gradient.
|
||||||
|
const gradientCandidates = sourceChroma < 0.035
|
||||||
|
? PALETTE.filter((color) => color.code.startsWith("H"))
|
||||||
|
: PALETTE;
|
||||||
|
const exact = nearestColor(sourceLab, gradientCandidates);
|
||||||
|
if (!gradientPalette.has(exact.code) && gradientPalette.size >= 8) continue;
|
||||||
|
gradientPalette.add(exact.code);
|
||||||
|
result[index] = exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanGloballyRareIsolatedColors(colors: Array<BeadColor | null>, width: number, height: number) {
|
||||||
|
const usage = new Map<string, number>();
|
||||||
|
colors.forEach((color) => { if (color) usage.set(color.code, (usage.get(color.code) ?? 0) + 1); });
|
||||||
|
const cleaned = [...colors];
|
||||||
|
let changed = 0;
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
for (let column = 0; column < width; column++) {
|
||||||
|
const index = row * width + column;
|
||||||
|
const current = colors[index];
|
||||||
|
if (!current || (usage.get(current.code) ?? 0) > 2) continue;
|
||||||
|
const neighbors = new Map<string, { color: BeadColor; count: number }>();
|
||||||
|
for (let rowOffset = -1; rowOffset <= 1; rowOffset++) {
|
||||||
|
for (let columnOffset = -1; columnOffset <= 1; columnOffset++) {
|
||||||
|
if (rowOffset === 0 && columnOffset === 0) continue;
|
||||||
|
const neighborRow = row + rowOffset;
|
||||||
|
const neighborColumn = column + columnOffset;
|
||||||
|
if (neighborRow < 0 || neighborColumn < 0 || neighborRow >= height || neighborColumn >= width) continue;
|
||||||
|
const neighbor = colors[neighborRow * width + neighborColumn];
|
||||||
|
if (!neighbor || neighbor.code === current.code) continue;
|
||||||
|
const entry = neighbors.get(neighbor.code) ?? { color: neighbor, count: 0 };
|
||||||
|
entry.count += 1;
|
||||||
|
neighbors.set(neighbor.code, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const majority = [...neighbors.values()].sort((a, b) => b.count - a.count)[0];
|
||||||
|
if (!majority) continue;
|
||||||
|
const requiredSupport = (usage.get(current.code) ?? 0) === 1 ? 3 : 5;
|
||||||
|
if (majority.count < requiredSupport) continue;
|
||||||
|
cleaned[index] = majority.color;
|
||||||
|
changed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { colors: cleaned, changed };
|
||||||
|
}
|
||||||
|
|
||||||
function sampleDominantRegions(
|
function sampleDominantRegions(
|
||||||
image: HTMLImageElement,
|
image: HTMLImageElement,
|
||||||
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,
|
||||||
@@ -293,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);
|
||||||
@@ -486,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;
|
||||||
@@ -501,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() {
|
||||||
@@ -550,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);
|
||||||
@@ -567,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;
|
||||||
@@ -607,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;
|
||||||
@@ -616,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 {
|
||||||
@@ -625,12 +756,12 @@ 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);
|
||||||
const limitedPalette = chooseDistinctMardColors(clusters, Math.max(2, Math.min(colorLimit, PALETTE.length)));
|
const limitedPalette = chooseDistinctMardColors(clusters, Math.max(2, Math.min(colorLimit, PALETTE.length)));
|
||||||
const clusterMatches = clusters.map((cluster) => nearestColor(cluster.lab, limitedPalette));
|
const clusterMatches = matchClustersToPalette(clusters, limitedPalette, colorLimit);
|
||||||
let matchedColors: Array<BeadColor | null> = pixelKeys.map((key) => key === null ? null : clusterMatches[binClusters.get(key) ?? 0]);
|
let matchedColors: Array<BeadColor | null> = pixelKeys.map((key) => key === null ? null : clusterMatches[binClusters.get(key) ?? 0]);
|
||||||
let cleanedCount = 0;
|
let cleanedCount = 0;
|
||||||
if (dominantConfidence) {
|
if (dominantConfidence) {
|
||||||
@@ -640,6 +771,10 @@ export default function Home() {
|
|||||||
const transitionCleaned = cleanThinTransitionBands(matchedColors, width, height);
|
const transitionCleaned = cleanThinTransitionBands(matchedColors, width, height);
|
||||||
matchedColors = transitionCleaned.colors;
|
matchedColors = transitionCleaned.colors;
|
||||||
cleanedCount += transitionCleaned.changed;
|
cleanedCount += transitionCleaned.changed;
|
||||||
|
matchedColors = preserveSmoothGradientSteps(matchedColors, data, width, height, colorLimit);
|
||||||
|
const rareCleaned = cleanGloballyRareIsolatedColors(matchedColors, width, height);
|
||||||
|
matchedColors = rareCleaned.colors;
|
||||||
|
cleanedCount += rareCleaned.changed;
|
||||||
}
|
}
|
||||||
const converted = matchedColors.map((color) => ({ color }));
|
const converted = matchedColors.map((color) => ({ color }));
|
||||||
setGridWidth(width);
|
setGridWidth(width);
|
||||||
@@ -672,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.
|
||||||
@@ -693,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>();
|
||||||
@@ -809,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) => {
|
||||||
@@ -1020,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>
|
||||||
@@ -1128,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>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"name": "rolldown-runtime"
|
"name": "rolldown-runtime"
|
||||||
},
|
},
|
||||||
"app/page.tsx": {
|
"app/page.tsx": {
|
||||||
"file": "_next/static/chunks/page-Clx65Ifw.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-BVJGUT0n.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-iZ0Yke16.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
@@ -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.DWpRPKap.css" data-rsc-css-href="/_next/static/css/index.DWpRPKap.css" data-precedence="vite-rsc/importer-resources"/><link rel="modulepreload" fetchPriority="low" href="/_next/static/chunks/index-iZ0Yke16.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-iZ0Yke16.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,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";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-iZ0Yke16.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.DWpRPKap.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.DWpRPKap.css\",{\"rel\":\"stylesheet\",\"precedence\":\"vite-rsc/importer-resources\",\"href\":\"/_next/static/css/index.DWpRPKap.css\",\"data-rsc-css-href\":\"/_next/static/css/index.DWpRPKap.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,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";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>
|
||||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -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-iZ0Yke16.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
File diff suppressed because one or more lines are too long
+2
-2
@@ -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.DWpRPKap.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.DWpRPKap.css",{"rel":"stylesheet","precedence":"vite-rsc/importer-resources","href":"/_next/static/css/index.DWpRPKap.css","data-rsc-css-href":"/_next/static/css/index.DWpRPKap.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":"031ba822-b678-4785-9e41-6342ae4e309a","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,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"appBrowserEntry": "_next/static/chunks/index-iZ0Yke16.js"
|
"appBrowserEntry": "_next/static/chunks/index-DAoL2pdt.js"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user