/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones import { makeTemplate } from "@syndicate-lang/html"; const svg = makeTemplate('svg'); export type Pointlike = { x: number, y: number } | [number, number]; export type Rectlike = { x: number, y: number, width: number, height: number } | [number, number, number, number]; export class Point { readonly x: number; readonly y: number; constructor (x: number, y: number) { this.x = x; this.y = y; } static from(p: Pointlike): Point { if ('x' in p) { return new Point(p.x, p.y); } else { return new Point(p[0], p[1]); } } toDOM(): DOMPoint { return new DOMPoint(this.x, this.y); } add(p0: Pointlike): Point { const p = Point.from(p0); return new Point(this.x + p.x, this.y + p.y); } sub(p0: Pointlike): Point { const p = Point.from(p0); return new Point(this.x - p.x, this.y - p.y); } scale(n: number): Point { return new Point(this.x * n, this.y * n); } max(p0: Pointlike): Point { const p = Point.from(p0); return new Point(Math.max(this.x, p.x), Math.max(this.y, p.y)); } min(p0: Pointlike): Point { const p = Point.from(p0); return new Point(Math.min(this.x, p.x), Math.min(this.y, p.y)); } apply(n: N): N { return applyAttributes(n, { "x": this.x, "y": this.y }); } withX(x: number): Point { return new Point(x, this.y); } withY(y: number): Point { return new Point(this.x, y); } static ZERO: Point = new Point(0, 0); } export class Rect { readonly x: number; readonly y: number; readonly width: number; readonly height: number; constructor (x: number, y: number, width: number, height: number) { this.x = x; this.y = y; this.width = width; this.height = height; } static from(r: Rectlike): Rect { if ('x' in r) { return new Rect(r.x, r.y, r.width, r.height); } else { return new Rect(r[0], r[1], r[2], r[3]); } } static originCorner(o0: Pointlike, c0: Pointlike): Rect { const o = Point.from(o0); const c = Point.from(c0); return new Rect(o.x, o.y, c.x - o.x, c.y - o.y); } static originExtent(o0: Pointlike, e0: Pointlike): Rect { const o = Point.from(o0); const e = Point.from(e0); return new Rect(o.x, o.y, e.x, e.y); } toDOM(): DOMRect { return new DOMRect(this.x, this.y, this.width, this.height); } get extent(): Point { return this.bottomRight.sub(this.topLeft); } withExtent(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(this.x, this.y, p.x, p.y); } get left(): number { return this.x; } get midX(): number { return this.x + this.width / 2; } get right(): number { return this.x + this.width; } get top(): number { return this.y; } get midY(): number { return this.y + this.height / 2; } get bottom(): number { return this.y + this.height; } withLeft(n: number): Rect { return new Rect(n, this.y, this.width, this.height); } withMidX(n: number): Rect { return new Rect(n - this.width / 2, this.y, this.width, this.height); } withRight(n: number): Rect { return new Rect(n - this.width, this.y, this.width, this.height); } withTop(n: number): Rect { return new Rect(this.x, n, this.width, this.height); } withMidY(n: number): Rect { return new Rect(this.x, n - this.height / 2, this.width, this.height); } withBottom(n: number): Rect { return new Rect(this.x, n - this.height, this.width, this.height); } withWidth(n: number): Rect { return new Rect(this.x, this.y, n, this.height); } withHeight(n: number): Rect { return new Rect(this.x, this.y, this.width, n); } get topLeft(): Point { return new Point(this.x, this.y); } get topMid(): Point { return this.topLeft.add(this.topRight).scale(0.5); } get topRight(): Point { return new Point(this.x + this.width, this.y); } get bottomLeft(): Point { return new Point(this.x, this.y + this.height); } get bottomMid(): Point { return this.bottomLeft.add(this.bottomRight).scale(0.5); } get bottomRight(): Point { return new Point(this.x + this.width, this.y + this.height); } get midLeft(): Point { return this.topLeft.add(this.bottomLeft).scale(0.5); } get center(): Point { return this.topLeft.add(this.bottomRight).scale(0.5); } get midRight(): Point { return this.topRight.add(this.bottomRight).scale(0.5); } withTopLeft(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x, p.y, this.width, this.height); } withTopMid(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x - this.width / 2, p.y, this.width, this.height); } withTopRight(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x - this.width, p.y, this.width, this.height); } withBottomLeft(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x, p.y - this.height, this.width, this.height); } withBottomMid(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x - this.width / 2, p.y - this.height, this.width, this.height); } withBottomRight(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x - this.width, p.y - this.height, this.width, this.height); } withMidLeft(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x, p.y - this.height / 2, this.width, this.height); } withCenter(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x - this.width / 2, p.y - this.height / 2, this.width, this.height); } withMidRight(p0: Pointlike): Rect { const p = Point.from(p0); return new Rect(p.x - this.width, p.y - this.height / 2, this.width, this.height); } apply(n: N): N { return applyAttributes(n, { "x": this.x, "y": this.y, "width": this.width, "height": this.height, }); } } export type Prop = (v: V) => { get: () => T, set: (t: TLike) => V, }; export type Anchor = Prop; function anchor(g: keyof V, s: keyof V): Prop { return (v: V) => ({ get: (): T => { return (v as any)[g]; }, set: (t: TLike): V => { return (v as any)[s](t); }, }); } export const TOP_LEFT = anchor('topLeft', 'withTopLeft'); export const TOP_MID = anchor('topMid', 'withTopMid'); export const TOP_RIGHT = anchor('topRight', 'withTopRight'); export const BOTTOM_LEFT = anchor('bottomLeft', 'withBottomLeft'); export const BOTTOM_MID = anchor('bottomMid', 'withBottomMid'); export const BOTTOM_RIGHT = anchor('bottomRight', 'withBottomRight'); export const MID_LEFT = anchor('midLeft', 'withMidLeft'); export const CENTER = anchor('center', 'withCenter'); export const MID_RIGHT = anchor('midRight', 'withMidRight'); export type Attributes = { [key: string]: string | number | undefined | null }; let playground: SVGSVGElement = null as any; export function init() { playground = document.getElementById('playground')! as any; } export function temporarilyInDom(e: SVGElement, f: () => R, parent = playground): R { parent.appendChild(e); const result = f(); parent.removeChild(e); return result; } export function clientRect(e: SVGElement, parent = playground) { return temporarilyInDom(e, () => Rect.from(e.getBoundingClientRect()), parent); } export function bbox(e: SVGGraphicsElement, parent = playground) { return temporarilyInDom(e, () => Rect.from(e.getBBox()), parent); } export function textLength(e: SVGTextContentElement, parent = playground) { return temporarilyInDom(e, () => e.getComputedTextLength(), parent); } export function applyAttributes(n: N, attributes?: Attributes): N { if (attributes !== void 0) { for (const key in attributes) { const val = attributes[key]; if (val !== void 0 && val !== null) { n.setAttribute(key, '' + val); } } } return n; } export function text(text: string, attributes?: Attributes): SVGTextElement { return applyAttributes(svg.tag(SVGTextElement)`${text}`, attributes); } export function rect(extent0: Pointlike = Point.ZERO, attributes?: Attributes): SVGRectElement { const extent = Point.from(extent0); return applyAttributes( svg.tag(SVGRectElement)``, attributes); } export function circle(radius: number, attributes?: Attributes): SVGCircleElement { return applyAttributes(svg.tag(SVGCircleElement)``, attributes); } export function alignTo(a: SVGGraphicsElement, n: Anchor, b: SVGGraphicsElement) { n(bbox(a)).set(n(bbox(b)).get()).apply(a); } export function placard(n: SVGGraphicsElement, attributes?: Attributes): SVGSVGElement { const padding = new Point(12, 6); const base = rect(bbox(n).extent.add(padding.scale(2)), attributes); return svg.tag(SVGSVGElement)` ${base} ${n} `; }