ixfx
Version:
A framework for programming interactivity
1,733 lines (1,724 loc) • 80 kB
JavaScript
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