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.