Move frames to animation curves
This commit is contained in:
parent
9f338ba6dc
commit
7bade4517c
552
src/main.js
552
src/main.js
|
|
@ -435,7 +435,8 @@ let startProps = {};
|
||||||
let actions = {
|
let actions = {
|
||||||
addShape: {
|
addShape: {
|
||||||
create: (parent, shape, ctx) => {
|
create: (parent, shape, ctx) => {
|
||||||
if (!parent.currentFrame?.exists) return;
|
// parent should be a GraphicsObject
|
||||||
|
if (!parent.activeLayer) return;
|
||||||
if (shape.curves.length == 0) return;
|
if (shape.curves.length == 0) return;
|
||||||
redoStack.length = 0; // Clear redo stack
|
redoStack.length = 0; // Clear redo stack
|
||||||
let serializableCurves = [];
|
let serializableCurves = [];
|
||||||
|
|
@ -448,6 +449,7 @@ let actions = {
|
||||||
};
|
};
|
||||||
let action = {
|
let action = {
|
||||||
parent: parent.idx,
|
parent: parent.idx,
|
||||||
|
layer: parent.activeLayer.idx,
|
||||||
curves: serializableCurves,
|
curves: serializableCurves,
|
||||||
startx: shape.startx,
|
startx: shape.startx,
|
||||||
starty: shape.starty,
|
starty: shape.starty,
|
||||||
|
|
@ -459,23 +461,20 @@ let actions = {
|
||||||
lineWidth: c.lineWidth,
|
lineWidth: c.lineWidth,
|
||||||
},
|
},
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
frame: parent.currentFrame.idx,
|
time: parent.currentTime, // Use currentTime instead of currentFrame
|
||||||
};
|
};
|
||||||
undoStack.push({ name: "addShape", action: action });
|
undoStack.push({ name: "addShape", action: action });
|
||||||
actions.addShape.execute(action);
|
actions.addShape.execute(action);
|
||||||
updateMenu();
|
updateMenu();
|
||||||
},
|
},
|
||||||
execute: (action) => {
|
execute: (action) => {
|
||||||
let object = pointerList[action.parent];
|
let layer = pointerList[action.layer];
|
||||||
let frame = action.frame
|
|
||||||
? pointerList[action.frame]
|
|
||||||
: object.currentFrame;
|
|
||||||
let curvesList = action.curves;
|
let curvesList = action.curves;
|
||||||
let cxt = {
|
let cxt = {
|
||||||
...context,
|
...context,
|
||||||
...action.context,
|
...action.context,
|
||||||
};
|
};
|
||||||
let shape = new Shape(action.startx, action.starty, cxt, action.uuid);
|
let shape = new Shape(action.startx, action.starty, cxt, layer, action.uuid);
|
||||||
for (let curve of curvesList) {
|
for (let curve of curvesList) {
|
||||||
shape.addCurve(
|
shape.addCurve(
|
||||||
new Bezier(
|
new Bezier(
|
||||||
|
|
@ -492,16 +491,55 @@ let actions = {
|
||||||
}
|
}
|
||||||
let shapes = shape.update();
|
let shapes = shape.update();
|
||||||
for (let newShape of shapes) {
|
for (let newShape of shapes) {
|
||||||
frame.addShape(newShape, cxt.sendToBack, frame);
|
// Add shape to layer's shapes array
|
||||||
|
layer.shapes.push(newShape);
|
||||||
|
|
||||||
|
// Determine zOrder based on sendToBack
|
||||||
|
let zOrder;
|
||||||
|
if (cxt.sendToBack) {
|
||||||
|
// Insert at back (zOrder 0), shift all other shapes up
|
||||||
|
zOrder = 0;
|
||||||
|
// Increment zOrder for all existing shapes
|
||||||
|
for (let existingShape of layer.shapes) {
|
||||||
|
if (existingShape !== newShape) {
|
||||||
|
let existingZOrderCurve = layer.animationData.curves[`shape.${existingShape.idx}.zOrder`];
|
||||||
|
if (existingZOrderCurve) {
|
||||||
|
// Find keyframe at this time and increment it
|
||||||
|
for (let kf of existingZOrderCurve.keyframes) {
|
||||||
|
if (kf.time === action.time) {
|
||||||
|
kf.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Insert at front (max zOrder + 1)
|
||||||
|
zOrder = layer.shapes.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add keyframes to AnimationData for this shape
|
||||||
|
let existsKeyframe = new Keyframe(action.time, 1, "hold");
|
||||||
|
layer.animationData.addKeyframe(`shape.${newShape.idx}.exists`, existsKeyframe);
|
||||||
|
|
||||||
|
let zOrderKeyframe = new Keyframe(action.time, zOrder, "hold");
|
||||||
|
layer.animationData.addKeyframe(`shape.${newShape.idx}.zOrder`, zOrderKeyframe);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rollback: (action) => {
|
rollback: (action) => {
|
||||||
let object = pointerList[action.parent];
|
let layer = pointerList[action.layer];
|
||||||
let frame = action.frame
|
|
||||||
? pointerList[action.frame]
|
|
||||||
: object.currentFrame;
|
|
||||||
let shape = pointerList[action.uuid];
|
let shape = pointerList[action.uuid];
|
||||||
frame.removeShape(shape);
|
|
||||||
|
// Remove shape from layer's shapes array
|
||||||
|
let shapeIndex = layer.shapes.indexOf(shape);
|
||||||
|
if (shapeIndex !== -1) {
|
||||||
|
layer.shapes.splice(shapeIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove keyframes from AnimationData
|
||||||
|
delete layer.animationData.curves[`shape.${shape.idx}.exists`];
|
||||||
|
delete layer.animationData.curves[`shape.${shape.idx}.zOrder`];
|
||||||
|
|
||||||
delete pointerList[action.uuid];
|
delete pointerList[action.uuid];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -1900,8 +1938,19 @@ function selectCurve(context, mouse) {
|
||||||
let closestDist = mouseTolerance;
|
let closestDist = mouseTolerance;
|
||||||
let closestCurve = undefined;
|
let closestCurve = undefined;
|
||||||
let closestShape = undefined;
|
let closestShape = undefined;
|
||||||
for (let shape of context.activeObject.currentFrame.shapes) {
|
|
||||||
|
// Get visible shapes from Layer using AnimationData
|
||||||
|
let currentTime = context.activeObject?.currentTime || 0;
|
||||||
|
let layer = context.activeObject?.activeLayer;
|
||||||
|
if (!layer) return undefined;
|
||||||
|
|
||||||
|
for (let shape of layer.shapes) {
|
||||||
if (shape instanceof TempShape) continue;
|
if (shape instanceof TempShape) continue;
|
||||||
|
|
||||||
|
// Check if shape exists at current time
|
||||||
|
let existsValue = layer.animationData.interpolate(`shape.${shape.idx}.exists`, currentTime);
|
||||||
|
if (!existsValue || existsValue <= 0) continue;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
mouse.x > shape.boundingBox.x.min - mouseTolerance &&
|
mouse.x > shape.boundingBox.x.min - mouseTolerance &&
|
||||||
mouse.x < shape.boundingBox.x.max + mouseTolerance &&
|
mouse.x < shape.boundingBox.x.max + mouseTolerance &&
|
||||||
|
|
@ -2099,7 +2148,9 @@ function redo() {
|
||||||
updateMenu();
|
updateMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
|
// LEGACY: Frame class - being phased out in favor of AnimationData
|
||||||
|
// TODO: Remove once all code is migrated to AnimationData system
|
||||||
class Frame {
|
class Frame {
|
||||||
constructor(frameType = "normal", uuid = undefined) {
|
constructor(frameType = "normal", uuid = undefined) {
|
||||||
this.keys = {};
|
this.keys = {};
|
||||||
|
|
@ -2202,23 +2253,239 @@ class TempFrame {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempFrame = new TempFrame();
|
const tempFrame = new TempFrame();
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
// Animation system classes
|
||||||
Theory:
|
class Keyframe {
|
||||||
class Keyframe:
|
constructor(time, value, interpolation = "linear", uuid = undefined) {
|
||||||
time: float
|
this.time = time;
|
||||||
value: float
|
this.value = value;
|
||||||
interpolation: str # 'linear', 'bezier', 'step'
|
this.interpolation = interpolation; // 'linear', 'bezier', 'step', 'hold'
|
||||||
# Optional: bezier handles, easing functions
|
// For bezier interpolation
|
||||||
|
this.easeIn = { x: 0.42, y: 0 }; // Default ease-in control point
|
||||||
class AnimationCurve:
|
this.easeOut = { x: 0.58, y: 1 }; // Default ease-out control point
|
||||||
parameter: str
|
if (!uuid) {
|
||||||
keyframes: List[Keyframe] # kept sorted by time
|
this.idx = uuidv4();
|
||||||
|
} else {
|
||||||
class AnimationData:
|
this.idx = uuid;
|
||||||
curves: Dict[str, AnimationCurve] # parameter name -> curve
|
}
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
static fromJSON(json) {
|
||||||
|
const keyframe = new Keyframe(json.time, json.value, json.interpolation, json.idx);
|
||||||
|
if (json.easeIn) keyframe.easeIn = json.easeIn;
|
||||||
|
if (json.easeOut) keyframe.easeOut = json.easeOut;
|
||||||
|
return keyframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
idx: this.idx,
|
||||||
|
time: this.time,
|
||||||
|
value: this.value,
|
||||||
|
interpolation: this.interpolation,
|
||||||
|
easeIn: this.easeIn,
|
||||||
|
easeOut: this.easeOut
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimationCurve {
|
||||||
|
constructor(parameter, uuid = undefined) {
|
||||||
|
this.parameter = parameter; // e.g., "x", "y", "rotation", "scale_x", "exists"
|
||||||
|
this.keyframes = []; // Always kept sorted by time
|
||||||
|
if (!uuid) {
|
||||||
|
this.idx = uuidv4();
|
||||||
|
} else {
|
||||||
|
this.idx = uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addKeyframe(keyframe) {
|
||||||
|
this.keyframes.push(keyframe);
|
||||||
|
// Keep sorted by time
|
||||||
|
this.keyframes.sort((a, b) => a.time - b.time);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeKeyframe(keyframe) {
|
||||||
|
const index = this.keyframes.indexOf(keyframe);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.keyframes.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeyframeAtTime(time) {
|
||||||
|
return this.keyframes.find(kf => kf.time === time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the two keyframes that bracket the given time
|
||||||
|
getBracketingKeyframes(time) {
|
||||||
|
if (this.keyframes.length === 0) return { prev: null, next: null };
|
||||||
|
if (this.keyframes.length === 1) return { prev: this.keyframes[0], next: this.keyframes[0] };
|
||||||
|
|
||||||
|
// Binary search to find the last keyframe at or before time
|
||||||
|
let left = 0;
|
||||||
|
let right = this.keyframes.length - 1;
|
||||||
|
let prevIndex = -1;
|
||||||
|
|
||||||
|
while (left <= right) {
|
||||||
|
const mid = Math.floor((left + right) / 2);
|
||||||
|
if (this.keyframes[mid].time <= time) {
|
||||||
|
prevIndex = mid; // This could be our answer
|
||||||
|
left = mid + 1; // But check if there's a better one to the right
|
||||||
|
} else {
|
||||||
|
right = mid - 1; // Time is too large, search left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If time is before all keyframes
|
||||||
|
if (prevIndex === -1) {
|
||||||
|
return { prev: this.keyframes[0], next: this.keyframes[0], t: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If time is after all keyframes
|
||||||
|
if (prevIndex === this.keyframes.length - 1) {
|
||||||
|
return { prev: this.keyframes[prevIndex], next: this.keyframes[prevIndex], t: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = this.keyframes[prevIndex];
|
||||||
|
const next = this.keyframes[prevIndex + 1];
|
||||||
|
const t = (time - prev.time) / (next.time - prev.time);
|
||||||
|
|
||||||
|
return { prev, next, t };
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate(time) {
|
||||||
|
if (this.keyframes.length === 0) return null;
|
||||||
|
|
||||||
|
const { prev, next, t } = this.getBracketingKeyframes(time);
|
||||||
|
|
||||||
|
if (!prev || !next) return null;
|
||||||
|
if (prev === next) return prev.value;
|
||||||
|
|
||||||
|
// Handle different interpolation types
|
||||||
|
switch (prev.interpolation) {
|
||||||
|
case "step":
|
||||||
|
case "hold":
|
||||||
|
return prev.value;
|
||||||
|
|
||||||
|
case "linear":
|
||||||
|
// Simple linear interpolation
|
||||||
|
if (typeof prev.value === "number" && typeof next.value === "number") {
|
||||||
|
return prev.value + (next.value - prev.value) * t;
|
||||||
|
}
|
||||||
|
return prev.value;
|
||||||
|
|
||||||
|
case "bezier":
|
||||||
|
// Cubic bezier interpolation using control points
|
||||||
|
if (typeof prev.value === "number" && typeof next.value === "number") {
|
||||||
|
// Use ease-in/ease-out control points
|
||||||
|
const easedT = this.cubicBezierEase(t, prev.easeOut, next.easeIn);
|
||||||
|
return prev.value + (next.value - prev.value) * easedT;
|
||||||
|
}
|
||||||
|
return prev.value;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return prev.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cubic bezier easing function
|
||||||
|
cubicBezierEase(t, easeOut, easeIn) {
|
||||||
|
// Simplified cubic bezier for 0,0 -> easeOut -> easeIn -> 1,1
|
||||||
|
const u = 1 - t;
|
||||||
|
return 3 * u * u * t * easeOut.y +
|
||||||
|
3 * u * t * t * easeIn.y +
|
||||||
|
t * t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON(json) {
|
||||||
|
const curve = new AnimationCurve(json.parameter, json.idx);
|
||||||
|
for (let kfJson of json.keyframes || []) {
|
||||||
|
curve.keyframes.push(Keyframe.fromJSON(kfJson));
|
||||||
|
}
|
||||||
|
return curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
idx: this.idx,
|
||||||
|
parameter: this.parameter,
|
||||||
|
keyframes: this.keyframes.map(kf => kf.toJSON())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimationData {
|
||||||
|
constructor(uuid = undefined) {
|
||||||
|
this.curves = {}; // parameter name -> AnimationCurve
|
||||||
|
if (!uuid) {
|
||||||
|
this.idx = uuidv4();
|
||||||
|
} else {
|
||||||
|
this.idx = uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurve(parameter) {
|
||||||
|
return this.curves[parameter];
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrCreateCurve(parameter) {
|
||||||
|
if (!this.curves[parameter]) {
|
||||||
|
this.curves[parameter] = new AnimationCurve(parameter);
|
||||||
|
}
|
||||||
|
return this.curves[parameter];
|
||||||
|
}
|
||||||
|
|
||||||
|
addKeyframe(parameter, keyframe) {
|
||||||
|
const curve = this.getOrCreateCurve(parameter);
|
||||||
|
curve.addKeyframe(keyframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeKeyframe(parameter, keyframe) {
|
||||||
|
const curve = this.curves[parameter];
|
||||||
|
if (curve) {
|
||||||
|
curve.removeKeyframe(keyframe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCurve(parameter) {
|
||||||
|
delete this.curves[parameter];
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolate(parameter, time) {
|
||||||
|
const curve = this.curves[parameter];
|
||||||
|
if (!curve) return null;
|
||||||
|
return curve.interpolate(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all animated values at a given time
|
||||||
|
getValuesAtTime(time) {
|
||||||
|
const values = {};
|
||||||
|
for (let parameter in this.curves) {
|
||||||
|
values[parameter] = this.curves[parameter].interpolate(time);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON(json) {
|
||||||
|
const animData = new AnimationData(json.idx);
|
||||||
|
for (let param in json.curves || {}) {
|
||||||
|
animData.curves[param] = AnimationCurve.fromJSON(json.curves[param]);
|
||||||
|
}
|
||||||
|
return animData;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
const curves = {};
|
||||||
|
for (let param in this.curves) {
|
||||||
|
curves[param] = this.curves[param].toJSON();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
idx: this.idx,
|
||||||
|
curves: curves
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Layer extends Widget {
|
class Layer extends Widget {
|
||||||
constructor(uuid) {
|
constructor(uuid) {
|
||||||
|
|
@ -2229,8 +2496,9 @@ class Layer extends Widget {
|
||||||
this.idx = uuid;
|
this.idx = uuid;
|
||||||
}
|
}
|
||||||
this.name = "Layer";
|
this.name = "Layer";
|
||||||
// this.frames = [new Frame("keyframe", this.idx + "-F1")];
|
// LEGACY: Keep frames array for backwards compatibility during migration
|
||||||
this.animationData = {}
|
this.frames = [new Frame("keyframe", this.idx + "-F1")];
|
||||||
|
this.animationData = new AnimationData();
|
||||||
// this.frameNum = 0;
|
// this.frameNum = 0;
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.audible = true;
|
this.audible = true;
|
||||||
|
|
@ -2238,7 +2506,6 @@ class Layer extends Widget {
|
||||||
this.children = []
|
this.children = []
|
||||||
this.shapes = []
|
this.shapes = []
|
||||||
}
|
}
|
||||||
// TODO
|
|
||||||
static fromJSON(json) {
|
static fromJSON(json) {
|
||||||
const layer = new Layer(json.idx);
|
const layer = new Layer(json.idx);
|
||||||
for (let i in json.children) {
|
for (let i in json.children) {
|
||||||
|
|
@ -2246,32 +2513,46 @@ class Layer extends Widget {
|
||||||
layer.children.push(GraphicsObject.fromJSON(child));
|
layer.children.push(GraphicsObject.fromJSON(child));
|
||||||
}
|
}
|
||||||
layer.name = json.name;
|
layer.name = json.name;
|
||||||
layer.frames = [];
|
|
||||||
for (let i in json.frames) {
|
// Load animation data if present (new system)
|
||||||
const frame = json.frames[i];
|
if (json.animationData) {
|
||||||
if (!frame) {
|
layer.animationData = AnimationData.fromJSON(json.animationData);
|
||||||
layer.frames.push(undefined)
|
}
|
||||||
continue;
|
|
||||||
}
|
// Load shapes if present
|
||||||
if (frame.frameType=="keyframe") {
|
if (json.shapes) {
|
||||||
layer.frames.push(Frame.fromJSON(frame));
|
layer.shapes = json.shapes.map(shape => Shape.fromJSON(shape));
|
||||||
} else {
|
}
|
||||||
if (layer.frames[layer.frames.length-1]) {
|
|
||||||
if (frame.frameType == "motion") {
|
// Load frames if present (old system - for backwards compatibility)
|
||||||
layer.frames[layer.frames.length-1].keyTypes.add("motion")
|
if (json.frames) {
|
||||||
} else if (frame.frameType == "shape") {
|
layer.frames = [];
|
||||||
layer.frames[layer.frames.length-1].keyTypes.add("shape")
|
for (let i in json.frames) {
|
||||||
}
|
const frame = json.frames[i];
|
||||||
|
if (!frame) {
|
||||||
|
layer.frames.push(undefined)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (frame.frameType=="keyframe") {
|
||||||
|
layer.frames.push(Frame.fromJSON(frame));
|
||||||
|
} else {
|
||||||
|
if (layer.frames[layer.frames.length-1]) {
|
||||||
|
if (frame.frameType == "motion") {
|
||||||
|
layer.frames[layer.frames.length-1].keyTypes.add("motion")
|
||||||
|
} else if (frame.frameType == "shape") {
|
||||||
|
layer.frames[layer.frames.length-1].keyTypes.add("shape")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layer.frames.push(undefined)
|
||||||
}
|
}
|
||||||
layer.frames.push(undefined)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layer.visible = json.visible;
|
layer.visible = json.visible;
|
||||||
layer.audible = json.audible;
|
layer.audible = json.audible;
|
||||||
|
|
||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
// TODO
|
|
||||||
toJSON(randomizeUuid = false) {
|
toJSON(randomizeUuid = false) {
|
||||||
const json = {};
|
const json = {};
|
||||||
json.type = "Layer";
|
json.type = "Layer";
|
||||||
|
|
@ -2285,38 +2566,112 @@ class Layer extends Widget {
|
||||||
json.children = [];
|
json.children = [];
|
||||||
let idMap = {}
|
let idMap = {}
|
||||||
for (let child of this.children) {
|
for (let child of this.children) {
|
||||||
// TODO: we may not need the randomizeUuid parameter anymore
|
|
||||||
let childJson = child.toJSON(randomizeUuid)
|
let childJson = child.toJSON(randomizeUuid)
|
||||||
idMap[child.idx] = childJson.idx
|
idMap[child.idx] = childJson.idx
|
||||||
json.children.push(childJson);
|
json.children.push(childJson);
|
||||||
}
|
}
|
||||||
json.frames = [];
|
|
||||||
for (let frame of this.frames) {
|
// Serialize animation data (new system)
|
||||||
if (frame) {
|
json.animationData = this.animationData.toJSON();
|
||||||
let frameJson = frame.toJSON(randomizeUuid)
|
|
||||||
for (let key in frameJson.keys) {
|
// If randomizing UUIDs, update the curve parameter keys to use new child IDs
|
||||||
if (key in idMap) {
|
if (randomizeUuid && json.animationData.curves) {
|
||||||
frameJson.keys[idMap[key]] = frameJson.keys[key]
|
const newCurves = {};
|
||||||
// delete frameJson.keys[key]
|
for (let paramKey in json.animationData.curves) {
|
||||||
|
// paramKey format: "childId.property"
|
||||||
|
const parts = paramKey.split('.');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const oldChildId = parts[0];
|
||||||
|
const property = parts.slice(1).join('.');
|
||||||
|
if (oldChildId in idMap) {
|
||||||
|
const newParamKey = `${idMap[oldChildId]}.${property}`;
|
||||||
|
newCurves[newParamKey] = json.animationData.curves[paramKey];
|
||||||
|
newCurves[newParamKey].parameter = newParamKey;
|
||||||
|
} else {
|
||||||
|
newCurves[paramKey] = json.animationData.curves[paramKey];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
newCurves[paramKey] = json.animationData.curves[paramKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
json.animationData.curves = newCurves;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize shapes
|
||||||
|
json.shapes = this.shapes.map(shape => shape.toJSON(randomizeUuid));
|
||||||
|
|
||||||
|
// Serialize frames (old system - for backwards compatibility)
|
||||||
|
if (this.frames) {
|
||||||
|
json.frames = [];
|
||||||
|
for (let frame of this.frames) {
|
||||||
|
if (frame) {
|
||||||
|
let frameJson = frame.toJSON(randomizeUuid)
|
||||||
|
for (let key in frameJson.keys) {
|
||||||
|
if (key in idMap) {
|
||||||
|
frameJson.keys[idMap[key]] = frameJson.keys[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
json.frames.push(frameJson);
|
||||||
|
} else {
|
||||||
|
json.frames.push(undefined)
|
||||||
}
|
}
|
||||||
json.frames.push(frameJson);
|
|
||||||
} else {
|
|
||||||
json.frames.push(undefined)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json.visible = this.visible;
|
json.visible = this.visible;
|
||||||
json.audible = this.audible;
|
json.audible = this.audible;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
getFrame(t) {
|
// Get all animated property values for all children at a given time
|
||||||
|
getAnimatedState(time) {
|
||||||
|
const state = {
|
||||||
|
shapes: [...this.shapes], // Base shapes from layer
|
||||||
|
childStates: {} // Animated states for each child GraphicsObject
|
||||||
|
};
|
||||||
|
|
||||||
|
// For each child, get its animated properties at this time
|
||||||
for (let child of this.children) {
|
for (let child of this.children) {
|
||||||
for (let prop of childProperties) {
|
const childState = {};
|
||||||
let animationCurve = this.animationData[`${child.idx}.${prop}`]
|
|
||||||
let value = interpolateAnimation(animationCurve, t)
|
// Animatable properties for GraphicsObjects
|
||||||
// and put it in a list and return it
|
const properties = ['x', 'y', 'rotation', 'scale_x', 'scale_y', 'exists', 'shapeIndex'];
|
||||||
|
|
||||||
|
for (let prop of properties) {
|
||||||
|
const paramKey = `${child.idx}.${prop}`;
|
||||||
|
const value = this.animationData.interpolate(paramKey, time);
|
||||||
|
|
||||||
|
if (value !== null) {
|
||||||
|
childState[prop] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(childState).length > 0) {
|
||||||
|
state.childStates[child.idx] = childState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to add a keyframe for a child's property
|
||||||
|
addKeyframeForChild(childId, property, time, value, interpolation = "linear") {
|
||||||
|
const paramKey = `${childId}.${property}`;
|
||||||
|
const keyframe = new Keyframe(time, value, interpolation);
|
||||||
|
this.animationData.addKeyframe(paramKey, keyframe);
|
||||||
|
return keyframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to remove a keyframe
|
||||||
|
removeKeyframeForChild(childId, property, keyframe) {
|
||||||
|
const paramKey = `${childId}.${property}`;
|
||||||
|
this.animationData.removeKeyframe(paramKey, keyframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get all keyframes for a child's property
|
||||||
|
getKeyframesForChild(childId, property) {
|
||||||
|
const paramKey = `${childId}.${property}`;
|
||||||
|
const curve = this.animationData.getCurve(paramKey);
|
||||||
|
return curve ? curve.keyframes : [];
|
||||||
}
|
}
|
||||||
getFrame(num) {
|
getFrame(num) {
|
||||||
if (this.frames[num]) {
|
if (this.frames[num]) {
|
||||||
|
|
@ -2611,13 +2966,36 @@ class Layer extends Widget {
|
||||||
t = (this.frameNum - frameInfo.prevIndex) / (frameInfo.nextIndex - frameInfo.prevIndex);
|
t = (this.frameNum - frameInfo.prevIndex) / (frameInfo.nextIndex - frameInfo.prevIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Draw shapes using AnimationData curves for exists and zOrder
|
||||||
|
let currentTime = context.activeObject?.currentTime || 0;
|
||||||
|
let visibleShapes = [];
|
||||||
|
|
||||||
|
for (let shape of this.shapes) {
|
||||||
|
// Check if shape exists at current time (>0 allows for future fade-in/out animations)
|
||||||
|
let existsValue = this.animationData.interpolate(`shape.${shape.idx}.exists`, currentTime);
|
||||||
|
if (existsValue !== null && existsValue > 0) {
|
||||||
|
let zOrder = this.animationData.interpolate(`shape.${shape.idx}.zOrder`, currentTime);
|
||||||
|
visibleShapes.push({ shape, zOrder: zOrder || 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by zOrder (lowest first = back, highest last = front)
|
||||||
|
visibleShapes.sort((a, b) => a.zOrder - b.zOrder);
|
||||||
|
|
||||||
|
// Draw sorted shapes
|
||||||
|
for (let { shape } of visibleShapes) {
|
||||||
|
cxt.selected = context.shapeselection.includes(shape);
|
||||||
|
shape.draw(cxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LEGACY: Keep old frame-based shape drawing for backwards compatibility
|
||||||
if (frame) {
|
if (frame) {
|
||||||
// Update shapes and children
|
// Update shapes and children from legacy Frame system
|
||||||
for (let shape of frame.shapes) {
|
for (let shape of frame.shapes) {
|
||||||
// If prev.frameType is "shape", look for a matching shape in next
|
// If prev.frameType is "shape", look for a matching shape in next
|
||||||
if (frameInfo.prev && frameInfo.prev.keyTypes.has("shape")) {
|
if (frameInfo.prev && frameInfo.prev.keyTypes.has("shape")) {
|
||||||
const shape2 = frameInfo.next.shapes.find(s => s.shapeId === shape.shapeId);
|
const shape2 = frameInfo.next.shapes.find(s => s.shapeId === shape.shapeId);
|
||||||
|
|
||||||
if (shape2) {
|
if (shape2) {
|
||||||
// If matching shape is found, interpolate and draw
|
// If matching shape is found, interpolate and draw
|
||||||
shape.lerpShape(shape2, t).draw(cxt);
|
shape.lerpShape(shape2, t).draw(cxt);
|
||||||
|
|
@ -2685,9 +3063,6 @@ class Layer extends Widget {
|
||||||
ctx.setTransform(transform)
|
ctx.setTransform(transform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.activeShape) {
|
|
||||||
this.activeShape.draw(cxt)
|
|
||||||
}
|
|
||||||
if (context.activeCurve) {
|
if (context.activeCurve) {
|
||||||
if (frame.shapes.indexOf(context.activeCurve.shape) != -1) {
|
if (frame.shapes.indexOf(context.activeCurve.shape) != -1) {
|
||||||
cxt.selected = true
|
cxt.selected = true
|
||||||
|
|
@ -2695,6 +3070,12 @@ class Layer extends Widget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Draw activeShape regardless of whether frame exists
|
||||||
|
if (this.activeShape) {
|
||||||
|
console.log("Layer.draw: Drawing activeShape", this.activeShape);
|
||||||
|
this.activeShape.draw(cxt)
|
||||||
|
console.log("Layer.draw: Drew activeShape");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bbox() {
|
bbox() {
|
||||||
let bbox = super.bbox()
|
let bbox = super.bbox()
|
||||||
|
|
@ -2711,15 +3092,19 @@ class Layer extends Widget {
|
||||||
return bbox
|
return bbox
|
||||||
}
|
}
|
||||||
mousedown(x, y) {
|
mousedown(x, y) {
|
||||||
|
console.log("Layer.mousedown called - this:", this.name, "activeLayer:", context.activeLayer?.name, "mode:", mode);
|
||||||
const mouse = {x: x, y: y}
|
const mouse = {x: x, y: y}
|
||||||
if (this==context.activeLayer) {
|
if (this==context.activeLayer) {
|
||||||
|
console.log("This IS the active layer");
|
||||||
switch(mode) {
|
switch(mode) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
case "draw":
|
case "draw":
|
||||||
|
console.log("Creating shape for mode:", mode);
|
||||||
this.clicked = true
|
this.clicked = true
|
||||||
this.activeShape = new Shape(x, y, context, uuidv4())
|
this.activeShape = new Shape(x, y, context, this, uuidv4())
|
||||||
this.lastMouse = mouse;
|
this.lastMouse = mouse;
|
||||||
|
console.log("Shape created:", this.activeShape);
|
||||||
break;
|
break;
|
||||||
case "select":
|
case "select":
|
||||||
case "transform":
|
case "transform":
|
||||||
|
|
@ -2798,7 +3183,9 @@ class Layer extends Widget {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
|
console.log("Layer.mousemove rectangle - activeShape:", this.activeShape);
|
||||||
if (this.activeShape) {
|
if (this.activeShape) {
|
||||||
|
console.log("Updating rectangle shape");
|
||||||
this.activeShape.clear();
|
this.activeShape.clear();
|
||||||
this.activeShape.addLine(x, this.activeShape.starty);
|
this.activeShape.addLine(x, this.activeShape.starty);
|
||||||
this.activeShape.addLine(x, y);
|
this.activeShape.addLine(x, y);
|
||||||
|
|
@ -2808,6 +3195,7 @@ class Layer extends Widget {
|
||||||
this.activeShape.starty,
|
this.activeShape.starty,
|
||||||
);
|
);
|
||||||
this.activeShape.update();
|
this.activeShape.update();
|
||||||
|
console.log("Rectangle updated");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
|
|
@ -2872,6 +3260,7 @@ class Layer extends Widget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mouseup(x, y) {
|
mouseup(x, y) {
|
||||||
|
console.log("Layer.mouseup called - mode:", mode, "activeShape:", this.activeShape);
|
||||||
this.clicked = false
|
this.clicked = false
|
||||||
if (this==context.activeLayer) {
|
if (this==context.activeLayer) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
|
|
@ -2883,7 +3272,9 @@ class Layer extends Widget {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
if (this.activeShape) {
|
if (this.activeShape) {
|
||||||
|
console.log("Adding shape via actions.addShape.create");
|
||||||
actions.addShape.create(context.activeObject, this.activeShape);
|
actions.addShape.create(context.activeObject, this.activeShape);
|
||||||
|
console.log("Shape added, clearing activeShape");
|
||||||
this.activeShape = undefined;
|
this.activeShape = undefined;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -3232,8 +3623,9 @@ class TempShape extends BaseShape {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Shape extends BaseShape {
|
class Shape extends BaseShape {
|
||||||
constructor(startx, starty, context, uuid = undefined, shapeId = undefined) {
|
constructor(startx, starty, context, parent, uuid = undefined, shapeId = undefined) {
|
||||||
super(startx, starty);
|
super(startx, starty);
|
||||||
|
this.parent = parent; // Reference to parent Layer (required)
|
||||||
this.vertices = [];
|
this.vertices = [];
|
||||||
this.triangles = [];
|
this.triangles = [];
|
||||||
this.fillStyle = context.fillStyle;
|
this.fillStyle = context.fillStyle;
|
||||||
|
|
@ -3695,7 +4087,8 @@ class GraphicsObject extends Widget {
|
||||||
pointerList[this.idx] = this;
|
pointerList[this.idx] = this;
|
||||||
this.name = this.idx;
|
this.name = this.idx;
|
||||||
|
|
||||||
this.currentFrameNum = 0;
|
this.currentFrameNum = 0; // LEGACY: kept for backwards compatibility
|
||||||
|
this.currentTime = 0; // New: continuous time for AnimationData curves
|
||||||
this.currentLayer = 0;
|
this.currentLayer = 0;
|
||||||
this.children = [new Layer(uuid + "-L1")];
|
this.children = [new Layer(uuid + "-L1")];
|
||||||
// this.layers = [new Layer(uuid + "-L1")];
|
// this.layers = [new Layer(uuid + "-L1")];
|
||||||
|
|
@ -5504,11 +5897,12 @@ function stage() {
|
||||||
// stageWrapper.appendChild(selectionRect)
|
// stageWrapper.appendChild(selectionRect)
|
||||||
// scroller.appendChild(stageWrapper)
|
// scroller.appendChild(stageWrapper)
|
||||||
stage.addEventListener("pointerdown", (e) => {
|
stage.addEventListener("pointerdown", (e) => {
|
||||||
|
console.log("POINTERDOWN EVENT - mode:", mode);
|
||||||
let mouse = getMousePos(stage, e);
|
let mouse = getMousePos(stage, e);
|
||||||
|
console.log("Mouse position:", mouse);
|
||||||
root.handleMouseEvent("mousedown", mouse.x, mouse.y)
|
root.handleMouseEvent("mousedown", mouse.x, mouse.y)
|
||||||
mouse = context.activeObject.transformMouse(mouse);
|
mouse = context.activeObject.transformMouse(mouse);
|
||||||
let selection;
|
let selection;
|
||||||
if (!context.activeObject.currentFrame?.exists) return;
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
|
|
@ -5806,7 +6200,6 @@ function stage() {
|
||||||
context.dragging = false;
|
context.dragging = false;
|
||||||
context.dragDirection = undefined;
|
context.dragDirection = undefined;
|
||||||
context.selectionRect = undefined;
|
context.selectionRect = undefined;
|
||||||
if (!context.activeObject.currentFrame?.exists) return;
|
|
||||||
let mouse = getMousePos(stage, e);
|
let mouse = getMousePos(stage, e);
|
||||||
root.handleMouseEvent("mouseup", mouse.x, mouse.y)
|
root.handleMouseEvent("mouseup", mouse.x, mouse.y)
|
||||||
mouse = context.activeObject.transformMouse(mouse);
|
mouse = context.activeObject.transformMouse(mouse);
|
||||||
|
|
@ -5894,7 +6287,6 @@ function stage() {
|
||||||
stage.mouseup(e);
|
stage.mouseup(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!context.activeObject.currentFrame?.exists) return;
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "draw":
|
case "draw":
|
||||||
stage.style.cursor = "default";
|
stage.style.cursor = "default";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue