Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johtso/f9268013568b561531f613dce8219a7f to your computer and use it in GitHub Desktop.
Save johtso/f9268013568b561531f613dce8219a7f to your computer and use it in GitHub Desktop.
Canvas Drawing! | 7GUI Tasks Challenge | @keyframers 7.8.6
<!--
1. Canvas Drawing & Click State handing
2. Undo/Redo History
-->
<div id="app" class="drawing window">
<header>Canvas Drawing</header>
<div class="controls">
<button type="button" id="undo">Undo</button>
<button type="button" id="redo">Redo</button>
<input type="range" id="radius" min="10" max="200" />
</div>
<canvas id="canvas" width="700" height="500" />
</div>
<a href="https://youtu.be/UssOjdq-LS0" target="_blank" data-keyframers-credit style="color: #000"></a>
<script src="https://codepen.io/shshaw/pen/QmZYMG.js"></script>
console.clear();
import {
createMachine,
assign,
send,
interpret
} from "https://cdn.skypack.dev/xstate";
const elApp = document.querySelector("#app");
const elCanvas = document.querySelector("#canvas");
const canvasContext = elCanvas.getContext("2d");
const elUndo = document.querySelector("#undo");
const elRedo = document.querySelector("#redo");
const elRadius = document.querySelector("#radius");
const snapshot = assign({
snapshots: (ctx) => {
return [...ctx.snapshots, ctx.circles];
}
});
function findLastIndex(array, predicate) {
let index = -1;
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i)) {
index = i;
}
}
return index;
}
function getCircleIndex(circles, x, y) {
return findLastIndex(circles, (circle) => {
return Math.hypot(circle.x - x, circle.y - y) < circle.radius;
});
}
const initialCircles = [
{
x: elCanvas.width / 2,
y: elCanvas.height / 2,
radius: 40
}
];
const drawingMachine = createMachine({
context: {
width: elCanvas.width,
height: elCanvas.height,
circles: initialCircles,
circle: null,
snapshots: [initialCircles],
redo: []
},
initial: "drawing",
states: {
drawing: {
on: {
"CIRCLE.CREATE": {
actions: [
assign({
circle: null,
circles: (ctx, e) => ctx.circles.concat(e.circle)
}),
snapshot
]
},
"CIRCLE.SELECT": {
actions: assign({
circle: (ctx, e) => e.circle
}),
target: "editing"
},
UNDO: {
cond: (ctx) => ctx.snapshots.length > 0,
actions: assign((ctx) => {
const lastCircles = ctx.snapshots.pop();
return {
circles: ctx.snapshots[ctx.snapshots.length - 1] || [],
snapshots: [...ctx.snapshots],
redo: [...ctx.redo, lastCircles]
};
})
},
REDO: {
cond: (ctx) => ctx.redo.length > 0,
actions: assign((ctx) => {
const redoCircles = ctx.redo.pop();
return {
circles: redoCircles,
snapshots: [...ctx.snapshots, redoCircles],
redo: [...ctx.redo]
};
})
}
}
},
editing: {
exit: assign({
circle: null
}),
on: {
"CIRCLE.CHANGE": {
actions: assign({
circles: (ctx, e) => {
return ctx.circles.map((circle, i) => {
if (i !== ctx.circle) {
return circle;
}
return {
...circle,
radius: e.radius
};
});
}
})
},
"CANVAS.CLICK": {
target: "drawing",
actions: snapshot
}
}
}
},
on: {
"CANVAS.CLICK": [
{
cond: (ctx, e) => {
return getCircleIndex(ctx.circles, e.circle.x, e.circle.y) !== -1;
},
actions: [
send((ctx, e) => ({
type: "CIRCLE.SELECT",
circle: getCircleIndex(ctx.circles, e.circle.x, e.circle.y)
}))
]
},
{
actions: send((ctx, e) => ({
type: "CIRCLE.CREATE",
circle: e.circle
}))
}
]
}
});
const canvasService = interpret(drawingMachine)
.onTransition((state) => {
elApp.dataset.state = state.value;
console.log(state.event);
const { circles, circle: selectedCircle, width, height } = state.context;
if (selectedCircle !== null) {
elRadius.value = circles[selectedCircle].radius;
}
canvasContext.clearRect(0, 0, width, height);
circles.forEach((circle, i) => {
canvasContext.beginPath();
canvasContext.arc(circle.x, circle.y, circle.radius, 0, 2 * Math.PI);
if (i === selectedCircle) {
canvasContext.strokeStyle = "#32302c";
canvasContext.fillStyle = "rgba(0,0,0, 0.5)";
canvasContext.fill();
} else {
canvasContext.strokeStyle =
selectedCircle !== null ? "rgba(0,0,0,0.1)" : "#32302c";
}
canvasContext.lineWidth = 5;
canvasContext.stroke();
});
})
.start();
window.c = canvasService;
elCanvas.addEventListener("click", (e) => {
const rect = elCanvas.getBoundingClientRect();
const pointer = e.touches ? e.touches[0] : e;
const pointerX = e.clientX;
const pointerY = e.clientY;
const scaleX = rect.width / elCanvas.width;
const scaleY = rect.height / elCanvas.height;
const circle = {
x: (pointerX - rect.left) / scaleX,
y: (pointerY - rect.top) / scaleY,
radius: 40
};
canvasService.send({
type: "CANVAS.CLICK",
circle
});
// canvasService.send({ type: 'CIRCLE.CREATE', circle });
});
elUndo.addEventListener("click", () => {
canvasService.send({ type: "UNDO" });
});
elRedo.addEventListener("click", () => {
canvasService.send({ type: "REDO" });
});
elRadius.addEventListener("input", () => {
canvasService.send({ type: "CIRCLE.CHANGE", radius: +elRadius.value });
});
@import "https://codepen.io/team/keyframers/pen/d7c789c869b6b3a92f98afdfe6de4c4a.css";
.drawing {
width: 30em;
max-width: 90%;
}
#canvas {
max-width: 100%;
height: auto;
border: solid 0.25rem;
}
.controls {
margin: 0.5rem 0;
display: flex;
gap: 0.5rem;
}
#radius {
margin-left: auto;
display: none;
}
[data-state="editing"] #radius {
display: block;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment