132 lines
5.2 KiB
JavaScript
132 lines
5.2 KiB
JavaScript
/**
|
|
* 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': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 1 0 18Z" fill="currentColor" stroke="none"/></svg>',
|
|
'hue-saturation': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="12" r="4"/><circle cx="15" cy="9.5" r="4"/><circle cx="15" cy="14.5" r="4"/></svg>',
|
|
'levels': '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="14" width="3" height="6" rx="0.5"/><rect x="8" y="9" width="3" height="11" rx="0.5"/><rect x="13" y="11" width="3" height="9" rx="0.5"/><rect x="18" y="6" width="3" height="14" rx="0.5"/></svg>',
|
|
'color-balance': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12a9 9 0 0 1 9-9v18a9 9 0 0 1-9-9z" fill="currentColor" stroke="none"/></svg>',
|
|
};
|
|
|
|
|
|
/** SVG used in the topbar/history button glyphs. */
|
|
export const HISTORY_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/><polyline points="12 7 12 12 16 14"/></svg>';
|
|
|
|
|
|
/** 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';
|
|
}
|