@lavalogic/scoria 0.40.13 → 0.40.15

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.
Files changed (143) hide show
  1. package/README.md +58 -58
  2. package/dist/Components/AccordionGroup.svelte +35 -46
  3. package/dist/Components/AccordionGroupButton.svelte +68 -76
  4. package/dist/Components/Action.svelte +103 -135
  5. package/dist/Components/AlertModal.svelte +171 -190
  6. package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +614 -697
  7. package/dist/Components/Base/BaseModal.svelte +90 -116
  8. package/dist/Components/Base/ContextWrapper.svelte +67 -72
  9. package/dist/Components/Base/Pagination.svelte +173 -196
  10. package/dist/Components/Base/Snippets.svelte +34 -36
  11. package/dist/Components/BigRadioSet.svelte +36 -38
  12. package/dist/Components/Bubble.svelte +78 -101
  13. package/dist/Components/Button.svelte +175 -208
  14. package/dist/Components/Checkbox.svelte +75 -86
  15. package/dist/Components/CollapsibleCard.svelte +72 -87
  16. package/dist/Components/CollapsibleMenuGroup.svelte +53 -57
  17. package/dist/Components/ConditionBuilder/ConditionBuilder.svelte +39 -64
  18. package/dist/Components/Contexts/ContextMenu.svelte +141 -156
  19. package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
  20. package/dist/Components/Contexts/ContextMenuOption.svelte +53 -60
  21. package/dist/Components/Contexts/Toast.svelte +81 -86
  22. package/dist/Components/Contexts/ToastContainer.svelte +58 -62
  23. package/dist/Components/Contexts/Tooltip.svelte +36 -37
  24. package/dist/Components/DataMatrixIcon.svelte +95 -108
  25. package/dist/Components/DateInput.svelte +118 -146
  26. package/dist/Components/DatePicker.svelte +255 -294
  27. package/dist/Components/DesktopModal.svelte +70 -90
  28. package/dist/Components/Dial.svelte +167 -197
  29. package/dist/Components/DimensionInput.svelte +124 -169
  30. package/dist/Components/DragMenu/DragMenuHolder.svelte +98 -114
  31. package/dist/Components/DragMenu/DraggableCard.svelte +64 -76
  32. package/dist/Components/FileCard.svelte +21 -24
  33. package/dist/Components/FileUpload.svelte +190 -225
  34. package/dist/Components/GridInput.svelte +159 -196
  35. package/dist/Components/GridInputSet.svelte +24 -29
  36. package/dist/Components/HorizontalTabGroup.svelte +73 -89
  37. package/dist/Components/HorizontalTabGroupButton.svelte +40 -44
  38. package/dist/Components/Icon.svelte +99 -128
  39. package/dist/Components/Icons.js +357 -357
  40. package/dist/Components/InfoAlert.svelte +35 -38
  41. package/dist/Components/LoadingAnimation.svelte +691 -755
  42. package/dist/Components/LoadingModal.svelte +36 -44
  43. package/dist/Components/LoadingOverlay.svelte +22 -26
  44. package/dist/Components/Modal.svelte +204 -254
  45. package/dist/Components/MultiSelect.svelte +186 -216
  46. package/dist/Components/Nav/Nav.svelte +94 -110
  47. package/dist/Components/Nav/NavServiceMenuList.svelte +57 -61
  48. package/dist/Components/Nav/NavTab.svelte +58 -62
  49. package/dist/Components/Nav/ServiceMenuItem.svelte +103 -111
  50. package/dist/Components/Nav/ServicesNav.svelte +72 -79
  51. package/dist/Components/NumberInput.svelte +121 -155
  52. package/dist/Components/OptionCards.svelte +62 -73
  53. package/dist/Components/PageHeading.svelte +23 -25
  54. package/dist/Components/PasswordInput.svelte +69 -88
  55. package/dist/Components/PopoverButton.svelte +151 -0
  56. package/dist/Components/PopoverButton.svelte.d.ts +9 -0
  57. package/dist/Components/PopoverButtonProps.d.ts +21 -0
  58. package/dist/Components/PopoverButtonProps.js +1 -0
  59. package/dist/Components/Portal.svelte +29 -35
  60. package/dist/Components/ProgressBubble.svelte +91 -106
  61. package/dist/Components/QuerySelect.svelte +260 -346
  62. package/dist/Components/SidebarPanel.svelte +34 -36
  63. package/dist/Components/SingleSelect.svelte +196 -244
  64. package/dist/Components/StepButton.svelte +65 -70
  65. package/dist/Components/StepForm.svelte +115 -135
  66. package/dist/Components/Switch.svelte +57 -69
  67. package/dist/Components/Table/ARCHITECTURE.md +298 -298
  68. package/dist/Components/Table/Body/QuickSearchCell.svelte +578 -655
  69. package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +42 -51
  70. package/dist/Components/Table/Body/Rows/ColumnList.svelte +42 -58
  71. package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +403 -442
  72. package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +51 -78
  73. package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +165 -184
  74. package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +110 -123
  75. package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +134 -160
  76. package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +190 -231
  77. package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +82 -104
  78. package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +52 -87
  79. package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +122 -150
  80. package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +132 -159
  81. package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +190 -236
  82. package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +173 -211
  83. package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +204 -244
  84. package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +140 -177
  85. package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +188 -217
  86. package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +173 -193
  87. package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +166 -187
  88. package/dist/Components/Table/Body/Rows/TableRow.svelte +245 -288
  89. package/dist/Components/Table/Body/TableBody.svelte +141 -154
  90. package/dist/Components/Table/Headers/HighlightAllHeader.svelte +35 -41
  91. package/dist/Components/Table/Headers/SelectAllHeader.svelte +24 -30
  92. package/dist/Components/Table/Headers/TableHead.svelte +348 -402
  93. package/dist/Components/Table/Misc/ColumnPanel.svelte +37 -39
  94. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +35 -39
  95. package/dist/Components/Table/Misc/FilterInput.svelte +556 -619
  96. package/dist/Components/Table/Misc/FilterPanel.svelte +170 -183
  97. package/dist/Components/Table/Misc/FilterPanelModal.svelte +25 -31
  98. package/dist/Components/Table/Misc/ShareViewModal.svelte +252 -281
  99. package/dist/Components/Table/Misc/SidePanel.svelte +32 -35
  100. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +574 -629
  101. package/dist/Components/Table/Misc/TableFooter.svelte +326 -332
  102. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +134 -140
  103. package/dist/Components/Table/Misc/TableSidebar.svelte +183 -198
  104. package/dist/Components/Table/Misc/TableViewDropdown.svelte +83 -103
  105. package/dist/Components/Table/Misc/ToolboxPanel.svelte +83 -100
  106. package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +88 -107
  107. package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +163 -190
  108. package/dist/Components/Table/NestedTable.svelte +102 -128
  109. package/dist/Components/Table/Table.svelte +531 -663
  110. package/dist/Components/Test/TestContextConsumer.svelte +11 -13
  111. package/dist/Components/TextArea.svelte +95 -130
  112. package/dist/Components/TextInput.svelte +125 -176
  113. package/dist/Components/ThreeStateRadio.svelte +111 -130
  114. package/dist/Components/Touch/BoolCard.svelte +33 -45
  115. package/dist/Components/Touch/DateModal.svelte +164 -204
  116. package/dist/Components/Touch/EditCard.svelte +48 -59
  117. package/dist/Components/Touch/InfoCard.svelte +28 -31
  118. package/dist/Components/Touch/InfoTextCard.svelte +26 -28
  119. package/dist/Components/Touch/ModalExplanation.svelte +15 -17
  120. package/dist/Components/Touch/NumberKeyboard.svelte +181 -218
  121. package/dist/Components/Touch/NumberModal.svelte +59 -79
  122. package/dist/Components/Touch/PlusMinusCard.svelte +43 -46
  123. package/dist/Components/Touch/SelectModal.svelte +111 -126
  124. package/dist/Components/Touch/SummaryCard.svelte +80 -93
  125. package/dist/Components/Touch/TextAreaModal.svelte +72 -94
  126. package/dist/Components/Touch/TextModal.svelte +77 -105
  127. package/dist/Components/Touch/TouchCard.svelte +31 -33
  128. package/dist/Components/Touch/TouchModal.svelte +214 -270
  129. package/dist/Components/Touch/UtilityButton.svelte +88 -109
  130. package/dist/Components/VerticalTabGroup.svelte +60 -78
  131. package/dist/Components/VerticalTabGroupButton.svelte +54 -58
  132. package/dist/Helpers/Datamatrix.d.ts +19 -19
  133. package/dist/Helpers/Datamatrix.js +291 -291
  134. package/dist/Types/Examples/ExampleModal.svelte +31 -35
  135. package/dist/index.d.ts +2 -0
  136. package/dist/index.js +1 -0
  137. package/dist/scss/_basics.scss +12 -12
  138. package/dist/scss/_colours.scss +134 -134
  139. package/dist/scss/_mixins.scss +3392 -3392
  140. package/dist/scss/_motion.scss +57 -57
  141. package/dist/scss/_sizing.scss +85 -85
  142. package/dist/scss/_transitions.scss +8 -8
  143. package/package.json +1 -1
