PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/fx/pixel-pass.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:00:44 UTC 9,281 bytes, 249 lines download raw bytes

1/**
2 * Apply a Brightness/Contrast, Hue/Saturation, Levels, or Color Balance
3 * adjustment to a source canvas and return a fresh canvas with the
4 * result. Pure pixel math — no DOM, no module state.
5 *
6 * Used by the editor's per-layer FX stack: each `adjLayer` calls
7 * `applyAdjustment(prevCanvas, adjLayer)` and the result feeds the
8 * next layer in the stack.
9 *
10 * Adjustment shape:
11 *   { type: 'brightness-contrast', params: { brightness, contrast } }
12 *   { type: 'hue-saturation',      params: { hue, saturation } }
13 *   { type: 'levels',              params: { inBlack, inWhite, gamma, outBlack, outWhite } }
14 *   { type: 'color-balance',       params: { shadows, midtones, highlights } }
15 */
16export function applyAdjustment(srcCanvas, adj) {
17  const w = srcCanvas.width, h = srcCanvas.height;
18  const out = document.createElement('canvas');
19  out.width = w; out.height = h;
20  const octx = out.getContext('2d');
21
22  // B/C and H/S can use the fast browser-native CSS filter pipeline.
23  if (adj.type === 'brightness-contrast') {
24    const p = adj.params;
25    octx.filter = `brightness(${p.brightness}) contrast(${p.contrast})`;
26    octx.drawImage(srcCanvas, 0, 0);
27    octx.filter = 'none';
28    return out;
29  }
30  if (adj.type === 'hue-saturation') {
31    const p = adj.params;
32    octx.filter = `saturate(${p.saturation}) hue-rotate(${p.hue}deg)`;
33    octx.drawImage(srcCanvas, 0, 0);
34    octx.filter = 'none';
35    return out;
36  }
37
38  // Levels + Color Balance need per-pixel math.
39  octx.drawImage(srcCanvas, 0, 0);
40  const img = octx.getImageData(0, 0, w, h);
41  const d = img.data;
42
43  if (adj.type === 'levels') {
44    const l = adj.params;
45    const inLow  = Math.max(0, Math.min(254, l.inBlack));
46    const inHigh = Math.max(inLow + 1, Math.min(255, l.inWhite));
47    const gamma  = Math.max(0.1, l.gamma || 1);
48    const outLow  = Math.max(0, Math.min(255, l.outBlack));
49    const outHigh = Math.max(outLow, Math.min(255, l.outWhite));
50    const inv = 1.0 / gamma;
51    const span = (outHigh - outLow);
52    const lut = new Uint8ClampedArray(256);
53    for (let v = 0; v < 256; v++) {
54      let t = (v - inLow) / (inHigh - inLow);
55      if (t < 0) t = 0; else if (t > 1) t = 1;
56      t = Math.pow(t, inv);
57      lut[v] = Math.round(t * span + outLow);
58    }
59    for (let i = 0; i < d.length; i += 4) {
60      d[i] = lut[d[i]]; d[i+1] = lut[d[i+1]]; d[i+2] = lut[d[i+2]];
61    }
62    octx.putImageData(img, 0, 0);
63    return out;
64  }
65
66  if (adj.type === 'color-balance') {
67    const cb = adj.params;
68    const scale = 0.6;
69    const s = cb.shadows, m = cb.midtones, hi = cb.highlights;
70    const sR = s.r*scale, sG = s.g*scale, sB = s.b*scale;
71    const mR = m.r*scale, mG = m.g*scale, mB = m.b*scale;
72    const hR = hi.r*scale, hG = hi.g*scale, hB = hi.b*scale;
73    // Bell-curve tone weights so each pixel's shift is proportional to
74    // how "shadow", "midtone", or "highlight" its luminance is.
75    const wS = new Float32Array(256), wM = new Float32Array(256), wH = new Float32Array(256);
76    const sig = 0.25;
77    for (let v = 0; v < 256; v++) {
78      const t = v / 255;
79      wS[v] = Math.exp(-(t*t) / (2*sig*sig));
80      wM[v] = Math.exp(-((t-0.5)*(t-0.5)) / (2*sig*sig));
81      wH[v] = Math.exp(-((1-t)*(1-t)) / (2*sig*sig));
82    }
83    for (let i = 0; i < d.length; i += 4) {
84      let r = d[i], g = d[i+1], b = d[i+2];
85      const Y = (0.2126*r + 0.7152*g + 0.0722*b) | 0;
86      const ws = wS[Y], wm = wM[Y], wh = wH[Y];
87      r += sR*ws + mR*wm + hR*wh;
88      g += sG*ws + mG*wm + hG*wh;
89      b += sB*ws + mB*wm + hB*wh;
90      d[i]   = r < 0 ? 0 : r > 255 ? 255 : r;
91      d[i+1] = g < 0 ? 0 : g > 255 ? 255 : g;
92      d[i+2] = b < 0 ? 0 : b > 255 ? 255 : b;
93    }
94    octx.putImageData(img, 0, 0);
95    return out;
96  }
97
98  return out;
99}
100
101
102/**
103 * Apply a combined Levels + Color Balance pass to a layer in-place via
104 * its `layer.adjustments` field. Cached on `layer._adjCache` keyed by
105 * `cacheKey` so repeated composite passes don't re-run the math.
106 *
107 * Returns the cached output canvas.
108 *
109 * @param {{
110 *   canvas: HTMLCanvasElement,
111 *   adjustments: object,
112 *   _adjCache?: HTMLCanvasElement,
113 *   _adjCacheKey?: string,
114 * }} layer
115 * @param {string} cacheKey  Stable signature of `layer.adjustments`.
116 */
117export function renderLayerPixelAdjustments(layer, cacheKey) {
118  const adj = layer.adjustments;
119  if (layer._adjCache && layer._adjCacheKey === cacheKey) return layer._adjCache;
120  if (!layer._adjCache) {
121    layer._adjCache = document.createElement('canvas');
122  }
123  const out = layer._adjCache;
124  out.width = layer.canvas.width;
125  out.height = layer.canvas.height;
126  const octx = out.getContext('2d');
127  octx.clearRect(0, 0, out.width, out.height);
128  octx.drawImage(layer.canvas, 0, 0);
129  const img = octx.getImageData(0, 0, out.width, out.height);
130  const d = img.data;
131
132  // Single 256-entry LUT for the Levels portion (applied per R/G/B
133  // channel identically — luma-style isn't right when colour balance
134  // follows, per-channel is fine here).
135  const l = adj.levels || { inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 };
136  const inLow  = Math.max(0, Math.min(254, l.inBlack));
137  const inHigh = Math.max(inLow + 1, Math.min(255, l.inWhite));
138  const gamma  = Math.max(0.1, l.gamma || 1);
139  const outLow  = Math.max(0, Math.min(255, l.outBlack));
140  const outHigh = Math.max(outLow, Math.min(255, l.outWhite));
141  const inv = 1.0 / gamma;
142  const span = (outHigh - outLow);
143  const lut = new Uint8ClampedArray(256);
144  for (let v = 0; v < 256; v++) {
145    let t = (v - inLow) / (inHigh - inLow);
146    if (t < 0) t = 0; else if (t > 1) t = 1;
147    t = Math.pow(t, inv);
148    lut[v] = Math.round(t * span + outLow);
149  }
150
151  // Color Balance bell-curve weights (see applyAdjustment).
152  const cb = adj.colorBalance || { shadows: {r:0,g:0,b:0}, midtones: {r:0,g:0,b:0}, highlights: {r:0,g:0,b:0} };
153  const s = cb.shadows || {r:0,g:0,b:0};
154  const m = cb.midtones || {r:0,g:0,b:0};
155  const h = cb.highlights || {r:0,g:0,b:0};
156  const scale = 0.6;
157  const sR = s.r * scale, sG = s.g * scale, sB = s.b * scale;
158  const mR = m.r * scale, mG = m.g * scale, mB = m.b * scale;
159  const hR = h.r * scale, hG = h.g * scale, hB = h.b * scale;
160
161  const wS = new Float32Array(256);
162  const wM = new Float32Array(256);
163  const wH = new Float32Array(256);
164  for (let v = 0; v < 256; v++) {
165    const t = v / 255;
166    const dS = t, wsig = 0.25;
167    const dM = t - 0.5;
168    const dH = 1 - t;
169    wS[v] = Math.exp(-(dS * dS) / (2 * wsig * wsig));
170    wM[v] = Math.exp(-(dM * dM) / (2 * wsig * wsig));
171    wH[v] = Math.exp(-(dH * dH) / (2 * wsig * wsig));
172  }
173
174  for (let i = 0; i < d.length; i += 4) {
175    let r = lut[d[i]];
176    let g = lut[d[i + 1]];
177    let b = lut[d[i + 2]];
178    const Y = (0.2126 * r + 0.7152 * g + 0.0722 * b) | 0;
179    const ws = wS[Y], wm = wM[Y], wh = wH[Y];
180    r += sR * ws + mR * wm + hR * wh;
181    g += sG * ws + mG * wm + hG * wh;
182    b += sB * ws + mB * wm + hB * wh;
183    d[i]     = r < 0 ? 0 : r > 255 ? 255 : r;
184    d[i + 1] = g < 0 ? 0 : g > 255 ? 255 : g;
185    d[i + 2] = b < 0 ? 0 : b > 255 ? 255 : b;
186  }
187  octx.putImageData(img, 0, 0);
188  layer._adjCacheKey = cacheKey;
189  return out;
190}
191
192
193/**
194 * Walk the layer's `adjLayers` stack (skipping the one currently being
195 * edited, if any) plus an optional staged preview adjustment, producing
196 * a final canvas the composite step can paint. The result is memoised
197 * on `layer._adjFinal` keyed by a signature of all adjLayer params +
198 * staged + editing id, so repeated composite passes are O(1) when
199 * nothing has changed.
200 *
201 * If the stack is empty AND nothing is staged, returns the layer's own
202 * canvas unchanged (no allocation).
203 *
204 * @param {{
205 *   canvas: HTMLCanvasElement,
206 *   adjLayers?: Array<{id: string, type: string, params: object, visible: boolean, opacity: number}>,
207 *   _stagedAdj?: {type: string, params: object} | null,
208 *   _editingAdjId?: string | null,
209 *   _adjFinal?: HTMLCanvasElement,
210 *   _adjFinalKey?: string,
211 * }} layer
212 * @returns {HTMLCanvasElement}
213 */
214export function renderLayerWithAdjLayers(layer) {
215  const editingId = layer._editingAdjId || null;
216  const stack = (layer.adjLayers || []).filter(a => a.visible && a.id !== editingId);
217  const staged = layer._stagedAdj;
218  if (stack.length === 0 && !staged) {
219    layer._adjFinalKey = '';
220    return layer.canvas;
221  }
222  const sig = stack.map(a => `${a.id}:${a.visible?1:0}:${a.opacity}:${a.type}:${JSON.stringify(a.params)}`).join('|') +
223    (staged ? `|S:${staged.type}:${JSON.stringify(staged.params)}` : '') +
224    (editingId ? `|E:${editingId}` : '');
225  if (layer._adjFinal && layer._adjFinalKey === sig) return layer._adjFinal;
226  let cur = layer.canvas;
227  const w = layer.canvas.width, h = layer.canvas.height;
228  for (const adj of stack) {
229    const adjOut = applyAdjustment(cur, adj);
230    if (adj.opacity >= 0.999) {
231      cur = adjOut;
232    } else {
233      const blend = document.createElement('canvas');
234      blend.width = w; blend.height = h;
235      const bctx = blend.getContext('2d');
236      bctx.drawImage(cur, 0, 0);
237      bctx.globalAlpha = adj.opacity;
238      bctx.drawImage(adjOut, 0, 0);
239      bctx.globalAlpha = 1;
240      cur = blend;
241    }
242  }
243  if (staged) {
244    cur = applyAdjustment(cur, staged);
245  }
246  layer._adjFinal = cur;
247  layer._adjFinalKey = sig;
248  return cur;
249}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.