/** * Pure helpers + constants for layers and adjustment sub-layers. * * Everything in this module is stateless — feed in a layer object and * get back a value. The legacy gallery editor's module-level helpers * re-export from here so existing call sites keep working unchanged. */ /** True if the layer has at least one FX/adjustment sub-layer. */ export function layerHasAdjustments(layer) { return !!(layer && layer.adjLayers && layer.adjLayers.length > 0); } /** * True if the layer carries a non-identity Levels OR Color-Balance * adjustment that needs the per-pixel pass (vs the cheap CSS-filter * path for plain B/C/H/S). */ export function layerNeedsPixelPass(layer) { if (!layer || !layer.adjustments) return false; const a = layer.adjustments; if (a.levels && (a.levels.inBlack !== 0 || a.levels.inWhite !== 255 || a.levels.gamma !== 1 || a.levels.outBlack !== 0 || a.levels.outWhite !== 255)) return true; if (a.colorBalance) { for (const tone of ['shadows', 'midtones', 'highlights']) { const v = a.colorBalance[tone]; if (v && (v.r || v.g || v.b)) return true; } } return false; } /** * Compact hash of a layer's Levels + Color-Balance values. Used to * key the per-pixel adjustment cache so we can skip recomputing when * nothing changed. */ export function adjustmentsKey(adj) { const l = adj.levels || {}; const cb = adj.colorBalance || {}; const s = cb.shadows || {}, m = cb.midtones || {}, h = cb.highlights || {}; return [ l.inBlack|0, l.inWhite|0, l.gamma || 1, l.outBlack|0, l.outWhite|0, s.r|0, s.g|0, s.b|0, m.r|0, m.g|0, m.b|0, h.r|0, h.g|0, h.b|0, ].join('|'); } /** Identity params for each adjustment type. */ export function defaultAdjParams(type) { switch (type) { case 'brightness-contrast': return { brightness: 1, contrast: 1 }; case 'hue-saturation': return { hue: 0, saturation: 1 }; case 'levels': return { inBlack: 0, inWhite: 255, gamma: 1.0, outBlack: 0, outWhite: 255 }; case 'color-balance': return { shadows: { r: 0, g: 0, b: 0 }, midtones: { r: 0, g: 0, b: 0 }, highlights: { r: 0, g: 0, b: 0 }, }; } return {}; } /** Human-readable name for an adjustment type. */ export function adjLayerLabel(type) { return { 'brightness-contrast': 'Brightness/Contrast', 'hue-saturation': 'Hue/Saturation', 'levels': 'Levels', 'color-balance': 'Color Balance', }[type] || type; } /** * Per-type SVG icon strings. Used in popup title bars, the minimised * FX-dock chips, and the layer-panel sub-row name so the same glyph * shows up everywhere a given adjustment type appears. */ export const ADJ_ICONS = { 'brightness-contrast': '', 'hue-saturation': '', 'levels': '', 'color-balance': '', }; /** SVG used in the topbar/history button glyphs. */ export const HISTORY_ICON = ''; /** Quick downsampled-alpha check: are there any opaque pixels on this canvas? */ export function isMaskCanvasEmpty(canvas) { if (!canvas) return true; try { const w = canvas.width, h = canvas.height; if (!w || !h) return true; const sw = Math.min(200, w), sh = Math.min(200, h); const tmp = document.createElement('canvas'); tmp.width = sw; tmp.height = sh; tmp.getContext('2d').drawImage(canvas, 0, 0, sw, sh); const d = tmp.getContext('2d').getImageData(0, 0, sw, sh).data; for (let i = 3; i < d.length; i += 4) if (d[i] > 0) return false; return true; } catch { return false; } } /** Same as `isMaskCanvasEmpty` but accepts a layer wrapper. */ export function isLayerEmpty(layer) { if (!layer || !layer.canvas) return true; return isMaskCanvasEmpty(layer.canvas); } /** * Compact "now / 30s / 12m / 4h" relative-time string. Used in the * editor's history panel labels. */ export function relTime(ts) { if (!ts) return ''; const dt = (Date.now() - ts) / 1000; if (dt < 5) return 'now'; if (dt < 60) return Math.round(dt) + 's'; if (dt < 3600) return Math.round(dt / 60) + 'm'; return Math.round(dt / 3600) + 'h'; }