Files
wehub-resource-sync 85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

182 lines
5.7 KiB
JavaScript

const DEFAULT_SIZE = 33;
const MAX_SIZE = 64;
function clamp(value, min, max) {
if (!Number.isFinite(value)) return 0;
return Math.min(max, Math.max(min, value));
}
function clampUnit(value) {
return clamp(value, 0, 1);
}
function readParam(params, key, min, max) {
return clamp(Number(params?.[key] ?? 0), min, max);
}
function luma([r, g, b]) {
// Rec.709 luma weightings (matches the color space the grading runtime uses).
return r * 0.2126 + g * 0.7152 + b * 0.0722;
}
function smoothstep(edge0, edge1, value) {
const t = clampUnit((value - edge0) / (edge1 - edge0));
return t * t * (3 - 2 * t);
}
function applyLiftGain(color, params) {
const y = luma(color);
const blacks = readParam(params, "blacks", -1, 1);
const shadows = readParam(params, "shadows", -1, 1);
const highlights = readParam(params, "highlights", -1, 1);
const whites = readParam(params, "whites", -1, 1);
const shadowMask = 1 - smoothstep(0.18, 0.62, y);
const highlightMask = smoothstep(0.38, 0.82, y);
const offset =
blacks * 0.08 + shadows * 0.12 * shadowMask + highlights * 0.12 * highlightMask + whites * 0.08;
return color.map((channel) => clampUnit(channel + offset));
}
function applyExposure(color, params) {
const exposure = readParam(params, "exposure", -2, 2);
const gain = 2 ** exposure;
const lift = Math.max(0, exposure) * 0.015;
return color.map((channel) => clampUnit(channel * gain + lift));
}
function applyContrast(color, params) {
const contrast = readParam(params, "contrast", -1, 1);
if (contrast === 0) return color;
const factor = 1 + contrast * 1.2;
return color.map((channel) => clampUnit(0.5 + (channel - 0.5) * factor));
}
function applyWhiteBalance(color, params) {
const temperature = readParam(params, "temperature", -1, 1);
const tint = readParam(params, "tint", -1, 1);
const redScale = 1 + temperature * 0.28 + tint * 0.08;
const greenScale = 1 - Math.abs(tint) * 0.1 - tint * 0.08;
const blueScale = 1 - temperature * 0.28 + tint * 0.08;
return [
clampUnit(color[0] * redScale),
clampUnit(color[1] * greenScale),
clampUnit(color[2] * blueScale),
];
}
function applySplitTone(color, params) {
const split = params?.splitTone;
if (!split) return color;
const intensity = clampUnit(Number(split.intensity ?? 0));
if (intensity === 0) return color;
const balance = clampUnit(Number(split.balance ?? 0.5));
const y = luma(color);
const shadowMask = 1 - smoothstep(balance - 0.25, balance + 0.2, y);
const highlightMask = smoothstep(balance - 0.2, balance + 0.25, y);
const shadows = Array.isArray(split.shadows) ? split.shadows : [0, 0, 0];
const highlights = Array.isArray(split.highlights) ? split.highlights : [0, 0, 0];
return color.map((channel, i) =>
clampUnit(
channel +
Number(shadows[i] ?? 0) * shadowMask * intensity +
Number(highlights[i] ?? 0) * highlightMask * intensity,
),
);
}
function applySaturation(color, params) {
const saturation = readParam(params, "saturation", -1, 1);
const vibrance = readParam(params, "vibrance", -1, 1);
if (saturation === 0 && vibrance === 0) return color;
const y = luma(color);
const currentSat = Math.max(
Math.abs(color[0] - y),
Math.abs(color[1] - y),
Math.abs(color[2] - y),
);
const vibranceWeight = 1 - clampUnit(currentSat * 2);
const factor = clamp(1 + saturation + vibrance * vibranceWeight, 0, 2.5);
return color.map((channel) => clampUnit(y + (channel - y) * factor));
}
function applyParams(color, params) {
let out = applyLiftGain(color, params);
out = applyExposure(out, params);
out = applyContrast(out, params);
out = applyWhiteBalance(out, params);
out = applySplitTone(out, params);
out = applySaturation(out, params);
return out;
}
function formatNumber(value) {
return clampUnit(value).toFixed(6);
}
export function buildCube(params = {}, size = DEFAULT_SIZE) {
if (!Number.isInteger(size) || size < 2 || size > MAX_SIZE) {
throw new Error(`LUT size must be an integer from 2 to ${MAX_SIZE}`);
}
const lines = [
`TITLE "media-use parametric grade"`,
"DOMAIN_MIN 0 0 0",
"DOMAIN_MAX 1 1 1",
`LUT_3D_SIZE ${size}`,
];
const denom = size - 1;
for (let b = 0; b < size; b++) {
for (let g = 0; g < size; g++) {
for (let r = 0; r < size; r++) {
const out = applyParams([r / denom, g / denom, b / denom], params);
lines.push(`${formatNumber(out[0])} ${formatNumber(out[1])} ${formatNumber(out[2])}`);
}
}
}
return `${lines.join("\n")}\n`;
}
export function paramsFromIntent(intent) {
const text = String(intent ?? "").toLowerCase();
const params = {};
let matched = false;
if (/\b(warm|golden|sunlit|sunny)\b/.test(text)) {
params.temperature = 0.18;
matched = true;
} else if (/\b(cool|blue|icy|crisp)\b/.test(text)) {
params.temperature = -0.16;
matched = true;
}
if (/\b(cinematic|film|movie)\b/.test(text)) {
params.contrast = 0.08;
params.saturation = 0.04;
matched = true;
}
if (/\b(punchy|contrast|dramatic|bold)\b/.test(text)) {
params.contrast = Math.max(params.contrast ?? 0, 0.22);
matched = true;
}
if (/\b(bright|airy|lift)\b/.test(text)) {
params.exposure = 0.16;
params.shadows = 0.08;
matched = true;
}
if (/\b(dark|moody|low-key)\b/.test(text)) {
params.exposure = -0.12;
params.contrast = Math.max(params.contrast ?? 0, 0.12);
matched = true;
}
if (/\b(vibrant|saturated|colorful)\b/.test(text)) {
params.saturation = Math.max(params.saturation ?? 0, 0.16);
params.vibrance = 0.12;
matched = true;
}
if (/\b(muted|desaturated|washed)\b/.test(text)) {
params.saturation = Math.min(params.saturation ?? 0, -0.16);
matched = true;
}
return matched ? params : null;
}