Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent e5c99a5eee
commit 6ee6502010
2050 changed files with 538359 additions and 57745 deletions
+263 -29
View File
@@ -1,5 +1,5 @@
/**
* Apply a Brightness/Contrast, Hue/Saturation, Levels, or Color Balance
* Apply a Brightness/Contrast, Black & White, Hue/Saturation, Levels, Curves, or Color Balance
* adjustment to a source canvas and return a fresh canvas with the
* result. Pure pixel math — no DOM, no module state.
*
@@ -11,8 +11,111 @@
* { type: 'brightness-contrast', params: { brightness, contrast } }
* { type: 'hue-saturation', params: { hue, saturation } }
* { type: 'levels', params: { inBlack, inWhite, gamma, outBlack, outWhite } }
* { type: 'curves', params: { points: {rgb, red, green, blue} } }
* { type: 'color-balance', params: { shadows, midtones, highlights } }
*/
function clampByte(value) {
return value < 0 ? 0 : value > 255 ? 255 : Math.round(value);
}
function finiteOr(value, fallback) {
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
}
function levelLut(values = {}) {
const inLow = Math.max(0, Math.min(254, finiteOr(values.inBlack, 0)));
const inHigh = Math.max(inLow + 1, Math.min(255, finiteOr(values.inWhite, 255)));
const gamma = Math.max(0.1, finiteOr(values.gamma, 1));
const outLow = Math.max(0, Math.min(255, finiteOr(values.outBlack, 0)));
const outHigh = Math.max(outLow, Math.min(255, finiteOr(values.outWhite, 255)));
const lut = new Uint8ClampedArray(256);
for (let value = 0; value < 256; value += 1) {
const normalized = Math.max(0, Math.min(1, (value - inLow) / (inHigh - inLow)));
lut[value] = clampByte(Math.pow(normalized, 1 / gamma) * (outHigh - outLow) + outLow);
}
return lut;
}
function normalizedCurvePoints(points) {
const clean = (Array.isArray(points) ? points : [])
.filter(point => Array.isArray(point) && point.length >= 2)
.map(point => [clampByte(Number(point[0]) || 0), clampByte(Number(point[1]) || 0)])
.sort((a, b) => a[0] - b[0]);
if (!clean.length || clean[0][0] !== 0) clean.unshift([0, clean[0]?.[1] ?? 0]);
if (clean[clean.length - 1][0] !== 255) clean.push([255, clean[clean.length - 1]?.[1] ?? 255]);
const unique = [];
for (const point of clean) {
if (unique.length && unique[unique.length - 1][0] === point[0]) unique[unique.length - 1] = point;
else unique.push(point);
}
return unique;
}
export function curveLut(points) {
const clean = normalizedCurvePoints(points);
const lut = new Uint8ClampedArray(256);
let segment = 0;
for (let value = 0; value < 256; value += 1) {
while (segment < clean.length - 2 && value > clean[segment + 1][0]) segment += 1;
const left = clean[segment];
const right = clean[Math.min(segment + 1, clean.length - 1)];
const span = Math.max(1, right[0] - left[0]);
const amount = Math.max(0, Math.min(1, (value - left[0]) / span));
lut[value] = clampByte(left[1] + (right[1] - left[1]) * amount);
}
return lut;
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const lightness = (max + min) / 2;
if (max === min) return [0, 0, lightness];
const delta = max - min;
const saturation = lightness > .5 ? delta / (2 - max - min) : delta / (max + min);
let hue;
if (max === r) hue = ((g - b) / delta + (g < b ? 6 : 0)) / 6;
else if (max === g) hue = ((b - r) / delta + 2) / 6;
else hue = ((r - g) / delta + 4) / 6;
return [hue, saturation, lightness];
}
function hueChannel(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
function hslToRgb(h, s, l) {
if (!s) return [l * 255, l * 255, l * 255];
const q = l < .5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
return [hueChannel(p, q, h + 1 / 3) * 255, hueChannel(p, q, h) * 255, hueChannel(p, q, h - 1 / 3) * 255];
}
function parseHexColor(value, fallback) {
const match = /^#?([0-9a-f]{6})$/i.exec(String(value || ''));
const hex = match ? match[1] : fallback;
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
}
function hueDistance(a, b) {
const distance = Math.abs(a - b);
return Math.min(distance, 1 - distance);
}
function selectiveRangeWeight(name, hue, saturation, lightness) {
if (name === 'blacks') return Math.max(0, Math.min(1, (0.55 - lightness) / 0.45));
if (name === 'neutrals') return Math.max(0, Math.min(1, (1 - saturation) * (1 - Math.abs(lightness - .5) * 1.5)));
const centers = { reds: 0, yellows: 1 / 6, greens: 1 / 3, cyans: .5, blues: 2 / 3, magentas: 5 / 6 };
const distance = hueDistance(hue, centers[name] ?? 0);
return Math.max(0, 1 - distance * 6) * saturation;
}
export function applyAdjustment(srcCanvas, adj) {
const w = srcCanvas.width, h = srcCanvas.height;
const out = document.createElement('canvas');
@@ -27,37 +130,129 @@ export function applyAdjustment(srcCanvas, adj) {
octx.filter = 'none';
return out;
}
if (adj.type === 'hue-saturation') {
const p = adj.params;
octx.filter = `saturate(${p.saturation}) hue-rotate(${p.hue}deg)`;
octx.drawImage(srcCanvas, 0, 0);
octx.filter = 'none';
return out;
}
// Levels + Color Balance need per-pixel math.
// The remaining adjustments need deterministic per-pixel math so preview,
// flattening, and reopened documents produce the same result.
octx.drawImage(srcCanvas, 0, 0);
const img = octx.getImageData(0, 0, w, h);
const d = img.data;
if (adj.type === 'exposure') {
const p = adj.params || {};
const multiplier = Math.pow(2, finiteOr(p.exposure, 0));
const offset = finiteOr(p.offset, 0);
const gamma = Math.max(.1, finiteOr(p.gamma, 1));
for (let i = 0; i < d.length; i += 4) {
d[i] = clampByte(Math.pow(Math.max(0, Math.min(1, d[i] / 255 * multiplier + offset)), 1 / gamma) * 255);
d[i + 1] = clampByte(Math.pow(Math.max(0, Math.min(1, d[i + 1] / 255 * multiplier + offset)), 1 / gamma) * 255);
d[i + 2] = clampByte(Math.pow(Math.max(0, Math.min(1, d[i + 2] / 255 * multiplier + offset)), 1 / gamma) * 255);
}
octx.putImageData(img, 0, 0);
return out;
}
if (adj.type === 'white-balance') {
const p = adj.params || {};
const temperature = finiteOr(p.temperature, 0) / 100;
const tint = finiteOr(p.tint, 0) / 100;
for (let i = 0; i < d.length; i += 4) {
d[i] = clampByte(d[i] * (1 + temperature * .28 + tint * .09));
d[i + 1] = clampByte(d[i + 1] * (1 - tint * .18));
d[i + 2] = clampByte(d[i + 2] * (1 - temperature * .28 + tint * .09));
}
octx.putImageData(img, 0, 0);
return out;
}
if (adj.type === 'hue-saturation') {
const p = adj.params || {};
const hueShift = finiteOr(p.hue, 0) / 360;
const saturationScale = Math.max(0, finiteOr(p.saturation, 1));
const lightnessShift = finiteOr(p.lightness, 0) / 100;
for (let i = 0; i < d.length; i += 4) {
let [hue, saturation, lightness] = rgbToHsl(d[i], d[i + 1], d[i + 2]);
hue = (hue + hueShift + 1) % 1;
saturation = Math.max(0, Math.min(1, saturation * saturationScale));
lightness = Math.max(0, Math.min(1, lightness + lightnessShift));
const rgb = hslToRgb(hue, saturation, lightness);
d[i] = clampByte(rgb[0]); d[i + 1] = clampByte(rgb[1]); d[i + 2] = clampByte(rgb[2]);
}
octx.putImageData(img, 0, 0);
return out;
}
if (adj.type === 'vibrance') {
const amount = Math.max(-1, Math.min(1, finiteOr(adj.params?.vibrance, 0) / 100));
for (let i = 0; i < d.length; i += 4) {
const [hue, saturation, lightness] = rgbToHsl(d[i], d[i + 1], d[i + 2]);
const adjustedSaturation = amount >= 0
? saturation + (1 - saturation) * amount
: saturation * (1 + amount);
const rgb = hslToRgb(hue, Math.max(0, Math.min(1, adjustedSaturation)), lightness);
d[i] = clampByte(rgb[0]); d[i + 1] = clampByte(rgb[1]); d[i + 2] = clampByte(rgb[2]);
}
octx.putImageData(img, 0, 0);
return out;
}
if (adj.type === 'black-white') {
const p = adj.params || {};
const red = finiteOr(p.red, 30) / 100;
const green = finiteOr(p.green, 59) / 100;
const blue = finiteOr(p.blue, 11) / 100;
const constant = finiteOr(p.constant, 0) * 2.55;
for (let i = 0; i < d.length; i += 4) {
const gray = d[i] * red + d[i + 1] * green + d[i + 2] * blue + constant;
d[i] = clampByte(gray);
d[i + 1] = clampByte(gray);
d[i + 2] = clampByte(gray);
}
octx.putImageData(img, 0, 0);
return out;
}
if (adj.type === 'shadows-highlights') {
const shadows = Math.max(-1, Math.min(1, finiteOr(adj.params?.shadows, 0) / 100));
const highlights = Math.max(-1, Math.min(1, finiteOr(adj.params?.highlights, 0) / 100));
const toneAdjust = (value, amount, weight) => amount >= 0
? value + (255 - value) * amount * weight
: value + value * amount * weight;
for (let i = 0; i < d.length; i += 4) {
const luminance = (0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]) / 255;
const shadowWeight = (1 - luminance) ** 2;
const highlightWeight = luminance ** 2;
d[i] = clampByte(toneAdjust(toneAdjust(d[i], shadows, shadowWeight), highlights, highlightWeight));
d[i + 1] = clampByte(toneAdjust(toneAdjust(d[i + 1], shadows, shadowWeight), highlights, highlightWeight));
d[i + 2] = clampByte(toneAdjust(toneAdjust(d[i + 2], shadows, shadowWeight), highlights, highlightWeight));
}
octx.putImageData(img, 0, 0);
return out;
}
if (adj.type === 'levels') {
const l = adj.params;
const inLow = Math.max(0, Math.min(254, l.inBlack));
const inHigh = Math.max(inLow + 1, Math.min(255, l.inWhite));
const gamma = Math.max(0.1, l.gamma || 1);
const outLow = Math.max(0, Math.min(255, l.outBlack));
const outHigh = Math.max(outLow, Math.min(255, l.outWhite));
const inv = 1.0 / gamma;
const span = (outHigh - outLow);
const lut = new Uint8ClampedArray(256);
for (let v = 0; v < 256; v++) {
let t = (v - inLow) / (inHigh - inLow);
if (t < 0) t = 0; else if (t > 1) t = 1;
t = Math.pow(t, inv);
lut[v] = Math.round(t * span + outLow);
}
const master = levelLut(l);
const red = levelLut(l.channels?.red);
const green = levelLut(l.channels?.green);
const blue = levelLut(l.channels?.blue);
for (let i = 0; i < d.length; i += 4) {
d[i] = lut[d[i]]; d[i+1] = lut[d[i+1]]; d[i+2] = lut[d[i+2]];
d[i] = red[master[d[i]]];
d[i + 1] = green[master[d[i + 1]]];
d[i + 2] = blue[master[d[i + 2]]];
}
octx.putImageData(img, 0, 0);
return out;
}
if (adj.type === 'curves') {
const points = adj.params?.points || {};
const master = curveLut(points.rgb);
const red = curveLut(points.red);
const green = curveLut(points.green);
const blue = curveLut(points.blue);
for (let i = 0; i < d.length; i += 4) {
d[i] = red[master[d[i]]];
d[i + 1] = green[master[d[i + 1]]];
d[i + 2] = blue[master[d[i + 2]]];
}
octx.putImageData(img, 0, 0);
return out;
@@ -95,6 +290,43 @@ export function applyAdjustment(srcCanvas, adj) {
return out;
}
if (adj.type === 'selective-color') {
const ranges = adj.params?.ranges || {};
for (let i = 0; i < d.length; i += 4) {
let r = d[i], g = d[i + 1], b = d[i + 2];
const [hue, saturation, lightness] = rgbToHsl(r, g, b);
for (const [name, values] of Object.entries(ranges)) {
const weight = selectiveRangeWeight(name, hue, saturation, lightness) * .65;
if (weight <= 0) continue;
const black = finiteOr(values.black, 0) / 100;
r += (-finiteOr(values.cyan, 0) / 100 * 255 - black * r) * weight;
g += (-finiteOr(values.magenta, 0) / 100 * 255 - black * g) * weight;
b += (-finiteOr(values.yellow, 0) / 100 * 255 - black * b) * weight;
}
d[i] = clampByte(r); d[i + 1] = clampByte(g); d[i + 2] = clampByte(b);
}
octx.putImageData(img, 0, 0);
return out;
}
if (adj.type === 'gradient-map') {
const p = adj.params || {};
let shadows = parseHexColor(p.shadows, '000000');
let highlights = parseHexColor(p.highlights, 'ffffff');
if (p.reverse) [shadows, highlights] = [highlights, shadows];
const midpoint = Math.max(.01, Math.min(.99, finiteOr(p.midpoint, 50) / 100));
const exponent = Math.log(.5) / Math.log(midpoint);
for (let i = 0; i < d.length; i += 4) {
const luminance = (0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]) / 255;
const amount = Math.pow(luminance, exponent);
d[i] = clampByte(shadows[0] + (highlights[0] - shadows[0]) * amount);
d[i + 1] = clampByte(shadows[1] + (highlights[1] - shadows[1]) * amount);
d[i + 2] = clampByte(shadows[2] + (highlights[2] - shadows[2]) * amount);
}
octx.putImageData(img, 0, 0);
return out;
}
return out;
}
@@ -215,14 +447,16 @@ export function renderLayerWithAdjLayers(layer) {
const editingId = layer._editingAdjId || null;
const stack = (layer.adjLayers || []).filter(a => a.visible && a.id !== editingId);
const staged = layer._stagedAdj;
if (stack.length === 0 && !staged) {
if (stack.length === 0 && (!staged || layer._adjCompare)) {
layer._adjFinalKey = '';
return layer.canvas;
}
const sig = stack.map(a => `${a.id}:${a.visible?1:0}:${a.opacity}:${a.type}:${JSON.stringify(a.params)}`).join('|') +
(staged ? `|S:${staged.type}:${JSON.stringify(staged.params)}` : '') +
(editingId ? `|E:${editingId}` : '');
if (layer._adjFinal && layer._adjFinalKey === sig) return layer._adjFinal;
const compare = !!layer._adjCompare;
const fullSig = `${sig}|C:${compare ? 1 : 0}`;
if (layer._adjFinal && layer._adjFinalKey === fullSig) return layer._adjFinal;
let cur = layer.canvas;
const w = layer.canvas.width, h = layer.canvas.height;
for (const adj of stack) {
@@ -240,10 +474,10 @@ export function renderLayerWithAdjLayers(layer) {
cur = blend;
}
}
if (staged) {
if (staged && !compare) {
cur = applyAdjustment(cur, staged);
}
layer._adjFinal = cur;
layer._adjFinalKey = sig;
layer._adjFinalKey = fullSig;
return cur;
}