@fusion.dev/helix-core 0.198.0 → 0.199.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.
@@ -3717,7 +3717,7 @@
3717
3717
  },
3718
3718
  {
3719
3719
  "kind": "class",
3720
- "description": "Runs a canvas painter's frame loop, and stops it whenever nobody is looking.\n\nIt holds the five pieces every animated canvas needs and none of which is\nabout what is drawn: an `IntersectionObserver` for whether the host is on\nscreen, `visibilitychange` for whether the tab is in front,\n`prefers-reduced-motion` and its `change` listener for whether the reader\nasked for less, a `ResizeObserver` for the box, and the\n`requestAnimationFrame` loop that reports a clamped step. It says whether it\nis running, whether the reader asked for less motion, and whether the host is\non screen.\n\n**Separable because none of it reads a pixel.** `Aura` and `Orb` are the\nlibrary's two canvas painters and are otherwise nothing alike, one a WebGL\nshader over a full-screen triangle and the other a depth-sorted cloud of 2D\ndots. Everything above is the same question in both, which is when a frame is\nworth drawing, and the answer never mentions the drawing.\n\n**Every path to a frame is gated, not only the observer's.** The tab's own\nstate is asked in `play` rather than in the intersection callback, because a\nrestored context, a resize, a state change and a preference all play too, and\na check that lives in the observer covers exactly one of them.\n\n**The next frame is asked for rather than booked blind.** A loop that books\nits own next frame unconditionally ends only where something calls `stop`,\nwhich is how a detached element and a host whose context was taken both went\non drawing into nothing.\n\n**The device-pixel cap is not here.** The aura scales by\n`Math.min(devicePixelRatio || 1, 2) * 0.85` and the orb by\n`Math.min(2, devicePixelRatio || 1)`, and the difference between them is a\nmeasured fill cost, five octaves of noise per pixel against a disc fill.\nThose are decisions about a drawing, not the mechanism around it.\n\n**Nor is context loss.** The event names differ by context type,\n`webglcontextlost` against `contextlost`, and so does the contract: a 2D\ncontext comes back unless the loss is canceled and a WebGL one comes back\nonly if it is. The aura's deferred release is an argument about a browser's\nbudget of about sixteen live WebGL contexts, which is nothing a 2D canvas\nhas.\n\nClient only, and by contract rather than by a guard: every caller holds a\ndrawing context before it calls `watch`, and there is no context on a server\nor on a machine that refuses one.",
3720
+ "description": "Runs a canvas painter's frame loop, and stops it whenever nobody is looking.\n\nIt holds the five pieces every animated canvas needs and none of which is\nabout what is drawn: an `IntersectionObserver` for whether the host is on\nscreen, `visibilitychange` for whether the tab is in front,\n`prefers-reduced-motion` and its `change` listener for whether the reader\nasked for less, a `ResizeObserver` for the box, and the\n`requestAnimationFrame` loop that reports a clamped step. It says whether it\nis running, whether the reader asked for less motion, and whether the host is\non screen.\n\n**Separable because none of it reads a pixel.** `Aura` and `Orb` are the\nlibrary's two canvas painters and are otherwise nothing alike, one a WebGL\nshader over a full-screen triangle and the other a depth-sorted cloud of 2D\ndots. Everything above is the same question in both, which is when a frame is\nworth drawing, and the answer never mentions the drawing.\n\n**The page's pause stops it too.** A page that pauses its motion, through\n`pauseMotion` or `Motion Toggle`, holds every painter on the frame it was\nshowing, and lets them run again when it lets motion run.\n\n**Every path to a frame is gated, not only the observer's.** The tab's own\nstate is asked in `play` rather than in the intersection callback, because a\nrestored context, a resize, a state change and a preference all play too, and\na check that lives in the observer covers exactly one of them.\n\n**The next frame is asked for rather than booked blind.** A loop that books\nits own next frame unconditionally ends only where something calls `stop`,\nwhich is how a detached element and a host whose context was taken both went\non drawing into nothing.\n\n**The device-pixel cap is not here.** The aura scales by\n`Math.min(devicePixelRatio || 1, 2) * 0.85` and the orb by\n`Math.min(2, devicePixelRatio || 1)`, and the difference between them is a\nmeasured fill cost, five octaves of noise per pixel against a disc fill.\nThose are decisions about a drawing, not the mechanism around it.\n\n**Nor is context loss.** The event names differ by context type,\n`webglcontextlost` against `contextlost`, and so does the contract: a 2D\ncontext comes back unless the loss is canceled and a WebGL one comes back\nonly if it is. The aura's deferred release is an argument about a browser's\nbudget of about sixteen live WebGL contexts, which is nothing a 2D canvas\nhas.\n\nClient only, and by contract rather than by a guard: every caller holds a\ndrawing context before it calls `watch`, and there is no context on a server\nor on a machine that refuses one.",
3721
3721
  "name": "PaintGateController",
3722
3722
  "members": [
3723
3723
  {
@@ -3786,6 +3786,15 @@
3786
3786
  },
3787
3787
  "privacy": "private"
3788
3788
  },
3789
+ {
3790
+ "kind": "field",
3791
+ "name": "unpaused",
3792
+ "type": {
3793
+ "text": "(() => void) | undefined"
3794
+ },
3795
+ "privacy": "private",
3796
+ "description": "What stops hearing the page's pause."
3797
+ },
3789
3798
  {
3790
3799
  "kind": "field",
3791
3800
  "name": "visible",
@@ -3883,6 +3892,13 @@
3883
3892
  "privacy": "private",
3884
3893
  "readonly": true
3885
3894
  },
3895
+ {
3896
+ "kind": "field",
3897
+ "name": "handlePause",
3898
+ "privacy": "private",
3899
+ "readonly": true,
3900
+ "description": "The page paused its motion, or let it run again."
3901
+ },
3886
3902
  {
3887
3903
  "kind": "field",
3888
3904
  "name": "handleVisibility",
@@ -18011,6 +18027,46 @@
18011
18027
  "module": "./utils/own-rule/own-rule.js"
18012
18028
  }
18013
18029
  },
