mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 23:12:22 +02:00
Snapshot current maintainer-preview application changes and regression fixtures for integration into lab. Excludes local runtime data, evaluation outputs and source backups. Focused Python regression selection: 140 passed; full suite not certified.
115 lines
6.8 KiB
JavaScript
115 lines
6.8 KiB
JavaScript
/** Layer-style definitions and canvas rendering shared with the effects worker. */
|
|
const color = (key, label, value) => ({ key, label, type: 'color', value });
|
|
const number = (key, label, value, min, max, suffix = 'px') => ({ key, label, value, min, max, step: 1, suffix });
|
|
const opacity = () => number('opacity', 'Opacity', 65, 0, 100, '%');
|
|
const size = () => number('size', 'Size', 8, 0, 100);
|
|
const angle = () => number('angle', 'Angle', 120, -180, 180, 'deg');
|
|
export const LAYER_STYLES = {
|
|
'bevel-emboss': { label: 'Bevel & Emboss', controls: [size(), angle(), number('depth', 'Depth', 100, 0, 300, '%'), color('highlight', 'Highlight', '#ffffff'), color('color', 'Shadow', '#000000'), opacity()] },
|
|
'inner-shadow': { label: 'Inner Shadow', controls: [color('color', 'Color', '#000000'), opacity(), size(), angle(), number('distance', 'Distance', 6, 0, 100)] },
|
|
'inner-glow': { label: 'Inner Glow', controls: [color('color', 'Color', '#ffffff'), opacity(), size()] },
|
|
satin: { label: 'Satin', controls: [color('color', 'Color', '#000000'), opacity(), size(), angle(), number('distance', 'Distance', 12, 0, 100)] },
|
|
'gradient-overlay': { label: 'Gradient Overlay', controls: [color('color', 'Start', '#e06c75'), color('endColor', 'End', '#ffffff'), opacity(), angle(), number('scale', 'Scale', 100, 10, 300, '%')] },
|
|
'pattern-overlay': { label: 'Pattern Overlay', controls: [color('color', 'Color', '#ffffff'), color('endColor', 'Background', '#222222'), opacity(), number('scale', 'Tile size', 16, 2, 128)] },
|
|
'outer-glow': { label: 'Outer Glow', controls: [color('color', 'Color', '#ffffff'), opacity(), size()] },
|
|
};
|
|
|
|
export function styleParams(type, values = {}) {
|
|
return Object.fromEntries(LAYER_STYLES[type].controls.map(control => {
|
|
const raw = values[control.key] ?? control.value;
|
|
const value = control.type === 'color'
|
|
? (/^#[0-9a-f]{6}$/i.test(raw) ? raw : control.value)
|
|
: Math.max(control.min, Math.min(control.max, Number.isFinite(Number(raw)) ? Number(raw) : control.value));
|
|
return [control.key, value];
|
|
}));
|
|
}
|
|
|
|
function surface(width, height) {
|
|
if (typeof document === 'undefined') return new OffscreenCanvas(width, height);
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width; canvas.height = height;
|
|
return canvas;
|
|
}
|
|
|
|
export function renderDropShadow(source, params) {
|
|
// Tint alpha first so RGB content cannot leak into the shadow. Draw only
|
|
// this silhouette; compositing the source again thickens translucent edges.
|
|
const silhouette = surface(source.width, source.height);
|
|
const sc = silhouette.getContext('2d');
|
|
sc.drawImage(source, 0, 0);
|
|
sc.globalCompositeOperation = 'source-in';
|
|
sc.fillStyle = params.color;
|
|
sc.fillRect(0, 0, source.width, source.height);
|
|
const shadow = surface(source.width, source.height), ctx = shadow.getContext('2d');
|
|
ctx.globalAlpha = params.opacity;
|
|
ctx.filter = `blur(${params.blur / 2}px)`;
|
|
ctx.drawImage(silhouette, params.x, params.y);
|
|
return shadow;
|
|
}
|
|
|
|
// Produce only the effect's contribution; the caller composites it over source.
|
|
export function renderLayerStyle(source, type, values) {
|
|
const p = styleParams(type, values), w = source.width, h = source.height;
|
|
const out = surface(w, h), ctx = out.getContext('2d');
|
|
const rad = (p.angle || 0) * Math.PI / 180;
|
|
const dx = Math.cos(rad) * (p.distance || 0), dy = -Math.sin(rad) * (p.distance || 0);
|
|
const clip = () => {
|
|
ctx.globalCompositeOperation = 'destination-in';
|
|
ctx.drawImage(source, 0, 0);
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
};
|
|
if (type === 'gradient-overlay' || type === 'pattern-overlay') {
|
|
if (type === 'gradient-overlay') {
|
|
const length = (Math.abs(Math.cos(rad)) * w + Math.abs(Math.sin(rad)) * h) * p.scale / 200;
|
|
const gradient = ctx.createLinearGradient(w / 2 - Math.cos(rad) * length, h / 2 + Math.sin(rad) * length, w / 2 + Math.cos(rad) * length, h / 2 - Math.sin(rad) * length);
|
|
gradient.addColorStop(0, p.color); gradient.addColorStop(1, p.endColor);
|
|
ctx.fillStyle = gradient;
|
|
} else {
|
|
const tile = surface(p.scale * 2, p.scale * 2), tc = tile.getContext('2d');
|
|
tc.fillStyle = p.endColor; tc.fillRect(0, 0, tile.width, tile.height);
|
|
tc.fillStyle = p.color; tc.fillRect(0, 0, p.scale, p.scale); tc.fillRect(p.scale, p.scale, p.scale, p.scale);
|
|
ctx.fillStyle = ctx.createPattern(tile, 'repeat');
|
|
}
|
|
ctx.fillRect(0, 0, w, h); clip();
|
|
} else if (type === 'outer-glow') {
|
|
ctx.filter = `blur(${p.size}px)`;
|
|
ctx.drawImage(source, 0, 0); ctx.filter = 'none';
|
|
ctx.globalCompositeOperation = 'source-in'; ctx.fillStyle = p.color; ctx.fillRect(0, 0, w, h);
|
|
ctx.globalCompositeOperation = 'destination-out'; ctx.drawImage(source, 0, 0);
|
|
} else if (type === 'inner-shadow' || type === 'inner-glow') {
|
|
// Pad the inverse alpha so edges touching the canvas still cast inward.
|
|
const pad = Math.ceil(p.size * 3 + (p.distance || 0) + 2);
|
|
const inverse = surface(w + pad * 2, h + pad * 2), ic = inverse.getContext('2d');
|
|
ic.fillStyle = p.color; ic.fillRect(0, 0, inverse.width, inverse.height);
|
|
ic.globalCompositeOperation = 'destination-out'; ic.drawImage(source, pad, pad);
|
|
ctx.filter = `blur(${p.size}px)`;
|
|
ctx.drawImage(inverse, -pad + dx, -pad + dy); ctx.filter = 'none'; clip();
|
|
} else if (type === 'satin') {
|
|
const a = surface(w, h), ac = a.getContext('2d');
|
|
ac.filter = `blur(${p.size}px)`; ac.drawImage(source, dx, dy);
|
|
ac.globalCompositeOperation = 'xor'; ac.drawImage(source, -dx, -dy); ac.filter = 'none';
|
|
ctx.drawImage(a, 0, 0);
|
|
ctx.globalCompositeOperation = 'source-in'; ctx.fillStyle = p.color; ctx.fillRect(0, 0, w, h); clip();
|
|
} else if (type === 'bevel-emboss') {
|
|
const blurred = surface(w, h), bc = blurred.getContext('2d');
|
|
bc.filter = `blur(${Math.max(0.5, p.size / 2)}px)`; bc.drawImage(source, 0, 0);
|
|
const alpha = bc.getImageData(0, 0, w, h).data;
|
|
const original = source.getContext('2d').getImageData(0, 0, w, h).data;
|
|
const pixels = ctx.createImageData(w, h), step = Math.max(1, Math.round(p.size / 2));
|
|
const sample = (x, y) => x < 0 || x >= w || y < 0 || y >= h ? 0 : alpha[(y * w + x) * 4 + 3] / 255;
|
|
const rgb = hex => [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
|
|
const light = rgb(p.highlight), dark = rgb(p.color);
|
|
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
|
|
const at = (y * w + x) * 4;
|
|
const slope = ((sample(x + step, y) - sample(x - step, y)) * Math.cos(rad)
|
|
- (sample(x, y + step) - sample(x, y - step)) * Math.sin(rad)) * p.depth / 100;
|
|
pixels.data.set(slope > 0 ? light : dark, at);
|
|
pixels.data[at + 3] = Math.min(1, Math.abs(slope)) * original[at + 3];
|
|
}
|
|
ctx.putImageData(pixels, 0, 0);
|
|
}
|
|
const faded = surface(w, h), fc = faded.getContext('2d');
|
|
fc.globalAlpha = p.opacity / 100; fc.drawImage(out, 0, 0);
|
|
return faded;
|
|
}
|