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) }