import { HousePlannerCanvasGrid } from './grid'; import { BezierSegment, Layer, Line, LineSegment, Vec2 } from './interfaces'; import { HousePlannerCanvasTools } from './tools'; import { rad2deg, vec2Add, vec2AngleFromOrigin, vec2Distance, vec2DivideScalar, vec2PointFromAngle, } from './utils'; export class HousePlannerCanvas { public ctx!: CanvasRenderingContext2D; public layers: Layer[] = []; public tools = new HousePlannerCanvasTools(this); public grid = new HousePlannerCanvasGrid(this, 8); constructor(public canvas: HTMLCanvasElement) { this.ctx = this.canvas.getContext('2d')!; this.setupEvents(); } get width() { return this.canvas.width; } get height() { return this.canvas.height; } addLayer(layer: Layer) { this.layers.push(layer); this.draw(); } cleanUp() { this.tools.cleanUp(); window.removeEventListener('keyup', this.boundKeyUpEvent); window.removeEventListener('keydown', this.boundKeyDownEvent); } draw() { this.ctx.clearRect(0, 0, this.width, this.height); this.grid.draw(); this.tools.drawHighlights(); for (const layer of this.layers) { if (!layer.visible) continue; if (!layer.contents?.length) continue; this.drawLayer(layer); } this.tools.drawControls(); } makeBezier(segment: BezierSegment, path: Path2D) { const bezier = segment as BezierSegment; const [cp1x, cp1y] = bezier.startControl; const [cp2x, cp2y] = bezier.endControl; const [x, y] = bezier.end; path.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } makeLinePath(line: Line) { const path = new Path2D(); const [firstSegment, ...segments] = line.segments; // first segment must have a starting point if (!firstSegment.start) return path; path.moveTo(...firstSegment.start); if (line.type === 'curve') { const lineLength = vec2Distance(firstSegment.start, firstSegment.end); const lineAngle = vec2AngleFromOrigin( firstSegment.end, firstSegment.start! ); const ninety = lineAngle + Math.PI / 2; path.moveTo(...firstSegment.end); path.arc( firstSegment.start[0], firstSegment.start[1], lineLength, lineAngle, ninety ); path.lineTo(...firstSegment.start); } else if ((firstSegment as BezierSegment).startControl) { this.makeBezier(firstSegment as BezierSegment, path); } else { path.lineTo(...firstSegment.end); } for (const segment of segments) { if ((segment as BezierSegment).startControl) { this.makeBezier(segment as BezierSegment, path); continue; } path.lineTo(...segment.end); } if (line.closed && line.type !== 'curve') { path.closePath(); } return path; } setupLine(line: Line, overBounds = 0, overrideColor?: string) { if (line.lineDash) { this.ctx.setLineDash(line.lineDash); } this.ctx.strokeStyle = overrideColor || line.color || '#000'; this.ctx.lineWidth = line.width + overBounds; if (line.lineCap) { this.ctx.lineCap = line.lineCap; } } isOnLine(line: Line, x: number, y: number, selectError = 16) { let path = line.render || this.makeLinePath(line); this.setupLine(line, selectError); return this.ctx.isPointInStroke(path, x, y); } isOnSegment( line: Line, segment: LineSegment, lastPoint: Vec2, x: number, y: number, selectError = 16 ) { const fakePath = this.makeLinePath({ ...line, closed: false, segments: [{ ...segment, start: lastPoint }], }); this.setupLine(line, selectError); return this.ctx.isPointInStroke(fakePath, x, y); } private keyDownEvent(e: KeyboardEvent) { if (e.target !== document.body && e.target != null) return; e.preventDefault(); this.tools.onKeyDown(e); } private boundKeyDownEvent = this.keyDownEvent.bind(this); private keyUpEvent(e: KeyboardEvent) { if (e.target !== document.body && e.target != null) return; e.preventDefault(); this.tools.onKeyUp(e); } private boundKeyUpEvent = this.keyUpEvent.bind(this); private setupEvents() { window.addEventListener('keyup', this.boundKeyUpEvent); window.addEventListener('keydown', this.boundKeyDownEvent); this.canvas.addEventListener('mousemove', (e) => this.tools.onMouseMove(e)); this.canvas.addEventListener('mousedown', (e) => this.tools.onMouseDown(e)); this.canvas.addEventListener('mouseup', (e) => this.tools.onMouseUp(e)); this.canvas.addEventListener('touchmove', (e) => this.tools.onTouchMove(e)); this.canvas.addEventListener('touchstart', (e) => this.tools.onTouchStart(e) ); this.canvas.addEventListener('touchend', (e) => this.tools.onTouchEnd(e)); this.canvas.addEventListener('wheel', (e) => this.tools.onMouseWheel(e)); this.canvas.addEventListener('pointerleave', () => this.tools.onPointerLeave() ); } private drawRoomText(line: Line) { const centerPoint = vec2DivideScalar( line.segments.reduce((prev, curr) => { if (!prev) { if (curr.start) { return vec2Add(curr.start, curr.end); } return curr.end; } let preadd = vec2Add(prev, curr.end); if (curr.start) { preadd = vec2Add(curr.start, preadd); } return preadd; }, null) as Vec2, line.segments.length + 1 ); this.ctx.font = '16px Arial'; this.ctx.fillStyle = line.color; const { width } = this.ctx.measureText(line.name); this.ctx.fillText( line.name, centerPoint[0] - width / 2, centerPoint[1] - 8 ); } private drawLine(line: Line) { const path = this.makeLinePath(line); line.render = path; this.setupLine(line); this.ctx.stroke(path); if (line.type === 'room') { this.drawRoomText(line); } } private drawLayer(layer: Layer) { for (const item of layer.contents) { if (!item.visible) continue; const line = item as Line; if (line.segments) { this.drawLine(line); continue; } } } }