18030
+ {
18031
+ "kind": "js",
18032
+ "name": "MOTION_ATTRIBUTE",
18033
+ "declaration": {
18034
+ "name": "MOTION_ATTRIBUTE",
18035
+ "module": "./utils/page-motion/page-motion.js"
18036
+ }
18037
+ },
18038
+ {
18039
+ "kind": "js",
18040
+ "name": "motionPaused",
18041
+ "declaration": {
18042
+ "name": "motionPaused",
18043
+ "module": "./utils/page-motion/page-motion.js"
18044
+ }
18045
+ },
18046
+ {
18047
+ "kind": "js",
18048
+ "name": "onMotionChange",
18049
+ "declaration": {
18050
+ "name": "onMotionChange",
18051
+ "module": "./utils/page-motion/page-motion.js"
18052
+ }
18053
+ },
18054
+ {
18055
+ "kind": "js",
18056
+ "name": "pauseMotion",
18057
+ "declaration": {
18058
+ "name": "pauseMotion",
18059
+ "module": "./utils/page-motion/page-motion.js"
18060
+ }
18061
+ },
18062
+ {
18063
+ "kind": "js",
18064
+ "name": "reducedMotion",
18065
+ "declaration": {
18066
+ "name": "reducedMotion",
18067
+ "module": "./utils/page-motion/page-motion.js"
18068
+ }
18069
+ },
18014
18070
  {
18015
18071
  "kind": "js",
18016
18072
  "name": "plainClick",
@@ -31824,6 +31880,117 @@
31824
31880
  }
31825
31881
  ]
31826
31882
  },