@@ -1,787 +1,723 @@
1
- <svelte:options runes />
2
-
3
- <script
4
- lang="ts"
5
- module
6
- >
7
- /**
8
- * Warm, on-brand phrases printed inside the box's rear flap. One is chosen at
9
- * random when the loader mounts and a fresh one is shown on each loop. Pass
10
- * the `messages` prop to override this set.
11
- */
12
- export const defaultLoaderMessages: string[] = [
13
- 'You rock',
14
- 'Have a great day',
15
- 'Thanks for waiting',
16
- 'Almost there',
17
- 'Hang tight',
18
- 'Worth the wait',
19
- "You're a star",
20
- 'Nearly done',
21
- 'Sit back and relax',
22
- 'Good things come to those who wait',
23
- 'On its way',
24
- 'Making it happen',
25
- 'Fresh out the box',
26
- 'Hold tight, nearly there',
27
- ];
28
-
29
- // ---- geometry constants for the thrown-letter extrusion ----
30
- const MAXLW = 200; // max combined letter width, box-local px (long words shrink to fit)
31
- const DEPTH = 19; // extrusion layers behind each letter face
32
- const EVX = 0.22; // extrusion direction: across the box's resting angle
33
- const EVY = -0.24;
34
- const EVZ = -1.0;
35
- const NFLUTES = 4; // corrugation waves shaded across the thickness
36
-
37
- function shuffle(a: number[]): number[] {
38
- for (let i = a.length - 1; i > 0; i--) {
39
- const j = (Math.random() * (i + 1)) | 0;
40
- const t = a[i];
41
- a[i] = a[j];
42
- a[j] = t;
43
- }
44
- return a;
1
+ <svelte:options runes />
2
+
3
+ <script
4
+ lang="ts"
5
+ module
6
+ >/**
7
+ * Warm, on-brand phrases printed inside the box's rear flap. One is chosen at
8
+ * random when the loader mounts and a fresh one is shown on each loop. Pass
9
+ * the `messages` prop to override this set.
10
+ */
11
+ export const defaultLoaderMessages = [
12
+ "You rock",
13
+ "Have a great day",
14
+ "Thanks for waiting",
15
+ "Almost there",
16
+ "Hang tight",
17
+ "Worth the wait",
18
+ "You're a star",
19
+ "Nearly done",
20
+ "Sit back and relax",
21
+ "Good things come to those who wait",
22
+ "On its way",
23
+ "Making it happen",
24
+ "Fresh out the box",
25
+ "Hold tight, nearly there"
26
+ ];
27
+ // ---- geometry constants for the thrown-letter extrusion ----
28
+ const MAXLW = 200;
29
+ const DEPTH = 19;
30
+ const EVX = .22;
31
+ const EVY = -.24;
32
+ const EVZ = -1;
33
+ const NFLUTES = 4;
34
+ function shuffle(a) {
35
+ for (let i = a.length - 1; i > 0; i--) {
36
+ const j = Math.random() * (i + 1) | 0;
37
+ const t = a[i];
38
+ a[i] = a[j];
39
+ a[j] = t;
45
40
  }
46
- function rand(min: number, max: number): number {
47
- return min + Math.random() * (max - min);
48
- }
49
- function n1(v: number): number {
50
- return Math.round(v * 10) / 10;
51
- }
52
- function parseColor(c: string): [number, number, number] {
53
- c = (c || '').trim();
54
- if (c.charAt(0) === '#') {
55
- if (c.length === 4) {
56
- c = '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];
57
- }
58
- return [
59
- parseInt(c.substr(1, 2), 16),
60
- parseInt(c.substr(3, 2), 16),
61
- parseInt(c.substr(5, 2), 16),
62
- ];
63
- }
64
- const m = c.match(/(\d+)[,\s]+(\d+)[,\s]+(\d+)/);
65
- if (m) {
66
- return [+m[1], +m[2], +m[3]];
41
+ return a;
42
+ }
43
+ function rand(min, max) {
44
+ return min + Math.random() * (max - min);
45
+ }
46
+ function n1(v) {
47
+ return Math.round(v * 10) / 10;
48
+ }
49
+ function parseColor(c) {
50
+ c = (c || "").trim();
51
+ if (c.charAt(0) === "#") {
52
+ if (c.length === 4) {
53
+ c = "#" + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];
67
54
  }
68
- return [154, 160, 166];
55
+ return [
56
+ parseInt(c.substr(1, 2), 16),
57
+ parseInt(c.substr(3, 2), 16),
58
+ parseInt(c.substr(5, 2), 16)
59
+ ];
69
60
  }
70
- function mixRGB(
71
- a: [number, number, number],
72
- b: [number, number, number],
73
- t: number
74
- ): [number, number, number] {
75
- t = t < 0 ? 0 : t > 1 ? 1 : t;
61
+ const m = c.match(/(\d+)[,\s]+(\d+)[,\s]+(\d+)/);
62
+ if (m) {
76
63
  return [
77
- Math.round(a[0] + (b[0] - a[0]) * t),
78
- Math.round(a[1] + (b[1] - a[1]) * t),
79
- Math.round(a[2] + (b[2] - a[2]) * t),
64
+ +m[1],
65
+ +m[2],
66
+ +m[3]
80
67
  ];
81
68
  }
82
- function rgbStr(c: [number, number, number]): string {
83
- return 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')';
69
+ return [
70
+ 154,
71
+ 160,
72
+ 166
73
+ ];
74
+ }
75
+ function mixRGB(a, b, t) {
76
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
77
+ return [
78
+ Math.round(a[0] + (b[0] - a[0]) * t),
79
+ Math.round(a[1] + (b[1] - a[1]) * t),
80
+ Math.round(a[2] + (b[2] - a[2]) * t)
81
+ ];
82
+ }
83
+ function rgbStr(c) {
84
+ return "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")";
85
+ }
86
+ // Shade a single extrusion layer like stacked fluted cardboard: it darkens
87
+ // toward the back, with flute banding so the thickness reads as corrugated
88
+ // sheets — matching the box stylistically.
89
+ function fluteCol(k, faceRGB, deepRGB) {
90
+ const t = k / DEPTH;
91
+ const wave = .5 - .5 * Math.cos(t * Math.PI * 2 * NFLUTES);
92
+ const ridge = mixRGB(faceRGB, deepRGB, .46 + .42 * t);
93
+ const valley = mixRGB(faceRGB, deepRGB, .74 + .26 * t);
94
+ return rgbStr(mixRGB(ridge, valley, wave));
95
+ }
96
+ </script>
97
+
98
+ <script lang="ts">import { loaderDefaults } from "../Helpers/loaderDefaults.svelte.js";
99
+ const { name = "", showText = true, label = "Loading", caption = "Loading", messages = defaultLoaderMessages, letterFace = "#cdab73", letterDeep = "#2a1d0d" } = $props();
100
+ // The word the box spells: the caller's first name, or the product name as a
101
+ // friendly fallback. Block capitals tumble cleanly and dodge lowercase
102
+ // descenders clipping the baseline on landing.
103
+ // Explicit `name` prop wins; otherwise fall back to the app-wide default
104
+ // (set by the host so internally-rendered loaders also show the user), then
105
+ // finally the product name.
106
+ const word = $derived((name.trim() || loaderDefaults.name.trim() || "FloWMS").toUpperCase());
107
+ // Caption text with any trailing dots/ellipsis stripped, since the pill
108
+ // appends its own animated "..." — avoids "moment……" style doubling.
109
+ const captionText = $derived(caption.replace(/[\s.…]+$/, ""));
110
+ // Per-letter render data: each letter is a stack of DEPTH layers (painted
111
+ // back-to-front) topped by the lit face, so it has genuine 3D thickness.
112
+ const geom = $derived.by(() => {
113
+ const chars = word.split("");
114
+ const N = Math.max(chars.length, 1);
115
+ const spacing = Math.min(40, MAXLW / N);
116
+ const fontSize = Math.max(13, Math.min(34, spacing * .92));
117
+ const perStep = Math.max(.6, fontSize * .046);
118
+ const faceRGB = parseColor(letterFace);
119
+ const deepRGB = parseColor(letterDeep);
120
+ const letters = chars.map((ch) => {
121
+ const glyph = ch === " " ? "\xA0" : ch;
122
+ const layers = [];
123
+ for (let k = DEPTH; k >= 1; k--) {
124
+ layers.push({
125
+ transform: "translate(-50%,-50%) translate3d(" + n1(EVX * perStep * k) + "px," + n1(EVY * perStep * k) + "px," + n1(EVZ * perStep * k) + "px)",
126
+ color: fluteCol(k, faceRGB, deepRGB)
127
+ });
128
+ }
129
+ return {
130
+ glyph,
131
+ layers
132
+ };
133
+ });
134
+ return {
135
+ N,
136
+ spacing,
137
+ fontSize,
138
+ letters
139
+ };
140
+ });
141
+ // Confetti launch vectors — fixed once per instance so the burst is stable.
142
+ const fills = Array.from({ length: 16 }, () => ({
143
+ dx: rand(-50, 56),
144
+ dz: rand(-40, 40),
145
+ rise: rand(48, 90)
146
+ }));
147
+ let rootRef = $state();
148
+ let boxRef = $state();
149
+ let glowRef = $state();
150
+ let s0pivot = $state();
151
+ let s1pivot = $state();
152
+ let s2pivot = $state();
153
+ let s3pivot = $state();
154
+ let letterRefs = $state([]);
155
+ let fillRefs = $state([]);
156
+ let msgIndex = $state(0);
157
+ $effect(() => {
158
+ const rootEl = rootRef;
159
+ const boxEl = boxRef;
160
+ if (!rootEl || !boxEl) {
161
+ return;
84
162
  }
85
- // Shade a single extrusion layer like stacked fluted cardboard: it darkens
86
- // toward the back, with flute banding so the thickness reads as corrugated
87
- // sheets matching the box stylistically.
88
- function fluteCol(
89
- k: number,
90
- faceRGB: [number, number, number],
91
- deepRGB: [number, number, number]
92
- ): string {
93
- const t = k / DEPTH; // 0 = just behind the face, 1 = deepest
94
- const wave = 0.5 - 0.5 * Math.cos(t * Math.PI * 2 * NFLUTES); // 0 = ridge, 1 = valley
95
- const ridge = mixRGB(faceRGB, deepRGB, 0.46 + 0.42 * t);
96
- const valley = mixRGB(faceRGB, deepRGB, 0.74 + 0.26 * t);
97
- return rgbStr(mixRGB(ridge, valley, wave));
163
+ // Read reactive inputs so the timeline rebuilds when the word changes.
164
+ const { N, spacing } = geom;
165
+ const letterEls = letterRefs.slice(0, geom.letters.length).filter((x) => !!x);
166
+ const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
167
+ if (reduce) {
168
+ return;
98
169
  }
99
- </script>
100
-
101
- <script lang="ts">
102
- import { loaderDefaults } from '../Helpers/loaderDefaults.svelte.js';
103
- import type { LoadingAnimationProps } from './LoadingAnimationProps.js';
104
-
105
- const {
106
- name = '',
107
- showText = true,
108
- label = 'Loading',
109
- caption = 'Loading',
110
- messages = defaultLoaderMessages,
111
- letterFace = '#cdab73',
112
- letterDeep = '#2a1d0d',
113
- }: LoadingAnimationProps = $props();
114
-
115
- // The word the box spells: the caller's first name, or the product name as a
116
- // friendly fallback. Block capitals tumble cleanly and dodge lowercase
117
- // descenders clipping the baseline on landing.
118
- // Explicit `name` prop wins; otherwise fall back to the app-wide default
119
- // (set by the host so internally-rendered loaders also show the user), then
120
- // finally the product name.
121
- const word = $derived(
122
- (name.trim() || loaderDefaults.name.trim() || 'FloWMS').toUpperCase()
123
- );
124
-
125
- // Caption text with any trailing dots/ellipsis stripped, since the pill
126
- // appends its own animated "..." — avoids "moment……" style doubling.
127
- const captionText = $derived(caption.replace(/[\s.…]+$/, ''));
128
-
129
- // Per-letter render data: each letter is a stack of DEPTH layers (painted
130
- // back-to-front) topped by the lit face, so it has genuine 3D thickness.
131
- const geom = $derived.by(() => {
132
- const chars = word.split('');
133
- const N = Math.max(chars.length, 1);
134
- const spacing = Math.min(40, MAXLW / N);
135
- const fontSize = Math.max(13, Math.min(34, spacing * 0.92));
136
- const perStep = Math.max(0.6, fontSize * 0.046);
137
- const faceRGB = parseColor(letterFace);
138
- const deepRGB = parseColor(letterDeep);
139
- const letters = chars.map((ch) => {
140
- const glyph = ch === ' ' ? ' ' : ch;
141
- const layers: { transform: string; color: string }[] = [];
142
- for (let k = DEPTH; k >= 1; k--) {
143
- layers.push({
144
- transform:
145
- 'translate(-50%,-50%) translate3d(' +
146
- n1(EVX * perStep * k) +
147
- 'px,' +
148
- n1(EVY * perStep * k) +
149
- 'px,' +
150
- n1(EVZ * perStep * k) +
151
- 'px)',
152
- color: fluteCol(k, faceRGB, deepRGB),
153
- });
154
- }
155
- return { glyph, layers };
156
- });
157
- return { N, spacing, fontSize, letters };
158
- });
159
-
160
- // Confetti launch vectors — fixed once per instance so the burst is stable.
161
- const fills = Array.from({ length: 16 }, () => ({
162
- dx: rand(-50, 56),
163
- dz: rand(-40, 40),
164
- rise: rand(48, 90),
165
- }));
166
-
167
- let rootRef: HTMLDivElement | undefined = $state();
168
- let boxRef: HTMLDivElement | undefined = $state();
169
- let glowRef: HTMLDivElement | undefined = $state();
170
- let s0pivot: HTMLDivElement | undefined = $state();
171
- let s1pivot: HTMLDivElement | undefined = $state();
172
- let s2pivot: HTMLDivElement | undefined = $state();
173
- let s3pivot: HTMLDivElement | undefined = $state();
174
- let letterRefs: (HTMLSpanElement | undefined)[] = $state([]);
175
- let fillRefs: (HTMLSpanElement | undefined)[] = $state([]);
176
-
177
- let msgIndex = $state(0);
178
-
179
- $effect(() => {
180
- const rootEl = rootRef;
181
- const boxEl = boxRef;
182
- if (!rootEl || !boxEl) {
183
- return;
170
+ const cs = getComputedStyle(rootEl);
171
+ const hh = parseFloat(cs.getPropertyValue("--hh")) || 55;
172
+ const openDeg = parseFloat(cs.getPropertyValue("--open")) || 130;
173
+ const landY = 30;
174
+ const landZ = 112;
175
+ const anims = [];
176
+ const track = (a) => {
177
+ if (a) {
178
+ anims.push(a);
184
179
  }
185
-
186
- // Read reactive inputs so the timeline rebuilds when the word changes.
187
- const { N, spacing } = geom;
188
- const letterEls = letterRefs
189
- .slice(0, geom.letters.length)
190
- .filter((x): x is HTMLSpanElement => !!x);
191
-
192
- const reduce =
193
- window.matchMedia &&
194
- window.matchMedia('(prefers-reduced-motion: reduce)').matches;
195
- if (reduce) {
196
- return;
180
+ };
181
+ // ---- timeline (ms), scaled to the word length ----
182
+ const D_rotin = 700;
183
+ const outerOpen = [600, 1080];
184
+ const innerOpen = [1020, 1520];
185
+ const outStagger = 205;
186
+ const outFlight = 860;
187
+ const outStart = 1560;
188
+ const outEnd = outStart + (N - 1) * outStagger + outFlight;
189
+ const confStart = outEnd + 240;
190
+ const confDur = 1300;
191
+ const confEnd = confStart + confDur;
192
+ const backStagger = 120;
193
+ const backFlight = 520;
194
+ const backStart = confEnd + 200;
195
+ const backEnd = backStart + (N - 1) * backStagger + backFlight;
196
+ const closeStart = backEnd + 180;
197
+ const innerClose = [closeStart, closeStart + 460];
198
+ const outerClose = [closeStart + 360, closeStart + 880];
199
+ const spinStart = outerClose[1] + 60;
200
+ const spinEnd = spinStart + 980;
201
+ const T = spinEnd + 460;
202
+ const o = (t) => Math.max(0, Math.min(1, t / T));
203
+ const loop = {
204
+ duration: T,
205
+ iterations: Infinity,
206
+ easing: "linear"
207
+ };
208
+ // box: flatten -> tip to resting angle -> hold -> spin shut
209
+ const boxKF = [
210
+ {
211
+ offset: 0,
212
+ transform: "rotateX(0deg) rotateY(0deg)",
213
+ easing: "cubic-bezier(.5,0,.3,1)"
214
+ },
215
+ {
216
+ offset: o(D_rotin),
217
+ transform: "rotateX(-26deg) rotateY(-32deg)",
218
+ easing: "linear"
219
+ },
220
+ {
221
+ offset: o(spinStart),
222
+ transform: "rotateX(-26deg) rotateY(-32deg)",
223
+ easing: "cubic-bezier(.6,0,.3,1)"
224
+ },
225
+ {
226
+ offset: o(spinEnd),
227
+ transform: "rotateX(0deg) rotateY(-360deg)",
228
+ easing: "linear"
229
+ },
230
+ {
231
+ offset: 1,
232
+ transform: "rotateX(0deg) rotateY(-360deg)"
197
233
  }
198
-
199
- const cs = getComputedStyle(rootEl);
200
- const hh = parseFloat(cs.getPropertyValue('--hh')) || 55;
201
- const openDeg = parseFloat(cs.getPropertyValue('--open')) || 130;
202
- const landY = 30;
203
- const landZ = 112; // box-local: on the floor plane, out in front of the box
204
-
205
- const anims: Animation[] = [];
206
- const track = (a: Animation | null | undefined): void => {
207
- if (a) {
208
- anims.push(a);
209
- }
210
- };
211
-
212
- // ---- timeline (ms), scaled to the word length ----
213
- const D_rotin = 700;
214
- const outerOpen: [number, number] = [600, 1080];
215
- const innerOpen: [number, number] = [1020, 1520];
216
- const outStagger = 205;
217
- const outFlight = 860;
218
- const outStart = 1560;
219
- const outEnd = outStart + (N - 1) * outStagger + outFlight;
220
- const confStart = outEnd + 240;
221
- const confDur = 1300;
222
- const confEnd = confStart + confDur;
223
- const backStagger = 120;
224
- const backFlight = 520;
225
- const backStart = confEnd + 200;
226
- const backEnd = backStart + (N - 1) * backStagger + backFlight;
227
- const closeStart = backEnd + 180;
228
- const innerClose: [number, number] = [closeStart, closeStart + 460];
229
- const outerClose: [number, number] = [closeStart + 360, closeStart + 880];
230
- const spinStart = outerClose[1] + 60;
231
- const spinEnd = spinStart + 980;
232
- const T = spinEnd + 460;
233
-
234
- const o = (t: number): number => Math.max(0, Math.min(1, t / T));
235
- const loop: KeyframeAnimationOptions = {
236
- duration: T,
237
- iterations: Infinity,
238
- easing: 'linear',
239
- };
240
-
241
- // box: flatten -> tip to resting angle -> hold -> spin shut
242
- const boxKF: Keyframe[] = [
234
+ ];
235
+ track(boxEl.animate(boxKF, loop));
236
+ // flaps: open, hold, close
237
+ function flapKF(open, close) {
238
+ const ov = "rotateX(" + -openDeg + "deg)";
239
+ return [
243
240
  {
244
241
  offset: 0,
245
- transform: 'rotateX(0deg) rotateY(0deg)',
246
- easing: 'cubic-bezier(.5,0,.3,1)',
242
+ transform: "rotateX(0deg)"
247
243
  },
248
244
  {
249
- offset: o(D_rotin),
250
- transform: 'rotateX(-26deg) rotateY(-32deg)',
251
- easing: 'linear',
245
+ offset: o(open[0]),
246
+ transform: "rotateX(0deg)",
247
+ easing: "cubic-bezier(.4,0,.2,1)"
252
248
  },
253
249
  {
254
- offset: o(spinStart),
255
- transform: 'rotateX(-26deg) rotateY(-32deg)',
256
- easing: 'cubic-bezier(.6,0,.3,1)',
250
+ offset: o(open[1]),
251
+ transform: ov,
252
+ easing: "ease-in-out"
257
253
  },
258
254
  {
259
- offset: o(spinEnd),
260
- transform: 'rotateX(0deg) rotateY(-360deg)',
261
- easing: 'linear',
255
+ offset: o(close[0]),
256
+ transform: ov,
257
+ easing: "cubic-bezier(.4,0,.2,1)"
262
258
  },
263
- { offset: 1, transform: 'rotateX(0deg) rotateY(-360deg)' },
259
+ {
260
+ offset: o(close[1]),
261
+ transform: "rotateX(0deg)"
262
+ },
263
+ {
264
+ offset: 1,
265
+ transform: "rotateX(0deg)"
266
+ }
264
267
  ];
265
- track(boxEl.animate(boxKF, loop));
266
-
267
- // flaps: open, hold, close
268
- function flapKF(open: [number, number], close: [number, number]): Keyframe[] {
269
- const ov = 'rotateX(' + -openDeg + 'deg)';
270
- return [
271
- { offset: 0, transform: 'rotateX(0deg)' },
272
- {
273
- offset: o(open[0]),
274
- transform: 'rotateX(0deg)',
275
- easing: 'cubic-bezier(.4,0,.2,1)',
276
- },
277
- { offset: o(open[1]), transform: ov, easing: 'ease-in-out' },
278
- {
279
- offset: o(close[0]),
280
- transform: ov,
281
- easing: 'cubic-bezier(.4,0,.2,1)',
282
- },
283
- { offset: o(close[1]), transform: 'rotateX(0deg)' },
284
- { offset: 1, transform: 'rotateX(0deg)' },
285
- ];
268
+ }
269
+ [s0pivot, s2pivot].forEach((p) => {
270
+ if (p) {
271
+ track(p.animate(flapKF(outerOpen, outerClose), loop));
286
272
  }
287
- [s0pivot, s2pivot].forEach((p) => {
288
- if (p) {
289
- track(p.animate(flapKF(outerOpen, outerClose), loop));
290
- }
273
+ });
274
+ [s1pivot, s3pivot].forEach((p) => {
275
+ if (p) {
276
+ track(p.animate(flapKF(innerOpen, innerClose), loop));
277
+ }
278
+ });
279
+ // letters: random pop order out, eruption pause, random pop order back in.
280
+ // They live in their own stage drawn ABOVE the box, so they sail up out of
281
+ // the opening and land in front of everything — never behind or through the
282
+ // front flap.
283
+ const outOrder = shuffle(letterEls.map((_, i) => i));
284
+ const backOrder = shuffle(letterEls.map((_, i) => i));
285
+ const outSlot = [];
286
+ outOrder.forEach((idx, k) => outSlot[idx] = k);
287
+ const backSlot = [];
288
+ backOrder.forEach((idx, k) => backSlot[idx] = k);
289
+ letterEls.forEach((el, i) => {
290
+ const lx = (i - (N - 1) / 2) * spacing;
291
+ const ot = outStart + outSlot[i] * outStagger;
292
+ const bt = backStart + backSlot[i] * backStagger;
293
+ const f = outFlight;
294
+ const bf = backFlight;
295
+ const sgn = Math.random() < .5 ? -1 : 1;
296
+ const spin = sgn * rand(55, 120);
297
+ const wonky = sgn * rand(12, 28);
298
+ const base = "translate(-50%,-50%) ";
299
+ function pose(x, y, z, rz, sx, sy) {
300
+ return base + "translate3d(" + n1(x) + "px," + n1(y) + "px," + n1(z) + "px) rotateZ(" + n1(rz) + "deg) scale(" + n1(sx) + "," + n1(sy) + ")";
301
+ }
302
+ function smooth(a) {
303
+ a = a < 0 ? 0 : a > 1 ? 1 : a;
304
+ return a * a * (3 - 2 * a);
305
+ }
306
+ const pHide = pose(rand(-8, 8), -hh * .22, 0, 0, .12, .12);
307
+ const SAMP = 15;
308
+ // THROW OUT: a real projectile sampled at even time steps — fast launch
309
+ // out of the opening, a hang at the apex, then a gravity-driven drop that
310
+ // lands it on the floor in front of the box.
311
+ const oy0 = -hh - 4;
312
+ const oyA = -hh - 80;
313
+ const oy1 = landY;
314
+ const oRise = oy0 - oyA;
315
+ const oFall = oy1 - oyA;
316
+ const otA = Math.sqrt(oRise) / (Math.sqrt(oRise) + Math.sqrt(oFall));
317
+ const oC = oRise / (otA * otA);
318
+ const oZlo = 6;
319
+ const oZ0 = .3;
320
+ const oZ1 = .6;
321
+ const FLYo = .84;
322
+ const kf = [{
323
+ offset: 0,
324
+ transform: pHide,
325
+ opacity: 0
326
+ }, {
327
+ offset: o(ot),
328
+ transform: pose(lx * .03, oy0, oZlo, 0, .45, .45),
329
+ opacity: 0,
330
+ easing: "linear"
331
+ }];
332
+ for (let sI = 1; sI <= SAMP; sI++) {
333
+ const s = sI / SAMP;
334
+ const oy = oyA + oC * (s - otA) * (s - otA);
335
+ const ox = lx * (.03 + .97 * s);
336
+ const oz = oZlo + (landZ - oZlo) * smooth((s - oZ0) / (oZ1 - oZ0));
337
+ const orz = wonky * s + spin * Math.sin(Math.PI * s) * .4;
338
+ const osc = s < .1 ? .45 + 5.5 * s : 1;
339
+ kf.push({
340
+ offset: o(ot + f * FLYo * s),
341
+ transform: pose(ox, oy, oz, orz, osc, osc),
342
+ opacity: Math.min(1, s / .05),
343
+ easing: "linear"
344
+ });
345
+ }
346
+ kf.push({
347
+ offset: o(ot + f * .88),
348
+ transform: pose(lx, landY, landZ, wonky, 1.08, .84),
349
+ opacity: 1,
350
+ easing: "cubic-bezier(.2,.7,.4,1)"
291
351
  });
292
- [s1pivot, s3pivot].forEach((p) => {
293
- if (p) {
294
- track(p.animate(flapKF(innerOpen, innerClose), loop));
295
- }
352
+ kf.push({
353
+ offset: o(ot + f * .94),
354
+ transform: pose(lx, landY - 5, landZ, wonky * .35, .98, 1.05),
355
+ opacity: 1,
356
+ easing: "ease-out"
296
357
  });
297
-
298
- // letters: random pop order out, eruption pause, random pop order back in.
299
- // They live in their own stage drawn ABOVE the box, so they sail up out of
300
- // the opening and land in front of everything — never behind or through the
301
- // front flap.
302
- const outOrder = shuffle(letterEls.map((_, i) => i));
303
- const backOrder = shuffle(letterEls.map((_, i) => i));
304
- const outSlot: number[] = [];
305
- outOrder.forEach((idx, k) => (outSlot[idx] = k));
306
- const backSlot: number[] = [];
307
- backOrder.forEach((idx, k) => (backSlot[idx] = k));
308
-
309
- letterEls.forEach((el, i) => {
310
- const lx = (i - (N - 1) / 2) * spacing;
311
- const ot = outStart + outSlot[i] * outStagger;
312
- const bt = backStart + backSlot[i] * backStagger;
313
- const f = outFlight;
314
- const bf = backFlight;
315
- const sgn = Math.random() < 0.5 ? -1 : 1;
316
- const spin = sgn * rand(55, 120); // tumble through the air
317
- const wonky = sgn * rand(12, 28); // askew angle on landing, tips upright on settle
318
- const base = 'translate(-50%,-50%) ';
319
- function pose(
320
- x: number,
321
- y: number,
322
- z: number,
323
- rz: number,
324
- sx: number,
325
- sy: number
326
- ): string {
327
- return (
328
- base +
329
- 'translate3d(' +
330
- n1(x) +
331
- 'px,' +
332
- n1(y) +
333
- 'px,' +
334
- n1(z) +
335
- 'px) rotateZ(' +
336
- n1(rz) +
337
- 'deg) scale(' +
338
- n1(sx) +
339
- ',' +
340
- n1(sy) +
341
- ')'
342
- );
343
- }
344
- function smooth(a: number): number {
345
- a = a < 0 ? 0 : a > 1 ? 1 : a;
346
- return a * a * (3 - 2 * a);
347
- }
348
- const pHide = pose(rand(-8, 8), -hh * 0.22, 0, 0, 0.12, 0.12);
349
- const SAMP = 15;
350
-
351
- // THROW OUT: a real projectile sampled at even time steps — fast launch
352
- // out of the opening, a hang at the apex, then a gravity-driven drop that
353
- // lands it on the floor in front of the box.
354
- const oy0 = -hh - 4;
355
- const oyA = -hh - 80;
356
- const oy1 = landY;
357
- const oRise = oy0 - oyA;
358
- const oFall = oy1 - oyA;
359
- const otA = Math.sqrt(oRise) / (Math.sqrt(oRise) + Math.sqrt(oFall));
360
- const oC = oRise / (otA * otA);
361
- const oZlo = 6;
362
- const oZ0 = 0.3;
363
- const oZ1 = 0.6;
364
- const FLYo = 0.84;
365
-
366
- const kf: Keyframe[] = [
367
- { offset: 0, transform: pHide, opacity: 0 },
368
- {
369
- offset: o(ot),
370
- transform: pose(lx * 0.03, oy0, oZlo, 0, 0.45, 0.45),
371
- opacity: 0,
372
- easing: 'linear',
373
- },
374
- ];
375
- for (let sI = 1; sI <= SAMP; sI++) {
376
- const s = sI / SAMP;
377
- const oy = oyA + oC * (s - otA) * (s - otA);
378
- const ox = lx * (0.03 + 0.97 * s);
379
- const oz = oZlo + (landZ - oZlo) * smooth((s - oZ0) / (oZ1 - oZ0));
380
- const orz = wonky * s + spin * Math.sin(Math.PI * s) * 0.4;
381
- const osc = s < 0.1 ? 0.45 + 5.5 * s : 1;
382
- kf.push({
383
- offset: o(ot + f * FLYo * s),
384
- transform: pose(ox, oy, oz, orz, osc, osc),
385
- opacity: Math.min(1, s / 0.05),
386
- easing: 'linear',
387
- });
388
- }
389
- kf.push({
390
- offset: o(ot + f * 0.88),
391
- transform: pose(lx, landY, landZ, wonky, 1.08, 0.84),
392
- opacity: 1,
393
- easing: 'cubic-bezier(.2,.7,.4,1)',
394
- }); // contact + squash
395
- kf.push({
396
- offset: o(ot + f * 0.94),
397
- transform: pose(lx, landY - 5, landZ, wonky * 0.35, 0.98, 1.05),
398
- opacity: 1,
399
- easing: 'ease-out',
400
- }); // rebound
401
- kf.push({
402
- offset: o(ot + f),
403
- transform: pose(lx, landY, landZ, 0, 1, 1),
404
- opacity: 1,
405
- easing: 'linear',
406
- }); // settle
407
-
408
- // PULL BACK IN: the same physics reversed — up in front, hang, then drop
409
- // back down into the box.
410
- const by0 = landY;
411
- const byA = -hh - 70;
412
- const by1 = -hh * 0.18;
413
- const bRise = by0 - byA;
414
- const bFall = by1 - byA;
415
- const btA = Math.sqrt(bRise) / (Math.sqrt(bRise) + Math.sqrt(bFall));
416
- const bC = bRise / (btA * btA);
417
- const bZ0 = 0.4;
418
- const bZ1 = 0.72;
358
+ kf.push({
359
+ offset: o(ot + f),
360
+ transform: pose(lx, landY, landZ, 0, 1, 1),
361
+ opacity: 1,
362
+ easing: "linear"
363
+ });
364
+ // PULL BACK IN: the same physics reversed — up in front, hang, then drop
365
+ // back down into the box.
366
+ const by0 = landY;
367
+ const byA = -hh - 70;
368
+ const by1 = -hh * .18;
369
+ const bRise = by0 - byA;
370
+ const bFall = by1 - byA;
371
+ const btA = Math.sqrt(bRise) / (Math.sqrt(bRise) + Math.sqrt(bFall));
372
+ const bC = bRise / (btA * btA);
373
+ const bZ0 = .4;
374
+ const bZ1 = .72;
375
+ kf.push({
376
+ offset: o(bt),
377
+ transform: pose(lx, landY, landZ, 0, 1, 1),
378
+ opacity: 1,
379
+ easing: "linear"
380
+ });
381
+ for (let bI = 1; bI <= SAMP; bI++) {
382
+ const bs = bI / SAMP;
383
+ const by = byA + bC * (bs - btA) * (bs - btA);
384
+ const bx = lx * (1 - bs);
385
+ const bz = landZ - (landZ - 8) * smooth((bs - bZ0) / (bZ1 - bZ0));
386
+ const brz = -spin * Math.sin(Math.PI * bs) * .4;
387
+ const bsc = bs > .84 ? Math.max(.12, 1 - 5.5 * (bs - .84)) : 1;
388
+ const bop = Math.max(0, Math.min(1, 1 - (bs - .52) / .2));
419
389
  kf.push({
420
- offset: o(bt),
421
- transform: pose(lx, landY, landZ, 0, 1, 1),
422
- opacity: 1,
423
- easing: 'linear',
390
+ offset: o(bt + bf * bs),
391
+ transform: pose(bx, by, bz, brz, bsc, bsc),
392
+ opacity: bop,
393
+ easing: "linear"
424
394
  });
425
- for (let bI = 1; bI <= SAMP; bI++) {
426
- const bs = bI / SAMP;
427
- const by = byA + bC * (bs - btA) * (bs - btA);
428
- const bx = lx * (1 - bs);
429
- const bz = landZ - (landZ - 8) * smooth((bs - bZ0) / (bZ1 - bZ0));
430
- const brz = -spin * Math.sin(Math.PI * bs) * 0.4;
431
- const bsc = bs > 0.84 ? Math.max(0.12, 1 - 5.5 * (bs - 0.84)) : 1;
432
- const bop = Math.max(0, Math.min(1, 1 - (bs - 0.52) / 0.2));
433
- kf.push({
434
- offset: o(bt + bf * bs),
435
- transform: pose(bx, by, bz, brz, bsc, bsc),
436
- opacity: bop,
437
- easing: 'linear',
438
- });
439
- }
440
- kf.push({ offset: o(bt + bf), transform: pHide, opacity: 0 });
441
- kf.push({ offset: 1, transform: pHide, opacity: 0 });
442
- track(el.animate(kf, loop));
395
+ }
396
+ kf.push({
397
+ offset: o(bt + bf),
398
+ transform: pHide,
399
+ opacity: 0
443
400
  });
444
-
445
- // confetti eruption
446
- fills.forEach((p, i) => {
447
- const elf = fillRefs[i];
448
- if (!elf) {
449
- return;
450
- }
451
- const { dx, dz, rise } = p;
452
- track(
453
- elf.animate(
454
- [
455
- {
456
- offset: 0,
457
- transform: 'translate3d(0,0,0) scale(.3)',
458
- opacity: 0,
459
- },
460
- {
461
- offset: o(confStart),
462
- transform: 'translate3d(0,0,0) scale(.3)',
463
- opacity: 0,
464
- easing: 'cubic-bezier(.2,.6,.3,1)',
465
- },
466
- {
467
- offset: o(confStart + confDur * 0.16),
468
- transform:
469
- 'translate3d(' +
470
- n1(dx * 0.3) +
471
- 'px,-8px,' +
472
- n1(dz * 0.3) +
473
- 'px) scale(1)',
474
- opacity: 1,
475
- },
476
- {
477
- offset: o(confStart + confDur * 0.46),
478
- transform:
479
- 'translate3d(' +
480
- n1(dx) +
481
- 'px,' +
482
- n1(-rise) +
483
- 'px,' +
484
- n1(dz) +
485
- 'px) scale(1)',
486
- opacity: 1,
487
- easing: 'ease-in',
488
- },
489
- {
490
- offset: o(confStart + confDur * 0.95),
491
- transform:
492
- 'translate3d(' +
493
- n1(dx * 1.2) +
494
- 'px,26px,' +
495
- n1(dz * 1.2) +
496
- 'px) scale(.5)',
497
- opacity: 0,
498
- },
499
- {
500
- offset: 1,
501
- transform:
502
- 'translate3d(' +
503
- n1(dx * 1.2) +
504
- 'px,26px,' +
505
- n1(dz * 1.2) +
506
- 'px) scale(.5)',
507
- opacity: 0,
508
- },
509
- ],
510
- loop
511
- )
512
- );
401
+ kf.push({
402
+ offset: 1,
403
+ transform: pHide,
404
+ opacity: 0
513
405
  });
514
-
515
- if (glowRef) {
516
- track(
517
- glowRef.animate(
518
- [
519
- { offset: 0, transform: 'scale(.4)', opacity: 0 },
520
- {
521
- offset: o(confStart),
522
- transform: 'scale(.4)',
523
- opacity: 0,
524
- easing: 'ease-out',
525
- },
526
- {
527
- offset: o(confStart + confDur * 0.2),
528
- transform: 'scale(1)',
529
- opacity: 0.9,
530
- },
531
- {
532
- offset: o(confStart + confDur * 0.6),
533
- transform: 'scale(1.15)',
534
- opacity: 0.5,
535
- },
536
- {
537
- offset: o(confStart + confDur * 0.92),
538
- transform: 'scale(.6)',
539
- opacity: 0,
540
- },
541
- { offset: 1, transform: 'scale(.6)', opacity: 0 },
542
- ],
543
- loop
544
- )
545
- );
546
- }
547
-
548
- // rotate the rear-flap message once per loop
549
- let timer: ReturnType<typeof setInterval> | undefined;
550
- if (messages.length > 1) {
551
- msgIndex = (Math.random() * messages.length) | 0;
552
- timer = setInterval(() => {
553
- msgIndex = (msgIndex + 1) % messages.length;
554
- }, T);
555
- } else {
556
- msgIndex = 0;
406
+ track(el.animate(kf, loop));
407
+ });
408
+ // confetti eruption
409
+ fills.forEach((p, i) => {
410
+ const elf = fillRefs[i];
411
+ if (!elf) {
412
+ return;
557
413
  }
558
-
559
- return () => {
560
- anims.forEach((a) => a.cancel());
561
- if (timer) {
562
- clearInterval(timer);
414
+ const { dx, dz, rise } = p;
415
+ track(elf.animate([
416
+ {
417
+ offset: 0,
418
+ transform: "translate3d(0,0,0) scale(.3)",
419
+ opacity: 0
420
+ },
421
+ {
422
+ offset: o(confStart),
423
+ transform: "translate3d(0,0,0) scale(.3)",
424
+ opacity: 0,
425
+ easing: "cubic-bezier(.2,.6,.3,1)"
426
+ },
427
+ {
428
+ offset: o(confStart + confDur * .16),
429
+ transform: "translate3d(" + n1(dx * .3) + "px,-8px," + n1(dz * .3) + "px) scale(1)",
430
+ opacity: 1
431
+ },
432
+ {
433
+ offset: o(confStart + confDur * .46),
434
+ transform: "translate3d(" + n1(dx) + "px," + n1(-rise) + "px," + n1(dz) + "px) scale(1)",
435
+ opacity: 1,
436
+ easing: "ease-in"
437
+ },
438
+ {
439
+ offset: o(confStart + confDur * .95),
440
+ transform: "translate3d(" + n1(dx * 1.2) + "px,26px," + n1(dz * 1.2) + "px) scale(.5)",
441
+ opacity: 0
442
+ },
443
+ {
444
+ offset: 1,
445
+ transform: "translate3d(" + n1(dx * 1.2) + "px,26px," + n1(dz * 1.2) + "px) scale(.5)",
446
+ opacity: 0
563
447
  }
564
- };
448
+ ], loop));
565
449
  });
566
- </script>
567
-
568
- <div
569
- bind:this={rootRef}
570
- class="flowms-loader"
571
- role="status"
572
- aria-live="polite"
573
- aria-label={label}
574
- >
575
- <svg
576
- width="0"
577
- height="0"
578
- style="position:absolute"
579
- aria-hidden="true"
580
- >
581
- <filter
582
- id="flowms-stamp"
583
- x="-15%"
584
- y="-15%"
585
- width="130%"
586
- height="130%"
587
- >
588
- <feTurbulence
589
- type="fractalNoise"
590
- baseFrequency="0.03 0.045"
591
- numOctaves="3"
592
- seed="6"
593
- result="warp"
594
- />
595
- <feDisplacementMap
596
- in="SourceGraphic"
597
- in2="warp"
598
- scale="3.4"
599
- xChannelSelector="R"
600
- yChannelSelector="G"
601
- result="rough"
602
- />
603
- <feTurbulence
604
- type="fractalNoise"
605
- baseFrequency="0.5 0.55"
606
- numOctaves="2"
607
- seed="11"
608
- result="grain"
609
- />
610
- <feColorMatrix
611
- in="grain"
612
- type="matrix"
613
- values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.3 0 0 0 0.05"
614
- result="grainA"
615
- />
616
- <feComposite
617
- in="rough"
618
- in2="grainA"
619
- operator="in"
620
- />
621
- </filter>
622
- </svg>
623
-
624
- <div class="scene">
625
- <div class="shadow"></div>
626
- <div
627
- bind:this={boxRef}
628
- class="box"
629
- >
630
- <div class="face back"></div>
631
- <div class="face left">
632
- <svg
633
- class="flo-logo"
634
- viewBox="0 0 172 300"
635
- xmlns="http://www.w3.org/2000/svg"
636
- aria-hidden="true"
637
- >
638
- <path
639
- d="M1.54715 179.524C4.94675 176.764 11.6045 178.966 13.4335 180.132L13.44 180.125L29.0209 188.966L25.7231 191.604C13.44 200.508 18.7976 216.338 37.1425 217.942L54.0844 219.353L56.3929 219.471V268.062C56.3929 269.123 55.7069 270.574 54.5791 271.201L54.2886 271.366L5.44142 299.569C2.62507 300.967 0 298.711 0 296.429V185.886C0 183.535 0.318398 180.522 1.54715 179.524Z"
640
- />
641
- <path
642
- d="M62.3754 133.49C66.3328 131.155 71.4576 130.799 76.0086 133.424L111.784 154.082L128.952 144.169C130.963 143.008 133.536 144.01 134.228 146.226L153.092 206.478V206.498L153.099 206.517C153.547 208.845 151.674 210.976 149.306 210.818L56.3865 204.855V204.861L39.0593 203.747C35.4781 203.516 34.3703 198.78 37.4768 196.986L55.4036 186.632L19.0811 165.703L19.0746 165.71C17.3158 164.58 12.8616 162.913 7.58502 165.222L62.3754 133.477V133.49Z"
643
- />
644
- <path
645
- d="M19.2923 34.575C10.3024 29.1402 0 35.2873 0 45.7151V145.85C0 148.133 2.98124 150.626 5.44142 148.99L54.5725 120.622C55.4893 120.094 56.3863 118.544 56.3863 117.483V55.6152L19.2923 34.5816V34.575Z"
646
- />
647
- <path
648
- d="M61.9664 2.15088C65.9765 -0.408228 71.2992 -0.890029 76.0085 1.83397L170.036 52.5942C173.393 54.5333 172.147 59.361 167.992 59.3618L167.813 59.3483L132.375 60.3248C130.264 60.3248 129.605 62.329 129.229 64.2422L125.357 84.5503C124.658 88.3819 123.135 90.4663 120.252 88.956L56.3864 55.6215V55.615L19.2923 34.5817C17.5446 33.3614 12.7433 31.4565 7.51929 33.5988L61.9664 2.15088Z"
649
- />
650
- </svg>
651
- </div>
652
- <div class="face right">
653
- <svg
654
- class="flo-logo"
655
- viewBox="0 0 172 300"
656
- xmlns="http://www.w3.org/2000/svg"
657
- aria-hidden="true"
658
- >
659
- <path
660
- d="M1.54715 179.524C4.94675 176.764 11.6045 178.966 13.4335 180.132L13.44 180.125L29.0209 188.966L25.7231 191.604C13.44 200.508 18.7976 216.338 37.1425 217.942L54.0844 219.353L56.3929 219.471V268.062C56.3929 269.123 55.7069 270.574 54.5791 271.201L54.2886 271.366L5.44142 299.569C2.62507 300.967 0 298.711 0 296.429V185.886C0 183.535 0.318398 180.522 1.54715 179.524Z"
661
- />
662
- <path
663
- d="M62.3754 133.49C66.3328 131.155 71.4576 130.799 76.0086 133.424L111.784 154.082L128.952 144.169C130.963 143.008 133.536 144.01 134.228 146.226L153.092 206.478V206.498L153.099 206.517C153.547 208.845 151.674 210.976 149.306 210.818L56.3865 204.855V204.861L39.0593 203.747C35.4781 203.516 34.3703 198.78 37.4768 196.986L55.4036 186.632L19.0811 165.703L19.0746 165.71C17.3158 164.58 12.8616 162.913 7.58502 165.222L62.3754 133.477V133.49Z"
664
- />
665
- <path
666
- d="M19.2923 34.575C10.3024 29.1402 0 35.2873 0 45.7151V145.85C0 148.133 2.98124 150.626 5.44142 148.99L54.5725 120.622C55.4893 120.094 56.3863 118.544 56.3863 117.483V55.6152L19.2923 34.5816V34.575Z"
667
- />
668
- <path
669
- d="M61.9664 2.15088C65.9765 -0.408228 71.2992 -0.890029 76.0085 1.83397L170.036 52.5942C173.393 54.5333 172.147 59.361 167.992 59.3618L167.813 59.3483L132.375 60.3248C130.264 60.3248 129.605 62.329 129.229 64.2422L125.357 84.5503C124.658 88.3819 123.135 90.4663 120.252 88.956L56.3864 55.6215V55.615L19.2923 34.5817C17.5446 33.3614 12.7433 31.4565 7.51929 33.5988L61.9664 2.15088Z"
670
- />
671
- </svg>
672
- </div>
673
- <div class="face floor"></div>
674
- <div class="face front">
675
- <div class="label-wrap">
676
- <div class="label">Goods<br />Loading</div>
677
- </div>
678
- </div>
679
-
680
- <div class="vent">
681
- <div
682
- bind:this={glowRef}
683
- class="glow"
684
- ></div>
685
- {#each fills as _fill, i (i)}
686
- <span
687
- bind:this={fillRefs[i]}
688
- class="fill"
689
- ></span>
690
- {/each}
691
- </div>
692
-
693
- <div class="flap-unit s0 outer">
694
- <div
695
- bind:this={s0pivot}
696
- class="flap-pivot"
697
- >
698
- <div class="flap-panel">
699
- <div class="flap-face out"></div>
700
- <div class="flap-face in"></div>
701
- </div>
702
- </div>
703
- </div>
704
- <div class="flap-unit s2 outer">
705
- <div
706
- bind:this={s2pivot}
707
- class="flap-pivot"
708
- >
709
- <div class="flap-panel">
710
- <div class="flap-face out"></div>
711
- <div class="flap-face in">
712
- {#if messages.length}
713
- <div class="flap-msg">{messages[msgIndex] ?? ''}</div>
714
- {/if}
715
- </div>
716
- </div>
717
- </div>
718
- </div>
719
- <div class="flap-unit s1 inner">
720
- <div
721
- bind:this={s1pivot}
722
- class="flap-pivot"
723
- >
724
- <div class="flap-panel">
725
- <div class="flap-face out"></div>
726
- <div class="flap-face in"></div>
727
- </div>
728
- </div>
729
- </div>
730
- <div class="flap-unit s3 inner">
731
- <div
732
- bind:this={s3pivot}
733
- class="flap-pivot"
734
- >
735
- <div class="flap-panel">
736
- <div class="flap-face out"></div>
737
- <div class="flap-face in"></div>
738
- </div>
739
- </div>
740
- </div>
741
- </div>
742
-
743
- </div>
744
-
745
- <!-- Letters live in their own perspective stage, drawn after (above) the box,
746
- so they erupt from the opening and land in front of everything. -->
747
- <div class="letterstage">
748
- <div class="l3d">
749
- {#each geom.letters as letter, i (i)}
750
- <span
751
- bind:this={letterRefs[i]}
752
- class="letter"
753
- style="font-size:{geom.fontSize}px"
754
- >
755
- {#each letter.layers as layer, k (k)}
756
- <span
757
- class="lyr"
758
- style="transform:{layer.transform};color:{layer.color}"
759
- >{letter.glyph}</span
760
- >
761
- {/each}
762
- <span
763
- class="lface"
764
- style="transform:translate(-50%,-50%);color:{letterFace}"
765
- >{letter.glyph}</span
766
- >
767
- </span>
768
- {/each}
769
- </div>
770
- </div>
771
-
772
- <!-- Caption sits above the box and is the loader's last child, so it is never
773
- hidden by the erupting letters. The dots are three always-present spans
774
- whose opacity cycles, so the pill width stays fixed as they animate. -->
775
- {#if showText}
776
- <div class="caption">
777
- <span class="caption-label">{captionText}</span><span
778
- class="caption-dots"
779
- aria-hidden="true"><span>.</span><span>.</span><span>.</span></span
780
- >
781
- </div>
782
- {/if}
783
- </div>
784
-
450
+ if (glowRef) {
451
+ track(glowRef.animate([
452
+ {
453
+ offset: 0,
454
+ transform: "scale(.4)",
455
+ opacity: 0
456
+ },
457
+ {
458
+ offset: o(confStart),
459
+ transform: "scale(.4)",
460
+ opacity: 0,
461
+ easing: "ease-out"
462
+ },
463
+ {
464
+ offset: o(confStart + confDur * .2),
465
+ transform: "scale(1)",
466
+ opacity: .9
467
+ },
468
+ {
469
+ offset: o(confStart + confDur * .6),
470
+ transform: "scale(1.15)",
471
+ opacity: .5
472
+ },
473
+ {
474
+ offset: o(confStart + confDur * .92),
475
+ transform: "scale(.6)",
476
+ opacity: 0
477
+ },
478
+ {
479
+ offset: 1,
480
+ transform: "scale(.6)",
481
+ opacity: 0
482
+ }
483
+ ], loop));
484
+ }
485
+ // rotate the rear-flap message once per loop
486
+ let timer;
487
+ if (messages.length > 1) {
488
+ msgIndex = Math.random() * messages.length | 0;
489
+ timer = setInterval(() => {
490
+ msgIndex = (msgIndex + 1) % messages.length;
491
+ }, T);
492
+ } else {
493
+ msgIndex = 0;
494
+ }
495
+ return () => {
496
+ anims.forEach((a) => a.cancel());
497
+ if (timer) {
498
+ clearInterval(timer);
499
+ }
500
+ };
501
+ });
502
+ </script>
503
+
504
+ <div
505
+ bind:this={rootRef}
506
+ class="flowms-loader"
507
+ role="status"
508
+ aria-live="polite"
509
+ aria-label={label}
510
+ >
511
+ <svg
512
+ width="0"
513
+ height="0"
514
+ style="position:absolute"
515
+ aria-hidden="true"
516
+ >
517
+ <filter
518
+ id="flowms-stamp"
519
+ x="-15%"
520
+ y="-15%"
521
+ width="130%"
522
+ height="130%"
523
+ >
524
+ <feTurbulence
525
+ type="fractalNoise"
526
+ baseFrequency="0.03 0.045"
527
+ numOctaves="3"
528
+ seed="6"
529
+ result="warp"
530
+ />
531
+ <feDisplacementMap
532
+ in="SourceGraphic"
533
+ in2="warp"
534
+ scale="3.4"
535
+ xChannelSelector="R"
536
+ yChannelSelector="G"
537
+ result="rough"
538
+ />
539
+ <feTurbulence
540
+ type="fractalNoise"
541
+ baseFrequency="0.5 0.55"
542
+ numOctaves="2"
543
+ seed="11"
544
+ result="grain"
545
+ />
546
+ <feColorMatrix
547
+ in="grain"
548
+ type="matrix"
549
+ values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.3 0 0 0 0.05"
550
+ result="grainA"
551
+ />
552
+ <feComposite
553
+ in="rough"
554
+ in2="grainA"
555
+ operator="in"
556
+ />
557
+ </filter>
558
+ </svg>
559
+
560
+ <div class="scene">
561
+ <div class="shadow"></div>
562
+ <div
563
+ bind:this={boxRef}
564
+ class="box"
565
+ >
566
+ <div class="face back"></div>
567
+ <div class="face left">
568
+ <svg
569
+ class="flo-logo"
570
+ viewBox="0 0 172 300"
571
+ xmlns="http://www.w3.org/2000/svg"
572
+ aria-hidden="true"
573
+ >
574
+ <path
575
+ d="M1.54715 179.524C4.94675 176.764 11.6045 178.966 13.4335 180.132L13.44 180.125L29.0209 188.966L25.7231 191.604C13.44 200.508 18.7976 216.338 37.1425 217.942L54.0844 219.353L56.3929 219.471V268.062C56.3929 269.123 55.7069 270.574 54.5791 271.201L54.2886 271.366L5.44142 299.569C2.62507 300.967 0 298.711 0 296.429V185.886C0 183.535 0.318398 180.522 1.54715 179.524Z"
576
+ />
577
+ <path
578
+ d="M62.3754 133.49C66.3328 131.155 71.4576 130.799 76.0086 133.424L111.784 154.082L128.952 144.169C130.963 143.008 133.536 144.01 134.228 146.226L153.092 206.478V206.498L153.099 206.517C153.547 208.845 151.674 210.976 149.306 210.818L56.3865 204.855V204.861L39.0593 203.747C35.4781 203.516 34.3703 198.78 37.4768 196.986L55.4036 186.632L19.0811 165.703L19.0746 165.71C17.3158 164.58 12.8616 162.913 7.58502 165.222L62.3754 133.477V133.49Z"
579
+ />
580
+ <path
581
+ d="M19.2923 34.575C10.3024 29.1402 0 35.2873 0 45.7151V145.85C0 148.133 2.98124 150.626 5.44142 148.99L54.5725 120.622C55.4893 120.094 56.3863 118.544 56.3863 117.483V55.6152L19.2923 34.5816V34.575Z"
582
+ />
583
+ <path
584
+ d="M61.9664 2.15088C65.9765 -0.408228 71.2992 -0.890029 76.0085 1.83397L170.036 52.5942C173.393 54.5333 172.147 59.361 167.992 59.3618L167.813 59.3483L132.375 60.3248C130.264 60.3248 129.605 62.329 129.229 64.2422L125.357 84.5503C124.658 88.3819 123.135 90.4663 120.252 88.956L56.3864 55.6215V55.615L19.2923 34.5817C17.5446 33.3614 12.7433 31.4565 7.51929 33.5988L61.9664 2.15088Z"
585
+ />
586
+ </svg>
587
+ </div>
588
+ <div class="face right">
589
+ <svg
590
+ class="flo-logo"
591
+ viewBox="0 0 172 300"
592
+ xmlns="http://www.w3.org/2000/svg"
593
+ aria-hidden="true"
594
+ >
595
+ <path
596
+ d="M1.54715 179.524C4.94675 176.764 11.6045 178.966 13.4335 180.132L13.44 180.125L29.0209 188.966L25.7231 191.604C13.44 200.508 18.7976 216.338 37.1425 217.942L54.0844 219.353L56.3929 219.471V268.062C56.3929 269.123 55.7069 270.574 54.5791 271.201L54.2886 271.366L5.44142 299.569C2.62507 300.967 0 298.711 0 296.429V185.886C0 183.535 0.318398 180.522 1.54715 179.524Z"
597
+ />
598
+ <path
599
+ d="M62.3754 133.49C66.3328 131.155 71.4576 130.799 76.0086 133.424L111.784 154.082L128.952 144.169C130.963 143.008 133.536 144.01 134.228 146.226L153.092 206.478V206.498L153.099 206.517C153.547 208.845 151.674 210.976 149.306 210.818L56.3865 204.855V204.861L39.0593 203.747C35.4781 203.516 34.3703 198.78 37.4768 196.986L55.4036 186.632L19.0811 165.703L19.0746 165.71C17.3158 164.58 12.8616 162.913 7.58502 165.222L62.3754 133.477V133.49Z"
600
+ />
601
+ <path
602
+ d="M19.2923 34.575C10.3024 29.1402 0 35.2873 0 45.7151V145.85C0 148.133 2.98124 150.626 5.44142 148.99L54.5725 120.622C55.4893 120.094 56.3863 118.544 56.3863 117.483V55.6152L19.2923 34.5816V34.575Z"
603
+ />
604
+ <path
605
+ d="M61.9664 2.15088C65.9765 -0.408228 71.2992 -0.890029 76.0085 1.83397L170.036 52.5942C173.393 54.5333 172.147 59.361 167.992 59.3618L167.813 59.3483L132.375 60.3248C130.264 60.3248 129.605 62.329 129.229 64.2422L125.357 84.5503C124.658 88.3819 123.135 90.4663 120.252 88.956L56.3864 55.6215V55.615L19.2923 34.5817C17.5446 33.3614 12.7433 31.4565 7.51929 33.5988L61.9664 2.15088Z"
606
+ />
607
+ </svg>
608
+ </div>
609
+ <div class="face floor"></div>
610
+ <div class="face front">
611
+ <div class="label-wrap">
612
+ <div class="label">Goods<br />Loading</div>
613
+ </div>
614
+ </div>
615
+
616
+ <div class="vent">
617
+ <div
618
+ bind:this={glowRef}
619
+ class="glow"
620
+ ></div>
621
+ {#each fills as _fill, i (i)}
622
+ <span
623
+ bind:this={fillRefs[i]}
624
+ class="fill"
625
+ ></span>
626
+ {/each}
627
+ </div>
628
+
629
+ <div class="flap-unit s0 outer">
630
+ <div
631
+ bind:this={s0pivot}
632
+ class="flap-pivot"
633
+ >
634
+ <div class="flap-panel">
635
+ <div class="flap-face out"></div>
636
+ <div class="flap-face in"></div>
637
+ </div>
638
+ </div>
639
+ </div>
640
+ <div class="flap-unit s2 outer">
641
+ <div
642
+ bind:this={s2pivot}
643
+ class="flap-pivot"
644
+ >
645
+ <div class="flap-panel">
646
+ <div class="flap-face out"></div>
647
+ <div class="flap-face in">
648
+ {#if messages.length}
649
+ <div class="flap-msg">{messages[msgIndex] ?? ''}</div>
650
+ {/if}
651
+ </div>
652
+ </div>
653
+ </div>
654
+ </div>
655
+ <div class="flap-unit s1 inner">
656
+ <div
657
+ bind:this={s1pivot}
658
+ class="flap-pivot"
659
+ >
660
+ <div class="flap-panel">
661
+ <div class="flap-face out"></div>
662
+ <div class="flap-face in"></div>
663
+ </div>
664
+ </div>
665
+ </div>
666
+ <div class="flap-unit s3 inner">
667
+ <div
668
+ bind:this={s3pivot}
669
+ class="flap-pivot"
670
+ >
671
+ <div class="flap-panel">
672
+ <div class="flap-face out"></div>
673
+ <div class="flap-face in"></div>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </div>
678
+
679
+ </div>
680
+
681
+ <!-- Letters live in their own perspective stage, drawn after (above) the box,
682
+ so they erupt from the opening and land in front of everything. -->
683
+ <div class="letterstage">
684
+ <div class="l3d">
685
+ {#each geom.letters as letter, i (i)}
686
+ <span
687
+ bind:this={letterRefs[i]}
688
+ class="letter"
689
+ style="font-size:{geom.fontSize}px"
690
+ >
691
+ {#each letter.layers as layer, k (k)}
692
+ <span
693
+ class="lyr"
694
+ style="transform:{layer.transform};color:{layer.color}"
695
+ >{letter.glyph}</span
696
+ >
697
+ {/each}
698
+ <span
699
+ class="lface"
700
+ style="transform:translate(-50%,-50%);color:{letterFace}"
701
+ >{letter.glyph}</span
702
+ >
703
+ </span>
704
+ {/each}
705
+ </div>
706
+ </div>
707
+
708
+ <!-- Caption sits above the box and is the loader's last child, so it is never
709
+ hidden by the erupting letters. The dots are three always-present spans
710
+ whose opacity cycles, so the pill width stays fixed as they animate. -->
711
+ {#if showText}
712
+ <div class="caption">
713
+ <span class="caption-label">{captionText}</span><span
714
+ class="caption-dots"
715
+ aria-hidden="true"><span>.</span><span>.</span><span>.</span></span
716
+ >
717
+ </div>
718
+ {/if}
719
+ </div>
720
+
785
721
  <style>@charset "UTF-8";
786
722
  .flowms-loader {
787
723
  --w: 120px; /* footprint width / depth (square) */
@@ -1201,4 +1137,4 @@
1201
1137
  66%, 100% {
1202
1138
  opacity: 1;
1203
1139
  }
1204
- }</style>
1140
+ }</style>