mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
chore: normalize line endings
This commit is contained in:
+182
-182
@@ -1,182 +1,182 @@
|
||||
import { converter, formatHex, formatHex8, parse, type Rgb } from "culori";
|
||||
|
||||
export type ColorFormat = "hex" | "rgb" | "hsl" | "hsv";
|
||||
|
||||
const toRgb = converter("rgb");
|
||||
const toHsv = converter("hsv");
|
||||
const toHsl = converter("hsl");
|
||||
|
||||
export function hexToHsv({ hex }: { hex: string }): [number, number, number] {
|
||||
const color = toHsv(`#${hex}`);
|
||||
if (!color) return [0, 0, 0];
|
||||
return [color.h ?? 0, color.s ?? 0, color.v ?? 0];
|
||||
}
|
||||
|
||||
export function hsvToHex({
|
||||
h,
|
||||
s,
|
||||
v,
|
||||
}: {
|
||||
h: number;
|
||||
s: number;
|
||||
v: number;
|
||||
}): string {
|
||||
const hex = formatHex({ mode: "hsv", h, s, v });
|
||||
return hex.slice(1);
|
||||
}
|
||||
|
||||
export function parseHexAlpha({ hex }: { hex: string }): {
|
||||
rgb: string;
|
||||
alpha: number;
|
||||
} {
|
||||
const color = parse(`#${hex}`);
|
||||
const rgbHex = color
|
||||
? formatHex(color).slice(1)
|
||||
: hex.slice(0, 6).toLowerCase();
|
||||
return {
|
||||
rgb: rgbHex,
|
||||
alpha: color?.alpha ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function appendAlpha({
|
||||
rgbHex,
|
||||
alpha,
|
||||
}: {
|
||||
rgbHex: string;
|
||||
alpha: number;
|
||||
}): string {
|
||||
if (alpha >= 1) return rgbHex;
|
||||
const hex8 = formatHex8({ mode: "rgb", r: 0, g: 0, b: 0, alpha });
|
||||
const alphaHex = hex8.slice(7, 9);
|
||||
return rgbHex + alphaHex;
|
||||
}
|
||||
|
||||
function stripCssNoise({ text }: { text: string }): string {
|
||||
let cleaned = text.trim();
|
||||
cleaned = cleaned
|
||||
.replace(/\s*!important\s*/gi, "")
|
||||
.replace(/;+\s*$/, "")
|
||||
.trim();
|
||||
|
||||
const colonIndex = cleaned.indexOf(":");
|
||||
const parenIndex = cleaned.indexOf("(");
|
||||
if (colonIndex !== -1 && (parenIndex === -1 || colonIndex < parenIndex)) {
|
||||
cleaned = cleaned.slice(colonIndex + 1).trim();
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function colorToHexWithAlpha({ color }: { color: Rgb }): string {
|
||||
const hex = formatHex(color).slice(1);
|
||||
if (color.alpha !== undefined && color.alpha < 1) {
|
||||
const hex8 = formatHex8(color);
|
||||
return hex8.slice(1);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
export function extractColorFromText({
|
||||
text,
|
||||
}: {
|
||||
text: string;
|
||||
}): string | null {
|
||||
const cleaned = stripCssNoise({ text });
|
||||
|
||||
const color = toRgb(cleaned);
|
||||
if (color) return colorToHexWithAlpha({ color });
|
||||
|
||||
// bare hex without # (culori needs the prefix)
|
||||
const bareHexMatch = cleaned.match(/^([0-9a-fA-F]{3,8})$/);
|
||||
if (bareHexMatch) {
|
||||
const withHash = toRgb(`#${bareHexMatch[1]}`);
|
||||
if (withHash) return colorToHexWithAlpha({ color: withHash });
|
||||
}
|
||||
|
||||
// fallback: find #hex anywhere in the original text
|
||||
const embeddedHexMatch = text.match(/#([0-9a-fA-F]{3,8})\b/);
|
||||
if (embeddedHexMatch) {
|
||||
const embedded = toRgb(`#${embeddedHexMatch[1]}`);
|
||||
if (embedded) return colorToHexWithAlpha({ color: embedded });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatColorValue({
|
||||
hex,
|
||||
format,
|
||||
}: {
|
||||
hex: string;
|
||||
format: ColorFormat;
|
||||
}): string {
|
||||
switch (format) {
|
||||
case "hex":
|
||||
return hex;
|
||||
case "rgb": {
|
||||
const color = toRgb(`#${hex}`);
|
||||
if (!color) return hex;
|
||||
return `${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}`;
|
||||
}
|
||||
case "hsl": {
|
||||
const color = toHsl(`#${hex}`);
|
||||
if (!color) return hex;
|
||||
return `${Math.round(color.h ?? 0)}, ${Math.round((color.s ?? 0) * 100)}%, ${Math.round((color.l ?? 0) * 100)}%`;
|
||||
}
|
||||
case "hsv": {
|
||||
const color = toHsv(`#${hex}`);
|
||||
if (!color) return hex;
|
||||
return `${Math.round(color.h ?? 0)}, ${Math.round((color.s ?? 0) * 100)}%, ${Math.round((color.v ?? 0) * 100)}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseColorInput({
|
||||
input,
|
||||
format,
|
||||
}: {
|
||||
input: string;
|
||||
format: ColorFormat;
|
||||
}): string | null {
|
||||
switch (format) {
|
||||
case "hex": {
|
||||
const cleaned = input.replace("#", "");
|
||||
const isValidHex = /^[0-9a-fA-F]{3,8}$/.test(cleaned);
|
||||
return isValidHex ? cleaned : null;
|
||||
}
|
||||
case "rgb": {
|
||||
const parts = input.split(",").map((part) => parseInt(part.trim(), 10));
|
||||
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
|
||||
const color = {
|
||||
mode: "rgb" as const,
|
||||
r: parts[0] / 255,
|
||||
g: parts[1] / 255,
|
||||
b: parts[2] / 255,
|
||||
};
|
||||
return formatHex(color).slice(1);
|
||||
}
|
||||
case "hsl": {
|
||||
const parts = input.split(",").map((part) => parseFloat(part.trim()));
|
||||
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
|
||||
const color = {
|
||||
mode: "hsl" as const,
|
||||
h: parts[0],
|
||||
s: parts[1] / 100,
|
||||
l: parts[2] / 100,
|
||||
};
|
||||
return formatHex(color).slice(1);
|
||||
}
|
||||
case "hsv": {
|
||||
const parts = input.split(",").map((part) => parseFloat(part.trim()));
|
||||
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
|
||||
const color = {
|
||||
mode: "hsv" as const,
|
||||
h: parts[0],
|
||||
s: parts[1] / 100,
|
||||
v: parts[2] / 100,
|
||||
};
|
||||
return formatHex(color).slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
import { converter, formatHex, formatHex8, parse, type Rgb } from "culori";
|
||||
|
||||
export type ColorFormat = "hex" | "rgb" | "hsl" | "hsv";
|
||||
|
||||
const toRgb = converter("rgb");
|
||||
const toHsv = converter("hsv");
|
||||
const toHsl = converter("hsl");
|
||||
|
||||
export function hexToHsv({ hex }: { hex: string }): [number, number, number] {
|
||||
const color = toHsv(`#${hex}`);
|
||||
if (!color) return [0, 0, 0];
|
||||
return [color.h ?? 0, color.s ?? 0, color.v ?? 0];
|
||||
}
|
||||
|
||||
export function hsvToHex({
|
||||
h,
|
||||
s,
|
||||
v,
|
||||
}: {
|
||||
h: number;
|
||||
s: number;
|
||||
v: number;
|
||||
}): string {
|
||||
const hex = formatHex({ mode: "hsv", h, s, v });
|
||||
return hex.slice(1);
|
||||
}
|
||||
|
||||
export function parseHexAlpha({ hex }: { hex: string }): {
|
||||
rgb: string;
|
||||
alpha: number;
|
||||
} {
|
||||
const color = parse(`#${hex}`);
|
||||
const rgbHex = color
|
||||
? formatHex(color).slice(1)
|
||||
: hex.slice(0, 6).toLowerCase();
|
||||
return {
|
||||
rgb: rgbHex,
|
||||
alpha: color?.alpha ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function appendAlpha({
|
||||
rgbHex,
|
||||
alpha,
|
||||
}: {
|
||||
rgbHex: string;
|
||||
alpha: number;
|
||||
}): string {
|
||||
if (alpha >= 1) return rgbHex;
|
||||
const hex8 = formatHex8({ mode: "rgb", r: 0, g: 0, b: 0, alpha });
|
||||
const alphaHex = hex8.slice(7, 9);
|
||||
return rgbHex + alphaHex;
|
||||
}
|
||||
|
||||
function stripCssNoise({ text }: { text: string }): string {
|
||||
let cleaned = text.trim();
|
||||
cleaned = cleaned
|
||||
.replace(/\s*!important\s*/gi, "")
|
||||
.replace(/;+\s*$/, "")
|
||||
.trim();
|
||||
|
||||
const colonIndex = cleaned.indexOf(":");
|
||||
const parenIndex = cleaned.indexOf("(");
|
||||
if (colonIndex !== -1 && (parenIndex === -1 || colonIndex < parenIndex)) {
|
||||
cleaned = cleaned.slice(colonIndex + 1).trim();
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function colorToHexWithAlpha({ color }: { color: Rgb }): string {
|
||||
const hex = formatHex(color).slice(1);
|
||||
if (color.alpha !== undefined && color.alpha < 1) {
|
||||
const hex8 = formatHex8(color);
|
||||
return hex8.slice(1);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
export function extractColorFromText({
|
||||
text,
|
||||
}: {
|
||||
text: string;
|
||||
}): string | null {
|
||||
const cleaned = stripCssNoise({ text });
|
||||
|
||||
const color = toRgb(cleaned);
|
||||
if (color) return colorToHexWithAlpha({ color });
|
||||
|
||||
// bare hex without # (culori needs the prefix)
|
||||
const bareHexMatch = cleaned.match(/^([0-9a-fA-F]{3,8})$/);
|
||||
if (bareHexMatch) {
|
||||
const withHash = toRgb(`#${bareHexMatch[1]}`);
|
||||
if (withHash) return colorToHexWithAlpha({ color: withHash });
|
||||
}
|
||||
|
||||
// fallback: find #hex anywhere in the original text
|
||||
const embeddedHexMatch = text.match(/#([0-9a-fA-F]{3,8})\b/);
|
||||
if (embeddedHexMatch) {
|
||||
const embedded = toRgb(`#${embeddedHexMatch[1]}`);
|
||||
if (embedded) return colorToHexWithAlpha({ color: embedded });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatColorValue({
|
||||
hex,
|
||||
format,
|
||||
}: {
|
||||
hex: string;
|
||||
format: ColorFormat;
|
||||
}): string {
|
||||
switch (format) {
|
||||
case "hex":
|
||||
return hex;
|
||||
case "rgb": {
|
||||
const color = toRgb(`#${hex}`);
|
||||
if (!color) return hex;
|
||||
return `${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}`;
|
||||
}
|
||||
case "hsl": {
|
||||
const color = toHsl(`#${hex}`);
|
||||
if (!color) return hex;
|
||||
return `${Math.round(color.h ?? 0)}, ${Math.round((color.s ?? 0) * 100)}%, ${Math.round((color.l ?? 0) * 100)}%`;
|
||||
}
|
||||
case "hsv": {
|
||||
const color = toHsv(`#${hex}`);
|
||||
if (!color) return hex;
|
||||
return `${Math.round(color.h ?? 0)}, ${Math.round((color.s ?? 0) * 100)}%, ${Math.round((color.v ?? 0) * 100)}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseColorInput({
|
||||
input,
|
||||
format,
|
||||
}: {
|
||||
input: string;
|
||||
format: ColorFormat;
|
||||
}): string | null {
|
||||
switch (format) {
|
||||
case "hex": {
|
||||
const cleaned = input.replace("#", "");
|
||||
const isValidHex = /^[0-9a-fA-F]{3,8}$/.test(cleaned);
|
||||
return isValidHex ? cleaned : null;
|
||||
}
|
||||
case "rgb": {
|
||||
const parts = input.split(",").map((part) => parseInt(part.trim(), 10));
|
||||
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
|
||||
const color = {
|
||||
mode: "rgb" as const,
|
||||
r: parts[0] / 255,
|
||||
g: parts[1] / 255,
|
||||
b: parts[2] / 255,
|
||||
};
|
||||
return formatHex(color).slice(1);
|
||||
}
|
||||
case "hsl": {
|
||||
const parts = input.split(",").map((part) => parseFloat(part.trim()));
|
||||
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
|
||||
const color = {
|
||||
mode: "hsl" as const,
|
||||
h: parts[0],
|
||||
s: parts[1] / 100,
|
||||
l: parts[2] / 100,
|
||||
};
|
||||
return formatHex(color).slice(1);
|
||||
}
|
||||
case "hsv": {
|
||||
const parts = input.split(",").map((part) => parseFloat(part.trim()));
|
||||
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
|
||||
const color = {
|
||||
mode: "hsv" as const,
|
||||
h: parts[0],
|
||||
s: parts[1] / 100,
|
||||
v: parts[2] / 100,
|
||||
};
|
||||
return formatHex(color).slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-28
@@ -1,28 +1,28 @@
|
||||
export function generateUUID(): string {
|
||||
if (
|
||||
typeof crypto !== "undefined" &&
|
||||
typeof crypto.randomUUID === "function"
|
||||
) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(16);
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
|
||||
|
||||
return (
|
||||
hex.slice(0, 4).join("") +
|
||||
"-" +
|
||||
hex.slice(4, 6).join("") +
|
||||
"-" +
|
||||
hex.slice(6, 8).join("") +
|
||||
"-" +
|
||||
hex.slice(8, 10).join("") +
|
||||
"-" +
|
||||
hex.slice(10, 16).join("")
|
||||
);
|
||||
}
|
||||
export function generateUUID(): string {
|
||||
if (
|
||||
typeof crypto !== "undefined" &&
|
||||
typeof crypto.randomUUID === "function"
|
||||
) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(16);
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
|
||||
|
||||
return (
|
||||
hex.slice(0, 4).join("") +
|
||||
"-" +
|
||||
hex.slice(4, 6).join("") +
|
||||
"-" +
|
||||
hex.slice(6, 8).join("") +
|
||||
"-" +
|
||||
hex.slice(8, 10).join("") +
|
||||
"-" +
|
||||
hex.slice(10, 16).join("")
|
||||
);
|
||||
}
|
||||
|
||||
+84
-84
@@ -1,84 +1,84 @@
|
||||
export function clamp({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
}: {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
export function clampRound({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
}: {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}): number {
|
||||
return Math.round(clamp({ value, min, max }));
|
||||
}
|
||||
|
||||
function getFractionDigitsForStep({ step }: { step: number }): number {
|
||||
const normalizedStep = step.toString().toLowerCase();
|
||||
if (normalizedStep.includes("e-")) {
|
||||
return Number(normalizedStep.split("e-")[1] ?? 0);
|
||||
}
|
||||
const [, fractionalPart = ""] = normalizedStep.split(".");
|
||||
return fractionalPart.length;
|
||||
}
|
||||
|
||||
export function snapToStep({
|
||||
value,
|
||||
step,
|
||||
}: {
|
||||
value: number;
|
||||
step: number;
|
||||
}): number {
|
||||
if (step <= 0) return value;
|
||||
const snappedValue = Math.round(value / step) * step;
|
||||
return Number(
|
||||
snappedValue.toFixed(getFractionDigitsForStep({ step })),
|
||||
);
|
||||
}
|
||||
|
||||
export function isNearlyEqual({
|
||||
leftValue,
|
||||
rightValue,
|
||||
epsilon = 0.0001,
|
||||
}: {
|
||||
leftValue: number;
|
||||
rightValue: number;
|
||||
epsilon?: number;
|
||||
}): boolean {
|
||||
return Math.abs(leftValue - rightValue) <= epsilon;
|
||||
}
|
||||
|
||||
export function formatNumberForDisplay({
|
||||
value,
|
||||
maxFractionDigits = 6,
|
||||
}: {
|
||||
value: number;
|
||||
maxFractionDigits?: number;
|
||||
}): string {
|
||||
return Number(value.toFixed(maxFractionDigits)).toString();
|
||||
}
|
||||
|
||||
export function evaluateMathExpression({
|
||||
input,
|
||||
}: {
|
||||
input: string;
|
||||
}): number | null {
|
||||
const sanitized = input.trim();
|
||||
if (!/^[\d.\s+\-*/()]+$/.test(sanitized)) return null;
|
||||
try {
|
||||
const result = new Function(`return (${sanitized})`)();
|
||||
if (typeof result !== "number" || !Number.isFinite(result)) return null;
|
||||
return result;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export function clamp({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
}: {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
export function clampRound({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
}: {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}): number {
|
||||
return Math.round(clamp({ value, min, max }));
|
||||
}
|
||||
|
||||
function getFractionDigitsForStep({ step }: { step: number }): number {
|
||||
const normalizedStep = step.toString().toLowerCase();
|
||||
if (normalizedStep.includes("e-")) {
|
||||
return Number(normalizedStep.split("e-")[1] ?? 0);
|
||||
}
|
||||
const [, fractionalPart = ""] = normalizedStep.split(".");
|
||||
return fractionalPart.length;
|
||||
}
|
||||
|
||||
export function snapToStep({
|
||||
value,
|
||||
step,
|
||||
}: {
|
||||
value: number;
|
||||
step: number;
|
||||
}): number {
|
||||
if (step <= 0) return value;
|
||||
const snappedValue = Math.round(value / step) * step;
|
||||
return Number(
|
||||
snappedValue.toFixed(getFractionDigitsForStep({ step })),
|
||||
);
|
||||
}
|
||||
|
||||
export function isNearlyEqual({
|
||||
leftValue,
|
||||
rightValue,
|
||||
epsilon = 0.0001,
|
||||
}: {
|
||||
leftValue: number;
|
||||
rightValue: number;
|
||||
epsilon?: number;
|
||||
}): boolean {
|
||||
return Math.abs(leftValue - rightValue) <= epsilon;
|
||||
}
|
||||
|
||||
export function formatNumberForDisplay({
|
||||
value,
|
||||
maxFractionDigits = 6,
|
||||
}: {
|
||||
value: number;
|
||||
maxFractionDigits?: number;
|
||||
}): string {
|
||||
return Number(value.toFixed(maxFractionDigits)).toString();
|
||||
}
|
||||
|
||||
export function evaluateMathExpression({
|
||||
input,
|
||||
}: {
|
||||
input: string;
|
||||
}): number | null {
|
||||
const sanitized = input.trim();
|
||||
if (!/^[\d.\s+\-*/()]+$/.test(sanitized)) return null;
|
||||
try {
|
||||
const result = new Function(`return (${sanitized})`)();
|
||||
if (typeof result !== "number" || !Number.isFinite(result)) return null;
|
||||
return result;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user