Replace history color icon with pattern thumbnail

This commit is contained in:
wuyanwanwu
2026-08-14 21:56:14 +08:00
parent 6f101661c7
commit 785a089518
14 changed files with 46 additions and 25 deletions
+3 -3
View File
@@ -128,9 +128,9 @@ input[type="range"] { accent-color: var(--green); }
.history-heading p:not(.section-kicker) { margin: 8px 0 0; color: var(--muted); font-size: 12px; }
.history-heading .search-box { width: min(330px, 100%); }
.history-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; padding-top: 18px; }
.history-card { min-width: 0; padding: 16px; display: grid; grid-template-columns: 58px minmax(0,1fr); gap: 12px; border: 1px solid var(--line); background: var(--card); }
.history-swatches { width: 58px; height: 58px; display: flex; overflow: hidden; border: 1px solid var(--ink); }
.history-swatches i { min-width: 3px; }
.history-card { min-width: 0; padding: 16px; display: grid; grid-template-columns: 86px minmax(0,1fr); gap: 13px; border: 1px solid var(--line); background: var(--card); }
.history-thumbnail { width: 86px; height: 86px; display: grid; place-items: center; overflow: hidden; border: 1px solid var(--ink); background-color: #f8f6f0; background-image: linear-gradient(45deg, #dedbd3 25%, transparent 25%), linear-gradient(-45deg, #dedbd3 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #dedbd3 75%), linear-gradient(-45deg, transparent 75%, #dedbd3 75%); background-position: 0 0, 0 5px, 5px -5px, -5px 0; background-size: 10px 10px; }
.history-thumbnail canvas { display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; image-rendering: pixelated; }
.history-card h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 3px 0 7px; font-size: 14px; }
.history-card p { margin: 0; color: var(--muted); font-size: 10px; }
.history-actions { grid-column: 1 / -1; display: flex; gap: 7px; justify-content: flex-end; }
+27 -6
View File
@@ -184,6 +184,7 @@ const PALETTE: BeadColor[] = MARD_221.map(([code, hex]) => {
const rgb: [number, number, number] = [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
return { code, name: `MARD ${MARD_SERIES_NAMES[code[0]]}`, hex, rgb, lab: rgbToOklab(rgb) };
});
const PALETTE_BY_CODE = new Map(PALETTE.map((color) => [color.code, color]));
const hexToRgb = (hex: string): [number, number, number] => [
parseInt(hex.slice(1, 3), 16),
@@ -191,6 +192,29 @@ const hexToRgb = (hex: string): [number, number, number] => [
parseInt(hex.slice(5, 7), 16),
];
function HistoryThumbnail({ entry }: { entry: SavedPattern }) {
const thumbnailRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = thumbnailRef.current;
if (!canvas) return;
canvas.width = Math.max(1, entry.width);
canvas.height = Math.max(1, entry.height);
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
entry.codes.forEach((code, index) => {
if (!code) return;
const color = PALETTE_BY_CODE.get(code);
if (!color) return;
ctx.fillStyle = color.hex;
ctx.fillRect(index % entry.width, Math.floor(index / entry.width), 1, 1);
});
}, [entry]);
return <div className="history-thumbnail" aria-label={`${entry.name} 的完整格子图缩略图`}>
<canvas ref={thumbnailRef} />
</div>;
}
function nearestColor(lab: Oklab, palette: BeadColor[]) {
let closest = palette[0];
let smallest = Number.POSITIVE_INFINITY;
@@ -1455,7 +1479,7 @@ export default function Home() {
if (color) {
ctx.fillStyle = color.hex;
ctx.fillRect(x, y, cell, cell);
const label = `${rowNumberToLetters(row + 1)}${column + 1}`;
const label = color.code;
const [red, green, blue] = hexToRgb(color.hex);
ctx.fillStyle = red * 0.299 + green * 0.587 + blue * 0.114 > 160 ? "#18211d" : "#ffffff";
let fontSize = Math.max(6, Math.floor(cell * 0.29));
@@ -1532,7 +1556,7 @@ export default function Home() {
link.href = url;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
setStatus("PNG 已生成:格内为行字母+列数字,底部为色块和 MARD 色号");
setStatus("PNG 已生成:格内为 MARD 色号,底部为色块和对应色号");
logUsage({ event: "download_png", width: gridWidth, height: gridHeight, colors: usedColors.length });
}, "image/png");
};
@@ -1715,11 +1739,8 @@ export default function Home() {
<div className="search-box"><span></span><input value={historyQuery} onChange={(e) => setHistoryQuery(e.target.value)} placeholder="按名称或 32x32 查询" /></div>
</div>
{filteredHistory.length ? <div className="history-grid">{filteredHistory.map((entry) => {
const colorCounts = new Map<string, number>();
entry.codes.forEach((code) => { if (code) colorCounts.set(code, (colorCounts.get(code) ?? 0) + 1); });
const topCodes = [...colorCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
return <article className="history-card" key={entry.id}>
<div className="history-swatches">{topCodes.map(([code, count]) => <i key={code} style={{ backgroundColor: PALETTE.find((color) => color.code === code)?.hex, flexGrow: count }} />)}</div>
<HistoryThumbnail entry={entry} />
<div><h3>{entry.name}</h3><p>{entry.width} × {entry.height} · {new Date(entry.savedAt).toLocaleString("zh-CN", { hour12: false })}</p></div>
<div className="history-actions"><button onClick={() => restorePattern(entry)}></button><button onClick={() => removePattern(entry.id)}></button></div>
</article>;