31883
+ {
31884
+ "kind": "javascript-module",
31885
+ "path": "src/utils/page-motion/page-motion.ts",
31886
+ "declarations": [
31887
+ {
31888
+ "kind": "variable",
31889
+ "name": "MOTION_ATTRIBUTE",
31890
+ "type": {
31891
+ "text": "string"
31892
+ },
31893
+ "default": "'data-helix-motion'",
31894
+ "description": "The attribute on the document element that holds the page's choice."
31895
+ },
31896
+ {
31897
+ "kind": "function",
31898
+ "name": "motionPaused",
31899
+ "return": {
31900
+ "type": {
31901
+ "text": "boolean"
31902
+ }
31903
+ },
31904
+ "description": "Whether the page has paused the motion that starts by itself. False on a server, where nothing moves."
31905
+ },
31906
+ {
31907
+ "kind": "function",
31908
+ "name": "pauseMotion",
31909
+ "return": {
31910
+ "type": {
31911
+ "text": "void"
31912
+ }
31913
+ },
31914
+ "parameters": [
31915
+ {
31916
+ "name": "paused",
31917
+ "default": "true"
31918
+ }
31919
+ ],
31920
+ "description": "Pauses the motion that starts by itself across the page, or with `false` lets it run again."
31921
+ },
31922
+ {
31923
+ "kind": "function",
31924
+ "name": "reducedMotion",
31925
+ "return": {
31926
+ "type": {
31927
+ "text": "boolean"
31928
+ }
31929
+ },
31930
+ "description": "Whether the reader's system asks for movement to be kept to a minimum."
31931
+ },
31932
+ {
31933
+ "kind": "function",
31934
+ "name": "onMotionChange",
31935
+ "return": {
31936
+ "type": {
31937
+ "text": "(() => void)"
31938
+ }
31939
+ },
31940
+ "parameters": [
31941
+ {
31942
+ "name": "listener",
31943
+ "type": {
31944
+ "text": "(paused: boolean) => void"
31945
+ }
31946
+ }
31947
+ ],
31948
+ "description": "Calls `listener` with the page's choice whenever it changes, however it\nwas changed. Returns what stops listening. **One observer for the page**,\nshared by every listener, and none at all while nobody listens."
31949
+ }
31950
+ ],
31951
+ "exports": [
31952
+ {
31953
+ "kind": "js",
31954
+ "name": "MOTION_ATTRIBUTE",
31955
+ "declaration": {
31956
+ "name": "MOTION_ATTRIBUTE",
31957
+ "module": "src/utils/page-motion/page-motion.ts"
31958
+ }
31959
+ },
31960
+ {
31961
+ "kind": "js",
31962
+ "name": "motionPaused",
31963
+ "declaration": {
31964
+ "name": "motionPaused",
31965
+ "module": "src/utils/page-motion/page-motion.ts"
31966
+ }
31967
+ },
31968
+ {
31969
+ "kind": "js",
31970
+ "name": "pauseMotion",
31971
+ "declaration": {
31972
+ "name": "pauseMotion",
31973
+ "module": "src/utils/page-motion/page-motion.ts"
31974
+ }
31975
+ },
31976
+ {
31977
+ "kind": "js",
31978
+ "name": "reducedMotion",
31979
+ "declaration": {
31980
+ "name": "reducedMotion",
31981
+ "module": "src/utils/page-motion/page-motion.ts"
31982
+ }
31983
+ },
31984
+ {
31985
+ "kind": "js",
31986
+ "name": "onMotionChange",
31987
+ "declaration": {
31988
+ "name": "onMotionChange",
31989
+ "module": "src/utils/page-motion/page-motion.ts"
31990
+ }
31991
+ }
31992
+ ]
31993
+ },
31827
31994
  {
31828
31995
  "kind": "javascript-module",
31829
31996
  "path": "src/utils/plain-click/plain-click.ts",
@@ -32124,9 +32291,13 @@
32124
32291
  "type": {
32125
32292
  "text": "number"
32126
32293
  }
32294
+ },
32295
+ {
32296
+ "name": "deep",
32297
+ "default": "width"
32127
32298
  }
32128
32299
  ],
32129
- "description": "A note as a solid, standing on the floor at `x` and `y`, `width` cells\nacross its footprint and `height` cells tall, as the camera sees it.\n\n**Each face is drawn only where it turns toward the camera**, so a block\nshows its top and the two sides that face the viewer from any turn, and a\nplan seen from straight above shows tops alone."
32300
+ "description": "A note as a solid, standing on the floor at `x` and `y`, `width` cells\nacross its footprint and `height` cells tall, as the camera sees it. A\nblock or a pyramid is `deep` cells back, as wide as it is deep where that\nis not given; a cylinder is round.\n\n**Each face is drawn only where it turns toward the camera**, so a block\nshows its top and the two sides that face the viewer from any turn, and a\nplan seen from straight above shows tops alone."
32130
32301
  },
