Files

337 lines
13 KiB
JavaScript

import { clampCompRotation, clampCompScale, getCompLayerOutputCorners, getCompSlots } from "../comp.js";
import {
cloneCompCorners,
compDragHandleToCorner,
ensureCompInputs,
getCompCursor,
removeSelectedCompLayer
} from "../nodes/comp.js";
import { getCanvasPointer, screenToWorld } from "../shared/geometry.js";
import { findWidget, resetNodeWidgetsToDefaults, setWidgetValue, widgetBoolean, widgetNumber } from "../shared/widgets.js";
function safeSetPointerCapture(el, pointerId) {
try {
el.setPointerCapture?.(pointerId);
} catch {
}
}
function safeReleasePointerCapture(el, pointerId) {
try {
el.releasePointerCapture?.(pointerId);
} catch {
}
}
function rotatePoint(x, y, angleRad) {
const cos = Math.cos(angleRad);
const sin = Math.sin(angleRad);
return { x: x * cos - y * sin, y: x * sin + y * cos };
}
function aspectRatioValue(value) {
switch (String(value || "custom").trim().toLowerCase()) {
case "1/1":
case "1:1":
return 1;
case "3/4":
case "3:4":
return 3 / 4;
case "4/3":
case "4:3":
return 4 / 3;
case "16/9":
case "16:9":
return 16 / 9;
case "9/16":
case "9:16":
return 9 / 16;
default:
return null;
}
}
function applyCompAspectRatio(node, ratioName) {
const ratio = aspectRatioValue(ratioName);
if (!ratio) return;
if (widgetBoolean(node, "use_first_layer_size", true) || widgetBoolean(node, "auto_layering", false)) return;
const width = Math.max(1, Math.round(widgetNumber(node, "width", 1024)));
setWidgetValue(findWidget(node, "height"), Math.max(1, Math.round(width / ratio)));
}
function attachInteractions(node, ctx) {
const st = node.__imageops_state;
if (!st?.canvas || st.compInteractiveHooked) return;
st.compInteractiveHooked = true;
let moveRafPending = false;
const canvas = st.canvas;
const listenerOptions = st._abortController?.signal ? { signal: st._abortController.signal } : void 0;
const worldPt = (event) => {
const raw = getCanvasPointer(canvas, event);
return screenToWorld(raw.x, raw.y, st.previewZoom ?? 1, st.previewPanX ?? 0, st.previewPanY ?? 0, canvas.width);
};
if (st.compAddButton) {
st.compAddButton.addEventListener("click", (event) => {
event.preventDefault();
ensureCompInputs(node, Math.max(1, getCompSlots(node).length + 1));
const layers = ctx.ensureCompState(node);
st.compSelectedSlot = layers[layers.length - 1]?.slot ?? st.compSelectedSlot;
ctx.updateCompControls(node);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
ctx.schedule(node, () => {
ctx.startLoopIfVideo(node);
ctx.refreshDependents(node);
}, 0);
}, listenerOptions);
}
if (st.compRemoveButton) {
st.compRemoveButton.addEventListener("click", (event) => {
event.preventDefault();
if (!removeSelectedCompLayer(node)) return;
ctx.updateCompControls(node);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
ctx.schedule(node, () => {
ctx.startLoopIfVideo(node);
ctx.refreshDependents(node);
}, 0);
}, listenerOptions);
}
st.compResetButton?.addEventListener("click", (event) => {
event.preventDefault();
resetNodeWidgetsToDefaults(node);
st.compSelectedSlot = null;
ctx.updateCompControls(node);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
ctx.schedule(node, () => {
ctx.startLoopIfVideo(node);
ctx.refreshDependents(node);
}, 0);
}, listenerOptions);
st.compResizeButton?.addEventListener("click", (event) => {
event.preventDefault();
st.compEditMode = "resize";
ctx.updateCompControls(node);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
}, listenerOptions);
st.compCornerPinButton?.addEventListener("click", (event) => {
event.preventDefault();
st.compEditMode = "cornerpin";
ctx.updateCompControls(node);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
}, listenerOptions);
st.compModeSelect?.addEventListener("change", () => {
ctx.updateSelectedCompLayer(node, (layer) => {
layer.mode = st.compModeSelect?.value ?? "over";
}, false);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
ctx.schedule(node, () => {
ctx.startLoopIfVideo(node);
ctx.refreshDependents(node);
}, 0);
}, listenerOptions);
st.compOpacityInput?.addEventListener("input", () => {
ctx.updateSelectedCompLayer(node, (layer) => {
layer.opacity = Math.max(0, Math.min(1, Number(st.compOpacityInput?.value ?? 100) / 100));
});
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
ctx.schedule(node, () => {
ctx.startLoopIfVideo(node);
ctx.refreshDependents(node);
}, 0);
}, listenerOptions);
canvas.addEventListener("pointerdown", (event) => {
const point = worldPt(event);
const hit = ctx.getCompHit(node, canvas.width, canvas.height, point.x, point.y);
if (!hit) return;
event.preventDefault();
event.stopPropagation();
safeSetPointerCapture(canvas, event.pointerId);
st.compSelectedSlot = hit.layer.slot;
ctx.updateCompControls(node);
const layers = ctx.ensureCompState(node);
const layer = layers.find((entry) => entry.slot === hit.layer.slot);
if (!layer) return;
st.compDrag = {
pointerId: event.pointerId,
slot: hit.layer.slot,
mode: hit.mode,
startCanvasX: point.x,
startCanvasY: point.y,
startCenterX: layer.centerX,
startCenterY: layer.centerY,
startScale: layer.scale,
startRotationDeg: layer.rotationDeg,
startPointerAngle: Math.atan2(
point.y - hit.layer.centerY * canvas.height / Math.max(1, st.compOutputHeight || canvas.height),
point.x - hit.layer.centerX * canvas.width / Math.max(1, st.compOutputWidth || canvas.width)
),
startLeft: hit.layer.left,
startTop: hit.layer.top,
startWidth: hit.layer.width,
startHeight: hit.layer.height,
startOutputCenterX: hit.layer.centerX,
startOutputCenterY: hit.layer.centerY,
startDrawWidth: hit.layer.drawWidth,
startDrawHeight: hit.layer.drawHeight,
sourceWidth: hit.layer.sourceWidth,
sourceHeight: hit.layer.sourceHeight,
startCorners: cloneCompCorners(hit.layer.corners)
};
canvas.style.cursor = getCompCursor(hit.mode);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
}, listenerOptions);
canvas.addEventListener("pointermove", (event) => {
const point = worldPt(event);
const drag = st.compDrag;
if (!drag || drag.pointerId !== event.pointerId) {
canvas.style.cursor = getCompCursor(ctx.getCompHit(node, canvas.width, canvas.height, point.x, point.y)?.mode ?? null);
return;
}
event.preventDefault();
const outputPoint = ctx.compCanvasToOutputPoint(node, canvas.width, canvas.height, point.x, point.y);
const startOutputPoint = ctx.compCanvasToOutputPoint(node, canvas.width, canvas.height, drag.startCanvasX, drag.startCanvasY);
ctx.updateSelectedCompLayer(node, (layer) => {
const corners = cloneCompCorners(drag.startCorners) ?? getCompLayerOutputCorners(
st.compOutputWidth || 1,
st.compOutputHeight || 1,
drag.sourceWidth,
drag.sourceHeight,
layer
);
const center = {
x: (corners.tl.x + corners.tr.x + corners.bl.x + corners.br.x) / 4,
y: (corners.tl.y + corners.tr.y + corners.bl.y + corners.br.y) / 4
};
if (drag.mode === "rotate") {
const currentAngle = Math.atan2(outputPoint.y - center.y, outputPoint.x - center.x);
const startAngle = Math.atan2(startOutputPoint.y - center.y, startOutputPoint.x - center.x);
const delta = currentAngle - startAngle;
for (const key of ["tl", "tr", "bl", "br"]) {
const start = corners[key];
const rotated = rotatePoint(start.x - center.x, start.y - center.y, delta);
corners[key] = { x: center.x + rotated.x, y: center.y + rotated.y };
}
ctx.writeCompLayerCorners(node, layer, corners);
layer.rotationDeg = clampCompRotation(drag.startRotationDeg + delta * 180 / Math.PI);
return;
}
if (drag.mode === "move") {
const deltaX = outputPoint.x - startOutputPoint.x;
const deltaY = outputPoint.y - startOutputPoint.y;
for (const key of ["tl", "tr", "bl", "br"]) {
const start = (drag.startCorners ?? corners)[key];
corners[key] = { x: start.x + deltaX, y: start.y + deltaY };
}
ctx.writeCompLayerCorners(node, layer, corners);
return;
}
const handle = compDragHandleToCorner(drag.mode);
if (!handle) return;
if (st.compEditMode === "cornerpin") {
corners[handle] = { x: outputPoint.x, y: outputPoint.y };
ctx.writeCompLayerCorners(node, layer, corners);
return;
}
const startCorners = drag.startCorners ?? corners;
const startCenter = {
x: (startCorners.tl.x + startCorners.tr.x + startCorners.bl.x + startCorners.br.x) / 4,
y: (startCorners.tl.y + startCorners.tr.y + startCorners.bl.y + startCorners.br.y) / 4
};
const startHandle = startCorners[handle];
const startDistance = Math.max(1e-3, Math.hypot(startHandle.x - startCenter.x, startHandle.y - startCenter.y));
const currentDistance = Math.max(1e-3, Math.hypot(outputPoint.x - startCenter.x, outputPoint.y - startCenter.y));
const scaleRatio = currentDistance / startDistance;
for (const key of ["tl", "tr", "bl", "br"]) {
const start = startCorners[key];
corners[key] = {
x: startCenter.x + (start.x - startCenter.x) * scaleRatio,
y: startCenter.y + (start.y - startCenter.y) * scaleRatio
};
}
ctx.writeCompLayerCorners(node, layer, corners);
layer.scale = clampCompScale(drag.startScale * scaleRatio);
});
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
if (!moveRafPending) {
moveRafPending = true;
requestAnimationFrame(() => {
moveRafPending = false;
ctx.refreshPreviewOnly(node);
});
}
}, listenerOptions);
const releaseDrag = (event) => {
if (!st.compDrag || st.compDrag.pointerId !== event.pointerId) return;
st.compDrag = null;
safeReleasePointerCapture(canvas, event.pointerId);
const point = worldPt(event);
canvas.style.cursor = getCompCursor(ctx.getCompHit(node, canvas.width, canvas.height, point.x, point.y)?.mode ?? null);
ctx.updateSelectedCompLayer(node, (_layer) => {
}, true);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
ctx.schedule(node, () => {
ctx.startLoopIfVideo(node);
ctx.refreshDependents(node);
}, 0);
};
canvas.addEventListener("pointerup", releaseDrag, listenerOptions);
canvas.addEventListener("pointercancel", releaseDrag, listenerOptions);
canvas.addEventListener("lostpointercapture", releaseDrag, listenerOptions);
canvas.addEventListener("pointerleave", () => {
if (!st.compDrag) canvas.style.cursor = "default";
}, listenerOptions);
if (!st.compKeyboardHooked) {
st.compKeyboardHooked = true;
let pointerOverCanvas = false;
const signal = st?._abortController?.signal;
canvas.addEventListener("pointerenter", () => {
pointerOverCanvas = true;
}, signal ? { signal } : void 0);
canvas.addEventListener("pointerleave", () => {
pointerOverCanvas = false;
}, signal ? { signal } : void 0);
const isEditableTarget = (target) => {
const el = target;
if (!el) return false;
const tag = (el.tagName || "").toUpperCase();
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable === true;
};
window.addEventListener("keydown", (event) => {
if (!pointerOverCanvas && document.activeElement !== canvas) return;
if (isEditableTarget(event.target)) return;
if (event.key === "Delete" || event.key === "Backspace") {
event.preventDefault();
if (!removeSelectedCompLayer(node)) return;
ctx.updateCompControls(node);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
ctx.schedule(node, () => {
ctx.startLoopIfVideo(node);
ctx.refreshDependents(node);
}, 0);
return;
}
const digit = event.key.charCodeAt(0);
if (digit >= 49 && digit <= 57 && event.key.length === 1) {
const idx = digit - 49;
const layers = ctx.ensureCompState(node);
const target = layers[idx];
if (target) {
event.preventDefault();
st.compSelectedSlot = target.slot;
ctx.updateCompControls(node);
ctx.markPreviewInteraction(node);
ctx.markCanvasDirty();
}
}
}, signal ? { signal } : void 0);
}
}
export {
attachInteractions
};