mirror of
https://github.com/tiennm99/rplace.git
synced 2026-10-15 14:16:39 +00:00
chore: prune dead code, unused exports, and orphan scripts
- scripts/: remove upload-colors.js (broken — imported removed constants) and image-to-colors.js (orphan — flow moved fully client-side to ImageImporter). Drop sharp from devDependencies. - constants.js: drop unused BITS_PER_PIXEL export. - dither-kernels.js: make KERNEL_* consts internal; only the ERROR_DIFFUSION_KERNELS map is consumed externally. - image-pipeline.js: drop unused pipeline.source() accessor. - image-to-palette.js: drop `options.dither` legacy boolean shim (no external caller uses it) and its test. - canvas-storage.js, App.svelte: drop comments that restated names.
This commit is contained in:
1 parent
8c6b9aa191
commit
6b790f3892
11 files changed
+24
-297
No files matched your search
Generated
+13
-4
@@ -12,7 +12,6 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"ioredis": "^5.10.1",
|
||||
"sharp": "^0.34.5",
|
||||
"svelte": "^5.28.2",
|
||||
"testcontainers": "^11.14.0",
|
||||
"vite": "^6.3.2",
|
||||
@@ -3397,6 +3396,16 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/miniflare/node_modules/undici": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz",
|
||||
"integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
@@ -4348,9 +4357,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz",
|
||||
"integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==",
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
|
||||
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"ioredis": "^5.10.1",
|
||||
"sharp": "^0.34.5",
|
||||
"svelte": "^5.28.2",
|
||||
"testcontainers": "^11.14.0",
|
||||
"vite": "^6.3.2",
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Convert PNG/JPG/WebP image to rplace palette JSON.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/image-to-colors.js <input> [-o output.json] [--alpha-threshold N]
|
||||
*
|
||||
* Output JSON: { width, height, pixels: number[] }
|
||||
* - pixels is a flat row-major array of length width*height
|
||||
* - each entry is a palette index [0..31], or -1 for transparent (skipped on upload)
|
||||
*/
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { parseArgs } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../src/lib/constants.js';
|
||||
import { rgbaToPalette } from '../src/lib/image-to-palette.js';
|
||||
import { resizeRgba } from '../src/lib/image-resize.js';
|
||||
import { transformRgba } from '../src/lib/image-transform.js';
|
||||
import { applyColorCorrection } from '../src/lib/image-color-correction.js';
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
output: { type: 'string', short: 'o' },
|
||||
'alpha-threshold': { type: 'string', default: '128' },
|
||||
dither: { type: 'boolean', default: false },
|
||||
'dither-method': { type: 'string' }, // see DITHER_METHODS; overrides --dither
|
||||
width: { type: 'string' },
|
||||
height: { type: 'string' },
|
||||
method: { type: 'string', default: 'nearest' }, // nearest | bilinear | box
|
||||
'flip-h': { type: 'boolean', default: false },
|
||||
'flip-v': { type: 'boolean', default: false },
|
||||
rotate: { type: 'string', default: '0' }, // 0 | 90 | 180 | 270 (CW)
|
||||
brightness: { type: 'string', default: '0' },
|
||||
contrast: { type: 'string', default: '0' },
|
||||
saturation: { type: 'string', default: '0' },
|
||||
gamma: { type: 'string', default: '1' },
|
||||
'skip-white': { type: 'boolean', default: false },
|
||||
'white-threshold': { type: 'string', default: '230' },
|
||||
'paint-transparent': { type: 'boolean', default: false },
|
||||
},
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (positionals.length < 1) {
|
||||
console.error('Usage: node scripts/image-to-colors.js <input> [-o output.json] [--alpha-threshold 128] [--dither|--dither-method <name>] [--width N] [--height N] [--method nearest|bilinear|box] [--flip-h] [--flip-v] [--rotate 0|90|180|270] [--brightness -100..100] [--contrast -100..100] [--saturation -100..100] [--gamma 0.1..3]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const input = positionals[0];
|
||||
const output = values.output ?? input.replace(/\.[^.]+$/, '.json');
|
||||
const alphaThreshold = parseInt(values['alpha-threshold'], 10);
|
||||
|
||||
if (!Number.isFinite(alphaThreshold) || alphaThreshold < 0 || alphaThreshold > 255) {
|
||||
console.error('--alpha-threshold must be an integer in [0, 255]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { data, info } = await sharp(input)
|
||||
.ensureAlpha()
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
|
||||
const { width: rawW, height: rawH } = info;
|
||||
|
||||
const rotation = parseInt(values.rotate, 10);
|
||||
if (![0, 90, 180, 270].includes(rotation)) {
|
||||
console.error('--rotate must be 0, 90, 180, or 270');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const transformed = (values['flip-h'] || values['flip-v'] || rotation !== 0)
|
||||
? transformRgba(data, rawW, rawH, { flipH: values['flip-h'], flipV: values['flip-v'], rotation })
|
||||
: { rgba: data, width: rawW, height: rawH };
|
||||
const srcW = transformed.width;
|
||||
const srcH = transformed.height;
|
||||
|
||||
// Optional resize — defaults to post-transform dims. Aspect-preserve when only one of W/H given.
|
||||
const reqW = values.width ? parseInt(values.width, 10) : null;
|
||||
const reqH = values.height ? parseInt(values.height, 10) : null;
|
||||
let outW = reqW ?? (reqH ? Math.round(srcW * reqH / srcH) : srcW);
|
||||
let outH = reqH ?? (reqW ? Math.round(srcH * reqW / srcW) : srcH);
|
||||
if (outW < 1 || outH < 1) {
|
||||
console.error('Resolved width/height must be >= 1');
|
||||
process.exit(1);
|
||||
}
|
||||
if (outW > CANVAS_WIDTH || outH > CANVAS_HEIGHT) {
|
||||
console.warn(`Warning: output ${outW}x${outH} exceeds canvas ${CANVAS_WIDTH}x${CANVAS_HEIGHT}. Upload will fail at the boundary.`);
|
||||
}
|
||||
|
||||
const resized = (outW === srcW && outH === srcH)
|
||||
? transformed.rgba
|
||||
: resizeRgba(transformed.rgba, srcW, srcH, outW, outH, values.method);
|
||||
|
||||
const brightness = parseFloat(values.brightness);
|
||||
const contrast = parseFloat(values.contrast);
|
||||
const saturation = parseFloat(values.saturation);
|
||||
const gamma = parseFloat(values.gamma);
|
||||
const hasCorrection = brightness !== 0 || contrast !== 0 || saturation !== 0 || gamma !== 1;
|
||||
const working = hasCorrection
|
||||
? applyColorCorrection(resized, outW, outH, { brightness, contrast, saturation, gamma })
|
||||
: resized;
|
||||
|
||||
// sharp raw buffer has channels=4 after ensureAlpha; same layout as Canvas ImageData.
|
||||
const ditherMethod = values['dither-method'] ?? (values.dither ? 'floyd' : 'none');
|
||||
const indices = rgbaToPalette(working, outW, outH, {
|
||||
alphaThreshold,
|
||||
method: ditherMethod,
|
||||
skipWhite: values['skip-white'],
|
||||
whiteThreshold: parseInt(values['white-threshold'], 10),
|
||||
paintTransparent: values['paint-transparent'],
|
||||
});
|
||||
const pixels = Array.from(indices);
|
||||
const opaque = pixels.reduce((n, p) => n + (p >= 0 ? 1 : 0), 0);
|
||||
|
||||
await writeFile(output, JSON.stringify({ width: outW, height: outH, pixels }));
|
||||
const transformNote = (values['flip-h'] || values['flip-v'] || rotation !== 0)
|
||||
? ` [transform: ${[values['flip-h'] && 'flipH', values['flip-v'] && 'flipV', rotation !== 0 && `rot${rotation}`].filter(Boolean).join('+')}]`
|
||||
: '';
|
||||
const resizeNote = (outW !== srcW || outH !== srcH) ? ` (resized from ${srcW}x${srcH} via ${values.method})` : '';
|
||||
const ditherNote = ditherMethod !== 'none' ? ` [dither: ${ditherMethod}]` : '';
|
||||
console.log(`Wrote ${output}: ${outW}x${outH}${resizeNote}${transformNote}, ${opaque}/${pixels.length} opaque pixels${ditherNote}`);
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Upload rplace palette JSON to the canvas at (x, y).
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/upload-colors.js <data.json> <x> <y> [--url=http://localhost:8787] [--batch=256] [--dry]
|
||||
*
|
||||
* Respects the server's credit bucket: starts assuming MAX_CREDITS, tracks regen
|
||||
* locally between batches, and on 429 waits `retryAfter` seconds before retry.
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { parseArgs } from 'node:util';
|
||||
import {
|
||||
CANVAS_WIDTH, CANVAS_HEIGHT,
|
||||
MAX_BATCH_SIZE, MAX_CREDITS, CREDIT_REGEN_RATE,
|
||||
} from '../src/lib/constants.js';
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
url: { type: 'string', default: 'http://localhost:8787' },
|
||||
batch: { type: 'string' },
|
||||
dry: { type: 'boolean', default: false },
|
||||
},
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (positionals.length < 3) {
|
||||
console.error('Usage: node scripts/upload-colors.js <data.json> <x> <y> [--url=http://localhost:8787] [--batch=256] [--dry]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [dataPath, xArg, yArg] = positionals;
|
||||
const originX = parseInt(xArg, 10);
|
||||
const originY = parseInt(yArg, 10);
|
||||
const batchSize = Math.max(1, Math.min(parseInt(values.batch ?? String(MAX_BATCH_SIZE), 10), MAX_BATCH_SIZE));
|
||||
const apiUrl = values.url.replace(/\/+$/, '');
|
||||
const dry = values.dry;
|
||||
|
||||
if (!Number.isInteger(originX) || !Number.isInteger(originY)) {
|
||||
console.error('x and y must be integers');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { width, height, pixels } = JSON.parse(await readFile(dataPath, 'utf8'));
|
||||
|
||||
if (
|
||||
originX < 0 || originY < 0 ||
|
||||
originX + width > CANVAS_WIDTH ||
|
||||
originY + height > CANVAS_HEIGHT
|
||||
) {
|
||||
console.error(`Image ${width}x${height} at (${originX},${originY}) exceeds canvas ${CANVAS_WIDTH}x${CANVAS_HEIGHT}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const toSend = [];
|
||||
for (let i = 0; i < pixels.length; i++) {
|
||||
const color = pixels[i];
|
||||
if (color < 0) continue;
|
||||
const lx = i % width;
|
||||
const ly = (i / width) | 0;
|
||||
toSend.push({ x: originX + lx, y: originY + ly, color });
|
||||
}
|
||||
|
||||
console.log(`Placing ${toSend.length} pixels (image ${width}x${height} at ${originX},${originY}), batch=${batchSize}`);
|
||||
|
||||
if (dry) {
|
||||
console.log('Dry run, no requests sent.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await upload(toSend);
|
||||
|
||||
async function upload(allPixels) {
|
||||
let credits = MAX_CREDITS;
|
||||
let lastUpdate = Date.now();
|
||||
let placed = 0;
|
||||
const started = Date.now();
|
||||
|
||||
for (let i = 0; i < allPixels.length; ) {
|
||||
const batch = allPixels.slice(i, i + batchSize);
|
||||
|
||||
// Refresh local credit estimate with elapsed regen.
|
||||
const now = Date.now();
|
||||
credits = Math.min(MAX_CREDITS, credits + ((now - lastUpdate) / 1000) * CREDIT_REGEN_RATE);
|
||||
lastUpdate = now;
|
||||
|
||||
if (credits < batch.length) {
|
||||
const waitSec = Math.ceil((batch.length - credits) / CREDIT_REGEN_RATE);
|
||||
console.log(` waiting ${waitSec}s for credits (${credits.toFixed(1)}/${batch.length})`);
|
||||
await sleep(waitSec * 1000);
|
||||
credits = Math.min(MAX_CREDITS, credits + waitSec * CREDIT_REGEN_RATE);
|
||||
lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${apiUrl}/api/place`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pixels: batch }),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(` network error: ${err.message}, retrying in 3s`);
|
||||
await sleep(3000);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (res.status === 429) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
const wait = Math.max(1, body.retryAfter ?? 2);
|
||||
console.log(` rate limited, waiting ${wait}s`);
|
||||
await sleep(wait * 1000);
|
||||
credits = 0;
|
||||
lastUpdate = Date.now();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
if (typeof body.credits === 'number') {
|
||||
credits = body.credits;
|
||||
lastUpdate = Date.now();
|
||||
} else {
|
||||
credits = Math.max(0, credits - batch.length);
|
||||
}
|
||||
|
||||
i += batch.length;
|
||||
placed += batch.length;
|
||||
const pct = ((placed / allPixels.length) * 100).toFixed(1);
|
||||
const elapsed = ((Date.now() - started) / 1000).toFixed(0);
|
||||
console.log(` [${placed}/${allPixels.length}] ${pct}% credits=${credits} elapsed=${elapsed}s`);
|
||||
}
|
||||
|
||||
const total = ((Date.now() - started) / 1000).toFixed(0);
|
||||
console.log(`Done. Placed ${placed} pixels in ${total}s`);
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
import DrawToolbar from './components/DrawToolbar.svelte';
|
||||
import ImageImporter from './components/ImageImporter.svelte';
|
||||
|
||||
let selectedColor = $state(0); // palette index 0 = black (first grayscale step)
|
||||
let selectedColor = $state(0);
|
||||
let cursorPos = $state({ x: 0, y: 0 });
|
||||
let zoom = $state(1);
|
||||
let mode = $state('paint');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { redisRaw, redisRawBinary } from './redis-client.js';
|
||||
import { CANVAS_WIDTH, TOTAL_PIXELS, REDIS_CANVAS_KEY } from './constants.js';
|
||||
|
||||
/** Total bytes needed for the canvas — 1 byte per pixel (u8 palette index). */
|
||||
const CANVAS_BYTES = TOTAL_PIXELS;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,9 +3,8 @@ export const CANVAS_WIDTH = 4096;
|
||||
export const CANVAS_HEIGHT = 4096;
|
||||
export const TOTAL_PIXELS = CANVAS_WIDTH * CANVAS_HEIGHT;
|
||||
|
||||
/** Color encoding: 1 byte per pixel, 256 palette entries. Byte-aligned means
|
||||
* the raw Redis bytes are directly the pixel indices — no bit-level decode. */
|
||||
export const BITS_PER_PIXEL = 8;
|
||||
/** 1 byte per pixel, 256 palette entries — the raw Redis bytes are directly
|
||||
* the pixel indices, no bit-level decode. */
|
||||
export const MAX_COLORS = 256;
|
||||
|
||||
/** Rate limiting — one request per second per user, batch size independent. */
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
const FS = 1 / 16;
|
||||
export const KERNEL_FLOYD = [
|
||||
const KERNEL_FLOYD = [
|
||||
[ 1, 0, 7 * FS],
|
||||
[-1, 1, 3 * FS],
|
||||
[ 0, 1, 5 * FS],
|
||||
@@ -17,7 +17,7 @@ export const KERNEL_FLOYD = [
|
||||
|
||||
// Atkinson (original Mac) — only diffuses 6/8 of the error; gives a softer look.
|
||||
const ATK = 1 / 8;
|
||||
export const KERNEL_ATKINSON = [
|
||||
const KERNEL_ATKINSON = [
|
||||
[ 1, 0, ATK],
|
||||
[ 2, 0, ATK],
|
||||
[-1, 1, ATK],
|
||||
@@ -28,7 +28,7 @@ export const KERNEL_ATKINSON = [
|
||||
|
||||
// Jarvis-Judice-Ninke — 12-entry kernel over 3 rows, smoother than FS.
|
||||
const JJN = 1 / 48;
|
||||
export const KERNEL_JARVIS = [
|
||||
const KERNEL_JARVIS = [
|
||||
[ 1, 0, 7 * JJN], [ 2, 0, 5 * JJN],
|
||||
[-2, 1, 3 * JJN], [-1, 1, 5 * JJN], [0, 1, 7 * JJN], [1, 1, 5 * JJN], [2, 1, 3 * JJN],
|
||||
[-2, 2, 1 * JJN], [-1, 2, 3 * JJN], [0, 2, 5 * JJN], [1, 2, 3 * JJN], [2, 2, 1 * JJN],
|
||||
@@ -36,14 +36,14 @@ export const KERNEL_JARVIS = [
|
||||
|
||||
// Burkes
|
||||
const BUR = 1 / 32;
|
||||
export const KERNEL_BURKES = [
|
||||
const KERNEL_BURKES = [
|
||||
[ 1, 0, 8 * BUR], [ 2, 0, 4 * BUR],
|
||||
[-2, 1, 2 * BUR], [-1, 1, 4 * BUR], [0, 1, 8 * BUR], [1, 1, 4 * BUR], [2, 1, 2 * BUR],
|
||||
];
|
||||
|
||||
// Sierra (3-line)
|
||||
const SIE = 1 / 32;
|
||||
export const KERNEL_SIERRA = [
|
||||
const KERNEL_SIERRA = [
|
||||
[ 1, 0, 5 * SIE], [ 2, 0, 3 * SIE],
|
||||
[-2, 1, 2 * SIE], [-1, 1, 4 * SIE], [0, 1, 5 * SIE], [1, 1, 4 * SIE], [2, 1, 2 * SIE],
|
||||
[-1, 2, 2 * SIE], [0, 2, 3 * SIE], [1, 2, 2 * SIE],
|
||||
@@ -51,7 +51,7 @@ export const KERNEL_SIERRA = [
|
||||
|
||||
// Sierra Lite — cheapest non-trivial
|
||||
const SLT = 1 / 4;
|
||||
export const KERNEL_SIERRA_LITE = [
|
||||
const KERNEL_SIERRA_LITE = [
|
||||
[ 1, 0, 2 * SLT],
|
||||
[-1, 1, 1 * SLT],
|
||||
[ 0, 1, 1 * SLT],
|
||||
|
||||
@@ -37,7 +37,6 @@ export function createPipeline() {
|
||||
slots.T = slots.R = slots.C = null;
|
||||
},
|
||||
hasSource() { return src !== null; },
|
||||
source() { return src; },
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
|
||||
@@ -51,15 +51,12 @@ function lutNearest(r, g, b) {
|
||||
* Convert an RGBA pixel buffer into a flat array of palette indices.
|
||||
* Pixels with alpha < threshold become -1 (caller should skip on upload).
|
||||
*
|
||||
* Back-compat: `dither: true` is treated as `method: 'floyd'`.
|
||||
*
|
||||
* @param {Uint8Array|Uint8ClampedArray|number[]} rgba - length = width*height*4
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {Object} [options]
|
||||
* @param {number} [options.alphaThreshold=128]
|
||||
* @param {boolean} [options.dither=false] - legacy; prefer `method`
|
||||
* @param {('none'|'floyd'|'atkinson'|'jarvis'|'burkes'|'sierra'|'sierra-lite'|'bayer-2'|'bayer-4'|'bayer-8')} [options.method]
|
||||
* @param {('none'|'floyd'|'atkinson'|'jarvis'|'burkes'|'sierra'|'sierra-lite'|'bayer-2'|'bayer-4'|'bayer-8')} [options.method='none']
|
||||
* @param {boolean} [options.skipWhite=false] - mark near-white source pixels as transparent
|
||||
* @param {number} [options.whiteThreshold=230] - r,g,b >= this counts as "white"
|
||||
* @param {boolean} [options.paintTransparent=false] - treat transparent pixels as opaque white before quantizing
|
||||
@@ -68,12 +65,11 @@ function lutNearest(r, g, b) {
|
||||
export function rgbaToPalette(rgba, width, height, options = {}) {
|
||||
const {
|
||||
alphaThreshold = 128,
|
||||
dither = false,
|
||||
method = 'none',
|
||||
skipWhite = false,
|
||||
whiteThreshold = 230,
|
||||
paintTransparent = false,
|
||||
} = options;
|
||||
const method = options.method ?? (dither ? 'floyd' : 'none');
|
||||
|
||||
// Substitute transparent→opaque white up-front so the rest of the pipeline
|
||||
// sees a consistent buffer. We only clone when substitutions actually occur.
|
||||
|
||||
@@ -32,13 +32,6 @@ describe('rgbaToPalette', () => {
|
||||
expect(idx[0]).toBe(-1);
|
||||
});
|
||||
|
||||
it('dither:true is equivalent to method:"floyd"', () => {
|
||||
const src = solidRgba(4, 4, 120, 120, 120);
|
||||
const a = rgbaToPalette(src, 4, 4, { dither: true });
|
||||
const b = rgbaToPalette(src, 4, 4, { method: 'floyd' });
|
||||
expect([...a]).toEqual([...b]);
|
||||
});
|
||||
|
||||
it('throws on unknown method', () => {
|
||||
const src = solidRgba(1, 1, 0, 0, 0);
|
||||
expect(() => rgbaToPalette(src, 1, 1, { method: 'nope' })).toThrow(/nope/i);
|
||||
|
||||
Reference in new issue
Block a user