From ee281b2b19f9bae490f41b6015f67957dc49ec9b Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 7 Feb 2023 15:59:00 -0700 Subject: [PATCH] draggable --- bundle.js | 381 ++++++++++++++++++++++++++++----------------- canvas.ts | 121 +++++++++++++- geometry/vector.ts | 1 - main.ts | 14 +- 4 files changed, 364 insertions(+), 153 deletions(-) diff --git a/bundle.js b/bundle.js index a2296d7..c418ceb 100644 --- a/bundle.js +++ b/bundle.js @@ -5,148 +5,6 @@ const Constants = { TWO_PI: Math.PI * 2 }; -const init = (opt)=>{ - window['doodler'] = new Doodler(opt); - window['doodler'].init(); -}; -class Doodler { - ctx; - _canvas; - layers = []; - bg; - framerate; - get width() { - return this.ctx.canvas.width; - } - get height() { - return this.ctx.canvas.height; - } - constructor({ width , height , canvas , bg , framerate }){ - if (!canvas) { - canvas = document.createElement('canvas'); - document.body.append(canvas); - } - this.bg = bg || 'white'; - this.framerate = framerate || 60; - canvas.width = width; - canvas.height = height; - this._canvas = canvas; - const ctx = canvas.getContext('2d'); - console.log(ctx); - if (!ctx) throw 'Unable to initialize Doodler: Canvas context not found'; - this.ctx = ctx; - } - init() { - this.startDrawLoop(); - } - timer; - startDrawLoop() { - this.timer = setInterval(()=>this.draw(), 1000 / this.framerate); - } - draw() { - this.ctx.fillStyle = this.bg; - this.ctx.fillRect(0, 0, this.width, this.height); - for (const [i, l] of (this.layers || []).entries()){ - l(this.ctx, i); - } - } - createLayer(layer) { - this.layers.push(layer); - } - deleteLayer(layer) { - this.layers = this.layers.filter((l)=>l !== layer); - } - moveLayer(layer, index) { - let temp = this.layers.filter((l)=>l !== layer); - temp = [ - ...temp.slice(0, index), - layer, - ...temp.slice(index) - ]; - this.layers = temp; - } - line(start, end, style) { - this.setStyle(style); - this.ctx.beginPath(); - this.ctx.moveTo(start.x, start.y); - this.ctx.lineTo(end.x, end.y); - this.ctx.stroke(); - } - dot(at, style) { - this.setStyle({ - ...style, - weight: 1 - }); - this.ctx.beginPath(); - this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants.TWO_PI); - this.ctx.fill(); - } - drawCircle(at, radius, style) { - this.setStyle(style); - this.ctx.beginPath(); - this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI); - this.ctx.stroke(); - } - fillCircle(at, radius, style) { - this.setStyle(style); - this.ctx.beginPath(); - this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI); - this.ctx.fill(); - } - drawRect(at, width, height, style) { - this.setStyle(style); - this.ctx.strokeRect(at.x, at.y, width, height); - } - fillRect(at, width, height, style) { - this.setStyle(style); - this.ctx.fillRect(at.x, at.y, width, height); - } - drawSquare(at, size, style) { - this.drawRect(at, size, size, style); - } - fillSquare(at, size, style) { - this.fillRect(at, size, size, style); - } - drawCenteredRect(at, width, height, style) { - this.ctx.save(); - this.ctx.translate(-width / 2, -height / 2); - this.drawRect(at, width, height, style); - this.ctx.restore(); - } - fillCenteredRect(at, width, height, style) { - this.ctx.save(); - this.ctx.translate(-width / 2, -height / 2); - this.fillRect(at, width, height, style); - this.ctx.restore(); - } - drawCenteredSquare(at, size, style) { - this.drawCenteredRect(at, size, size, style); - } - fillCenteredSquare(at, size, style) { - this.fillCenteredRect(at, size, size, style); - } - drawBezier(a, b, c, d, style) { - this.setStyle(style); - this.ctx.beginPath(); - this.ctx.moveTo(a.x, a.y); - this.ctx.bezierCurveTo(b.x, b.y, c.x, c.y, d.x, d.y); - this.ctx.stroke(); - } - drawRotated(origin, angle, cb) { - this.ctx.save(); - this.ctx.translate(origin.x, origin.y); - this.ctx.rotate(angle); - this.ctx.translate(-origin.x, -origin.y); - cb(); - this.ctx.restore(); - } - setStyle(style) { - const ctx = this.ctx; - ctx.fillStyle = style?.color || style?.fillColor || 'black'; - ctx.strokeStyle = style?.color || style?.strokeColor || 'black'; - ctx.lineWidth = style?.weight || 1; - } -} class Vector { x; y; @@ -311,9 +169,8 @@ class Vector { return new Vector(this.x, this.y, this.z); } drawDot() { - let doodler1 = window['doodler']; - if (!doodler1) return; - doodler1.dot(this, { + if (!doodler) return; + doodler.dot(this, { weight: 2, color: 'red' }); @@ -376,12 +233,240 @@ class Vector { return Vector.dot(Vector.sub(a, b), Vector.sub(a, b)); } } +const init = (opt)=>{ + if (window.doodler) throw 'Doodler has already been initialized in this window'; + window.doodler = new Doodler(opt); + window.doodler.init(); +}; +class Doodler { + ctx; + _canvas; + layers = []; + bg; + framerate; + get width() { + return this.ctx.canvas.width; + } + get height() { + return this.ctx.canvas.height; + } + draggables = []; + constructor({ width , height , canvas , bg , framerate }){ + if (!canvas) { + canvas = document.createElement('canvas'); + document.body.append(canvas); + } + this.bg = bg || 'white'; + this.framerate = framerate || 60; + canvas.width = width; + canvas.height = height; + this._canvas = canvas; + const ctx = canvas.getContext('2d'); + console.log(ctx); + if (!ctx) throw 'Unable to initialize Doodler: Canvas context not found'; + this.ctx = ctx; + } + init() { + this._canvas.addEventListener('mousedown', (e)=>this.onClick(e)); + this._canvas.addEventListener('mouseup', (e)=>this.offClick(e)); + this._canvas.addEventListener('mousemove', (e)=>{ + const rect = this._canvas.getBoundingClientRect(); + this.mouseX = e.clientX - rect.left; + this.mouseY = e.clientY - rect.top; + for (const d of this.draggables.filter((d)=>d.beingDragged)){ + d.point.add(e.movementX, e.movementY); + } + }); + this.startDrawLoop(); + } + timer; + startDrawLoop() { + this.timer = setInterval(()=>this.draw(), 1000 / this.framerate); + } + draw() { + this.ctx.fillStyle = this.bg; + this.ctx.fillRect(0, 0, this.width, this.height); + for (const [i, l] of (this.layers || []).entries()){ + l(this.ctx, i); + } + this.drawUI(); + } + createLayer(layer) { + this.layers.push(layer); + } + deleteLayer(layer) { + this.layers = this.layers.filter((l)=>l !== layer); + } + moveLayer(layer, index) { + let temp = this.layers.filter((l)=>l !== layer); + temp = [ + ...temp.slice(0, index), + layer, + ...temp.slice(index) + ]; + this.layers = temp; + } + line(start, end, style) { + this.setStyle(style); + this.ctx.beginPath(); + this.ctx.moveTo(start.x, start.y); + this.ctx.lineTo(end.x, end.y); + this.ctx.stroke(); + } + dot(at, style) { + this.setStyle({ + ...style, + weight: 1 + }); + this.ctx.beginPath(); + this.ctx.arc(at.x, at.y, style?.weight || 1, 0, Constants.TWO_PI); + this.ctx.fill(); + } + drawCircle(at, radius, style) { + this.setStyle(style); + this.ctx.beginPath(); + this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI); + this.ctx.stroke(); + } + fillCircle(at, radius, style) { + this.setStyle(style); + this.ctx.beginPath(); + this.ctx.arc(at.x, at.y, radius, 0, Constants.TWO_PI); + this.ctx.fill(); + } + drawRect(at, width, height, style) { + this.setStyle(style); + this.ctx.strokeRect(at.x, at.y, width, height); + } + fillRect(at, width, height, style) { + this.setStyle(style); + this.ctx.fillRect(at.x, at.y, width, height); + } + drawSquare(at, size, style) { + this.drawRect(at, size, size, style); + } + fillSquare(at, size, style) { + this.fillRect(at, size, size, style); + } + drawCenteredRect(at, width, height, style) { + this.ctx.save(); + this.ctx.translate(-width / 2, -height / 2); + this.drawRect(at, width, height, style); + this.ctx.restore(); + } + fillCenteredRect(at, width, height, style) { + this.ctx.save(); + this.ctx.translate(-width / 2, -height / 2); + this.fillRect(at, width, height, style); + this.ctx.restore(); + } + drawCenteredSquare(at, size, style) { + this.drawCenteredRect(at, size, size, style); + } + fillCenteredSquare(at, size, style) { + this.fillCenteredRect(at, size, size, style); + } + drawBezier(a, b, c, d, style) { + this.setStyle(style); + this.ctx.beginPath(); + this.ctx.moveTo(a.x, a.y); + this.ctx.bezierCurveTo(b.x, b.y, c.x, c.y, d.x, d.y); + this.ctx.stroke(); + } + drawRotated(origin, angle, cb) { + this.ctx.save(); + this.ctx.translate(origin.x, origin.y); + this.ctx.rotate(angle); + this.ctx.translate(-origin.x, -origin.y); + cb(); + this.ctx.restore(); + } + setStyle(style) { + const ctx = this.ctx; + ctx.fillStyle = style?.color || style?.fillColor || 'black'; + ctx.strokeStyle = style?.color || style?.strokeColor || 'black'; + ctx.lineWidth = style?.weight || 1; + } + mouseX = 0; + mouseY = 0; + registerDraggable(point, radius, style) { + const id = this.addUIElement('circle', point, radius, { + fillColor: '#5533ff50', + strokeColor: '#5533ff50' + }); + this.draggables.push({ + point, + radius, + style, + id + }); + } + unregisterDraggable(point) { + for (const d of this.draggables){ + if (d.point === point) { + this.removeUIElement(d.id); + } + } + this.draggables = this.draggables.filter((d)=>d.point !== point); + } + onClick(e) { + const rect = this._canvas.getBoundingClientRect(); + e.clientX - rect.left; + e.clientY - rect.top; + for (const d of this.draggables){ + if (d.point.dist(new Vector(this.mouseX, this.mouseY)) <= d.radius) { + d.beingDragged = true; + } else d.beingDragged = false; + } + } + offClick(e) { + for (const d of this.draggables){ + d.beingDragged = false; + } + } + uiElements = new Map(); + uiDrawing = { + rectangle: (...args)=>{ + !args[3].noFill && this.fillRect(args[0], args[1], args[2], args[3]); + !args[3].noStroke && this.drawRect(args[0], args[1], args[2], args[3]); + }, + square: (...args)=>{ + !args[2].noFill && this.fillSquare(args[0], args[1], args[2]); + !args[2].noStroke && this.drawSquare(args[0], args[1], args[2]); + }, + circle: (...args)=>{ + !args[2].noFill && this.fillCircle(args[0], args[1], args[2]); + !args[2].noStroke && this.drawCircle(args[0], args[1], args[2]); + } + }; + drawUI() { + for (const [shape, ...args] of this.uiElements.values()){ + this.uiDrawing[shape].apply(null, args); + } + } + addUIElement(shape, ...args) { + const id = crypto.randomUUID(); + for (const arg of args){ + delete arg.color; + } + this.uiElements.set(id, [ + shape, + ...args + ]); + return id; + } + removeUIElement(id) { + this.uiElements.delete(id); + } +} init({ width: 400, height: 400 }); const movingVector = new Vector(100, 300); let angleMultiplier = 0; +const v = new Vector(30, 30); +doodler.registerDraggable(v, 20); doodler.createLayer(()=>{ doodler.line(new Vector(100, 100), new Vector(200, 200)); doodler.dot(new Vector(300, 300)); @@ -402,3 +487,9 @@ doodler.createLayer(()=>{ movingVector.set((movingVector.x + 1) % 400, movingVector.y); angleMultiplier += .001; }); +document.addEventListener('keyup', (e)=>{ + e.preventDefault(); + if (e.key === ' ') { + doodler.unregisterDraggable(v); + } +}); diff --git a/canvas.ts b/canvas.ts index db4f2a5..93cdf26 100644 --- a/canvas.ts +++ b/canvas.ts @@ -5,8 +5,9 @@ import { Constants } from "./geometry/constants.ts"; import { Vector } from "./geometry/vector.ts"; export const init = (opt: IDoodlerOptions) => { - window['doodler'] = new Doodler(opt); - window['doodler'].init(); + if (window.doodler) throw 'Doodler has already been initialized in this window' + window.doodler = new Doodler(opt); + window.doodler.init(); } interface IDoodlerOptions { @@ -35,6 +36,8 @@ export class Doodler { return this.ctx.canvas.height; } + private draggables: Draggable[] = []; + constructor({ width, height, @@ -62,6 +65,17 @@ export class Doodler { } init() { + this._canvas.addEventListener('mousedown', e => this.onClick(e)); + this._canvas.addEventListener('mouseup', e => this.offClick(e)); + this._canvas.addEventListener('mousemove', e => { + const rect = this._canvas.getBoundingClientRect(); + this.mouseX = e.clientX - rect.left; + this.mouseY = e.clientY - rect.top; + + for (const d of this.draggables.filter(d => d.beingDragged)) { + d.point.add(e.movementX, e.movementY) + } + }) this.startDrawLoop(); } @@ -73,11 +87,17 @@ export class Doodler { private draw() { this.ctx.fillStyle = this.bg; this.ctx.fillRect(0, 0, this.width, this.height); + // for (const d of this.draggables.filter(d => d.beingDragged)) { + // d.point.set(this.mouseX,this.mouseY); + // } for (const [i, l] of (this.layers || []).entries()) { l(this.ctx, i); } + this.drawUI(); } + // Layer management + createLayer(layer: layer) { this.layers.push(layer); } @@ -94,6 +114,8 @@ export class Doodler { this.layers = temp; } + // Drawing + line(start: Vector, end: Vector, style?: IStyle) { this.setStyle(style); this.ctx.beginPath(); @@ -163,7 +185,7 @@ export class Doodler { this.ctx.stroke(); } - drawRotated(origin: Vector, angle: number, cb: () => void){ + drawRotated(origin: Vector, angle: number, cb: () => void) { this.ctx.save(); this.ctx.translate(origin.x, origin.y); this.ctx.rotate(angle); @@ -179,6 +201,82 @@ export class Doodler { ctx.lineWidth = style?.weight || 1; } + + // Interaction + + mouseX = 0; + mouseY = 0; + + + registerDraggable(point: Vector, radius: number, style?: IStyle & { shape: 'square' | 'circle' }) { + const id = this.addUIElement('circle', point, radius, { fillColor: '#5533ff50', strokeColor: '#5533ff50' }) + this.draggables.push({ point, radius, style, id }); + } + unregisterDraggable(point: Vector) { + for (const d of this.draggables) { + if (d.point === point) { + this.removeUIElement(d.id); + } + } + this.draggables = this.draggables.filter(d => d.point !== point); + } + + onClick(e: MouseEvent) { + const rect = this._canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + for (const d of this.draggables) { + if (d.point.dist(new Vector(this.mouseX, this.mouseY)) <= d.radius) { + d.beingDragged = true; + } else d.beingDragged = false; + } + } + + offClick(e:MouseEvent){ + for (const d of this.draggables) { + d.beingDragged = false; + } + } + + // UI Layer + uiElements: Map = new Map(); + private uiDrawing: uiDrawing = { + rectangle: (...args: any[]) => { + !args[3].noFill && this.fillRect(args[0], args[1], args[2], args[3]) + !args[3].noStroke && this.drawRect(args[0], args[1], args[2], args[3]) + }, + square: (...args: any[]) => { + !args[2].noFill && this.fillSquare(args[0], args[1], args[2]) + !args[2].noStroke && this.drawSquare(args[0], args[1], args[2]) + }, + circle: (...args: any[]) => { + !args[2].noFill && this.fillCircle(args[0], args[1], args[2]) + !args[2].noStroke && this.drawCircle(args[0], args[1], args[2]) + }, + } + + private drawUI() { + for (const [shape, ...args] of this.uiElements.values()) { + this.uiDrawing[shape].apply(null, args as []) + } + } + + addUIElement(shape: 'rectangle', at: Vector, width: number, height: number, style?: IStyle): string; + addUIElement(shape: 'square', at: Vector, size: number, style?: IStyle): string; + addUIElement(shape: 'circle', at: Vector, radius: number, style?: IStyle): string; + addUIElement(shape: keyof uiDrawing, ...args: any[]) { + const id = crypto.randomUUID(); + for (const arg of args) { + delete arg.color; + } + this.uiElements.set(id, [shape, ...args]); + return id; + } + + removeUIElement(id: string) { + this.uiElements.delete(id); + } } interface IStyle { @@ -186,8 +284,25 @@ interface IStyle { fillColor?: string; strokeColor?: string; weight?: number; + + noStroke?: boolean; + noFill?: boolean; } interface IDrawable { draw: () => void; } + +type Draggable = { + point: Vector; + radius: number; + style?: IStyle & { shape: 'square' | 'circle' }; + beingDragged?: boolean; + id: string; +} + +type uiDrawing = { + circle: () => void; + square: () => void; + rectangle: () => void; +} diff --git a/geometry/vector.ts b/geometry/vector.ts index 3d4cf63..20f048c 100644 --- a/geometry/vector.ts +++ b/geometry/vector.ts @@ -197,7 +197,6 @@ export class Vector { } drawDot() { - let doodler = window['doodler']; if (!doodler) return; doodler.dot(this, {weight: 2, color: 'red'}); diff --git a/main.ts b/main.ts index 108be21..8922b85 100644 --- a/main.ts +++ b/main.ts @@ -1,5 +1,3 @@ -import { Doodler } from "./canvas.ts"; -import { Constants } from "./geometry/constants.ts"; /// import { Vector, initializeDoodler } from './mod.ts' @@ -9,10 +7,10 @@ initializeDoodler({ height: 400 }) -// let doodler = window['doodler']; - const movingVector = new Vector(100, 300); let angleMultiplier = 0; +const v = new Vector(30,30); +doodler.registerDraggable(v, 20) doodler.createLayer(() => { doodler.line(new Vector(100, 100), new Vector(200, 200)) @@ -29,6 +27,14 @@ doodler.createLayer(() => { doodler.drawCenteredSquare(rotatedOrigin, 30) }) + movingVector.set((movingVector.x + 1) % 400, movingVector.y); angleMultiplier += .001; }); + +document.addEventListener('keyup', e => { + e.preventDefault(); + if (e.key === ' ') { + doodler.unregisterDraggable(v); + } +})