Changes to image sort
This commit is contained in:
@ -4,4 +4,12 @@ const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
4138
package-lock.json
generated
4138
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@ -43,17 +43,19 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"culori": "^4.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "15.4.2",
|
||||
"next-auth": "^4.24.11",
|
||||
"next": "16.0.0",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"oklab": "^0.0.2-alpha.1",
|
||||
"platejs": "^49.1.5",
|
||||
"react": "19.1.0",
|
||||
"react": "19.2.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
@ -67,16 +69,21 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/culori": "^4.0.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.2",
|
||||
"eslint-config-next": "16.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"prisma": "^6.12.0",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "PortfolioImage" ADD COLUMN "okLabA" DOUBLE PRECISION,
|
||||
ADD COLUMN "okLabB" DOUBLE PRECISION,
|
||||
ADD COLUMN "okLabL" DOUBLE PRECISION,
|
||||
ADD COLUMN "sortKey" INTEGER;
|
||||
@ -34,7 +34,11 @@ model PortfolioImage {
|
||||
altText String?
|
||||
description String?
|
||||
month Int?
|
||||
sortKey Int?
|
||||
year Int?
|
||||
okLabL Float?
|
||||
okLabA Float?
|
||||
okLabB Float?
|
||||
creationDate DateTime?
|
||||
|
||||
albumId String?
|
||||
|
||||
159
prisma/scripts/backfillSortKeys.ts
Normal file
159
prisma/scripts/backfillSortKeys.ts
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
Backfill sortKey for images using a Hilbert (order 15) index on OKLab (a,b).
|
||||
|
||||
- Safe to resume: processes in ID order, skips rows with existing sortKey.
|
||||
- Batches writes via Prisma transaction.
|
||||
- Uses existing swatches from imageColor/color; if none, falls back to mid-gray.
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { converter, parse } from "culori";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const toOklab = converter("oklab");
|
||||
|
||||
// ---------- Config ----------
|
||||
const BATCH_SIZE = 300; // tune for your DB
|
||||
const LOG_EVERY = 5; // batches
|
||||
// Fixed OKLab box for incremental safety (covers real gamut with cushion)
|
||||
const A_MIN = -0.5, A_MAX = 0.5;
|
||||
const B_MIN = -0.5, B_MAX = 0.5;
|
||||
|
||||
// Weight Node-Vibrant swatch types toward a perceptual centroid.
|
||||
// Adjust these if your dataset prefers different emphasis.
|
||||
const TYPE_WEIGHTS: Record<string, number> = {
|
||||
Vibrant: 0.7,
|
||||
Muted: 0.15,
|
||||
DarkVibrant: 0.07,
|
||||
DarkMuted: 0.05,
|
||||
LightVibrant: 0.02,
|
||||
LightMuted: 0.01,
|
||||
};
|
||||
|
||||
// ---------- Math helpers ----------
|
||||
function clamp01(x: number) { return Math.max(0, Math.min(1, x)); }
|
||||
function norm(x: number, lo: number, hi: number) { return clamp01((x - lo) / (hi - lo)); }
|
||||
|
||||
// Hilbert curve index for a 32768 x 32768 grid (order = 15) → fits INT4
|
||||
function hilbertIndex15(x01: number, y01: number): number {
|
||||
let x = Math.floor(clamp01(x01) * 32767);
|
||||
let y = Math.floor(clamp01(y01) * 32767);
|
||||
let index = 0;
|
||||
for (let s = 1 << 14; s > 0; s >>= 1) {
|
||||
const rx = (x & s) ? 1 : 0;
|
||||
const ry = (y & s) ? 1 : 0;
|
||||
index += s * s * ((3 * rx) ^ ry);
|
||||
if (ry === 0) {
|
||||
if (rx === 1) { x = 32767 - x; y = 32767 - y; }
|
||||
const t = x; x = y; y = t;
|
||||
}
|
||||
}
|
||||
// stays within 0..(2^30-1)
|
||||
return index | 0;
|
||||
}
|
||||
|
||||
type SwatchRow = { imageId: string; type: string; color: { hex: string } };
|
||||
|
||||
function centroidFromSwatches(swatches: SwatchRow[]) {
|
||||
// Build hexByType and choose a fallback if any missing
|
||||
const byType: Record<string, string | undefined> = {};
|
||||
for (const s of swatches) {
|
||||
// Ensure we only keep one hex per type (first wins)
|
||||
if (!byType[s.type]) byType[s.type] = s.color?.hex;
|
||||
}
|
||||
const fallbackHex =
|
||||
byType["Vibrant"] ||
|
||||
byType["Muted"] ||
|
||||
byType["DarkVibrant"] ||
|
||||
byType["DarkMuted"] ||
|
||||
byType["LightVibrant"] ||
|
||||
byType["LightMuted"];
|
||||
|
||||
if (!fallbackHex) {
|
||||
// No swatches at all: mid-gray
|
||||
return { l: 0.5, a: 0, b: 0 };
|
||||
}
|
||||
|
||||
let L = 0, A = 0, B = 0, W = 0;
|
||||
for (const [type, w] of Object.entries(TYPE_WEIGHTS)) {
|
||||
const hex = byType[type] ?? fallbackHex;
|
||||
if (!hex || w <= 0) continue;
|
||||
const c = toOklab(parse(hex));
|
||||
if (!c) continue;
|
||||
L += c.l * w; A += c.a * w; B += c.b * w; W += w;
|
||||
}
|
||||
if (W === 0) return { l: 0.5, a: 0, b: 0 };
|
||||
return { l: L / W, a: A / W, b: B / W };
|
||||
}
|
||||
|
||||
async function fetchBatch(afterId?: string) {
|
||||
// Only rows that still need a key; stable ID ordering so we can resume
|
||||
return prisma.portfolioImage.findMany({
|
||||
where: { sortKey: null },
|
||||
orderBy: { id: "asc" },
|
||||
...(afterId ? { cursor: { id: afterId }, skip: 1 } : {}),
|
||||
take: BATCH_SIZE,
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
async function backfill() {
|
||||
let processed = 0;
|
||||
let batches = 0;
|
||||
let cursor: string | undefined = undefined;
|
||||
|
||||
while (true) {
|
||||
const images = await fetchBatch(cursor);
|
||||
if (images.length === 0) break;
|
||||
|
||||
const ids = images.map((i: { id: string }) => i.id);
|
||||
cursor = ids[ids.length - 1];
|
||||
|
||||
// Pull all swatches for this batch in one go
|
||||
const swatches = await prisma.imageColor.findMany({
|
||||
where: { imageId: { in: ids } },
|
||||
select: { imageId: true, type: true, color: { select: { hex: true } } },
|
||||
});
|
||||
|
||||
// Group swatches by imageId
|
||||
const byImage = new Map<string, SwatchRow[]>();
|
||||
for (const s of swatches) {
|
||||
const arr = byImage.get(s.imageId) ?? [];
|
||||
arr.push(s as SwatchRow);
|
||||
byImage.set(s.imageId, arr);
|
||||
}
|
||||
|
||||
// Prepare updates
|
||||
const updates = images.map(({ id }: { id: string }) => {
|
||||
const rows = byImage.get(id) ?? [];
|
||||
const { a, b, l } = centroidFromSwatches(rows);
|
||||
const ax = norm(a, A_MIN, A_MAX);
|
||||
const bx = norm(b, B_MIN, B_MAX);
|
||||
const sortKey = hilbertIndex15(ax, bx);
|
||||
// Optional: also persist okLab* for debugging/analytics
|
||||
return prisma.portfolioImage.update({
|
||||
where: { id },
|
||||
data: { sortKey, okLabL: l, okLabA: a, okLabB: b },
|
||||
});
|
||||
});
|
||||
|
||||
await prisma.$transaction(updates);
|
||||
processed += images.length;
|
||||
batches += 1;
|
||||
|
||||
if (batches % LOG_EVERY === 0) {
|
||||
console.log(`[backfill] processed ${processed} images… last id: ${cursor}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[backfill] done. processed ${processed} images total.`);
|
||||
}
|
||||
|
||||
backfill()
|
||||
.catch((e) => {
|
||||
console.error("[backfill] error:", e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
52
src/actions/portfolio/images/generateColorSortKey.ts
Normal file
52
src/actions/portfolio/images/generateColorSortKey.ts
Normal file
@ -0,0 +1,52 @@
|
||||
"use server";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { centroidFromSwatches, sortKeyFromCentroid } from "@/utils/colorSortKey";
|
||||
|
||||
const BATCH_SIZE = 300;
|
||||
|
||||
export async function getRemainingSortKeyBackfill() {
|
||||
return prisma.portfolioImage.count({ where: { sortKey: null } });
|
||||
}
|
||||
|
||||
export async function backfillSortKeysBatch() {
|
||||
// Grab a stable batch (ASC id so it’s deterministic)
|
||||
const images = await prisma.portfolioImage.findMany({
|
||||
where: { sortKey: null },
|
||||
orderBy: { id: "asc" },
|
||||
take: BATCH_SIZE,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const ids = images.map((i: { id: string }) => i.id);
|
||||
|
||||
// Get all swatches for those images
|
||||
const swatches = await prisma.imageColor.findMany({
|
||||
where: { imageId: { in: ids } },
|
||||
select: { imageId: true, type: true, color: { select: { hex: true } } },
|
||||
});
|
||||
|
||||
// Group into hexByType per image
|
||||
const byImage = new Map<string, Record<string, string | undefined>>();
|
||||
for (const s of swatches) {
|
||||
const map = byImage.get(s.imageId) ?? {};
|
||||
if (!map[s.type]) map[s.type] = s.color?.hex ?? undefined;
|
||||
byImage.set(s.imageId, map);
|
||||
}
|
||||
|
||||
// Prepare updates
|
||||
const updates = images.map(({ id }: { id: string }) => {
|
||||
const hexByType = byImage.get(id) ?? {};
|
||||
const { l, a, b } = centroidFromSwatches(hexByType);
|
||||
const sortKey = sortKeyFromCentroid(a, b);
|
||||
return prisma.portfolioImage.update({
|
||||
where: { id },
|
||||
data: { sortKey, okLabL: l, okLabA: a, okLabB: b },
|
||||
});
|
||||
});
|
||||
|
||||
await prisma.$transaction(updates);
|
||||
|
||||
const remaining = await getRemainingSortKeyBackfill();
|
||||
return { processed: ids.length, remaining };
|
||||
}
|
||||
@ -4,8 +4,75 @@ import prisma from "@/lib/prisma";
|
||||
import { VibrantSwatch } from "@/types/VibrantSwatch";
|
||||
import { getImageBufferFromS3 } from "@/utils/getImageBufferFromS3";
|
||||
import { generateColorName, rgbToHex } from "@/utils/uploadHelper";
|
||||
import { converter, parse } from "culori";
|
||||
import { Vibrant } from "node-vibrant/node";
|
||||
|
||||
const toOklab = converter("oklab");
|
||||
const A_MIN = -0.5, A_MAX = 0.5;
|
||||
const B_MIN = -0.5, B_MAX = 0.5;
|
||||
|
||||
function clamp01(x: number) {
|
||||
return Math.max(0, Math.min(1, x));
|
||||
}
|
||||
|
||||
function norm(x: number, lo: number, hi: number) {
|
||||
return clamp01((x - lo) / (hi - lo));
|
||||
}
|
||||
|
||||
function hilbertIndex15(x01: number, y01: number): number {
|
||||
let x = Math.floor(clamp01(x01) * 32767);
|
||||
let y = Math.floor(clamp01(y01) * 32767);
|
||||
let index = 0;
|
||||
for (let s = 1 << 14; s > 0; s >>= 1) { // start at bit 14
|
||||
const rx = (x & s) ? 1 : 0;
|
||||
const ry = (y & s) ? 1 : 0;
|
||||
index += s * s * ((3 * rx) ^ ry);
|
||||
if (ry === 0) {
|
||||
if (rx === 1) { x = 32767 - x; y = 32767 - y; }
|
||||
const t = x; x = y; y = t;
|
||||
}
|
||||
}
|
||||
return index >>> 0;
|
||||
}
|
||||
|
||||
function centroidFromPaletteHexes(hexByType: Record<string, string | undefined>) {
|
||||
// Tweak weights as you like. Biasing toward Vibrant keeps things “readable”.
|
||||
const weights: Record<string, number> = {
|
||||
Vibrant: 0.7,
|
||||
Muted: 0.15,
|
||||
DarkVibrant: 0.07,
|
||||
DarkMuted: 0.05,
|
||||
LightVibrant: 0.02,
|
||||
LightMuted: 0.01,
|
||||
};
|
||||
|
||||
// Ensure we have at least a vibrant color to anchor on
|
||||
const fallbackHex =
|
||||
hexByType["Vibrant"] ||
|
||||
hexByType["Muted"] ||
|
||||
hexByType["DarkVibrant"] ||
|
||||
hexByType["DarkMuted"] ||
|
||||
hexByType["LightVibrant"] ||
|
||||
hexByType["LightMuted"];
|
||||
|
||||
let L = 0, A = 0, B = 0, W = 0;
|
||||
|
||||
const entries = Object.entries(weights);
|
||||
for (const [type, w] of entries) {
|
||||
const hex = hexByType[type] ?? fallbackHex;
|
||||
if (!hex || w <= 0) continue;
|
||||
const c = toOklab(parse(hex));
|
||||
if (!c) continue;
|
||||
L += c.l * w; A += c.a * w; B += c.b * w; W += w;
|
||||
}
|
||||
|
||||
if (W === 0) {
|
||||
// Should be rare; default to mid-gray
|
||||
return { l: 0.5, a: 0, b: 0 };
|
||||
}
|
||||
return { l: L / W, a: A / W, b: B / W };
|
||||
}
|
||||
|
||||
export async function generateImageColors(imageId: string, fileKey: string, fileType?: string) {
|
||||
const buffer = await getImageBufferFromS3(fileKey, fileType);
|
||||
const palette = await Vibrant.from(buffer).getPalette();
|
||||
@ -57,7 +124,23 @@ export async function generateImageColors(imageId: string, fileKey: string, file
|
||||
colorId: color.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Compute OKLab centroid → Hilbert sortKey (incremental-safe)
|
||||
const hexByType: Record<string, string | undefined> = Object.fromEntries(
|
||||
vibrantHexes.map(({ type, hex }) => [type, hex])
|
||||
);
|
||||
|
||||
const { l, a, b } = centroidFromPaletteHexes(hexByType);
|
||||
const ax = norm(a, A_MIN, A_MAX);
|
||||
const bx = norm(b, B_MIN, B_MAX);
|
||||
const sortKey = hilbertIndex15(ax, bx);
|
||||
|
||||
// 3) Store on the Image (plus optional OKLab fields)
|
||||
await prisma.portfolioImage.update({
|
||||
where: { id: imageId },
|
||||
data: { sortKey, okLabL: l, okLabA: a, okLabB: b },
|
||||
});
|
||||
|
||||
return await prisma.imageColor.findMany({
|
||||
where: { imageId },
|
||||
|
||||
76
src/components/portfolio/images/BackfillButton.tsx
Normal file
76
src/components/portfolio/images/BackfillButton.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { backfillSortKeysBatch, getRemainingSortKeyBackfill } from "@/actions/portfolio/images/generateColorSortKey";
|
||||
import { Button } from "@/components/ui/button"; // shadcn
|
||||
import { Loader2, Palette } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function BackfillButton({ className }: Props) {
|
||||
const [remaining, setRemaining] = React.useState<number | null>(null);
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
// initial load (client-only)
|
||||
React.useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const n = await getRemainingSortKeyBackfill();
|
||||
if (mounted) setRemaining(n);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (mounted) setRemaining(0);
|
||||
}
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
const disabled = remaining === null || isPending || remaining === 0;
|
||||
|
||||
const onClick = () => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const res = await backfillSortKeysBatch();
|
||||
setRemaining(res.remaining);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// swap for your toast if you like
|
||||
alert("Backfill failed. See console.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||
Processing…
|
||||
</>
|
||||
) : remaining === null ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
||||
Loading…
|
||||
</>
|
||||
) : remaining > 0 ? (
|
||||
<>
|
||||
<Palette className="mr-2 h-4 w-4" aria-hidden />
|
||||
Backfill color sort keys · {remaining} left
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Palette className="mr-2 h-4 w-4" aria-hidden />
|
||||
All done
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { PortfolioAlbum, PortfolioType } from "@/generated/prisma";
|
||||
import { SortAscIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import BackfillButton from "./BackfillButton";
|
||||
|
||||
type FilterBarProps = {
|
||||
types: PortfolioType[];
|
||||
@ -47,13 +48,19 @@ export default function FilterBar({
|
||||
|
||||
const sortHref = `${pathname}/sort?${params.toString()}`;
|
||||
|
||||
const sortByColor = () => {
|
||||
params.set("sortBy", "color");
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end gap-4">
|
||||
<Link href={sortHref} className="flex gap-2 items-center cursor-pointer bg-secondary hover:bg-secondary/90 text-secondary-foreground px-4 py-2 rounded">
|
||||
<SortAscIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Sort images
|
||||
</Link>
|
||||
<BackfillButton />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-6 pb-6">
|
||||
|
||||
64
src/utils/colorSortKey.ts
Normal file
64
src/utils/colorSortKey.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { converter, parse } from "culori";
|
||||
const toOklab = converter("oklab");
|
||||
|
||||
// Fixed OKLab box so inserts/edits are incremental-safe
|
||||
const A_MIN = -0.5, A_MAX = 0.5;
|
||||
const B_MIN = -0.5, B_MAX = 0.5;
|
||||
|
||||
const TYPE_WEIGHTS: Record<string, number> = {
|
||||
Vibrant: 0.7,
|
||||
Muted: 0.15,
|
||||
DarkVibrant: 0.07,
|
||||
DarkMuted: 0.05,
|
||||
LightVibrant: 0.02,
|
||||
LightMuted: 0.01,
|
||||
};
|
||||
|
||||
function clamp01(x: number) { return Math.max(0, Math.min(1, x)); }
|
||||
function norm(x: number, lo: number, hi: number) { return clamp01((x - lo) / (hi - hi ? hi - lo : 1)); }
|
||||
|
||||
// Hilbert index for 32768 x 32768 grid (order 15) → fits INT4
|
||||
export function hilbertIndex15(x01: number, y01: number): number {
|
||||
let x = Math.floor(clamp01(x01) * 32767);
|
||||
let y = Math.floor(clamp01(y01) * 32767);
|
||||
let index = 0;
|
||||
for (let s = 1 << 14; s > 0; s >>= 1) {
|
||||
const rx = (x & s) ? 1 : 0;
|
||||
const ry = (y & s) ? 1 : 0;
|
||||
index += s * s * ((3 * rx) ^ ry);
|
||||
if (ry === 0) {
|
||||
if (rx === 1) { x = 32767 - x; y = 32767 - y; }
|
||||
const t = x; x = y; y = t;
|
||||
}
|
||||
}
|
||||
return index | 0;
|
||||
}
|
||||
|
||||
export function centroidFromSwatches(hexByType: Record<string, string | undefined>) {
|
||||
const fallback =
|
||||
hexByType["Vibrant"] ??
|
||||
hexByType["Muted"] ??
|
||||
hexByType["DarkVibrant"] ??
|
||||
hexByType["DarkMuted"] ??
|
||||
hexByType["LightVibrant"] ??
|
||||
hexByType["LightMuted"];
|
||||
|
||||
if (!fallback) return { l: 0.5, a: 0, b: 0 };
|
||||
|
||||
let L = 0, A = 0, B = 0, W = 0;
|
||||
for (const [type, w] of Object.entries(TYPE_WEIGHTS)) {
|
||||
const hex = hexByType[type] ?? fallback;
|
||||
if (!hex || w <= 0) continue;
|
||||
const c = toOklab(parse(hex));
|
||||
if (!c) continue;
|
||||
L += c.l * w; A += c.a * w; B += c.b * w; W += w;
|
||||
}
|
||||
if (!W) return { l: 0.5, a: 0, b: 0 };
|
||||
return { l: L / W, a: A / W, b: B / W };
|
||||
}
|
||||
|
||||
export function sortKeyFromCentroid(a: number, b: number) {
|
||||
const ax = norm(a, A_MIN, A_MAX);
|
||||
const bx = norm(b, B_MIN, B_MAX);
|
||||
return hilbertIndex15(ax, bx);
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@ -11,7 +15,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@ -19,9 +23,19 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user