@fundamental-engine/dom 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/apply-recipe.d.ts +103 -0
- package/dist/apply-recipe.d.ts.map +1 -0
- package/dist/apply-recipe.js +271 -0
- package/dist/apply-recipe.js.map +1 -0
- package/dist/bind-data.d.ts +72 -0
- package/dist/bind-data.d.ts.map +1 -0
- package/dist/bind-data.js +164 -0
- package/dist/bind-data.js.map +1 -0
- package/dist/browser-host.d.ts +11 -0
- package/dist/browser-host.d.ts.map +1 -0
- package/dist/browser-host.js +41 -0
- package/dist/browser-host.js.map +1 -0
- package/dist/contours.d.ts +79 -0
- package/dist/contours.d.ts.map +1 -0
- package/dist/contours.js +88 -0
- package/dist/contours.js.map +1 -0
- package/dist/env.d.ts +39 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +47 -0
- package/dist/env.js.map +1 -0
- package/dist/export-dom.d.ts +7 -0
- package/dist/export-dom.d.ts.map +1 -0
- package/dist/export-dom.js +28 -0
- package/dist/export-dom.js.map +1 -0
- package/dist/feedback.d.ts +57 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +134 -0
- package/dist/feedback.js.map +1 -0
- package/dist/field-nav.d.ts +35 -0
- package/dist/field-nav.d.ts.map +1 -0
- package/dist/field-nav.js +82 -0
- package/dist/field-nav.js.map +1 -0
- package/dist/flip.d.ts +31 -0
- package/dist/flip.d.ts.map +1 -0
- package/dist/flip.js +65 -0
- package/dist/flip.js.map +1 -0
- package/dist/governor.d.ts +37 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +72 -0
- package/dist/governor.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +78 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +153 -0
- package/dist/lint.js.map +1 -0
- package/dist/measurement.d.ts +44 -0
- package/dist/measurement.d.ts.map +1 -0
- package/dist/measurement.js +95 -0
- package/dist/measurement.js.map +1 -0
- package/dist/metrics.d.ts +70 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +119 -0
- package/dist/metrics.js.map +1 -0
- package/dist/overlays.d.ts +48 -0
- package/dist/overlays.d.ts.map +1 -0
- package/dist/overlays.js +48 -0
- package/dist/overlays.js.map +1 -0
- package/dist/perf.d.ts +62 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +94 -0
- package/dist/perf.js.map +1 -0
- package/dist/platform.d.ts +40 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +61 -0
- package/dist/platform.js.map +1 -0
- package/dist/relationships.d.ts +79 -0
- package/dist/relationships.d.ts.map +1 -0
- package/dist/relationships.js +155 -0
- package/dist/relationships.js.map +1 -0
- package/dist/schedule.d.ts +84 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +91 -0
- package/dist/schedule.js.map +1 -0
- package/dist/state.d.ts +36 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +113 -0
- package/dist/state.js.map +1 -0
- package/dist/text-bodies.d.ts +71 -0
- package/dist/text-bodies.d.ts.map +1 -0
- package/dist/text-bodies.js +159 -0
- package/dist/text-bodies.js.map +1 -0
- package/dist/thread-overlay.d.ts +63 -0
- package/dist/thread-overlay.d.ts.map +1 -0
- package/dist/thread-overlay.js +110 -0
- package/dist/thread-overlay.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/visual-bindings.d.ts +95 -0
- package/dist/visual-bindings.d.ts.map +1 -0
- package/dist/visual-bindings.js +211 -0
- package/dist/visual-bindings.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bindData — make real application data participate in field behavior. Records become bodies, mapped
|
|
3
|
+
* metrics become state, mapped relationships become graph edges, and a recipe supplies the behavior
|
|
4
|
+
* (metric/feedback framework) via applyRecipe(). Updates are deterministic (diff by id); removed
|
|
5
|
+
* records decay before they leave rather than popping.
|
|
6
|
+
*
|
|
7
|
+
* const binding = bindData(container, records, mapper, { recipe: 'search-relevance-field' });
|
|
8
|
+
* binding.update(nextRecords);
|
|
9
|
+
* binding.destroy();
|
|
10
|
+
*
|
|
11
|
+
* The recipe frames the field (which metrics → --field-* are tracked); the per-record mapper owns the
|
|
12
|
+
* body tokens, metric values, and relationships — so the data drives the field, not a mock.
|
|
13
|
+
*/
|
|
14
|
+
import { recipeById } from '@fundamental-engine/core';
|
|
15
|
+
import { applyRecipe } from "./apply-recipe.js";
|
|
16
|
+
/** Diff two id sets (pure). */
|
|
17
|
+
export function diffIds(prev, next) {
|
|
18
|
+
const p = new Set(prev);
|
|
19
|
+
const n = new Set(next);
|
|
20
|
+
const added = [];
|
|
21
|
+
const removed = [];
|
|
22
|
+
const kept = [];
|
|
23
|
+
for (const id of n)
|
|
24
|
+
(p.has(id) ? kept : added).push(id);
|
|
25
|
+
for (const id of p)
|
|
26
|
+
if (!n.has(id))
|
|
27
|
+
removed.push(id);
|
|
28
|
+
return { added, removed, kept };
|
|
29
|
+
}
|
|
30
|
+
const setNum = (el, k, v) => {
|
|
31
|
+
if (v != null && Number.isFinite(v))
|
|
32
|
+
el.setAttribute(k, String(v));
|
|
33
|
+
else
|
|
34
|
+
el.removeAttribute(k);
|
|
35
|
+
};
|
|
36
|
+
/** Apply a mapped record's body tokens, metric values, label/content, and relationship anchors. */
|
|
37
|
+
function applyMapped(el, m, contentHtml) {
|
|
38
|
+
const doc = el.ownerDocument;
|
|
39
|
+
el.setAttribute('data-body', m.body.tokens.join(' '));
|
|
40
|
+
setNum(el, 'data-strength', m.body.strength);
|
|
41
|
+
setNum(el, 'data-range', m.body.range);
|
|
42
|
+
setNum(el, 'data-spin', m.body.spin);
|
|
43
|
+
setNum(el, 'data-angle', m.body.angle);
|
|
44
|
+
if (m.body.feedback)
|
|
45
|
+
el.setAttribute('data-feedback', '');
|
|
46
|
+
// metric values → data-field-<metric> (the recipe + applyRecipe turn these into --field-* state)
|
|
47
|
+
for (const [k, v] of Object.entries(m.metrics ?? {}))
|
|
48
|
+
setNum(el, `data-field-${k}`, v);
|
|
49
|
+
// domain content (overrides label), else the plain label
|
|
50
|
+
if (contentHtml != null) {
|
|
51
|
+
let box = el.querySelector(':scope > .bd-content');
|
|
52
|
+
if (!box) {
|
|
53
|
+
box = doc.createElement('div');
|
|
54
|
+
box.className = 'bd-content';
|
|
55
|
+
el.prepend(box);
|
|
56
|
+
}
|
|
57
|
+
box.innerHTML = contentHtml;
|
|
58
|
+
}
|
|
59
|
+
else if (m.label != null) {
|
|
60
|
+
let lbl = el.querySelector(':scope > .bd-label');
|
|
61
|
+
if (!lbl) {
|
|
62
|
+
lbl = doc.createElement('span');
|
|
63
|
+
lbl.className = 'bd-label';
|
|
64
|
+
el.prepend(lbl);
|
|
65
|
+
}
|
|
66
|
+
lbl.textContent = m.label;
|
|
67
|
+
}
|
|
68
|
+
// relationships → child anchors the RelationshipRegistry discovers (one per edge)
|
|
69
|
+
el.querySelectorAll(':scope > .bd-rel').forEach((a) => a.remove());
|
|
70
|
+
for (const r of m.relationships ?? []) {
|
|
71
|
+
const a = doc.createElement('a');
|
|
72
|
+
a.className = 'bd-rel';
|
|
73
|
+
a.setAttribute('aria-hidden', 'true');
|
|
74
|
+
a.setAttribute('href', `#${r.to}`);
|
|
75
|
+
a.setAttribute('data-field-relation', r.type);
|
|
76
|
+
a.setAttribute('data-field-target', `#${r.to}`);
|
|
77
|
+
if (r.strength != null)
|
|
78
|
+
a.setAttribute('data-field-strength', String(r.strength));
|
|
79
|
+
el.appendChild(a);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Bind records to a field. Returns a handle with update()/destroy(). */
|
|
83
|
+
export function bindData(container, records, mapper, options = {}) {
|
|
84
|
+
const decayMs = options.decayMs ?? 400;
|
|
85
|
+
const tag = options.tag ?? 'div';
|
|
86
|
+
const doc = container.ownerDocument;
|
|
87
|
+
const els = new Map();
|
|
88
|
+
let applied = null;
|
|
89
|
+
const recipeArg = options.recipe;
|
|
90
|
+
const reapply = () => {
|
|
91
|
+
applied?.destroy();
|
|
92
|
+
applied = null;
|
|
93
|
+
const items = [...els.values()].filter((e) => !('bdExiting' in e.dataset));
|
|
94
|
+
const recipe = typeof recipeArg === 'string' ? recipeById(recipeArg) : recipeArg;
|
|
95
|
+
if (recipe && items.length)
|
|
96
|
+
applied = applyRecipe(container, recipe, { bodies: items, annotateBodies: false, reducedMotion: options.reducedMotion });
|
|
97
|
+
};
|
|
98
|
+
const render = (recs) => {
|
|
99
|
+
const mapped = recs.map((rec, i) => mapper(rec, i));
|
|
100
|
+
const nextIds = mapped.map((m) => m.id);
|
|
101
|
+
const { added, removed } = diffIds(els.keys(), nextIds);
|
|
102
|
+
mapped.forEach((m, i) => {
|
|
103
|
+
let el = els.get(m.id);
|
|
104
|
+
if (!el) {
|
|
105
|
+
el = doc.createElement(tag);
|
|
106
|
+
el.dataset.bdId = m.id;
|
|
107
|
+
el.id = m.id; // addressable so relationships/anchors can target a record by id
|
|
108
|
+
if (options.className)
|
|
109
|
+
el.className = options.className;
|
|
110
|
+
el.dataset.bdEntering = '';
|
|
111
|
+
container.appendChild(el);
|
|
112
|
+
els.set(m.id, el);
|
|
113
|
+
const created = el;
|
|
114
|
+
if (typeof requestAnimationFrame !== 'undefined')
|
|
115
|
+
requestAnimationFrame(() => created.removeAttribute('data-bd-entering'));
|
|
116
|
+
else
|
|
117
|
+
el.removeAttribute('data-bd-entering');
|
|
118
|
+
}
|
|
119
|
+
applyMapped(el, m, options.content?.(recs[i], m));
|
|
120
|
+
container.appendChild(el); // keep DOM order aligned with records
|
|
121
|
+
});
|
|
122
|
+
for (const id of removed) {
|
|
123
|
+
const el = els.get(id);
|
|
124
|
+
if (!el)
|
|
125
|
+
continue;
|
|
126
|
+
el.dataset.bdExiting = ''; // CSS fades it; zero its metric vars so feedback eases down
|
|
127
|
+
for (const attr of Array.from(el.attributes))
|
|
128
|
+
if (attr.name.startsWith('data-field-'))
|
|
129
|
+
el.setAttribute(attr.name, '0');
|
|
130
|
+
els.delete(id);
|
|
131
|
+
const finish = () => {
|
|
132
|
+
el.remove();
|
|
133
|
+
reapply();
|
|
134
|
+
};
|
|
135
|
+
if (typeof setTimeout !== 'undefined')
|
|
136
|
+
setTimeout(finish, decayMs);
|
|
137
|
+
else
|
|
138
|
+
finish();
|
|
139
|
+
}
|
|
140
|
+
if (added.length || removed.length)
|
|
141
|
+
reapply();
|
|
142
|
+
};
|
|
143
|
+
render(records);
|
|
144
|
+
return {
|
|
145
|
+
container,
|
|
146
|
+
update: render,
|
|
147
|
+
ids: () => [...els.keys()],
|
|
148
|
+
applied: () => applied,
|
|
149
|
+
inspect: () => {
|
|
150
|
+
if (!applied)
|
|
151
|
+
return null;
|
|
152
|
+
const ins = applied.inspect();
|
|
153
|
+
return { records: els.size, bodies: ins.measurements, relationships: ins.relationships };
|
|
154
|
+
},
|
|
155
|
+
destroy: () => {
|
|
156
|
+
applied?.destroy();
|
|
157
|
+
applied = null;
|
|
158
|
+
for (const el of els.values())
|
|
159
|
+
el.remove();
|
|
160
|
+
els.clear();
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=bind-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bind-data.js","sourceRoot":"","sources":["../src/bind-data.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browserHost — the default {@link FieldHost}, binding the renderer-agnostic core engine to the
|
|
3
|
+
* browser (`window` / `document` / `requestAnimationFrame`). It lives in `@fundamental-engine/dom` (the DOM
|
|
4
|
+
* participation layer), NOT in `Fundamental` — core imports zero DOM. `createField(canvas, opts)`
|
|
5
|
+
* requires a host; pass `browserHost()` in the browser (or `createBrowserField` for the convenience),
|
|
6
|
+
* or a custom host to drive the same engine from a headless renderer / a different document / a test.
|
|
7
|
+
*/
|
|
8
|
+
import type { FieldHost } from '@fundamental-engine/core';
|
|
9
|
+
/** Build a FieldHost backed by `window` / `document`. */
|
|
10
|
+
export declare function browserHost(): FieldHost;
|
|
11
|
+
//# sourceMappingURL=browser-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-host.d.ts","sourceRoot":"","sources":["../src/browser-host.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAK1D,yDAAyD;AACzD,wBAAgB,WAAW,IAAI,SAAS,CAkCvC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { prefersReducedMotion, pageHidden } from "./env.js";
|
|
2
|
+
const INPUT_EVENTS = ['pointerdown', 'wheel', 'keydown', 'touchstart'];
|
|
3
|
+
/** Build a FieldHost backed by `window` / `document`. */
|
|
4
|
+
export function browserHost() {
|
|
5
|
+
return {
|
|
6
|
+
root: document,
|
|
7
|
+
viewport: () => ({ width: window.innerWidth, height: window.innerHeight, dpr: window.devicePixelRatio || 1 }),
|
|
8
|
+
scrollY: () => window.scrollY || 0,
|
|
9
|
+
scrollHeight: () => document.documentElement.scrollHeight,
|
|
10
|
+
reducedMotion: () => prefersReducedMotion(),
|
|
11
|
+
hidden: () => pageHidden(),
|
|
12
|
+
raf: (cb) => requestAnimationFrame(cb),
|
|
13
|
+
cancelRaf: (id) => cancelAnimationFrame(id),
|
|
14
|
+
createCanvas: () => document.createElement('canvas'),
|
|
15
|
+
onResize: (cb) => {
|
|
16
|
+
window.addEventListener('resize', cb, { passive: true });
|
|
17
|
+
return () => window.removeEventListener('resize', cb);
|
|
18
|
+
},
|
|
19
|
+
onScroll: (cb) => {
|
|
20
|
+
window.addEventListener('scroll', cb, { passive: true });
|
|
21
|
+
return () => window.removeEventListener('scroll', cb);
|
|
22
|
+
},
|
|
23
|
+
onVisibility: (cb) => {
|
|
24
|
+
document.addEventListener('visibilitychange', cb);
|
|
25
|
+
return () => document.removeEventListener('visibilitychange', cb);
|
|
26
|
+
},
|
|
27
|
+
onInput: (cb) => {
|
|
28
|
+
for (const ev of INPUT_EVENTS)
|
|
29
|
+
window.addEventListener(ev, cb, { passive: true });
|
|
30
|
+
return () => {
|
|
31
|
+
for (const ev of INPUT_EVENTS)
|
|
32
|
+
window.removeEventListener(ev, cb);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
onBodyEvent: (type, cb) => {
|
|
36
|
+
document.addEventListener(type, cb);
|
|
37
|
+
return () => document.removeEventListener(type, cb);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=browser-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-host.js","sourceRoot":"","sources":["../src/browser-host.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE5D,MAAM,YAAY,GAAG,CAAC,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,CAAU,CAAC;AAEhF,yDAAyD;AACzD,MAAM,UAAU,WAAW;IACzB,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,GAAG,EAAE,MAAM,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC;QAC7G,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC;QAClC,YAAY,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,YAAY;QACzD,aAAa,EAAE,GAAG,EAAE,CAAC,oBAAoB,EAAE;QAC3C,MAAM,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE;QAC1B,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,qBAAqB,CAAC,EAAE,CAAC;QACtC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,oBAAoB,CAAC,EAAE,CAAC;QAC3C,YAAY,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC;QACpD,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE;YACf,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE;YACf,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,YAAY,EAAE,CAAC,EAAE,EAAE,EAAE;YACnB,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;YAClD,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE;YACd,KAAK,MAAM,EAAE,IAAI,YAAY;gBAAE,MAAM,CAAC,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAClF,OAAO,GAAG,EAAE;gBACV,KAAK,MAAM,EAAE,IAAI,YAAY;oBAAE,MAAM,CAAC,mBAAmB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YACpE,CAAC,CAAC;QACJ,CAAC;QACD,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE;YACxB,QAAQ,CAAC,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACpC,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACtD,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contour typography (#257, #363) — glyph-outline generation for ANY font the author applies to a
|
|
3
|
+
* body element. The Contour Sink tier of Body Matter Interaction: the element absorbs field matter,
|
|
4
|
+
* the generated vector layer shows what that absorption means, the semantic text remains the source
|
|
5
|
+
* of meaning.
|
|
6
|
+
*
|
|
7
|
+
* Fundamental ships no font parser (the zero-dependency rule): the caller supplies the parsed font —
|
|
8
|
+
* any object structurally matching `ContourFont`, which opentype.js's `Font` satisfies directly:
|
|
9
|
+
*
|
|
10
|
+
* import { load } from 'opentype.js';
|
|
11
|
+
* import { contourSvgFor } from '@fundamental-engine/dom';
|
|
12
|
+
* const font = await load('/fonts/your-font.woff'); // WHATEVER face the element uses
|
|
13
|
+
* const handle = contourSvgFor(document.querySelector('#hero-title'), font, { rings: 3 });
|
|
14
|
+
* // → an aria-hidden SVG bound via data-field-visual-for, inserted after the element;
|
|
15
|
+
* // the platform's state mirroring carries --d / --load onto it from the body.
|
|
16
|
+
*
|
|
17
|
+
* The same primitive runs in node for build-time generation (parse the font file, call
|
|
18
|
+
* `contourPathData`, commit the output) — the site's gen-contours script is that usage.
|
|
19
|
+
*
|
|
20
|
+
* Layout is per-glyph with pair kerning, no shaping: correct for Latin-script display text, the
|
|
21
|
+
* contour use case. Complex scripts (ligatures, contextual forms) need a real shaper and are out
|
|
22
|
+
* of scope here.
|
|
23
|
+
*/
|
|
24
|
+
/** The minimal parsed-font surface this module needs — opentype.js `Font` satisfies it. */
|
|
25
|
+
export interface ContourGlyph {
|
|
26
|
+
advanceWidth?: number;
|
|
27
|
+
getPath(x: number, y: number, fontSize: number): {
|
|
28
|
+
toPathData(decimals?: number): string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface ContourFont {
|
|
32
|
+
unitsPerEm: number;
|
|
33
|
+
ascender: number;
|
|
34
|
+
charToGlyph(char: string): ContourGlyph;
|
|
35
|
+
getKerningValue(left: ContourGlyph, right: ContourGlyph): number;
|
|
36
|
+
}
|
|
37
|
+
export interface ContourPathOptions {
|
|
38
|
+
/** path-data decimal places (default 2). */
|
|
39
|
+
decimals?: number;
|
|
40
|
+
/** padding around the bounding viewBox so wide ring strokes don't clip (px, default 12). */
|
|
41
|
+
pad?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ContourPath {
|
|
44
|
+
text: string;
|
|
45
|
+
fontSize: number;
|
|
46
|
+
viewBox: string;
|
|
47
|
+
d: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Lay `text` out in `font` at `fontSize` and return the combined glyph-outline path data plus a
|
|
51
|
+
* padded viewBox. Pure — runs in the browser or node identically.
|
|
52
|
+
*/
|
|
53
|
+
export declare function contourPathData(font: ContourFont, text: string, fontSize: number, opts?: ContourPathOptions): ContourPath;
|
|
54
|
+
export interface ContourSvgOptions extends ContourPathOptions {
|
|
55
|
+
/** text to outline — defaults to the element's textContent, trimmed. */
|
|
56
|
+
text?: string;
|
|
57
|
+
/** layout size in px — defaults to the element's computed font-size, else 96. */
|
|
58
|
+
fontSize?: number;
|
|
59
|
+
/** how many stacked-stroke rings to emit (default 3, classed ring-1…ring-N, innermost first). */
|
|
60
|
+
rings?: number;
|
|
61
|
+
/** insert the SVG into the DOM after the element (default true); false returns it unattached. */
|
|
62
|
+
attach?: boolean;
|
|
63
|
+
/** document override (tests / detached trees); defaults to the element's ownerDocument. */
|
|
64
|
+
doc?: Document;
|
|
65
|
+
}
|
|
66
|
+
export interface ContourSvgHandle {
|
|
67
|
+
svg: SVGSVGElement;
|
|
68
|
+
path: ContourPath;
|
|
69
|
+
/** remove the SVG from the DOM. */
|
|
70
|
+
remove(): void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate the bound vector representation for a body element from ITS OWN font: an `aria-hidden`
|
|
74
|
+
* SVG of stacked-stroke contour rings, carrying `data-field-visual-for` back to the element so the
|
|
75
|
+
* platform's state mirroring (Bound Visual Sink) drives it from the body's live `--d` / `--load`.
|
|
76
|
+
* The element keeps its semantic text; if it has no id one is assigned (the binding needs a ref).
|
|
77
|
+
*/
|
|
78
|
+
export declare function contourSvgFor(el: HTMLElement, font: ContourFont, opts?: ContourSvgOptions): ContourSvgHandle;
|
|
79
|
+
//# sourceMappingURL=contours.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contours.d.ts","sourceRoot":"","sources":["../src/contours.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;QAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CAC5F;AACD,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IACxC,eAAe,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC;CAClE;AAED,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,WAAW,CAqB7H;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IAC3D,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2FAA2F;IAC3F,GAAG,CAAC,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,aAAa,CAAC;IACnB,IAAI,EAAE,WAAW,CAAC;IAClB,mCAAmC;IACnC,MAAM,IAAI,IAAI,CAAC;CAChB;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,GAAE,iBAAsB,GAAG,gBAAgB,CA4BhH"}
|
package/dist/contours.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contour typography (#257, #363) — glyph-outline generation for ANY font the author applies to a
|
|
3
|
+
* body element. The Contour Sink tier of Body Matter Interaction: the element absorbs field matter,
|
|
4
|
+
* the generated vector layer shows what that absorption means, the semantic text remains the source
|
|
5
|
+
* of meaning.
|
|
6
|
+
*
|
|
7
|
+
* Fundamental ships no font parser (the zero-dependency rule): the caller supplies the parsed font —
|
|
8
|
+
* any object structurally matching `ContourFont`, which opentype.js's `Font` satisfies directly:
|
|
9
|
+
*
|
|
10
|
+
* import { load } from 'opentype.js';
|
|
11
|
+
* import { contourSvgFor } from '@fundamental-engine/dom';
|
|
12
|
+
* const font = await load('/fonts/your-font.woff'); // WHATEVER face the element uses
|
|
13
|
+
* const handle = contourSvgFor(document.querySelector('#hero-title'), font, { rings: 3 });
|
|
14
|
+
* // → an aria-hidden SVG bound via data-field-visual-for, inserted after the element;
|
|
15
|
+
* // the platform's state mirroring carries --d / --load onto it from the body.
|
|
16
|
+
*
|
|
17
|
+
* The same primitive runs in node for build-time generation (parse the font file, call
|
|
18
|
+
* `contourPathData`, commit the output) — the site's gen-contours script is that usage.
|
|
19
|
+
*
|
|
20
|
+
* Layout is per-glyph with pair kerning, no shaping: correct for Latin-script display text, the
|
|
21
|
+
* contour use case. Complex scripts (ligatures, contextual forms) need a real shaper and are out
|
|
22
|
+
* of scope here.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Lay `text` out in `font` at `fontSize` and return the combined glyph-outline path data plus a
|
|
26
|
+
* padded viewBox. Pure — runs in the browser or node identically.
|
|
27
|
+
*/
|
|
28
|
+
export function contourPathData(font, text, fontSize, opts = {}) {
|
|
29
|
+
const decimals = opts.decimals ?? 2;
|
|
30
|
+
const pad = opts.pad ?? 12;
|
|
31
|
+
const scale = fontSize / font.unitsPerEm;
|
|
32
|
+
const ascent = font.ascender * scale; // baseline at ascender height → bbox starts near y=0
|
|
33
|
+
const parts = [];
|
|
34
|
+
let x = 0;
|
|
35
|
+
let prev = null;
|
|
36
|
+
// track the bounds ourselves so we don't depend on a Path#getBoundingBox implementation —
|
|
37
|
+
// the em box is a safe, font-true envelope (exact ink bounds vary per glyph renderer).
|
|
38
|
+
for (const ch of text) {
|
|
39
|
+
const glyph = font.charToGlyph(ch);
|
|
40
|
+
if (prev)
|
|
41
|
+
x += font.getKerningValue(prev, glyph) * scale;
|
|
42
|
+
parts.push(glyph.getPath(x, ascent, fontSize).toPathData(decimals));
|
|
43
|
+
x += (glyph.advanceWidth ?? font.unitsPerEm / 2) * scale;
|
|
44
|
+
prev = glyph;
|
|
45
|
+
}
|
|
46
|
+
// the em box (height = fontSize, baseline at ascent) is the font-true envelope; exact ink
|
|
47
|
+
// bounds vary per glyph and aren't needed — pad absorbs over/undershoot.
|
|
48
|
+
const viewBox = `${(-pad).toFixed(1)} ${(-pad).toFixed(1)} ${(x + pad * 2).toFixed(1)} ${(fontSize + pad * 2).toFixed(1)}`;
|
|
49
|
+
return { text, fontSize, viewBox, d: parts.join('') };
|
|
50
|
+
}
|
|
51
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
52
|
+
/**
|
|
53
|
+
* Generate the bound vector representation for a body element from ITS OWN font: an `aria-hidden`
|
|
54
|
+
* SVG of stacked-stroke contour rings, carrying `data-field-visual-for` back to the element so the
|
|
55
|
+
* platform's state mirroring (Bound Visual Sink) drives it from the body's live `--d` / `--load`.
|
|
56
|
+
* The element keeps its semantic text; if it has no id one is assigned (the binding needs a ref).
|
|
57
|
+
*/
|
|
58
|
+
export function contourSvgFor(el, font, opts = {}) {
|
|
59
|
+
const doc = opts.doc ?? el.ownerDocument;
|
|
60
|
+
const text = opts.text ?? (el.textContent ?? '').trim();
|
|
61
|
+
let fontSize = opts.fontSize;
|
|
62
|
+
if (fontSize === undefined) {
|
|
63
|
+
const view = doc?.defaultView;
|
|
64
|
+
const computed = view?.getComputedStyle ? parseFloat(view.getComputedStyle(el).fontSize) : NaN;
|
|
65
|
+
fontSize = Number.isFinite(computed) && computed > 0 ? computed : 96;
|
|
66
|
+
}
|
|
67
|
+
const path = contourPathData(font, text, fontSize, opts);
|
|
68
|
+
if (!el.id)
|
|
69
|
+
el.id = `contour-${Math.abs(text.split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 7))}`;
|
|
70
|
+
const svg = doc.createElementNS(SVG_NS, 'svg');
|
|
71
|
+
svg.setAttribute('viewBox', path.viewBox);
|
|
72
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
73
|
+
svg.setAttribute('focusable', 'false');
|
|
74
|
+
svg.setAttribute('data-field-visual-for', el.id);
|
|
75
|
+
svg.setAttribute('data-field-visual-role', 'representation');
|
|
76
|
+
const rings = Math.max(1, opts.rings ?? 3);
|
|
77
|
+
for (let i = rings; i >= 1; i--) {
|
|
78
|
+
const p = doc.createElementNS(SVG_NS, 'path');
|
|
79
|
+
p.setAttribute('class', `ring ring-${i}`);
|
|
80
|
+
p.setAttribute('d', path.d);
|
|
81
|
+
p.setAttribute('fill', 'none');
|
|
82
|
+
svg.appendChild(p);
|
|
83
|
+
}
|
|
84
|
+
if (opts.attach !== false)
|
|
85
|
+
el.insertAdjacentElement('afterend', svg);
|
|
86
|
+
return { svg, path, remove: () => svg.remove() };
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=contours.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contours.js","sourceRoot":"","sources":["../src/contours.ts"],"names":[],"mappings":"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"}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env — shared, SSR-safe environment probes for the platform layer.
|
|
3
|
+
*
|
|
4
|
+
* Two helpers that multiple platform modules need independently:
|
|
5
|
+
* - `prefersReducedMotion()` — true when the OS/browser signals reduced motion preference.
|
|
6
|
+
* - `pageHidden()` — true when the document visibility state is "hidden".
|
|
7
|
+
*
|
|
8
|
+
* Both are safe to call in a non-DOM environment (SSR, Node test runners, custom hosts): they
|
|
9
|
+
* default to `false` when `window` / `document` are absent, rather than throwing.
|
|
10
|
+
*
|
|
11
|
+
* **Overriding for tests:** call `setEnvOverrides({ reducedMotion, hidden })` before the code
|
|
12
|
+
* under test runs, then `clearEnvOverrides()` (or a second call with `{}`) afterward. The
|
|
13
|
+
* override object is shallow-merged — passing only `reducedMotion` leaves `hidden` live, and
|
|
14
|
+
* vice versa. This is the intended seam; tests no longer need to stub `globalThis.matchMedia` or
|
|
15
|
+
* `document.hidden` directly.
|
|
16
|
+
*/
|
|
17
|
+
interface EnvOverrides {
|
|
18
|
+
reducedMotion?: boolean;
|
|
19
|
+
hidden?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Override one or both env probes — for test use only. Shallow-merges with any existing
|
|
23
|
+
* overrides so callers can set just the field they need.
|
|
24
|
+
*/
|
|
25
|
+
export declare function setEnvOverrides(o: EnvOverrides): void;
|
|
26
|
+
/** Remove all env overrides, restoring the live DOM probe behaviour. */
|
|
27
|
+
export declare function clearEnvOverrides(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Returns `true` when `prefers-reduced-motion: reduce` is active.
|
|
30
|
+
* SSR-safe: returns `false` when `matchMedia` is not available.
|
|
31
|
+
*/
|
|
32
|
+
export declare function prefersReducedMotion(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Returns `true` when the document is currently hidden (background tab / minimised window).
|
|
35
|
+
* SSR-safe: returns `false` when `document` is not available.
|
|
36
|
+
*/
|
|
37
|
+
export declare function pageHidden(): boolean;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,UAAU,YAAY;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAID;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAErD;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAG9C;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAGpC"}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env — shared, SSR-safe environment probes for the platform layer.
|
|
3
|
+
*
|
|
4
|
+
* Two helpers that multiple platform modules need independently:
|
|
5
|
+
* - `prefersReducedMotion()` — true when the OS/browser signals reduced motion preference.
|
|
6
|
+
* - `pageHidden()` — true when the document visibility state is "hidden".
|
|
7
|
+
*
|
|
8
|
+
* Both are safe to call in a non-DOM environment (SSR, Node test runners, custom hosts): they
|
|
9
|
+
* default to `false` when `window` / `document` are absent, rather than throwing.
|
|
10
|
+
*
|
|
11
|
+
* **Overriding for tests:** call `setEnvOverrides({ reducedMotion, hidden })` before the code
|
|
12
|
+
* under test runs, then `clearEnvOverrides()` (or a second call with `{}`) afterward. The
|
|
13
|
+
* override object is shallow-merged — passing only `reducedMotion` leaves `hidden` live, and
|
|
14
|
+
* vice versa. This is the intended seam; tests no longer need to stub `globalThis.matchMedia` or
|
|
15
|
+
* `document.hidden` directly.
|
|
16
|
+
*/
|
|
17
|
+
let _overrides = {};
|
|
18
|
+
/**
|
|
19
|
+
* Override one or both env probes — for test use only. Shallow-merges with any existing
|
|
20
|
+
* overrides so callers can set just the field they need.
|
|
21
|
+
*/
|
|
22
|
+
export function setEnvOverrides(o) {
|
|
23
|
+
_overrides = { ..._overrides, ...o };
|
|
24
|
+
}
|
|
25
|
+
/** Remove all env overrides, restoring the live DOM probe behaviour. */
|
|
26
|
+
export function clearEnvOverrides() {
|
|
27
|
+
_overrides = {};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns `true` when `prefers-reduced-motion: reduce` is active.
|
|
31
|
+
* SSR-safe: returns `false` when `matchMedia` is not available.
|
|
32
|
+
*/
|
|
33
|
+
export function prefersReducedMotion() {
|
|
34
|
+
if (_overrides.reducedMotion !== undefined)
|
|
35
|
+
return _overrides.reducedMotion;
|
|
36
|
+
return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns `true` when the document is currently hidden (background tab / minimised window).
|
|
40
|
+
* SSR-safe: returns `false` when `document` is not available.
|
|
41
|
+
*/
|
|
42
|
+
export function pageHidden() {
|
|
43
|
+
if (_overrides.hidden !== undefined)
|
|
44
|
+
return _overrides.hidden;
|
|
45
|
+
return typeof document !== 'undefined' && document.hidden;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=env.js.map
|
package/dist/env.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,IAAI,UAAU,GAAiB,EAAE,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,CAAe;IAC7C,UAAU,GAAG,EAAE,GAAG,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC;AACvC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,iBAAiB;IAC/B,UAAU,GAAG,EAAE,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,IAAI,UAAU,CAAC,aAAa,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,aAAa,CAAC;IAC5E,OAAO,OAAO,UAAU,KAAK,WAAW,IAAI,UAAU,CAAC,kCAAkC,CAAC,CAAC,OAAO,CAAC;AACrG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU;IACxB,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,MAAM,CAAC;IAC9D,OAAO,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Trigger a browser download of a data/blob URL. */
|
|
2
|
+
export declare function downloadUrl(url: string, filename: string): void;
|
|
3
|
+
/** Download arbitrary text (e.g. an SVG document from `segmentsToSvg`) as a file. */
|
|
4
|
+
export declare function downloadText(text: string, filename: string, mime?: string): void;
|
|
5
|
+
/** Download a canvas as a PNG file (`canvasToPng` + a download). */
|
|
6
|
+
export declare function downloadCanvasPng(canvas: HTMLCanvasElement, filename?: string): void;
|
|
7
|
+
//# sourceMappingURL=export-dom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-dom.d.ts","sourceRoot":"","sources":["../src/export-dom.ts"],"names":[],"mappings":"AAQA,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQ/D;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAkB,GAAG,IAAI,CAIzF;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,QAAQ,SAAc,GAAG,IAAI,CAEzF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM download helpers (Phase: frontier). Triggering a file download needs `document` (an anchor
|
|
3
|
+
* click), so these live in `@fundamental-engine/dom`, not `Fundamental`. The pure serializers stay in
|
|
4
|
+
* core: `segmentsToSvg` (vector) and `canvasToPng` (a canvas's own `toDataURL`). Pair them here to
|
|
5
|
+
* actually save a file.
|
|
6
|
+
*/
|
|
7
|
+
import { canvasToPng } from '@fundamental-engine/core';
|
|
8
|
+
/** Trigger a browser download of a data/blob URL. */
|
|
9
|
+
export function downloadUrl(url, filename) {
|
|
10
|
+
const a = document.createElement('a');
|
|
11
|
+
a.href = url;
|
|
12
|
+
a.download = filename;
|
|
13
|
+
a.rel = 'noopener';
|
|
14
|
+
document.body.appendChild(a);
|
|
15
|
+
a.click();
|
|
16
|
+
a.remove();
|
|
17
|
+
}
|
|
18
|
+
/** Download arbitrary text (e.g. an SVG document from `segmentsToSvg`) as a file. */
|
|
19
|
+
export function downloadText(text, filename, mime = 'image/svg+xml') {
|
|
20
|
+
const url = URL.createObjectURL(new Blob([text], { type: mime }));
|
|
21
|
+
downloadUrl(url, filename);
|
|
22
|
+
URL.revokeObjectURL(url);
|
|
23
|
+
}
|
|
24
|
+
/** Download a canvas as a PNG file (`canvasToPng` + a download). */
|
|
25
|
+
export function downloadCanvasPng(canvas, filename = 'field.png') {
|
|
26
|
+
downloadUrl(canvasToPng(canvas), filename);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=export-dom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-dom.js","sourceRoot":"","sources":["../src/export-dom.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,qDAAqD;AACrD,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,QAAgB;IACvD,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACtC,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC;IACb,CAAC,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACtB,CAAC,CAAC,GAAG,GAAG,UAAU,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC,KAAK,EAAE,CAAC;IACV,CAAC,CAAC,MAAM,EAAE,CAAC;AACb,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,QAAgB,EAAE,IAAI,GAAG,eAAe;IACjF,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAClE,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC3B,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,iBAAiB,CAAC,MAAyB,EAAE,QAAQ,GAAG,WAAW;IACjF,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { StateRegistry } from './state.ts';
|
|
2
|
+
/** Map of state-key → CSS-var name written for an element. */
|
|
3
|
+
type VarBinding = Record<string, string>;
|
|
4
|
+
export interface ThresholdOptions {
|
|
5
|
+
/** the state key whose value crosses the threshold. */
|
|
6
|
+
metric: string;
|
|
7
|
+
enter: number;
|
|
8
|
+
exit: number;
|
|
9
|
+
debounce?: number;
|
|
10
|
+
/** event dispatched on the exit edge (e.g. `field:dim` to pair with `field:lit`). */
|
|
11
|
+
exitEvent?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class FeedbackRegistry {
|
|
14
|
+
private readonly bindings;
|
|
15
|
+
private readonly direct;
|
|
16
|
+
private readonly thresholds;
|
|
17
|
+
private _cssWritesLastFrame;
|
|
18
|
+
/** Declare which state keys map to which CSS vars on an element (e.g. `{ density: '--field-density' }`). */
|
|
19
|
+
bind(element: Element, map: VarBinding): void;
|
|
20
|
+
/**
|
|
21
|
+
* Remove the CSS var bound to `key` on `element` from the DOM. Use
|
|
22
|
+
* when a metric becomes absent — e.g. the host stops supplying `data-field-confidence` — so a value
|
|
23
|
+
* written on an earlier `flush()` doesn't linger. A no-op when nothing is bound for `key`; the
|
|
24
|
+
* binding itself is left intact, so the var is rewritten if the metric returns. `flush()` already
|
|
25
|
+
* skips keys with no state, so this only clears the previously written inline value.
|
|
26
|
+
*/
|
|
27
|
+
clearVar(element: Element, key: string): void;
|
|
28
|
+
/** The declared bindings (element → the CSS-var names it writes), for lint / inspection. */
|
|
29
|
+
boundVars(): Array<{
|
|
30
|
+
element: Element;
|
|
31
|
+
vars: string[];
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Actual `style.setProperty` calls made during the last `flush()`. Use this (not
|
|
35
|
+
* `boundVars().length`) to measure real per-frame DOM write cost: off-screen elements with
|
|
36
|
+
* active bindings still generate mutations even though they produce no visible change.
|
|
37
|
+
*/
|
|
38
|
+
cssWritesLastFrame(): number;
|
|
39
|
+
/** Queue a direct CSS-var write (applied on the next `flush`). */
|
|
40
|
+
set(element: Element, vars: Record<string, number | string>): void;
|
|
41
|
+
/** Register a thresholded, debounced event for an element metric (hysteresis via enter/exit). */
|
|
42
|
+
threshold(element: Element, eventName: string, opts: ThresholdOptions): void;
|
|
43
|
+
/**
|
|
44
|
+
* Drop ALL bindings and thresholds registered for one element. Use when an element is removed from
|
|
45
|
+
* the DOM and you want immediate reclamation rather than waiting for the next flush() sweep.
|
|
46
|
+
*/
|
|
47
|
+
unregister(element: Element): void;
|
|
48
|
+
/**
|
|
49
|
+
* Write-phase: apply bound state → CSS vars, apply queued direct writes, and run thresholders →
|
|
50
|
+
* fire edge events. `state` supplies the numeric values for bound vars + thresholds. Disconnected
|
|
51
|
+
* elements are pruned here — the natural per-frame moment — so bindings and thresholds for removed
|
|
52
|
+
* elements never accumulate across the lifetime of the registry.
|
|
53
|
+
*/
|
|
54
|
+
flush(state: StateRegistry, now?: number): void;
|
|
55
|
+
}
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=feedback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../src/feedback.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,8DAA8D;AAC9D,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA6BzC,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,mBAAmB,CAAK;IAEhC,4GAA4G;IAC5G,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI;IAI7C;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAK7C,4FAA4F;IAC5F,SAAS,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAIxD;;;;OAIG;IACH,kBAAkB,IAAI,MAAM;IAI5B,kEAAkE;IAClE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI;IAMlE,iGAAiG;IACjG,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAU5E;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IASlC;;;;;OAKG;IACH,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,SAAI,GAAG,IAAI;CA0B3C"}
|