Initial undo/redo support
This commit is contained in:
parent
709bd46ab8
commit
45a055250b
|
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg7384"
|
||||
height="16"
|
||||
width="16"
|
||||
version="1.1"
|
||||
sodipodi:docname="gimp-prefs-systemc.svg"
|
||||
viewBox="0 0 16 16"
|
||||
inkscape:version="0.92pre1 unknown">
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1016"
|
||||
id="namedview8"
|
||||
showgrid="true"
|
||||
inkscape:zoom="43.1875"
|
||||
inkscape:cx="5.725592"
|
||||
inkscape:cy="8"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg7384">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4233" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata90">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Gnome Symbolic Icon Theme</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<title
|
||||
id="title9167">Gnome Symbolic Icon Theme</title>
|
||||
<g
|
||||
id="g4953"
|
||||
transform="matrix(1.0088031,0,0,1.0014609,-445.94089,-462.66315)">
|
||||
<path
|
||||
id="path3908"
|
||||
style="color:#000000;text-indent:0;text-transform:none;fill:#bebebe"
|
||||
d="m 445.55,462.09 c -0.39933,0 -0.78638,0.0916 -1.1433,0.21572 l 1.8849,1.8797 c 0.38735,0.38627 0.38735,1.0004 0,1.3867 l -0.71069,0.70874 c -0.38735,0.38628 -1.0031,0.38628 -1.3905,0 l -1.8849,-1.8797 c -0.12444,0.35591 -0.2163,0.74191 -0.2163,1.1402 0,1.9061 1.5494,3.4513 3.4608,3.4513 0.39933,0 0.78638,-0.0916 1.1433,-0.2157 l 1.1742,1.171 a 2.4722,2.4654 0 0 1 0.0618,0 l 2.0703,-2.0646 -1.2051,-1.2018 c 0.12444,-0.35592 0.2163,-0.74191 0.2163,-1.1402 0,-1.9061 -1.5494,-3.4513 -3.4608,-3.4513 z m 6.5507,7.8886 -2.0703,2.0646 a 2.4722,2.4654 0 0 1 0.0309,0.0924 l 1.1433,1.1402 c -0.12444,0.35596 -0.2163,0.74196 -0.2163,1.1402 0,1.9061 1.5494,3.4513 3.4608,3.4513 0.43346,0 0.8536,-0.10141 1.236,-0.24653 l -2.0085,-2.003 c -0.38735,-0.38629 -0.38735,-1.0312 0,-1.4175 l 0.67979,-0.67792 c 0.19367,-0.19315 0.45794,-0.30816 0.71069,-0.30816 0.25276,0 0.51702,0.11501 0.7107,0.30816 l 1.9467,1.9413 c 0.10485,-0.32958 0.1854,-0.68351 0.1854,-1.0477 0,-1.9061 -1.5494,-3.4513 -3.4608,-3.4513 -0.39933,0 -0.78639,0.0916 -1.1433,0.2157 l -1.2051,-1.2018 z"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path3910"
|
||||
style="color:#000000;text-indent:0;text-transform:none;fill:#bebebe"
|
||||
d="m 455.86,462 -1.5425,1.4375 c -0.45151,0.42079 -0.5292,1.1488 -0.2663,1.7065 l -5.8882,5.9958 a 1.4917,1.4876 0 0 0 -0.0311,2.5e-4 1.4917,1.4876 0 0 0 -0.84016,-0.1484 1.4917,1.4876 0 0 0 -0.86663,0.44059 l -3.9462,3.9973 a 1.49411,1.49001 0 1 0 2.1294,2.0907 l 3.9462,-3.9973 a 1.4917,1.4876 0 0 0 0.29713,-1.7377 l 5.8885,-5.9648 c 0.55782,0.24837 1.2732,0.14697 1.7068,-0.2922 l 1.41,-1.57 -2,-1.97 z"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
265
src/main.js
265
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: "<ctrl>+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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue