Spirograph experiment
I took inspiration in the action of drawing on a rotating wheel, and observing how the lines seemingly moved. The result was a bit boring, so I modified it a bit, so that the line segments would rotate at unequal speeds, resulting in an ever-deforming line.
Try clicking in the drawing area and dragging it around.
// @ts-check
/**
* @typedef { [number, number]} Vector2
*/
const canvas = document.createElement('canvas');
const container = document.getElementById('code-target')
const restartButton = document.createElement('button');
if (!container) {
throw new Error('Container not found');
}
container.appendChild(canvas);
// container.appendChild(restartButton);
restartButton.style.position = 'fixed';
restartButton.style.bottom = '0';
restartButton.style.right = '0';
restartButton.style.zIndex = '1';
restartButton.innerText = 'Restart';
restartButton.onclick = () => {
window.location.reload();
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Canvas context not found');
}
/**
* @typedef {Object} DrawScope
* @property {CanvasRenderingContext2D} context - canvas context
* @property {number} width -
* @property {number} height -
* @property {Vector2} middle -
*/
/**
* @type {DrawScope}
*/
const drawScope = {
context,
width: 0,
height: 0,
middle: [0, 0],
}
drawScope.width = canvas.width;
drawScope.height = canvas.height;
drawScope.middle = [drawScope.width / 2, drawScope.height / 2];
let lastTime = new Date().getTime();
/** @type {Vector2[]} */
let currentTrace = [];
/** @type {Vector2[][]} */
const traces = [currentTrace];
const tracesLength = 512;
let traceCount = 0;
/**
* @param {Vector2} point
*/
const addtracePoint = (point) => {
currentTrace.push([...point]);
traceCount += 1;
while (traceCount > tracesLength) {
if (traces[0].length === 0) {
traces.shift();
continue;
}
traces[0].shift();
traceCount -= 1;
}
}
const startNewTrace = () => {
currentTrace = [];
traces.push(currentTrace);
}
/**
* @param {Vector2} coords
* @param {number} angle
* @returns {Vector2}
*/
const rotateCoords = (coords, angle) => {
const [x, y] = coords;
const sin = Math.sin(angle);
const cos = Math.cos(angle);
return [
x * cos - y * sin,
x * sin + y * cos
];
}
const translateCoords = (coords, offset) => {
const [x, y] = coords;
const [dx, dy] = offset;
return [x + dx, y + dy];
}
/**
* @param {Vector2} coords
* @param {...Function} callbacks
* @returns {Vector2}
*/
const processCoords = (coords, ...callbacks) => {
let res = coords;
callbacks.forEach(callback => {
res = callback(res);
});
return res;
}
/**
* @param {number} time
* @param {DrawScope} drawScope
*/
let draw = (time, {
context,
width,
height
}) => {
context.strokeStyle = 'white';
context.fillStyle = 'white';
context.lineWidth = 4;
context.lineCap = 'round';
const deltaTime = time - lastTime;
lastTime = time;
context.clearRect(0, 0, width, height);
if (mouse.down) {
addtracePoint(mouse.pos);
}
const rotationCenter = drawScope.middle;
const negativeRotationCenter = rotationCenter.map((value) => -value);
traces.forEach(trace => {
trace.forEach((point, index) => {
const [x, y] = processCoords(point, (point) => {
return translateCoords(point, negativeRotationCenter);
}, (point) => {
return rotateCoords(point, (0.0001 + index / 200000) * deltaTime);
}, (point) => {
return translateCoords(point, rotationCenter);
});
{
point[0] = x;
point[1] = y;
}
if (index === 0) {
context.beginPath();
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
});
context.stroke();
});
}
const windowResizedListener = () => {
const containerRect = container.getBoundingClientRect();
canvas.width = containerRect.width;
canvas.height = containerRect.width;
drawScope.width = canvas.width;
drawScope.height = canvas.width;
drawScope.middle = [drawScope.width / 2, drawScope.height / 2];
}
window.addEventListener('resize', windowResizedListener);
const frame = () => {
const time = new Date().getTime();
draw(time, drawScope);
requestAnimationFrame(frame);
}
/**
* @typedef {Object} Mouse
* @property {Vector2} pos
* @property {boolean} down
*/
/** @type {Mouse} */
const mouse = {
pos: [0, 0],
down: false,
};
canvas.addEventListener('mousemove', (event) => {
mouse.pos = [event.offsetX, event.offsetY];
});
canvas.addEventListener('mousedown', () => {
mouse.down = true;
startNewTrace();
});
window.addEventListener('mouseup', () => {
mouse.down = false;
});
window.addEventListener('load', () => {
windowResizedListener();
});
frame();