chroma-panel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +44 -0
- package/README.md +119 -0
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/a11y/contrast.cjs +107 -0
- package/dist/a11y/contrast.d.cts +34 -0
- package/dist/a11y/contrast.d.ts +34 -0
- package/dist/a11y/contrast.js +101 -0
- package/dist/color/convert.cjs +162 -0
- package/dist/color/convert.d.cts +16 -0
- package/dist/color/convert.d.ts +16 -0
- package/dist/color/convert.js +153 -0
- package/dist/color/parse.cjs +153 -0
- package/dist/color/parse.d.cts +7 -0
- package/dist/color/parse.d.ts +7 -0
- package/dist/color/parse.js +151 -0
- package/dist/color/serialize.cjs +79 -0
- package/dist/color/serialize.d.cts +11 -0
- package/dist/color/serialize.d.ts +11 -0
- package/dist/color/serialize.js +73 -0
- package/dist/color/sticky.cjs +20 -0
- package/dist/color/sticky.d.cts +6 -0
- package/dist/color/sticky.d.ts +6 -0
- package/dist/color/sticky.js +19 -0
- package/dist/color/types.d.cts +43 -0
- package/dist/color/types.d.ts +43 -0
- package/dist/components/ChromaPanel.cjs +254 -0
- package/dist/components/ChromaPanel.d.cts +47 -0
- package/dist/components/ChromaPanel.d.ts +47 -0
- package/dist/components/ChromaPanel.js +251 -0
- package/dist/components/ColorInput.cjs +128 -0
- package/dist/components/ColorInput.d.cts +19 -0
- package/dist/components/ColorInput.d.ts +19 -0
- package/dist/components/ColorInput.js +127 -0
- package/dist/components/ModeToolbar.cjs +33 -0
- package/dist/components/ModeToolbar.d.cts +10 -0
- package/dist/components/ModeToolbar.d.ts +10 -0
- package/dist/components/ModeToolbar.js +32 -0
- package/dist/components/PanelFooter.cjs +82 -0
- package/dist/components/PanelFooter.d.cts +5 -0
- package/dist/components/PanelFooter.d.ts +5 -0
- package/dist/components/PanelFooter.js +80 -0
- package/dist/components/Popover.cjs +154 -0
- package/dist/components/Popover.d.cts +13 -0
- package/dist/components/Popover.d.ts +13 -0
- package/dist/components/Popover.js +153 -0
- package/dist/contrast.cjs +10 -0
- package/dist/contrast.d.cts +2 -0
- package/dist/contrast.d.ts +2 -0
- package/dist/contrast.js +2 -0
- package/dist/core/context.cjs +30 -0
- package/dist/core/context.d.cts +43 -0
- package/dist/core/context.d.ts +43 -0
- package/dist/core/context.js +26 -0
- package/dist/core/dev.cjs +11 -0
- package/dist/core/dev.js +11 -0
- package/dist/core/store.cjs +43 -0
- package/dist/core/store.d.cts +17 -0
- package/dist/core/store.d.ts +17 -0
- package/dist/core/store.js +44 -0
- package/dist/core/styleInjector.cjs +38 -0
- package/dist/core/styleInjector.d.cts +4 -0
- package/dist/core/styleInjector.d.ts +4 -0
- package/dist/core/styleInjector.js +38 -0
- package/dist/core/useColorStore.cjs +43 -0
- package/dist/core/useColorStore.d.cts +6 -0
- package/dist/core/useColorStore.d.ts +6 -0
- package/dist/core/useColorStore.js +41 -0
- package/dist/core/useFormControl.cjs +87 -0
- package/dist/core/useFormControl.js +82 -0
- package/dist/core/useMediaQuery.cjs +48 -0
- package/dist/core/useMediaQuery.js +46 -0
- package/dist/core/usePointerDrag.cjs +177 -0
- package/dist/core/usePointerDrag.d.cts +35 -0
- package/dist/core/usePointerDrag.d.ts +35 -0
- package/dist/core/usePointerDrag.js +175 -0
- package/dist/core/useScrollFade.cjs +44 -0
- package/dist/core/useScrollFade.js +43 -0
- package/dist/core.cjs +33 -0
- package/dist/core.d.cts +7 -0
- package/dist/core.d.ts +7 -0
- package/dist/core.js +6 -0
- package/dist/data/defaults.cjs +123 -0
- package/dist/data/defaults.d.cts +6 -0
- package/dist/data/defaults.d.ts +6 -0
- package/dist/data/defaults.js +122 -0
- package/dist/data/namedColors.cjs +11 -0
- package/dist/data/namedColors.d.cts +3 -0
- package/dist/data/namedColors.d.ts +3 -0
- package/dist/data/namedColors.js +12 -0
- package/dist/image/extract.cjs +98 -0
- package/dist/image/extract.d.cts +16 -0
- package/dist/image/extract.d.ts +16 -0
- package/dist/image/extract.js +99 -0
- package/dist/image/mmcq.cjs +182 -0
- package/dist/image/mmcq.d.cts +8 -0
- package/dist/image/mmcq.d.ts +8 -0
- package/dist/image/mmcq.js +183 -0
- package/dist/image-worker.js +202 -0
- package/dist/image.cjs +12 -0
- package/dist/image.d.cts +4 -0
- package/dist/image.d.ts +4 -0
- package/dist/image.js +9 -0
- package/dist/index.cjs +119 -0
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +44 -0
- package/dist/modes/gradients.cjs +48 -0
- package/dist/modes/gradients.js +43 -0
- package/dist/modes/image/index.cjs +193 -0
- package/dist/modes/image/index.d.cts +10 -0
- package/dist/modes/image/index.d.ts +10 -0
- package/dist/modes/image/index.js +191 -0
- package/dist/modes/palettes/index.cjs +76 -0
- package/dist/modes/palettes/index.d.cts +6 -0
- package/dist/modes/palettes/index.d.ts +6 -0
- package/dist/modes/palettes/index.js +74 -0
- package/dist/modes/pencils/index.cjs +36 -0
- package/dist/modes/pencils/index.d.cts +6 -0
- package/dist/modes/pencils/index.d.ts +6 -0
- package/dist/modes/pencils/index.js +34 -0
- package/dist/modes/registry.cjs +26 -0
- package/dist/modes/registry.d.cts +13 -0
- package/dist/modes/registry.d.ts +13 -0
- package/dist/modes/registry.js +25 -0
- package/dist/modes/sliders/index.cjs +300 -0
- package/dist/modes/sliders/index.d.cts +6 -0
- package/dist/modes/sliders/index.d.ts +6 -0
- package/dist/modes/sliders/index.js +298 -0
- package/dist/modes/wheel/index.cjs +62 -0
- package/dist/modes/wheel/index.d.cts +6 -0
- package/dist/modes/wheel/index.d.ts +6 -0
- package/dist/modes/wheel/index.js +60 -0
- package/dist/named-colors.cjs +3 -0
- package/dist/named-colors.d.cts +2 -0
- package/dist/named-colors.d.ts +2 -0
- package/dist/named-colors.js +2 -0
- package/dist/palettes.cjs +8 -0
- package/dist/palettes.d.cts +2 -0
- package/dist/palettes.d.ts +2 -0
- package/dist/palettes.js +7 -0
- package/dist/panel.cjs +80 -0
- package/dist/panel.d.cts +27 -0
- package/dist/panel.d.ts +27 -0
- package/dist/panel.js +27 -0
- package/dist/pencils.cjs +8 -0
- package/dist/pencils.d.cts +2 -0
- package/dist/pencils.d.ts +2 -0
- package/dist/pencils.js +7 -0
- package/dist/primitives/AxisInput.cjs +26 -0
- package/dist/primitives/AxisInput.js +25 -0
- package/dist/primitives/ChannelSlider.cjs +94 -0
- package/dist/primitives/ChannelSlider.d.cts +18 -0
- package/dist/primitives/ChannelSlider.d.ts +18 -0
- package/dist/primitives/ChannelSlider.js +93 -0
- package/dist/primitives/ColorArea.cjs +85 -0
- package/dist/primitives/ColorArea.d.cts +8 -0
- package/dist/primitives/ColorArea.d.ts +8 -0
- package/dist/primitives/ColorArea.js +84 -0
- package/dist/primitives/ColorDisc.cjs +95 -0
- package/dist/primitives/ColorDisc.d.cts +8 -0
- package/dist/primitives/ColorDisc.d.ts +8 -0
- package/dist/primitives/ColorDisc.js +94 -0
- package/dist/primitives/ColorField.cjs +68 -0
- package/dist/primitives/ColorField.d.cts +9 -0
- package/dist/primitives/ColorField.d.ts +9 -0
- package/dist/primitives/ColorField.js +67 -0
- package/dist/primitives/NumberField.cjs +65 -0
- package/dist/primitives/NumberField.d.cts +14 -0
- package/dist/primitives/NumberField.d.ts +14 -0
- package/dist/primitives/NumberField.js +64 -0
- package/dist/primitives/SegmentedControl.cjs +66 -0
- package/dist/primitives/SegmentedControl.d.cts +21 -0
- package/dist/primitives/SegmentedControl.d.ts +21 -0
- package/dist/primitives/SegmentedControl.js +65 -0
- package/dist/primitives/Swatch.cjs +22 -0
- package/dist/primitives/Swatch.d.cts +13 -0
- package/dist/primitives/Swatch.d.ts +13 -0
- package/dist/primitives/Swatch.js +21 -0
- package/dist/primitives/SwatchGrid.cjs +63 -0
- package/dist/primitives/SwatchGrid.d.cts +17 -0
- package/dist/primitives/SwatchGrid.d.ts +17 -0
- package/dist/primitives/SwatchGrid.js +62 -0
- package/dist/primitives/icons.cjs +149 -0
- package/dist/primitives/icons.d.cts +9 -0
- package/dist/primitives/icons.d.ts +9 -0
- package/dist/primitives/icons.js +141 -0
- package/dist/primitives/useEyedropper.cjs +30 -0
- package/dist/primitives/useEyedropper.d.cts +7 -0
- package/dist/primitives/useEyedropper.d.ts +7 -0
- package/dist/primitives/useEyedropper.js +29 -0
- package/dist/sliders.cjs +8 -0
- package/dist/sliders.d.cts +2 -0
- package/dist/sliders.d.ts +2 -0
- package/dist/sliders.js +7 -0
- package/dist/style.css +2 -0
- package/dist/styles/css.cjs +4 -0
- package/dist/styles/css.js +5 -0
- package/dist/wheel.cjs +8 -0
- package/dist/wheel.d.cts +2 -0
- package/dist/wheel.d.ts +2 -0
- package/dist/wheel.js +7 -0
- package/package.json +222 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const require_mmcq = require("./mmcq.cjs");
|
|
2
|
+
//#region src/image/extract.ts
|
|
3
|
+
function aborted(signal) {
|
|
4
|
+
return signal?.aborted === true;
|
|
5
|
+
}
|
|
6
|
+
async function toBlob(source) {
|
|
7
|
+
if (typeof source !== "string") return source;
|
|
8
|
+
const response = await fetch(source, { mode: "cors" });
|
|
9
|
+
if (!response.ok) throw new Error(`chroma-panel: could not load image (${response.status}).`);
|
|
10
|
+
return await response.blob();
|
|
11
|
+
}
|
|
12
|
+
async function decode(blob, size) {
|
|
13
|
+
if (typeof createImageBitmap !== "function") throw new Error("chroma-panel: this browser cannot decode images off-thread.");
|
|
14
|
+
const longest = Math.max(8, Math.round(size));
|
|
15
|
+
try {
|
|
16
|
+
const probe = await createImageBitmap(blob);
|
|
17
|
+
const wide = probe.width >= probe.height;
|
|
18
|
+
probe.close();
|
|
19
|
+
return await createImageBitmap(blob, {
|
|
20
|
+
...wide ? { resizeWidth: longest } : { resizeHeight: longest },
|
|
21
|
+
resizeQuality: "medium",
|
|
22
|
+
premultiplyAlpha: "none"
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
return await createImageBitmap(blob);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readPixels(bitmap) {
|
|
29
|
+
const { width, height } = bitmap;
|
|
30
|
+
const ctx = (typeof OffscreenCanvas === "function" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), {
|
|
31
|
+
width,
|
|
32
|
+
height
|
|
33
|
+
})).getContext("2d", { willReadFrequently: true });
|
|
34
|
+
if (ctx === null) throw new Error("chroma-panel: could not get a 2D canvas context.");
|
|
35
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
36
|
+
return ctx.getImageData(0, 0, width, height).data;
|
|
37
|
+
}
|
|
38
|
+
function runInWorker(worker, data, maxColors, alphaThreshold, signal) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const id = Math.random().toString(36).slice(2);
|
|
41
|
+
const cleanup = () => {
|
|
42
|
+
worker.removeEventListener("message", onMessage);
|
|
43
|
+
worker.removeEventListener("error", onError);
|
|
44
|
+
signal?.removeEventListener("abort", onAbort);
|
|
45
|
+
};
|
|
46
|
+
const onMessage = (event) => {
|
|
47
|
+
const payload = event.data;
|
|
48
|
+
if (payload.id !== id) return;
|
|
49
|
+
cleanup();
|
|
50
|
+
if (payload.error !== void 0) reject(new Error(payload.error));
|
|
51
|
+
else resolve(payload.swatches ?? []);
|
|
52
|
+
};
|
|
53
|
+
const onError = (event) => {
|
|
54
|
+
cleanup();
|
|
55
|
+
reject(new Error(event.message));
|
|
56
|
+
};
|
|
57
|
+
const onAbort = () => {
|
|
58
|
+
cleanup();
|
|
59
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
60
|
+
};
|
|
61
|
+
worker.addEventListener("message", onMessage);
|
|
62
|
+
worker.addEventListener("error", onError);
|
|
63
|
+
signal?.addEventListener("abort", onAbort);
|
|
64
|
+
const copy = new Uint8ClampedArray(data);
|
|
65
|
+
worker.postMessage({
|
|
66
|
+
id,
|
|
67
|
+
data: copy,
|
|
68
|
+
maxColors,
|
|
69
|
+
alphaThreshold
|
|
70
|
+
}, [copy.buffer]);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function extractPalette(source, options = {}) {
|
|
74
|
+
const { maxColors = 8, size = 100, alphaThreshold = 128, worker, signal } = options;
|
|
75
|
+
if (!Number.isFinite(maxColors) || maxColors < 2) throw new RangeError("chroma-panel: maxColors must be a number of at least 2.");
|
|
76
|
+
if (!Number.isFinite(size) || size < 8) throw new RangeError("chroma-panel: size must be a number of at least 8.");
|
|
77
|
+
if (!Number.isFinite(alphaThreshold) || alphaThreshold < 0 || alphaThreshold > 255) throw new RangeError("chroma-panel: alphaThreshold must be between 0 and 255.");
|
|
78
|
+
if (aborted(signal)) throw new DOMException("Aborted", "AbortError");
|
|
79
|
+
const blob = await toBlob(source);
|
|
80
|
+
if (aborted(signal)) throw new DOMException("Aborted", "AbortError");
|
|
81
|
+
const bitmap = await decode(blob, size);
|
|
82
|
+
let data;
|
|
83
|
+
try {
|
|
84
|
+
if (aborted(signal)) throw new DOMException("Aborted", "AbortError");
|
|
85
|
+
data = readPixels(bitmap);
|
|
86
|
+
} finally {
|
|
87
|
+
bitmap.close();
|
|
88
|
+
}
|
|
89
|
+
let sampled = 0;
|
|
90
|
+
for (let i = 3; i < data.length; i += 4) if (data[i] >= alphaThreshold) sampled++;
|
|
91
|
+
const instance = typeof worker === "function" ? worker() : worker;
|
|
92
|
+
return {
|
|
93
|
+
swatches: instance !== void 0 ? await runInWorker(instance, data, maxColors, alphaThreshold, signal) : require_mmcq.quantize(data, maxColors, alphaThreshold),
|
|
94
|
+
sampled
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
exports.extractPalette = extractPalette;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { QuantizedSwatch } from "./mmcq.cjs";
|
|
2
|
+
//#region src/image/extract.d.ts
|
|
3
|
+
export interface ExtractOptions {
|
|
4
|
+
maxColors?: number;
|
|
5
|
+
size?: number;
|
|
6
|
+
alphaThreshold?: number;
|
|
7
|
+
worker?: Worker | (() => Worker);
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface ExtractResult {
|
|
11
|
+
swatches: QuantizedSwatch[];
|
|
12
|
+
sampled: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function extractPalette(source: Blob | File | string, options?: ExtractOptions): Promise<ExtractResult>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export type { QuantizedSwatch };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { QuantizedSwatch } from "./mmcq.js";
|
|
2
|
+
//#region src/image/extract.d.ts
|
|
3
|
+
export interface ExtractOptions {
|
|
4
|
+
maxColors?: number;
|
|
5
|
+
size?: number;
|
|
6
|
+
alphaThreshold?: number;
|
|
7
|
+
worker?: Worker | (() => Worker);
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface ExtractResult {
|
|
11
|
+
swatches: QuantizedSwatch[];
|
|
12
|
+
sampled: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function extractPalette(source: Blob | File | string, options?: ExtractOptions): Promise<ExtractResult>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export type { QuantizedSwatch };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { quantize } from "./mmcq.js";
|
|
2
|
+
//#region src/image/extract.ts
|
|
3
|
+
function aborted(signal) {
|
|
4
|
+
return signal?.aborted === true;
|
|
5
|
+
}
|
|
6
|
+
async function toBlob(source) {
|
|
7
|
+
if (typeof source !== "string") return source;
|
|
8
|
+
const response = await fetch(source, { mode: "cors" });
|
|
9
|
+
if (!response.ok) throw new Error(`chroma-panel: could not load image (${response.status}).`);
|
|
10
|
+
return await response.blob();
|
|
11
|
+
}
|
|
12
|
+
async function decode(blob, size) {
|
|
13
|
+
if (typeof createImageBitmap !== "function") throw new Error("chroma-panel: this browser cannot decode images off-thread.");
|
|
14
|
+
const longest = Math.max(8, Math.round(size));
|
|
15
|
+
try {
|
|
16
|
+
const probe = await createImageBitmap(blob);
|
|
17
|
+
const wide = probe.width >= probe.height;
|
|
18
|
+
probe.close();
|
|
19
|
+
return await createImageBitmap(blob, {
|
|
20
|
+
...wide ? { resizeWidth: longest } : { resizeHeight: longest },
|
|
21
|
+
resizeQuality: "medium",
|
|
22
|
+
premultiplyAlpha: "none"
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
return await createImageBitmap(blob);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readPixels(bitmap) {
|
|
29
|
+
const { width, height } = bitmap;
|
|
30
|
+
const ctx = (typeof OffscreenCanvas === "function" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), {
|
|
31
|
+
width,
|
|
32
|
+
height
|
|
33
|
+
})).getContext("2d", { willReadFrequently: true });
|
|
34
|
+
if (ctx === null) throw new Error("chroma-panel: could not get a 2D canvas context.");
|
|
35
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
36
|
+
return ctx.getImageData(0, 0, width, height).data;
|
|
37
|
+
}
|
|
38
|
+
function runInWorker(worker, data, maxColors, alphaThreshold, signal) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const id = Math.random().toString(36).slice(2);
|
|
41
|
+
const cleanup = () => {
|
|
42
|
+
worker.removeEventListener("message", onMessage);
|
|
43
|
+
worker.removeEventListener("error", onError);
|
|
44
|
+
signal?.removeEventListener("abort", onAbort);
|
|
45
|
+
};
|
|
46
|
+
const onMessage = (event) => {
|
|
47
|
+
const payload = event.data;
|
|
48
|
+
if (payload.id !== id) return;
|
|
49
|
+
cleanup();
|
|
50
|
+
if (payload.error !== void 0) reject(new Error(payload.error));
|
|
51
|
+
else resolve(payload.swatches ?? []);
|
|
52
|
+
};
|
|
53
|
+
const onError = (event) => {
|
|
54
|
+
cleanup();
|
|
55
|
+
reject(new Error(event.message));
|
|
56
|
+
};
|
|
57
|
+
const onAbort = () => {
|
|
58
|
+
cleanup();
|
|
59
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
60
|
+
};
|
|
61
|
+
worker.addEventListener("message", onMessage);
|
|
62
|
+
worker.addEventListener("error", onError);
|
|
63
|
+
signal?.addEventListener("abort", onAbort);
|
|
64
|
+
const copy = new Uint8ClampedArray(data);
|
|
65
|
+
worker.postMessage({
|
|
66
|
+
id,
|
|
67
|
+
data: copy,
|
|
68
|
+
maxColors,
|
|
69
|
+
alphaThreshold
|
|
70
|
+
}, [copy.buffer]);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function extractPalette(source, options = {}) {
|
|
74
|
+
const { maxColors = 8, size = 100, alphaThreshold = 128, worker, signal } = options;
|
|
75
|
+
if (!Number.isFinite(maxColors) || maxColors < 2) throw new RangeError("chroma-panel: maxColors must be a number of at least 2.");
|
|
76
|
+
if (!Number.isFinite(size) || size < 8) throw new RangeError("chroma-panel: size must be a number of at least 8.");
|
|
77
|
+
if (!Number.isFinite(alphaThreshold) || alphaThreshold < 0 || alphaThreshold > 255) throw new RangeError("chroma-panel: alphaThreshold must be between 0 and 255.");
|
|
78
|
+
if (aborted(signal)) throw new DOMException("Aborted", "AbortError");
|
|
79
|
+
const blob = await toBlob(source);
|
|
80
|
+
if (aborted(signal)) throw new DOMException("Aborted", "AbortError");
|
|
81
|
+
const bitmap = await decode(blob, size);
|
|
82
|
+
let data;
|
|
83
|
+
try {
|
|
84
|
+
if (aborted(signal)) throw new DOMException("Aborted", "AbortError");
|
|
85
|
+
data = readPixels(bitmap);
|
|
86
|
+
} finally {
|
|
87
|
+
bitmap.close();
|
|
88
|
+
}
|
|
89
|
+
let sampled = 0;
|
|
90
|
+
for (let i = 3; i < data.length; i += 4) if (data[i] >= alphaThreshold) sampled++;
|
|
91
|
+
const instance = typeof worker === "function" ? worker() : worker;
|
|
92
|
+
return {
|
|
93
|
+
swatches: instance !== void 0 ? await runInWorker(instance, data, maxColors, alphaThreshold, signal) : quantize(data, maxColors, alphaThreshold),
|
|
94
|
+
sampled
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { extractPalette };
|
|
99
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//#region src/image/mmcq.ts
|
|
2
|
+
const SIGBITS = 5;
|
|
3
|
+
const RSHIFT = 3;
|
|
4
|
+
const HIST_SIZE = 32768;
|
|
5
|
+
const MAX_ITERATIONS = 1e3;
|
|
6
|
+
const FRACT_BY_POPULATIONS = .75;
|
|
7
|
+
function histIndex(r, g, b) {
|
|
8
|
+
return (r << 10) + (g << SIGBITS) + b;
|
|
9
|
+
}
|
|
10
|
+
function makeVBox(r1, r2, g1, g2, b1, b2) {
|
|
11
|
+
return {
|
|
12
|
+
r1,
|
|
13
|
+
r2,
|
|
14
|
+
g1,
|
|
15
|
+
g2,
|
|
16
|
+
b1,
|
|
17
|
+
b2,
|
|
18
|
+
count: 0,
|
|
19
|
+
countValid: false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function copyVBox(v) {
|
|
23
|
+
return makeVBox(v.r1, v.r2, v.g1, v.g2, v.b1, v.b2);
|
|
24
|
+
}
|
|
25
|
+
function volume(v) {
|
|
26
|
+
return (v.r2 - v.r1 + 1) * (v.g2 - v.g1 + 1) * (v.b2 - v.b1 + 1);
|
|
27
|
+
}
|
|
28
|
+
function count(v, histo) {
|
|
29
|
+
if (v.countValid) return v.count;
|
|
30
|
+
let total = 0;
|
|
31
|
+
for (let r = v.r1; r <= v.r2; r++) for (let g = v.g1; g <= v.g2; g++) for (let b = v.b1; b <= v.b2; b++) total += histo[histIndex(r, g, b)];
|
|
32
|
+
v.count = total;
|
|
33
|
+
v.countValid = true;
|
|
34
|
+
return total;
|
|
35
|
+
}
|
|
36
|
+
function average(v, h) {
|
|
37
|
+
const mult = 8;
|
|
38
|
+
let total = 0;
|
|
39
|
+
let rSum = 0;
|
|
40
|
+
let gSum = 0;
|
|
41
|
+
let bSum = 0;
|
|
42
|
+
for (let r = v.r1; r <= v.r2; r++) for (let g = v.g1; g <= v.g2; g++) for (let b = v.b1; b <= v.b2; b++) {
|
|
43
|
+
const index = histIndex(r, g, b);
|
|
44
|
+
const hval = h.counts[index];
|
|
45
|
+
if (hval === 0) continue;
|
|
46
|
+
total += hval;
|
|
47
|
+
rSum += h.rSum[index];
|
|
48
|
+
gSum += h.gSum[index];
|
|
49
|
+
bSum += h.bSum[index];
|
|
50
|
+
}
|
|
51
|
+
if (total === 0) return [
|
|
52
|
+
Math.min(255, Math.round(mult * (v.r1 + v.r2 + 1) / 2)),
|
|
53
|
+
Math.min(255, Math.round(mult * (v.g1 + v.g2 + 1) / 2)),
|
|
54
|
+
Math.min(255, Math.round(mult * (v.b1 + v.b2 + 1) / 2))
|
|
55
|
+
];
|
|
56
|
+
return [
|
|
57
|
+
Math.min(255, Math.round(rSum / total)),
|
|
58
|
+
Math.min(255, Math.round(gSum / total)),
|
|
59
|
+
Math.min(255, Math.round(bSum / total))
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function medianCut(histo, vbox) {
|
|
63
|
+
if (count(vbox, histo) === 0) return [];
|
|
64
|
+
if (count(vbox, histo) === 1) return [copyVBox(vbox)];
|
|
65
|
+
const rw = vbox.r2 - vbox.r1 + 1;
|
|
66
|
+
const gw = vbox.g2 - vbox.g1 + 1;
|
|
67
|
+
const bw = vbox.b2 - vbox.b1 + 1;
|
|
68
|
+
const maxw = Math.max(rw, gw, bw);
|
|
69
|
+
const partial = /* @__PURE__ */ new Int32Array(33);
|
|
70
|
+
let total = 0;
|
|
71
|
+
const axis = maxw === rw ? "r" : maxw === gw ? "g" : "b";
|
|
72
|
+
if (axis === "r") for (let r = vbox.r1; r <= vbox.r2; r++) {
|
|
73
|
+
let sum = 0;
|
|
74
|
+
for (let g = vbox.g1; g <= vbox.g2; g++) for (let b = vbox.b1; b <= vbox.b2; b++) sum += histo[histIndex(r, g, b)];
|
|
75
|
+
total += sum;
|
|
76
|
+
partial[r] = total;
|
|
77
|
+
}
|
|
78
|
+
else if (axis === "g") for (let g = vbox.g1; g <= vbox.g2; g++) {
|
|
79
|
+
let sum = 0;
|
|
80
|
+
for (let r = vbox.r1; r <= vbox.r2; r++) for (let b = vbox.b1; b <= vbox.b2; b++) sum += histo[histIndex(r, g, b)];
|
|
81
|
+
total += sum;
|
|
82
|
+
partial[g] = total;
|
|
83
|
+
}
|
|
84
|
+
else for (let b = vbox.b1; b <= vbox.b2; b++) {
|
|
85
|
+
let sum = 0;
|
|
86
|
+
for (let r = vbox.r1; r <= vbox.r2; r++) for (let g = vbox.g1; g <= vbox.g2; g++) sum += histo[histIndex(r, g, b)];
|
|
87
|
+
total += sum;
|
|
88
|
+
partial[b] = total;
|
|
89
|
+
}
|
|
90
|
+
const lo = axis === "r" ? vbox.r1 : axis === "g" ? vbox.g1 : vbox.b1;
|
|
91
|
+
const hi = axis === "r" ? vbox.r2 : axis === "g" ? vbox.g2 : vbox.b2;
|
|
92
|
+
for (let i = lo; i <= hi; i++) {
|
|
93
|
+
if (partial[i] <= total / 2) continue;
|
|
94
|
+
const left = i - lo;
|
|
95
|
+
const right = hi - i;
|
|
96
|
+
let d2 = left <= right ? Math.min(hi - 1, Math.trunc(i + right / 2)) : Math.max(lo, Math.trunc(i - 1 - left / 2));
|
|
97
|
+
while (d2 < hi && partial[d2] === 0) d2++;
|
|
98
|
+
let count2 = total - partial[d2];
|
|
99
|
+
while (count2 === 0 && d2 > lo && partial[d2 - 1] > 0) {
|
|
100
|
+
d2--;
|
|
101
|
+
count2 = total - partial[d2];
|
|
102
|
+
}
|
|
103
|
+
const a = copyVBox(vbox);
|
|
104
|
+
const b = copyVBox(vbox);
|
|
105
|
+
if (axis === "r") {
|
|
106
|
+
a.r2 = d2;
|
|
107
|
+
b.r1 = d2 + 1;
|
|
108
|
+
} else if (axis === "g") {
|
|
109
|
+
a.g2 = d2;
|
|
110
|
+
b.g1 = d2 + 1;
|
|
111
|
+
} else {
|
|
112
|
+
a.b2 = d2;
|
|
113
|
+
b.b1 = d2 + 1;
|
|
114
|
+
}
|
|
115
|
+
return [a, b];
|
|
116
|
+
}
|
|
117
|
+
return [copyVBox(vbox)];
|
|
118
|
+
}
|
|
119
|
+
function quantize(data, maxColors, alphaThreshold = 128) {
|
|
120
|
+
const target = Math.max(2, Math.min(256, Math.round(maxColors)));
|
|
121
|
+
const hist = {
|
|
122
|
+
counts: new Int32Array(HIST_SIZE),
|
|
123
|
+
rSum: new Int32Array(HIST_SIZE),
|
|
124
|
+
gSum: new Int32Array(HIST_SIZE),
|
|
125
|
+
bSum: new Int32Array(HIST_SIZE)
|
|
126
|
+
};
|
|
127
|
+
const histo = hist.counts;
|
|
128
|
+
let rMin = 1e9, rMax = -1, gMin = 1e9, gMax = -1, bMin = 1e9, bMax = -1;
|
|
129
|
+
let used = 0;
|
|
130
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
131
|
+
if (data[i + 3] < alphaThreshold) continue;
|
|
132
|
+
const r8 = data[i];
|
|
133
|
+
const g8 = data[i + 1];
|
|
134
|
+
const b8 = data[i + 2];
|
|
135
|
+
const r = r8 >> RSHIFT;
|
|
136
|
+
const g = g8 >> RSHIFT;
|
|
137
|
+
const b = b8 >> RSHIFT;
|
|
138
|
+
const bin = histIndex(r, g, b);
|
|
139
|
+
histo[bin] = histo[bin] + 1;
|
|
140
|
+
hist.rSum[bin] = hist.rSum[bin] + r8;
|
|
141
|
+
hist.gSum[bin] = hist.gSum[bin] + g8;
|
|
142
|
+
hist.bSum[bin] = hist.bSum[bin] + b8;
|
|
143
|
+
used++;
|
|
144
|
+
if (r < rMin) rMin = r;
|
|
145
|
+
if (r > rMax) rMax = r;
|
|
146
|
+
if (g < gMin) gMin = g;
|
|
147
|
+
if (g > gMax) gMax = g;
|
|
148
|
+
if (b < bMin) bMin = b;
|
|
149
|
+
if (b > bMax) bMax = b;
|
|
150
|
+
}
|
|
151
|
+
if (used === 0) return [];
|
|
152
|
+
let boxes = [makeVBox(rMin, rMax, gMin, gMax, bMin, bMax)];
|
|
153
|
+
const split = (limit, byVolume) => {
|
|
154
|
+
let iterations = 0;
|
|
155
|
+
while (iterations++ < MAX_ITERATIONS) {
|
|
156
|
+
if (boxes.length >= limit) return;
|
|
157
|
+
boxes.sort((a, b) => byVolume ? count(a, histo) * volume(a) - count(b, histo) * volume(b) : count(a, histo) - count(b, histo));
|
|
158
|
+
const biggest = boxes.pop();
|
|
159
|
+
if (biggest === void 0) return;
|
|
160
|
+
if (count(biggest, histo) === 0) {
|
|
161
|
+
boxes.unshift(biggest);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const parts = medianCut(histo, biggest);
|
|
165
|
+
if (parts.length === 0) return;
|
|
166
|
+
boxes.push(...parts);
|
|
167
|
+
if (parts.length === 1) return;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
split(Math.max(2, Math.round(target * FRACT_BY_POPULATIONS)), false);
|
|
171
|
+
split(target, true);
|
|
172
|
+
return boxes.map((box) => {
|
|
173
|
+
const rgb = average(box, hist);
|
|
174
|
+
return {
|
|
175
|
+
rgb,
|
|
176
|
+
population: count(box, histo),
|
|
177
|
+
hex: "#" + rgb.map((c) => c.toString(16).padStart(2, "0")).join("")
|
|
178
|
+
};
|
|
179
|
+
}).filter((s) => s.population > 0).sort((a, b) => b.population - a.population);
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
exports.quantize = quantize;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/image/mmcq.d.ts
|
|
2
|
+
export interface QuantizedSwatch {
|
|
3
|
+
hex: string;
|
|
4
|
+
rgb: [number, number, number];
|
|
5
|
+
population: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function quantize(data: Uint8ClampedArray, maxColors: number, alphaThreshold?: number): QuantizedSwatch[];
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/image/mmcq.d.ts
|
|
2
|
+
export interface QuantizedSwatch {
|
|
3
|
+
hex: string;
|
|
4
|
+
rgb: [number, number, number];
|
|
5
|
+
population: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function quantize(data: Uint8ClampedArray, maxColors: number, alphaThreshold?: number): QuantizedSwatch[];
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//#region src/image/mmcq.ts
|
|
2
|
+
const SIGBITS = 5;
|
|
3
|
+
const RSHIFT = 3;
|
|
4
|
+
const HIST_SIZE = 32768;
|
|
5
|
+
const MAX_ITERATIONS = 1e3;
|
|
6
|
+
const FRACT_BY_POPULATIONS = .75;
|
|
7
|
+
function histIndex(r, g, b) {
|
|
8
|
+
return (r << 10) + (g << SIGBITS) + b;
|
|
9
|
+
}
|
|
10
|
+
function makeVBox(r1, r2, g1, g2, b1, b2) {
|
|
11
|
+
return {
|
|
12
|
+
r1,
|
|
13
|
+
r2,
|
|
14
|
+
g1,
|
|
15
|
+
g2,
|
|
16
|
+
b1,
|
|
17
|
+
b2,
|
|
18
|
+
count: 0,
|
|
19
|
+
countValid: false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function copyVBox(v) {
|
|
23
|
+
return makeVBox(v.r1, v.r2, v.g1, v.g2, v.b1, v.b2);
|
|
24
|
+
}
|
|
25
|
+
function volume(v) {
|
|
26
|
+
return (v.r2 - v.r1 + 1) * (v.g2 - v.g1 + 1) * (v.b2 - v.b1 + 1);
|
|
27
|
+
}
|
|
28
|
+
function count(v, histo) {
|
|
29
|
+
if (v.countValid) return v.count;
|
|
30
|
+
let total = 0;
|
|
31
|
+
for (let r = v.r1; r <= v.r2; r++) for (let g = v.g1; g <= v.g2; g++) for (let b = v.b1; b <= v.b2; b++) total += histo[histIndex(r, g, b)];
|
|
32
|
+
v.count = total;
|
|
33
|
+
v.countValid = true;
|
|
34
|
+
return total;
|
|
35
|
+
}
|
|
36
|
+
function average(v, h) {
|
|
37
|
+
const mult = 8;
|
|
38
|
+
let total = 0;
|
|
39
|
+
let rSum = 0;
|
|
40
|
+
let gSum = 0;
|
|
41
|
+
let bSum = 0;
|
|
42
|
+
for (let r = v.r1; r <= v.r2; r++) for (let g = v.g1; g <= v.g2; g++) for (let b = v.b1; b <= v.b2; b++) {
|
|
43
|
+
const index = histIndex(r, g, b);
|
|
44
|
+
const hval = h.counts[index];
|
|
45
|
+
if (hval === 0) continue;
|
|
46
|
+
total += hval;
|
|
47
|
+
rSum += h.rSum[index];
|
|
48
|
+
gSum += h.gSum[index];
|
|
49
|
+
bSum += h.bSum[index];
|
|
50
|
+
}
|
|
51
|
+
if (total === 0) return [
|
|
52
|
+
Math.min(255, Math.round(mult * (v.r1 + v.r2 + 1) / 2)),
|
|
53
|
+
Math.min(255, Math.round(mult * (v.g1 + v.g2 + 1) / 2)),
|
|
54
|
+
Math.min(255, Math.round(mult * (v.b1 + v.b2 + 1) / 2))
|
|
55
|
+
];
|
|
56
|
+
return [
|
|
57
|
+
Math.min(255, Math.round(rSum / total)),
|
|
58
|
+
Math.min(255, Math.round(gSum / total)),
|
|
59
|
+
Math.min(255, Math.round(bSum / total))
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function medianCut(histo, vbox) {
|
|
63
|
+
if (count(vbox, histo) === 0) return [];
|
|
64
|
+
if (count(vbox, histo) === 1) return [copyVBox(vbox)];
|
|
65
|
+
const rw = vbox.r2 - vbox.r1 + 1;
|
|
66
|
+
const gw = vbox.g2 - vbox.g1 + 1;
|
|
67
|
+
const bw = vbox.b2 - vbox.b1 + 1;
|
|
68
|
+
const maxw = Math.max(rw, gw, bw);
|
|
69
|
+
const partial = /* @__PURE__ */ new Int32Array(33);
|
|
70
|
+
let total = 0;
|
|
71
|
+
const axis = maxw === rw ? "r" : maxw === gw ? "g" : "b";
|
|
72
|
+
if (axis === "r") for (let r = vbox.r1; r <= vbox.r2; r++) {
|
|
73
|
+
let sum = 0;
|
|
74
|
+
for (let g = vbox.g1; g <= vbox.g2; g++) for (let b = vbox.b1; b <= vbox.b2; b++) sum += histo[histIndex(r, g, b)];
|
|
75
|
+
total += sum;
|
|
76
|
+
partial[r] = total;
|
|
77
|
+
}
|
|
78
|
+
else if (axis === "g") for (let g = vbox.g1; g <= vbox.g2; g++) {
|
|
79
|
+
let sum = 0;
|
|
80
|
+
for (let r = vbox.r1; r <= vbox.r2; r++) for (let b = vbox.b1; b <= vbox.b2; b++) sum += histo[histIndex(r, g, b)];
|
|
81
|
+
total += sum;
|
|
82
|
+
partial[g] = total;
|
|
83
|
+
}
|
|
84
|
+
else for (let b = vbox.b1; b <= vbox.b2; b++) {
|
|
85
|
+
let sum = 0;
|
|
86
|
+
for (let r = vbox.r1; r <= vbox.r2; r++) for (let g = vbox.g1; g <= vbox.g2; g++) sum += histo[histIndex(r, g, b)];
|
|
87
|
+
total += sum;
|
|
88
|
+
partial[b] = total;
|
|
89
|
+
}
|
|
90
|
+
const lo = axis === "r" ? vbox.r1 : axis === "g" ? vbox.g1 : vbox.b1;
|
|
91
|
+
const hi = axis === "r" ? vbox.r2 : axis === "g" ? vbox.g2 : vbox.b2;
|
|
92
|
+
for (let i = lo; i <= hi; i++) {
|
|
93
|
+
if (partial[i] <= total / 2) continue;
|
|
94
|
+
const left = i - lo;
|
|
95
|
+
const right = hi - i;
|
|
96
|
+
let d2 = left <= right ? Math.min(hi - 1, Math.trunc(i + right / 2)) : Math.max(lo, Math.trunc(i - 1 - left / 2));
|
|
97
|
+
while (d2 < hi && partial[d2] === 0) d2++;
|
|
98
|
+
let count2 = total - partial[d2];
|
|
99
|
+
while (count2 === 0 && d2 > lo && partial[d2 - 1] > 0) {
|
|
100
|
+
d2--;
|
|
101
|
+
count2 = total - partial[d2];
|
|
102
|
+
}
|
|
103
|
+
const a = copyVBox(vbox);
|
|
104
|
+
const b = copyVBox(vbox);
|
|
105
|
+
if (axis === "r") {
|
|
106
|
+
a.r2 = d2;
|
|
107
|
+
b.r1 = d2 + 1;
|
|
108
|
+
} else if (axis === "g") {
|
|
109
|
+
a.g2 = d2;
|
|
110
|
+
b.g1 = d2 + 1;
|
|
111
|
+
} else {
|
|
112
|
+
a.b2 = d2;
|
|
113
|
+
b.b1 = d2 + 1;
|
|
114
|
+
}
|
|
115
|
+
return [a, b];
|
|
116
|
+
}
|
|
117
|
+
return [copyVBox(vbox)];
|
|
118
|
+
}
|
|
119
|
+
function quantize(data, maxColors, alphaThreshold = 128) {
|
|
120
|
+
const target = Math.max(2, Math.min(256, Math.round(maxColors)));
|
|
121
|
+
const hist = {
|
|
122
|
+
counts: new Int32Array(HIST_SIZE),
|
|
123
|
+
rSum: new Int32Array(HIST_SIZE),
|
|
124
|
+
gSum: new Int32Array(HIST_SIZE),
|
|
125
|
+
bSum: new Int32Array(HIST_SIZE)
|
|
126
|
+
};
|
|
127
|
+
const histo = hist.counts;
|
|
128
|
+
let rMin = 1e9, rMax = -1, gMin = 1e9, gMax = -1, bMin = 1e9, bMax = -1;
|
|
129
|
+
let used = 0;
|
|
130
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
131
|
+
if (data[i + 3] < alphaThreshold) continue;
|
|
132
|
+
const r8 = data[i];
|
|
133
|
+
const g8 = data[i + 1];
|
|
134
|
+
const b8 = data[i + 2];
|
|
135
|
+
const r = r8 >> RSHIFT;
|
|
136
|
+
const g = g8 >> RSHIFT;
|
|
137
|
+
const b = b8 >> RSHIFT;
|
|
138
|
+
const bin = histIndex(r, g, b);
|
|
139
|
+
histo[bin] = histo[bin] + 1;
|
|
140
|
+
hist.rSum[bin] = hist.rSum[bin] + r8;
|
|
141
|
+
hist.gSum[bin] = hist.gSum[bin] + g8;
|
|
142
|
+
hist.bSum[bin] = hist.bSum[bin] + b8;
|
|
143
|
+
used++;
|
|
144
|
+
if (r < rMin) rMin = r;
|
|
145
|
+
if (r > rMax) rMax = r;
|
|
146
|
+
if (g < gMin) gMin = g;
|
|
147
|
+
if (g > gMax) gMax = g;
|
|
148
|
+
if (b < bMin) bMin = b;
|
|
149
|
+
if (b > bMax) bMax = b;
|
|
150
|
+
}
|
|
151
|
+
if (used === 0) return [];
|
|
152
|
+
let boxes = [makeVBox(rMin, rMax, gMin, gMax, bMin, bMax)];
|
|
153
|
+
const split = (limit, byVolume) => {
|
|
154
|
+
let iterations = 0;
|
|
155
|
+
while (iterations++ < MAX_ITERATIONS) {
|
|
156
|
+
if (boxes.length >= limit) return;
|
|
157
|
+
boxes.sort((a, b) => byVolume ? count(a, histo) * volume(a) - count(b, histo) * volume(b) : count(a, histo) - count(b, histo));
|
|
158
|
+
const biggest = boxes.pop();
|
|
159
|
+
if (biggest === void 0) return;
|
|
160
|
+
if (count(biggest, histo) === 0) {
|
|
161
|
+
boxes.unshift(biggest);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const parts = medianCut(histo, biggest);
|
|
165
|
+
if (parts.length === 0) return;
|
|
166
|
+
boxes.push(...parts);
|
|
167
|
+
if (parts.length === 1) return;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
split(Math.max(2, Math.round(target * FRACT_BY_POPULATIONS)), false);
|
|
171
|
+
split(target, true);
|
|
172
|
+
return boxes.map((box) => {
|
|
173
|
+
const rgb = average(box, hist);
|
|
174
|
+
return {
|
|
175
|
+
rgb,
|
|
176
|
+
population: count(box, histo),
|
|
177
|
+
hex: "#" + rgb.map((c) => c.toString(16).padStart(2, "0")).join("")
|
|
178
|
+
};
|
|
179
|
+
}).filter((s) => s.population > 0).sort((a, b) => b.population - a.population);
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export { quantize };
|
|
183
|
+
|