flet-box 1.1.0 → 1.1.1
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/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +4 -0
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/colors.js +0 -8
- package/bin/utils/helpers.js +0 -5
- package/bin/utils/images/jpeg.js +2 -3
- package/bin/utils/templates.js +78 -73
- package/docs/README.md +2 -0
- package/docs/cli/README.md +18 -1
- package/docs/guides/README.md +5 -2
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +53 -8
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/utilities.md +116 -4
- package/docs/guides/widget-structure.md +30 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +134 -11
- package/docs/tools/device.md +75 -3
- package/docs/tools/text-and-time.md +116 -6
- package/docs/widget/Accordion.md +2 -0
- package/docs/widget/AnimatedBox.md +46 -20
- package/docs/widget/AnimatedText.md +13 -7
- package/docs/widget/Badge.md +1 -0
- package/docs/widget/COMMON_PROPS.md +26 -3
- package/docs/widget/Chart.md +1 -0
- package/docs/widget/Checkbox.md +7 -1
- package/docs/widget/CircularChart.md +2 -0
- package/docs/widget/Drawer.md +1 -0
- package/docs/widget/GridView.md +4 -1
- package/docs/widget/Input.md +2 -0
- package/docs/widget/InstallButton.md +1 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/Rating.md +2 -0
- package/docs/widget/Slider.md +2 -0
- package/docs/widget/Stack.md +6 -5
- package/docs/widget/Stepper.md +2 -0
- package/docs/widget/Tooltip.md +3 -1
- package/package.json +3 -2
- package/skills/fletbox-design/SKILL.md +390 -14
- package/skills/fletbox-motion/SKILL.md +145 -47
- package/skills/fletbox-rules/SKILL.md +140 -1
- package/src/animations/AnimatedBox.js +321 -226
- package/src/animations/MatrixRain.js +7 -1
- package/src/animations/ParallaxBox.js +13 -10
- package/src/animations/animate.js +269 -106
- package/src/animations/color.js +91 -0
- package/src/animations/easings.js +275 -0
- package/src/animations/index.js +21 -0
- package/src/animations/motion.js +204 -0
- package/src/animations/transitions.js +327 -0
- package/src/core/runApp.js +0 -6
- package/src/helpers/boxEdges.js +69 -0
- package/src/helpers/html.js +39 -0
- package/src/index.d.ts +377 -25
- package/src/index.js +77 -0
- package/src/navigations/AdaptiveScaffold.js +6 -2
- package/src/navigations/AppBar.js +16 -7
- package/src/navigations/BottomNavigation.js +24 -1
- package/src/navigations/CollapsibleSideBar.js +0 -1
- package/src/navigations/Drawer.js +32 -1
- package/src/navigations/DrawerItem.js +1 -1
- package/src/navigations/Router.js +0 -1
- package/src/navigations/Scaffold.js +9 -3
- package/src/navigations/Tabs.js +104 -31
- package/src/services/RamStore.js +1 -2
- package/src/tools/breakpoints.js +113 -0
- package/src/tools/createList.js +0 -2
- package/src/tools/fonts.js +99 -0
- package/src/tools/index.js +74 -1
- package/src/tools/margin.js +5 -63
- package/src/tools/padding.js +5 -63
- package/src/tools/print.js +0 -8
- package/src/tools/scale.js +9 -2
- package/src/tools/time.js +56 -15
- package/src/tools/useState.js +1 -54
- package/src/utils/fieldFont.js +77 -0
- package/src/utils/focusRing.js +73 -0
- package/src/utils/focusScope.js +113 -0
- package/src/utils/markdownParser.js +7 -9
- package/src/utils/readableInk.js +7 -1
- package/src/utils/stackLayout.js +55 -0
- package/src/utils/styleInjector.js +25 -0
- package/src/utils/syntaxHighlight.js +1 -17
- package/src/utils/themes.js +15 -0
- package/src/utils/visualEffects.js +49 -47
- package/src/widget-factory/createWidget.js +16 -3
- package/src/widget-factory/effects.js +76 -3
- package/src/widget-factory/reactiveValue.js +0 -7
- package/src/widget-factory/tools.js +26 -143
- package/src/widget-factory/translateProps.js +5 -0
- package/src/widget-factory/widgetFactory.js +1 -1
- package/src/widgets/Accordion.js +70 -12
- package/src/widgets/AdMob.js +1 -1
- package/src/widgets/AdSense.js +1 -1
- package/src/widgets/AlertDialog.js +11 -9
- package/src/widgets/Avatar.js +10 -1
- package/src/widgets/Badge.js +7 -1
- package/src/widgets/BottomSheet.js +39 -5
- package/src/widgets/Chart.js +41 -9
- package/src/widgets/Checkbox.js +59 -9
- package/src/widgets/Chip.js +13 -1
- package/src/widgets/CircularBar.js +17 -5
- package/src/widgets/CircularChart.js +24 -2
- package/src/widgets/CodeViewer.js +5 -3
- package/src/widgets/Dropdown.js +223 -20
- package/src/widgets/GridView.js +17 -1
- package/src/widgets/Icon.js +21 -0
- package/src/widgets/Input.js +75 -44
- package/src/widgets/InstallButton.js +8 -2
- package/src/widgets/ListTile.js +6 -5
- package/src/widgets/ListView.js +21 -7
- package/src/widgets/Markdown.js +25 -44
- package/src/widgets/Modal.js +43 -6
- package/src/widgets/Pagination.js +35 -3
- package/src/widgets/ProgressBar.js +18 -0
- package/src/widgets/Radio.js +7 -0
- package/src/widgets/Rating.js +104 -16
- package/src/widgets/Skeleton.js +9 -5
- package/src/widgets/Slider.js +87 -7
- package/src/widgets/SnackBar.js +30 -1
- package/src/widgets/Stack.js +25 -13
- package/src/widgets/Stepper.js +112 -64
- package/src/widgets/Switch.js +10 -1
- package/src/widgets/Tooltip.js +93 -10
- package/src/widgets/TreeView.js +5 -5
- package/src/create-index.sh +0 -237
- package/src/widget-factory/pixTool.js +0 -40
- package/src/widget-factory/stringTool.js +0 -26
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @module animations/MatrixRain
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { prefersReducedMotion } from "./motion.js";
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Creates and starts a Matrix-rain animation on a `<canvas>` element.
|
|
17
19
|
*
|
|
@@ -203,7 +205,11 @@ export const MatrixRain = (props = {}) => {
|
|
|
203
205
|
drops[i] += speed + Math.random() * speed;
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
|
|
208
|
+
// Reduced motion stops the rain rather than slowing it: the field is painted
|
|
209
|
+
// once and left, because a decorative texture that scrolls forever is the
|
|
210
|
+
// exact pattern the preference was invented for. The check sits on the
|
|
211
|
+
// re-arm, so the OS setting flipping mid-session stops it within a frame.
|
|
212
|
+
animationId = prefersReducedMotion() ? null : requestAnimationFrame(draw);
|
|
207
213
|
};
|
|
208
214
|
|
|
209
215
|
// Initialize canvas and start animation
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module animations/ParallaxBox
|
|
15
15
|
*/
|
|
16
16
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
17
|
-
import {
|
|
17
|
+
import { prefersReducedMotion } from "./motion.js";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Creates a parallax-effect container.
|
|
@@ -65,10 +65,6 @@ export const ParallaxBox = (props = {}) => {
|
|
|
65
65
|
|
|
66
66
|
let containerRef = null;
|
|
67
67
|
let animationFrame = null;
|
|
68
|
-
let currentX = 0;
|
|
69
|
-
let currentY = 0;
|
|
70
|
-
let targetX = 0;
|
|
71
|
-
let targetY = 0;
|
|
72
68
|
let animatedContent = null;
|
|
73
69
|
let initialTimer = null;
|
|
74
70
|
const transitionTimers = new Set();
|
|
@@ -100,6 +96,18 @@ export const ParallaxBox = (props = {}) => {
|
|
|
100
96
|
const applyTransform = (x, y, animate = true) => {
|
|
101
97
|
if (disabled) return;
|
|
102
98
|
|
|
99
|
+
// A layer that shifts under the reader's own scroll is the motion
|
|
100
|
+
// `prefers-reduced-motion` exists to stop, and the check belongs here rather
|
|
101
|
+
// than in the handlers: the OS setting can flip while the screen is open, and
|
|
102
|
+
// the next scroll frame is the moment that has to notice.
|
|
103
|
+
if (prefersReducedMotion()) {
|
|
104
|
+
if (animatedContent && animatedContent.style.transform) {
|
|
105
|
+
animatedContent.style.transform = "translateX(0) translateY(0)";
|
|
106
|
+
animatedContent.style.transition = "none";
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
let moveX = x;
|
|
104
112
|
let moveY = y;
|
|
105
113
|
|
|
@@ -114,9 +122,6 @@ export const ParallaxBox = (props = {}) => {
|
|
|
114
122
|
moveX = Math.max(-maxOffset, Math.min(maxOffset, moveX));
|
|
115
123
|
moveY = Math.max(-maxOffset, Math.min(maxOffset, moveY));
|
|
116
124
|
|
|
117
|
-
targetX = moveX;
|
|
118
|
-
targetY = moveY;
|
|
119
|
-
|
|
120
125
|
if (onParallaxMove) onParallaxMove({ x: moveX, y: moveY });
|
|
121
126
|
|
|
122
127
|
if (animatedContent) {
|
|
@@ -144,8 +149,6 @@ export const ParallaxBox = (props = {}) => {
|
|
|
144
149
|
* @returns {void}
|
|
145
150
|
*/
|
|
146
151
|
const resetTransform = (animate = true) => {
|
|
147
|
-
targetX = 0;
|
|
148
|
-
targetY = 0;
|
|
149
152
|
if (animatedContent) {
|
|
150
153
|
animatedContent.style.transform = "translateX(0) translateY(0)";
|
|
151
154
|
if (animate) {
|
|
@@ -19,14 +19,28 @@
|
|
|
19
19
|
* @module animations/animate
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { toREM } from "../utils/units.js";
|
|
23
|
+
import { isColor, mixColor } from "./color.js";
|
|
24
|
+
import { motionDuration } from "./motion.js";
|
|
25
|
+
import { resolveEasing, spring as springCurve } from "./easings.js";
|
|
26
|
+
|
|
22
27
|
// ========== INTERPOLATION HELPERS ==========
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Interpolates between two values at a given progress point.
|
|
26
|
-
* Handles
|
|
31
|
+
* Handles four value shapes:
|
|
27
32
|
* - `number → number`: straight numeric lerp.
|
|
28
|
-
* - `
|
|
29
|
-
* - `"16px" →
|
|
33
|
+
* - `#rrggbb` / `rgb()` / `rgba()`: the shared mixer in `color.js`.
|
|
34
|
+
* - `"16px" → anything numeric`: parses the numeric part, lerps it, re-attaches
|
|
35
|
+
* the starting value's unit.
|
|
36
|
+
* - anything else (a named color, `hidden`/`visible`, a font stack): a cut at the
|
|
37
|
+
* end of the loop.
|
|
38
|
+
*
|
|
39
|
+
* The last case is the one worth naming: the unit branch used to run `parseFloat`
|
|
40
|
+
* on values with no number in them and write `"NaNpx"` on every frame. The browser
|
|
41
|
+
* drops a declaration it cannot parse, so the animation looked like it ran and
|
|
42
|
+
* changed nothing at all — the same silent failure the transform registry fixed
|
|
43
|
+
* for `translateY`.
|
|
30
44
|
*
|
|
31
45
|
* @param {number|string} from - Starting value.
|
|
32
46
|
* @param {number|string} to - Ending value.
|
|
@@ -38,103 +52,202 @@ const interpolate = (from, to, progress) => {
|
|
|
38
52
|
if (typeof from === "number" && typeof to === "number") {
|
|
39
53
|
return from + (to - from) * progress;
|
|
40
54
|
}
|
|
41
|
-
// Color to color
|
|
42
|
-
if (
|
|
43
|
-
return
|
|
55
|
+
// Color to color — hex, rgb() and rgba() all blend
|
|
56
|
+
if (isColor(from) && isColor(to)) {
|
|
57
|
+
return mixColor(from, to, progress);
|
|
44
58
|
}
|
|
45
59
|
// String with units (e.g., "16px", "2rem")
|
|
46
60
|
if (typeof from === "string") {
|
|
47
61
|
const fromNum = parseFloat(from);
|
|
48
62
|
const toNum = parseFloat(to);
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
if (Number.isFinite(fromNum) && Number.isFinite(toNum)) {
|
|
64
|
+
const unit = from.replace(fromNum.toString(), "");
|
|
65
|
+
return fromNum + (toNum - fromNum) * progress + unit;
|
|
66
|
+
}
|
|
51
67
|
}
|
|
52
|
-
|
|
68
|
+
// Not a number, not a length, not a color: arrive at the end, don't pretend.
|
|
69
|
+
if (from === to) return from;
|
|
70
|
+
return progress >= 1 ? to : from;
|
|
53
71
|
};
|
|
54
72
|
|
|
73
|
+
// ========== EASING FUNCTIONS ==========
|
|
74
|
+
|
|
55
75
|
/**
|
|
56
|
-
*
|
|
76
|
+
* Applies a named easing curve to a linear progress value.
|
|
77
|
+
*
|
|
78
|
+
* The curves live in `easings.js` so the imperative engine, the presets and a
|
|
79
|
+
* `spring()` all read the same table. A function is accepted as the curve, which
|
|
80
|
+
* is how a caller passes one that is not in the vocabulary.
|
|
57
81
|
*
|
|
58
|
-
* @param {string
|
|
59
|
-
* @param {
|
|
60
|
-
* @
|
|
61
|
-
* @returns {string} Interpolated color as `"rgb(r, g, b)"`.
|
|
82
|
+
* @param {string|((t: number) => number)} type - Easing name or curve.
|
|
83
|
+
* @param {number} t - Raw linear progress in [0, 1].
|
|
84
|
+
* @returns {number} Eased progress; outside `[0, 1]` for the overshoot curves.
|
|
62
85
|
*/
|
|
63
|
-
const
|
|
64
|
-
const rgb1 = hexToRgb(color1);
|
|
65
|
-
const rgb2 = hexToRgb(color2);
|
|
66
|
-
const r = Math.round(rgb1.r + (rgb2.r - rgb1.r) * progress);
|
|
67
|
-
const g = Math.round(rgb1.g + (rgb2.g - rgb1.g) * progress);
|
|
68
|
-
const b = Math.round(rgb1.b + (rgb2.b - rgb1.b) * progress);
|
|
69
|
-
return `rgb(${r}, ${g}, ${b})`;
|
|
70
|
-
};
|
|
86
|
+
const getEasing = (type, t) => resolveEasing(type)(t);
|
|
71
87
|
|
|
72
88
|
/**
|
|
73
|
-
*
|
|
89
|
+
* Transform functions the engine can animate, with the kind of value each one
|
|
90
|
+
* takes and the value that means "no effect" — the value an animation with no
|
|
91
|
+
* explicit `from` starts from.
|
|
74
92
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
93
|
+
* `translateX`/`rotate`/`skewY` are not properties of `CSSStyleDeclaration`, so
|
|
94
|
+
* the generic write path used to land them on the widget object as a harmless JS
|
|
95
|
+
* field: `animate(row, "translateY", 24, 0)` ran a full animation loop and moved
|
|
96
|
+
* nothing, which is why the skill had to tell you to reach for `scale` even for a
|
|
97
|
+
* slide. They are now composed into one `transform` string.
|
|
98
|
+
*
|
|
99
|
+
* A `length` function takes framework units, not pixels: the bare `24` a caller
|
|
100
|
+
* writes becomes `1.5rem`, the same way every routed prop does, so a slide grows
|
|
101
|
+
* with the reader's font instead of staying a pixel while the row it belongs to
|
|
102
|
+
* got taller. `angle` functions take degrees and `number` functions are scalars.
|
|
103
|
+
*
|
|
104
|
+
* @type {Object<string, { kind: "length"|"angle"|"number", neutral: number }>}
|
|
78
105
|
*/
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
const TRANSFORM_FUNCTIONS = {
|
|
107
|
+
translateX: { kind: "length", neutral: 0 },
|
|
108
|
+
translateY: { kind: "length", neutral: 0 },
|
|
109
|
+
translateZ: { kind: "length", neutral: 0 },
|
|
110
|
+
rotate: { kind: "angle", neutral: 0 },
|
|
111
|
+
rotateX: { kind: "angle", neutral: 0 },
|
|
112
|
+
rotateY: { kind: "angle", neutral: 0 },
|
|
113
|
+
rotateZ: { kind: "angle", neutral: 0 },
|
|
114
|
+
skewX: { kind: "angle", neutral: 0 },
|
|
115
|
+
skewY: { kind: "angle", neutral: 0 },
|
|
116
|
+
scale: { kind: "number", neutral: 1 },
|
|
117
|
+
scaleX: { kind: "number", neutral: 1 },
|
|
118
|
+
scaleY: { kind: "number", neutral: 1 },
|
|
88
119
|
};
|
|
89
120
|
|
|
90
|
-
|
|
121
|
+
/**
|
|
122
|
+
* The order the parts are written in. CSS transforms apply left to right, so
|
|
123
|
+
* translate first, then rotate, then scale keeps a rotation spinning about the
|
|
124
|
+
* element's own centre instead of the page's.
|
|
125
|
+
* @type {string[]}
|
|
126
|
+
*/
|
|
127
|
+
const TRANSFORM_ORDER = Object.keys(TRANSFORM_FUNCTIONS);
|
|
91
128
|
|
|
92
129
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
130
|
+
* Live transform parts per widget, so two loops on one element (a slide and a
|
|
131
|
+
* scale) each own a function rather than overwriting the whole string.
|
|
132
|
+
* @type {WeakMap<object, Map<string, number|string>>}
|
|
133
|
+
*/
|
|
134
|
+
const transformParts = new WeakMap();
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Merges one transform function into the widget's `transform` string.
|
|
95
138
|
*
|
|
96
|
-
* @param {
|
|
97
|
-
* @param {
|
|
98
|
-
* @
|
|
139
|
+
* @param {HTMLElement} widget - The element to write on.
|
|
140
|
+
* @param {string} property - A key of {@link TRANSFORM_FUNCTIONS}.
|
|
141
|
+
* @param {number|string} value - A bare number gets the unit that function needs.
|
|
142
|
+
* @returns {void}
|
|
99
143
|
*/
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
const writeTransformPart = (widget, property, value) => {
|
|
145
|
+
let parts = transformParts.get(widget);
|
|
146
|
+
if (!parts) {
|
|
147
|
+
parts = new Map();
|
|
148
|
+
transformParts.set(widget, parts);
|
|
149
|
+
}
|
|
150
|
+
parts.set(property, formatTransformValue(property, value));
|
|
151
|
+
widget.style.transform = TRANSFORM_ORDER.filter((name) => parts.has(name))
|
|
152
|
+
.map((name) => `${name}(${parts.get(name)})`)
|
|
153
|
+
.join(" ");
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The CSS value of one transform function.
|
|
158
|
+
*
|
|
159
|
+
* A string already carries its unit and is passed through; a bare number is a
|
|
160
|
+
* framework unit for a `length`, degrees for an `angle`, and a plain scalar for a
|
|
161
|
+
* `number`.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} property - A key of {@link TRANSFORM_FUNCTIONS}.
|
|
164
|
+
* @param {number|string} value
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
const formatTransformValue = (property, value) => {
|
|
168
|
+
if (typeof value !== "number") return String(value);
|
|
169
|
+
const { kind } = TRANSFORM_FUNCTIONS[property];
|
|
170
|
+
if (kind === "length") return toREM(value);
|
|
171
|
+
if (kind === "angle") return `${value}deg`;
|
|
172
|
+
return String(value);
|
|
108
173
|
};
|
|
109
174
|
|
|
110
175
|
/**
|
|
111
176
|
* Reads the current animated value of a property from a widget.
|
|
112
|
-
*
|
|
113
|
-
*
|
|
177
|
+
* Transform functions live inside a `transform` string rather than as
|
|
178
|
+
* standalone properties, so a transform with no part stored yet reads neutral
|
|
179
|
+
* (`1` for `scale`, `0` for the rest).
|
|
114
180
|
*
|
|
115
181
|
* @param {HTMLElement|object} widget - The target widget or DOM element.
|
|
116
182
|
* @param {string} property - The property to read.
|
|
117
|
-
* @returns {number|string} The current value, or
|
|
183
|
+
* @returns {number|string} The current value, or the property's neutral value.
|
|
118
184
|
*/
|
|
119
185
|
const readAnimatedValue = (widget, property) => {
|
|
120
|
-
if (property
|
|
186
|
+
if (TRANSFORM_FUNCTIONS[property]) {
|
|
187
|
+
const stored = transformParts.get(widget)?.get(property);
|
|
188
|
+
if (stored !== undefined) return stored;
|
|
189
|
+
return TRANSFORM_FUNCTIONS[property].neutral;
|
|
190
|
+
}
|
|
121
191
|
if (widget?.style && property in widget.style) {
|
|
122
192
|
const inline = widget.style[property];
|
|
123
193
|
// An inline-unset property reads as "" and `interpolate` turns that into the
|
|
124
194
|
// string "NaN", which the browser silently drops — so `animate(el, "opacity",
|
|
125
195
|
// undefined, 1, 300)` (the documented API) wrote "NaN" every frame and never
|
|
126
|
-
// animated anything.
|
|
196
|
+
// animated anything. What the element actually looks like is the next place to
|
|
197
|
+
// ask, because a widget styled through a class or a stylesheet has an empty
|
|
198
|
+
// inline value and a real one: `fadeOut` of a card the theme set to 0.8 must
|
|
199
|
+
// start at 0.8, not at the neutral guess below.
|
|
127
200
|
if (inline !== "" && inline !== null && inline !== undefined) return inline;
|
|
201
|
+
const computed = readComputedStyle(widget, property);
|
|
202
|
+
if (computed !== undefined) return computed;
|
|
128
203
|
return property === "opacity" ? 1 : 0;
|
|
129
204
|
}
|
|
130
205
|
return widget?.[property];
|
|
131
206
|
};
|
|
132
207
|
|
|
208
|
+
/**
|
|
209
|
+
* The value a real element shows right now, from its computed style.
|
|
210
|
+
*
|
|
211
|
+
* Read once when a loop starts, never per frame. A plain object passed by a caller
|
|
212
|
+
* (a widget-shaped mock) has no computed style, and asking is what a `null` answer
|
|
213
|
+
* means here.
|
|
214
|
+
*
|
|
215
|
+
* @param {HTMLElement|object} widget
|
|
216
|
+
* @param {string} property
|
|
217
|
+
* @returns {string|undefined}
|
|
218
|
+
*/
|
|
219
|
+
const readComputedStyle = (widget, property) => {
|
|
220
|
+
if (typeof getComputedStyle !== "function") return undefined;
|
|
221
|
+
if (!(widget instanceof Element)) return undefined;
|
|
222
|
+
const value = getComputedStyle(widget)[property];
|
|
223
|
+
return value === "" || value === undefined ? undefined : value;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Style properties a bare number must NOT get a length unit on.
|
|
228
|
+
*
|
|
229
|
+
* The same exemptions the factory's `toCSSValue` keeps, listed here because the
|
|
230
|
+
* animation engine writes styles directly and cannot ask the prop registry.
|
|
231
|
+
* @type {Set<string>}
|
|
232
|
+
*/
|
|
233
|
+
const UNITLESS_STYLE_PROPS = new Set([
|
|
234
|
+
"opacity",
|
|
235
|
+
"zIndex",
|
|
236
|
+
"order",
|
|
237
|
+
"flex",
|
|
238
|
+
"flexGrow",
|
|
239
|
+
"flexShrink",
|
|
240
|
+
"fontWeight",
|
|
241
|
+
"lineHeight",
|
|
242
|
+
"aspectRatio",
|
|
243
|
+
]);
|
|
244
|
+
|
|
133
245
|
/**
|
|
134
246
|
* Writes an animated value to a widget property.
|
|
135
|
-
* -
|
|
136
|
-
* -
|
|
137
|
-
*
|
|
247
|
+
* - Transform functions merge into `transform` (see {@link writeTransformPart}).
|
|
248
|
+
* - A bare number on any other length is a framework unit and becomes rem, the way
|
|
249
|
+
* a routed prop does — `animate(el, "width", 100, 300)` grows with the reader's
|
|
250
|
+
* font instead of staying a pixel while the card around it got taller.
|
|
138
251
|
* - Falls back to setting the property directly on the widget object for
|
|
139
252
|
* non-style properties.
|
|
140
253
|
*
|
|
@@ -145,22 +258,15 @@ const readAnimatedValue = (widget, property) => {
|
|
|
145
258
|
*/
|
|
146
259
|
const writeAnimatedValue = (widget, property, value) => {
|
|
147
260
|
if (!widget) return;
|
|
148
|
-
if (property
|
|
149
|
-
widget.style
|
|
261
|
+
if (TRANSFORM_FUNCTIONS[property]) {
|
|
262
|
+
if (!widget.style) return;
|
|
263
|
+
writeTransformPart(widget, property, value);
|
|
150
264
|
return;
|
|
151
265
|
}
|
|
152
266
|
if (widget.style && property in widget.style) {
|
|
153
|
-
const unitlessProperties = new Set([
|
|
154
|
-
"opacity",
|
|
155
|
-
"zIndex",
|
|
156
|
-
"fontWeight",
|
|
157
|
-
"lineHeight",
|
|
158
|
-
"flexGrow",
|
|
159
|
-
"flexShrink",
|
|
160
|
-
]);
|
|
161
267
|
widget.style[property] =
|
|
162
|
-
typeof value === "number" && !
|
|
163
|
-
?
|
|
268
|
+
typeof value === "number" && !UNITLESS_STYLE_PROPS.has(property)
|
|
269
|
+
? toREM(value)
|
|
164
270
|
: value;
|
|
165
271
|
return;
|
|
166
272
|
}
|
|
@@ -240,16 +346,13 @@ export const stopAnimation = (widget, property) => {
|
|
|
240
346
|
* @param {number} duration
|
|
241
347
|
* @param {string} easing
|
|
242
348
|
* @param {(progress: number) => number|string} [valueAt] - Custom value curve,
|
|
243
|
-
* receiving eased progress in `[0, 1]`. Used by `pulse`
|
|
244
|
-
*
|
|
245
|
-
* @param {(progress: number) => number|string} [valueAt] - Custom value curve,
|
|
246
|
-
* receiving eased progress in `[0, 1]`. Used by `pulse` to keep the whole
|
|
247
|
-
* up-and-down gesture inside a single cancelable loop.
|
|
349
|
+
* receiving eased progress in `[0, 1]`. Used by `pulse` and the named
|
|
350
|
+
* transitions to keep a multi-phase gesture inside a single cancelable loop.
|
|
248
351
|
* @param {(widget: HTMLElement|object) => void} [onDone] - Called when the loop
|
|
249
352
|
* ends — also when it is cancelled or replaced, so awaiting code never hangs.
|
|
250
353
|
* @returns {() => boolean} Cancel function; true when it stopped a live loop.
|
|
251
354
|
*/
|
|
252
|
-
const runAnimation = (
|
|
355
|
+
export const runAnimation = (
|
|
253
356
|
widget,
|
|
254
357
|
property,
|
|
255
358
|
from,
|
|
@@ -263,7 +366,9 @@ const runAnimation = (
|
|
|
263
366
|
// never reaches 1 and the loop re-arms forever (and `animateAsync` never
|
|
264
367
|
// resolves). Clamping the progress is not enough — it just sits at 0 and
|
|
265
368
|
// `0 < 1` stays true — so sanitise the duration itself. `0` collapses to one frame.
|
|
266
|
-
|
|
369
|
+
// Reduced motion goes through the same door: one frame that writes the end
|
|
370
|
+
// value, so the state still lands and an `await` still resolves.
|
|
371
|
+
const safeDuration = motionDuration(duration);
|
|
267
372
|
const startValue = from !== undefined ? from : readAnimatedValue(widget, property);
|
|
268
373
|
|
|
269
374
|
// Replace the loop already animating this property instead of racing it.
|
|
@@ -325,7 +430,8 @@ const runAnimation = (
|
|
|
325
430
|
* value is read from the widget via {@link readAnimatedValue}.
|
|
326
431
|
* @param {number|string} to - Ending value.
|
|
327
432
|
* @param {number} [duration=300] - Animation duration in milliseconds.
|
|
328
|
-
* @param {string}
|
|
433
|
+
* @param {string|((t: number) => number)} [easing="linear"] - Easing curve name
|
|
434
|
+
* from `EASING_NAMES`, or a curve (`spring().easing`, `cubicBezier(…)`).
|
|
329
435
|
* @returns {() => boolean} Cancel function — true if it stopped a loop still running.
|
|
330
436
|
* The animation also stops on its own when a new animation takes the same property,
|
|
331
437
|
* or via {@link stopAnimation}.
|
|
@@ -352,7 +458,8 @@ export const animate = (
|
|
|
352
458
|
* value is read from the widget via {@link readAnimatedValue}.
|
|
353
459
|
* @param {number|string} to - Ending value.
|
|
354
460
|
* @param {number} [duration=300] - Animation duration in milliseconds.
|
|
355
|
-
* @param {string}
|
|
461
|
+
* @param {string|((t: number) => number)} [easing="linear"] - Easing curve name
|
|
462
|
+
* from `EASING_NAMES`, or a curve (`spring().easing`, `cubicBezier(…)`).
|
|
356
463
|
* @returns {Promise<HTMLElement|object>} Resolves with `widget` when the animation
|
|
357
464
|
* finishes — or as soon as it is cancelled or replaced, so `await` never hangs.
|
|
358
465
|
*/
|
|
@@ -370,22 +477,75 @@ export const animateAsync = (
|
|
|
370
477
|
runAnimation(widget, property, from, to, duration, easing, null, resolve);
|
|
371
478
|
});
|
|
372
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Animates a property the way a physical object settles: overshoot, then stop.
|
|
482
|
+
*
|
|
483
|
+
* There is no duration to choose — a spring runs until its envelope has decayed
|
|
484
|
+
* inside `epsilon`, and `spring()` works that number out. Which is why this is a
|
|
485
|
+
* separate call instead of another easing name: passing `spring().duration` by
|
|
486
|
+
* hand is the same result, this only saves the arithmetic.
|
|
487
|
+
*
|
|
488
|
+
* @param {HTMLElement|object} widget - The target widget or DOM element.
|
|
489
|
+
* @param {string} property - The CSS property (or `"scale"`) to animate.
|
|
490
|
+
* @param {number|string} from - Starting value; reads the widget when `undefined`.
|
|
491
|
+
* @param {number|string} to - Ending value.
|
|
492
|
+
* @param {object} [options] - Spring parameters (`stiffness`,
|
|
493
|
+
* `damping`, `mass`, `initialVelocity`, `epsilon`, `maxDuration`); see {@link spring}.
|
|
494
|
+
* @returns {() => boolean} Cancel function (see {@link animate}).
|
|
495
|
+
*/
|
|
496
|
+
export const animateSpring = (widget, property, from, to, options = {}) => {
|
|
497
|
+
const { duration, easing } = springCurve(options);
|
|
498
|
+
return animate(widget, property, from, to, duration, easing ?? "linear");
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The awaitable form of {@link animateSpring}.
|
|
503
|
+
*
|
|
504
|
+
* @param {HTMLElement|object} widget - The target widget or DOM element.
|
|
505
|
+
* @param {string} property - The CSS property (or `"scale"`) to animate.
|
|
506
|
+
* @param {number|string} from - Starting value.
|
|
507
|
+
* @param {number|string} to - Ending value.
|
|
508
|
+
* @param {object} [options] - Spring parameters (see {@link animateSpring}).
|
|
509
|
+
* @returns {Promise<HTMLElement|object>} Resolves with `widget` when the spring
|
|
510
|
+
* settles — or as soon as it is cancelled or replaced.
|
|
511
|
+
*/
|
|
512
|
+
export const animateSpringAsync = (widget, property, from, to, options = {}) => {
|
|
513
|
+
const { duration, easing } = springCurve(options);
|
|
514
|
+
return animateAsync(widget, property, from, to, duration, easing ?? "linear");
|
|
515
|
+
};
|
|
516
|
+
|
|
373
517
|
// ========== HELPER FUNCTIONS (Fire and Forget) ==========
|
|
374
518
|
|
|
375
519
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
520
|
+
* The scale a pulse is at, given eased progress.
|
|
521
|
+
*
|
|
522
|
+
* Up for the first half of the gesture and back down for the second, inside one
|
|
523
|
+
* loop: a pulse is one cancelable movement, not an animation plus a timer that
|
|
524
|
+
* starts the second half on a widget someone already unmounted.
|
|
525
|
+
*
|
|
526
|
+
* @param {number} progress - Progress through the pulse in `[0, 1]`.
|
|
527
|
+
* @returns {number} The scale to write.
|
|
528
|
+
*/
|
|
529
|
+
const pulseScale = (progress) =>
|
|
530
|
+
interpolate(1, 1.1, progress < 0.5 ? progress * 2 : 2 - progress * 2);
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Fades a widget out by animating its `opacity` to `0`.
|
|
534
|
+
*
|
|
535
|
+
* It starts from wherever the widget actually is — the element's own value, then
|
|
536
|
+
* its computed style — so a card the theme set to `0.8` fades from `0.8` instead
|
|
537
|
+
* of first jumping to fully opaque.
|
|
378
538
|
*
|
|
379
539
|
* @param {HTMLElement|object} widget - The target widget.
|
|
380
540
|
* @param {number} [duration=300] - Fade duration in milliseconds.
|
|
381
541
|
* @returns {() => boolean} Cancel function (see {@link animate}).
|
|
382
542
|
*/
|
|
383
543
|
export const fadeOut = (widget, duration = 300) => {
|
|
384
|
-
return animate(widget, "opacity",
|
|
544
|
+
return animate(widget, "opacity", undefined, 0, duration);
|
|
385
545
|
};
|
|
386
546
|
|
|
387
547
|
/**
|
|
388
|
-
* Fades a widget in by animating its `opacity`
|
|
548
|
+
* Fades a widget in by animating its `opacity` to `1`.
|
|
389
549
|
* Fire-and-forget — does not return a Promise.
|
|
390
550
|
*
|
|
391
551
|
* @param {HTMLElement|object} widget - The target widget.
|
|
@@ -393,7 +553,7 @@ export const fadeOut = (widget, duration = 300) => {
|
|
|
393
553
|
* @returns {() => boolean} Cancel function (see {@link animate}).
|
|
394
554
|
*/
|
|
395
555
|
export const fadeIn = (widget, duration = 300) => {
|
|
396
|
-
return animate(widget, "opacity",
|
|
556
|
+
return animate(widget, "opacity", undefined, 1, duration);
|
|
397
557
|
};
|
|
398
558
|
|
|
399
559
|
/**
|
|
@@ -407,23 +567,12 @@ export const fadeIn = (widget, duration = 300) => {
|
|
|
407
567
|
* @returns {() => boolean} Cancel function (see {@link animate}).
|
|
408
568
|
*/
|
|
409
569
|
export const pulse = (widget, duration = 300) =>
|
|
410
|
-
runAnimation(
|
|
411
|
-
widget,
|
|
412
|
-
"scale",
|
|
413
|
-
1,
|
|
414
|
-
1,
|
|
415
|
-
duration,
|
|
416
|
-
"linear",
|
|
417
|
-
// One loop with a triangular curve instead of an animation plus a
|
|
418
|
-
// `setTimeout`: the timer used to start phase two on a widget that had
|
|
419
|
-
// already been removed from the DOM, and nothing could cancel it.
|
|
420
|
-
(progress) => interpolate(1, 1.1, progress < 0.5 ? progress * 2 : 2 - progress * 2),
|
|
421
|
-
);
|
|
570
|
+
runAnimation(widget, "scale", 1, 1, duration, "linear", pulseScale);
|
|
422
571
|
|
|
423
572
|
// ========== ASYNC HELPER FUNCTIONS (Returns Promise) ==========
|
|
424
573
|
|
|
425
574
|
/**
|
|
426
|
-
* Fades a widget out by animating its `opacity`
|
|
575
|
+
* Fades a widget out by animating its `opacity` to `0`.
|
|
427
576
|
* Returns a Promise that resolves when the fade completes.
|
|
428
577
|
*
|
|
429
578
|
* @param {HTMLElement|object} widget - The target widget.
|
|
@@ -431,11 +580,11 @@ export const pulse = (widget, duration = 300) =>
|
|
|
431
580
|
* @returns {Promise<HTMLElement|object>} Resolves with `widget` on completion.
|
|
432
581
|
*/
|
|
433
582
|
export const fadeOutAsync = (widget, duration = 300) => {
|
|
434
|
-
return animateAsync(widget, "opacity",
|
|
583
|
+
return animateAsync(widget, "opacity", undefined, 0, duration);
|
|
435
584
|
};
|
|
436
585
|
|
|
437
586
|
/**
|
|
438
|
-
* Fades a widget in by animating its `opacity`
|
|
587
|
+
* Fades a widget in by animating its `opacity` to `1`.
|
|
439
588
|
* Returns a Promise that resolves when the fade completes.
|
|
440
589
|
*
|
|
441
590
|
* @param {HTMLElement|object} widget - The target widget.
|
|
@@ -443,19 +592,33 @@ export const fadeOutAsync = (widget, duration = 300) => {
|
|
|
443
592
|
* @returns {Promise<HTMLElement|object>} Resolves with `widget` on completion.
|
|
444
593
|
*/
|
|
445
594
|
export const fadeInAsync = (widget, duration = 300) => {
|
|
446
|
-
return animateAsync(widget, "opacity",
|
|
595
|
+
return animateAsync(widget, "opacity", undefined, 1, duration);
|
|
447
596
|
};
|
|
448
597
|
|
|
449
598
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
599
|
+
* The awaitable form of {@link pulse}.
|
|
600
|
+
*
|
|
601
|
+
* The same single loop, not an up-animation chained to a down-animation: the two
|
|
602
|
+
* halves were twice the frames of the gesture they described, stopped on the
|
|
603
|
+
* `setTimeout` that separated them, and resolved with nothing while every other
|
|
604
|
+
* `Async` helper resolves with the widget it just moved.
|
|
452
605
|
*
|
|
453
606
|
* @param {HTMLElement|object} widget - The target widget.
|
|
454
607
|
* @param {number} [duration=300] - Total pulse duration in milliseconds
|
|
455
608
|
* (split evenly between scale-up and scale-down).
|
|
456
|
-
* @returns {Promise<
|
|
609
|
+
* @returns {Promise<HTMLElement|object>} Resolves with `widget` when the pulse
|
|
610
|
+
* finishes — or as soon as it is cancelled or replaced.
|
|
457
611
|
*/
|
|
458
|
-
export const pulseAsync =
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
612
|
+
export const pulseAsync = (widget, duration = 300) =>
|
|
613
|
+
new Promise((resolve) => {
|
|
614
|
+
runAnimation(
|
|
615
|
+
widget,
|
|
616
|
+
"scale",
|
|
617
|
+
1,
|
|
618
|
+
1,
|
|
619
|
+
duration,
|
|
620
|
+
"linear",
|
|
621
|
+
pulseScale,
|
|
622
|
+
resolve,
|
|
623
|
+
);
|
|
624
|
+
});
|