diff --git a/client/declaration-merging/document-extended.d.ts b/client/declaration-merging/document-extended.d.ts index 61609af3..0eab1b24 100644 --- a/client/declaration-merging/document-extended.d.ts +++ b/client/declaration-merging/document-extended.d.ts @@ -10,7 +10,7 @@ */ declare function create( tag: K, - attrs?: any, + attrs?: any ): HTMLElementTagNameMap[K]; /** * Creates an element with the specified tag name. @@ -23,7 +23,7 @@ declare function create( */ declare function create( tag: string, - attrs?: any, + attrs?: any ): HTMLElementDeprecatedTagNameMap[K]; /** * Creates an element with the specified tag name. @@ -58,7 +58,7 @@ declare function create(html: string, attrs?: any): HTMLElement; */ declare function createDeep( tag: string, - attrs?: any, + attrs?: any ): ( | HTMLElementDeprecatedTagNameMap[keyof HTMLElementDeprecatedTagNameMap] | HTMLElementTagNameMap[keyof HTMLElementTagNameMap] @@ -74,7 +74,7 @@ declare function createDeep( * @returns {HTMLElementTagNameMap[K]} */ declare function find( - selector: K, + selector: K ): HTMLElementTagNameMap[K]; /** * Using querySelector, returns the first element that matches the selector @@ -85,7 +85,7 @@ declare function find( * @returns {HTMLElementDeprecatedTagNameMap[K]} */ declare function find( - selector: string, + selector: string ): HTMLElementDeprecatedTagNameMap[K]; /** * Using querySelector, returns the first element that matches the selector @@ -105,7 +105,7 @@ declare function find(selector: string): HTMLElement; * @returns {HTMLElementTagNameMap[K][]} */ declare function findAll( - selector: K, + selector: K ): HTMLElementTagNameMap[K][]; /** @@ -117,7 +117,7 @@ declare function findAll( * @returns {HTMLElementDeprecatedTagNameMap[K][]} */ declare function findAll( - selector: string, + selector: string ): HTMLElementDeprecatedTagNameMap[K][]; /** diff --git a/client/declaration-merging/pdfjslib.d.ts b/client/declaration-merging/pdfjslib.d.ts index 27543ca5..c5f35a70 100644 --- a/client/declaration-merging/pdfjslib.d.ts +++ b/client/declaration-merging/pdfjslib.d.ts @@ -26,9 +26,9 @@ export interface PdfJsPage { scale: | number | { - scale: number; - rotation: number; - }, + scale: number; + rotation: number; + } ): PdfJsViewport; getTextContent(): Promise; getAnnotations(): Promise; diff --git a/client/models/canvas/border.ts b/client/models/canvas/border.ts new file mode 100644 index 00000000..57d230ac --- /dev/null +++ b/client/models/canvas/border.ts @@ -0,0 +1,37 @@ +import { Point2D } from '../../../shared/submodules/calculations/src/linear-algebra/point'; +import { Polygon } from './polygon'; + +export class Border extends Polygon { + isIn(point: Point2D) { + return !super.isIn(point); + } + + draw(ctx: CanvasRenderingContext2D) { + const region = new Path2D(); + + region.moveTo( + this.points[0][0] * ctx.canvas.width, + this.points[0][1] * ctx.canvas.height, + ); + + for (let i = 1; i < this.points.length; i++) { + region.lineTo( + this.points[i][0] * ctx.canvas.width, + this.points[i][1] * ctx.canvas.height, + ); + } + region.closePath(); + + region.moveTo(0, 0); + region.lineTo(ctx.canvas.width, 0); + region.lineTo(ctx.canvas.width, ctx.canvas.height); + region.lineTo(0, ctx.canvas.height); + region.closePath(); + + // fill the area between the polygon and the canvas edges + if (this.$properties?.fill?.color) { + ctx.fillStyle = this.$properties.fill.color; + } + if (this.$properties?.fill) ctx.fill(region, 'evenodd'); + } +} diff --git a/client/models/canvas/canvas.ts b/client/models/canvas/canvas.ts new file mode 100644 index 00000000..115137c8 --- /dev/null +++ b/client/models/canvas/canvas.ts @@ -0,0 +1,521 @@ +import { Drawable } from './drawable'; +import { EventEmitter } from '../../../shared/event-emitter'; +import { attempt } from '../../../shared/attempt'; +import { Point2D } from '../../../shared/submodules/calculations/src/linear-algebra/point'; +import { DrawableEvent } from './drawable'; + +/** + * Description placeholder + * @date 1/25/2024 - 12:50:19 PM + * + * @typedef {CanvasEvents} + */ +type CanvasEvents = { + animatestart: void; + animateend: void; + draw: void; + click: DrawableEvent; + touchstart: DrawableEvent; + touchmove: DrawableEvent; + touchend: DrawableEvent; + touchcancel: DrawableEvent; + mousemove: DrawableEvent; + mousedown: DrawableEvent; + mouseup: DrawableEvent; + mouseover: DrawableEvent; + mouseleave: DrawableEvent; + mouseenter: DrawableEvent; +}; + +/** + * Options for the canvas + * @date 1/25/2024 - 12:50:19 PM + * + * @typedef {CanvasOptions} + */ +type CanvasOptions = { + /** + * All events that the canvas should listen for (this will be deduped) + * @date 1/25/2024 - 12:51:53 PM + * + * @type {(keyof CanvasEvents)[]} + */ + events: (keyof CanvasEvents)[]; +}; + +/** + * A class to manage the canvas and drawables + * @date 1/25/2024 - 12:50:19 PM + * + * @export + * @class Canvas + * @typedef {Canvas} + */ +export class Canvas { + /** + * All drawables on the canvas + * @date 1/25/2024 - 12:50:19 PM + * + * @public + * @readonly + * @type {Drawable[]} + */ + public readonly $drawables: Drawable[] = []; + /** + * Emitter for canvas events + * @date 1/25/2024 - 12:50:19 PM + * + * @public + * @readonly + * @type {*} + */ + public readonly $emitter = new EventEmitter(); + /** + * Animation status + * @date 1/25/2024 - 12:50:19 PM + * + * @public + * @type {boolean} + */ + public $animating = false; + /** + * Frames per second (default 60) + * @date 1/25/2024 - 12:50:19 PM + * + * @public + * @type {number} + */ + public $fps = 60; + /** + * Canvas element + * @date 1/25/2024 - 12:50:19 PM + * + * @public + * @readonly + * @type {HTMLCanvasElement} + */ + public readonly $canvas: HTMLCanvasElement; + /** + * Canvas context + * @date 1/25/2024 - 12:50:19 PM + * + * @public + * @readonly + * @type {CanvasRenderingContext2D} + */ + public readonly $ctx: CanvasRenderingContext2D; + /** + * Options for the canvas + * @date 1/25/2024 - 12:50:19 PM + * + * @public + * @readonly + * @type {Partial} + */ + public readonly $options: Partial; + + /** + * Creates an instance of Canvas. + * @date 1/25/2024 - 12:50:19 PM + * + * @constructor + * @param {CanvasRenderingContext2D} ctx + * @param {Partial} [options={}] + */ + constructor( + ctx: CanvasRenderingContext2D, + options: Partial = {}, + ) { + this.$canvas = ctx.canvas; + this.$ctx = ctx; + this.$options = options; + + if (this.$options.events) { + this.$options.events = this.$options.events.filter( + (e, i, a) => a.indexOf(e) === i, + ); + for (const event of this.$options.events) { + switch (event) { + case 'click': + this.$canvas.addEventListener('click', (event) => { + const e = new DrawableEvent(event); + this.emit('click', e); + const point = this.getXY(event)[0]; + for (const drawable of this.$drawables) { + if (drawable.$doDraw && drawable.isIn(point)) { + drawable.emit('click', e); + } + } + }); + break; + case 'touchstart': + this.$canvas.addEventListener('touchstart', (event) => { + const e = new DrawableEvent(event); + this.emit('touchstart', e); + const points = this.getXY(event); + for (const drawable of this.$drawables) { + if ( + drawable.$doDraw && + points.some((point) => drawable.isIn(point)) + ) { + drawable.emit('touchstart', e); + } + } + }); + break; + case 'touchmove': + this.$canvas.addEventListener('touchmove', (event) => { + const e = new DrawableEvent(event); + this.emit('touchmove', e); + const points = this.getXY(event); + for (const drawable of this.$drawables) { + if ( + drawable.$doDraw && + points.some((point) => drawable.isIn(point)) + ) { + drawable.emit('touchmove', e); + } + } + }); + break; + case 'touchend': + this.$canvas.addEventListener('touchend', (event) => { + const e = new DrawableEvent(event); + this.emit('touchend', e); + const points = this.getXY(event); + for (const drawable of this.$drawables) { + if ( + drawable.$doDraw && + points.some((point) => drawable.isIn(point)) + ) { + drawable.emit('touchend', e); + } + } + }); + break; + case 'touchcancel': + this.$canvas.addEventListener( + 'touchcancel', + (event) => { + const e = new DrawableEvent(event); + this.emit('touchcancel', e); + const points = this.getXY(event); + for (const drawable of this.$drawables) { + if ( + drawable.$doDraw && + points.some((point) => + drawable.isIn(point) + ) + ) { + drawable.emit('touchcancel', e); + } + } + }, + ); + break; + case 'mousemove': + this.$canvas.addEventListener('mousemove', (event) => { + const e = new DrawableEvent(event); + this.emit('mousemove', e); + const point = this.getXY(event)[0]; + for (const drawable of this.$drawables) { + if (drawable.$doDraw && drawable.isIn(point)) { + drawable.emit('mousemove', e); + } + } + }); + break; + case 'mousedown': + this.$canvas.addEventListener('mousedown', (event) => { + const e = new DrawableEvent(event); + this.emit('mousedown', e); + const point = this.getXY(event)[0]; + for (const drawable of this.$drawables) { + if (drawable.$doDraw && drawable.isIn(point)) { + drawable.emit('mousedown', e); + } + } + }); + break; + case 'mouseup': + this.$canvas.addEventListener('mouseup', (event) => { + const e = new DrawableEvent(event); + this.emit('mouseup', e); + const point = this.getXY(event)[0]; + for (const drawable of this.$drawables) { + if (drawable.$doDraw && drawable.isIn(point)) { + drawable.emit('mouseup', e); + } + } + }); + break; + case 'mouseleave': + this.$canvas.addEventListener('mouseleave', (event) => { + const e = new DrawableEvent(event); + this.emit('mouseleave', e); + const point = this.getXY(event)[0]; + for (const drawable of this.$drawables) { + if (drawable.$doDraw && drawable.isIn(point)) { + drawable.emit('mouseleave', e); + } + } + }); + break; + case 'mouseenter': + this.$canvas.addEventListener('mouseenter', (event) => { + const e = new DrawableEvent(event); + this.emit('mouseenter', e); + const point = this.getXY(event)[0]; + for (const drawable of this.$drawables) { + if (drawable.$doDraw && drawable.isIn(point)) { + drawable.emit('mouseenter', e); + } + } + }); + break; + } + } + } + } + + /** + * Width of the canvas + * @date 1/25/2024 - 12:50:19 PM + * + * @type {number} + */ + get width() { + return this.$canvas.width; + } + + /** + * Width of the canvas + * @date 1/25/2024 - 12:50:19 PM + * + * @type {number} + */ + set width(width: number) { + this.$canvas.width = width; + } + + /** + * Height of the canvas + * @date 1/25/2024 - 12:50:18 PM + * + * @type {number} + */ + get height() { + return this.$canvas.height; + } + + /** + * Height of the canvas + * @date 1/25/2024 - 12:50:18 PM + * + * @type {number} + */ + set height(height: number) { + this.$canvas.height = height; + } + + /** + * Adds an event listener to the canvas + * @date 1/25/2024 - 12:50:18 PM + * + * @template {keyof CanvasEvents} K + * @param {K} event + * @param {(data: CanvasEvents[K]) => void} listener + * @returns {void) => void} + */ + on( + event: K, + listener: (data: CanvasEvents[K]) => void, + ) { + this.$emitter.on(event, listener); + } + + /** + * Removes an event listener from the canvas + * @date 1/25/2024 - 12:50:18 PM + * + * @template {keyof CanvasEvents} K + * @param {K} event + * @param {(data: CanvasEvents[K]) => void} listener + * @returns {void) => void} + */ + off( + event: K, + listener: (data: CanvasEvents[K]) => void, + ) { + this.$emitter.off(event, listener); + } + + /** + * Listens for an event once + * @date 1/25/2024 - 12:50:18 PM + * + * @template {keyof CanvasEvents} K + * @param {K} event + * @param {(data: CanvasEvents[K]) => void} listener + * @returns {void) => void} + */ + once( + event: K, + listener: (data: CanvasEvents[K]) => void, + ) { + this.$emitter.once(event, listener); + } + + /** + * Emits an event + * @date 1/25/2024 - 12:50:18 PM + * + * @template {keyof CanvasEvents} K + * @param {K} event + * @param {CanvasEvents[K]} data + */ + emit(event: K, data: CanvasEvents[K]) { + this.$emitter.emit(event, data); + } + + /** + * Adds drawables to the canvas + * @date 1/25/2024 - 12:50:18 PM + * + * @param {...Drawable[]} drawables + */ + add(...drawables: Drawable[]) { + this.$drawables.push(...drawables); + } + + /** + * Removes drawables from the canvas + * @date 1/25/2024 - 12:50:18 PM + * + * @param {...Drawable[]} drawables + */ + remove(...drawables: Drawable[]) { + for (const drawable of drawables) { + const index = this.$drawables.indexOf(drawable); + if (index !== -1) { + this.$drawables.splice(index, 1); + } + } + } + + /** + * Clears the canvas context + * @date 1/25/2024 - 12:50:18 PM + */ + clear() { + this.$ctx.clearRect(0, 0, this.width, this.height); + } + + /** + * Removes all drawables from the canvas + * @date 1/25/2024 - 12:50:18 PM + */ + clearDrawables() { + this.$drawables.length = 0; + } + + /** + * Draws all drawables on the canvas + * @date 1/25/2024 - 12:50:18 PM + */ + draw() { + this.clear(); + for (const drawable of this.$drawables) { + this.$ctx.save(); + + // forces the canvas to draw the drawable at a lower opacity + const fadeScale = drawable.$currentFadeFrame / drawable.$fadeFrames; + if (fadeScale < 1) { + this.$ctx.globalAlpha = fadeScale; + } else { + this.$ctx.globalAlpha = 1; + } + + if (!drawable.$doDraw) { + this.$ctx.globalAlpha = 0; + } + + if (drawable.$properties?.doDraw) { + if (!drawable.$properties.doDraw(drawable)) { + this.$ctx.globalAlpha = 0; + } + } + + const res = attempt(() => drawable.draw(this.$ctx)); + this.$ctx.restore(); + if (res.isOk()) { + if ( + drawable.$currentFadeFrame > 0 && + drawable.$currentFadeFrame < drawable.$fadeFrames + ) { + drawable.$currentFadeFrame += drawable.$fadeDirection; + } + + if (!drawable.$drawn) { + drawable.$drawn = true; + drawable.emit('draw', undefined); + } + } + } + } + + /** + * Returns normalized points from a mouse or touch event + * @date 1/25/2024 - 12:50:18 PM + * + * @param {(MouseEvent | TouchEvent)} e + * @returns {Point2D[]} + */ + getXY(e: MouseEvent | TouchEvent): Point2D[] { + const rect = this.$ctx.canvas.getBoundingClientRect(); + + const makePoint = (x: number, y: number): [number, number] => { + return [(x - rect.left) / rect.width, (y - rect.top) / rect.height]; + }; + + if (e instanceof MouseEvent) { + return [makePoint(e.clientX, e.clientY)]; + } else { + return Array.from(e.touches).map((touch) => + makePoint(touch.clientX, touch.clientY) + ); + } + } + + /** + * Removes all drawables, clears, and stops animation + * @date 1/25/2024 - 12:50:18 PM + */ + destroy() { + this.clearDrawables(); + this.clear(); + this.$animating = false; + } + + /** + * Starts animating the canvas, returns a function to stop the animation + * @date 1/25/2024 - 12:50:18 PM + * + * @returns {() => void} + */ + animate(): () => void { + const stop = () => (this.$animating = false); + if (this.$animating) return stop; + + this.$animating = true; + const loop = async () => { + if (!this.$animating) return; + this.clear(); + this.draw(); + // update?.(this); + requestAnimationFrame(loop); + }; + requestAnimationFrame(loop); + return stop; + } +} diff --git a/client/models/canvas/circle.ts b/client/models/canvas/circle.ts new file mode 100644 index 00000000..6ee2b16b --- /dev/null +++ b/client/models/canvas/circle.ts @@ -0,0 +1,98 @@ +import { Drawable } from './drawable'; +import { Point2D } from '../../../shared/submodules/calculations/src/linear-algebra/point'; + +export class Circle extends Drawable { + /** + * Creates an instance of Circle. + * @date 1/9/2024 - 11:47:29 AM + * + * @constructor + * @param {Point2D} center [x, y] normalized coordinates + * @param {number} radius normalized radius of the circle to the height of the canvas + */ + constructor( + public center: Point2D, + public radius: number, + ) { + super(); + } + + /** + * If the given point is inside the circle + * @date 1/9/2024 - 11:47:29 AM + * + * @param {Point2D} point + * @returns {boolean} + */ + public isIn(point: Point2D) { + const [x, y] = point; + return ( + Math.sqrt(Math.pow(x - this.x, 2) + Math.pow(y - this.y, 2)) < + this.radius + ); + } + + /** + * Draw the circle + * @date 1/9/2024 - 11:47:29 AM + * + * @param {CanvasRenderingContext2D} context + */ + public draw(context: CanvasRenderingContext2D) { + context.beginPath(); + if (this.$properties?.line?.color) { + context.strokeStyle = this.$properties.line.color; + } + if (this.$properties?.line?.width) { + context.lineWidth = this.$properties.line.width; + } + if (this.$properties?.fill?.color) { + context.fillStyle = this.$properties.fill.color; + } + context.arc( + this.x * context.canvas.width, + this.y * context.canvas.height, + this.radius * context.canvas.height, + 0, + 2 * Math.PI, + ); + if (this.$properties?.fill) context.fill(); + context.stroke(); + } + + /** + * Returns the x coordinate of the center + * @date 1/9/2024 - 11:47:29 AM + * + * @readonly + * @type {Point2D} + */ + get x() { + return this.center[0]; + } + + /** + * Returns the x coordinate of the center + */ + set x(x: number) { + this.center[0] = x; + } + + /** + * Returns the y coordinate of the center + * @date 1/9/2024 - 11:47:29 AM + * + * @readonly + * @type {Point2D} + */ + get y() { + return this.center[1]; + } + + /** + * Returns the y coordinate of the center + */ + set y(y: number) { + this.center[1] = y; + } +} diff --git a/client/models/canvas/drawable.ts b/client/models/canvas/drawable.ts new file mode 100644 index 00000000..bed2bd06 --- /dev/null +++ b/client/models/canvas/drawable.ts @@ -0,0 +1,226 @@ +import { Point2D } from '../../../shared/submodules/calculations/src/linear-algebra/point'; +import { EventEmitter } from '../../../shared/event-emitter'; +import { ShapeProperties } from './properties'; + +/** + * Event wrapper for drawable events + * @date 1/25/2024 - 1:25:32 PM + * + * @export + * @class CanvasEvent + * @typedef {DrawableEvent} + * @template [event=unknown] + */ +export class DrawableEvent { + /** + * Creates an instance of CanvasEvent. + * @date 1/25/2024 - 1:25:32 PM + * + * @constructor + * @param {event} $event + */ + constructor(public readonly $event: event) {} +} + +/** + * All drawable events + * @date 1/25/2024 - 1:25:32 PM + * + * @typedef {DrawableEvents} + */ +type DrawableEvents = { + draw: void; + click: DrawableEvent; + touchstart: DrawableEvent; + touchmove: DrawableEvent; + touchend: DrawableEvent; + touchcancel: DrawableEvent; + mousemove: DrawableEvent; + mousedown: DrawableEvent; + mouseup: DrawableEvent; + mouseover: DrawableEvent; + mouseleave: DrawableEvent; + mouseenter: DrawableEvent; +}; + +/** + * A blank drawable + * @date 1/25/2024 - 1:25:32 PM + * + * @export + * @class Drawable + * @typedef {Drawable} + * @template [T=unknown] + */ +export class Drawable { + /** + * Event emitter + * @date 1/25/2024 - 1:25:32 PM + * + * @public + * @readonly + * @type {*} + */ + public readonly $emitter = new EventEmitter(); + /** + * Draw the drawable + * @date 1/25/2024 - 1:25:32 PM + * + * @public + * @type {boolean} + */ + public $doDraw = true; + /** + * If the drawable has been drawn + * @date 1/25/2024 - 1:25:32 PM + * + * @public + * @type {boolean} + */ + public $drawn = false; + /** + * If the drawable is fading in or out + * @date 1/25/2024 - 1:25:32 PM + * + * @public + * @type {(-1 | 0 | 1)} + */ + public $fadeDirection: -1 | 0 | 1 = 0; // -1 = fade out, 0 = no fade, 1 = fade in + /** + * The number of frames to fade in or out + * @date 1/25/2024 - 1:25:32 PM + * + * @public + * @type {number} + */ + public $fadeFrames = 0; + /** + * The current frame of the fade + * @date 1/25/2024 - 1:25:32 PM + * + * @public + * @type {number} + */ + public $currentFadeFrame = 0; + /** + * All properties of the drawable + * @date 1/25/2024 - 1:25:32 PM + * + * @public + * @readonly + * @type {Partial>} + */ + public readonly $properties: Partial> = {}; + + /** + * Add a listener to the given event + * @date 1/25/2024 - 1:25:32 PM + * + * @template {keyof DrawableEvents} K + * @param {K} event + * @param {(data: DrawableEvents[K]) => void} listener + * @returns {void) => void} + */ + on( + event: K, + listener: (data: DrawableEvents[K]) => void, + ) { + this.$emitter.on(event, listener); + } + + /** + * Remove a listener from the given event + * @date 1/25/2024 - 1:25:32 PM + * + * @template {keyof DrawableEvents} K + * @param {K} event + * @param {(data: DrawableEvents[K]) => void} listener + * @returns {void) => void} + */ + off( + event: K, + listener: (data: DrawableEvents[K]) => void, + ) { + this.$emitter.off(event, listener); + } + + /** + * Add a listener to the given event that will only be called once + * @date 1/25/2024 - 1:25:32 PM + * + * @template {keyof DrawableEvents} K + * @param {K} event + * @param {(data: DrawableEvents[K]) => void} listener + * @returns {void) => void} + */ + once( + event: K, + listener: (data: DrawableEvents[K]) => void, + ) { + this.$emitter.once(event, listener); + } + + /** + * Emit the given event with the given data + * @date 1/25/2024 - 1:25:32 PM + * + * @template {keyof DrawableEvents} K + * @param {K} event + * @param {DrawableEvents[K]} data + */ + emit(event: K, data: DrawableEvents[K]) { + this.$emitter.emit(event, data); + } + + /** + * Draw the drawable (must be implemented by child) + * @date 1/25/2024 - 1:25:32 PM + * + * @param {CanvasRenderingContext2D} _ctx + */ + draw(_ctx: CanvasRenderingContext2D): void { + console.warn('Method not implemented on ' + this.constructor.name); + } + + /** + * Returns if the given point is inside the drawable (must be implemented by child) + * @date 1/25/2024 - 1:25:32 PM + * + * @param {Point2D} _point + * @returns {boolean} + */ + isIn(_point: Point2D): boolean { + console.warn('Method not implemented on ' + this.constructor.name); + return false; + } + + /** + * Hide the drawable + * @date 1/25/2024 - 1:25:32 PM + */ + hide() { + this.$doDraw = false; + this.$fadeDirection = -1; + this.$currentFadeFrame = this.$fadeFrames; + } + + /** + * Show the drawable + * @date 1/25/2024 - 1:25:32 PM + */ + show() { + this.$doDraw = true; + this.$fadeDirection = 1; + this.$currentFadeFrame = this.$fadeFrames; + } + + /** + * Fade the drawable in or out + * @date 1/25/2024 - 1:25:32 PM + * + * @param {number} frames + */ + fade(frames: number) { + this.$fadeFrames = frames; + } +} diff --git a/client/models/canvas/image.ts b/client/models/canvas/image.ts new file mode 100644 index 00000000..741ea593 --- /dev/null +++ b/client/models/canvas/image.ts @@ -0,0 +1,165 @@ +import { Drawable } from './drawable'; +import { Point2D } from '../../../shared/submodules/calculations/src/linear-algebra/point'; + +/** + * Location and size of the image + * @date 1/9/2024 - 11:48:39 AM + * + * @typedef {CanvasImgOptions} + */ +export type CanvasImgOptions = { + x: number; + y: number; + width: number; + height: number; +}; + +export class Img extends Drawable { + /** + * Description placeholder + * @date 1/9/2024 - 11:48:39 AM + * + * @public + * @readonly + * @type {HTMLImageElement} + */ + public readonly img: HTMLImageElement; + private data: HTMLImageElement | null = null; + + constructor( + public readonly src: string, + public readonly options: Partial = {}, + ) { + super(); + + this.img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = this.img.width; + canvas.height = this.img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.drawImage(this.img, 0, 0); + // to data url + const i = document.createElement('img'); + i.src = canvas.toDataURL(); + this.data = i; + }; + } + + /** + * X coordinate of the image (left side) + * @date 1/9/2024 - 11:50:23 AM + * + * @type {number} + */ + get x() { + return this.options.x ?? 0; + } + + /** + * X coordinate of the image (left side) + * @date 1/9/2024 - 11:50:23 AM + * + * @type {number} + */ + set x(x: number) { + this.options.x = x; + } + + /** + * Y coordinate of the image (top side) + * @date 1/9/2024 - 11:50:23 AM + * + * @type {number} + */ + get y() { + return this.options.y ?? 0; + } + + /** + * Y coordinate of the image (top side) + * @date 1/9/2024 - 11:50:23 AM + * + * @type {number} + */ + set y(y: number) { + this.options.y = y; + } + + /** + * Width of the image (0 - 1) + * @date 1/9/2024 - 11:50:23 AM + * + * @type {number} + */ + get width() { + return this.options.width ?? this.img.width; + } + + /** + * Width of the image (0 - 1) + * @date 1/9/2024 - 11:50:23 AM + * + * @type {number} + */ + set width(width: number) { + this.options.width = width; + } + + /** + * Height of the image (0 - 1) + * @date 1/9/2024 - 11:50:23 AM + * + * @type {number} + */ + get height() { + return this.options.height ?? this.img.height; + } + + /** + * Height of the image (0 - 1) + * @date 1/9/2024 - 11:50:23 AM + * + * @type {number} + */ + set height(height: number) { + this.options.height = height; + } + + /** + * Draw the image + * @date 1/9/2024 - 11:48:39 AM + * + * @param {CanvasRenderingContext2D} ctx + */ + draw(ctx: CanvasRenderingContext2D) { + const { x, y, width, height } = this.options; + if (!this.data) return; + + ctx.drawImage( + this.data, + (x || 0) * ctx.canvas.width, + (y || 0) * ctx.canvas.height, + (width || 0) * ctx.canvas.width, + (height || 0) * ctx.canvas.height, + ); + } + + /** + * Determines if the given point is inside the image + * @date 1/9/2024 - 11:50:23 AM + * + * @param {number} x + * @param {number} y + * @returns {boolean} + */ + isIn(point: Point2D) { + const [x, y] = point; + return ( + x >= this.x && + x <= this.x + this.width && + y >= this.y && + y <= this.y + this.height + ); + } +} diff --git a/client/models/canvas/path.ts b/client/models/canvas/path.ts new file mode 100644 index 00000000..7c8627cc --- /dev/null +++ b/client/models/canvas/path.ts @@ -0,0 +1,43 @@ +import { Drawable } from './drawable'; +import { + Point, + Point2D, +} from '../../../shared/submodules/calculations/src/linear-algebra/point'; + +export class Path extends Drawable { + constructor(public points: Point2D[]) { + super(); + } + + /** + * Draw the path + * @date 1/9/2024 - 11:55:50 AM + * + * @param {CanvasRenderingContext2D} ctx + */ + draw(ctx: CanvasRenderingContext2D) { + ctx.beginPath(); + if (!this.points[0]) return; + ctx.moveTo( + this.points[0][0] * ctx.canvas.width, + this.points[0][1] * ctx.canvas.height, + ); + if (this.$properties?.line?.color) { + ctx.strokeStyle = this.$properties.line?.color; + } + if (this.$properties?.line?.width) { + ctx.lineWidth = this.$properties.line?.width; + } + for (let i = 1; i < this.points.length; i++) { + ctx.lineTo( + this.points[i][0] * ctx.canvas.width, + this.points[i][1] * ctx.canvas.height, + ); + } + ctx.stroke(); + } + + add(...points: Point2D[]) { + this.points.push(...points); + } +} diff --git a/client/models/canvas/polygon.ts b/client/models/canvas/polygon.ts new file mode 100644 index 00000000..dde01eef --- /dev/null +++ b/client/models/canvas/polygon.ts @@ -0,0 +1,92 @@ +import { Drawable } from './drawable'; +import { ShapeProperties } from './properties'; +import { Point2D } from '../../../shared/submodules/calculations/src/linear-algebra/point'; + +/** + * Polygon + * @date 1/25/2024 - 12:36:59 PM + * + * @export + * @class Polygon + * @typedef {Polygon} + * @extends {Drawable} + */ +export class Polygon extends Drawable { + /** + * Creates an instance of Polygon. + * @date 1/25/2024 - 12:36:59 PM + * + * @constructor + * @param {Point2D[]} points + * @param {Partial>} [$properties={}] + */ + constructor( + public points: Point2D[], + public readonly $properties: Partial> = {}, + ) { + super(); + } + + /** + * Draw the polygon + * @date 1/25/2024 - 12:36:59 PM + * + * @param {CanvasRenderingContext2D} ctx + */ + draw(ctx: CanvasRenderingContext2D) { + ctx.beginPath(); + const x0 = this.points[0][0] * ctx.canvas.width; + const y0 = this.points[0][1] * ctx.canvas.height; + ctx.moveTo(x0, y0); + + for (let i = 1; i < this.points.length; i++) { + ctx.lineTo( + this.points[i][0] * ctx.canvas.width, + this.points[i][1] * ctx.canvas.height, + ); + } + ctx.closePath(); + + if (this.$properties?.line?.color) { + ctx.strokeStyle = this.$properties.line.color; + } + if (this.$properties?.line?.width) { + ctx.lineWidth = this.$properties.line.width; + } + + if (this.$properties?.line) ctx.stroke(); + + if (this.$properties?.fill?.color) { + ctx.fillStyle = this.$properties.fill.color; + } + if (this.$properties?.fill) ctx.fill(); + } + + /** + * Check if the given point is inside the polygon + * @date 1/25/2024 - 12:36:59 PM + * + * @param {Point2D} point + * @returns {boolean} + */ + isIn(point: Point2D) { + const [x, y] = point; + let inside = false; + for ( + let i = 0, j = this.points.length - 1; + i < this.points.length; + j = i++ + ) { + const xi = this.points[i][0], + yi = this.points[i][1]; + const xj = this.points[j][0], + yj = this.points[j][1]; + + const intersect = yi > y !== yj > y && + x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + + return inside; + } +} diff --git a/client/models/canvas/properties.ts b/client/models/canvas/properties.ts new file mode 100644 index 00000000..0bfe924f --- /dev/null +++ b/client/models/canvas/properties.ts @@ -0,0 +1,25 @@ +export type LineProperties = { + width: number; + color: string; + doDraw: (element: T) => boolean; +}; + +export type FillProperties = { + color: string; + doDraw: (element: T) => boolean; +}; + +export type TextProperties = { + font: string; + color: string; + height: number; + width: number; + doDraw: (element: T) => boolean; +}; + +export type ShapeProperties = Partial<{ + line: Partial>; + fill: Partial>; + text: Partial>; + doDraw: (element: T) => boolean; +}>; diff --git a/client/models/canvas/svg.ts b/client/models/canvas/svg.ts new file mode 100644 index 00000000..06305933 --- /dev/null +++ b/client/models/canvas/svg.ts @@ -0,0 +1,47 @@ +import { Point2D } from '../../../shared/submodules/calculations/src/linear-algebra/point'; +import { Drawable } from './drawable'; + +export class SVG extends Drawable { + private readonly $img: HTMLImageElement = new Image(); + private $ready = false; + constructor( + public readonly src: string, + public center: Point2D, + ) { + super(); + + this.$img.src = src; + + this.$img.onload = () => { + this.$ready = true; + + if (this.$properties?.text?.height) { + this.$img.height = this.$properties.text.height; + } + + if (this.$properties?.text?.width) { + this.$img.width = this.$properties.text.width; + } + }; + } + + draw(ctx: CanvasRenderingContext2D) { + if (!this.$ready) return; + + if (this.$properties?.text?.color) { + ctx.fillStyle = this.$properties.text.color; + } + + if (this.$properties?.text?.font) { + console.warn("You can't set the font of an SVG"); + } + + ctx.drawImage( + this.$img, + this.center[0] * ctx.canvas.width, + this.center[1] * ctx.canvas.height, + this.$img.width, + this.$img.height, + ); + } +}