UNPKG

@webwriter/geometry-cloze

Version:

Create and view geometry exercises with coloring, styling and labeling options.

136 lines (115 loc) 4.23 kB
import Element from '../elements/base/Element'; import Draggable from '../elements/base/Draggable'; import DividerLine from '../elements/DividerLine'; import Shape from '../elements/Shape'; import { Child } from './ChildrenTypes'; import InteractionManager from './InteractionManager'; export default abstract class ChildrenManager { private static FRAME_RATE = 60; private _canvas: HTMLCanvasElement; private _ctx: CanvasRenderingContext2D; private children: Child[] = []; constructor(canvas: HTMLCanvasElement) { this._canvas = canvas; this._ctx = this._canvas.getContext('2d')!; } // pass ctx here since this method can be overwritten by super classes and can be used to render additional elements protected redraw(ctx: CanvasRenderingContext2D) { if (!ctx || !this._canvas) return; ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); // reverse order so that the first shape is on top for (const shape of this.children.map((s) => s).reverse()) { if (shape.hidden) continue; shape.draw(ctx); } } protected getElementAt(point: { x: number; y: number }): Draggable | null { const hit = this.children.reduce<Draggable | null>((cur, shape) => { if (cur) return cur; if (shape instanceof Draggable) { const hit = shape.getHit(point)[0] ?? null; if (this instanceof InteractionManager) { if (this.canSelect(hit)) return hit; } else return hit; } return null; }, null); return hit; } /** * first timestamp where we requested a redraw for current batch */ private firstRequestTimestamp: number | null = null; /** * last timestamp where we actually redrew */ private lastRedrawTimestamp: number = 0; requestRedraw(originallyScheduledAt?: number) { const now = performance.now(); if (!originallyScheduledAt) originallyScheduledAt = now; // check if we've already redrawn since this was scheduled if (originallyScheduledAt < this.lastRedrawTimestamp) return; if (!this.firstRequestTimestamp) this.firstRequestTimestamp = performance.now(); if (now - this.firstRequestTimestamp > 1000 / ChildrenManager.FRAME_RATE) { this.lastRedrawTimestamp = now; this.redraw(this._ctx); this.firstRequestTimestamp = null; } else requestAnimationFrame( this.requestRedraw.bind(this, originallyScheduledAt) ); } public addChild(ele: Child, preventRedraw?: boolean) { this.children.push(ele); if (!preventRedraw) this.requestRedraw(); ele.registerParent(this as any); ele.addEventListener('request-redraw', this.redraw.bind(this, this._ctx)); } public removeChild(element: Child) { const index = this.children.indexOf(element); if (index < 0) return; this.children.splice(index, 1); this.requestRedraw(); } public moveToTop(shape: Child) { const index = this.children.indexOf(shape); if (index < 0) return; const ele = this.children.splice(index, 1); this.children.push(...ele); } public getChildren(filter?: (child: Child) => boolean) { if (filter) return this.children.filter(filter); return this.children.slice(0); } public getCanvasDimensions(): { width: number; height: number } { return { width: this._canvas.width, height: this._canvas.height }; } protected getChildByID(id: number) { return this.children.reduce<Element | null>( (cur, child) => cur ?? child.getChildByID(id), null ); } public export() { if (this.children.length) return { children: this.children.map((child) => child.export()) }; return {}; } public import(data: Partial<ReturnType<this['export']>>) { const children = data.children?.map((child) => child._type === 'divider-line' ? DividerLine.import(child as any, this as any) : Shape.import(child, this as any) ) ?? []; this.children = []; children.forEach((child) => this.addChild(child, true)); this.requestRedraw(); } }