274 lines
9.6 KiB
TypeScript
274 lines
9.6 KiB
TypeScript
|
/// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
/// SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||
|
|
||
|
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 extends SVGElement>(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 extends SVGElement>(n: N): N {
|
||
|
return applyAttributes(n, {
|
||
|
"x": this.x,
|
||
|
"y": this.y,
|
||
|
"width": this.width,
|
||
|
"height": this.height,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export type Prop<V, T, TLike> = (v: V) => {
|
||
|
get: () => T,
|
||
|
set: (t: TLike) => V,
|
||
|
};
|
||
|
|
||
|
export type Anchor = Prop<Rect, Point, Pointlike>;
|
||
|
|
||
|
function anchor<V, T, TLike>(g: keyof V, s: keyof V): Prop<V, T, TLike> {
|
||
|
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<Rect, Point, Pointlike>('topLeft', 'withTopLeft');
|
||
|
export const TOP_MID = anchor<Rect, Point, Pointlike>('topMid', 'withTopMid');
|
||
|
export const TOP_RIGHT = anchor<Rect, Point, Pointlike>('topRight', 'withTopRight');
|
||
|
export const BOTTOM_LEFT = anchor<Rect, Point, Pointlike>('bottomLeft', 'withBottomLeft');
|
||
|
export const BOTTOM_MID = anchor<Rect, Point, Pointlike>('bottomMid', 'withBottomMid');
|
||
|
export const BOTTOM_RIGHT = anchor<Rect, Point, Pointlike>('bottomRight', 'withBottomRight');
|
||
|
export const MID_LEFT = anchor<Rect, Point, Pointlike>('midLeft', 'withMidLeft');
|
||
|
export const CENTER = anchor<Rect, Point, Pointlike>('center', 'withCenter');
|
||
|
export const MID_RIGHT = anchor<Rect, Point, Pointlike>('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<R>(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 extends Element>(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>${text}</text>`, attributes);
|
||
|
}
|
||
|
|
||
|
export function rect(extent0: Pointlike = Point.ZERO, attributes?: Attributes): SVGRectElement {
|
||
|
const extent = Point.from(extent0);
|
||
|
return applyAttributes(
|
||
|
svg.tag(SVGRectElement)`<rect width="${extent.x}" height="${extent.y}"/>`,
|
||
|
attributes);
|
||
|
}
|
||
|
|
||
|
export function circle(radius: number, attributes?: Attributes): SVGCircleElement {
|
||
|
return applyAttributes(svg.tag(SVGCircleElement)`<circle r="${radius}"/>`, 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)`
|
||
|
<svg class="placard">
|
||
|
${base}
|
||
|
<g transform="translate(${padding.x} ${padding.y})">${n}</g>
|
||
|
</svg>`;
|
||
|
}
|