diff --git a/src/assets/preferences.svg b/src/assets/preferences.svg
new file mode 100644
index 0000000..ef6313c
--- /dev/null
+++ b/src/assets/preferences.svg
@@ -0,0 +1,73 @@
+
+
+
+
diff --git a/src/main.js b/src/main.js
index 13d5068..6743c68 100644
--- a/src/main.js
+++ b/src/main.js
@@ -16,6 +16,9 @@ let mode = "draw"
let minSegmentSize = 5;
let maxSmoothAngle = 0.6;
+let undoStack = [];
+let redoStack = [];
+
let tools = {
select: {
icon: "/assets/select.svg",
@@ -84,6 +87,55 @@ let context = {
let config = {
shortcuts: {
playAnimation: " ",
+ // undo: "+z"
+ undo: "z",
+ redo: "Z",
+ }
+}
+
+// Pointers to all objects
+let pointerList = {}
+
+let actions = {
+ addShape: {
+ create: (parent, shape) => {
+ redoStack.length = 0; // Clear redo stack
+ let serializableCurves = []
+ for (let curve of shape.curves) {
+ serializableCurves.push({ points: curve.points })
+ }
+ let action = {
+ parent: parent.idx,
+ curves: serializableCurves,
+ startx: shape.startx,
+ starty: shape.starty,
+ uuid: uuidv4()
+ }
+ undoStack.push({name: "addShape", action: action})
+ actions.addShape.execute(action)
+ },
+ execute: (action) => {
+ let object = pointerList[action.parent]
+ console.log(object)
+ let curvesList = action.curves
+ let shape = new Shape(action.startx, action.starty, context, action.uuid)
+ for (let curve of curvesList) {
+ shape.addCurve(
+ new Bezier(
+ curve.points[0].x, curve.points[0].y,
+ curve.points[1].x, curve.points[1].y,
+ curve.points[2].x, curve.points[2].y,
+ curve.points[3].x, curve.points[3].y
+ ))
+ }
+ object.addShape(shape)
+ },
+ rollback: (action) => {
+ let object = pointerList[action.parent]
+ let shape = pointerList[action.uuid]
+ object.removeShape(shape)
+ delete pointerList[action.uuid]
+ }
}
}
@@ -125,7 +177,7 @@ function setProperty(context, path, value) {
function selectCurve(context, mouse) {
let mouseTolerance = 15;
- for (let shape of context.activeObject.frames[context.activeObject.currentFrame].shapes) {
+ for (let shape of context.activeObject.currentFrame.shapes) {
if (mouse.x > shape.boundingBox.x.min - mouseTolerance &&
mouse.x < shape.boundingBox.x.max + mouseTolerance &&
mouse.y > shape.boundingBox.y.min - mouseTolerance &&
@@ -185,6 +237,36 @@ function hitTest(candidate, object) {
}
}
+function pushState() {
+ // console.log(context)
+ // let ctx = context.ctx
+ // context.ctx = undefined
+ // undoStack.push(window.structuredClone([root,context]))
+ // context.ctx = ctx
+}
+function undo() {
+ let action = undoStack.pop()
+ if (action) {
+ actions[action.name].rollback(action.action)
+ redoStack.push(action)
+ updateUI()
+ } else {
+ console.log("No actions to undo")
+ }
+}
+
+function redo() {
+ let action = redoStack.pop()
+ if (action) {
+ actions[action.name].execute(action.action)
+ undoStack.push(action)
+ updateUI()
+ } else {
+ console.log("No actions to redo")
+ }
+}
+
+
class Curve {
constructor(startx, starty, cp1x, cp1y, cp2x, cp2y, x, y) {
this.startx = startx
@@ -199,14 +281,33 @@ class Curve {
}
class Frame {
- constructor() {
+ constructor(uuid) {
this.keys = {}
this.shapes = []
+ if (!uuid) {
+ this.idx = uuidv4()
+ } else {
+ this.idx = uuid
+ }
+ pointerList[this.idx] = this
+ }
+}
+
+class Layer {
+ constructor(uuid) {
+ this.frames = [new Frame()]
+ this.children = []
+ if (!uuid) {
+ this.idx = uuidv4()
+ } else {
+ this.idx = uuid
+ }
+ pointerList[this.idx] = this
}
}
class Shape {
- constructor(startx, starty, context, stroked=true) {
+ constructor(startx, starty, context, uuid=undefined) {
this.startx = startx;
this.starty = starty;
this.curves = [];
@@ -215,15 +316,21 @@ class Shape {
this.strokeStyle = context.strokeStyle;
this.lineWidth = context.lineWidth
this.filled = context.fillShape;
- this.stroked = stroked;
+ this.stroked = context.strokeShape || true;
this.boundingBox = {
x: {min: startx, max: starty},
y: {min: starty, max: starty}
}
+ if (!uuid) {
+ this.idx = uuidv4()
+ } else {
+ this.idx = uuid
+ }
+ pointerList[this.idx] = this
}
addCurve(curve) {
this.curves.push(curve)
- this.growBoundingBox(curve.bbox())
+ growBoundingBox(this.boundingBox, curve.bbox())
}
addLine(x, y) {
let lastpoint;
@@ -287,27 +394,79 @@ class Shape {
}
this.recalculateBoundingBox()
}
+ draw(context) {
+ let ctx = context.ctx;
+ ctx.beginPath()
+ ctx.lineWidth = this.lineWidth
+ ctx.moveTo(this.startx, this.starty)
+ for (let curve of this.curves) {
+ 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)
+
+ // Debug, show curve endpoints
+ // ctx.beginPath()
+ // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI)
+ // ctx.fill()
+ }
+ if (this.filled) {
+ if (this.fillImage) {
+ let pat = ctx.createPattern(this.fillImage, "no-repeat")
+ ctx.fillStyle = pat
+ } else {
+ ctx.fillStyle = this.fillStyle
+ }
+ ctx.fill()
+ }
+ if (this.stroked) {
+ ctx.strokeStyle = this.strokeStyle
+ ctx.stroke()
+ }
+
+ }
}
class GraphicsObject {
- constructor() {
+ constructor(uuid) {
this.x = 0;
this.y = 0;
this.rotation = 0; // in radians
this.scale = 1;
- this.idx = uuidv4()
+ if (!uuid) {
+ this.idx = uuidv4()
+ } else {
+ this.idx = uuid
+ }
+ pointerList[this.idx] = this
- this.frames = [new Frame()]
- this.currentFrame = 0;
- this.children = []
+ this.currentFrameNum = 0;
+ this.currentLayer = 0;
+ this.layers = [new Layer()]
+ // this.children = []
this.shapes = []
}
+ get activeLayer() {
+ return this.layers[this.currentLayer]
+ }
+ get children() {
+ return this.layers[this.currentLayer].children
+ }
+ get currentFrame() {
+ return this.layers[this.currentLayer].frames[this.currentFrameNum]
+ }
+ get maxFrame() {
+ let maxFrames = []
+ for (let layer of this.layers) {
+ maxFrames.push(layer.frames.length)
+ }
+ return Math.max(maxFrames)
+ }
bbox() {
let bbox;
- if (this.frames[this.currentFrame].shapes.length > 0) {
- bbox = this.frames[this.currentFrame].shapes[0].boundingBox
- for (let shape of this.frames[this.currentFrame].shapes) {
+ if (this.currentFrame.shapes.length > 0) {
+ bbox = this.currentFrame.shapes[0].boundingBox
+ for (let shape of this.currentFrame.shapes) {
growBoundingBox(bbox, shape.boundingBox)
}
}
@@ -325,50 +484,24 @@ class GraphicsObject {
let ctx = context.ctx;
ctx.translate(this.x, this.y)
ctx.rotate(this.rotation)
- if (this.currentFrame>=this.frames.length) {
- this.currentFrame = 0;
+ if (this.currentFrameNum>=this.maxFrame) {
+ this.currentFrameNum = 0;
+ }
+ for (let shape of this.currentFrame.shapes) {
+ shape.draw(context)
}
for (let child of this.children) {
let idx = child.idx
- if (idx in this.frames[this.currentFrame].keys) {
- child.x = this.frames[this.currentFrame].keys[idx].x;
- child.y = this.frames[this.currentFrame].keys[idx].y;
- child.rotation = this.frames[this.currentFrame].keys[idx].rotation;
- child.scale = this.frames[this.currentFrame].keys[idx].scale;
+ if (idx in this.currentFrame.keys) {
+ child.x = this.currentFrame.keys[idx].x;
+ child.y = this.currentFrame.keys[idx].y;
+ child.rotation = this.currentFrame.keys[idx].rotation;
+ child.scale = this.currentFrame.keys[idx].scale;
ctx.save()
child.draw(context)
ctx.restore()
}
}
- for (let shape of this.frames[this.currentFrame].shapes) {
- ctx.beginPath()
- ctx.lineWidth = shape.lineWidth
- ctx.moveTo(shape.startx, shape.starty)
- for (let curve of shape.curves) {
- // 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)
-
- // Debug, show curve endpoints
- // ctx.beginPath()
- // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI)
- // ctx.fill()
- }
- if (shape.filled) {
- if (shape.fillImage) {
- let pat = ctx.createPattern(shape.fillImage, "no-repeat")
- ctx.fillStyle = pat
- } else {
- ctx.fillStyle = shape.fillStyle
- }
- ctx.fill()
- }
- if (shape.stroked) {
- ctx.strokeStyle = shape.strokeStyle
- ctx.stroke()
- }
- }
if (this == context.activeObject) {
if (context.activeCurve) {
ctx.strokeStyle = "magenta"
@@ -405,18 +538,28 @@ class GraphicsObject {
}
}
addShape(shape) {
- this.frames[this.currentFrame].shapes.push(shape)
+ this.currentFrame.shapes.push(shape)
}
addObject(object, x=0, y=0) {
this.children.push(object)
let idx = object.idx
- this.frames[this.currentFrame].keys[idx] = {
+ this.currentFrame.keys[idx] = {
x: x,
y: y,
rotation: 0,
scale: 1,
}
}
+ removeShape(shape) {
+ for (let layer of this.layers) {
+ for (let frame of layer.frames) {
+ let shapeIndex = frame.shapes.indexOf(shape)
+ if (shapeIndex >= 0) {
+ frame.shapes.splice(shapeIndex, 1)
+ }
+ }
+ }
+ }
}
let root = new GraphicsObject();
@@ -443,8 +586,18 @@ window.addEventListener("resize", () => {
})
window.addEventListener("keypress", (e) => {
+ // let shortcuts = {}
+ // for (let shortcut of config.shortcuts) {
+ // shortcut = shortcut.split("+")
+ // TODO
+ // }
+ console.log(e)
if (e.key == config.shortcuts.playAnimation) {
console.log("Spacebar pressed")
+ } else if (e.key == config.shortcuts.undo && e.ctrlKey == true) {
+ undo()
+ } else if (e.key == config.shortcuts.redo && e.ctrlKey == true) {
+ redo()
}
})
@@ -509,9 +662,10 @@ function stage() {
switch (mode) {
case "rectangle":
case "draw":
+ pushState()
context.mouseDown = true
context.activeShape = new Shape(mouse.x, mouse.y, context, true, true)
- context.activeObject.addShape(context.activeShape)
+ console.log(context.activeObject)
context.lastMouse = mouse
break;
case "select":
@@ -558,7 +712,11 @@ function stage() {
if (context.activeShape) {
context.activeShape.addLine(mouse.x, mouse.y)
context.activeShape.simplify(context.simplifyMode)
+ actions.addShape.create(context.activeObject, context.activeShape)
+ // context.activeObject.addShape(context.activeShape)
context.activeShape = undefined
+ console.log(pointerList)
+ console.log(undoStack)
}
break;
case "rectangle":
@@ -863,6 +1021,9 @@ function updateUI() {
context.ctx = ctx;
root.draw(context)
+ if (context.activeShape) {
+ context.activeShape.draw(context)
+ }
// let mouse;
// if (mouseEvent) {