Release standalone timeline editor 1.0.0
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
// ComfyUI extension registration and node lifecycle wiring.
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import {
|
||||
CANVAS_SIZE_PROPERTY,
|
||||
CONTROL_TAB_PROPERTY,
|
||||
LAYER_LOCK_PROPERTY,
|
||||
OUTPUT_SETTINGS_PROPERTY,
|
||||
UI_INSTANCE_PROPERTY,
|
||||
UPLOAD_MODE_PROPERTY,
|
||||
} from "./constants.js";
|
||||
import {
|
||||
applyBackendTimelineSettings,
|
||||
installBackendTimelineSettingsListener,
|
||||
isTargetNode,
|
||||
normalizeTimelineSettings,
|
||||
payloadsMatchNode,
|
||||
setTimelineStatus,
|
||||
uiPayloadValue,
|
||||
} from "./runtime.js";
|
||||
import {
|
||||
activeLayer,
|
||||
controlsTab,
|
||||
deleteActiveLayer,
|
||||
ensureSelectedKeyframe,
|
||||
layerEditingLocked,
|
||||
normalizeKeyframes,
|
||||
normalizePan,
|
||||
preloadImage,
|
||||
preserveExecutedFilterFields,
|
||||
refreshTimeline,
|
||||
rejectLayerEditIfLocked,
|
||||
selectedKeyframe,
|
||||
selectedKeyframeIndex,
|
||||
serializableTimelinePayload,
|
||||
setActiveLayerId,
|
||||
setControlsTab,
|
||||
setSelectedKeyframe,
|
||||
syncTimelineWidget,
|
||||
timelineReloadSource,
|
||||
toggleLayerEditingLock,
|
||||
toggleUploadMode,
|
||||
updateActiveLayer,
|
||||
uploadMode,
|
||||
} from "./model.js";
|
||||
import {
|
||||
fixPromptWidgetHeights,
|
||||
migrateDisableRelayWidgetOrder,
|
||||
syncChainInheritedWidgets,
|
||||
syncKeyframePromptEditor,
|
||||
syncTextLayerEditor,
|
||||
} from "./editors.js";
|
||||
import {
|
||||
configuredCanvasSize,
|
||||
rememberTimelineCanvasSize,
|
||||
scheduleTimelineCanvasSizeRestore,
|
||||
setupNode,
|
||||
} from "./node_setup.js";
|
||||
import {
|
||||
addKeyframeAt,
|
||||
bounds,
|
||||
deleteKeyframeAtIndex,
|
||||
duplicateKeyframeAtIndex,
|
||||
findKeyframeAt,
|
||||
previewImageBox,
|
||||
slotConflict,
|
||||
slotToSeconds,
|
||||
updateTimelineSliderDrag,
|
||||
widgetOutputSettings,
|
||||
xToSlot,
|
||||
} from "./timeline_ops.js";
|
||||
import {
|
||||
controlTabAt,
|
||||
layerChipAt,
|
||||
previewPlacement,
|
||||
previewSourceForLayer,
|
||||
refreshButtonBounds,
|
||||
} from "./rendering.js";
|
||||
import {
|
||||
addTextLayer,
|
||||
deleteLayerButtonBounds,
|
||||
imageButtonBounds,
|
||||
installKeyboardShortcuts,
|
||||
installPointerRelease,
|
||||
installWheelCapture,
|
||||
keyframeStrengthSliderAt,
|
||||
layerFilterSliderAt,
|
||||
lockLayerButtonBounds,
|
||||
modeButtonBounds,
|
||||
releaseDrag,
|
||||
textLayerButtonBounds,
|
||||
uploadModeButtonBounds,
|
||||
zoomPreviewAt,
|
||||
} from "./interaction.js";
|
||||
import { installClipboardPasteCapture, installFileDropCapture } from "./media.js";
|
||||
export function selectedKeyframeIndexForMenu(node) {
|
||||
setupNode(node);
|
||||
return selectedKeyframeIndex(node, normalizeKeyframes(node.properties.etk_ltxv_timeline_keyframes, node));
|
||||
}
|
||||
|
||||
export function timelineNodeMenuItems(node) {
|
||||
if (!isTargetNode(node)) return [];
|
||||
return [
|
||||
null,
|
||||
{
|
||||
content: "ETK Timeline: Refresh previews",
|
||||
callback: () => {
|
||||
setupNode(node);
|
||||
refreshTimeline(node);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: `ETK Timeline: Upload mode: ${uploadMode(node) === "layer" ? "add layer" : "replace"}`,
|
||||
callback: () => {
|
||||
setupNode(node);
|
||||
toggleUploadMode(node);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "ETK Timeline: Upload image to selected keyframe...",
|
||||
callback: () => {
|
||||
setupNode(node);
|
||||
const keyframe = ensureSelectedKeyframe(node);
|
||||
if (!keyframe) return;
|
||||
if (node.etkTimelineFileInput) node.etkTimelineFileInput.click();
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "ETK Timeline: Duplicate selected keyframe",
|
||||
callback: () => {
|
||||
const index = selectedKeyframeIndexForMenu(node);
|
||||
if (index < 0 || !duplicateKeyframeAtIndex(node, index)) {
|
||||
setTimelineStatus(node, "Select a keyframe to duplicate.", "error");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "ETK Timeline: Delete selected keyframe",
|
||||
callback: () => {
|
||||
const index = selectedKeyframeIndexForMenu(node);
|
||||
if (index < 0 || !deleteKeyframeAtIndex(node, index)) {
|
||||
setTimelineStatus(node, "Select a keyframe to delete.", "error");
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "ETK.LTXVTimelineImageEditor",
|
||||
getNodeMenuItems(node) {
|
||||
return timelineNodeMenuItems(node);
|
||||
},
|
||||
beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (!isTargetNode(nodeData.name)) return;
|
||||
if (nodeType.prototype.etkTimelinePatched) return;
|
||||
nodeType.prototype.etkTimelinePatched = true;
|
||||
installBackendTimelineSettingsListener();
|
||||
installWheelCapture();
|
||||
installPointerRelease();
|
||||
installKeyboardShortcuts();
|
||||
installFileDropCapture();
|
||||
installClipboardPasteCapture();
|
||||
|
||||
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (canvas, options) {
|
||||
const extra = originalGetExtraMenuOptions ? originalGetExtraMenuOptions.apply(this, arguments) : undefined;
|
||||
const timelineItems = timelineNodeMenuItems(this);
|
||||
if (Array.isArray(extra)) return extra.concat(timelineItems);
|
||||
if (Array.isArray(options)) options.push(...timelineItems);
|
||||
return extra;
|
||||
};
|
||||
|
||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
if (originalOnNodeCreated) originalOnNodeCreated.apply(this, arguments);
|
||||
setupNode(this);
|
||||
scheduleTimelineCanvasSizeRestore(this);
|
||||
};
|
||||
|
||||
const originalOnConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function (config) {
|
||||
if (originalOnConfigure) originalOnConfigure.apply(this, arguments);
|
||||
if (!this.properties) this.properties = {};
|
||||
const migratedWidgetOrder = migrateDisableRelayWidgetOrder(this);
|
||||
this.properties.etk_ltxv_timeline_keyframes = normalizeKeyframes(timelineReloadSource(this, migratedWidgetOrder), this);
|
||||
setupNode(this);
|
||||
scheduleTimelineCanvasSizeRestore(this, configuredCanvasSize(config));
|
||||
selectedKeyframeIndex(this, this.properties.etk_ltxv_timeline_keyframes);
|
||||
};
|
||||
|
||||
const originalOnSerialize = nodeType.prototype.onSerialize;
|
||||
nodeType.prototype.onSerialize = function (serialized) {
|
||||
if (originalOnSerialize) originalOnSerialize.apply(this, arguments);
|
||||
if (!serialized) return;
|
||||
setupNode(this);
|
||||
const keyframes = normalizeKeyframes(this.properties.etk_ltxv_timeline_keyframes, this);
|
||||
this.properties.etk_ltxv_timeline_keyframes = keyframes;
|
||||
syncTimelineWidget(this);
|
||||
if (!serialized.properties) serialized.properties = {};
|
||||
serialized.properties.etk_ltxv_timeline_keyframes = serializableTimelinePayload(this, keyframes);
|
||||
serialized.properties.etk_ltxv_selected_keyframe = this.properties.etk_ltxv_selected_keyframe;
|
||||
serialized.properties.etk_ltxv_selected_keyframe_id = this.properties.etk_ltxv_selected_keyframe_id;
|
||||
serialized.properties[UPLOAD_MODE_PROPERTY] = uploadMode(this);
|
||||
serialized.properties[LAYER_LOCK_PROPERTY] = layerEditingLocked(this);
|
||||
serialized.properties[CONTROL_TAB_PROPERTY] = controlsTab(this);
|
||||
const outputSettings =
|
||||
normalizeTimelineSettings(this.etkTimelineBackendSettings || this.properties[OUTPUT_SETTINGS_PROPERTY]) ||
|
||||
widgetOutputSettings(this);
|
||||
if (outputSettings) {
|
||||
serialized.properties[OUTPUT_SETTINGS_PROPERTY] = outputSettings;
|
||||
}
|
||||
const canvasSize = rememberTimelineCanvasSize(this);
|
||||
if (canvasSize) {
|
||||
serialized.size = [...canvasSize];
|
||||
serialized.properties[CANVAS_SIZE_PROPERTY] = [...canvasSize];
|
||||
}
|
||||
delete serialized.properties[UI_INSTANCE_PROPERTY];
|
||||
};
|
||||
|
||||
const originalOnExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
if (originalOnExecuted) originalOnExecuted.apply(this, arguments);
|
||||
const payload = uiPayloadValue(message, "etk_ltxv_timeline_keyframes");
|
||||
const settingsPayload = uiPayloadValue(message, "etk_ltxv_timeline_settings");
|
||||
if (!payload && !settingsPayload) return;
|
||||
if (!payloadsMatchNode(this, [payload, settingsPayload])) return;
|
||||
setupNode(this);
|
||||
applyBackendTimelineSettings(this, settingsPayload);
|
||||
if (payload) {
|
||||
const keyframes = preserveExecutedFilterFields(this, payload);
|
||||
this.properties.etk_ltxv_timeline_keyframes = keyframes;
|
||||
const selectedIndex = selectedKeyframeIndex(this, keyframes);
|
||||
if (selectedIndex < 0 && keyframes.length) {
|
||||
setSelectedKeyframe(this, 0, keyframes);
|
||||
} else {
|
||||
syncKeyframePromptEditor(this, selectedIndex >= 0 ? keyframes[selectedIndex] : null);
|
||||
syncTextLayerEditor(this);
|
||||
}
|
||||
for (const keyframe of keyframes) preloadImage(this, keyframe);
|
||||
}
|
||||
syncTimelineWidget(this);
|
||||
this.setDirtyCanvas(true, true);
|
||||
if (this.graph && typeof this.graph.setDirtyCanvas === "function") {
|
||||
this.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
const originalOnRemoved = nodeType.prototype.onRemoved;
|
||||
nodeType.prototype.onRemoved = function () {
|
||||
releaseDrag(this);
|
||||
if (this.etkTimelineFileInput) {
|
||||
this.etkTimelineFileInput.remove();
|
||||
this.etkTimelineFileInput = null;
|
||||
}
|
||||
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
|
||||
};
|
||||
|
||||
const originalComputeSize = nodeType.prototype.computeSize;
|
||||
nodeType.prototype.computeSize = function () {
|
||||
fixPromptWidgetHeights(this);
|
||||
const size = originalComputeSize ? originalComputeSize.apply(this, arguments) : [360, 260];
|
||||
size[0] = Math.max(size[0], 460);
|
||||
return size;
|
||||
};
|
||||
|
||||
const originalOnResize = nodeType.prototype.onResize;
|
||||
nodeType.prototype.onResize = function (size) {
|
||||
fixPromptWidgetHeights(this);
|
||||
syncChainInheritedWidgets(this);
|
||||
if (originalOnResize) originalOnResize.apply(this, arguments);
|
||||
if (!size) return;
|
||||
size[0] = Math.max(size[0], 460);
|
||||
rememberTimelineCanvasSize(this, size);
|
||||
if (this.setDirtyCanvas) this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const originalOnConnectionsChange = nodeType.prototype.onConnectionsChange;
|
||||
nodeType.prototype.onConnectionsChange = function () {
|
||||
if (originalOnConnectionsChange) originalOnConnectionsChange.apply(this, arguments);
|
||||
setupNode(this);
|
||||
syncChainInheritedWidgets(this);
|
||||
if (this.setDirtyCanvas) this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const handleTimelineDoubleClick = function () {
|
||||
const pos = Array.from(arguments).find((arg) => Array.isArray(arg) && arg.length >= 2);
|
||||
if (!pos) return false;
|
||||
setupNode(this);
|
||||
const { padX, width, timelineY, timelineH } = bounds(this);
|
||||
const [x, y] = pos;
|
||||
if (x < padX || x > padX + width || y < timelineY || y > timelineY + timelineH) return false;
|
||||
if (findKeyframeAt(this, pos) >= 0) return false;
|
||||
addKeyframeAt(this, x);
|
||||
return true;
|
||||
};
|
||||
|
||||
const originalMouseDblClick = nodeType.prototype.onMouseDblClick;
|
||||
nodeType.prototype.onMouseDblClick = function (_event, pos) {
|
||||
if (handleTimelineDoubleClick.apply(this, arguments)) return true;
|
||||
return originalMouseDblClick ? originalMouseDblClick.apply(this, arguments) : false;
|
||||
};
|
||||
|
||||
const originalDblClick = nodeType.prototype.onDblClick;
|
||||
nodeType.prototype.onDblClick = function () {
|
||||
if (handleTimelineDoubleClick.apply(this, arguments)) return true;
|
||||
return originalDblClick ? originalDblClick.apply(this, arguments) : false;
|
||||
};
|
||||
|
||||
const originalMouseDown = nodeType.prototype.onMouseDown;
|
||||
nodeType.prototype.onMouseDown = function (event, pos) {
|
||||
if (event && event.button != null && event.button !== 0) {
|
||||
return originalMouseDown ? originalMouseDown.apply(this, arguments) : false;
|
||||
}
|
||||
setupNode(this);
|
||||
const keyframes = normalizeKeyframes(this.properties.etk_ltxv_timeline_keyframes, this);
|
||||
const hitIndex = findKeyframeAt(this, pos);
|
||||
const { padX, width, timelineY, timelineH, controlsY, controlsH, previewY, previewH } = bounds(this);
|
||||
const [x, y] = pos;
|
||||
const inControls = x >= padX && x <= padX + width && y >= controlsY && y <= controlsY + controlsH;
|
||||
const inPreview = x >= padX && x <= padX + width && y >= previewY && y <= previewY + previewH;
|
||||
const refreshButton = refreshButtonBounds(this);
|
||||
|
||||
if (
|
||||
x >= refreshButton.x &&
|
||||
x <= refreshButton.x + refreshButton.w &&
|
||||
y >= refreshButton.y &&
|
||||
y <= refreshButton.y + refreshButton.h
|
||||
) {
|
||||
refreshTimeline(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hitIndex >= 0) {
|
||||
setSelectedKeyframe(this, hitIndex, keyframes);
|
||||
this.etkTimelineDragging = true;
|
||||
this.etkTimelineDraggingId = keyframes[hitIndex].id;
|
||||
window.etkTimelineActiveDragNode = this;
|
||||
this.setDirtyCanvas(true, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x >= padX && x <= padX + width && y >= timelineY && y <= timelineY + timelineH) {
|
||||
const now = performance.now();
|
||||
const isDoubleClick =
|
||||
(event && event.detail && event.detail >= 2) ||
|
||||
(this.etkTimelineLastClick && now - this.etkTimelineLastClick < 350);
|
||||
if (isDoubleClick) {
|
||||
addKeyframeAt(this, x);
|
||||
this.etkTimelineLastClick = 0;
|
||||
} else {
|
||||
this.etkTimelineLastClick = now;
|
||||
this.setDirtyCanvas(true, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (inControls || inPreview) {
|
||||
if (inControls) {
|
||||
const tab = controlTabAt(this, x, y);
|
||||
if (tab) {
|
||||
setControlsTab(this, tab);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const selected = selectedKeyframe(this);
|
||||
if (selected) {
|
||||
const activeControlTab = controlsTab(this);
|
||||
if (inControls && activeControlTab === "layers") {
|
||||
const button = modeButtonBounds(this);
|
||||
const imageButton = imageButtonBounds(this);
|
||||
const textButton = textLayerButtonBounds(this);
|
||||
const uploadButton = uploadModeButtonBounds(this);
|
||||
const lockLayerButton = lockLayerButtonBounds(this);
|
||||
const deleteLayerButton = deleteLayerButtonBounds(this);
|
||||
if (x >= lockLayerButton.x && x <= lockLayerButton.x + lockLayerButton.w && y >= lockLayerButton.y && y <= lockLayerButton.y + lockLayerButton.h) {
|
||||
toggleLayerEditingLock(this);
|
||||
return true;
|
||||
}
|
||||
if (x >= button.x && x <= button.x + button.w && y >= button.y && y <= button.y + button.h) {
|
||||
if (rejectLayerEditIfLocked(this, "Fit mode change")) return true;
|
||||
const layer = activeLayer(selected);
|
||||
if (layer) {
|
||||
updateActiveLayer(this, selected.id, (current) => ({
|
||||
...current,
|
||||
fitMode: current.fitMode === "crop" ? "pad" : "crop",
|
||||
}));
|
||||
} else {
|
||||
selected.fitMode = selected.fitMode === "crop" ? "pad" : "crop";
|
||||
const selectedIndex = selectedKeyframeIndex(this, keyframes);
|
||||
if (selectedIndex < 0) return true;
|
||||
keyframes[selectedIndex] = selected;
|
||||
this.properties.etk_ltxv_timeline_keyframes = keyframes;
|
||||
syncTimelineWidget(this);
|
||||
this.setDirtyCanvas(true, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (x >= imageButton.x && x <= imageButton.x + imageButton.w && y >= imageButton.y && y <= imageButton.y + imageButton.h) {
|
||||
if (rejectLayerEditIfLocked(this, "Image upload")) return true;
|
||||
this.etkTimelineFileInput.click();
|
||||
return true;
|
||||
}
|
||||
if (x >= textButton.x && x <= textButton.x + textButton.w && y >= textButton.y && y <= textButton.y + textButton.h) {
|
||||
addTextLayer(this, selected.id);
|
||||
return true;
|
||||
}
|
||||
if (x >= uploadButton.x && x <= uploadButton.x + uploadButton.w && y >= uploadButton.y && y <= uploadButton.y + uploadButton.h) {
|
||||
if (rejectLayerEditIfLocked(this, "Upload mode change")) return true;
|
||||
toggleUploadMode(this);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
x >= deleteLayerButton.x &&
|
||||
x <= deleteLayerButton.x + deleteLayerButton.w &&
|
||||
y >= deleteLayerButton.y &&
|
||||
y <= deleteLayerButton.y + deleteLayerButton.h
|
||||
) {
|
||||
deleteActiveLayer(this, selected.id);
|
||||
return true;
|
||||
}
|
||||
const chip = layerChipAt(this, selected, x, y);
|
||||
if (chip) {
|
||||
setActiveLayerId(this, selected.id, chip.layerId);
|
||||
return true;
|
||||
}
|
||||
} else if (inControls && activeControlTab === "filters") {
|
||||
const layer = activeLayer(selected);
|
||||
const filterSlider = layer ? layerFilterSliderAt(this, x, y) : null;
|
||||
if (layer && filterSlider) {
|
||||
this.etkSliderDragging = { family: "filter", keyframeId: selected.id, layerId: layer.id, kind: filterSlider.kind };
|
||||
window.etkTimelineActiveDragNode = this;
|
||||
updateTimelineSliderDrag(this, pos);
|
||||
return true;
|
||||
}
|
||||
} else if (inControls && activeControlTab === "strength") {
|
||||
const strengthSlider = keyframeStrengthSliderAt(this, x, y);
|
||||
if (strengthSlider) {
|
||||
this.etkSliderDragging = { family: "strength", keyframeId: selected.id, kind: strengthSlider.kind };
|
||||
window.etkTimelineActiveDragNode = this;
|
||||
updateTimelineSliderDrag(this, pos);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const layer = activeLayer(selected);
|
||||
if (layer && inPreview) {
|
||||
if (layerEditingLocked(this)) {
|
||||
rejectLayerEditIfLocked(this, "Layer pan");
|
||||
return true;
|
||||
}
|
||||
const { boxX, boxY, boxW, boxH } = previewImageBox(this);
|
||||
if (x >= boxX && x <= boxX + boxW && y >= boxY && y <= boxY + boxH) {
|
||||
this.etkPreviewPanning = {
|
||||
keyframeId: selected.id,
|
||||
layerId: layer.id,
|
||||
startX: x,
|
||||
startY: y,
|
||||
startPanX: normalizePan(layer.panX),
|
||||
startPanY: normalizePan(layer.panY),
|
||||
};
|
||||
window.etkTimelineActiveDragNode = this;
|
||||
return true;
|
||||
}
|
||||
} else if (!layer && inPreview) {
|
||||
if (rejectLayerEditIfLocked(this, "Image upload")) return true;
|
||||
this.etkTimelineFileInput.click();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return originalMouseDown ? originalMouseDown.apply(this, arguments) : false;
|
||||
};
|
||||
|
||||
const originalMouseMove = nodeType.prototype.onMouseMove;
|
||||
nodeType.prototype.onMouseMove = function (event, pos) {
|
||||
if (this.etkSliderDragging) {
|
||||
if (event && event.buttons === 0) {
|
||||
releaseDrag(this);
|
||||
return true;
|
||||
}
|
||||
updateTimelineSliderDrag(this, pos);
|
||||
return true;
|
||||
}
|
||||
if (this.etkPreviewPanning) {
|
||||
if (event && event.buttons === 0) {
|
||||
releaseDrag(this);
|
||||
return true;
|
||||
}
|
||||
const keyframes = normalizeKeyframes(this.properties.etk_ltxv_timeline_keyframes, this);
|
||||
const keyframe = keyframes.find((candidate) => candidate.id === this.etkPreviewPanning.keyframeId);
|
||||
const layer = keyframe && (keyframe.layers || []).find((candidate) => candidate.id === this.etkPreviewPanning.layerId);
|
||||
const image = layer ? previewSourceForLayer(this, layer) : null;
|
||||
if (keyframe && layer && image && image.complete && image.naturalWidth > 0 && image.naturalHeight > 0) {
|
||||
const { boxW, boxH } = previewImageBox(this);
|
||||
const placement = previewPlacement(
|
||||
image,
|
||||
boxW,
|
||||
boxH,
|
||||
layer.fitMode,
|
||||
layer.panX,
|
||||
layer.panY,
|
||||
layer.zoom
|
||||
);
|
||||
const dx = pos[0] - this.etkPreviewPanning.startX;
|
||||
const dy = pos[1] - this.etkPreviewPanning.startY;
|
||||
const nextPanX =
|
||||
placement.panRangeX > 0
|
||||
? this.etkPreviewPanning.startPanX + dx / (placement.panRangeX / 2)
|
||||
: 0;
|
||||
const nextPanY =
|
||||
placement.panRangeY > 0
|
||||
? this.etkPreviewPanning.startPanY + dy / (placement.panRangeY / 2)
|
||||
: 0;
|
||||
updateActiveLayer(this, keyframe.id, (current) => ({
|
||||
...current,
|
||||
panX: normalizePan(nextPanX),
|
||||
panY: normalizePan(nextPanY),
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (this.etkTimelineDragging) {
|
||||
if (event && event.buttons === 0) {
|
||||
releaseDrag(this);
|
||||
return true;
|
||||
}
|
||||
const keyframes = normalizeKeyframes(this.properties.etk_ltxv_timeline_keyframes, this);
|
||||
const draggingId = this.etkTimelineDraggingId || this.properties.etk_ltxv_selected_keyframe_id;
|
||||
const index = draggingId
|
||||
? keyframes.findIndex((keyframe) => keyframe.id === draggingId)
|
||||
: selectedKeyframeIndex(this, keyframes);
|
||||
if (index >= 0 && index < keyframes.length) {
|
||||
const slot = xToSlot(this, pos[0]);
|
||||
if (slotConflict(this, slot, index) < 0) {
|
||||
const selected = keyframes[index];
|
||||
selected.slot = slot;
|
||||
selected.time = slotToSeconds(this, slot);
|
||||
keyframes.sort((a, b) => a.time - b.time || a.slot - b.slot);
|
||||
this.properties.etk_ltxv_timeline_keyframes = keyframes;
|
||||
setSelectedKeyframe(this, keyframes.findIndex((keyframe) => keyframe.id === selected.id), keyframes);
|
||||
syncTimelineWidget(this);
|
||||
}
|
||||
this.setDirtyCanvas(true, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return originalMouseMove ? originalMouseMove.apply(this, arguments) : false;
|
||||
};
|
||||
|
||||
const originalMouseWheel = nodeType.prototype.onMouseWheel;
|
||||
nodeType.prototype.onMouseWheel = function () {
|
||||
const pos = Array.from(arguments).find((arg) => Array.isArray(arg) && arg.length >= 2);
|
||||
const event = Array.from(arguments).find((arg) => arg && typeof arg === "object" && !Array.isArray(arg));
|
||||
if (zoomPreviewAt(this, event, pos)) {
|
||||
if (event && event.preventDefault) event.preventDefault();
|
||||
if (event && event.stopPropagation) event.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
return originalMouseWheel ? originalMouseWheel.apply(this, arguments) : false;
|
||||
};
|
||||
|
||||
const originalMouseUp = nodeType.prototype.onMouseUp;
|
||||
nodeType.prototype.onMouseUp = function () {
|
||||
if (releaseDrag(this)) return true;
|
||||
return originalMouseUp ? originalMouseUp.apply(this, arguments) : false;
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user