UNPKG

ixfx

Version:

A framework for programming interactivity

1,733 lines (1,724 loc) 80 kB
import { svg_exports } from "./chunk-5CYQHQM5.js"; import { Video_exports } from "./chunk-QR64IITR.js"; import { clamp as clamp2 } from "./chunk-72ZAAY6K.js"; import { Empty2, EmptyPositioned, ImageDataGrid_exports, Placeholder, PlaceholderPositioned, corners, corners2, isEqualSize, rect_exports } from "./chunk-AKHRG6J4.js"; import { StackImmutable } from "./chunk-2HVEZD7J.js"; import { immutable } from "./chunk-QNOJ4POP.js"; import { Empty, Ops, guard, guard3 as guard2, intersectsPoint, isCubicBezier, isEqual, isLine, isPlaceholder2 as isPlaceholder, isQuadraticBezier, piPi2 as piPi, point_exports, quantiseEvery, sources_exports, subtract } from "./chunk-R4JIRHLR.js"; import { MapOfSimpleMutable } from "./chunk-72EKR3DZ.js"; import { resolveEl, resolveElementTry, resolveEls } from "./chunk-ICXKAKPN.js"; import { withoutUndefined } from "./chunk-LKZ4HZTV.js"; import { resultErrorToString } from "./chunk-QVTHCRNR.js"; import { throwArrayTest } from "./chunk-WYMJKVGY.js"; import { colour_exports, multiplyOpacity, randomHue, scaler, scalerTwoWay, toStringFirst } from "./chunk-DLFRRV7R.js"; import { clamp } from "./chunk-QAEJS6HO.js"; import { round } from "./chunk-3UVU2F72.js"; import { __export } from "./chunk-L5EJU35C.js"; // src/visual/index.ts var visual_exports = {}; __export(visual_exports, { BipolarView: () => BipolarView_exports, CanvasRegion: () => CanvasRegion, CanvasSource: () => CanvasSource, CartesianCanvasPlot: () => CartesianCanvasPlot, Colour: () => colour_exports, Drawing: () => Drawing_exports, ImageDataGrid: () => ImageDataGrid_exports, NamedColourPalette: () => NamedColourPalette_exports, Plot: () => plot_exports, SceneGraph: () => SceneGraph_exports, Svg: () => svg_exports, Video: () => Video_exports, insert: () => insert }); // src/visual/NamedColourPalette.ts var NamedColourPalette_exports = {}; __export(NamedColourPalette_exports, { create: () => create }); var create = (fallbacks) => new NamedColourPaletteImpl(fallbacks); var NamedColourPaletteImpl = class { #store = /* @__PURE__ */ new Map(); #aliases = /* @__PURE__ */ new Map(); #lastFallback = 0; #elementBase; constructor(fallbacks) { if (fallbacks !== void 0) this.fallbacks = fallbacks; else this.fallbacks = [`red`, `blue`, `green`, `orange`]; this.#elementBase = document.body; } setElementBase(el) { this.#elementBase = el; } add(key, colour) { this.#store.set(key, colour); } alias(from, to) { this.#aliases.set(from, to); } get(key, fallback) { const alias = this.#aliases.get(key); if (alias !== void 0) key = alias; const c = this.#store.get(key); if (c !== void 0) return c; const variableName = `--` + key; let fromCss = getComputedStyle(this.#elementBase).getPropertyValue(variableName).trim(); if (fromCss === void 0 || fromCss.length === 0) { if (fallback !== void 0) return fallback; fromCss = this.fallbacks[this.#lastFallback]; this.#lastFallback++; if (this.#lastFallback === this.fallbacks.length) this.#lastFallback = 0; } return fromCss; } getOrAdd(key, fallback) { if (this.has(key)) return this.get(key); const c = this.get(key, fallback); this.add(key, c); return c; } has(key) { return this.#store.has(key); } }; // src/visual/SceneGraph.ts var SceneGraph_exports = {}; __export(SceneGraph_exports, { Box: () => Box, CanvasBox: () => CanvasBox, CanvasLayoutState: () => CanvasLayoutState, CanvasMeasureState: () => CanvasMeasureState, LayoutState: () => LayoutState, MeasureState: () => MeasureState, boxRectFromPx: () => boxRectFromPx, boxRectFromRectPx: () => boxRectFromRectPx, boxUnitFromPx: () => boxUnitFromPx }); // src/geometry/rect/Clamp.ts var clamp3 = (value, maximum) => { return Object.freeze({ ...value, width: Math.min(value.width, maximum.width), height: Math.min(value.height, maximum.height) }); }; // src/visual/SceneGraph.ts var boxUnitFromPx = (v) => { return { type: `px`, value: v }; }; var boxRectFromPx = (x, y, width, height) => { return { x: boxUnitFromPx(x), y: boxUnitFromPx(y), width: boxUnitFromPx(width), height: boxUnitFromPx(height) }; }; var boxRectFromRectPx = (r) => { return { x: boxUnitFromPx(r.x), y: boxUnitFromPx(r.y), width: boxUnitFromPx(r.width), height: boxUnitFromPx(r.height) }; }; var unitIsEqual = (a, b) => { if (a.type === `px` && b.type === `px`) { return a.value === b.value; } return false; }; var boxRectIsEqual = (a, b) => { if (a === void 0 && b === void 0) return true; if (a === void 0) return false; if (b === void 0) return false; if (a.x && b.x && !unitIsEqual(a.x, b.x)) return false; if (a.y && b.y && !unitIsEqual(a.y, b.y)) return false; if (a.width && b.width && !unitIsEqual(a.width, b.width)) return false; if (a.height && b.height && !unitIsEqual(a.height, b.height)) return false; return true; }; var BaseState = class { constructor(bounds) { this.bounds = bounds; this.pass = 0; } resolveToPx(u, maxValue, defaultValue) { if (u === void 0 && defaultValue !== void 0) return defaultValue; if (u === void 0) return; if (u.type === void 0) throw new TypeError(`Expected 'type' and 'value' fields. Type is missing`); if (u.value === void 0) throw new TypeError(`Expected 'type' and 'value' fields. Value is missing`); if (u.type === `px`) return u.value; if (u.type === `pc`) return u.value * maxValue; throw new Error(`Unknown unit type: ${u.type}`); } resolveBox(box) { if (box === void 0) return void 0; const x = this.resolveToPx(box.x, this.bounds.width); const y = this.resolveToPx(box.y, this.bounds.height); const width = this.resolveToPx(box.width, this.bounds.width); const height = this.resolveToPx(box.height, this.bounds.height); if (!width || !height) throw new TypeError(`Expected width and height`); if (x === void 0 && y === void 0) { return Object.freeze({ width, height }); } else { if (!x || !y) throw new TypeError(`Expected x and y`); return Object.freeze({ x, y, width, height }); } } }; var MeasureState = class extends BaseState { constructor(bounds) { super(bounds); this.measurements = /* @__PURE__ */ new Map(); } getActualSize(id) { const s = this.measurements.get(id); if (s === void 0) return; if (isPlaceholder(s.actual)) return; return s.actual; } whatIsMeasured() { return [...this.measurements.keys()]; } }; var LayoutState = class extends BaseState { constructor(bounds) { super(bounds); this.layouts = /* @__PURE__ */ new Map(); } }; var Box = class { /** * Constructor. * * If `parent` is provided, `parent.onChildAdded(this)` is called. * @param parent parent box * @param id id of this box */ constructor(parent, id) { /** Rectangle Box occupies in canvas/etc */ this.canvasRegion = PlaceholderPositioned; this.children = []; this._idMap = /* @__PURE__ */ new Map(); this.debugLayout = false; this._visible = true; this._ready = true; this.takesSpaceWhenInvisible = false; this._needsMeasuring = true; this._needsLayoutX = true; this._needsDrawing = true; this.debugHue = randomHue(); this.id = id; this._parent = parent; parent?.onChildAdded(this); } /** * Returns _true_ if `box` is a child * @param box * @returns */ hasChild(box) { const byReference = this.children.find((c) => c === box); const byId = this.children.find((c) => c.id === box.id); return byReference !== void 0 || byId !== void 0; } /** * Sends a message to all child boxes. * * This first calls `onNotify` on this instance, * before calling `notify()` on each child. * @param message * @param source */ notify(message, source) { this.onNotify(message, source); for (const c of this.children) c.notify(message, source); } *getChildren() { return this.children.entries(); } /** * Handles a received message * @param _message * @param _source */ onNotify(_message, _source) { } /** * Notification a child box has been added * * Throws if * - child has parent as its own child * - child is same as this * - child is already child of this * @param child */ onChildAdded(child) { if (child.hasChild(this)) throw new Error(`Recursive`); if (child === this) throw new Error(`Cannot add self as child`); if (this.hasChild(child)) throw new Error(`Child already present`); this.children.push(child); this._idMap.set(child.id, child); this.layoutInvalidated(`Box.onChildAdded`); } /** * Sets `_ready` to `ready`. If `includeChildren` is _true_, * `setReady` is called on each child * @param ready * @param includeChildren */ setReady(ready, includeChildren = false) { this._ready = ready; if (includeChildren) { for (const c of this.children) c.setReady(ready, includeChildren); } } /** * Gets visible state */ get visible() { return this._visible; } /** * Sets visible state */ set visible(v) { if (this._visible === v) return; this._visible = v; this.layoutInvalidated(`Box.set visible`); } /** * Gets the box's desired region, or _undefined_ */ get desiredRegion() { return this._desiredRect; } /** * Sets the box's desired region. * Calls `onLayoutNeeded()` */ set desiredRegion(v) { if (boxRectIsEqual(v, this._desiredRect)) return; this._desiredRect = v; this.layoutInvalidated(`set desiredRegion`); } /** * Calls `notifyChildLayoutNeeded` */ layoutInvalidated(reason) { if (reason === void 0) debugger; this.debugLog(`layoutInvalidated ${reason}`); this._needsMeasuring = true; this._needsLayoutX = true; this._needsDrawing = true; this.notifyChildLayoutNeeded(); } drawingInvalidated(_reason) { this._needsDrawing = true; } /** * Called from a child, notifying us that * its layout has changed * @returns */ notifyChildLayoutNeeded() { this._needsDrawing = true; this._needsLayoutX = true; this._needsMeasuring = true; if (this._parent === void 0) return; this._parent.notifyChildLayoutNeeded(); } /** * Returns the root box */ get root() { if (this._parent === void 0) return this; return this._parent.root; } /** * Prepare for measuring */ measurePreflight() { } /** * Applies actual size, returning _true_ if size is different than before * * 1. Sets `_needsLayout` to _false_. * 2. Sets `visual` to `m` * 3. Calls `measureApply` on each child * 4. If there's a change or `force`, sets `needsDrawing` to _true_, and notifies root of `measureApplied` * @param m Measurement for box * @returns */ measureApply(m) { this._needsMeasuring = false; const different = this._measuredSize === void 0 ? true : !isEqualSize(m.actual, this._measuredSize); if (different) { this._needsLayoutX = true; } this._measuredSize = { width: m.actual.width, height: m.actual.height }; for (const c of m.children) { if (c !== void 0) c.ref.measureApply(c); } if (different) { this.root.notify(`measureApplied`, this); } return different; } layoutApply(l) { this._needsLayoutX = false; const different = this._layoutPosition === void 0 ? true : !isEqual(l.actual, this._layoutPosition); this._layoutPosition = { x: l.actual.x, y: l.actual.y }; for (const c of l.children) { if (c !== void 0) c.ref.layoutApply(c); } if (different) { this.root.notify(`layoutApplied`, this); } return different; } /** * Debug log from this box context * @param m */ debugLog(m) { if (!this.debugLayout) return; console.log(`SceneGraph[${this.id}]`, m); } layoutStart(measureState, layoutState, force, parent) { const m = { ref: this, actual: Empty, children: [] }; layoutState.layouts.set(this.id, m); const currentPosition = this.layoutSelf(measureState, layoutState, parent); this.root.notify(`laidout`, this); if (currentPosition === void 0) return; m.actual = currentPosition; m.children = this.children.map((c) => c.layoutStart(measureState, layoutState, force, m)); if (withoutUndefined(m.children).length < this.children.length) { return void 0; } return m; } layoutSelf(measureState, layoutState, _parent) { const box = layoutState.resolveBox(this._desiredRect); const x = box === void 0 ? 0 : `x` in box ? box.x : 0; const y = box === void 0 ? 0 : `y` in box ? box.y : 0; if (x === void 0) debugger; if (y === void 0) debugger; return { x, y }; } /** * Start of measuring * 1. Keeps track of measurements in `opts.measurements` * 2. If this box takes space * 2.1. Measure itself if needed * 2.2. Use size * 2. Calls `measureStart` on each child * @param opts Options * @param force Force measurement * @param parent Parent's measurement * @returns Measurement */ measureStart(opts, force, parent) { this.measurePreflight(); const m = { ref: this, // So far no known measurement actual: Placeholder, children: [] }; opts.measurements.set(this.id, m); if (!this._visible && !this.takesSpaceWhenInvisible) { m.actual = EmptyPositioned; } else { let currentMeasurement = this._measuredSize; if (this._needsMeasuring || this._measuredSize === void 0) { currentMeasurement = this.measureSelf(opts, parent); this.root.notify(`measured`, this); } if (typeof currentMeasurement === `string`) { return; } else if (currentMeasurement === void 0) { return; } m.actual = currentMeasurement; } m.children = this.children.map((c) => c.measureStart(opts, force, m)); if (withoutUndefined(m.children).length < this.children.length) { return void 0; } return m; } /** * Measure the box * 1. Uses desired rectangle, if possible * 2. Otherwise uses parent's size * @param opts Measure state * @param parent Parent size * @returns */ measureSelf(opts, parent) { let size = Placeholder; const context = parent ? parent.actual : opts.bounds; const desired = opts.resolveBox(this._desiredRect); size = desired ? clamp3(desired, context) : context; if (isPlaceholder(size)) { return `Box.measureSelf - No size for box?`; } return size; } // protected updateDone(state: MeasureState, force: boolean): void { // this.onUpdateDone(state, force); // for (const c of this.children) c.updateDone(state, force); // } /** * Update has completed * @param state * @param force */ //abstract onUpdateDone(state: MeasureState, force: boolean): void; /** * Update * 1. Calls `this.updateBegin()` to initialise measurement state * 2. In a loop, run `measureStart()` and then `measureApply` if possible * 3. Call `updateDone` when finished * @param force Force update * @returns */ update(context, force = false) { if (context === void 0) throw new Error(`context is undefined`); if (!this._needsMeasuring && !this._needsLayoutX && !force) return; const [measureState, layoutState] = this.updateBegin(context); let attempts = 5; let measureApplied = false; let layoutApplied = false; if (this._needsMeasuring || force) { while (attempts--) { const m = this.measureStart(measureState, force); if (m !== void 0) { this.measureApply(m); if (!this._ready) return; measureApplied = true; } } if (!measureApplied) this.debugLog(`Ran out of measurement attempts`); } if (this._needsLayoutX || force) { const p = this.layoutStart(measureState, layoutState, force); if (p === void 0) { this.debugLog(`Warning: could not layout`); } else { this.layoutApply(p); layoutApplied = true; } } this.updateComplete(measureApplied, layoutApplied); } }; var CanvasMeasureState = class extends MeasureState { constructor(bounds, ctx) { super(bounds); this.ctx = ctx; if (ctx === void 0) throw new Error(`ctx is undefined`); } }; var CanvasLayoutState = class extends LayoutState { constructor(bounds, ctx) { super(bounds); this.ctx = ctx; if (ctx === void 0) throw new Error(`ctx is undefined`); } }; var CanvasBox = class _CanvasBox extends Box { constructor(parent, id, bounds) { super(parent, id); this.bounds = bounds; this.debugLog(`CanvasBox ctor bounds: ${JSON.stringify(bounds)}`); } static fromCanvas(canvasElement) { const box = new _CanvasBox(void 0, `canvas-box`, canvasElement.getBoundingClientRect()); return box; } /** * Called if this is the parent Box */ addEventHandlers(element) { element.addEventListener(`pointermove`, (event) => { const p = { x: event.offsetX, y: event.offsetY }; this.notifyPointerMove(p); }); element.addEventListener(`pointerleave`, (_event) => { this.notifyPointerLeave(); }); element.addEventListener(`click`, (event) => { const p = { x: event.offsetX, y: event.offsetY }; this.notifyClick(p); }); } onClick(_p) { } /** * Click event has happened on canvas * 1. If it's within our range, call `onClick` and pass to all children via `notifyClick` * @param p * @returns */ notifyClick(p) { if (isPlaceholder(this.canvasRegion)) return; if (intersectsPoint(this.canvasRegion, p)) { const pp = subtract(p, this.canvasRegion.x, this.canvasRegion.y); this.onClick(pp); for (const c of this.children) c.notifyClick(pp); } } /** * Pointer has left * 1. Pass notification to all children via `notifyPointerLeave` */ notifyPointerLeave() { this.onPointerLeave(); for (const c of this.children) c.notifyPointerLeave(); } /** * Pointer has moved * 1. If it's within range `onPointerMove` is called, and pass on to all children via `notifyPointerMove` * @param p * @returns */ notifyPointerMove(p) { if (isPlaceholder(this.canvasRegion)) return; if (intersectsPoint(this.canvasRegion, p)) { const pp = subtract(p, this.canvasRegion.x, this.canvasRegion.y); this.onPointerMove(pp); for (const c of this.children) c.notifyPointerMove(pp); } } /** * Handler when pointer has left */ onPointerLeave() { } /** * Handler when pointer moves within our region * @param _p */ onPointerMove(_p) { } /** * Performs recalculations and drawing as necessary * If nothing needs to happen, function returns. * @param context * @param force Force update */ update(context, force = false) { super.update(context, force); this.draw(context, force); } getBounds() { return this.bounds === void 0 && this._parent ? this._parent.bounds : this.bounds; } unsetBounds() { this.bounds = void 0; } /** * Update begins. * @returns MeasureState */ updateBegin(context) { if (context === void 0) throw new Error(`Context is undefined`); let bounds = this.getBounds(); if (bounds === void 0) { this.debugLog(`No bounds for element or parent, using canvas bounds`); bounds = { x: 0, y: 0, width: context.canvas.width, height: context.canvas.height }; } return [ new CanvasMeasureState(bounds, context), new CanvasLayoutState(bounds, context) ]; } updateComplete(_measureChanged, _layoutChanged) { this.canvasRegion = PlaceholderPositioned; } measureApply(m) { const different = super.measureApply(m); if (different) this.canvasRegion = PlaceholderPositioned; return different; } layoutApply(l) { const different = super.layoutApply(l); if (different) this.canvasRegion = PlaceholderPositioned; return different; } draw(ctx, force = false) { if (this._needsDrawing || force) { if (isPlaceholder(this.canvasRegion)) { if (this._layoutPosition === void 0) return; if (this._measuredSize === void 0) return; this.canvasRegion = { x: this._layoutPosition.x, y: this._layoutPosition.y, width: this._measuredSize.width, height: this._measuredSize.height }; } if (this._needsLayoutX || this._needsMeasuring) { } ctx.save(); const v = this.canvasRegion; ctx.translate(v.x, v.y); ctx.beginPath(); ctx.rect(0, 0, v.width, v.height); ctx.clip(); if (this.debugLayout) { ctx.lineWidth = 1; ctx.strokeStyle = `hsl(${this.debugHue}, 100%, 50%)`; ctx.strokeRect(0, 0, v.width, v.height); ctx.fillStyle = ctx.strokeStyle; ctx.fillText(this.id, 10, 10, v.width); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(v.width, v.height); ctx.stroke(); } this.drawSelf(ctx); this._needsDrawing = false; ctx.restore(); } for (const c of this.children) { c.draw(ctx, force); } } /** * Draw this object * @param _ctx */ drawSelf(_ctx) { } }; // src/visual/CanvasRegion.ts var CanvasSource = class { #canvasEl; #ctx; #sizeBasis; #sizeScaler; #logicalSize; #pixelScaling; #regions = []; constructor(canvasElementOrQuery, sizeBasis = `min`) { this.#canvasEl = resolveEl(canvasElementOrQuery); this.#sizeBasis = sizeBasis; this.#pixelScaling = window.devicePixelRatio || 1; this.#sizeScaler = this.#createSizeScaler(); this.#logicalSize = this.setLogicalSize({ width: this.#canvasEl.width, height: this.#canvasEl.height }); } setLogicalSize(size) { this.#logicalSize = size; const el = this.#canvasEl; el.width = size.width * this.#pixelScaling; el.height = size.height * this.#pixelScaling; el.style.width = `${size.width.toString()}px`; el.style.height = `${size.height.toString()}px`; this.#sizeScaler = this.#createSizeScaler(); this.invalidateContext(); return size; } #createSizeScaler() { let inMax = 1; switch (this.#sizeBasis) { case `min`: inMax = Math.min(this.#canvasEl.width, this.#canvasEl.height); break; case `max`: inMax = Math.max(this.#canvasEl.width, this.#canvasEl.height); break; } const s = scalerTwoWay(0, inMax, 0, 1); return { abs: s.in, rel: s.out }; } invalidateContext() { this.#ctx = void 0; } #add(region) { if (!region) throw new Error(`Param 'region' is undefined/null`); if (this.#regions.includes(region)) throw new Error(`Region already exists`); this.#regions.push(region); return region; } toAbsPoint(pt, kind = `independent`) { let { x, y } = pt; switch (kind) { case `independent`: x *= this.width; y *= this.height; } return { x, y }; } get offset() { const b = this.#canvasEl.getBoundingClientRect(); return { x: b.left, y: b.top }; } toRelPoint(pt, source, kind = `independent`, clamped = true) { let { x, y } = pt; if (source === `screen`) { const b = this.#canvasEl.getBoundingClientRect(); x -= b.x; y -= b.y; } switch (kind) { case `independent`: x /= this.width; y /= this.height; break; case `skip`: break; } if (clamped) { x = clamp(x); y = clamp(y); } return { x, y }; } toAbsRect(rect2, kind = `independent`) { let { width, height } = rect2; switch (kind) { case `independent`: width *= this.width; height *= this.height; if (rect_exports.isRectPositioned(rect2)) { return { ...this.toAbsPoint(rect2), width, height }; } } return { width, height }; } /** * Creates a region * * Absolute positioned. Uses source coordinates which don't change * ```js * source.createRegion({ * absPositioned: { x: 0, y: 0, width: 100, height: 100} * }); * ``` * * Relative positioned. Uses coordiantes relative to source dimensions. * Updated if source changes. * ```js * source.createRegion({ * relativePositioned: { x: 0, y:0, width: 1, height: 0.5 }, * scale: `independent` * }); * ``` * * Relative sized. Uses size relative to source dimension. By default centers. * ```js * source.createRegion({ * relativeSize: { width: 0.5, height: 0.5 } * position: `center` * }) * ``` * @param spec * @returns */ createRegion(spec) { const marginPx = spec.marginPx ?? 0; const marginPx2 = marginPx * 2; if (`absPositioned` in spec) { const rect2 = rect_exports.subtractSize(spec.absPositioned, marginPx, marginPx); return this.#add(new CanvasRegion(this, () => rect2)); } if (`relativePositioned` in spec) { let compute; const rect2 = spec.relativePositioned; switch (spec.scale) { case `independent`: compute = (source) => ({ x: rect2.x * source.width + marginPx, y: rect2.y * source.height + marginPx, width: rect2.width * source.width - marginPx2, height: rect2.height * source.height - marginPx2 }); break; default: throw new Error(`Param 'kind' unknown (${spec.scale})`); } return this.#add(new CanvasRegion(this, compute)); } if (`relativeSize` in spec) { let compute; const rect2 = spec.relativeSize; const position = spec.position; switch (spec.scale) { case `independent`: compute = (source) => { const width = rect2.width * source.width - marginPx2; const height = rect2.height * source.height - marginPx2; let x = source.width / 2 - width / 2; let y = source.height / 2 - height / 2; switch (position) { case `n`: y = 0; break; case `s`: y = source.height - height; break; default: } x += marginPx; y += marginPx; return { width, height, x, y }; }; break; default: throw new Error(`Param 'kind' unknown (${spec.scale})`); } return this.#add(new CanvasRegion(this, compute)); } if (`match` in spec) { const result = resolveElementTry(spec.match); if (!result.success) { throw new Error(`Could not resolve match element. ${resultErrorToString(result)}`); } const compute = (_source) => { const bounds = result.value.getBoundingClientRect(); return { x: bounds.x + marginPx, y: bounds.y + marginPx, width: bounds.width - marginPx2, height: bounds.height - marginPx2 }; }; return this.#add(new CanvasRegion(this, compute)); } throw new Error(`Spec doesn't seem valid`); } clear() { const c = this.context; c.clearRect(0, 0, this.width, this.height); } get context() { if (this.#ctx) return this.#ctx; const c = this.#canvasEl.getContext(`2d`); if (!c) throw new Error(`Could not create 2d context`); c.setTransform(1, 0, 0, 1, 0, 0); c.scale(this.#pixelScaling, this.#pixelScaling); this.#ctx = c; for (const r of this.#regions) { r.recomputeRegion(); } return this.#ctx; } get sizeScaler() { return this.#sizeScaler; } get width() { return this.#logicalSize.width; } get height() { return this.#logicalSize.height; } }; var CanvasRegion = class { #regionCompute; #r; /** * Creates, using coordinate in canvas coordinates */ constructor(source, regionCompute) { this.source = source; this.#regionCompute = regionCompute; this.#r = regionCompute(source); } /** * Calls the original `regionCompute` function passed in to the constructor * to recompute the absolute region */ recomputeRegion() { this.#r = this.#regionCompute(this.source); } /** * Converts a region-relative point (0..1) to an absolute * point, which uses region-relative coordinates. * * Eg if the region had an x,y of 100,100, `toAbsRegion({x:0,y:0})` * will return 0,0. * * @param regionRel * @param scaleBy * @returns */ toAbsRegion(regionRel, scaleBy = `both`) { switch (scaleBy) { case `both`: return { x: regionRel.x * this.#r.width, y: regionRel.y * this.#r.height }; break; } } /** * Returns a copy of `p` offset by the region's x & y * @param p * @returns */ applyRegionOffset(p) { return { x: p.x + this.#r.x, y: p.y + this.#r.y }; } /** * Draws a line from a series of points. * Assumes region-relative, % coordinates (ie 0..1 scale) * @param relativePoints Points to connect, in region-relative coordinates * @param strokeStyle Stroke style * @param lineWidth Line with */ drawConnectedPointsRelative(relativePoints, strokeStyle, lineWidth = 1) { const points = relativePoints.map((p) => this.toAbsRegion(p)); this.drawConnectedPoints(points, strokeStyle, lineWidth); } /** * Draws connected points in absolute coordinates, * however with 0,0 being the top-left of the region. * * Thus, this will apply the region offset before drawing. * @param points Points to draw * @param strokeStyle Stroke style * @param lineWidth Line width */ drawConnectedPoints(points, strokeStyle, lineWidth = 1) { const c = this.context; c.save(); c.translate(this.#r.x, this.#r.y); c.beginPath(); c.strokeStyle = strokeStyle; c.lineWidth = lineWidth; for (let index = 0; index < points.length; index++) { if (index === 0) { c.moveTo(points[index].x, points[index].y); } else { c.lineTo(points[index].x, points[index].y); } } c.stroke(); c.restore(); } /** * Fills text at a relative position * @param text * @param relPos Relative, meaning 0.5,0.5 is the middle of the region * @param fillStyle * @param baseline * @param align */ fillTextRelative(text, relPos, fillStyle = `black`, font, baseline = `alphabetic`, align = `start`) { const point = this.toAbsRegion(relPos); this.fillTextRelative(text, point, fillStyle, font, baseline, align); } /** * Fills text at a region-relative position * @param text * @param point Region relative, meaning 0,0 is top-left of region * @param fillStyle * @param baseline * @param align */ fillText(text, point, fillStyle = `black`, font, baseline = `alphabetic`, align = `start`) { const c = this.context; c.save(); c.translate(this.#r.x, this.#r.y); if (font.length > 0) { c.font = font; } c.textBaseline = baseline; c.textAlign = align; c.fillStyle = fillStyle; c.fillText(text, point.x, point.y); c.restore(); } drawCircles(relativeCircles, fillStyle, strokeStyle = ``, lineWidth = 1) { const circles = relativeCircles.map((c2) => { return { ...this.toAbsRegion(c2), radius: this.source.sizeScaler.abs(c2.radius) }; }); const c = this.context; c.save(); c.translate(this.#r.x, this.#r.y); c.fillStyle = fillStyle; c.strokeStyle = strokeStyle; c.lineWidth = lineWidth; for (const circle2 of circles) { c.beginPath(); c.arc(circle2.x, circle2.y, circle2.radius, 0, piPi); c.closePath(); if (fillStyle.length > 0) { c.fill(); } if (strokeStyle.length > 0) { c.stroke(); } } c.restore(); } clear() { const c = this.context; c.clearRect(this.#r.x, this.#r.y, this.#r.width, this.#r.height); } fill(fillStyle = `white`) { const c = this.context; c.fillStyle = fillStyle; c.fillRect(this.#r.x, this.#r.y, this.#r.width, this.#r.height); } drawBounds(strokeStyle, lineWidth = 1) { this.drawConnectedPointsRelative([ { x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }, { x: 0, y: 0 } ], strokeStyle, lineWidth); this.drawConnectedPointsRelative([ { x: 0, y: 1 }, { x: 1, y: 0 } ], strokeStyle, lineWidth); this.drawConnectedPointsRelative([ { x: 0, y: 0 }, { x: 1, y: 1 } ], strokeStyle, lineWidth); } /** * Converts a point to a region-relative one. * @param pt * @param kind * @returns */ toRelPoint(pt, source = `screen`, kind = `independent`, clamped = true) { pt = this.source.toRelPoint(pt, source, `skip`, false); let { x, y } = pt; x -= this.x; y -= this.y; switch (kind) { case `independent`: x /= this.width; y /= this.height; } if (clamped) { x = clamp(x); y = clamp(y); } return { x, y }; } absToRegionPoint(pt, source, clamped) { if (source === `screen`) { pt = point_exports.subtract(pt, this.source.offset); } let { x, y } = pt; x -= this.x; y -= this.y; if (clamped) { if (x < 0) x = 0; if (y < 0) y = 0; if (x > this.width + this.x) x = this.x + this.width; if (y > this.height + this.y) y = this.y + this.height; } return { x, y }; } get center() { return rect_exports.center(this.#r); } get context() { return this.source.context; } set region(value) { this.#r = value; } get region() { return this.#r; } get width() { return this.#r.width; } get height() { return this.#r.height; } get x() { return this.#r.x; } get y() { return this.#r.y; } get dimensionMin() { return Math.min(this.#r.width, this.#r.height); } }; // src/visual/BipolarView.ts var BipolarView_exports = {}; __export(BipolarView_exports, { init: () => init }); // src/visual/Drawing.ts var Drawing_exports = {}; __export(Drawing_exports, { arc: () => arc, bezier: () => bezier, circle: () => circle, connectedPoints: () => connectedPoints, copyToImg: () => copyToImg, dot: () => dot, drawingStack: () => drawingStack, ellipse: () => ellipse, getContext: () => getContext, line: () => line, lineThroughPoints: () => lineThroughPoints, makeHelper: () => makeHelper, paths: () => paths, pointLabels: () => pointLabels, rect: () => rect, textBlock: () => textBlock, textBlockAligned: () => textBlockAligned, textHeight: () => textHeight, textRect: () => textRect, textWidth: () => textWidth, translatePoint: () => translatePoint, triangle: () => triangle }); var PIPI = Math.PI * 2; var getContext = (canvasElementContextOrQuery) => { if (canvasElementContextOrQuery === null) { throw new Error( `canvasElCtxOrQuery null. Must be a 2d drawing context or Canvas element` ); } if (canvasElementContextOrQuery === void 0) { throw new Error( `canvasElCtxOrQuery undefined. Must be a 2d drawing context or Canvas element` ); } const ctx = canvasElementContextOrQuery instanceof CanvasRenderingContext2D ? canvasElementContextOrQuery : canvasElementContextOrQuery instanceof HTMLCanvasElement ? canvasElementContextOrQuery.getContext(`2d`) : typeof canvasElementContextOrQuery === `string` ? resolveEl(canvasElementContextOrQuery).getContext(`2d`) : canvasElementContextOrQuery; if (ctx === null) throw new Error(`Could not create 2d context for canvas`); return ctx; }; var makeHelper = (ctxOrCanvasEl, canvasBounds) => { const ctx = getContext(ctxOrCanvasEl); return { ctx, paths(pathsToDraw, opts) { paths(ctx, pathsToDraw, opts); }, line(lineToDraw, opts) { line(ctx, lineToDraw, opts); }, rect(rectsToDraw, opts) { rect(ctx, rectsToDraw, opts); }, bezier(bezierToDraw, opts) { bezier(ctx, bezierToDraw, opts); }, connectedPoints(pointsToDraw, opts) { connectedPoints(ctx, pointsToDraw, opts); }, pointLabels(pointsToDraw, opts) { pointLabels(ctx, pointsToDraw, opts); }, dot(dotPosition, opts) { dot(ctx, dotPosition, opts); }, circle(circlesToDraw, opts) { circle(ctx, circlesToDraw, opts); }, arc(arcsToDraw, opts) { arc(ctx, arcsToDraw, opts); }, textBlock(lines, opts) { if (opts.bounds === void 0 && canvasBounds !== void 0) { opts = { ...opts, bounds: { ...canvasBounds, x: 0, y: 0 } }; } textBlock(ctx, lines, opts); } }; }; var optsOp = (opts) => coloringOp(opts.strokeStyle, opts.fillStyle); var applyOpts = (ctx, opts = {}, ...additionalOps) => { if (ctx === void 0) throw new Error(`ctx undefined`); const stack = drawingStack(ctx).push(optsOp(opts), ...additionalOps); stack.apply(); return stack; }; var arc = (ctx, arcs, opts = {}) => { applyOpts(ctx, opts); const draw = (arc2) => { ctx.beginPath(); ctx.arc(arc2.x, arc2.y, arc2.radius, arc2.startRadian, arc2.endRadian); ctx.stroke(); }; const arcsArray = Array.isArray(arcs) ? arcs : [arcs]; for (const arc2 of arcsArray) { draw(arc2); } }; var coloringOp = (strokeStyle, fillStyle) => { const apply = (ctx) => { if (fillStyle) ctx.fillStyle = fillStyle; if (strokeStyle) ctx.strokeStyle = strokeStyle; }; return apply; }; var lineOp = (lineWidth, lineJoin, lineCap) => { const apply = (ctx) => { if (lineWidth) ctx.lineWidth = lineWidth; if (lineJoin) ctx.lineJoin = lineJoin; if (lineCap) ctx.lineCap = lineCap; }; return apply; }; var drawingStack = (ctx, stk) => { if (stk === void 0) stk = new StackImmutable(); const push = (...ops) => { if (stk === void 0) stk = new StackImmutable(); const s = stk.push(...ops); for (const o of ops) o(ctx); return drawingStack(ctx, s); }; const pop = () => { const s = stk?.pop(); return drawingStack(ctx, s); }; const apply = () => { if (stk === void 0) return drawingStack(ctx); for (const op of stk.data) op(ctx); return drawingStack(ctx, stk); }; return { push, pop, apply }; }; var lineThroughPoints = (ctx, points, opts) => { applyOpts(ctx, opts); ctx.moveTo(points[0].x, points[0].y); for (const [index, p] of points.entries()) { if (index + 2 >= points.length) continue; const pNext = points[index + 1]; const mid = { x: (p.x + pNext.x) / 2, y: (p.y + pNext.y) / 2 }; const cpX1 = (mid.x + p.x) / 2; const cpX2 = (mid.x + pNext.x) / 2; ctx.quadraticCurveTo(cpX1, pNext.y, mid.x, mid.y); ctx.quadraticCurveTo(cpX2, pNext.y, pNext.x, pNext.y); } }; var circle = (ctx, circlesToDraw, opts = {}) => { applyOpts(ctx, opts); const draw = (c) => { ctx.beginPath(); ctx.arc(c.x, c.y, c.radius, 0, PIPI); if (opts.strokeStyle) ctx.stroke(); if (opts.fillStyle) ctx.fill(); }; if (Array.isArray(circlesToDraw)) { for (const c of circlesToDraw) draw(c); } else { draw(circlesToDraw); } }; var ellipse = (ctx, ellipsesToDraw, opts = {}) => { applyOpts(ctx, opts); const draw = (ellipse2) => { ctx.beginPath(); const rotation = ellipse2.rotation ?? 0; const startAngle = ellipse2.startAngle ?? 0; const endAngle = ellipse2.endAngle ?? PIPI; ctx.ellipse(ellipse2.x, ellipse2.y, ellipse2.radiusX, ellipse2.radiusY, rotation, startAngle, endAngle); if (opts.strokeStyle) ctx.stroke(); if (opts.fillStyle) ctx.fill(); }; const ellipsesArray = Array.isArray(ellipsesToDraw) ? ellipsesToDraw : [ellipsesToDraw]; for (const ellipse2 of ellipsesArray) { draw(ellipse2); } }; var paths = (ctx, pathsToDraw, opts = {}) => { applyOpts(ctx, opts); const draw = (path) => { if (isQuadraticBezier(path)) quadraticBezier(ctx, path, opts); else if (isLine(path)) line(ctx, path, opts); else throw new Error(`Unknown path type ${JSON.stringify(path)}`); }; if (Array.isArray(pathsToDraw)) { for (const p of pathsToDraw) draw(p); } else { draw(pathsToDraw); } }; var connectedPoints = (ctx, pts, opts = {}) => { const shouldLoop = opts.loop ?? false; throwArrayTest(pts); if (pts.length === 0) return; for (const [index, pt] of pts.entries()) guard(pt, `Index ${index}`); applyOpts(ctx, opts); if (opts.lineWidth) ctx.lineWidth = opts.lineWidth; ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); for (const pt of pts) ctx.lineTo(pt.x, pt.y); if (shouldLoop) ctx.lineTo(pts[0].x, pts[0].y); if (opts.strokeStyle || opts.strokeStyle === void 0 && opts.fillStyle === void 0) { ctx.stroke(); } if (opts.fillStyle) { ctx.fill(); } }; var pointLabels = (ctx, pts, opts = {}, labels) => { if (pts.length === 0) return; for (const [index, pt] of pts.entries()) guard(pt, `Index ${index}`); applyOpts(ctx, opts); for (const [index, pt] of pts.entries()) { const label = labels !== void 0 && index < labels.length ? labels[index] : index.toString(); ctx.fillText(label.toString(), pt.x, pt.y); } }; var translatePoint = (ctx, point) => { const m = ctx.getTransform(); return { x: point.x * m.a + point.y * m.c + m.e, y: point.x * m.b + point.y * m.d + m.f }; }; var copyToImg = (canvasEl) => { const img = document.createElement(`img`); img.src = canvasEl.toDataURL(`image/jpeg`); return img; }; var dot = (ctx, pos, opts) => { if (opts === void 0) opts = {}; const radius = opts.radius ?? 10; const positions = Array.isArray(pos) ? pos : [pos]; const stroke = opts.stroke ? opts.stroke : opts.strokeStyle !== void 0; let filled = opts.filled ? opts.filled : opts.fillStyle !== void 0; if (!stroke && !filled) filled = true; applyOpts(ctx, opts); for (const pos2 of positions) { ctx.beginPath(); if (`radius` in pos2) { ctx.arc(pos2.x, pos2.y, pos2.radius, 0, 2 * Math.PI); } else { ctx.arc(pos2.x, pos2.y, radius, 0, 2 * Math.PI); } if (filled) { ctx.fill(); } if (stroke) { ctx.stroke(); } } }; var bezier = (ctx, bezierToDraw, opts) => { if (isQuadraticBezier(bezierToDraw)) { quadraticBezier(ctx, bezierToDraw, opts); } else if (isCubicBezier(bezierToDraw)) { cubicBezier(ctx, bezierToDraw, opts); } }; var cubicBezier = (ctx, bezierToDraw, opts = {}) => { let stack = applyOpts(ctx, opts); const { a, b, cubic1, cubic2 } = bezierToDraw; const isDebug = opts.debug ?? false; if (isDebug) { } ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.bezierCurveTo(cubic1.x, cubic1.y, cubic2.x, cubic2.y, b.x, b.y); ctx.stroke(); if (isDebug) { stack = stack.push( optsOp({ ...opts, strokeStyle: multiplyOpacity(opts.strokeStyle ?? `silver`, 0.6), fillStyle: multiplyOpacity(opts.fillStyle ?? `yellow`, 0.4) }) ); stack.apply(); ctx.moveTo(a.x, a.y); ctx.lineTo(cubic1.x, cubic1.y); ctx.stroke(); ctx.moveTo(b.x, b.y); ctx.lineTo(cubic2.x, cubic2.y); ctx.stroke(); ctx.fillText(`a`, a.x + 5, a.y); ctx.fillText(`b`, b.x + 5, b.y); ctx.fillText(`c1`, cubic1.x + 5, cubic1.y); ctx.fillText(`c2`, cubic2.x + 5, cubic2.y); dot(ctx, cubic1, { radius: 3 }); dot(ctx, cubic2, { radius: 3 }); dot(ctx, a, { radius: 3 }); dot(ctx, b, { radius: 3 }); stack = stack.pop(); stack.apply(); } }; var quadraticBezier = (ctx, bezierToDraw, opts = {}) => { const { a, b, quadratic } = bezierToDraw; const isDebug = opts.debug ?? false; let stack = applyOpts(ctx, opts); ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.quadraticCurveTo(quadratic.x, quadratic.y, b.x, b.y); ctx.stroke(); if (isDebug) { stack = stack.push( optsOp({ ...opts, strokeStyle: multiplyOpacity(opts.strokeStyle ?? `silver`, 0.6), fillStyle: multiplyOpacity(opts.fillStyle ?? `yellow`, 0.4) }) ); connectedPoints(ctx, [a, quadratic, b]); ctx.fillText(`a`, a.x + 5, a.y); ctx.fillText(`b`, b.x + 5, b.y); ctx.fillText(`h`, quadratic.x + 5, quadratic.y); dot(ctx, quadratic, { radius: 3 }); dot(ctx, a, { radius: 3 }); dot(ctx, b, { radius: 3 }); stack = stack.pop(); stack.apply(); } }; var line = (ctx, toDraw, opts = {}) => { const isDebug = opts.debug ?? false; const o = lineOp(opts.lineWidth, opts.lineJoin, opts.lineCap); applyOpts(ctx, opts, o); const draw = (d) => { const { a, b } = d; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); if (isDebug) { ctx.fillText(`a`, a.x, a.y); ctx.fillText(`b`, b.x, b.y); dot(ctx, a, { radius: 5, strokeStyle: `black` }); dot(ctx, b, { radius: 5, strokeStyle: `black` }); } ctx.stroke(); }; if (Array.isArray(toDraw)) { for (const t of toDraw) draw(t); } else { draw(toDraw); } }; var triangle = (ctx, toDraw, opts = {}) => { applyOpts(ctx, opts); const draw = (t) => { connectedPoints(ctx, corners2(t), { ...opts, loop: true }); if (opts.debug) { pointLabels(ctx, corners2(t), void 0, [`a`, `b`, `c`]); } }; if (Array.isArray(toDraw)) { for (const t of toDraw) { draw(t); } } else { draw(toDraw); } }; var rect = (ctx, toDraw, opts = {}) => { applyOpts(ctx, opts); const filled = opts.filled ?? (opts.fillStyle === void 0 ? false : true); const stroke = opts.stroke ?? (opts.strokeStyle === void 0 ? false : true); const draw = (d) => { const x = `x` in d ? d.x : 0; const y = `y` in d ? d.y : 0; if (filled) ctx.fillRect(x, y, d.width, d.height); if (stroke) { if (opts.strokeWidth) ctx.lineWidth = opts.strokeWidth; ctx.strokeRect(x, y, d.width, d.height); } if (opts.crossed) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(d.width, d.height); ctx.stroke(); ctx.moveTo(0, d.height); ctx.lineTo(d.width, 0); ctx.stroke(); } if (opts.debug) { pointLabels(ctx, corners(d), void 0, [`NW`, `NE`, `SE`, `SW`]); } }; if (Array.isArray(toDraw)) { for (const t of toDraw) { draw(t); } } else { draw(toDraw); } }; var textWidth = (ctx, text, padding = 0, widthMultiple) => { const rect2 = textRect(ctx, text, padding, widthMultiple); return rect2.width; }; var textRect = (ctx, text, padding = 0, widthMultiple) => { if (text === void 0 || text === null || text.length === 0) return Empty2; const m = ctx.measureText(text); const width = widthMultiple ? quantiseEvery(m.width, widthMultiple) + padding : m.width + padding; return { width, height: m.actualBoundingBoxAscent + m.actualBoundingBoxDescent + padding + padding }; }; var textHeight = (ctx, text, padding = 0) => { const rect2 = textRect(ctx, text, padding); return rect2.height; }; var textBlock = (ctx, lines, opts) => { applyOpts(ctx, opts); const anchorPadding = opts.anchorPadding ?? 0; const align = opts.align ?? `top`; const anchor = opts.anchor; const bounds = opts.bounds ?? { x: 0, y: 0, width: 1e6, height: 1e6 }; const blocks = lines.map((l) => ctx.measureText(l)); const widths = blocks.map((tm) => tm.width); const heights = blocks.map( (tm) => tm.actualBoundingBoxAscent + tm.actualBoundingBoxDescent + 3 ); const maxWidth = Math.max(...widths); const totalHeight = heights.reduce((accumulator, value) => accumulator + value, 0); let { x, y } = anchor; if (anchor.x + maxWidth > bounds.width) { x = bounds.width - (maxWidth + anchorPadding); } else x -= anchorPadding; if (x < bounds.x) x = bounds.x + anchorPadding; if (anchor.y + totalHeight > bounds.height) { y = bounds.height - (totalHeight + anchorPadding); } else y -= anchorPadding; if (y < bounds.y) y = bounds.y + anchorPadding; if (align === `top`) { ctx.textBaseline = `top`; } else { ctx.textBaseline = `middle`; } for (const [index, line2] of lines.entries()) { ctx.fillText(line2, x, y); y += heights[index]; } }; var textBlockAligned = (ctx, text, opts) => { const { bounds } = opts; const { horiz = `left`, vert = `top` } = opts; const lines = typeof text === `string` ? [text] : text; applyOpts(ctx, opts); ctx.save(); ctx.translate(bounds.x, bounds.y); ctx.textAlign = `left`; ctx.textBaseline = `top`; const middleX = bounds.width / 2; const middleY = bounds.height / 2; const blocks = lines.map((l) => ctx.measureText(l)); const heights = blocks.map( (tm) => tm.actualBoundingBoxAscent + tm.actualBoundingBoxDescent ); const totalHeight = heights.reduce((accumulator, value) => accumulator + value, 0); let y = 0; if (vert === `center`) y = middleY - totalHeight / 2; else if (vert === `bottom`) { y = bounds.height - totalHeight; } for (const [index, line2] of lines.entries()) { let x = 0; if (horiz === `center`) x = middleX - blocks[index].width / 2; else if (horiz === `right`) x = bounds.width - blocks[index].width; ctx.fillText(line2, x, y); y += heights[index]; } ctx.restore(); }; // src/visual/BipolarView.ts function getNumericAttribute(el, name, defaultValue) { const a = el.getAttribute(name); if (a === null) return defaultValue; return Number.parseInt(a); } var init = (elementQuery, options = {}) => { const element = document.querySelector(elementQuery); if (!element) throw new Error(`Element query could not be found (${elementQue