syndicate-render-msd/src/pict.ts

274 lines
9.6 KiB
TypeScript
Raw Normal View History

2022-01-24 08:28:56 +00:00
/// 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>`;
}