Files
odysseus/static/js/editor/tools/gradient.js
T

120 lines
4.1 KiB
JavaScript

/**
* Local two-color gradient tool.
*
* The drag becomes a retained effect on completion, so previews never mutate
* source pixels or compound across pointer-move events.
*/
import { state } from '../state.js';
import { canvasCoords } from '../canvas-coords.js';
import { isLayerPixelLocked } from '../layer-groups.js';
export function createGradientTool({ activeLayer, saveState, composite, schedulePersist, renderLayerPanel, getSettings }) {
let start = null;
let lastPoint = null;
let activeLayerId = null;
let historySaved = false;
function settings() {
const values = getSettings?.() || {};
return {
start: values.start || state.gradientStart || state.color,
mid: values.mid || state.gradientMid || '#808080',
midPosition: values.midPosition ?? state.gradientMidPosition ?? 50,
midEnabled: values.midEnabled ?? state.gradientMidEnabled ?? false,
extraStops: Array.isArray(values.extraStops) ? values.extraStops : (state.gradientStops || []),
end: values.end || state.gradientEnd || '#ffffff',
endAlpha: values.endAlpha ?? state.gradientEndAlpha ?? 100,
opacity: values.opacity ?? state.gradientOpacity ?? 100,
type: values.type || state.gradientType || 'linear-gradient',
};
}
function preview(point) {
const layer = activeLayer();
if (!start || !layer || layer.id !== activeLayerId) return false;
const dx = point.x - start.x;
const dy = point.y - start.y;
if (Math.hypot(dx, dy) < 0.5) return false;
const values = settings();
const stops = [
{ position: 0, color: values.start, alpha: 1 },
...(values.midEnabled ? [{ position: Number(values.midPosition), color: values.mid, alpha: 1 }] : []),
...values.extraStops.slice(0, 10).map(stop => ({
position: Math.max(1, Math.min(99, Number(stop.position) || 50)),
color: stop.color || '#808080',
alpha: 1,
})),
{ position: 100, color: values.end, alpha: Math.max(0, Math.min(100, Number(values.endAlpha))) / 100 },
].sort((a, b) => a.position - b.position);
layer._effectPreview = {
id: 'gradient-preview', type: values.type, name: values.type === 'radial-gradient' ? 'Radial Gradient' : 'Gradient', visible: true, opacity: 1,
params: { x1: start.x, y1: start.y, x2: point.x, y2: point.y, stops, opacity: Math.max(0, Math.min(100, Number(values.opacity))) / 100 },
};
composite();
return true;
}
return {
begin(e) {
const layer = activeLayer();
if (!layer) return false;
if (isLayerPixelLocked(state, layer)) return false;
if (layer.kind === 'placed') return false;
activeLayerId = layer.id;
start = canvasCoords(e, state.mainCanvas);
lastPoint = { ...start };
state.gradientActive = true;
historySaved = false;
return true;
},
drag(e) {
if (!state.gradientActive) return false;
lastPoint = canvasCoords(e, state.mainCanvas);
preview(lastPoint);
return true;
},
end(e) {
if (!state.gradientActive) return false;
const point = e && (Number.isFinite(e.clientX) || (e.touches && e.touches.length))
? canvasCoords(e, state.mainCanvas)
: lastPoint;
const changed = point ? preview(point) : false;
const layer = activeLayer();
state.gradientActive = false;
if (layer?._effectPreview && changed) {
if (!historySaved) {
saveState('Gradient');
historySaved = true;
}
layer.effects = Array.isArray(layer.effects) ? layer.effects : [];
layer.effects.push(layer._effectPreview);
}
if (layer) layer._effectPreview = null;
start = null;
lastPoint = null;
activeLayerId = null;
historySaved = false;
if (changed) {
renderLayerPanel?.();
schedulePersist?.();
}
return true;
},
cancel() {
if (!state.gradientActive) return false;
const layer = activeLayer();
if (layer) layer._effectPreview = null;
state.gradientActive = false;
start = null;
lastPoint = null;
activeLayerId = null;
historySaved = false;
composite();
return true;
},
};
}