flet-box 1.1.0 → 1.1.2
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/README.md +16 -12
- package/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +8 -1
- package/bin/commands/createBundle.js +18 -6
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/bundleConfig.js +47 -3
- 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 +101 -85
- package/docs/README.md +24 -1
- package/docs/cli/README.md +51 -10
- 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/first-app.md +6 -7
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/online-updates.md +18 -6
- 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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* color.js - The one color reader both animation engines use.
|
|
3
|
+
*
|
|
4
|
+
* The declarative engine (`AnimatedBox`) and the imperative one (`animate`) each
|
|
5
|
+
* grew a color mixer, and they disagreed: one parsed hex and `rgb()` and wrote an
|
|
6
|
+
* unclamped `rgba()`, the other parsed hex only. A caller who moved a working
|
|
7
|
+
* `from: "rgba(255, 0, 0, 0.4)"` from one to the other got the string `"NaNpx"`
|
|
8
|
+
* written every frame, which the browser drops silently — the animation looked
|
|
9
|
+
* like it ran and changed nothing. One parser, one output, both engines.
|
|
10
|
+
*
|
|
11
|
+
* @module animations/color
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The channels of a CSS color, or `null` when the value is not a color this
|
|
16
|
+
* module can blend.
|
|
17
|
+
*
|
|
18
|
+
* Hex (3- and 6-digit) and `rgb()`/`rgba()` are the shapes a design token
|
|
19
|
+
* actually arrives in. A named color (`"tomato"`) is not: blending it needs a
|
|
20
|
+
* lookup table for 148 names, and the honest answer is to refuse rather than to
|
|
21
|
+
* interpolate towards black.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} value - A hex or `rgb()`/`rgba()` color.
|
|
24
|
+
* @returns {[number, number, number, number]|null} `[red, green, blue, alpha]`,
|
|
25
|
+
* with the channels in `[0, 255]` and alpha in `[0, 1]`.
|
|
26
|
+
*/
|
|
27
|
+
export const parseColor = (value) => {
|
|
28
|
+
if (typeof value !== "string") return null;
|
|
29
|
+
const text = value.trim();
|
|
30
|
+
|
|
31
|
+
const hex = text.match(/^#?([a-f\d]{3,4}|[a-f\d]{6}|[a-f\d]{8})$/i);
|
|
32
|
+
if (hex) {
|
|
33
|
+
const digits = hex[1];
|
|
34
|
+
if (digits.length <= 4) {
|
|
35
|
+
// `#rgb` and `#rgba` are the short forms of the same eight digits.
|
|
36
|
+
const channel = (index) => parseInt(digits[index], 16) * 0x11;
|
|
37
|
+
const alpha = digits.length === 4 ? parseInt(digits[3], 16) / 255 : 1;
|
|
38
|
+
return [channel(0), channel(1), channel(2), alpha];
|
|
39
|
+
}
|
|
40
|
+
const channel = (offset) => parseInt(digits.slice(offset, offset + 2), 16);
|
|
41
|
+
const alpha = digits.length === 8 ? channel(6) / 255 : 1;
|
|
42
|
+
return [channel(0), channel(2), channel(4), alpha];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rgb = text.match(
|
|
46
|
+
/^rgba?\(\s*([\d.]+)(?:%?)\s*[,\s]\s*([\d.]+)(?:%?)\s*[,\s]\s*([\d.]+)(?:%?)\s*(?:[,/]\s*([\d.]+)\s*%?)?\)$/i,
|
|
47
|
+
);
|
|
48
|
+
if (rgb) {
|
|
49
|
+
const channels = [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
|
|
50
|
+
if (channels.some((n) => !Number.isFinite(n))) return null;
|
|
51
|
+
const alpha = rgb[4] === undefined ? 1 : Number(rgb[4]);
|
|
52
|
+
return [...channels, Number.isFinite(alpha) ? alpha : 1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* True when this module knows how to blend the value.
|
|
60
|
+
*
|
|
61
|
+
* @param {unknown} value
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export const isColor = (value) => parseColor(value) !== null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Linearly interpolates between two CSS colors.
|
|
68
|
+
*
|
|
69
|
+
* A curve that overshoots runs past 1, and an `rgb()` channel outside 0–255 makes
|
|
70
|
+
* the browser drop the whole declaration — the color would blink, not overshoot.
|
|
71
|
+
* Both channels and alpha are therefore clamped, so the value is always writable.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} from - Starting color (hex or `rgb()`/`rgba()`).
|
|
74
|
+
* @param {string} to - Ending color.
|
|
75
|
+
* @param {number} t - Interpolation factor; values outside `[0, 1]` are clamped.
|
|
76
|
+
* @returns {string} The blended color as `"rgba(r, g, b, a)"`, or the starting
|
|
77
|
+
* color when either side is not a color this module parses.
|
|
78
|
+
*/
|
|
79
|
+
export const mixColor = (from, to, t) => {
|
|
80
|
+
const a = parseColor(from);
|
|
81
|
+
const b = parseColor(to);
|
|
82
|
+
if (!a || !b) return from;
|
|
83
|
+
const progress = Math.min(1, Math.max(0, t));
|
|
84
|
+
const channel = (start, end) =>
|
|
85
|
+
Math.round(Math.min(255, Math.max(0, start + (end - start) * progress)));
|
|
86
|
+
const alpha = Math.min(1, Math.max(0, a[3] + (b[3] - a[3]) * progress));
|
|
87
|
+
return `rgba(${channel(a[0], b[0])}, ${channel(a[1], b[1])}, ${channel(
|
|
88
|
+
a[2],
|
|
89
|
+
b[2],
|
|
90
|
+
)}, ${Number(alpha.toFixed(3))})`;
|
|
91
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* easings.js - The curves the imperative engine knows.
|
|
3
|
+
*
|
|
4
|
+
* A curve is the difference between an element arriving and an element being
|
|
5
|
+
* teleported. Four names (`linear`, `easeIn`, `easeOut`, `easeInOut`) cover
|
|
6
|
+
* almost nothing, and the gap gets filled by hand-written `requestAnimationFrame`
|
|
7
|
+
* loops in app code — a second animation system, which this repo already has one
|
|
8
|
+
* of. So the standard set lives here.
|
|
9
|
+
*
|
|
10
|
+
* Two rules the shapes obey:
|
|
11
|
+
* - An **entrance** decelerates (`easeOut*`, `spring`): it leaves the start line
|
|
12
|
+
* fast, which is what reads as responsive.
|
|
13
|
+
* - Anything the user presses repeatedly either has no curve at all (no
|
|
14
|
+
* animation) or a short `easeOut`; `easeIn*` starts slow and feels broken.
|
|
15
|
+
*
|
|
16
|
+
* Overshoot curves (`*Back`, `Elastic`, `Bounce`, an underdamped spring) return
|
|
17
|
+
* values outside `[0, 1]`. That is the point — the element travels slightly past
|
|
18
|
+
* its resting place — and every consumer handles it: numbers and units
|
|
19
|
+
* extrapolate, colors clamp, `scale` accepts any positive factor.
|
|
20
|
+
*
|
|
21
|
+
* @module animations/easings
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Overshoot amount for the `Back` family — 1.70158 is the value the CSS easing
|
|
26
|
+
* community settled on (`1.70158 * 1.525` for the in-out variant).
|
|
27
|
+
* @type {number}
|
|
28
|
+
*/
|
|
29
|
+
const BACK_OVERSHOOT = 1.70158;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Solves a cubic Bézier timing function for x, using Newton-Raphson with a
|
|
33
|
+
* bisection fallback, and returns the curve as an easing function.
|
|
34
|
+
*
|
|
35
|
+
* This is the bridge between the two easing vocabularies in the framework:
|
|
36
|
+
* `AnimatedBox({ timing })` and `ParallaxBox({ easing })` hand a string to CSS,
|
|
37
|
+
* while `animate(..., easing)` reads a name. `cubicBezier()` gives the imperative
|
|
38
|
+
* side the exact curve a stylesheet would use, so a screen can share one motion
|
|
39
|
+
* language across declarative and imperative motion.
|
|
40
|
+
*
|
|
41
|
+
* @param {number} x1 - First control point x (must be in `[0, 1]`).
|
|
42
|
+
* @param {number} y1 - First control point y (any value; >1 overshoots).
|
|
43
|
+
* @param {number} x2 - Second control point x (must be in `[0, 1]`).
|
|
44
|
+
* @param {number} y2 - Second control point y.
|
|
45
|
+
* @returns {(t: number) => number} Easing function, `t` in `[0, 1]`.
|
|
46
|
+
*/
|
|
47
|
+
export const cubicBezier = (x1, y1, x2, y2) => {
|
|
48
|
+
const px = (t) => 3 * t * (1 - t) * (1 - t) * x1 + 3 * t * t * (1 - t) * x2 + t * t * t;
|
|
49
|
+
const py = (t) => 3 * t * (1 - t) * (1 - t) * y1 + 3 * t * t * (1 - t) * y2 + t * t * t;
|
|
50
|
+
// Derivative of px, used to walk the parameter toward the requested x.
|
|
51
|
+
const dx = (t) =>
|
|
52
|
+
3 * (1 - t) * (1 - t) * x1 + 6 * t * (1 - t) * (x2 - x1) + 3 * t * t * (1 - x2);
|
|
53
|
+
|
|
54
|
+
return (progress) => {
|
|
55
|
+
if (!(progress > 0)) return 0;
|
|
56
|
+
if (progress >= 1) return 1;
|
|
57
|
+
let parameter = progress;
|
|
58
|
+
for (let i = 0; i < 8; i++) {
|
|
59
|
+
const error = px(parameter) - progress;
|
|
60
|
+
if (Math.abs(error) < 1e-5) return py(parameter);
|
|
61
|
+
const slope = dx(parameter);
|
|
62
|
+
if (Math.abs(slope) < 1e-6) break;
|
|
63
|
+
parameter -= error / slope;
|
|
64
|
+
}
|
|
65
|
+
// Newton wandered off the curve; bisect between the ends, which cannot fail.
|
|
66
|
+
let low = 0;
|
|
67
|
+
let high = 1;
|
|
68
|
+
parameter = progress;
|
|
69
|
+
for (let i = 0; i < 20; i++) {
|
|
70
|
+
const value = px(parameter);
|
|
71
|
+
if (Math.abs(value - progress) < 1e-5) break;
|
|
72
|
+
if (value > progress) high = parameter;
|
|
73
|
+
else low = parameter;
|
|
74
|
+
parameter = (low + high) / 2;
|
|
75
|
+
}
|
|
76
|
+
return py(parameter);
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The named curves `animate()`, `animateAsync()` and the presets accept.
|
|
82
|
+
* Read off the standard Penner set, plus the four Material timing shapes.
|
|
83
|
+
*
|
|
84
|
+
* `easeIn` / `easeOut` / `easeInOut` keep their original quadratic shapes so any
|
|
85
|
+
* animation already in an app moves exactly as it did before.
|
|
86
|
+
*
|
|
87
|
+
* @type {Object<string, (t: number) => number>}
|
|
88
|
+
*/
|
|
89
|
+
export const EASINGS = {
|
|
90
|
+
linear: (t) => t,
|
|
91
|
+
|
|
92
|
+
easeIn: (t) => t * t,
|
|
93
|
+
easeOut: (t) => t * (2 - t),
|
|
94
|
+
easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
|
|
95
|
+
|
|
96
|
+
easeInCubic: (t) => t * t * t,
|
|
97
|
+
easeOutCubic: (t) => 1 - (1 - t) ** 3,
|
|
98
|
+
easeInOutCubic: (t) =>
|
|
99
|
+
t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2,
|
|
100
|
+
|
|
101
|
+
easeInQuart: (t) => t ** 4,
|
|
102
|
+
easeOutQuart: (t) => 1 - (1 - t) ** 4,
|
|
103
|
+
easeInOutQuart: (t) => (t < 0.5 ? 8 * t ** 4 : 1 - (-2 * t + 2) ** 4 / 2),
|
|
104
|
+
|
|
105
|
+
easeInExpo: (t) => (t === 0 ? 0 : 2 ** (10 * t - 10)),
|
|
106
|
+
easeOutExpo: (t) => (t === 1 ? 1 : 1 - 2 ** (-10 * t)),
|
|
107
|
+
easeInOutExpo: (t) =>
|
|
108
|
+
t === 0 || t === 1
|
|
109
|
+
? t
|
|
110
|
+
: t < 0.5
|
|
111
|
+
? 2 ** (20 * t - 10) / 2
|
|
112
|
+
: (2 - 2 ** (-20 * t + 10)) / 2,
|
|
113
|
+
|
|
114
|
+
easeInCirc: (t) => 1 - Math.sqrt(1 - t * t),
|
|
115
|
+
easeOutCirc: (t) => Math.sqrt(1 - (t - 1) ** 2),
|
|
116
|
+
easeInOutCirc: (t) =>
|
|
117
|
+
t < 0.5
|
|
118
|
+
? (1 - Math.sqrt(1 - (2 * t) ** 2)) / 2
|
|
119
|
+
: (Math.sqrt(1 - (-2 * t + 2) ** 2) + 1) / 2,
|
|
120
|
+
|
|
121
|
+
easeInBack: (t) => (BACK_OVERSHOOT + 1) * t ** 3 - BACK_OVERSHOOT * t * t,
|
|
122
|
+
easeOutBack: (t) =>
|
|
123
|
+
1 + (BACK_OVERSHOOT + 1) * (t - 1) ** 3 + BACK_OVERSHOOT * (t - 1) ** 2,
|
|
124
|
+
easeInOutBack: (t) => {
|
|
125
|
+
const c = BACK_OVERSHOOT * 1.525;
|
|
126
|
+
return t < 0.5
|
|
127
|
+
? ((2 * t) ** 2 * ((c + 1) * 2 * t - c)) / 2
|
|
128
|
+
: ((2 * t - 2) ** 2 * ((c + 1) * (2 * t - 2) + c) + 2) / 2;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
easeInElastic: (t) => {
|
|
132
|
+
if (t === 0 || t === 1) return t;
|
|
133
|
+
const c4 = (2 * Math.PI) / 3;
|
|
134
|
+
return -(2 ** (10 * t - 10)) * Math.sin((t * 10 - 10.75) * c4);
|
|
135
|
+
},
|
|
136
|
+
easeOutElastic: (t) => {
|
|
137
|
+
if (t === 0 || t === 1) return t;
|
|
138
|
+
const c4 = (2 * Math.PI) / 3;
|
|
139
|
+
return 2 ** (-10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
|
140
|
+
},
|
|
141
|
+
easeOutBounce: (t) => {
|
|
142
|
+
const n1 = 7.5625;
|
|
143
|
+
const d1 = 2.75;
|
|
144
|
+
if (t < 1 / d1) return n1 * t * t;
|
|
145
|
+
if (t < 2 / d1) return n1 * (t - 1.5 / d1) ** 2 + 0.75;
|
|
146
|
+
if (t < 2.5 / d1) return n1 * (t - 2.25 / d1) ** 2 + 0.9375;
|
|
147
|
+
return n1 * (t - 2.625 / d1) ** 2 + 0.984375;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// Material 3 shapes, as the curves rather than the CSS strings, so an
|
|
151
|
+
// imperative call can match a stylesheet on the same screen.
|
|
152
|
+
standard: cubicBezier(0.2, 0, 0, 1),
|
|
153
|
+
emphasized: cubicBezier(0.3, 0, 0.1, 1),
|
|
154
|
+
emphasizedDecelerate: cubicBezier(0.05, 0.7, 0.1, 1),
|
|
155
|
+
emphasizedAccelerate: cubicBezier(0.3, 0, 0.8, 0.15),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Every accepted easing name, in the order they are documented.
|
|
160
|
+
* @type {string[]}
|
|
161
|
+
*/
|
|
162
|
+
export const EASING_NAMES = Object.keys(EASINGS);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Resolves whatever a caller passed as `easing` into a curve.
|
|
166
|
+
*
|
|
167
|
+
* A function is used as given — that is how a `spring()` or a `cubicBezier()`
|
|
168
|
+
* reaches `animate()`. An unknown name falls back to `linear` with a warning:
|
|
169
|
+
* silently linear is how a hand-typed `'ease-out'` (a CSS name, not a framework
|
|
170
|
+
* one) ends up moving like a machine on a screen that was supposed to glide.
|
|
171
|
+
*
|
|
172
|
+
* @param {string|((t: number) => number)} [easing] - A name or a curve.
|
|
173
|
+
* @returns {(t: number) => number}
|
|
174
|
+
*/
|
|
175
|
+
export const resolveEasing = (easing) => {
|
|
176
|
+
if (typeof easing === "function") return easing;
|
|
177
|
+
if (easing === undefined || easing === null || easing === "") return EASINGS.linear;
|
|
178
|
+
const curve = EASINGS[easing];
|
|
179
|
+
if (!curve) {
|
|
180
|
+
console.warn(
|
|
181
|
+
`⚠️ [easings] "${easing}" is not a FletBox easing. Known names: ${EASING_NAMES.join(", ")}. Falling back to "linear". Note that CSS names ("ease-out") and cubic-bezier() strings belong to AnimatedBox({ timing }), not here.`,
|
|
182
|
+
);
|
|
183
|
+
return EASINGS.linear;
|
|
184
|
+
}
|
|
185
|
+
return curve;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A damped harmonic oscillator: the curve a real object follows when it is
|
|
190
|
+
* pulled to a target and released.
|
|
191
|
+
*
|
|
192
|
+
* Springs are why an interface feels physical instead of scripted — the movement
|
|
193
|
+
* has no fixed end time, it settles. That also makes them the one thing a
|
|
194
|
+
* duration-based API cannot express, so `spring()` returns both halves: the curve
|
|
195
|
+
* and the duration it needs to settle within `epsilon`.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} [options]
|
|
198
|
+
* @param {number} [options.stiffness=170] - Spring constant `k`. Higher snaps.
|
|
199
|
+
* @param {number} [options.damping=26] - Damping `c`. Lower keeps bouncing.
|
|
200
|
+
* @param {number} [options.mass=1] - Mass `m`. Heavier lags.
|
|
201
|
+
* @param {number} [options.initialVelocity=0] - Starting velocity; a flick hands
|
|
202
|
+
* the spring the pointer's speed so the entrance continues the gesture.
|
|
203
|
+
* @param {number} [options.epsilon=0.001] - How close to the target counts as settled.
|
|
204
|
+
* @param {number} [options.maxDuration=2000] - Ceiling, in ms: an underdamped
|
|
205
|
+
* spring with a lot of energy is still not a 4s modal.
|
|
206
|
+
* @returns {{ easing: (t: number) => number, duration: number, overshoot: boolean }}
|
|
207
|
+
* `easing` is a function, so it goes straight into `animate(...)`'s last
|
|
208
|
+
* argument; `duration` is the `animate(...)` duration to pass with it.
|
|
209
|
+
*/
|
|
210
|
+
export const spring = ({
|
|
211
|
+
stiffness = 170,
|
|
212
|
+
damping = 26,
|
|
213
|
+
mass = 1,
|
|
214
|
+
initialVelocity = 0,
|
|
215
|
+
epsilon = 0.001,
|
|
216
|
+
maxDuration = 2000,
|
|
217
|
+
} = {}) => {
|
|
218
|
+
const k = Math.max(0.0001, Number(stiffness) || 0);
|
|
219
|
+
const c = Math.max(0, Number(damping) || 0);
|
|
220
|
+
const m = Math.max(0.0001, Number(mass) || 0);
|
|
221
|
+
const v0 = Number(initialVelocity) || 0;
|
|
222
|
+
const eps = Math.min(0.5, Math.max(0.000001, Number(epsilon) || 0.001));
|
|
223
|
+
|
|
224
|
+
const omega0 = Math.sqrt(k / m);
|
|
225
|
+
const zeta = c / (2 * Math.sqrt(k * m));
|
|
226
|
+
|
|
227
|
+
// The unit-step response: 0 at t = 0, 1 once settled, with the overshoot the
|
|
228
|
+
// damping allows in between. Each branch is the closed form for that regime.
|
|
229
|
+
const position = (seconds) => {
|
|
230
|
+
if (seconds <= 0) return 0;
|
|
231
|
+
if (zeta < 1) {
|
|
232
|
+
const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);
|
|
233
|
+
const envelope = Math.exp(-zeta * omega0 * seconds);
|
|
234
|
+
return (
|
|
235
|
+
1 -
|
|
236
|
+
envelope *
|
|
237
|
+
(Math.cos(omegaD * seconds) +
|
|
238
|
+
((zeta * omega0 - v0) / omegaD) * Math.sin(omegaD * seconds))
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (zeta === 1) {
|
|
242
|
+
const envelope = Math.exp(-omega0 * seconds);
|
|
243
|
+
return 1 - envelope * (1 + (omega0 - v0) * seconds);
|
|
244
|
+
}
|
|
245
|
+
const root = omega0 * Math.sqrt(zeta * zeta - 1);
|
|
246
|
+
const r1 = -zeta * omega0 + root;
|
|
247
|
+
const r2 = -zeta * omega0 - root;
|
|
248
|
+
const c1 = (r2 - v0) / (r2 - r1);
|
|
249
|
+
const c2 = (v0 - r1) / (r2 - r1);
|
|
250
|
+
return 1 - (c1 * Math.exp(r1 * seconds) + c2 * Math.exp(r2 * seconds));
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Time for the exponential envelope to fall inside epsilon. The overdamped
|
|
254
|
+
// tail is slower than its envelope, so solve that branch numerically instead.
|
|
255
|
+
const settleSeconds = () => {
|
|
256
|
+
if (zeta > 0) {
|
|
257
|
+
const estimate = -Math.log(eps) / (zeta * omega0);
|
|
258
|
+
if (zeta < 1) return estimate;
|
|
259
|
+
}
|
|
260
|
+
let seconds = 0.001;
|
|
261
|
+
while (seconds < 10 && Math.abs(position(seconds) - 1) > eps) seconds += 0.001;
|
|
262
|
+
return seconds;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const duration = Math.min(
|
|
266
|
+
Math.max(1, Math.round(settleSeconds() * 1000)),
|
|
267
|
+
Math.max(1, Number(maxDuration) || 2000),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
easing: (progress) => (progress >= 1 ? 1 : position(progress * (duration / 1000))),
|
|
272
|
+
duration,
|
|
273
|
+
overshoot: zeta < 1,
|
|
274
|
+
};
|
|
275
|
+
};
|
package/src/animations/index.js
CHANGED
|
@@ -16,6 +16,8 @@ export { MatrixRain } from "./MatrixRain.js";
|
|
|
16
16
|
export {
|
|
17
17
|
animate,
|
|
18
18
|
animateAsync,
|
|
19
|
+
animateSpring,
|
|
20
|
+
animateSpringAsync,
|
|
19
21
|
stopAnimation,
|
|
20
22
|
fadeOut,
|
|
21
23
|
fadeIn,
|
|
@@ -24,3 +26,22 @@ export {
|
|
|
24
26
|
fadeInAsync,
|
|
25
27
|
pulseAsync,
|
|
26
28
|
} from "./animate.js";
|
|
29
|
+
export {
|
|
30
|
+
slideIn,
|
|
31
|
+
slideInAsync,
|
|
32
|
+
slideOut,
|
|
33
|
+
slideOutAsync,
|
|
34
|
+
popIn,
|
|
35
|
+
popInAsync,
|
|
36
|
+
shake,
|
|
37
|
+
shakeAsync,
|
|
38
|
+
} from "./transitions.js";
|
|
39
|
+
export {
|
|
40
|
+
prefersReducedMotion,
|
|
41
|
+
getMotionMode,
|
|
42
|
+
setMotionMode,
|
|
43
|
+
onMotionModeChange,
|
|
44
|
+
motionDuration,
|
|
45
|
+
stagger,
|
|
46
|
+
} from "./motion.js";
|
|
47
|
+
export { EASING_NAMES, cubicBezier, spring } from "./easings.js";
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motion.js - The one motion preference the whole app shares.
|
|
3
|
+
*
|
|
4
|
+
* `prefers-reduced-motion` is a real accessibility requirement, not a style
|
|
5
|
+
* note: vestibular disorders make parallax, spin and large-scale movement
|
|
6
|
+
* physically unpleasant. FletBox answers it at the engine level rather than in
|
|
7
|
+
* each widget, because an animation that asks the widget to opt in is an
|
|
8
|
+
* animation that ships without the check.
|
|
9
|
+
*
|
|
10
|
+
* Three modes:
|
|
11
|
+
* - `"auto"` (default) — follow the operating system.
|
|
12
|
+
* - `"reduce"` — no movement, regardless of what the OS says.
|
|
13
|
+
* - `"full"` — always animate, for an app that owns a "motion" setting.
|
|
14
|
+
*
|
|
15
|
+
* `markMotionLoop(element)` is the shared hook for anything that writes a CSS
|
|
16
|
+
* `animation`: a JS check is read at build time and cannot reach a loop already
|
|
17
|
+
* running, so the preference is also answered by one injected media rule.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is lazy: `window` and `matchMedia` are touched inside the
|
|
20
|
+
* functions, never at module scope, so `flet-box` stays importable in Node.
|
|
21
|
+
*
|
|
22
|
+
* @module animations/motion
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const MODES = new Set(["auto", "reduce", "full"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The mode an app or the previous conversation settled on. `auto` means
|
|
29
|
+
* "whatever the system says", which is what a screen shipped without an
|
|
30
|
+
* opinion about motion should do.
|
|
31
|
+
* @type {"auto"|"reduce"|"full"}
|
|
32
|
+
*/
|
|
33
|
+
let mode = "auto";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Listeners notified when the mode changes, so a widget holding a live loop can
|
|
37
|
+
* stop it the moment the user turns motion off instead of waiting to unmount.
|
|
38
|
+
* @type {Set<(reduced: boolean) => void>}
|
|
39
|
+
*/
|
|
40
|
+
const listeners = new Set();
|
|
41
|
+
|
|
42
|
+
/** Cached `MediaQueryList` for the system preference, created on first read. */
|
|
43
|
+
let systemQuery = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reads the system preference, wiring the change listener once.
|
|
47
|
+
*
|
|
48
|
+
* @returns {boolean} True when the OS asks for reduced motion.
|
|
49
|
+
*/
|
|
50
|
+
const systemPrefersReduce = () => {
|
|
51
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (!systemQuery) {
|
|
55
|
+
systemQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
56
|
+
// The user can flip the OS setting while the app is open; notifying keeps
|
|
57
|
+
// `prefersReducedMotion()` honest without a poll.
|
|
58
|
+
if (typeof systemQuery.addEventListener === "function") {
|
|
59
|
+
systemQuery.addEventListener("change", () => notify());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return systemQuery.matches === true;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {boolean} reduced - The value every listener should now act on.
|
|
67
|
+
* @returns {void}
|
|
68
|
+
*/
|
|
69
|
+
const notify = (reduced) => {
|
|
70
|
+
const value = reduced === undefined ? prefersReducedMotion() : reduced;
|
|
71
|
+
listeners.forEach((listener) => {
|
|
72
|
+
try {
|
|
73
|
+
listener(value);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.warn("⚠️ [motion] A motion-mode listener threw:", error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether anything animated right now should skip its movement.
|
|
82
|
+
*
|
|
83
|
+
* @returns {boolean} True for `"reduce"`, and for `"auto"` when the system asks.
|
|
84
|
+
*/
|
|
85
|
+
export const prefersReducedMotion = () =>
|
|
86
|
+
mode === "reduce" || (mode === "auto" && systemPrefersReduce());
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The active mode.
|
|
90
|
+
*
|
|
91
|
+
* @returns {"auto"|"reduce"|"full"}
|
|
92
|
+
*/
|
|
93
|
+
export const getMotionMode = () => mode;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Sets the app-wide motion mode.
|
|
97
|
+
*
|
|
98
|
+
* `"reduce"` is what a settings switch turns on; `"full"` is what an app with an
|
|
99
|
+
* "animations" toggle turns on for a user who wants motion even though their OS
|
|
100
|
+
* says otherwise. Anything unrecognized leaves the mode untouched and warns.
|
|
101
|
+
*
|
|
102
|
+
* @param {"auto"|"reduce"|"full"} next - The mode to apply.
|
|
103
|
+
* @returns {"auto"|"reduce"|"full"} The mode now in effect.
|
|
104
|
+
*/
|
|
105
|
+
export const setMotionMode = (next) => {
|
|
106
|
+
if (!MODES.has(next)) {
|
|
107
|
+
console.warn(
|
|
108
|
+
`⚠️ [motion] Unknown motion mode "${next}". Use "auto", "reduce" or "full".`,
|
|
109
|
+
);
|
|
110
|
+
return mode;
|
|
111
|
+
}
|
|
112
|
+
if (next === mode) return mode;
|
|
113
|
+
mode = next;
|
|
114
|
+
notify();
|
|
115
|
+
return mode;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Subscribes to motion-mode changes (including the system flipping its own
|
|
120
|
+
* setting while the app is open).
|
|
121
|
+
*
|
|
122
|
+
* @param {(reduced: boolean) => void} listener - Called with the new answer.
|
|
123
|
+
* @returns {() => void} Unsubscribe — call it from the widget's `onUnmount`.
|
|
124
|
+
*/
|
|
125
|
+
export const onMotionModeChange = (listener) => {
|
|
126
|
+
if (typeof listener !== "function") return () => {};
|
|
127
|
+
listeners.add(listener);
|
|
128
|
+
return () => listeners.delete(listener);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The `id` of the one stylesheet that answers the operating-system preference.
|
|
133
|
+
* @type {string}
|
|
134
|
+
*/
|
|
135
|
+
const REDUCED_MOTION_RULE_ID = "fletbox-reduced-motion";
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Marks an element as carrying a CSS animation, and injects — once — the media
|
|
139
|
+
* rule that stops every such animation when the reader asks for reduced motion.
|
|
140
|
+
*
|
|
141
|
+
* A JS check is read when the widget builds, so it cannot reach an element that
|
|
142
|
+
* is already looping. The OS preference can flip mid-session, and this rule is
|
|
143
|
+
* the browser answering live, with no listener to register and none to leak.
|
|
144
|
+
* The JS path stays: it is what covers an app's own `setMotionMode('reduce')`,
|
|
145
|
+
* which CSS cannot see.
|
|
146
|
+
*
|
|
147
|
+
* @param {HTMLElement} element - The element about to be given a CSS animation.
|
|
148
|
+
* @returns {void}
|
|
149
|
+
*/
|
|
150
|
+
export const markMotionLoop = (element) => {
|
|
151
|
+
if (!element || typeof element.setAttribute !== "function") return;
|
|
152
|
+
element.setAttribute("data-fb-anim", "");
|
|
153
|
+
if (typeof document === "undefined") return;
|
|
154
|
+
if (document.getElementById(REDUCED_MOTION_RULE_ID)) return;
|
|
155
|
+
const style = document.createElement("style");
|
|
156
|
+
style.id = REDUCED_MOTION_RULE_ID;
|
|
157
|
+
style.textContent = `@media (prefers-reduced-motion: reduce) {
|
|
158
|
+
[data-fb-anim] {
|
|
159
|
+
animation-duration: 0.01ms !important;
|
|
160
|
+
animation-delay: 0s !important;
|
|
161
|
+
animation-iteration-count: 1 !important;
|
|
162
|
+
}
|
|
163
|
+
}`;
|
|
164
|
+
document.head.appendChild(style);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The duration an animation should actually run under the current mode.
|
|
169
|
+
*
|
|
170
|
+
* `0` is not a shortcut for "skip it": the engine's single frame writes the end
|
|
171
|
+
* value and resolves, so a state change still lands where the caller expects it
|
|
172
|
+
* and an `await` never hangs. Only the movement disappears.
|
|
173
|
+
*
|
|
174
|
+
* @param {number} duration - The duration the caller asked for, in ms.
|
|
175
|
+
* @returns {number} `0` when motion is reduced, otherwise `duration` sanitized.
|
|
176
|
+
*/
|
|
177
|
+
export const motionDuration = (duration) => {
|
|
178
|
+
const safe = Math.max(0, Number(duration) || 0);
|
|
179
|
+
return prefersReducedMotion() ? 0 : safe;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The delay for item `index` of an entrance, capped so a long list cannot turn
|
|
184
|
+
* into a wait.
|
|
185
|
+
*
|
|
186
|
+
* Staggering is the difference between a screen arriving and a screen loading,
|
|
187
|
+
* and the failure mode is arithmetic: 20 rows at 60ms is a 1.2s entrance. Beyond
|
|
188
|
+
* `max` every item shares the same delay, so the tail appears at once.
|
|
189
|
+
*
|
|
190
|
+
* @param {number} index - Position in the list, 0-based.
|
|
191
|
+
* @param {object} [options]
|
|
192
|
+
* @param {number} [options.start=0] - Delay the first item gets, in ms.
|
|
193
|
+
* @param {number} [options.step=60] - Extra delay per item, in ms.
|
|
194
|
+
* @param {number} [options.max=8] - Index past which the delay stops growing.
|
|
195
|
+
* @returns {number} Delay in ms, ready to use as an `animations[].delay` or a
|
|
196
|
+
* `setTimeout` argument.
|
|
197
|
+
*/
|
|
198
|
+
export const stagger = (index, { start = 0, step = 60, max = 8 } = {}) => {
|
|
199
|
+
const position = Math.max(0, Number(index) || 0);
|
|
200
|
+
const capped = Math.min(position, Math.max(0, Number(max) || 0));
|
|
201
|
+
const safeStart = Math.max(0, Number(start) || 0);
|
|
202
|
+
const safeStep = Math.max(0, Number(step) || 0);
|
|
203
|
+
return safeStart + capped * safeStep;
|
|
204
|
+
};
|