draggable

This commit is contained in:
Emma 2023-02-07 15:59:00 -07:00
parent 2dbaeb57b9
commit ee281b2b19
4 changed files with 364 additions and 153 deletions

381
bundle.js
View File

@ -5,148 +5,6 @@
const Constants = { const Constants = {
TWO_PI: Math.PI * 2 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 { class Vector {
x; x;
y; y;
@ -311,9 +169,8 @@ class Vector {
return new Vector(this.x, this.y, this.z); return new Vector(this.x, this.y, this.z);
} }
drawDot() { drawDot() {
let doodler1 = window['doodler']; if (!doodler) return;
if (!doodler1) return; doodler.dot(this, {
doodler1.dot(this, {
weight: 2, weight: 2,
color: 'red' color: 'red'
}); });
@ -376,12 +233,240 @@ class Vector {
return Vector.dot(Vector.sub(a, b), Vector.sub(a, b)); 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({ init({
width: 400, width: 400,
height: 400 height: 400
}); });
const movingVector = new Vector(100, 300); const movingVector = new Vector(100, 300);
let angleMultiplier = 0; let angleMultiplier = 0;
const v = new Vector(30, 30);
doodler.registerDraggable(v, 20);
doodler.createLayer(()=>{ doodler.createLayer(()=>{
doodler.line(new Vector(100, 100), new Vector(200, 200)); doodler.line(new Vector(100, 100), new Vector(200, 200));
doodler.dot(new Vector(300, 300)); doodler.dot(new Vector(300, 300));
@ -402,3 +487,9 @@ doodler.createLayer(()=>{
movingVector.set((movingVector.x + 1) % 400, movingVector.y); movingVector.set((movingVector.x + 1) % 400, movingVector.y);
angleMultiplier += .001; angleMultiplier += .001;
}); });
document.addEventListener('keyup', (e)=>{
e.preventDefault();
if (e.key === ' ') {
doodler.unregisterDraggable(v);
}
});

121
canvas.ts
View File

@ -5,8 +5,9 @@ import { Constants } from "./geometry/constants.ts";
import { Vector } from "./geometry/vector.ts"; import { Vector } from "./geometry/vector.ts";
export const init = (opt: IDoodlerOptions) => { export const init = (opt: IDoodlerOptions) => {
window['doodler'] = new Doodler(opt); if (window.doodler) throw 'Doodler has already been initialized in this window'
window['doodler'].init(); window.doodler = new Doodler(opt);
window.doodler.init();
} }
interface IDoodlerOptions { interface IDoodlerOptions {
@ -35,6 +36,8 @@ export class Doodler {
return this.ctx.canvas.height; return this.ctx.canvas.height;
} }
private draggables: Draggable[] = [];
constructor({ constructor({
width, width,
height, height,
@ -62,6 +65,17 @@ export class Doodler {
} }
init() { 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(); this.startDrawLoop();
} }
@ -73,11 +87,17 @@ export class Doodler {
private draw() { private draw() {
this.ctx.fillStyle = this.bg; this.ctx.fillStyle = this.bg;
this.ctx.fillRect(0, 0, this.width, this.height); 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()) { for (const [i, l] of (this.layers || []).entries()) {
l(this.ctx, i); l(this.ctx, i);
} }
this.drawUI();
} }
// Layer management
createLayer(layer: layer) { createLayer(layer: layer) {
this.layers.push(layer); this.layers.push(layer);
} }
@ -94,6 +114,8 @@ export class Doodler {
this.layers = temp; this.layers = temp;
} }
// Drawing
line(start: Vector, end: Vector, style?: IStyle) { line(start: Vector, end: Vector, style?: IStyle) {
this.setStyle(style); this.setStyle(style);
this.ctx.beginPath(); this.ctx.beginPath();
@ -163,7 +185,7 @@ export class Doodler {
this.ctx.stroke(); this.ctx.stroke();
} }
drawRotated(origin: Vector, angle: number, cb: () => void){ drawRotated(origin: Vector, angle: number, cb: () => void) {
this.ctx.save(); this.ctx.save();
this.ctx.translate(origin.x, origin.y); this.ctx.translate(origin.x, origin.y);
this.ctx.rotate(angle); this.ctx.rotate(angle);
@ -179,6 +201,82 @@ export class Doodler {
ctx.lineWidth = style?.weight || 1; 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<string, [keyof uiDrawing, ...any]> = 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 { interface IStyle {
@ -186,8 +284,25 @@ interface IStyle {
fillColor?: string; fillColor?: string;
strokeColor?: string; strokeColor?: string;
weight?: number; weight?: number;
noStroke?: boolean;
noFill?: boolean;
} }
interface IDrawable { interface IDrawable {
draw: () => void; 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;
}

View File

@ -197,7 +197,6 @@ export class Vector {
} }
drawDot() { drawDot() {
let doodler = window['doodler'];
if (!doodler) return; if (!doodler) return;
doodler.dot(this, {weight: 2, color: 'red'}); doodler.dot(this, {weight: 2, color: 'red'});

14
main.ts
View File

@ -1,5 +1,3 @@
import { Doodler } from "./canvas.ts";
import { Constants } from "./geometry/constants.ts";
/// <reference types="./global.d.ts" /> /// <reference types="./global.d.ts" />
import { Vector, initializeDoodler } from './mod.ts' import { Vector, initializeDoodler } from './mod.ts'
@ -9,10 +7,10 @@ initializeDoodler({
height: 400 height: 400
}) })
// let doodler = window['doodler'];
const movingVector = new Vector(100, 300); const movingVector = new Vector(100, 300);
let angleMultiplier = 0; let angleMultiplier = 0;
const v = new Vector(30,30);
doodler.registerDraggable(v, 20)
doodler.createLayer(() => { doodler.createLayer(() => {
doodler.line(new Vector(100, 100), new Vector(200, 200)) doodler.line(new Vector(100, 100), new Vector(200, 200))
@ -29,6 +27,14 @@ doodler.createLayer(() => {
doodler.drawCenteredSquare(rotatedOrigin, 30) doodler.drawCenteredSquare(rotatedOrigin, 30)
}) })
movingVector.set((movingVector.x + 1) % 400, movingVector.y); movingVector.set((movingVector.x + 1) % 400, movingVector.y);
angleMultiplier += .001; angleMultiplier += .001;
}); });
document.addEventListener('keyup', e => {
e.preventDefault();
if (e.key === ' ') {
doodler.unregisterDraggable(v);
}
})