From 3819988d5e4fc039f8a6d6d94062d30873c15d8c Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 00:11:55 -0500 Subject: [PATCH 01/18] initial refactoring work --- src/main.js | 1065 +++++++++++++++++++++++++++++++++++------------- src/utils.js | 24 +- src/widgets.js | 24 +- 3 files changed, 820 insertions(+), 293 deletions(-) diff --git a/src/main.js b/src/main.js index daadc63..6fe5e01 100644 --- a/src/main.js +++ b/src/main.js @@ -39,6 +39,7 @@ import { getRotatedBoundingBox, rotateAroundPointIncremental, rgbToHsv, + multiplyMatrices, } from "./utils.js"; import { backgroundColor, @@ -57,7 +58,7 @@ import { shadow, } from "./styles.js"; import { Icon } from "./icon.js"; -import { AlphaSelectionBar, ColorSelectorWidget, ColorWidget, HueSelectionBar, SaturationValueSelectionGradient } from "./widgets.js"; +import { AlphaSelectionBar, ColorSelectorWidget, ColorWidget, HueSelectionBar, SaturationValueSelectionGradient, Widget } from "./widgets.js"; const { writeTextFile: writeTextFile, readTextFile: readTextFile, @@ -1176,9 +1177,11 @@ let actions = { let addedFrames = {}; if (frameNum >= layer.frames.length) { formerType = "none"; - for (let i = layer.frames.length; i <= frameNum; i++) { - addedFrames[i] = uuidv4(); - } + // for (let i = layer.frames.length; i <= frameNum; i++) { + // addedFrames[i] = uuidv4(); + // } + } else if (!layer.frames[frameNum]) { + formerType = undefined } else if (layer.frames[frameNum].frameType != "keyframe") { formerType = layer.frames[frameNum].frameType; } else { @@ -1217,7 +1220,11 @@ let actions = { } } else { let layer = pointerList[action.layer]; - layer.frames[action.frameNum].frameType = action.formerType; + if (action.formerType) { + layer.frames[action.frameNum].frameType = action.formerType; + } else { + layer.frames[action.frameNum = undefined] + } } updateLayers(); updateUI(); @@ -1309,11 +1316,17 @@ let actions = { redoStack.length = 0; let frameNum = context.activeObject.currentFrameNum; let layer = context.activeObject.activeLayer; - let frames = layer.frames; - let { lastKeyframeBefore, firstKeyframeAfter } = getKeyframesSurrounding( - frames, - frameNum, - ); + + const frameInfo = layer.getFrameValue(frameNum) + let lastKeyframeBefore, firstKeyframeAfter + if (frameInfo.valueAtN) { + lastKeyframeBefore = frameNum + } else if (frameInfo.prev) { + lastKeyframeBefore = frameInfo.prevIndex + } else { + return + } + firstKeyframeAfter = frameInfo.nextIndex let action = { frameNum: frameNum, @@ -1328,13 +1341,9 @@ let actions = { execute: (action) => { let layer = pointerList[action.layer]; let frames = layer.frames; - if (action.lastBefore != undefined && action.firstAfter != undefined) { - layer.updateFrameNextAndPrev( - action.frameNum, - "motion", - action.lastBefore, - action.firstAfter, - ); + if (action.lastBefore != undefined) { + console.log("adding motion") + frames[action.lastBefore].keyTypes.add("motion") } updateLayers(); updateUI(); @@ -1342,13 +1351,8 @@ let actions = { rollback: (action) => { let layer = pointerList[action.layer]; let frames = layer.frames; - if (action.lastBefore != undefined && action.firstAfter != undefined) { - layer.updateFrameNextAndPrev( - action.frameNum, - "normal", - action.lastBefore, - action.firstAfter, - ); + if (action.lastBefore != undefined) { + frames[action.lastBefore].keyTypes.delete("motion") } updateLayers(); updateUI(); @@ -1359,11 +1363,18 @@ let actions = { redoStack.length = 0; let frameNum = context.activeObject.currentFrameNum; let layer = context.activeObject.activeLayer; - let frames = layer.frames; - let { lastKeyframeBefore, firstKeyframeAfter } = getKeyframesSurrounding( - frames, - frameNum, - ); + + const frameInfo = layer.getFrameValue(frameNum) + let lastKeyframeBefore, firstKeyframeAfter + if (frameInfo.valueAtN) { + lastKeyframeBefore = frameNum + } else if (frameInfo.prev) { + lastKeyframeBefore = frameInfo.prevIndex + } else { + return + } + firstKeyframeAfter = frameInfo.nextIndex + let action = { frameNum: frameNum, @@ -1371,6 +1382,7 @@ let actions = { lastBefore: lastKeyframeBefore, firstAfter: firstKeyframeAfter, }; + console.log(action) undoStack.push({ name: "addShapeTween", action: action }); actions.addShapeTween.execute(action); updateMenu(); @@ -1378,14 +1390,8 @@ let actions = { execute: (action) => { let layer = pointerList[action.layer]; let frames = layer.frames; - if (action.lastBefore != undefined && action.firstAfter != undefined) { - for (let i = action.lastBefore + 1; i < action.firstAfter; i++) { - frames[i].frameType = "shape"; - frames[i].prev = frames[action.lastBefore]; - frames[i].next = frames[action.firstAfter]; - frames[i].prevIndex = action.lastBefore; - frames[i].nextIndex = action.firstAfter; - } + if (action.lastBefore != undefined) { + frames[action.lastBefore].keyTypes.add("shape") } updateLayers(); updateUI(); @@ -1393,8 +1399,8 @@ let actions = { rollback: (action) => { let layer = pointerList[action.layer]; let frames = layer.frames; - for (let i = action.lastBefore + 1; i < action.firstAfter; i++) { - frames[i].frameType = "normal"; + if (action.lastBefore != undefined) { + frames[action.lastBefore].keyTypes.delete("shape") } updateLayers(); updateUI(); @@ -2029,6 +2035,7 @@ class Frame { this.keys = {}; this.shapes = []; this.frameType = frameType; + this.keyTypes = new Set() if (!uuid) { this.idx = uuidv4(); } else { @@ -2124,9 +2131,9 @@ class TempFrame { const tempFrame = new TempFrame(); -class Layer { +class Layer extends Widget { constructor(uuid) { - this.children = []; + super(0,0) if (!uuid) { this.idx = uuidv4(); } else { @@ -2134,6 +2141,7 @@ class Layer { } this.name = "Layer"; this.frames = [new Frame("keyframe", this.idx + "-F1")]; + this.frameNum = 0; this.visible = true; this.audible = true; pointerList[this.idx] = this; @@ -2368,25 +2376,25 @@ class Layer { return newLayer; } addFrame(num, frame, addedFrames) { - let updateDest = undefined; - if (!this.frames[num]) { - for (const [index, idx] of Object.entries(addedFrames)) { - if (!this.frames[index]) { - this.frames[index] = new Frame("normal", idx); - } - } - } else { - if (this.frames[num].frameType == "motion") { - updateDest = "motion"; - } else if (this.frames[num].frameType == "shape") { - updateDest = "shape"; - } - } + // let updateDest = undefined; + // if (!this.frames[num]) { + // for (const [index, idx] of Object.entries(addedFrames)) { + // if (!this.frames[index]) { + // this.frames[index] = new Frame("normal", idx); + // } + // } + // } else { + // if (this.frames[num].frameType == "motion") { + // updateDest = "motion"; + // } else if (this.frames[num].frameType == "shape") { + // updateDest = "shape"; + // } + // } this.frames[num] = frame; - if (updateDest) { - this.updateFrameNextAndPrev(num - 1, updateDest); - this.updateFrameNextAndPrev(num + 1, updateDest); - } + // if (updateDest) { + // this.updateFrameNextAndPrev(num - 1, updateDest); + // this.updateFrameNextAndPrev(num + 1, updateDest); + // } } addOrChangeFrame(num, frameType, uuid, addedFrames) { let latestFrame = this.getLatestFrame(num); @@ -2452,6 +2460,261 @@ class Layer { updateMenu(); updateLayers(); } + getFrameValue(n) { + const valueAtN = this.frames[n]; + if (valueAtN !== undefined) { + return { valueAtN, prev: null, next: null, prevIndex: null, nextIndex: null }; + } + let prev = n - 1; + let next = n + 1; + + while (prev >= 0 && this.frames[prev] === undefined) { + prev--; + } + while (next < this.frames.length && this.frames[next] === undefined) { + next++; + } + + return { + valueAtN: undefined, + prev: prev >= 0 ? this.frames[prev] : null, + next: next < this.frames.length ? this.frames[next] : null, + prevIndex: prev >= 0 ? prev : null, + nextIndex: next < this.frames.length ? next : null + }; + } + + draw(ctx) { + super.draw(ctx) + let frameInfo = this.getFrameValue(this.frameNum); + let frame = frameInfo.valueAtN !== undefined ? frameInfo.valueAtN : frameInfo.prev; + + // let frame = this.getFrame(this.currentFrameNum); + let cxt = {...context} + cxt.ctx = ctx + let t = null; + if (frameInfo.prev && frameInfo.next) { + t = (this.frameNum - frameInfo.prevIndex) / (frameInfo.nextIndex - frameInfo.prevIndex); + } + + if (frame) { + // Update shapes and children + for (let shape of frame.shapes) { + // If prev.frameType is "shape", look for a matching shape in next + if (frameInfo.prev && frameInfo.prev.keyTypes.has("shape")) { + const shape2 = frameInfo.next.shapes.find(s => s.shapeId === shape.shapeId); + + if (shape2) { + // If matching shape is found, interpolate and draw + shape.lerpShape(shape2, t).draw(cxt); + continue; // Skip to next shape + } + } + + // Otherwise, just draw the shape as usual + cxt.selected = context.shapeselection.includes(shape); + shape.draw(cxt); + } + + for (let child of this.children) { + if (child.idx in frame.keys) { + for (let key in frame.keys[child.idx]) { + // If both prev and next exist and prev.frameType is "motion", interpolate child keys + if (frameInfo.prev && frameInfo.next && frameInfo.prev.keyTypes.has("motion")) { + // Interpolate between prev and next for this key + child[key] = lerp(frameInfo.prev.keys[child.idx][key], frameInfo.next.keys[child.idx][key], t); + } else { + // Otherwise, use the value from the current frame + child[key] = frame.keys[child.idx][key]; + } + } + } + } + } + if (this.activeShape) { + this.activeShape.draw(cxt) + } + if (context.activeCurve) { + if (frame.shapes.indexOf(context.activeCurve.shape) != -1) { + cxt.selected = true + + } + } + } + mousedown(x, y) { + const mouse = {x: x, y: y} + switch(mode) { + case "rectangle": + case "ellipse": + case "draw": + this.clicked = true + this.activeShape = new Shape(x, y, context, uuidv4()) + this.lastMouse = mouse; + break; + case "select": + case "transform": + break; + case "paint_bucket": + debugCurves = []; + debugPoints = []; + let epsilon = context.fillGaps; + let regionPoints; + + // First, see if there's an existing shape to change the color of + // TODO: get this from self + let pointShape = getShapeAtPoint( + mouse, + context.activeObject.currentFrame.shapes, + ); + + if (pointShape) { + actions.colorShape.create(pointShape, context.fillStyle); + break; + } + + // We didn't find an existing region to paintbucket, see if we can make one + try { + regionPoints = floodFillRegion( + mouse, + epsilon, + config.fileWidth, + config.fileHeight, + context, + debugPoints, + debugPaintbucket, + ); + } catch (e) { + updateUI(); + throw e; + } + if (regionPoints.length > 0 && regionPoints.length < 10) { + // probably a very small area, rerun with minimum epsilon + regionPoints = floodFillRegion( + mouse, + 1, + config.fileWidth, + config.fileHeight, + context, + debugPoints, + ); + } + let points = []; + for (let point of regionPoints) { + points.push([point.x, point.y]); + } + let cxt = { + ...context, + fillShape: true, + strokeShape: false, + sendToBack: true, + }; + let shape = new Shape(regionPoints[0].x, regionPoints[0].y, cxt); + shape.fromPoints(points, 1); + actions.addShape.create(context.activeObject, shape, cxt); + break; + } + } + mousemove(x, y) { + const mouse = {x: x, y: y} + switch (mode) { + case "draw": + if (this.activeShape) { + if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { + this.activeShape.addLine(x, y); + this.lastMouse = mouse; + } + } + break; + case "rectangle": + if (this.activeShape) { + this.activeShape.clear(); + this.activeShape.addLine(x, this.activeShape.starty); + this.activeShape.addLine(x, y); + this.activeShape.addLine(this.activeShape.startx, y); + this.activeShape.addLine( + this.activeShape.startx, + this.activeShape.starty, + ); + this.activeShape.update(); + } + break; + case "ellipse": + if (this.activeShape) { + let midX = (mouse.x + this.activeShape.startx) / 2; + let midY = (mouse.y + this.activeShape.starty) / 2; + let xDiff = (mouse.x - this.activeShape.startx) / 2; + let yDiff = (mouse.y - this.activeShape.starty) / 2; + let ellipseConst = 0.552284749831; // (4/3)*tan(pi/(2n)) where n=4 + this.activeShape.clear(); + this.activeShape.addCurve( + new Bezier( + midX, + this.activeShape.starty, + midX + ellipseConst * xDiff, + this.activeShape.starty, + mouse.x, + midY - ellipseConst * yDiff, + mouse.x, + midY, + ), + ); + this.activeShape.addCurve( + new Bezier( + mouse.x, + midY, + mouse.x, + midY + ellipseConst * yDiff, + midX + ellipseConst * xDiff, + mouse.y, + midX, + mouse.y, + ), + ); + this.activeShape.addCurve( + new Bezier( + midX, + mouse.y, + midX - ellipseConst * xDiff, + mouse.y, + this.activeShape.startx, + midY + ellipseConst * yDiff, + this.activeShape.startx, + midY, + ), + ); + this.activeShape.addCurve( + new Bezier( + this.activeShape.startx, + midY, + this.activeShape.startx, + midY - ellipseConst * yDiff, + midX - ellipseConst * xDiff, + this.activeShape.starty, + midX, + this.activeShape.starty, + ), + ); + } + break; + } + } + mouseup(x, y) { + this.clicked = false + switch (mode) { + case "draw": + if (this.activeShape) { + this.activeShape.addLine(x, y); + this.activeShape.simplify(context.simplifyMode); + } + case "rectangle": + case "ellipse": + if (this.activeShape) { + actions.addShape.create(context.activeObject, this.activeShape); + this.activeShape = undefined; + } + break; + } + } } class AudioLayer { @@ -2605,24 +2868,27 @@ class BaseShape { ctx.fill() } } + function drawCurve(curve, selected) { + ctx.strokeStyle = curve.color; + ctx.beginPath(); + ctx.moveTo(curve.points[0].x, curve.points[0].y); + ctx.bezierCurveTo( + curve.points[1].x, + curve.points[1].y, + curve.points[2].x, + curve.points[2].y, + curve.points[3].x, + curve.points[3].y, + ); + ctx.stroke(); + if (selected) { + ctx.strokeStyle = pattern + ctx.stroke() + } + } if (this.stroked && !context.debugColor) { for (let curve of this.curves) { - ctx.strokeStyle = curve.color; - ctx.beginPath(); - ctx.moveTo(curve.points[0].x, curve.points[0].y); - ctx.bezierCurveTo( - curve.points[1].x, - curve.points[1].y, - curve.points[2].x, - curve.points[2].y, - curve.points[3].x, - curve.points[3].y, - ); - ctx.stroke(); - if (context.selected) { - ctx.strokeStyle = pattern - ctx.stroke() - } + drawCurve(curve, context.selected) // // Debug, show curve control points // ctx.beginPath() @@ -2632,11 +2898,115 @@ class BaseShape { // ctx.fill() } } + if (context.activeCurve && this==context.activeCurve.shape) { + drawCurve(context.activeCurve.current, true) + } + if (context.activeVertex && this==context.activeVertex.shape) { + const curves = { + ...context.activeVertex.current.startCurves, + ...context.activeVertex.current.endCurves + } + for (let i in curves) { + let curve = curves[i] + drawCurve(curve, true) + } + ctx.fillStyle = "#000000aa"; + ctx.beginPath(); + let vertexSize = 15 / context.zoomLevel; + ctx.rect( + context.activeVertex.current.point.x - vertexSize / 2, + context.activeVertex.current.point.y - vertexSize / 2, + vertexSize, + vertexSize, + ); + ctx.fill(); + } // Debug, show quadtree if (debugQuadtree && this.quadtree && !context.debugColor) { this.quadtree.draw(ctx); } } + lerpShape(shape2, t) { + if (this.curves.length == 0) return this; + let path1 = [ + { + type: "M", + x: this.curves[0].points[0].x, + y: this.curves[0].points[0].y, + }, + ]; + for (let curve of this.curves) { + path1.push({ + type: "C", + x1: curve.points[1].x, + y1: curve.points[1].y, + x2: curve.points[2].x, + y2: curve.points[2].y, + x: curve.points[3].x, + y: curve.points[3].y, + }); + } + let path2 = []; + if (shape2.curves.length > 0) { + path2.push({ + type: "M", + x: shape2.curves[0].points[0].x, + y: shape2.curves[0].points[0].y, + }); + for (let curve of shape2.curves) { + path2.push({ + type: "C", + x1: curve.points[1].x, + y1: curve.points[1].y, + x2: curve.points[2].x, + y2: curve.points[2].y, + x: curve.points[3].x, + y: curve.points[3].y, + }); + } + } + const interpolator = d3.interpolatePathCommands(path1, path2); + let current = interpolator(t); + let curves = []; + let start = current.shift(); + let { x, y } = start; + for (let curve of current) { + curves.push( + new Bezier( + x, + y, + curve.x1, + curve.y1, + curve.x2, + curve.y2, + curve.x, + curve.y, + ), + ); + x = curve.x; + y = curve.y; + } + let lineWidth = lerp(this.lineWidth, shape2.lineWidth, t); + let strokeStyle = lerpColor( + this.strokeStyle, + shape2.strokeStyle, + t, + ); + let fillStyle; + if (!this.fillImage) { + fillStyle = lerpColor(this.fillStyle, shape2.fillStyle, t); + } + return new TempShape( + start.x, + start.y, + curves, + lineWidth, + this.stroked, + this.filled, + strokeStyle, + fillStyle, + ) + } } class TempShape extends BaseShape { @@ -3096,10 +3466,9 @@ class Shape extends BaseShape { } } -class GraphicsObject { +class GraphicsObject extends Widget { constructor(uuid) { - this.x = 0; - this.y = 0; + super(0, 0) this.rotation = 0; // in radians this.scale_x = 1; this.scale_y = 1; @@ -3113,7 +3482,8 @@ class GraphicsObject { this.currentFrameNum = 0; this.currentLayer = 0; - this.layers = [new Layer(uuid + "-L1")]; + this.children = [new Layer(uuid + "-L1")]; + // this.layers = [new Layer(uuid + "-L1")]; this.audioLayers = []; // this.children = [] @@ -3168,8 +3538,11 @@ class GraphicsObject { get activeLayer() { return this.layers[this.currentLayer]; } - get children() { - return this.activeLayer.children; + // get children() { + // return this.activeLayer.children; + // } + get layers() { + return this.children } get allLayers() { return [...this.audioLayers, ...this.layers]; @@ -3201,6 +3574,7 @@ class GraphicsObject { num = Math.max(0, num); for (let layer of this.layers) { this.currentFrameNum = num; + layer.frameNum = num let frame = layer.getFrame(num); for (let child of this.children) { let idx = child.idx; @@ -3269,6 +3643,7 @@ class GraphicsObject { bbox.y.max += this.y; return bbox; } + /* draw(context, calculateTransform=false) { let ctx = context.ctx; ctx.save(); @@ -3445,7 +3820,7 @@ class GraphicsObject { } } ctx.restore(); - } + }*/ transformCanvas(ctx) { if (this.parent) { this.parent.transformCanvas(ctx) @@ -3455,18 +3830,72 @@ class GraphicsObject { ctx.scale(this.scale_x, this.scale_y); } transformMouse(mouse) { - if (this.parent) { - mouse = this.parent.transformMouse(mouse); + // Apply the transformation matrix to the mouse position + let matrix = this.generateTransformMatrix(); + let { x, y } = mouse; + + return { + x: matrix[0][0] * x + matrix[0][1] * y + matrix[0][2], + y: matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] + }; + } + generateTransformMatrix() { + // Start with the parent's transform matrix if it exists + let parentMatrix = this.parent ? this.parent.generateTransformMatrix() : [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; + + // Calculate the rotation matrix components + const cos = Math.cos(this.rotation); + const sin = Math.sin(this.rotation); + + // Scaling matrix + const scaleMatrix = [ + [1/this.scale_x, 0, 0], + [0, 1/this.scale_y, 0], + [0, 0, 1] + ]; + + // Rotation matrix (inverse rotation for transforming back) + const rotationMatrix = [ + [cos, -sin, 0], + [sin, cos, 0], + [0, 0, 1] + ]; + + // Translation matrix (inverse translation to adjust for object's position) + const translationMatrix = [ + [1, 0, -this.x], + [0, 1, -this.y], + [0, 0, 1] + ]; + + // Multiply translation * rotation * scaling to get the current object's final transformation matrix + let tempMatrix = multiplyMatrices(translationMatrix, rotationMatrix); + let objectMatrix = multiplyMatrices(tempMatrix, scaleMatrix); + + // Now combine with the parent's matrix (parent * object) + let finalMatrix = multiplyMatrices(parentMatrix, objectMatrix); + + return finalMatrix; + } + handleMouseEvent(eventType, x, y) { + for (let i in this.layers) { + if (i==this.currentLayer) { + this.layers[i]._globalEvents.add("mousedown") + this.layers[i]._globalEvents.add("mousemove") + this.layers[i]._globalEvents.add("mouseup") + } else { + this.layers[i]._globalEvents.delete("mousedown") + this.layers[i]._globalEvents.delete("mousemove") + this.layers[i]._globalEvents.delete("mouseup") + } } - mouse.x -= this.x; - mouse.y -= this.y; - return mouse; + super.handleMouseEvent(eventType, x, y) } addObject(object, x = 0, y = 0, frame = undefined) { if (frame == undefined) { frame = this.currentFrame; } - this.children.push(object); + // this.children.push(object); object.parent = this; object.x = x; object.y = y; @@ -3487,13 +3916,13 @@ class GraphicsObject { delete frame[idx]; } } - this.children.splice(this.children.indexOf(childObject), 1); + // this.children.splice(this.children.indexOf(childObject), 1); } addLayer(layer) { - this.layers.push(layer); + this.children.push(layer); } removeLayer(layer) { - this.layers.splice(this.layers.indexOf(layer), 1); + this.children.splice(this.children.indexOf(layer), 1); } saveState() { startProps[this.idx] = { @@ -4508,6 +4937,7 @@ function stage() { // scroller.appendChild(stageWrapper) stage.addEventListener("mousedown", (e) => { let mouse = getMousePos(stage, e); + root.handleMouseEvent("mousedown", mouse.x, mouse.y) mouse = context.activeObject.transformMouse(mouse); let selection; if (!context.activeObject.currentFrame?.exists) return; @@ -4515,9 +4945,9 @@ function stage() { case "rectangle": case "ellipse": case "draw": - context.mouseDown = true; - context.activeShape = new Shape(mouse.x, mouse.y, context, uuidv4()); - context.lastMouse = mouse; + // context.mouseDown = true; + // context.activeShape = new Shape(mouse.x, mouse.y, context, uuidv4()); + // context.lastMouse = mouse; break; case "select": if (context.activeObject.currentFrame.frameType != "keyframe") break; @@ -4693,73 +5123,73 @@ function stage() { break; case "paint_bucket": let line = { p1: mouse, p2: { x: mouse.x + 3000, y: mouse.y } }; - debugCurves = []; - debugPoints = []; - let epsilon = context.fillGaps; - let min_x = Infinity; - let curveB = undefined; - let point = undefined; - let regionPoints; + // debugCurves = []; + // debugPoints = []; + // let epsilon = context.fillGaps; + // let min_x = Infinity; + // let curveB = undefined; + // let point = undefined; + // let regionPoints; - // First, see if there's an existing shape to change the color of - const startTime = performance.now(); - let pointShape = getShapeAtPoint( - mouse, - context.activeObject.currentFrame.shapes, - ); - const endTime = performance.now(); + // // First, see if there's an existing shape to change the color of + // const startTime = performance.now(); + // let pointShape = getShapeAtPoint( + // mouse, + // context.activeObject.currentFrame.shapes, + // ); + // const endTime = performance.now(); - console.log( - `getShapeAtPoint took ${endTime - startTime} milliseconds.`, - ); + // console.log( + // `getShapeAtPoint took ${endTime - startTime} milliseconds.`, + // ); - if (pointShape) { - actions.colorShape.create(pointShape, context.fillStyle); - break; - } + // if (pointShape) { + // actions.colorShape.create(pointShape, context.fillStyle); + // break; + // } - // We didn't find an existing region to paintbucket, see if we can make one - const offset = context.activeObject.transformMouse({x:0, y:0}) - try { - regionPoints = floodFillRegion( - mouse, - epsilon, - offset, - config.fileWidth, - config.fileHeight, - context, - debugPoints, - debugPaintbucket, - ); - } catch (e) { - updateUI(); - throw e; - } - if (regionPoints.length > 0 && regionPoints.length < 10) { - // probably a very small area, rerun with minimum epsilon - regionPoints = floodFillRegion( - mouse, - 1, - offset, - config.fileWidth, - config.fileHeight, - context, - debugPoints, - ); - } - let points = []; - for (let point of regionPoints) { - points.push([point.x, point.y]); - } - let cxt = { - ...context, - fillShape: true, - strokeShape: false, - sendToBack: true, - }; - let shape = new Shape(regionPoints[0].x, regionPoints[0].y, cxt); - shape.fromPoints(points, 1); - actions.addShape.create(context.activeObject, shape, cxt); + // // We didn't find an existing region to paintbucket, see if we can make one + // const offset = context.activeObject.transformMouse({x:0, y:0}) + // try { + // regionPoints = floodFillRegion( + // mouse, + // epsilon, + // offset, + // config.fileWidth, + // config.fileHeight, + // context, + // debugPoints, + // debugPaintbucket, + // ); + // } catch (e) { + // updateUI(); + // throw e; + // } + // if (regionPoints.length > 0 && regionPoints.length < 10) { + // // probably a very small area, rerun with minimum epsilon + // regionPoints = floodFillRegion( + // mouse, + // 1, + // offset, + // config.fileWidth, + // config.fileHeight, + // context, + // debugPoints, + // ); + // } + // let points = []; + // for (let point of regionPoints) { + // points.push([point.x, point.y]); + // } + // let cxt = { + // ...context, + // fillShape: true, + // strokeShape: false, + // sendToBack: true, + // }; + // let shape = new Shape(regionPoints[0].x, regionPoints[0].y, cxt); + // shape.fromPoints(points, 1); + // actions.addShape.create(context.activeObject, shape, cxt); break; // Loop labels in JS! // Iterate in reverse so we paintbucket the frontmost shape @@ -4810,20 +5240,21 @@ function stage() { context.selectionRect = undefined; if (!context.activeObject.currentFrame?.exists) return; let mouse = getMousePos(stage, e); + root.handleMouseEvent("mouseup", mouse.x, mouse.y) mouse = context.activeObject.transformMouse(mouse); switch (mode) { case "draw": - if (context.activeShape) { - context.activeShape.addLine(mouse.x, mouse.y); - context.activeShape.simplify(context.simplifyMode); - actions.addShape.create(context.activeObject, context.activeShape); - context.activeShape = undefined; - } + // if (context.activeShape) { + // context.activeShape.addLine(mouse.x, mouse.y); + // context.activeShape.simplify(context.simplifyMode); + // actions.addShape.create(context.activeObject, context.activeShape); + // context.activeShape = undefined; + // } break; case "rectangle": case "ellipse": - actions.addShape.create(context.activeObject, context.activeShape); - context.activeShape = undefined; + // actions.addShape.create(context.activeObject, context.activeShape); + // context.activeShape = undefined; break; case "select": if (context.activeAction) { @@ -4881,6 +5312,7 @@ function stage() { stage.addEventListener("mouseup", stage.mouseup); stage.addEventListener("mousemove", (e) => { let mouse = getMousePos(stage, e); + root.handleMouseEvent("mousemove", mouse.x, mouse.y) mouse = context.activeObject.transformMouse(mouse); context.mousePos = mouse; // if mouse is released, even if it happened outside the stage @@ -4909,78 +5341,78 @@ function stage() { case "rectangle": stage.style.cursor = "default"; context.activeCurve = undefined; - if (context.activeShape) { - context.activeShape.clear(); - context.activeShape.addLine(mouse.x, context.activeShape.starty); - context.activeShape.addLine(mouse.x, mouse.y); - context.activeShape.addLine(context.activeShape.startx, mouse.y); - context.activeShape.addLine( - context.activeShape.startx, - context.activeShape.starty, - ); - context.activeShape.update(); - } - break; + // if (context.activeShape) { + // context.activeShape.clear(); + // context.activeShape.addLine(mouse.x, context.activeShape.starty); + // context.activeShape.addLine(mouse.x, mouse.y); + // context.activeShape.addLine(context.activeShape.startx, mouse.y); + // context.activeShape.addLine( + // context.activeShape.startx, + // context.activeShape.starty, + // ); + // context.activeShape.update(); + // } + // break; case "ellipse": stage.style.cursor = "default"; context.activeCurve = undefined; - if (context.activeShape) { - let midX = (mouse.x + context.activeShape.startx) / 2; - let midY = (mouse.y + context.activeShape.starty) / 2; - let xDiff = (mouse.x - context.activeShape.startx) / 2; - let yDiff = (mouse.y - context.activeShape.starty) / 2; - let ellipseConst = 0.552284749831; // (4/3)*tan(pi/(2n)) where n=4 - context.activeShape.clear(); - context.activeShape.addCurve( - new Bezier( - midX, - context.activeShape.starty, - midX + ellipseConst * xDiff, - context.activeShape.starty, - mouse.x, - midY - ellipseConst * yDiff, - mouse.x, - midY, - ), - ); - context.activeShape.addCurve( - new Bezier( - mouse.x, - midY, - mouse.x, - midY + ellipseConst * yDiff, - midX + ellipseConst * xDiff, - mouse.y, - midX, - mouse.y, - ), - ); - context.activeShape.addCurve( - new Bezier( - midX, - mouse.y, - midX - ellipseConst * xDiff, - mouse.y, - context.activeShape.startx, - midY + ellipseConst * yDiff, - context.activeShape.startx, - midY, - ), - ); - context.activeShape.addCurve( - new Bezier( - context.activeShape.startx, - midY, - context.activeShape.startx, - midY - ellipseConst * yDiff, - midX - ellipseConst * xDiff, - context.activeShape.starty, - midX, - context.activeShape.starty, - ), - ); - } - break; + // if (context.activeShape) { + // let midX = (mouse.x + context.activeShape.startx) / 2; + // let midY = (mouse.y + context.activeShape.starty) / 2; + // let xDiff = (mouse.x - context.activeShape.startx) / 2; + // let yDiff = (mouse.y - context.activeShape.starty) / 2; + // let ellipseConst = 0.552284749831; // (4/3)*tan(pi/(2n)) where n=4 + // context.activeShape.clear(); + // context.activeShape.addCurve( + // new Bezier( + // midX, + // context.activeShape.starty, + // midX + ellipseConst * xDiff, + // context.activeShape.starty, + // mouse.x, + // midY - ellipseConst * yDiff, + // mouse.x, + // midY, + // ), + // ); + // context.activeShape.addCurve( + // new Bezier( + // mouse.x, + // midY, + // mouse.x, + // midY + ellipseConst * yDiff, + // midX + ellipseConst * xDiff, + // mouse.y, + // midX, + // mouse.y, + // ), + // ); + // context.activeShape.addCurve( + // new Bezier( + // midX, + // mouse.y, + // midX - ellipseConst * xDiff, + // mouse.y, + // context.activeShape.startx, + // midY + ellipseConst * yDiff, + // context.activeShape.startx, + // midY, + // ), + // ); + // context.activeShape.addCurve( + // new Bezier( + // context.activeShape.startx, + // midY, + // context.activeShape.startx, + // midY - ellipseConst * yDiff, + // midX - ellipseConst * xDiff, + // context.activeShape.starty, + // midX, + // context.activeShape.starty, + // ), + // ); + // } + // break; case "select": stage.style.cursor = "default"; if (context.dragging) { @@ -6099,7 +6531,8 @@ function renderUI() { ctx.fillRect(0, 0, config.fileWidth, config.fileHeight); context.ctx = ctx; - root.draw(context); + // root.draw(context); + root.draw(ctx) if (context.activeObject != root) { ctx.fillStyle = "rgba(255,255,255,0.5)"; ctx.fillRect(0, 0, config.fileWidth, config.fileHeight); @@ -6316,57 +6749,113 @@ function renderLayers() { } // Draw existing frames if (layer instanceof Layer) { - layer.frames.forEach((frame, j) => { - if (!frame) return; - switch (frame.frameType) { - case "keyframe": - ctx.fillStyle = foregroundColor; - drawBorderedRect( - ctx, - j * frameWidth, - 0, - frameWidth, - layerHeight, - highlight, - shadow, - shadow, - shadow, - ); - ctx.fillStyle = "#111"; - ctx.beginPath(); - ctx.arc( - (j + 0.5) * frameWidth, - layerHeight * 0.75, - frameWidth * 0.25, - 0, - 2 * Math.PI, - ); - ctx.fill(); - break; - case "normal": - ctx.fillStyle = foregroundColor; - drawBorderedRect( - ctx, - j * frameWidth, - 0, - frameWidth, - layerHeight, - highlight, - shadow, - backgroundColor, - backgroundColor, - ); - break; - case "motion": - ctx.fillStyle = "#7a00b3"; - ctx.fillRect(j * frameWidth, 0, frameWidth, layerHeight); - break; - case "shape": - ctx.fillStyle = "#9bff9b"; - ctx.fillRect(j * frameWidth, 0, frameWidth, layerHeight); - break; + for (let j=0; j { + // if (!frame) return; + // switch (frame.frameType) { + // case "keyframe": + // ctx.fillStyle = foregroundColor; + // drawBorderedRect( + // ctx, + // j * frameWidth, + // 0, + // frameWidth, + // layerHeight, + // highlight, + // shadow, + // shadow, + // shadow, + // ); + // ctx.fillStyle = "#111"; + // ctx.beginPath(); + // ctx.arc( + // (j + 0.5) * frameWidth, + // layerHeight * 0.75, + // frameWidth * 0.25, + // 0, + // 2 * Math.PI, + // ); + // ctx.fill(); + // break; + // case "normal": + // ctx.fillStyle = foregroundColor; + // drawBorderedRect( + // ctx, + // j * frameWidth, + // 0, + // frameWidth, + // layerHeight, + // highlight, + // shadow, + // backgroundColor, + // backgroundColor, + // ); + // break; + // case "motion": + // ctx.fillStyle = "#7a00b3"; + // ctx.fillRect(j * frameWidth, 0, frameWidth, layerHeight); + // break; + // case "shape": + // ctx.fillStyle = "#9bff9b"; + // ctx.fillRect(j * frameWidth, 0, frameWidth, layerHeight); + // break; + // } + // }); } else if (layer instanceof AudioLayer) { // TODO: split waveform into chunks for (let i in layer.sounds) { @@ -7054,12 +7543,14 @@ async function renderMenu() { deleteFrameMenuItem, { text: "Add Motion Tween", - enabled: activeFrame && !activeKeyframe, + enabled: true, + // enabled: activeFrame && !activeKeyframe, action: actions.addMotionTween.create, }, { text: "Add Shape Tween", - enabled: activeFrame && !activeKeyframe, + enabled: true, + // enabled: activeFrame && !activeKeyframe, action: actions.addShapeTween.create, }, { diff --git a/src/utils.js b/src/utils.js index 5b9ec5e..19707a2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -145,10 +145,27 @@ function generateWaveform(img, buffer, imgHeight, frameWidth, framesPerSecond) { img.src = dataUrl; } +function multiplyMatrices(a, b) { + let result = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0] + ]; + + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + for (let k = 0; k < 3; k++) { + result[i][j] += a[i][k] * b[k][j]; + } + } + } + + return result; +} + function floodFillRegion( startPoint, epsilon, - offset, // TODO: this needs to be a generalized transform fileWidth, fileHeight, context, @@ -156,8 +173,10 @@ function floodFillRegion( debugPaintbucket) { // Helper function to check if the point is at the boundary of the region function isBoundaryPoint(point) { + return point.x <= 0 || point.x >= fileWidth || + point.y <= 0 || point.y >= fileHeight; return point.x <= offset.x || point.x >= offset.x + fileWidth || - point.y <= offset.y || point.y >= offset.y + fileHeight; + point.y <= offset.y || point.y >= offset.y + fileHeight; } let halfEpsilon = epsilon/2 @@ -873,6 +892,7 @@ export { camelToWords, generateWaveform, floodFillRegion, + multiplyMatrices, getShapeAtPoint, hslToRgb, hsvToRgb, diff --git a/src/widgets.js b/src/widgets.js index d53d533..4ac3820 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -5,12 +5,26 @@ class Widget { this._globalEvents = new Set() this.x = x this.y = y + this.rotation = 0 this.children = [] } handleMouseEvent(eventType, x, y) { for (let child of this.children) { - if (child.hitTest(x, y) || child._globalEvents.has(eventType)) { - child.handleMouseEvent(eventType, x-child.x, y-child.y) + // Adjust for translation + const dx = x - child.x; + const dy = y - child.y; + + // Apply inverse rotation + const cosTheta = Math.cos(child.rotation); + const sinTheta = Math.sin(child.rotation); + + // Rotate coordinates to child's local space + const rotatedX = dx * cosTheta + dy * sinTheta; + const rotatedY = -dx * sinTheta + dy * cosTheta; + + // First, perform hit test using original (global) coordinates + if (child.hitTest(rotatedX, rotatedY) || child._globalEvents.has(eventType)) { + child.handleMouseEvent(eventType, rotatedX, rotatedY); } } const eventTypes = [ @@ -26,8 +40,9 @@ class Widget { } } hitTest(x, y) { - if ((x >= this.x) && (x <= this.x+this.width) && - (y >= this.y) && (y <= this.y+this.height)) { + // if ((x >= this.x) && (x <= this.x+this.width) && + // (y >= this.y) && (y <= this.y+this.height)) { + if ((x>=0) && (x <= this.width) && (y >= 0) && (y <= this.height)) { return true } return false @@ -36,6 +51,7 @@ class Widget { for (let child of this.children) { const transform = ctx.getTransform() ctx.translate(child.x, child.y) + ctx.rotate(child.rotation) child.draw(ctx) ctx.setTransform(transform) } From 94f887efb1c24da377cd8841dcf684466c31bf09 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 01:33:15 -0500 Subject: [PATCH 02/18] Fix bounding box issues --- src/main.js | 105 ++++++++++++++++++++++++++++++++++++------------- src/widgets.js | 31 ++++++++++++++- 2 files changed, 107 insertions(+), 29 deletions(-) diff --git a/src/main.js b/src/main.js index 6fe5e01..de97ecd 100644 --- a/src/main.js +++ b/src/main.js @@ -1437,6 +1437,7 @@ let actions = { groupUuid: uuidv4(), parent: context.activeObject.idx, frame: context.activeObject.currentFrame.idx, + layer: context.activeObject.activeLayer.idx, position: { x: (bbox.x.min + bbox.x.max) / 2, y: (bbox.y.min + bbox.y.max) / 2, @@ -1452,6 +1453,21 @@ let actions = { let frame = action.frame ? pointerList[action.frame] : parent.currentFrame; + let layer; + if (action.layer) { + layer = pointerList[action.layer] + } else { + for (let _layer of parent.layers) { + for (let _frame of _layer.frames) { + if (_frame.idx == frame.idx) { + layer = _layer + } + } + } + if (layer==undefined) { + layer = parent.activeLayer + } + } for (let shapeIdx of action.shapes) { let shape = pointerList[shapeIdx]; shape.translate(-action.position.x, -action.position.y); @@ -2485,7 +2501,7 @@ class Layer extends Widget { } draw(ctx) { - super.draw(ctx) + // super.draw(ctx) let frameInfo = this.getFrameValue(this.frameNum); let frame = frameInfo.valueAtN !== undefined ? frameInfo.valueAtN : frameInfo.prev; @@ -2531,6 +2547,21 @@ class Layer extends Widget { } } } + for (let child of this.children) { + const transform = ctx.getTransform() + ctx.translate(child.x, child.y) + ctx.rotate(child.rotation) + child.draw(ctx) + if (context.selection.includes(child)) { + ctx.lineWidth = 1; + ctx.strokeStyle = "#00ffff"; + ctx.beginPath(); + let bbox = child.bbox() + ctx.rect(bbox.x.min-child.x, bbox.y.min-child.y, bbox.x.max-bbox.x.min, bbox.y.max-bbox.y.min) + ctx.stroke() + } + ctx.setTransform(transform) + } if (this.activeShape) { this.activeShape.draw(cxt) } @@ -2541,6 +2572,19 @@ class Layer extends Widget { } } } + bbox() { + let bbox = super.bbox() + const frameInfo = this.getFrameValue(this.frameNum); + const frame = frameInfo.valueAtN + if (!frame) return bbox; + if (frame.shapes.length > 0 && bbox == undefined) { + bbox = structuredClone(frame.shapes[0].boundingBox); + } + for (let shape of frame.shapes) { + growBoundingBox(bbox, shape.boundingBox); + } + return bbox + } mousedown(x, y) { const mouse = {x: x, y: y} switch(mode) { @@ -2970,19 +3014,20 @@ class BaseShape { let curves = []; let start = current.shift(); let { x, y } = start; + let bezier; for (let curve of current) { - curves.push( - new Bezier( - x, - y, - curve.x1, - curve.y1, - curve.x2, - curve.y2, - curve.x, - curve.y, - ), - ); + bezier = new Bezier( + x, + y, + curve.x1, + curve.y1, + curve.x2, + curve.y2, + curve.x, + curve.y, + ) + bezier.color = lerpColor(this.strokeStyle, shape2.strokeStyle) + curves.push(bezier); x = curve.x; y = curve.y; } @@ -3615,15 +3660,15 @@ class GraphicsObject extends Widget { } bbox() { let bbox; - for (let layer of this.layers) { - let frame = layer.getFrame(this.currentFrameNum); - if (frame.shapes.length > 0 && bbox == undefined) { - bbox = structuredClone(frame.shapes[0].boundingBox); - } - for (let shape of frame.shapes) { - growBoundingBox(bbox, shape.boundingBox); - } - } + // for (let layer of this.layers) { + // let frame = layer.getFrame(this.currentFrameNum); + // if (frame.shapes.length > 0 && bbox == undefined) { + // bbox = structuredClone(frame.shapes[0].boundingBox); + // } + // for (let shape of frame.shapes) { + // growBoundingBox(bbox, shape.boundingBox); + // } + // } if (this.children.length > 0) { if (!bbox) { bbox = structuredClone(this.children[0].bbox()); @@ -3891,11 +3936,15 @@ class GraphicsObject extends Widget { } super.handleMouseEvent(eventType, x, y) } - addObject(object, x = 0, y = 0, frame = undefined) { + addObject(object, x = 0, y = 0, frame = undefined, layer=undefined) { if (frame == undefined) { frame = this.currentFrame; } + if (layer==undefined) { + layer = this.activeLayer + } // this.children.push(object); + layer.children.push(object) object.parent = this; object.x = x; object.y = y; @@ -4999,11 +5048,11 @@ function stage() { if (!context.dragging) { // Have to iterate in reverse order to grab the frontmost object when two overlap for ( - let i = context.activeObject.children.length - 1; + let i = context.activeObject.activeLayer.children.length - 1; i >= 0; i-- ) { - child = context.activeObject.children[i]; + child = context.activeObject.activeLayer.children[i]; if (!(child.idx in context.activeObject.currentFrame.keys)) continue; // let bbox = child.bbox() @@ -5464,7 +5513,7 @@ function stage() { context.selectionRect.y2 = mouse.y; context.selection = []; context.shapeselection = []; - for (let child of context.activeObject.children) { + for (let child of context.activeObject.activeLayer.children) { if (hitTest(regionToBbox(context.selectionRect), child)) { context.selection.push(child); } @@ -5601,8 +5650,8 @@ function stage() { mouse = context.activeObject.transformMouse(mouse); modeswitcher: switch (mode) { case "select": - for (let i = context.activeObject.children.length - 1; i >= 0; i--) { - let child = context.activeObject.children[i]; + for (let i = context.activeObject.activeLayer.children.length - 1; i >= 0; i--) { + let child = context.activeObject.activeLayer.children[i]; if (!(child.idx in context.activeObject.currentFrame.keys)) continue; if (hitTest(mouse, child)) { context.objectStack.push(child); diff --git a/src/widgets.js b/src/widgets.js index 4ac3820..423aea8 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -1,10 +1,19 @@ import { clamp, drawCheckerboardBackground, hslToRgb, hsvToRgb, rgbToHex } from "./utils.js" +function growBoundingBox(bboxa, bboxb) { + bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min); + bboxa.y.min = Math.min(bboxa.y.min, bboxb.y.min); + bboxa.x.max = Math.max(bboxa.x.max, bboxb.x.max); + bboxa.y.max = Math.max(bboxa.y.max, bboxb.y.max); + } + class Widget { constructor(x, y) { this._globalEvents = new Set() this.x = x this.y = y + this.scale_x = 1 + this.scale_y = 1 this.rotation = 0 this.children = [] } @@ -47,6 +56,27 @@ class Widget { } return false } + bbox() { + let bbox; + if (this.children.length > 0) { + if (!bbox) { + bbox = structuredClone(this.children[0].bbox()); + } + for (let child of this.children) { + growBoundingBox(bbox, child.bbox()); + } + } + if (bbox == undefined) { + bbox = { x: { min: 0, max: 0 }, y: { min: 0, max: 0 } }; + } + bbox.x.max *= this.scale_x; + bbox.y.max *= this.scale_y; + bbox.x.min += this.x; + bbox.x.max += this.x; + bbox.y.min += this.y; + bbox.y.max += this.y; + return bbox; + } draw(ctx) { for (let child of this.children) { const transform = ctx.getTransform() @@ -57,7 +87,6 @@ class Widget { } } } - class HueSelectionBar extends Widget { constructor(width, height, x, y, colorCvs) { super(x, y) From b4d8a9a10a596145b4a4428e5fc32db152058e79 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 03:11:16 -0500 Subject: [PATCH 03/18] fix play from frame --- src/main.js | 80 +++++++++++++++++++++++++++++++++++++++++--------- src/widgets.js | 1 + 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/main.js b/src/main.js index de97ecd..0991569 100644 --- a/src/main.js +++ b/src/main.js @@ -1025,7 +1025,12 @@ let actions = { }; for (let obj in action.selection) { const object = pointerList[obj]; - object.draw(cxt); + const transform = ctx.getTransform() + ctx.translate(object.x, object.y) + ctx.scale(object.scale_x, object.scale_y) + ctx.rotate(object.rotation) + object.draw(ctx) + ctx.setTransform(transform) } ctx.strokeStyle = "#00ffff"; ctx.lineWidth = 1; @@ -2082,6 +2087,7 @@ class Frame { return undefined } const frame = new Frame(json.frameType, json.idx); + frame.keyTypes = new Set(json.keyTypes) frame.keys = json.keys; for (let i in json.shapes) { const shape = json.shapes[i]; @@ -2094,6 +2100,7 @@ class Frame { const json = {}; json.type = "Frame"; json.frameType = this.frameType; + json.keyTypes = Array.from(this.keyTypes) if (randomizeUuid) { json.idx = uuidv4(); } else { @@ -2172,15 +2179,26 @@ class Layer extends Widget { layer.frames = []; for (let i in json.frames) { const frame = json.frames[i]; - layer.frames.push(Frame.fromJSON(frame)); - } - for (let frame in layer.frames) { - if (layer.frames[frame]) { - if (["motion", "shape"].indexOf(layer.frames[frame].frameType) != -1) { - layer.updateFrameNextAndPrev(frame, layer.frames[frame].frameType); + 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) } } + // for (let frame in layer.frames) { + // if (layer.frames[frame]) { + // if (["motion", "shape"].indexOf(layer.frames[frame].frameType) != -1) { + // layer.updateFrameNextAndPrev(frame, layer.frames[frame].frameType); + // } + // } + // } layer.visible = json.visible; layer.audible = json.audible; @@ -2504,6 +2522,7 @@ class Layer extends Widget { // super.draw(ctx) let frameInfo = this.getFrameValue(this.frameNum); let frame = frameInfo.valueAtN !== undefined ? frameInfo.valueAtN : frameInfo.prev; + const keyframe = frameInfo.valueAtN ? true : false // let frame = this.getFrame(this.currentFrameNum); let cxt = {...context} @@ -2548,8 +2567,35 @@ class Layer extends Widget { } } for (let child of this.children) { + if (!context.objectStack.includes(child)) { + if (keyframe) { + if (child.goToFrame != undefined) { + child.setFrameNum(child.goToFrame - 1) + if (child.playFromFrame) { + child.playing = true + } else { + child.playing = false + } + child.playing = true + } + } + if (child.playing) { + let lastFrame = 0; + for (let i = this.frameNum; i >= 0; i--) { + if ( + this.frames[i] && + this.frames[i].keys[child.idx].playFromFrame + ) { + lastFrame = i; + break; + } + } + child.setFrameNum(this.frameNum - lastFrame); + } + } const transform = ctx.getTransform() ctx.translate(child.x, child.y) + ctx.scale(child.scale_x, child.scale_y) ctx.rotate(child.rotation) child.draw(ctx) if (context.selection.includes(child)) { @@ -3544,7 +3590,7 @@ class GraphicsObject extends Widget { graphicsObject.name = json.name; graphicsObject.currentFrameNum = json.currentFrameNum; graphicsObject.currentLayer = json.currentLayer; - graphicsObject.layers = []; + graphicsObject.children = []; for (let layer of json.layers) { graphicsObject.layers.push(Layer.fromJSON(layer)); } @@ -3787,6 +3833,10 @@ class GraphicsObject extends Widget { ctx.fill(); ctx.restore(); } + */ + draw(ctx) { + super.draw(ctx) + if (this==context.activeObject) { if (mode == "select") { for (let item of context.selection) { if (!item) continue; @@ -3864,15 +3914,14 @@ class GraphicsObject extends Widget { } } } - ctx.restore(); - }*/ + } transformCanvas(ctx) { if (this.parent) { this.parent.transformCanvas(ctx) } ctx.translate(this.x, this.y); - ctx.rotate(this.rotation); ctx.scale(this.scale_x, this.scale_y); + ctx.rotate(this.rotation); } transformMouse(mouse) { // Apply the transformation matrix to the mouse position @@ -5667,7 +5716,7 @@ function stage() { // we didn't click on a child, go up a level if (context.activeObject.parent) { context.selection = [context.activeObject]; - context.activeObject.currentFrameNum = 0; + context.activeObject.setFrameNum(0); context.shapeselection = []; context.objectStack.pop(); updateUI(); @@ -6585,7 +6634,10 @@ function renderUI() { if (context.activeObject != root) { ctx.fillStyle = "rgba(255,255,255,0.5)"; ctx.fillRect(0, 0, config.fileWidth, config.fileHeight); - context.activeObject.draw(context, true); + const transform = ctx.getTransform() + context.activeObject.transformCanvas(ctx) + context.activeObject.draw(ctx); + ctx.setTransform(transform) } if (context.activeShape) { context.activeShape.draw(context); @@ -7795,7 +7847,7 @@ function renderAll() { if (errorMessage !== lastErrorMessage) { // A new error, log it and reset repeat count - console.error("Error during rendering:", errorMessage); + console.error(error); lastErrorMessage = errorMessage; repeatCount = 1; } else if (repeatCount === 1) { diff --git a/src/widgets.js b/src/widgets.js index 423aea8..e0b56d3 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -81,6 +81,7 @@ class Widget { for (let child of this.children) { const transform = ctx.getTransform() ctx.translate(child.x, child.y) + ctx.scale(child.scale_x, child.scale_y) ctx.rotate(child.rotation) child.draw(ctx) ctx.setTransform(transform) From 9205205ecd52a1170192ad322fe750e183dd1998 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 18:25:39 -0500 Subject: [PATCH 04/18] Add line numbers back to web console --- src/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index 0991569..48e02d8 100644 --- a/src/main.js +++ b/src/main.js @@ -91,17 +91,17 @@ function forwardConsole(fnName, dest) { const stackLines = error.stack.split("\n"); let message = args.join(" "); // Join all arguments into a single string + const location = stackLines[1].match(/([a-zA-Z0-9_-]+\.js:\d+)/); if (fnName === "error") { // Send the full stack trace for errors invoke(dest, { msg: `${message}\nStack trace:\n${stackLines.slice(1).join("\n")}` }); } else { // For other log levels, just extract the file and line number - const location = stackLines[stackLines.length - 1].match(/([a-zA-Z0-9_-]+\.js:\d+)/); invoke(dest, { msg: `${location ? location[0] : 'unknown'}: ${message}` }); } - original(...args); // Pass all arguments to the original console method + original(location ? location[0] : 'unknown', ...args); // Pass all arguments to the original console method }; } From 112de7ed1a251b163d21739af3d712bb6c2b8b97 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 18:26:13 -0500 Subject: [PATCH 05/18] Fix paint bucket coordinates inside objects --- src/main.js | 349 ++++++++++++++++++++++++++++------------------------ 1 file changed, 187 insertions(+), 162 deletions(-) diff --git a/src/main.js b/src/main.js index 48e02d8..d69aab4 100644 --- a/src/main.js +++ b/src/main.js @@ -40,6 +40,7 @@ import { rotateAroundPointIncremental, rgbToHsv, multiplyMatrices, + growBoundingBox, } from "./utils.js"; import { backgroundColor, @@ -1976,13 +1977,6 @@ function deriveControlPoints(S, A, E, e1, e2, t) { return { v1, v2, C1, C2 }; } -function growBoundingBox(bboxa, bboxb) { - bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min); - bboxa.y.min = Math.min(bboxa.y.min, bboxb.y.min); - bboxa.x.max = Math.max(bboxa.x.max, bboxb.x.max); - bboxa.y.max = Math.max(bboxa.y.max, bboxb.y.max); -} - function regionToBbox(region) { return { x: { @@ -2633,176 +2627,182 @@ class Layer extends Widget { } mousedown(x, y) { const mouse = {x: x, y: y} - switch(mode) { - case "rectangle": - case "ellipse": - case "draw": - this.clicked = true - this.activeShape = new Shape(x, y, context, uuidv4()) - this.lastMouse = mouse; - break; - case "select": - case "transform": - break; - case "paint_bucket": - debugCurves = []; - debugPoints = []; - let epsilon = context.fillGaps; - let regionPoints; - - // First, see if there's an existing shape to change the color of - // TODO: get this from self - let pointShape = getShapeAtPoint( - mouse, - context.activeObject.currentFrame.shapes, - ); - - if (pointShape) { - actions.colorShape.create(pointShape, context.fillStyle); + if (this==context.activeLayer) { + switch(mode) { + case "rectangle": + case "ellipse": + case "draw": + this.clicked = true + this.activeShape = new Shape(x, y, context, uuidv4()) + this.lastMouse = mouse; break; - } + case "select": + case "transform": + break; + case "paint_bucket": + debugCurves = []; + debugPoints = []; + let epsilon = context.fillGaps; + let regionPoints; - // We didn't find an existing region to paintbucket, see if we can make one - try { - regionPoints = floodFillRegion( + // First, see if there's an existing shape to change the color of + // TODO: get this from self + let pointShape = getShapeAtPoint( mouse, - epsilon, - config.fileWidth, - config.fileHeight, - context, - debugPoints, - debugPaintbucket, + context.activeObject.currentFrame.shapes, ); - } catch (e) { - updateUI(); - throw e; - } - if (regionPoints.length > 0 && regionPoints.length < 10) { - // probably a very small area, rerun with minimum epsilon - regionPoints = floodFillRegion( - mouse, - 1, - config.fileWidth, - config.fileHeight, - context, - debugPoints, - ); - } - let points = []; - for (let point of regionPoints) { - points.push([point.x, point.y]); - } - let cxt = { - ...context, - fillShape: true, - strokeShape: false, - sendToBack: true, - }; - let shape = new Shape(regionPoints[0].x, regionPoints[0].y, cxt); - shape.fromPoints(points, 1); - actions.addShape.create(context.activeObject, shape, cxt); - break; + + if (pointShape) { + actions.colorShape.create(pointShape, context.fillStyle); + break; + } + + // We didn't find an existing region to paintbucket, see if we can make one + try { + regionPoints = floodFillRegion( + mouse, + epsilon, + config.fileWidth, + config.fileHeight, + context, + debugPoints, + debugPaintbucket, + ); + } catch (e) { + updateUI(); + throw e; + } + if (regionPoints.length > 0 && regionPoints.length < 10) { + // probably a very small area, rerun with minimum epsilon + regionPoints = floodFillRegion( + mouse, + 1, + config.fileWidth, + config.fileHeight, + context, + debugPoints, + ); + } + let points = []; + for (let point of regionPoints) { + points.push([point.x, point.y]); + } + let cxt = { + ...context, + fillShape: true, + strokeShape: false, + sendToBack: true, + }; + let shape = new Shape(regionPoints[0].x, regionPoints[0].y, cxt); + shape.fromPoints(points, 1); + actions.addShape.create(context.activeObject, shape, cxt); + break; + } } } mousemove(x, y) { const mouse = {x: x, y: y} - switch (mode) { - case "draw": - if (this.activeShape) { - if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { - this.activeShape.addLine(x, y); - this.lastMouse = mouse; + if (this==context.activeLayer) { + switch (mode) { + case "draw": + if (this.activeShape) { + if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { + this.activeShape.addLine(x, y); + this.lastMouse = mouse; + } } - } - break; - case "rectangle": - if (this.activeShape) { - this.activeShape.clear(); - this.activeShape.addLine(x, this.activeShape.starty); - this.activeShape.addLine(x, y); - this.activeShape.addLine(this.activeShape.startx, y); - this.activeShape.addLine( - this.activeShape.startx, - this.activeShape.starty, - ); - this.activeShape.update(); - } - break; - case "ellipse": - if (this.activeShape) { - let midX = (mouse.x + this.activeShape.startx) / 2; - let midY = (mouse.y + this.activeShape.starty) / 2; - let xDiff = (mouse.x - this.activeShape.startx) / 2; - let yDiff = (mouse.y - this.activeShape.starty) / 2; - let ellipseConst = 0.552284749831; // (4/3)*tan(pi/(2n)) where n=4 - this.activeShape.clear(); - this.activeShape.addCurve( - new Bezier( - midX, - this.activeShape.starty, - midX + ellipseConst * xDiff, - this.activeShape.starty, - mouse.x, - midY - ellipseConst * yDiff, - mouse.x, - midY, - ), - ); - this.activeShape.addCurve( - new Bezier( - mouse.x, - midY, - mouse.x, - midY + ellipseConst * yDiff, - midX + ellipseConst * xDiff, - mouse.y, - midX, - mouse.y, - ), - ); - this.activeShape.addCurve( - new Bezier( - midX, - mouse.y, - midX - ellipseConst * xDiff, - mouse.y, + break; + case "rectangle": + if (this.activeShape) { + this.activeShape.clear(); + this.activeShape.addLine(x, this.activeShape.starty); + this.activeShape.addLine(x, y); + this.activeShape.addLine(this.activeShape.startx, y); + this.activeShape.addLine( this.activeShape.startx, - midY + ellipseConst * yDiff, - this.activeShape.startx, - midY, - ), - ); - this.activeShape.addCurve( - new Bezier( - this.activeShape.startx, - midY, - this.activeShape.startx, - midY - ellipseConst * yDiff, - midX - ellipseConst * xDiff, this.activeShape.starty, - midX, - this.activeShape.starty, - ), - ); - } - break; + ); + this.activeShape.update(); + } + break; + case "ellipse": + if (this.activeShape) { + let midX = (mouse.x + this.activeShape.startx) / 2; + let midY = (mouse.y + this.activeShape.starty) / 2; + let xDiff = (mouse.x - this.activeShape.startx) / 2; + let yDiff = (mouse.y - this.activeShape.starty) / 2; + let ellipseConst = 0.552284749831; // (4/3)*tan(pi/(2n)) where n=4 + this.activeShape.clear(); + this.activeShape.addCurve( + new Bezier( + midX, + this.activeShape.starty, + midX + ellipseConst * xDiff, + this.activeShape.starty, + mouse.x, + midY - ellipseConst * yDiff, + mouse.x, + midY, + ), + ); + this.activeShape.addCurve( + new Bezier( + mouse.x, + midY, + mouse.x, + midY + ellipseConst * yDiff, + midX + ellipseConst * xDiff, + mouse.y, + midX, + mouse.y, + ), + ); + this.activeShape.addCurve( + new Bezier( + midX, + mouse.y, + midX - ellipseConst * xDiff, + mouse.y, + this.activeShape.startx, + midY + ellipseConst * yDiff, + this.activeShape.startx, + midY, + ), + ); + this.activeShape.addCurve( + new Bezier( + this.activeShape.startx, + midY, + this.activeShape.startx, + midY - ellipseConst * yDiff, + midX - ellipseConst * xDiff, + this.activeShape.starty, + midX, + this.activeShape.starty, + ), + ); + } + break; + } } } mouseup(x, y) { this.clicked = false - switch (mode) { - case "draw": - if (this.activeShape) { - this.activeShape.addLine(x, y); - this.activeShape.simplify(context.simplifyMode); - } - case "rectangle": - case "ellipse": - if (this.activeShape) { - actions.addShape.create(context.activeObject, this.activeShape); - this.activeShape = undefined; - } - break; + if (this==context.activeLayer) { + switch (mode) { + case "draw": + if (this.activeShape) { + this.activeShape.addLine(x, y); + this.activeShape.simplify(context.simplifyMode); + } + case "rectangle": + case "ellipse": + if (this.activeShape) { + actions.addShape.create(context.activeObject, this.activeShape); + this.activeShape = undefined; + } + break; + } } } } @@ -3579,6 +3579,10 @@ class GraphicsObject extends Widget { // this.children = [] this.shapes = []; + + this._globalEvents.add("mousedown") + this._globalEvents.add("mousemove") + this._globalEvents.add("mouseup") } static fromJSON(json) { const graphicsObject = new GraphicsObject(json.idx); @@ -4059,6 +4063,11 @@ Object.defineProperty(context, "activeObject", { return this.objectStack.at(-1); }, }); +Object.defineProperty(context, "activeLayer", { + get: function () { + return this.objectStack.at(-1).activeLayer + } +}) context.objectStack = [root]; async function greet() { @@ -6875,6 +6884,22 @@ function renderLayers() { 2 * Math.PI, ); ctx.fill(); + if (frameInfo.valueAtN.keyTypes.has("motion")) { + ctx.strokeStyle = "#7a00b3"; + ctx.lineWidth = 2; + ctx.beginPath() + ctx.moveTo(j*frameWidth, layerHeight*0.25) + ctx.lineTo((j+1)*frameWidth, layerHeight*0.25) + ctx.stroke() + } + if (frameInfo.valueAtN.keyTypes.has("shape")) { + ctx.strokeStyle = "#9bff9b"; + ctx.lineWidth = 2; + ctx.beginPath() + ctx.moveTo(j*frameWidth, layerHeight*0.35) + ctx.lineTo((j+1)*frameWidth, layerHeight*0.35) + ctx.stroke() + } } else if (frameInfo.prev && frameInfo.next) { ctx.fillStyle = foregroundColor; drawBorderedRect( From 596dd9501ce437083ebc191a449456453610c27d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 18:27:10 -0500 Subject: [PATCH 06/18] Move growBoundingBox to utils.js --- src/utils.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils.js b/src/utils.js index 19707a2..b6b41f1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -163,6 +163,13 @@ function multiplyMatrices(a, b) { return result; } +function growBoundingBox(bboxa, bboxb) { + bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min); + bboxa.y.min = Math.min(bboxa.y.min, bboxb.y.min); + bboxa.x.max = Math.max(bboxa.x.max, bboxb.x.max); + bboxa.y.max = Math.max(bboxa.y.max, bboxb.y.max); +} + function floodFillRegion( startPoint, epsilon, From cf90cc183ce2bb2b4a9ab48d55d6c0af62898b61 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 18:28:21 -0500 Subject: [PATCH 07/18] Make flood fill use shape bounds to instead of stage --- src/utils.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/utils.js b/src/utils.js index b6b41f1..7519861 100644 --- a/src/utils.js +++ b/src/utils.js @@ -178,13 +178,7 @@ function floodFillRegion( context, debugPoints, debugPaintbucket) { - // Helper function to check if the point is at the boundary of the region - function isBoundaryPoint(point) { - return point.x <= 0 || point.x >= fileWidth || - point.y <= 0 || point.y >= fileHeight; - return point.x <= offset.x || point.x >= offset.x + fileWidth || - point.y <= offset.y || point.y >= offset.y + fileHeight; - } + let halfEpsilon = epsilon/2 // Helper function to check if a point is near any curve in the shape @@ -212,6 +206,24 @@ function floodFillRegion( const visited = new Set(); const stack = [startPoint]; const regionPoints = []; + let bbox; + if (shapes.length>0) { + bbox = shapes[0].boundingBox + } else { + throw new Error("No shapes in layer") + } + for (const shape of shapes) { + growBoundingBox(bbox, shape.boundingBox) + } + console.log(bbox) + console.log(startPoint) + // Helper function to check if the point is at the boundary of the region + function isBoundaryPoint(point) { + return point.x <= bbox.x.min - 100 || point.x >= bbox.x.max + 100 || + point.y <= bbox.y.min - 100 || point.y >= bbox.y.max + 100; + return point.x <= offset.x || point.x >= offset.x + fileWidth || + point.y <= offset.y || point.y >= offset.y + fileHeight; + } // Begin the flood fill process while (stack.length > 0) { @@ -898,6 +910,7 @@ export { lerpColor, camelToWords, generateWaveform, + growBoundingBox, floodFillRegion, multiplyMatrices, getShapeAtPoint, From e22bafdeeb197ec68fe255d69eb259068b2604f0 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 18:47:43 -0500 Subject: [PATCH 08/18] remove unnecessary logs --- src/utils.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils.js b/src/utils.js index 7519861..59318df 100644 --- a/src/utils.js +++ b/src/utils.js @@ -215,8 +215,6 @@ function floodFillRegion( for (const shape of shapes) { growBoundingBox(bbox, shape.boundingBox) } - console.log(bbox) - console.log(startPoint) // Helper function to check if the point is at the boundary of the region function isBoundaryPoint(point) { return point.x <= bbox.x.min - 100 || point.x >= bbox.x.max + 100 || From 4b3c89d9197892379179900e9ad6fe1781a09a13 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 18:48:15 -0500 Subject: [PATCH 09/18] Prevent right-click menu from showing up --- src-tauri/src/lib.rs | 10 ++++++++++ src/main.js | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b4dc898..71dbeda 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,6 +2,7 @@ use tauri_plugin_log::{Target, TargetKind}; use log::{trace, info, debug, warn, error}; use tracing_subscriber::EnvFilter; use chrono::Local; +use tauri::Manager; // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ @@ -62,6 +63,15 @@ pub fn run() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![greet, trace, debug, info, warn, error]) + .setup(|app| { + #[cfg(debug_assertions)] // only include this code on debug builds + { + let window = app.get_webview_window("main").unwrap(); + window.open_devtools(); + window.close_devtools(); + } + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); tracing_subscriber::fmt().with_env_filter(EnvFilter::new(format!("{}=trace", pkg_name))).init(); diff --git a/src/main.js b/src/main.js index d69aab4..2b190ed 100644 --- a/src/main.js +++ b/src/main.js @@ -4118,6 +4118,15 @@ window.addEventListener("click", function (event) { } }); +window.addEventListener("contextmenu", async (e) => { + e.preventDefault() + // const menu = await Menu.new({ + // items: [ + // ], + // }); + // menu.popup({ x: event.clientX, y: event.clientY }); +}) + window.addEventListener("keydown", (e) => { // let shortcuts = {} // for (let shortcut of config.shortcuts) { From 33212cb2baff8a781ec04a334c971e93c257ac2a Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 19:04:15 -0500 Subject: [PATCH 10/18] Change identifier to allow log folder to be opened on macOS --- src-tauri/src/lib.rs | 6 +++--- src-tauri/tauri.conf.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 71dbeda..97f0a7a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -51,9 +51,9 @@ pub fn run() { .targets([ Target::new(TargetKind::Stdout), // LogDir locations: - // Linux: /home/user/.local/share/org.lightningbeam.app/logs - // macOS: /Users/user/Library/Logs/org.lightningbeam.app/logs - // Windows: C:\Users\user\AppData\Local\org.lightningbeam.app\logs + // Linux: /home/user/.local/share/org.lightningbeam.core/logs + // macOS: /Users/user/Library/Logs/org.lightningbeam.core/logs + // Windows: C:\Users\user\AppData\Local\org.lightningbeam.core\logs Target::new(TargetKind::LogDir { file_name: Some("logs".to_string()) }), Target::new(TargetKind::Webview), ]) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5a856cf..c1c85a7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Lightningbeam", "version": "0.6.18-alpha", - "identifier": "org.lightningbeam.app", + "identifier": "org.lightningbeam.core", "build": { "frontendDist": "../src" }, From 2ce75df4d0914895b420ad0654b0b2f53d79d98c Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 19:22:14 -0500 Subject: [PATCH 11/18] Fix selection on later frames --- src/main.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 2b190ed..62a4664 100644 --- a/src/main.js +++ b/src/main.js @@ -2615,7 +2615,8 @@ class Layer extends Widget { bbox() { let bbox = super.bbox() const frameInfo = this.getFrameValue(this.frameNum); - const frame = frameInfo.valueAtN + // TODO: if there are multiple layers we should only be counting valid frames + const frame = frameInfo.valueAtN ? frameInfo.valueAtN : frameInfo.prev if (!frame) return bbox; if (frame.shapes.length > 0 && bbox == undefined) { bbox = structuredClone(frame.shapes[0].boundingBox); From 66bef53738f69db3f4926949ba05afd1a061dec5 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 10 Jan 2025 19:27:56 -0500 Subject: [PATCH 12/18] Bump version to 0.7.0 --- Changelog.md | 15 +++++++++++++++ src-tauri/tauri.conf.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index fd48600..e0171b0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,18 @@ +# 0.7.0-alpha: +New features: +- Keyframes can now have both motion and shape tweens on the same frame + +Changes: +- Tweens are now indicated with colored lines +- Tweens are now attached to keyframes rather than the frames in between them + +Bugfixes: +- Fix paint bucket coordinates being incorrect inside of movie clips +- Fix paint bucket not working for large shapes and shapes whose internal coordinates crossed 0,0 +- Fixed dragging frames breaking tweens +- Fixed logs being inaccessible on macOS +- Fixed right-click causing a menu with "Reload" to appear which would reset the application + # 0.6.18-alpha: New features: - Errors and debug messages are now logged to a file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c1c85a7..c4e9de9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Lightningbeam", - "version": "0.6.18-alpha", + "version": "0.7.0-alpha", "identifier": "org.lightningbeam.core", "build": { "frontendDist": "../src" From 901530178ed35af28f47f5b1bcedd1263d4c8238 Mon Sep 17 00:00:00 2001 From: skykooler Date: Wed, 8 Jan 2025 05:25:43 -0500 Subject: [PATCH 13/18] Debug release notes extraction --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bffc972..d82d4f6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,6 +39,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Debug the extracted release notes + run: | + echo "Extracted Release Notes: ${{ steps.changelog.outputs.markdown }}" + echo "All outputs: ${{ steps.changelog.outputs }}" + - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. run: | From 923b7abdad0994d23b660402c704eba0976c057e Mon Sep 17 00:00:00 2001 From: skykooler Date: Wed, 8 Jan 2025 05:30:07 -0500 Subject: [PATCH 14/18] Set version in changelog extraction step --- .github/workflows/main.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d82d4f6..5954f55 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,6 +11,15 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 + + - name: Set version for changelog extraction + shell: bash + run: | + # Read the version from src-tauri/tauri.conf.json + VERSION=$(jq -r '.version' src-tauri/tauri.conf.json) + # Set the version in the environment variable + echo "VERSION=$VERSION" >> $GITHUB_ENV + - name: Extract release notes from Changelog.md id: changelog uses: sean0x42/markdown-extract@v2.1.0 From f621ebf225882a63a3995a6f9dfe94b5d001688e Mon Sep 17 00:00:00 2001 From: skykooler Date: Wed, 8 Jan 2025 05:32:54 -0500 Subject: [PATCH 15/18] Debug better --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5954f55..e7c0287 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,6 +26,11 @@ jobs: with: pattern: "${{ env.VERSION }}:" # Look for the version header (e.g., # 0.6.15-alpha:) file: Changelog.md + + - name: Debug the extracted release notes + run: | + echo "Extracted Release Notes: ${{ steps.changelog.outputs.markdown }}" + echo "All outputs: ${{ steps.changelog.outputs }}" publish-tauri: needs: extract-changelog From c28e835ac64881a35f7b2c69e7b5d8015f32880c Mon Sep 17 00:00:00 2001 From: skykooler Date: Wed, 8 Jan 2025 05:39:13 -0500 Subject: [PATCH 16/18] Add output to extract changelog job --- .github/workflows/main.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e7c0287..c523fc0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,10 +27,9 @@ jobs: pattern: "${{ env.VERSION }}:" # Look for the version header (e.g., # 0.6.15-alpha:) file: Changelog.md - - name: Debug the extracted release notes - run: | - echo "Extracted Release Notes: ${{ steps.changelog.outputs.markdown }}" - echo "All outputs: ${{ steps.changelog.outputs }}" + # Set the markdown output of the job + - name: Set markdown output + run: echo "::set-output name=markdown::$(echo '${{ steps.changelog.outputs.markdown }}')" publish-tauri: needs: extract-changelog @@ -55,8 +54,7 @@ jobs: - name: Debug the extracted release notes run: | - echo "Extracted Release Notes: ${{ steps.changelog.outputs.markdown }}" - echo "All outputs: ${{ steps.changelog.outputs }}" + echo "Extracted Release Notes: ${{ needs.extract-changelog.outputs.markdown }}" - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. From 203b9646c3b66e050a34dcad9b341ddb6865ee73 Mon Sep 17 00:00:00 2001 From: skykooler Date: Wed, 8 Jan 2025 05:43:36 -0500 Subject: [PATCH 17/18] Don't use deprecated commands --- .github/workflows/main.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c523fc0..805b5ea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,9 +27,10 @@ jobs: pattern: "${{ env.VERSION }}:" # Look for the version header (e.g., # 0.6.15-alpha:) file: Changelog.md - # Set the markdown output of the job + # Set the markdown output using environment files - name: Set markdown output - run: echo "::set-output name=markdown::$(echo '${{ steps.changelog.outputs.markdown }}')" + run: | + echo "markdown=${{ steps.changelog.outputs.markdown }}" >> $GITHUB_ENV publish-tauri: needs: extract-changelog @@ -54,7 +55,7 @@ jobs: - name: Debug the extracted release notes run: | - echo "Extracted Release Notes: ${{ needs.extract-changelog.outputs.markdown }}" + echo "Extracted Release Notes: ${{ env.markdown }}" - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. @@ -141,7 +142,7 @@ jobs: with: tagName: "app-v${{ env.VERSION }}" # Use the original version tag for the release releaseName: "Lightningbeam v${{ env.VERSION }}" - releaseBody: "${{ needs.extract-changelog.outputs.markdown }}" + releaseBody: "${{ env.markdown }}" releaseDraft: true # Set to true if you want the release to be a draft prerelease: true args: ${{ matrix.args }} From 4f2b74beaa4b0630149a4f7ec91ff06b143bb968 Mon Sep 17 00:00:00 2001 From: skykooler Date: Wed, 8 Jan 2025 05:51:57 -0500 Subject: [PATCH 18/18] Fix multiline release notes --- .github/workflows/main.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 805b5ea..657ee38 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,10 +27,12 @@ jobs: pattern: "${{ env.VERSION }}:" # Look for the version header (e.g., # 0.6.15-alpha:) file: Changelog.md - # Set the markdown output using environment files - name: Set markdown output + id: set-markdown-output run: | - echo "markdown=${{ steps.changelog.outputs.markdown }}" >> $GITHUB_ENV + echo 'RELEASE_NOTES<> $GITHUB_OUTPUT + echo "${{ steps.changelog.outputs.markdown }}" >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT publish-tauri: needs: extract-changelog @@ -55,7 +57,7 @@ jobs: - name: Debug the extracted release notes run: | - echo "Extracted Release Notes: ${{ env.markdown }}" + echo "Extracted Release Notes: ${{ needs.extract-changelog.outputs.RELEASE_NOTES }}" - name: install dependencies (ubuntu only) if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above. @@ -138,11 +140,11 @@ jobs: uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_NOTES: ${{ needs.extract-changelog.outputs.markdown }} + RELEASE_NOTES: ${{ needs.extract-changelog.outputs.RELEASE_NOTES }} with: tagName: "app-v${{ env.VERSION }}" # Use the original version tag for the release releaseName: "Lightningbeam v${{ env.VERSION }}" - releaseBody: "${{ env.markdown }}" + releaseBody: "${{ needs.extract-changelog.outputs.RELEASE_NOTES }}" releaseDraft: true # Set to true if you want the release to be a draft prerelease: true args: ${{ matrix.args }}