UNPKG

@webwriter/geometry-cloze

Version:

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

225 lines (200 loc) 6.22 kB
import Calc, { MathLine, MathPoint } from '../helper/Calc'; import Vector from '../helper/Vector'; import Element, { NamedElement } from './base/Element'; import { StylableData } from './base/Stylable'; import Draggable, { DraggableData } from './base/Draggable'; import Point from './Point'; import { ContextMenuItem } from '../../types/ContextMenu'; import Numbers from '../helper/Numbers'; import Manager from '../CanvasManager/Abstracts'; export type BaseLine = MathLine & NamedElement; export default class Line extends Draggable { private _start: MathPoint; private _end: MathPoint; protected _x: number; protected _y: number; protected clickTargetSize = 2; constructor( manager: Manager, data: BaseLine & Partial<StylableData & DraggableData> ) { super(manager, data); this._start = data.start; this._end = data.end; this._x = data.start.x; this._y = data.start.y; } public move(coords: { x?: number | undefined; y?: number | undefined; relative: boolean; }): void { const relativeCoords = coords.relative ? coords : { x: (coords?.x ?? this._x) - this._x, y: (coords?.y ?? this._y) - this._y, relative: true }; super.move(relativeCoords); if (this._start instanceof Point) this._start.move(relativeCoords); if (this._end instanceof Point) this._end.move(relativeCoords); this.fireEvent('move', this); this.requestRedraw(); } protected removeChild(child: Element): void { this.delete(); } draw(ctx: CanvasRenderingContext2D) { if (this.hidden) return; super.draw(ctx); ctx.beginPath(); const vector = { x: this._end.x - this._start.x, y: this._end.y - this._start.y }; const normalized = Vector.normalize(vector); const start = { x: this._start.x, y: this._start.y }; if (this.start instanceof Point) { start.x += normalized.x * this.start.size; start.y += normalized.y * this.start.size; } ctx.moveTo(start.x, start.y); const end = { x: this._end.x, y: this._end.y }; if (this.end instanceof Point) { end.x -= normalized.x * this.end.size; end.y -= normalized.y * this.end.size; } ctx.lineTo(end.x, end.y); ctx.stroke(); // draw label if (this.showLabel) { const padding = 5; const middlePoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }; const label = this.getLabel(); ctx.font = '24px Arial'; ctx.fillStyle = this.labelColor; const metrics = ctx.measureText(label); const fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; if (this.parent && 'getPoints' in this.parent) { const ortho = Vector.orthogonal(normalized); const point1 = Vector.add( middlePoint, Vector.scale(ortho, fontHeight / 2) ); const factor = Calc.isPointInPolygon(point1, this.parent.getPoints()) ? -1 : 1; const angleFactor = 1 + Math.min(Math.abs(ortho.x), Math.abs(ortho.y)); const startPoint = Vector.add( middlePoint, Vector.scale(ortho, fontHeight * factor * angleFactor) ); ctx.clearRect( startPoint.x - metrics.width / 2 - padding, startPoint.y - fontHeight / 2 - padding, metrics.width + 2 * padding, fontHeight + 2 * padding ); ctx.fillText( label, startPoint.x - metrics.width / 2, startPoint.y + fontHeight / 4 ); } else { ctx.clearRect( middlePoint.x - metrics.width / 2 - padding, middlePoint.y - fontHeight / 2 - padding, metrics.width + 2 * padding, fontHeight + 2 * padding ); ctx.fillText( label, middlePoint.x - metrics.width / 2, middlePoint.y + fontHeight / 4 ); } } } setStart(start: MathPoint) { if (start instanceof Point || !(this._start instanceof Point)) this._start = start; else this._start.move({ ...start, relative: false }); this.requestRedraw(); } setEnd(end: MathPoint) { if (end instanceof Point || !(this._end instanceof Point)) this._end = end; else this._end.move({ ...end, relative: false }); this.requestRedraw(); } get start() { return this._start; } get end() { return this._end; } getHit(point: MathPoint, point2?: MathPoint): Draggable[] { if (this.hidden) return []; if (point2) { const rect = { x1: point.x, y1: point.y, x2: point2.x, y2: point2.y }; const hits = []; const lineHit = Calc.isInRect(rect, this); if (lineHit) hits.push(this); return hits; } else { const pointHit = super.getHit(point); if (pointHit.length) return pointHit; if (Calc.distance(this, point) <= this.lineWidth + this.clickTargetSize) return [this]; return []; } } protected getValueLabel() { return Numbers.round( Calc.distance(this.start, this.end) * this.manager.scale ); } public getContextMenuItems(): ContextMenuItem[] { return [ ...super.getContextMenuItems(), ...this.getStyleContextMenuItems({ stroke: true, lineWidth: true }) ]; } public isEndpoint(point: MathPoint) { return this._start === point || this._end === point; } public export() { return { ...super.export(), _type: 'line' as const, start: { x: this._start.x, y: this._start.y, ...(this._start instanceof Point && { id: this._start.id }) }, end: { x: this._end.x, y: this._end.y, ...(this._end instanceof Point && { id: this._end.id }) } }; } public static import(data: BaseLine, manager: Manager) { return new Line(manager, data); } }