32131
32302
  {
32132
32303
  "kind": "function",
@@ -36359,7 +36530,7 @@
36359
36530
  "type": {
36360
36531
  "text": "string"
36361
36532
  },
36362
- "default": "'0.198.0'",
36533
+ "default": "'0.199.0'",
36363
36534
  "description": "The version of the package this file ships in.\n\n**Written by `fusion/scripts/version.mjs`** wherever it writes the package's\n`package.json`, and stamped back into the repository beside it after a\nrelease, so the two never disagree; a spec beside this holds them to it.\nNothing else in the running code knows which release it is."
36364
36535
  }
36365
36536
  ],
@@ -36995,6 +37166,6 @@
36995
37166
  ],
36996
37167
  "helix": {
36997
37168
  "package": "@fusion.dev/helix-core",
36998
- "version": "0.198.0"
37169
+ "version": "0.199.0"
36999
37170
  }
37000
37171
  }
@@ -58,6 +58,10 @@ export interface PaintGateOptions {
58
58
  * dots. Everything above is the same question in both, which is when a frame is
59
59
  * worth drawing, and the answer never mentions the drawing.
60
60
  *
61
+ * **The page's pause stops it too.** A page that pauses its motion, through
62
+ * `pauseMotion` or `Motion Toggle`, holds every painter on the frame it was
63
+ * showing, and lets them run again when it lets motion run.
64
+ *
61
65
  * **Every path to a frame is gated, not only the observer's.** The tab's own
62
66
  * state is asked in `play` rather than in the intersection callback, because a
63
67
  * restored context, a resize, a state change and a preference all play too, and
@@ -99,6 +103,8 @@ export declare class PaintGateController implements ReactiveController {
99
103
  private intersecting;
100
104
  private motion;
101
105
  private sized;
106
+ /** What stops hearing the page's pause. */
107
+ private unpaused;
102
108
  private visible;
103
109
  constructor(host: ReactiveControllerHost & HTMLElement, options: PaintGateOptions);
104
110
  /** Whether the host is on screen, as the intersection observer last said. */
@@ -131,5 +137,7 @@ export declare class PaintGateController implements ReactiveController {
131
137
  private wanted;
132
138
  private readonly tick;
133
139
  private readonly handleMotion;
140
+ /** The page paused its motion, or let it run again. */
141
+ private readonly handlePause;
134
142
  private readonly handleVisibility;
135
143
  }
@@ -1,3 +1,4 @@
1
+ import { motionPaused, onMotionChange } from '../../utils/page-motion/page-motion.js';
1
2
  export const MAX_FRAME_STEP = 0.05;
2
3
  const STILL_CLOCK = 0.016;
3
4
  export class PaintGateController {
@@ -22,6 +23,14 @@ export class PaintGateController {
22
23
  this.options.onMotion?.(this.reduced);
23
24
  this.play();
24
25
  };
26
+ this.handlePause = (paused) => {
27
+ if (paused) {
28
+ this.stop();
29
+ }
30
+ else {
31
+ this.play();
32
+ }
33
+ };
25
34
  this.handleVisibility = () => {
26
35
  if (document.hidden) {
27
36
  this.stop();
@@ -57,6 +66,7 @@ export class PaintGateController {
57
66
  });
58
67
  this.visible.observe(this.host);
59
68
  document.addEventListener('visibilitychange', this.handleVisibility);
69
+ this.unpaused ??= onMotionChange(this.handlePause);
60
70
  this.sized ??= new ResizeObserver(() => {
61
71
  this.options.onResize?.();
62
72
  });
@@ -72,6 +82,8 @@ export class PaintGateController {
72
82
  this.motion?.removeEventListener('change', this.handleMotion);
73
83
  this.motion = undefined;
74
84
  document.removeEventListener('visibilitychange', this.handleVisibility);
85
+ this.unpaused?.();
86
+ this.unpaused = undefined;
75
87
  }
76
88
  play() {
77
89
  if (this.frame || !this.wanted()) {
@@ -91,6 +103,7 @@ export class PaintGateController {
91
103
  return (this.host.isConnected &&
92
104
  this.intersecting &&
93
105
  !this.reduced &&
106
+ !motionPaused() &&
94
107
  !document.hidden &&
95
108
  (this.options.canRun?.() ?? true));
96
109
  }
package/dist/index.d.ts CHANGED
@@ -118,6 +118,7 @@ export { nearest } from './utils/nearest/nearest.js';
118
118
  export { formatNumber, formatPercent, parseNumber, readingText, } from './utils/number-format/number-format.js';
119
119
  export { onUnlessFalse } from './utils/on-unless-false/on-unless-false.js';
120
120
  export { applyOwnRule, type OwnRule } from './utils/own-rule/own-rule.js';
121
+ export { MOTION_ATTRIBUTE, motionPaused, onMotionChange, pauseMotion, reducedMotion, } from './utils/page-motion/page-motion.js';
121
122
  export { plainClick } from './utils/plain-click/plain-click.js';
122
123
  export { appleTouch, onAndroid, onApple, type Platform } from './utils/platform/platform.js';
123
124
  export { pressType, type RecentPress } from './utils/press-type/press-type.js';
package/dist/index.js CHANGED
@@ -118,6 +118,7 @@ export { nearest } from './utils/nearest/nearest.js';
118
118
  export { formatNumber, formatPercent, parseNumber, readingText, } from './utils/number-format/number-format.js';
119
119
  export { onUnlessFalse } from './utils/on-unless-false/on-unless-false.js';
120
120
  export { applyOwnRule } from './utils/own-rule/own-rule.js';
121
+ export { MOTION_ATTRIBUTE, motionPaused, onMotionChange, pauseMotion, reducedMotion, } from './utils/page-motion/page-motion.js';
121
122
  export { plainClick } from './utils/plain-click/plain-click.js';
122
123
  export { appleTouch, onAndroid, onApple } from './utils/platform/platform.js';
123
124
  export { pressType } from './utils/press-type/press-type.js';
@@ -13,7 +13,8 @@
13
13
  *
14
14
  * **A number that would cover another label is left out**, which thins an
15
15
  * axis seen at a slant, and an axis whose edge is seen end on is left out
16
- * whole. Every name is kept.
16
+ * whole. Every name is kept, and where two axes' labels meet, the axis with
17
+ * fewer keeps its own.
17
18
  *
18
19
  * Pure, and measured in the page's own units: the SVG drawing passes its
19
20
  * drawing's units, and three.js passes pixels.
@@ -1,5 +1,6 @@
1
+ const INK = 0.8;
1
2
  const overlaps = (one, other, pad) => Math.abs(one.x - other.x) < one.half.width + other.half.width + pad &&
2
- Math.abs(one.y - other.y) < one.half.height + other.half.height + pad;
3
+ Math.abs(one.y - other.y) < (one.half.height + other.half.height) * INK + pad;
3
4
  export const boxLabels = (seen, axes, sizes) => {
4
5
  const corners = [];
5
6
  for (const x of [-1, 1]) {
@@ -59,14 +60,22 @@ export const boxLabels = (seen, axes, sizes) => {
59
60
  let deepest = 0;
60
61
  for (const tick of axis.ticks) {
61
62
  const box = size(tick.text, false);
62
- ticks.push({ ...off(seen(along(tick.at)), out, box, sizes.gap), text: tick.text });
63
+ ticks.push({
64
+ ...off(seen(along(tick.at)), out, box, sizes.gap),
65
+ of: axis.ticks.length,
66
+ text: tick.text,
67
+ });
63
68
  deepest = Math.max(deepest, Math.abs(out.x) * box.width + Math.abs(out.y) * box.height);
64
69
  }
70
+ if (axis.name === '') {
71
+ continue;
72
+ }
65
73
  const box = size(axis.name, true);
74
+ const topped = axis.ticks.some((tick) => tick.at > 1 - 1e-6);
66
75
  names.push({
67
76
  ...(key === 'y'
68
77
  ?
69
- off(to, { x: 0, y: -1 }, box, sizes.gap)
78
+ off(to, { x: 0, y: -1 }, box, sizes.gap + (topped ? sizes.tick / 2 : 0))
70
79
  : off(seen(along(0)), out, box, sizes.gap * 2 + deepest)),
71
80
  text: axis.name,
72
81
  });
@@ -78,7 +87,7 @@ export const boxLabels = (seen, axes, sizes) => {
78
87
  x: one.x,
79
88
  y: one.y,
80
89
  }));
81
- for (const tick of ticks) {
90
+ for (const tick of [...ticks].sort((one, other) => one.of - other.of)) {
82
91
  if (kept.some((other) => overlaps(tick, other, sizes.gap / 2))) {
83
92
  continue;
84
93
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Whether the page has paused the motion that starts by itself: a
3
+ * carousel's rotation, an autoplaying video, an ambient drawing such as
4
+ * `Aura`, a graph settling or turning, a map's frames. **One switch for the
5
+ * page**, as WCAG 2.2.2 asks of motion that starts by itself and runs longer
6
+ * than five seconds, so a reader who stops it once has stopped all of it.
7
+ *
8
+ * **Kept on the document element**, as `data-helix-motion="paused"`, so a
9
+ * page can write it in its markup or set it from its own code as well as
10
+ * from `Motion Toggle`, and a stylesheet can select on it. Every component
11
+ * that moves by itself reads it, and hears it change.
12
+ *
13
+ * Separate from `prefers-reduced-motion`, which a component reads too: that
14
+ * asks for less movement everywhere and from the start, and this stops
15
+ * motion already running, on this page, when the reader asks.
16
+ */
17
+ /** The attribute on the document element that holds the page's choice. */
18
+ export declare const MOTION_ATTRIBUTE = "data-helix-motion";
19
+ /** Whether the page has paused the motion that starts by itself. False on a server, where nothing moves. */
20
+ export declare const motionPaused: () => boolean;
21
+ /** Pauses the motion that starts by itself across the page, or with `false` lets it run again. */
22
+ export declare const pauseMotion: (paused?: boolean) => void;
23
+ /** Whether the reader's system asks for movement to be kept to a minimum. */
24
+ export declare const reducedMotion: () => boolean;
25
+ /**
26
+ * Calls `listener` with the page's choice whenever it changes, however it
27
+ * was changed. Returns what stops listening. **One observer for the page**,
28
+ * shared by every listener, and none at all while nobody listens.
29
+ */
30
+ export declare const onMotionChange: (listener: (paused: boolean) => void) => (() => void);
@@ -0,0 +1,47 @@
1
+ export const MOTION_ATTRIBUTE = 'data-helix-motion';
2
+ export const motionPaused = () => typeof document !== 'undefined' &&
3
+ document.documentElement?.getAttribute(MOTION_ATTRIBUTE) === 'paused';
4
+ export const pauseMotion = (paused = true) => {
5
+ if (typeof document === 'undefined') {
6
+ return;
7
+ }
8
+ if (paused) {
9
+ document.documentElement.setAttribute(MOTION_ATTRIBUTE, 'paused');
10
+ }
11
+ else {
12
+ document.documentElement.removeAttribute(MOTION_ATTRIBUTE);
13
+ }
14
+ };
15
+ export const reducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
16
+ const listeners = new Set();
17
+ let watcher;
18
+ let last = false;
19
+ export const onMotionChange = (listener) => {
20
+ if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') {
21
+ return () => undefined;
22
+ }
23
+ listeners.add(listener);
24
+ if (!watcher) {
25
+ last = motionPaused();
26
+ watcher = new MutationObserver(() => {
27
+ const now = motionPaused();
28
+ if (now !== last) {
29
+ last = now;
30
+ for (const one of [...listeners]) {
31
+ one(now);
32
+ }
33
+ }
34
+ });
35
+ watcher.observe(document.documentElement, {
36
+ attributeFilter: [MOTION_ATTRIBUTE],
37
+ attributes: true,
38
+ });
39
+ }
40
+ return () => {
41
+ listeners.delete(listener);
42
+ if (listeners.size === 0) {
43
+ watcher?.disconnect();
44
+ watcher = undefined;
45
+ }
46
+ };
47
+ };
@@ -79,13 +79,15 @@ export interface Drawn {
79
79
  }
80
80
  /**
81
81
  * A note as a solid, standing on the floor at `x` and `y`, `width` cells
82
- * across its footprint and `height` cells tall, as the camera sees it.
82
+ * across its footprint and `height` cells tall, as the camera sees it. A
83
+ * block or a pyramid is `deep` cells back, as wide as it is deep where that
84
+ * is not given; a cylinder is round.
83
85
  *
84
86
  * **Each face is drawn only where it turns toward the camera**, so a block
85
87
  * shows its top and the two sides that face the viewer from any turn, and a
86
88
  * plan seen from straight above shows tops alone.
87
89
  */
88
- export declare const solid: (projection: Projection, kind: Solid, x: number, y: number, width: number, height: number) => Drawn;
90
+ export declare const solid: (projection: Projection, kind: Solid, x: number, y: number, width: number, height: number, deep?: number) => Drawn;
89
91
  /**
90
92
  * The order to paint notes in, back to front: by how near the camera each
91
93
  * stands on the floor, and the taller first where two stand level.
@@ -55,8 +55,9 @@ const facing = (normal, projection) => normal[0] * projection.toward[0] +
55
55
  normal[2] * projection.toward[2] >
56
56
  1e-6;
57
57
  const ARC = 12;
58
- export const solid = (projection, kind, x, y, width, height) => {
58
+ export const solid = (projection, kind, x, y, width, height, deep = width) => {
59
59
  const half = width / 2;
60
+ const back = deep / 2;
60
61
  const tall = Math.max(0, height);
61
62
  const on = (corner) => projection.at(corner[0], corner[1], corner[2]);
62
63
  if (kind === 'cylinder') {
@@ -82,10 +83,10 @@ export const solid = (projection, kind, x, y, width, height) => {
82
83
  };
83
84
  }
84
85
  const floor = [
85
- [x - half, y - half, 0],
86
- [x + half, y - half, 0],
87
- [x + half, y + half, 0],
88
- [x - half, y + half, 0],
86
+ [x - half, y - back, 0],
87
+ [x + half, y - back, 0],
88
+ [x + half, y + back, 0],
89
+ [x - half, y + back, 0],
89
90
  ];
90
91
  const outward = [
91
92
  [0, -1, 0],
@@ -97,13 +98,13 @@ export const solid = (projection, kind, x, y, width, height) => {
97
98
  let corners;
98
99
  let peak;
99
100
  if (kind === 'pyramid') {
100
- const apex = [x, y, Math.max(tall, width)];
101
+ const apex = [x, y, Math.max(tall, width, deep)];
101
102
  corners = [...floor, apex];
102
103
  peak = on(apex);
103
104
  floor.forEach((corner, at) => {
104
105
  const next = floor[(at + 1) % 4];
105
106
  const [ox, oy] = outward[at];
106
- const normal = [ox * apex[2], oy * apex[2], half];
107
+ const normal = [ox * apex[2], oy * apex[2], ox === 0 ? back : half];
107
108
  if (facing(normal, projection)) {
108
109
  faces.push({
109
110
  d: pathData([on(corner), on(next), on(apex)]),
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * release, so the two never disagree; a spec beside this holds them to it.
7
7
  * Nothing else in the running code knows which release it is.
8
8
  */
9
- export declare const VERSION = "0.198.0";
9
+ export declare const VERSION = "0.199.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.198.0';
1
+ export const VERSION = '0.199.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-core",
3
3
  "description": "Helix element primitives: base classes, mixins, decorators, utilities.",
4
- "version": "0.198.0",
4
+ "version": "0.199.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -74,12 +74,12 @@
74
74
  "bugs": "https://github.com/project-fusion/fusion/issues",
75
75
  "dependencies": {
76
76
  "@floating-ui/dom": "^1.8.0",
77
- "@fusion.dev/helix-icons": "0.198.0",
77
+ "@fusion.dev/helix-icons": "0.199.0",
78
78
  "tslib": "^2.8.1"
79
79
  },
80
80
  "devDependencies": {
81
- "@fusion.dev/helix-cem": "0.198.0",
82
- "@fusion.dev/helix-testing": "0.198.0",
81
+ "@fusion.dev/helix-cem": "0.199.0",
82
+ "@fusion.dev/helix-testing": "0.199.0",
83
83
  "@lit-labs/ssr": "^4.1.0",
84
84
  "lit": "^3.2.1"
85
85
  },