mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
refactor: parse hex color
This commit is contained in:
@@ -110,12 +110,15 @@ round-семантика `clampByte` и trunc-семантика `clampInt` со
|
||||
переведён на общий `clamp` через `?? -Infinity / ?? Infinity`. Копия в
|
||||
`palette.test.ts` оставлена — тесты независимы.
|
||||
|
||||
### M8. Два параллельных hex-парсера
|
||||
### M8. Два параллельных hex-парсера — СДЕЛАНО (2026-10)
|
||||
|
||||
`parseHex` (`core/alpha.ts:147`, tuple, бросает `ToolError`) и `hexToRgb`
|
||||
(`core/palette.ts:8`, объект `{r,g,b}`). Половина core-модулей использует первый
|
||||
(`affine.ts:31`, `geometry.ts:20`, `morphology.ts:123,150`), другая — второй
|
||||
(`masks.ts:25,98`, `pixel-fx.ts:195`, `registry/tools/generate.ts:36`).
|
||||
Единый парсер — `parseHexColor(hex): Rgb` в `core/palette.ts` (терпимый: `#?`,
|
||||
3/6 цифр, бросает `ToolError`). `hexToRgb` удалён; `parseHex` из `core/alpha.ts`
|
||||
удалён, потребители (`affine`, `background`, `geometry`, `morphology`, `alpha`)
|
||||
переведены на объектную деструктуризацию и импорт из `palette`. Тест `parseHex`
|
||||
переехал из `alpha.test.ts` в `palette.test.ts`. Поведенческое послабление:
|
||||
бывшие потребители `hexToRgb` теперь принимают и 3-значный hex — вход всё равно
|
||||
проходит санитизацию схемы.
|
||||
|
||||
### M9. Гард `localStorage` продублирован 4 раза — СДЕЛАНО (2026-10)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseHex } from "./alpha";
|
||||
import { parseHexColor } from "./palette";
|
||||
import { ToolError } from "./errors";
|
||||
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
||||
import { sampleBilinear } from "./geometry";
|
||||
@@ -28,7 +28,7 @@ export function transformImage(
|
||||
): PixelImage {
|
||||
const [a, b, c, d, e, f] = dstToSrc;
|
||||
const out = createPixelImage(outWidth, outHeight);
|
||||
const bg = bgHex ? parseHex(bgHex) : null;
|
||||
const bg = bgHex ? parseHexColor(bgHex) : null;
|
||||
for (let y = 0; y < outHeight; y++) {
|
||||
for (let x = 0; x < outWidth; x++) {
|
||||
const sx = a * x + c * y + e;
|
||||
@@ -41,9 +41,9 @@ export function transformImage(
|
||||
sy > img.height - 1 + EPS
|
||||
) {
|
||||
if (bg) {
|
||||
out.data[di] = bg[0];
|
||||
out.data[di + 1] = bg[1];
|
||||
out.data[di + 2] = bg[2];
|
||||
out.data[di] = bg.r;
|
||||
out.data[di + 1] = bg.g;
|
||||
out.data[di + 2] = bg.b;
|
||||
out.data[di + 3] = 255;
|
||||
}
|
||||
continue;
|
||||
@@ -51,9 +51,9 @@ export function transformImage(
|
||||
const [r, g, bl, al] = sampleBilinear(img, sx, sy);
|
||||
if (bg) {
|
||||
const sa = al / 255;
|
||||
out.data[di] = r * sa + bg[0] * (1 - sa);
|
||||
out.data[di + 1] = g * sa + bg[1] * (1 - sa);
|
||||
out.data[di + 2] = bl * sa + bg[2] * (1 - sa);
|
||||
out.data[di] = r * sa + bg.r * (1 - sa);
|
||||
out.data[di + 1] = g * sa + bg.g * (1 - sa);
|
||||
out.data[di + 2] = bl * sa + bg.b * (1 - sa);
|
||||
out.data[di + 3] = 255;
|
||||
} else {
|
||||
out.data[di] = r;
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
flattenOntoColor,
|
||||
hardenAlpha,
|
||||
invertAlpha,
|
||||
parseHex,
|
||||
removeColorToAlpha,
|
||||
roundCorners,
|
||||
setAlphaChannel,
|
||||
@@ -171,15 +170,3 @@ describe("flattenOntoColor", () => {
|
||||
expect([...out.data]).toEqual([132, 137, 142, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseHex", () => {
|
||||
it("parses #rrggbb, rrggbb, #rgb", () => {
|
||||
expect(parseHex("#ff8040")).toEqual([255, 128, 64]);
|
||||
expect(parseHex("ff8040")).toEqual([255, 128, 64]);
|
||||
expect(parseHex("#F80")).toEqual([255, 136, 0]);
|
||||
});
|
||||
|
||||
it.each(["zzz", "12345", "##ff", ""])('throws on "%s"', (bad) => {
|
||||
expect(() => parseHex(bad)).toThrow(/errors\.badHex/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createPixelImage, type PixelImage } from "./types";
|
||||
import { ToolError } from "./errors";
|
||||
import { clamp } from "./math";
|
||||
import { parseHexColor } from "./palette";
|
||||
|
||||
const MAX_COLOR_DISTANCE = Math.sqrt(3 * 255 * 255);
|
||||
|
||||
@@ -9,7 +10,7 @@ export function removeColorToAlpha(
|
||||
hex: string,
|
||||
tolerancePercent = 0,
|
||||
): PixelImage {
|
||||
const [targetR, targetG, targetB] = parseHex(hex);
|
||||
const { r: targetR, g: targetG, b: targetB } = parseHexColor(hex);
|
||||
const tolerance =
|
||||
(clamp(tolerancePercent, 0, 100) / 100) * MAX_COLOR_DISTANCE;
|
||||
const thresholdSq = tolerance * tolerance;
|
||||
@@ -113,7 +114,7 @@ export function colorMask(
|
||||
hex: string,
|
||||
tolerancePercent = 0,
|
||||
): PixelImage {
|
||||
const [targetR, targetG, targetB] = parseHex(hex);
|
||||
const { r: targetR, g: targetG, b: targetB } = parseHexColor(hex);
|
||||
const tolerance =
|
||||
(clamp(tolerancePercent, 0, 100) / 100) * MAX_COLOR_DISTANCE;
|
||||
const thresholdSq = tolerance * tolerance;
|
||||
@@ -132,7 +133,7 @@ export function colorMask(
|
||||
}
|
||||
|
||||
export function flattenOntoColor(img: PixelImage, hex: string): PixelImage {
|
||||
const [bgR, bgG, bgB] = parseHex(hex);
|
||||
const { r: bgR, g: bgG, b: bgB } = parseHexColor(hex);
|
||||
const out = createPixelImage(img.width, img.height);
|
||||
for (let i = 0; i < out.data.length; i += 4) {
|
||||
const a = img.data[i + 3] / 255;
|
||||
@@ -144,23 +145,3 @@ export function flattenOntoColor(img: PixelImage, hex: string): PixelImage {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseHex(hex: string): [number, number, number] {
|
||||
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
||||
if (!match) {
|
||||
throw new ToolError("errors.badHex", { value: hex });
|
||||
}
|
||||
const digits = match[1];
|
||||
if (digits.length === 3) {
|
||||
return [
|
||||
parseInt(digits[0] + digits[0], 16),
|
||||
parseInt(digits[1] + digits[1], 16),
|
||||
parseInt(digits[2] + digits[2], 16),
|
||||
];
|
||||
}
|
||||
return [
|
||||
parseInt(digits.slice(0, 2), 16),
|
||||
parseInt(digits.slice(2, 4), 16),
|
||||
parseInt(digits.slice(4, 6), 16),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseHex } from "./alpha";
|
||||
import { parseHexColor } from "./palette";
|
||||
import { createPixelImage, type PixelImage } from "./types";
|
||||
import { clamp } from "./math";
|
||||
|
||||
@@ -93,7 +93,7 @@ export function backgroundRemovalMask(
|
||||
img: PixelImage,
|
||||
options: BackgroundOptions,
|
||||
): Uint8Array {
|
||||
const [tr, tg, tb] = parseHex(options.color);
|
||||
const { r: tr, g: tg, b: tb } = parseHexColor(options.color);
|
||||
const mask = buildRawMask(img, tr, tg, tb, options.tolerancePercent);
|
||||
if (options.outerOnly) {
|
||||
floodFromBorders(mask, img.width, img.height);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hexToRgb } from "./palette";
|
||||
import { parseHexColor } from "./palette";
|
||||
import { renderSpace, SPACES } from "./channels";
|
||||
import { makeImage } from "./test-helpers";
|
||||
|
||||
describe("space conversions", () => {
|
||||
it("hsl of red: h=0, s=1, l=0.5", () => {
|
||||
const [h, s, l] = SPACES.hsl.convert(hexToRgb("#ff0000"));
|
||||
const [h, s, l] = SPACES.hsl.convert(parseHexColor("#ff0000"));
|
||||
expect(h).toBeCloseTo(0);
|
||||
expect(s).toBeCloseTo(1);
|
||||
expect(l).toBeCloseTo(0.5);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseHex } from "./alpha";
|
||||
import { parseHexColor } from "./palette";
|
||||
import { ToolError } from "./errors";
|
||||
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
||||
import { clampInt } from "./math";
|
||||
@@ -18,7 +18,7 @@ export function expandCanvas(
|
||||
const b = Math.max(0, Math.trunc(bottom));
|
||||
const out = createPixelImage(img.width + l + r, img.height + t + b);
|
||||
if (backgroundHex !== undefined && backgroundAlpha > 0) {
|
||||
const [cr, cg, cb] = parseHex(backgroundHex);
|
||||
const { r: cr, g: cg, b: cb } = parseHexColor(backgroundHex);
|
||||
for (let i = 0; i < out.data.length; i += 4) {
|
||||
out.data[i] = cr;
|
||||
out.data[i + 1] = cg;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hexToRgb } from "./palette";
|
||||
import { parseHexColor } from "./palette";
|
||||
import type { PixelImage } from "./types";
|
||||
import { createPixelImage } from "./types";
|
||||
|
||||
@@ -22,7 +22,7 @@ export function renderPredicateMask(
|
||||
): PixelImage {
|
||||
const out = createPixelImage(img.width, img.height);
|
||||
const highlight = o.mode !== "binary";
|
||||
const tint = o.color ? hexToRgb(o.color) : { r: 255, g: 0, b: 170 };
|
||||
const tint = o.color ? parseHexColor(o.color) : { r: 255, g: 0, b: 170 };
|
||||
const opacity = Math.min(Math.max(o.opacityPercent ?? 70, 0), 100) / 100;
|
||||
if (!highlight) {
|
||||
// Binary mask: opaque white-on-black
|
||||
@@ -95,7 +95,7 @@ export function extractByColor(
|
||||
tolerancePercent: number,
|
||||
): PixelImage {
|
||||
const out = createPixelImage(img.width, img.height);
|
||||
const t = hexToRgb(targetHex);
|
||||
const t = parseHexColor(targetHex);
|
||||
const tol = (Math.min(Math.max(tolerancePercent, 0), 100) / 100) * 255;
|
||||
for (let i = 0; i < img.data.length; i += 4) {
|
||||
if (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseHex } from "./alpha";
|
||||
import { parseHexColor } from "./palette";
|
||||
import { clonePixelImage, createPixelImage, type PixelImage } from "./types";
|
||||
|
||||
type Mask = Uint8Array;
|
||||
@@ -120,7 +120,7 @@ export function strokeImage(
|
||||
): PixelImage {
|
||||
const r = Math.trunc(radiusPx);
|
||||
if (r < 1) return clonePixelImage(img);
|
||||
const [cr, cg, cb] = parseHex(colorHex);
|
||||
const { r: cr, g: cg, b: cb } = parseHexColor(colorHex);
|
||||
const mask = buildAlphaMask(img);
|
||||
const ring = dilateMask(mask, img.width, img.height, r);
|
||||
const out = createPixelImage(img.width, img.height);
|
||||
@@ -147,7 +147,7 @@ export function contourImage(
|
||||
colorHex: string,
|
||||
): PixelImage {
|
||||
const r = Math.max(1, Math.trunc(radiusPx));
|
||||
const [cr, cg, cb] = parseHex(colorHex);
|
||||
const { r: cr, g: cg, b: cb } = parseHexColor(colorHex);
|
||||
const mask = buildAlphaMask(img);
|
||||
const inner = erodeMask(mask, img.width, img.height, r);
|
||||
const line = new Uint8Array(mask.length);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
analogousSet,
|
||||
complementarySet,
|
||||
hexToRgb,
|
||||
parseHexColor,
|
||||
hslToRgb,
|
||||
monochromaticSet,
|
||||
mixColors,
|
||||
@@ -19,9 +19,21 @@ import {
|
||||
tetradicSet,
|
||||
} from "./palette";
|
||||
|
||||
describe("parseHexColor", () => {
|
||||
it("parses #rrggbb, rrggbb, #rgb", () => {
|
||||
expect(parseHexColor("#ff8040")).toEqual({ r: 255, g: 128, b: 64 });
|
||||
expect(parseHexColor("ff8040")).toEqual({ r: 255, g: 128, b: 64 });
|
||||
expect(parseHexColor("#F80")).toEqual({ r: 255, g: 136, b: 0 });
|
||||
});
|
||||
|
||||
it.each(["zzz", "12345", "##ff", ""])('throws on "%s"', (bad) => {
|
||||
expect(() => parseHexColor(bad)).toThrow(/errors\.badHex/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rgb<->hsl conversion", () => {
|
||||
it("red -> hsl(0,100%,50%) and back", () => {
|
||||
const hsl = rgbToHsl(hexToRgb("#ff0000"));
|
||||
const hsl = rgbToHsl(parseHexColor("#ff0000"));
|
||||
expect(hsl.h).toBeCloseTo(0, 0);
|
||||
expect(hsl.s).toBeCloseTo(1, 5);
|
||||
expect(hsl.l).toBeCloseTo(0.5, 5);
|
||||
@@ -29,14 +41,14 @@ describe("rgb<->hsl conversion", () => {
|
||||
});
|
||||
|
||||
it("grays have no hue", () => {
|
||||
expect(rgbToHsl(hexToRgb("#808080")).s).toBe(0);
|
||||
expect(rgbToHsl(parseHexColor("#808080")).s).toBe(0);
|
||||
});
|
||||
|
||||
it("360 deg wraparound returns original color", () => {
|
||||
const base = "#3b82f6";
|
||||
expect(
|
||||
rgbToHex(hslToRgb({ ...rgbToHsl(hexToRgb(base)), h: 720 + 30 })),
|
||||
).toBe(rgbToHex(hslToRgb({ ...rgbToHsl(hexToRgb(base)), h: 30 })));
|
||||
rgbToHex(hslToRgb({ ...rgbToHsl(parseHexColor(base)), h: 720 + 30 })),
|
||||
).toBe(rgbToHex(hslToRgb({ ...rgbToHsl(parseHexColor(base)), h: 30 })));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,15 +56,15 @@ describe("harmonies", () => {
|
||||
it("complementary - pair shifted 180 deg", () => {
|
||||
const [a, b] = complementarySet("#ff0000");
|
||||
expect(a).toBe("#ff0000");
|
||||
const ha = rgbToHsl(hexToRgb(a)).h;
|
||||
const hb = rgbToHsl(hexToRgb(b)).h;
|
||||
const ha = rgbToHsl(parseHexColor(a)).h;
|
||||
const hb = rgbToHsl(parseHexColor(b)).h;
|
||||
expect(Math.abs((hb - ha + 360) % 360)).toBeCloseTo(180, 0);
|
||||
});
|
||||
|
||||
it("triadic - three colors 120 deg apart", () => {
|
||||
const set = triadicSet("#ff0000");
|
||||
expect(set).toHaveLength(3);
|
||||
const hues = set.map((h) => rgbToHsl(hexToRgb(h)).h);
|
||||
const hues = set.map((h) => rgbToHsl(parseHexColor(h)).h);
|
||||
expect(hues[1] - hues[0]).toBeCloseTo(120, 0);
|
||||
expect(hues[2] - hues[0]).toBeCloseTo(240, 0);
|
||||
});
|
||||
@@ -71,15 +83,17 @@ describe("harmonies", () => {
|
||||
it("monochromatic keeps hue, varies lightness", () => {
|
||||
const set = monochromaticSet("#ff8800", 5, 60);
|
||||
expect(set).toHaveLength(5);
|
||||
const hues = new Set(set.map((h) => Math.round(rgbToHsl(hexToRgb(h)).h)));
|
||||
const hues = new Set(
|
||||
set.map((h) => Math.round(rgbToHsl(parseHexColor(h)).h)),
|
||||
);
|
||||
expect(hues.size).toBe(1);
|
||||
const lights = set.map((h) => rgbToHsl(hexToRgb(h)).l);
|
||||
const lights = set.map((h) => rgbToHsl(parseHexColor(h)).l);
|
||||
expect(Math.min(...lights)).toBeLessThan(Math.max(...lights));
|
||||
});
|
||||
|
||||
it("shades - dark edge darker than base", () => {
|
||||
const set = shadeSet("#88cc44", 4, 70);
|
||||
const lights = set.map((h) => rgbToHsl(hexToRgb(h)).l);
|
||||
const lights = set.map((h) => rgbToHsl(parseHexColor(h)).l);
|
||||
expect(lights[lights.length - 1]).toBeLessThan(lights[0]);
|
||||
});
|
||||
});
|
||||
|
||||
+22
-16
@@ -6,10 +6,16 @@ import { clamp01 } from "./math";
|
||||
export type Rgb = { r: number; g: number; b: number };
|
||||
export type Hsl = { h: number; s: number; l: number };
|
||||
|
||||
export function hexToRgb(hex: string): Rgb {
|
||||
const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
||||
export function parseHexColor(hex: string): Rgb {
|
||||
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
||||
if (!m) throw new ToolError("errors.badHex", { value: hex });
|
||||
const d = m[1];
|
||||
const d =
|
||||
m[1].length === 3
|
||||
? m[1]
|
||||
.split("")
|
||||
.map((ch) => ch + ch)
|
||||
.join("")
|
||||
: m[1];
|
||||
return {
|
||||
r: parseInt(d.slice(0, 2), 16),
|
||||
g: parseInt(d.slice(2, 4), 16),
|
||||
@@ -66,12 +72,12 @@ export function hslToRgb({ h, s, l }: Hsl): Rgb {
|
||||
}
|
||||
|
||||
export function shiftHue(hex: string, deltaDeg: number): string {
|
||||
const hsl = rgbToHsl(hexToRgb(hex));
|
||||
const hsl = rgbToHsl(parseHexColor(hex));
|
||||
return rgbToHex(hslToRgb({ ...hsl, h: hsl.h + deltaDeg }));
|
||||
}
|
||||
|
||||
function withLightness(hex: string, l: number): string {
|
||||
return rgbToHex(hslToRgb({ ...rgbToHsl(hexToRgb(hex)), l }));
|
||||
return rgbToHex(hslToRgb({ ...rgbToHsl(parseHexColor(hex)), l }));
|
||||
}
|
||||
|
||||
export function complementarySet(base: string): string[] {
|
||||
@@ -109,7 +115,7 @@ export function monochromaticSet(
|
||||
rangePercent: number,
|
||||
): string[] {
|
||||
const n = Math.max(2, Math.min(9, Math.round(count)));
|
||||
const baseL = rgbToHsl(hexToRgb(normalizeHex(base))).l;
|
||||
const baseL = rgbToHsl(parseHexColor(normalizeHex(base))).l;
|
||||
const halfSpan = Math.min(0.495, rangePercent / 200);
|
||||
return Array.from({ length: n }, (_, i) => {
|
||||
const t = n === 1 ? 0.5 : i / (n - 1);
|
||||
@@ -124,7 +130,7 @@ export function shadeSet(
|
||||
depthPercent: number,
|
||||
): string[] {
|
||||
const n = Math.max(2, Math.min(9, Math.round(count)));
|
||||
const baseL = rgbToHsl(hexToRgb(normalizeHex(base))).l;
|
||||
const baseL = rgbToHsl(parseHexColor(normalizeHex(base))).l;
|
||||
const floorL = Math.max(0.03, baseL - depthPercent / 100);
|
||||
return Array.from({ length: n }, (_, i) => {
|
||||
const t = i / (n - 1);
|
||||
@@ -150,7 +156,7 @@ export function normalizeHex(hex: string): string {
|
||||
|
||||
export function mixColors(hexes: string[]): string {
|
||||
const sum = hexes
|
||||
.map(hexToRgb)
|
||||
.map(parseHexColor)
|
||||
.reduce((acc, c) => ({ r: acc.r + c.r, g: acc.g + c.g, b: acc.b + c.b }), {
|
||||
r: 0,
|
||||
g: 0,
|
||||
@@ -164,7 +170,7 @@ export function mixColors(hexes: string[]): string {
|
||||
}
|
||||
|
||||
export function luma(hex: string): number {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
const { r, g, b } = parseHexColor(hex);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
@@ -173,7 +179,7 @@ export type SortKey = "hue" | "luma" | "sat";
|
||||
export function sortPalette(hexes: string[], key: SortKey): string[] {
|
||||
const scored = hexes.map((h) => {
|
||||
if (key === "luma") return { h, k: luma(h) };
|
||||
const hsl = rgbToHsl(hexToRgb(h));
|
||||
const hsl = rgbToHsl(parseHexColor(h));
|
||||
return { h, k: key === "hue" ? hsl.h : hsl.s };
|
||||
});
|
||||
return scored
|
||||
@@ -193,7 +199,7 @@ export function renderSwatches(
|
||||
const height = Math.max(24, Math.round(cellW));
|
||||
const out = createPixelImage(width, height);
|
||||
colors.forEach((hex, i) => {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
const { r, g, b } = parseHexColor(hex);
|
||||
fillRect(
|
||||
out,
|
||||
Math.floor(i * cellW),
|
||||
@@ -213,7 +219,7 @@ export function renderSwatches(
|
||||
const height = cell * rows;
|
||||
const out = createPixelImage(width, height);
|
||||
colors.forEach((hex, i) => {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
const { r, g, b } = parseHexColor(hex);
|
||||
fillRect(
|
||||
out,
|
||||
(i % cols) * cell,
|
||||
@@ -254,8 +260,8 @@ export function renderWheel(size: number, lightness: number): PixelImage {
|
||||
export function renderBlend(a: string, b: string, width: number): PixelImage {
|
||||
const height = Math.max(24, Math.round(width / 4));
|
||||
const out = createPixelImage(width, height);
|
||||
const ca = hexToRgb(a);
|
||||
const cb = hexToRgb(b);
|
||||
const ca = parseHexColor(a);
|
||||
const cb = parseHexColor(b);
|
||||
for (let x = 0; x < width; x++) {
|
||||
const t = width === 1 ? 0 : x / (width - 1);
|
||||
fillRect(
|
||||
@@ -278,8 +284,8 @@ export function stepColors(
|
||||
steps: number,
|
||||
): string[] {
|
||||
const n = Math.max(2, Math.min(12, Math.round(steps)));
|
||||
const ca = hexToRgb(aHex);
|
||||
const cb = hexToRgb(bHex);
|
||||
const ca = parseHexColor(aHex);
|
||||
const cb = parseHexColor(bHex);
|
||||
return Array.from({ length: n }, (_, i) => {
|
||||
const t = i / (n - 1);
|
||||
return rgbToHex({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hexToRgb } from "./palette";
|
||||
import { parseHexColor } from "./palette";
|
||||
import type { PixelImage } from "./types";
|
||||
import { createPixelImage } from "./types";
|
||||
import { gaussianBlur } from "./convolution";
|
||||
@@ -189,7 +189,7 @@ export function silhouette(
|
||||
alphaThreshold: number,
|
||||
): PixelImage {
|
||||
const out = createPixelImage(img.width, img.height);
|
||||
const { r, g, b } = hexToRgb(colorHex);
|
||||
const { r, g, b } = parseHexColor(colorHex);
|
||||
for (let i = 0; i < img.data.length; i += 4) {
|
||||
if (img.data[i + 3] <= alphaThreshold) continue;
|
||||
out.data[i] = r;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PixelImage } from "./types";
|
||||
import { createPixelImage } from "./types";
|
||||
import { rgbToHex } from "./palette";
|
||||
import { hexToRgb } from "./palette";
|
||||
import { parseHexColor } from "./palette";
|
||||
|
||||
type Rgb = { r: number; g: number; b: number };
|
||||
|
||||
@@ -127,7 +127,7 @@ export function mapToNearest(
|
||||
img: PixelImage,
|
||||
paletteHexes: string[],
|
||||
): PixelImage {
|
||||
const palette = paletteHexes.map(hexToRgb);
|
||||
const palette = paletteHexes.map(parseHexColor);
|
||||
const out = createPixelImage(img.width, img.height);
|
||||
for (let i = 0; i < img.data.length; i += 4) {
|
||||
out.data[i + 3] = img.data[i + 3];
|
||||
@@ -165,7 +165,7 @@ export function ditherImage(
|
||||
forcedPaletteHexes?: string[],
|
||||
): PixelImage {
|
||||
const palette = forcedPaletteHexes
|
||||
? forcedPaletteHexes.map(hexToRgb)
|
||||
? forcedPaletteHexes.map(parseHexColor)
|
||||
: medianCutPalette(img, k);
|
||||
const total = img.width * img.height;
|
||||
const buf = new Float32Array(total * 3);
|
||||
|
||||
@@ -9,9 +9,9 @@ import { changeCanvasSize } from "../../core/geometry";
|
||||
import {
|
||||
analogousSet,
|
||||
complementarySet,
|
||||
hexToRgb,
|
||||
mixColors,
|
||||
monochromaticSet,
|
||||
parseHexColor,
|
||||
renderBlend,
|
||||
renderSwatches,
|
||||
renderWheel,
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { genTool, type Tool } from "../types";
|
||||
|
||||
function rgba(hex: string): [number, number, number, number] {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
const { r, g, b } = parseHexColor(hex);
|
||||
return [r, g, b, 255];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user