@yewdiel21/motion-kit 0.0.0-stage → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +38 -0
- package/README.md +80 -2
- package/bin/motion-kit.mjs +242 -0
- package/css.d.ts +2 -0
- package/dist/auto/index.d.ts +63 -0
- package/dist/auto/index.js +207 -0
- package/dist/auto/index.js.map +1 -0
- package/dist/chunk-2BQGFOA7.js +177 -0
- package/dist/chunk-2BQGFOA7.js.map +1 -0
- package/dist/chunk-34XYOXHG.js +2374 -0
- package/dist/chunk-34XYOXHG.js.map +1 -0
- package/dist/chunk-6OJAP6ZL.js +832 -0
- package/dist/chunk-6OJAP6ZL.js.map +1 -0
- package/dist/chunk-ABNRDDO7.js +28 -0
- package/dist/chunk-ABNRDDO7.js.map +1 -0
- package/dist/chunk-FS2HJVVY.js +3320 -0
- package/dist/chunk-FS2HJVVY.js.map +1 -0
- package/dist/chunk-J27KH2A2.js +157 -0
- package/dist/chunk-J27KH2A2.js.map +1 -0
- package/dist/chunk-KLY5BRRI.js +18 -0
- package/dist/chunk-KLY5BRRI.js.map +1 -0
- package/dist/chunk-Q4LVFNLN.js +1617 -0
- package/dist/chunk-Q4LVFNLN.js.map +1 -0
- package/dist/chunk-RFDIZYFX.js +304 -0
- package/dist/chunk-RFDIZYFX.js.map +1 -0
- package/dist/chunk-TGV27ZWM.js +17 -0
- package/dist/chunk-TGV27ZWM.js.map +1 -0
- package/dist/confetti-BSX1CLLV.d.ts +41 -0
- package/dist/css/components/chat-background.css +87 -0
- package/dist/css/components/chat.css +237 -0
- package/dist/css/components/cursor.css +178 -0
- package/dist/css/components/dialog.css +259 -0
- package/dist/css/components/island.css +301 -0
- package/dist/css/components/menu.css +105 -0
- package/dist/css/components/nav.css +1054 -0
- package/dist/css/components/steps.css +50 -0
- package/dist/css/components/theme-picker.css +115 -0
- package/dist/css/components/toast.css +115 -0
- package/dist/css/controls.css +950 -0
- package/dist/css/effects.css +587 -0
- package/dist/css/fonts.css +3 -0
- package/dist/css/themes/black-green.css +126 -0
- package/dist/css/themes/black.css +126 -0
- package/dist/css/themes/blue.css +126 -0
- package/dist/css/themes/brown.css +126 -0
- package/dist/css/themes/cold-blue.css +126 -0
- package/dist/css/themes/funny.css +126 -0
- package/dist/css/themes/gray.css +126 -0
- package/dist/css/themes/green.css +126 -0
- package/dist/css/themes/modern-1.css +126 -0
- package/dist/css/themes/neogreen.css +126 -0
- package/dist/css/themes/new.css +126 -0
- package/dist/css/themes/new2.css +126 -0
- package/dist/css/themes/oled.css +126 -0
- package/dist/css/themes/orange.css +126 -0
- package/dist/css/themes/pastel-pink.css +126 -0
- package/dist/css/themes/pink.css +126 -0
- package/dist/css/themes/purple.css +126 -0
- package/dist/css/themes/red.css +126 -0
- package/dist/css/themes/super-blue.css +126 -0
- package/dist/css/themes.css +2431 -0
- package/dist/css/tokens.css +452 -0
- package/dist/elements/index.d.ts +183 -0
- package/dist/elements/index.js +472 -0
- package/dist/elements/index.js.map +1 -0
- package/dist/engine-CUr9QijM.d.ts +46 -0
- package/dist/gl/index.d.ts +61 -0
- package/dist/gl/index.js +18 -0
- package/dist/gl/index.js.map +1 -0
- package/dist/index.css +5296 -0
- package/dist/index.d.ts +440 -0
- package/dist/index.js +390 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +476 -0
- package/dist/react/index.js +969 -0
- package/dist/react/index.js.map +1 -0
- package/dist/reduced-BM4Ls0g5.d.ts +566 -0
- package/dist/reveal-WFUWDrei.d.ts +416 -0
- package/dist/store-Bqy_obnD.d.ts +195 -0
- package/dist/theme/generate.d.ts +18 -0
- package/dist/theme/generate.js +64 -0
- package/dist/theme/generate.js.map +1 -0
- package/dist/theme/index.d.ts +17 -0
- package/dist/theme/index.js +32 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme-BFMoCzi6.d.ts +158 -0
- package/dist/types-BkS_raL8.d.ts +7 -0
- package/package.json +114 -3
- package/registry.json +469 -0
- package/skills/motion-kit/SKILL.md +189 -0
- package/skills/motion-kit/reference/components.md +86 -0
- package/skills/motion-kit/reference/design.md +92 -0
- package/skills/motion-kit/reference/recipes.md +172 -0
- package/src/auto/auto-init.ts +151 -0
- package/src/auto/index.ts +3 -0
- package/src/auto/parse-options.ts +68 -0
- package/src/components/chat/README.md +90 -0
- package/src/components/chat/chat.css +236 -0
- package/src/components/chat/element.ts +98 -0
- package/src/components/chat/engine.ts +295 -0
- package/src/components/chat/react.tsx +196 -0
- package/src/components/chat-background/README.md +50 -0
- package/src/components/chat-background/audio.ts +56 -0
- package/src/components/chat-background/chat-background.css +86 -0
- package/src/components/chat-background/element.ts +31 -0
- package/src/components/chat-background/engine.ts +108 -0
- package/src/components/chat-background/react.tsx +36 -0
- package/src/components/cursor/README.md +53 -0
- package/src/components/cursor/cursor.css +177 -0
- package/src/components/cursor/element.ts +23 -0
- package/src/components/cursor/engine.ts +254 -0
- package/src/components/cursor/react.tsx +26 -0
- package/src/components/dialog/README.md +106 -0
- package/src/components/dialog/dialog.css +258 -0
- package/src/components/dialog/element.ts +118 -0
- package/src/components/dialog/engine.ts +432 -0
- package/src/components/dialog/react.tsx +200 -0
- package/src/components/icon.tsx +10 -0
- package/src/components/icons.ts +24 -0
- package/src/components/island/README.md +91 -0
- package/src/components/island/element.ts +45 -0
- package/src/components/island/engine.ts +200 -0
- package/src/components/island/island.css +300 -0
- package/src/components/island/react.tsx +115 -0
- package/src/components/menu/README.md +67 -0
- package/src/components/menu/element.ts +55 -0
- package/src/components/menu/engine.ts +142 -0
- package/src/components/menu/menu.css +104 -0
- package/src/components/menu/react.tsx +82 -0
- package/src/components/nav/README.md +85 -0
- package/src/components/nav/element.ts +39 -0
- package/src/components/nav/engine.ts +164 -0
- package/src/components/nav/nav.css +1053 -0
- package/src/components/nav/react.tsx +90 -0
- package/src/components/steps/README.md +70 -0
- package/src/components/steps/element.ts +43 -0
- package/src/components/steps/engine.ts +185 -0
- package/src/components/steps/react.tsx +91 -0
- package/src/components/steps/steps.css +49 -0
- package/src/components/theme-picker/README.md +57 -0
- package/src/components/theme-picker/element.ts +20 -0
- package/src/components/theme-picker/engine.ts +91 -0
- package/src/components/theme-picker/react.tsx +31 -0
- package/src/components/theme-picker/theme-picker.css +114 -0
- package/src/components/toast/README.md +60 -0
- package/src/components/toast/element.ts +25 -0
- package/src/components/toast/engine.ts +483 -0
- package/src/components/toast/react.tsx +22 -0
- package/src/components/toast/store.ts +122 -0
- package/src/components/toast/toast.css +114 -0
- package/src/core/channels.ts +165 -0
- package/src/core/curves.ts +35 -0
- package/src/core/frame.ts +12 -0
- package/src/core/gsap.ts +65 -0
- package/src/core/loop.ts +44 -0
- package/src/core/math.ts +9 -0
- package/src/core/physics-spring.ts +59 -0
- package/src/core/pointer.ts +63 -0
- package/src/core/reduced.ts +21 -0
- package/src/core/scope.ts +70 -0
- package/src/core/scroll-track.ts +106 -0
- package/src/core/split.ts +68 -0
- package/src/core/spring.ts +22 -0
- package/src/core/styles.ts +31 -0
- package/src/core/timing.ts +204 -0
- package/src/core/types.ts +20 -0
- package/src/core/visible.ts +19 -0
- package/src/css/base.css +142 -0
- package/src/css/controls/README.md +122 -0
- package/src/css/controls/burger.css +67 -0
- package/src/css/controls/button.css +226 -0
- package/src/css/controls/checkbox.css +53 -0
- package/src/css/controls/chip.css +85 -0
- package/src/css/controls/field.css +124 -0
- package/src/css/controls/glass.css +220 -0
- package/src/css/controls/icon.css +26 -0
- package/src/css/controls/segmented.css +60 -0
- package/src/css/controls/switch.css +72 -0
- package/src/css/default-theme.css +123 -0
- package/src/css/effects/README.md +134 -0
- package/src/css/effects/anim.css +224 -0
- package/src/css/effects/fade.css +15 -0
- package/src/css/effects/float.css +64 -0
- package/src/css/effects/frost.css +62 -0
- package/src/css/effects/grain.css +58 -0
- package/src/css/effects/loading.css +60 -0
- package/src/css/effects/success.css +22 -0
- package/src/css/effects/text.css +67 -0
- package/src/css/fonts.css +3 -0
- package/src/css/states.css +51 -0
- package/src/css/themes/black-green.css +126 -0
- package/src/css/themes/black.css +126 -0
- package/src/css/themes/blue.css +126 -0
- package/src/css/themes/brown.css +126 -0
- package/src/css/themes/cold-blue.css +126 -0
- package/src/css/themes/funny.css +126 -0
- package/src/css/themes/gray.css +126 -0
- package/src/css/themes/green.css +126 -0
- package/src/css/themes/modern-1.css +126 -0
- package/src/css/themes/neogreen.css +126 -0
- package/src/css/themes/new.css +126 -0
- package/src/css/themes/new2.css +126 -0
- package/src/css/themes/oled.css +126 -0
- package/src/css/themes/orange.css +126 -0
- package/src/css/themes/pastel-pink.css +126 -0
- package/src/css/themes/pink.css +126 -0
- package/src/css/themes/purple.css +126 -0
- package/src/css/themes/red.css +126 -0
- package/src/css/themes/super-blue.css +126 -0
- package/src/css/tokens.css +182 -0
- package/src/elements/base.ts +76 -0
- package/src/elements/index.ts +48 -0
- package/src/gl/index.ts +15 -0
- package/src/gl/liquid-blend/README.md +23 -0
- package/src/gl/liquid-blend/create-liquid-blend.ts +7 -0
- package/src/gl/liquid-blend/shader.ts +9 -0
- package/src/gl/matcap/README.md +23 -0
- package/src/gl/matcap/create-matcap.ts +7 -0
- package/src/gl/matcap/shader.ts +16 -0
- package/src/gl/particle-hero/README.md +41 -0
- package/src/gl/particle-hero/create-particle-hero.ts +544 -0
- package/src/gl/particle-hero/particle-hero.css +19 -0
- package/src/gl/particle-hero/shaders.ts +84 -0
- package/src/gl/shader-blur/README.md +23 -0
- package/src/gl/shader-blur/create-shader-blur.ts +7 -0
- package/src/gl/shader-blur/shader.ts +11 -0
- package/src/gl/shader-grain/README.md +23 -0
- package/src/gl/shader-grain/create-shader-grain.ts +7 -0
- package/src/gl/shader-grain/shader.ts +6 -0
- package/src/gl/shared/create-shader-effect.ts +186 -0
- package/src/gl/shared/shader.css +3 -0
- package/src/gl/shared/shaders.ts +30 -0
- package/src/index.ts +177 -0
- package/src/motion/anchor.ts +45 -0
- package/src/motion/chat.ts +97 -0
- package/src/motion/confetti.ts +130 -0
- package/src/motion/count.ts +95 -0
- package/src/motion/details.ts +70 -0
- package/src/motion/drag-dismiss.ts +231 -0
- package/src/motion/effects.ts +81 -0
- package/src/motion/entrances.ts +27 -0
- package/src/motion/floating.ts +389 -0
- package/src/motion/flow.ts +115 -0
- package/src/motion/indicator.ts +44 -0
- package/src/motion/layout.ts +39 -0
- package/src/motion/liquid.ts +227 -0
- package/src/motion/morph.ts +540 -0
- package/src/motion/reveal.ts +112 -0
- package/src/motion/sheet.ts +65 -0
- package/src/motion/surface.ts +108 -0
- package/src/react/animated-number.tsx +30 -0
- package/src/react/flip-group.tsx +44 -0
- package/src/react/frost.tsx +33 -0
- package/src/react/index.ts +80 -0
- package/src/react/slot-text.tsx +44 -0
- package/src/react/theme-provider.tsx +42 -0
- package/src/react/use-card-dock.ts +9 -0
- package/src/react/use-cascade.ts +9 -0
- package/src/react/use-contrast.ts +9 -0
- package/src/react/use-counter.ts +8 -0
- package/src/react/use-curved-carousel.ts +10 -0
- package/src/react/use-depth-grid.ts +9 -0
- package/src/react/use-details.ts +7 -0
- package/src/react/use-dock-pill.ts +10 -0
- package/src/react/use-drag-ruler.ts +10 -0
- package/src/react/use-fill-text.ts +9 -0
- package/src/react/use-fit-text.ts +8 -0
- package/src/react/use-flow.ts +8 -0
- package/src/react/use-frame-zoom.ts +9 -0
- package/src/react/use-infinite-reel.ts +8 -0
- package/src/react/use-letters.ts +9 -0
- package/src/react/use-liquid-blend.ts +8 -0
- package/src/react/use-marquee.ts +9 -0
- package/src/react/use-mask-text.ts +9 -0
- package/src/react/use-matcap.ts +8 -0
- package/src/react/use-parallax.ts +9 -0
- package/src/react/use-particle-hero.ts +9 -0
- package/src/react/use-pile.ts +8 -0
- package/src/react/use-pop.ts +8 -0
- package/src/react/use-radial-menu.ts +10 -0
- package/src/react/use-recipe.ts +38 -0
- package/src/react/use-reflow.ts +9 -0
- package/src/react/use-repel-bento.ts +8 -0
- package/src/react/use-reveal.ts +8 -0
- package/src/react/use-rotator.ts +10 -0
- package/src/react/use-shader-blur.ts +8 -0
- package/src/react/use-shader-grain.ts +8 -0
- package/src/react/use-slot-text.ts +17 -0
- package/src/react/use-snap-strip.ts +9 -0
- package/src/react/use-sortable.ts +14 -0
- package/src/react/use-spring-cards.ts +8 -0
- package/src/react/use-stretch-footer.ts +20 -0
- package/src/react/use-tabs.ts +10 -0
- package/src/react/use-text-roll.ts +9 -0
- package/src/react/use-ticket-menu.ts +15 -0
- package/src/react/use-words.ts +8 -0
- package/src/recipes/card-dock/README.md +36 -0
- package/src/recipes/card-dock/card-dock.css +178 -0
- package/src/recipes/card-dock/create-card-dock.ts +97 -0
- package/src/recipes/cascade/README.md +33 -0
- package/src/recipes/cascade/create-cascade.ts +111 -0
- package/src/recipes/contrast/README.md +29 -0
- package/src/recipes/contrast/create-contrast.ts +68 -0
- package/src/recipes/counter/README.md +42 -0
- package/src/recipes/counter/create-counter.ts +84 -0
- package/src/recipes/curved-carousel/README.md +50 -0
- package/src/recipes/curved-carousel/carousel-math.ts +20 -0
- package/src/recipes/curved-carousel/create-curved-carousel.ts +257 -0
- package/src/recipes/curved-carousel/curved-carousel.css +50 -0
- package/src/recipes/depth-grid/README.md +30 -0
- package/src/recipes/depth-grid/create-depth-grid.ts +120 -0
- package/src/recipes/details/README.md +31 -0
- package/src/recipes/details/create-details.ts +27 -0
- package/src/recipes/dock-pill/README.md +23 -0
- package/src/recipes/dock-pill/create-dock-pill.ts +68 -0
- package/src/recipes/dock-pill/dock-pill.css +5 -0
- package/src/recipes/drag-ruler/README.md +21 -0
- package/src/recipes/drag-ruler/create-drag-ruler.ts +95 -0
- package/src/recipes/drag-ruler/drag-ruler.css +16 -0
- package/src/recipes/fill-text/README.md +27 -0
- package/src/recipes/fill-text/create-fill-text.ts +93 -0
- package/src/recipes/fit-text/README.md +37 -0
- package/src/recipes/fit-text/create-fit-text.ts +55 -0
- package/src/recipes/fit-text/fit-text.css +6 -0
- package/src/recipes/flow/README.md +46 -0
- package/src/recipes/flow/create-flow.ts +55 -0
- package/src/recipes/frame-zoom/README.md +29 -0
- package/src/recipes/frame-zoom/create-frame-zoom.ts +91 -0
- package/src/recipes/infinite-reel/README.md +23 -0
- package/src/recipes/infinite-reel/create-infinite-reel.ts +140 -0
- package/src/recipes/infinite-reel/infinite-reel.css +8 -0
- package/src/recipes/letters/README.md +30 -0
- package/src/recipes/letters/create-letters.ts +113 -0
- package/src/recipes/marquee/README.md +32 -0
- package/src/recipes/marquee/create-marquee.ts +74 -0
- package/src/recipes/marquee/marquee.css +41 -0
- package/src/recipes/mask-text/README.md +39 -0
- package/src/recipes/mask-text/create-mask-text.ts +134 -0
- package/src/recipes/parallax/README.md +31 -0
- package/src/recipes/parallax/create-parallax.ts +74 -0
- package/src/recipes/pile/README.md +48 -0
- package/src/recipes/pile/create-pile.ts +50 -0
- package/src/recipes/pile/pile-math.ts +27 -0
- package/src/recipes/pile/pile.css +63 -0
- package/src/recipes/pill/README.md +30 -0
- package/src/recipes/pill/pill.css +48 -0
- package/src/recipes/pop/README.md +36 -0
- package/src/recipes/pop/create-pop.ts +52 -0
- package/src/recipes/radial-menu/README.md +44 -0
- package/src/recipes/radial-menu/create-radial-menu.ts +96 -0
- package/src/recipes/radial-menu/radial-menu.css +45 -0
- package/src/recipes/reflow/README.md +47 -0
- package/src/recipes/reflow/create-reflow.ts +49 -0
- package/src/recipes/repel-bento/README.md +21 -0
- package/src/recipes/repel-bento/create-repel-bento.ts +65 -0
- package/src/recipes/repel-bento/repel-bento.css +3 -0
- package/src/recipes/reveal/README.md +36 -0
- package/src/recipes/reveal/create-reveal.ts +48 -0
- package/src/recipes/rotator/README.md +21 -0
- package/src/recipes/rotator/create-rotator.ts +66 -0
- package/src/recipes/rotator/rotator.css +3 -0
- package/src/recipes/slot-text/README.md +44 -0
- package/src/recipes/slot-text/create-slot-text.ts +244 -0
- package/src/recipes/slot-text/slot-text.css +42 -0
- package/src/recipes/snap-strip/README.md +31 -0
- package/src/recipes/snap-strip/create-snap-strip.ts +133 -0
- package/src/recipes/snap-strip/snap-strip.css +23 -0
- package/src/recipes/sortable/README.md +88 -0
- package/src/recipes/sortable/create-sortable.ts +863 -0
- package/src/recipes/sortable/sortable.css +40 -0
- package/src/recipes/spring-cards/README.md +48 -0
- package/src/recipes/spring-cards/create-spring-cards.ts +140 -0
- package/src/recipes/spring-cards/spring-cards.css +23 -0
- package/src/recipes/stretch-footer/README.md +44 -0
- package/src/recipes/stretch-footer/create-stretch-footer.ts +69 -0
- package/src/recipes/stretch-footer/stretch-footer.css +28 -0
- package/src/recipes/tabs/README.md +39 -0
- package/src/recipes/tabs/create-tabs.ts +91 -0
- package/src/recipes/tabs/tabs.css +19 -0
- package/src/recipes/text-roll/README.md +35 -0
- package/src/recipes/text-roll/create-text-roll.ts +137 -0
- package/src/recipes/text-roll/text-roll.css +21 -0
- package/src/recipes/ticket-menu/README.md +24 -0
- package/src/recipes/ticket-menu/create-ticket-menu.ts +6 -0
- package/src/recipes/words/README.md +43 -0
- package/src/recipes/words/create-words.ts +114 -0
- package/src/theme/css-format.ts +32 -0
- package/src/theme/generate.ts +72 -0
- package/src/theme/index.ts +15 -0
- package/src/theme/presets.ts +79 -0
- package/src/theme/theme.ts +119 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { reduced } from '../core/reduced';
|
|
2
|
+
import { TIMING } from '../core/timing';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Dousan's success confetti: 45 paper pieces burst from a point (2–8 px a
|
|
6
|
+
* frame, a little upward), fall with gravity .15, slow by .98 a frame, spin and
|
|
7
|
+
* fade (.015–.03 a frame). Physics per 60 fps frame, scaled by the real frame
|
|
8
|
+
* time. The canvas is decorative and removes itself when the last piece fades.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface ConfettiOptions {
|
|
12
|
+
count?: number;
|
|
13
|
+
/** Default: the theme's primary, tertiary, secondary and containers. */
|
|
14
|
+
colors?: string[];
|
|
15
|
+
/** Draw inside this (positioned) element instead of over the whole window. */
|
|
16
|
+
container?: HTMLElement | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ConfettiHandle {
|
|
20
|
+
stop(): void;
|
|
21
|
+
finished: Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FALLBACK = ['#3DDC84', '#A3E8C1', '#E8D0A3', '#1A73E8', '#8AB4F8'];
|
|
25
|
+
const ROLES = ['primary', 'tertiary', 'secondary', 'primary-container', 'tertiary-container'];
|
|
26
|
+
const between = ([min, max]: readonly [number, number]) => min + Math.random() * (max - min);
|
|
27
|
+
|
|
28
|
+
function themeColors(from: Element) {
|
|
29
|
+
const style = getComputedStyle(from);
|
|
30
|
+
const colors = ROLES.map((role) => style.getPropertyValue(`--md-sys-color-${role}`).trim()).filter(Boolean);
|
|
31
|
+
return colors.length ? colors : FALLBACK;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function confetti(origin?: Element | { x: number; y: number } | null, options: ConfettiOptions = {}): ConfettiHandle {
|
|
35
|
+
const idle: ConfettiHandle = { stop() {}, finished: Promise.resolve() };
|
|
36
|
+
if (typeof document === 'undefined' || reduced()) return idle;
|
|
37
|
+
const canvas = document.createElement('canvas');
|
|
38
|
+
const context = canvas.getContext('2d');
|
|
39
|
+
if (!context) return idle;
|
|
40
|
+
const config = TIMING.confetti;
|
|
41
|
+
const container = options.container ?? null;
|
|
42
|
+
const box = container ? container.getBoundingClientRect() : { left: 0, top: 0, width: innerWidth, height: innerHeight };
|
|
43
|
+
const ratio = window.devicePixelRatio || 1;
|
|
44
|
+
canvas.width = Math.max(1, Math.round(box.width * ratio));
|
|
45
|
+
canvas.height = Math.max(1, Math.round(box.height * ratio));
|
|
46
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
47
|
+
canvas.dataset.mkConfetti = '';
|
|
48
|
+
Object.assign(canvas.style, {
|
|
49
|
+
position: container ? 'absolute' : 'fixed',
|
|
50
|
+
inset: '0',
|
|
51
|
+
width: container ? '100%' : '100vw',
|
|
52
|
+
height: container ? '100%' : '100vh',
|
|
53
|
+
zIndex: '2147483647',
|
|
54
|
+
pointerEvents: 'none',
|
|
55
|
+
});
|
|
56
|
+
context.scale(ratio, ratio);
|
|
57
|
+
|
|
58
|
+
let x = box.width / 2;
|
|
59
|
+
let y = box.height / 2;
|
|
60
|
+
if (origin instanceof Element) {
|
|
61
|
+
const rect = origin.getBoundingClientRect();
|
|
62
|
+
x = rect.left + rect.width / 2 - box.left;
|
|
63
|
+
y = rect.top + rect.height / 2 - box.top;
|
|
64
|
+
} else if (origin) {
|
|
65
|
+
x = origin.x - box.left;
|
|
66
|
+
y = origin.y - box.top;
|
|
67
|
+
}
|
|
68
|
+
const colors = options.colors?.length
|
|
69
|
+
? options.colors
|
|
70
|
+
: themeColors(origin instanceof Element ? origin : (container ?? document.documentElement));
|
|
71
|
+
const pieces = Array.from({ length: options.count ?? config.count }, () => {
|
|
72
|
+
const angle = Math.random() * Math.PI * 2;
|
|
73
|
+
const speed = between(config.speed);
|
|
74
|
+
return {
|
|
75
|
+
x,
|
|
76
|
+
y,
|
|
77
|
+
vx: Math.cos(angle) * speed,
|
|
78
|
+
vy: Math.sin(angle) * speed - config.lift,
|
|
79
|
+
radius: between(config.radius),
|
|
80
|
+
color: colors[Math.floor(Math.random() * colors.length)]!,
|
|
81
|
+
alpha: 1,
|
|
82
|
+
decay: between(config.decay),
|
|
83
|
+
rotation: Math.random() * 360,
|
|
84
|
+
spin: (Math.random() * 2 - 1) * config.spin,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let frame = 0;
|
|
89
|
+
let last = 0;
|
|
90
|
+
let stopped = false;
|
|
91
|
+
let resolve!: () => void;
|
|
92
|
+
const finished = new Promise<void>((done) => (resolve = done));
|
|
93
|
+
const stop = () => {
|
|
94
|
+
if (stopped) return;
|
|
95
|
+
stopped = true;
|
|
96
|
+
cancelAnimationFrame(frame);
|
|
97
|
+
canvas.remove();
|
|
98
|
+
resolve();
|
|
99
|
+
};
|
|
100
|
+
const draw = (now: number) => {
|
|
101
|
+
const f = last ? Math.min((now - last) / (1000 / 60), 3) : 1;
|
|
102
|
+
last = now;
|
|
103
|
+
context.clearRect(0, 0, box.width, box.height);
|
|
104
|
+
let alive = false;
|
|
105
|
+
const drag = Math.pow(config.drag, f);
|
|
106
|
+
for (const piece of pieces) {
|
|
107
|
+
if (piece.alpha <= 0) continue;
|
|
108
|
+
alive = true;
|
|
109
|
+
piece.x += piece.vx * f;
|
|
110
|
+
piece.y += piece.vy * f;
|
|
111
|
+
piece.vy += config.gravity * f;
|
|
112
|
+
piece.vx *= drag;
|
|
113
|
+
piece.vy *= drag;
|
|
114
|
+
piece.alpha -= piece.decay * f;
|
|
115
|
+
piece.rotation += piece.spin * f;
|
|
116
|
+
context.save();
|
|
117
|
+
context.translate(piece.x, piece.y);
|
|
118
|
+
context.rotate((piece.rotation * Math.PI) / 180);
|
|
119
|
+
context.globalAlpha = Math.max(0, piece.alpha);
|
|
120
|
+
context.fillStyle = piece.color;
|
|
121
|
+
context.fillRect(-piece.radius, -piece.radius / 2, piece.radius * 2, piece.radius);
|
|
122
|
+
context.restore();
|
|
123
|
+
}
|
|
124
|
+
if (alive && !stopped) frame = requestAnimationFrame(draw);
|
|
125
|
+
else stop();
|
|
126
|
+
};
|
|
127
|
+
(container ?? document.body).append(canvas);
|
|
128
|
+
frame = requestAnimationFrame(draw);
|
|
129
|
+
return { stop, finished };
|
|
130
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../core/gsap';
|
|
2
|
+
import { reduced } from '../core/reduced';
|
|
3
|
+
import { TIMING } from '../core/timing';
|
|
4
|
+
import { requestFrame } from '../core/frame';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Numbers that count to their new value: Stepbro's animated number (`numberSoft`,
|
|
8
|
+
* 0.6 s; 0.7 s on prices) and the onboarding's revealed balance (rAF, 0.9 s,
|
|
9
|
+
* ease-out cubic). Both continue from the value currently shown.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type NumberFormat = (value: number) => string;
|
|
13
|
+
export interface CountOptions {
|
|
14
|
+
/** Start value. Default: the last value counted on this element, else the number in its text. */
|
|
15
|
+
from?: number;
|
|
16
|
+
duration?: number;
|
|
17
|
+
format?: NumberFormat;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const shown = new WeakMap<Element, number>();
|
|
21
|
+
const tweens = new WeakMap<Element, gsap.core.Tween>();
|
|
22
|
+
const frames = new WeakMap<Element, () => void>();
|
|
23
|
+
const defaultFormat: NumberFormat = (value) => String(Math.round(value));
|
|
24
|
+
|
|
25
|
+
function current(el: Element): number {
|
|
26
|
+
const known = shown.get(el);
|
|
27
|
+
if (known !== undefined) return known;
|
|
28
|
+
const parsed = Number((el.textContent ?? '').replace(/[^\d.-]/g, ''));
|
|
29
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Tween a plain number (no DOM): `onUpdate` gets each value. Reduced motion jumps to `to`. */
|
|
33
|
+
export function tweenNumber(from: number, to: number, onUpdate: (value: number) => void, { duration = TIMING.number }: { duration?: number } = {}) {
|
|
34
|
+
registerMotion();
|
|
35
|
+
if (from === to || reduced()) {
|
|
36
|
+
onUpdate(to);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const state = { value: from };
|
|
40
|
+
return gsap.to(state, { value: to, duration, ease: EASE.flow, onUpdate: () => onUpdate(state.value), onComplete: () => onUpdate(to) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function countTo(el: HTMLElement, to: number, { from, duration = TIMING.number, format = defaultFormat }: CountOptions = {}) {
|
|
44
|
+
registerMotion();
|
|
45
|
+
tweens.get(el)?.kill();
|
|
46
|
+
const start = from ?? current(el);
|
|
47
|
+
const write = (value: number) => {
|
|
48
|
+
shown.set(el, value);
|
|
49
|
+
el.textContent = format(value);
|
|
50
|
+
};
|
|
51
|
+
if (start === to || reduced()) {
|
|
52
|
+
write(to);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const state = { value: start };
|
|
56
|
+
const tween = gsap.to(state, {
|
|
57
|
+
value: to,
|
|
58
|
+
duration,
|
|
59
|
+
ease: EASE.flow,
|
|
60
|
+
onUpdate: () => write(state.value),
|
|
61
|
+
onComplete: () => {
|
|
62
|
+
write(to);
|
|
63
|
+
tweens.delete(el);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
tweens.set(el, tween);
|
|
67
|
+
return tween;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Count with requestAnimationFrame and ease-out cubic. Returns a cancel function. */
|
|
71
|
+
export function revealCounter(el: HTMLElement, to: number, { from, duration = TIMING.revealCounter, format = defaultFormat }: CountOptions = {}) {
|
|
72
|
+
frames.get(el)?.();
|
|
73
|
+
const start = from ?? current(el);
|
|
74
|
+
const write = (value: number) => {
|
|
75
|
+
shown.set(el, value);
|
|
76
|
+
el.textContent = format(value);
|
|
77
|
+
};
|
|
78
|
+
if (start === to || reduced()) {
|
|
79
|
+
write(to);
|
|
80
|
+
return () => {};
|
|
81
|
+
}
|
|
82
|
+
let began: number | null = null;
|
|
83
|
+
let cancel = () => {};
|
|
84
|
+
const step = (time: number) => {
|
|
85
|
+
began ??= time;
|
|
86
|
+
const progress = Math.min(1, (time - began) / (duration * 1000));
|
|
87
|
+
write(start + (to - start) * (1 - (1 - progress) ** 3));
|
|
88
|
+
if (progress < 1) cancel = requestFrame(step);
|
|
89
|
+
else frames.delete(el);
|
|
90
|
+
};
|
|
91
|
+
cancel = requestFrame(step);
|
|
92
|
+
const stop = () => cancel();
|
|
93
|
+
frames.set(el, stop);
|
|
94
|
+
return stop;
|
|
95
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../core/gsap';
|
|
2
|
+
import { reduced } from '../core/reduced';
|
|
3
|
+
import { TIMING } from '../core/timing';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Stepbro's PrettyDetails: a disclosure grows to its content's height while the
|
|
7
|
+
* content fades in, and folds back to its summary (0.32 s, signature). A
|
|
8
|
+
* `[data-mk-chevron]` inside the summary turns 180° in 0.25 s.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const running = new WeakMap<HTMLDetailsElement, gsap.core.Timeline>();
|
|
12
|
+
|
|
13
|
+
export function toggleDetails(details: HTMLDetailsElement, open: boolean, { duration = TIMING.details }: { duration?: number } = {}) {
|
|
14
|
+
registerMotion();
|
|
15
|
+
const summary = details.querySelector<HTMLElement>(':scope > summary');
|
|
16
|
+
const chevron = summary?.querySelector<HTMLElement>('[data-mk-chevron]');
|
|
17
|
+
running.get(details)?.kill();
|
|
18
|
+
if (!summary || reduced()) {
|
|
19
|
+
details.open = open;
|
|
20
|
+
if (chevron) gsap.set(chevron, { rotation: open ? 180 : 0 });
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const from = details.offsetHeight;
|
|
24
|
+
const content = [...details.children].filter((child): child is HTMLElement => child !== summary && child instanceof HTMLElement);
|
|
25
|
+
// Both ends measured without painting them; it stays open while it animates.
|
|
26
|
+
details.style.height = '';
|
|
27
|
+
details.open = false;
|
|
28
|
+
const closed = details.offsetHeight;
|
|
29
|
+
details.open = true;
|
|
30
|
+
const to = open ? details.offsetHeight : closed;
|
|
31
|
+
const timeline = gsap.timeline({
|
|
32
|
+
onComplete: () => {
|
|
33
|
+
if (!open) details.open = false;
|
|
34
|
+
gsap.set(details, { clearProps: 'height,overflow,boxSizing' });
|
|
35
|
+
gsap.set(content, { clearProps: 'opacity' });
|
|
36
|
+
running.delete(details);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (from !== to) {
|
|
40
|
+
timeline.fromTo(details, { height: from, overflow: 'hidden', boxSizing: 'border-box' }, { height: to, duration, ease: EASE.signature }, 0);
|
|
41
|
+
}
|
|
42
|
+
timeline.fromTo(content, { opacity: open ? 0 : 1 }, { opacity: open ? 1 : 0, duration, ease: EASE.signature }, 0);
|
|
43
|
+
if (chevron) timeline.to(chevron, { rotation: open ? 180 : 0, duration: TIMING.chevron, ease: EASE.signature }, 0);
|
|
44
|
+
running.set(details, timeline);
|
|
45
|
+
return timeline;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Animate every disclosure under `root` with one listener. Controls inside a
|
|
50
|
+
* summary keep their clicks; `data-mk-motion="none"` opts a disclosure out.
|
|
51
|
+
* Returns the function that removes the listener.
|
|
52
|
+
*/
|
|
53
|
+
export function installDetailsMotion(root: Document | HTMLElement = document): () => void {
|
|
54
|
+
const onClick = (event: Event) => {
|
|
55
|
+
const mouse = event as MouseEvent;
|
|
56
|
+
if (mouse.defaultPrevented || mouse.button !== 0) return;
|
|
57
|
+
const target = mouse.target instanceof Element ? mouse.target : null;
|
|
58
|
+
const summary = target?.closest('summary');
|
|
59
|
+
const details = summary?.parentElement;
|
|
60
|
+
if (!summary || !(details instanceof HTMLDetailsElement)) return;
|
|
61
|
+
if (details.querySelector(':scope > summary') !== summary) return;
|
|
62
|
+
if (details.dataset.mkMotion === 'none') return;
|
|
63
|
+
const control = target?.closest('a, button, input, select, textarea');
|
|
64
|
+
if (control && control !== summary && summary.contains(control)) return;
|
|
65
|
+
mouse.preventDefault();
|
|
66
|
+
toggleDetails(details, !details.open);
|
|
67
|
+
};
|
|
68
|
+
root.addEventListener('click', onClick);
|
|
69
|
+
return () => root.removeEventListener('click', onClick);
|
|
70
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../core/gsap';
|
|
2
|
+
import { reduced } from '../core/reduced';
|
|
3
|
+
import { TIMING } from '../core/timing';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Stepbro's modal drag (ported from Kayro): a surface follows a drag (or a pull
|
|
7
|
+
* with the wheel) at half speed and shrinks as it goes down (1 − y/400); past
|
|
8
|
+
* 100 px it is dismissed, otherwise it springs back (elastic.out(.5,.5), 0.5 s).
|
|
9
|
+
* A drag starts after 6 px, ignores controls, scrollable areas that can still
|
|
10
|
+
* scroll, popovers and [data-mk-no-drag]; with a [data-mk-drag-handle] only the
|
|
11
|
+
* handle drags. The wheel dismisses at speed 1.5 past 16 px, or past 100 px.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const START = 6;
|
|
15
|
+
const FOLLOW = 0.5;
|
|
16
|
+
const DISMISS = 100;
|
|
17
|
+
const SHRINK = 400;
|
|
18
|
+
const WHEEL_SPEED = 1.5;
|
|
19
|
+
const WHEEL_MIN = 16;
|
|
20
|
+
const WHEEL_IDLE = 90;
|
|
21
|
+
const WHEEL_COOLDOWN = 250;
|
|
22
|
+
const CONTROLS = [
|
|
23
|
+
'a[href]',
|
|
24
|
+
'button',
|
|
25
|
+
'input',
|
|
26
|
+
'select',
|
|
27
|
+
'textarea',
|
|
28
|
+
'label',
|
|
29
|
+
'summary',
|
|
30
|
+
'[contenteditable="true"]',
|
|
31
|
+
'[role="button"]',
|
|
32
|
+
'[role="slider"]',
|
|
33
|
+
'[role="combobox"]',
|
|
34
|
+
'[role="option"]',
|
|
35
|
+
'[role="checkbox"]',
|
|
36
|
+
'[role="menuitem"]',
|
|
37
|
+
].join(',');
|
|
38
|
+
|
|
39
|
+
/** The nearest element between `target` and `surface` that scrolls vertically. */
|
|
40
|
+
function scrollerOf(target: EventTarget | null, surface: HTMLElement) {
|
|
41
|
+
let element = target instanceof Element ? target : null;
|
|
42
|
+
while (element) {
|
|
43
|
+
if (element.scrollHeight > element.clientHeight + 1) {
|
|
44
|
+
const overflow = getComputedStyle(element).overflowY;
|
|
45
|
+
if (overflow === 'auto' || overflow === 'scroll') return element;
|
|
46
|
+
}
|
|
47
|
+
if (element === surface) break;
|
|
48
|
+
element = element.parentElement;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const draggableFrom = (target: EventTarget | null, surface: HTMLElement) =>
|
|
54
|
+
target instanceof Element &&
|
|
55
|
+
!target.closest('[data-mk-no-drag], [popover]') &&
|
|
56
|
+
!target.closest(CONTROLS) &&
|
|
57
|
+
!(surface.querySelector('[data-mk-drag-handle]') && !target.closest('[data-mk-drag-handle]'));
|
|
58
|
+
|
|
59
|
+
export interface DragDismissOptions {
|
|
60
|
+
/** Whether a gesture may start now (not while opening, saving or floating). */
|
|
61
|
+
canDrag?: () => boolean;
|
|
62
|
+
onDismiss: () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Let `surface` be dragged away. Returns the function that detaches it. */
|
|
66
|
+
export function attachDragDismiss(surface: HTMLElement, { canDrag = () => true, onDismiss }: DragDismissOptions) {
|
|
67
|
+
registerMotion();
|
|
68
|
+
let pointer: number | null = null;
|
|
69
|
+
let startX = 0;
|
|
70
|
+
let startY = 0;
|
|
71
|
+
let mode: 'drag' | 'scroll' | null = null;
|
|
72
|
+
let pulled = 0;
|
|
73
|
+
let base = { x: 0, y: 0, scale: 1 };
|
|
74
|
+
let target: EventTarget | null = null;
|
|
75
|
+
|
|
76
|
+
const noSelect = (event: Event) => event.preventDefault();
|
|
77
|
+
const begin = () => {
|
|
78
|
+
mode = 'drag';
|
|
79
|
+
gsap.killTweensOf(surface, 'x,y,scale');
|
|
80
|
+
base = {
|
|
81
|
+
x: Number(gsap.getProperty(surface, 'x')) || 0,
|
|
82
|
+
y: Number(gsap.getProperty(surface, 'y')) || 0,
|
|
83
|
+
scale: Number(gsap.getProperty(surface, 'scaleX')) || 1,
|
|
84
|
+
};
|
|
85
|
+
window.getSelection?.()?.removeAllRanges();
|
|
86
|
+
document.body.style.userSelect = 'none';
|
|
87
|
+
document.addEventListener('selectstart', noSelect);
|
|
88
|
+
};
|
|
89
|
+
const follow = (dx: number, dy: number) => {
|
|
90
|
+
pulled = dy * FOLLOW;
|
|
91
|
+
gsap.set(surface, { x: base.x + dx * FOLLOW, y: base.y + pulled, scale: Math.max(0.1, base.scale * (1 - pulled / SHRINK)) });
|
|
92
|
+
};
|
|
93
|
+
const restore = () => gsap.to(surface, { x: 0, y: 0, scale: 1, duration: TIMING.drag.restore, ease: 'elastic.out(0.5,0.5)' });
|
|
94
|
+
const release = () => {
|
|
95
|
+
document.removeEventListener('selectstart', noSelect);
|
|
96
|
+
document.body.style.userSelect = '';
|
|
97
|
+
const dismiss = pulled > DISMISS;
|
|
98
|
+
mode = null;
|
|
99
|
+
pulled = 0;
|
|
100
|
+
if (dismiss) onDismiss();
|
|
101
|
+
else restore();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
105
|
+
if (event.button !== 0 || pointer !== null || reduced() || !canDrag()) return;
|
|
106
|
+
if (!draggableFrom(event.target, surface)) return;
|
|
107
|
+
pointer = event.pointerId;
|
|
108
|
+
target = event.target;
|
|
109
|
+
startX = event.clientX;
|
|
110
|
+
startY = event.clientY;
|
|
111
|
+
mode = null;
|
|
112
|
+
pulled = 0;
|
|
113
|
+
};
|
|
114
|
+
const onPointerMove = (event: PointerEvent) => {
|
|
115
|
+
if (event.pointerId !== pointer || mode === 'scroll') return;
|
|
116
|
+
const dx = event.clientX - startX;
|
|
117
|
+
const dy = event.clientY - startY;
|
|
118
|
+
if (mode === null) {
|
|
119
|
+
if (Math.abs(dx) < START && Math.abs(dy) < START) return;
|
|
120
|
+
if (event.pointerType !== 'mouse') {
|
|
121
|
+
// A touch decides between scrolling the content and dragging the surface.
|
|
122
|
+
const scroller = scrollerOf(target, surface);
|
|
123
|
+
if (scroller && Math.abs(dx) < Math.abs(dy) && !(dy > 0 && scroller.scrollTop <= 0)) {
|
|
124
|
+
mode = 'scroll';
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
begin();
|
|
129
|
+
}
|
|
130
|
+
if (event.cancelable) event.preventDefault();
|
|
131
|
+
follow(dx, dy);
|
|
132
|
+
};
|
|
133
|
+
const onPointerUp = (event: PointerEvent) => {
|
|
134
|
+
if (event.pointerId !== pointer) return;
|
|
135
|
+
pointer = null;
|
|
136
|
+
target = null;
|
|
137
|
+
if (mode !== 'drag') {
|
|
138
|
+
mode = null;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// The click that ends a drag does not reach what is under the pointer.
|
|
142
|
+
const swallow = (click: Event) => {
|
|
143
|
+
click.stopPropagation();
|
|
144
|
+
click.preventDefault();
|
|
145
|
+
};
|
|
146
|
+
window.addEventListener('click', swallow, { capture: true, once: true });
|
|
147
|
+
setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 50);
|
|
148
|
+
release();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Pulling down with the wheel (scrolling up past the top) drags it as well.
|
|
152
|
+
let wheeling = false;
|
|
153
|
+
let wheelPull = 0;
|
|
154
|
+
let wheelSpeed = 0;
|
|
155
|
+
let lastWheel = 0;
|
|
156
|
+
let idle: ReturnType<typeof setTimeout> | undefined;
|
|
157
|
+
let quietUntil = 0;
|
|
158
|
+
const deltaOf = (event: WheelEvent) =>
|
|
159
|
+
event.deltaMode === 1 ? event.deltaY * 16 : event.deltaMode === 2 ? event.deltaY * innerHeight : event.deltaY;
|
|
160
|
+
function endWheel(dismiss: boolean, at: number) {
|
|
161
|
+
window.removeEventListener('wheel', onWheelCapture, { capture: true });
|
|
162
|
+
clearTimeout(idle);
|
|
163
|
+
wheeling = false;
|
|
164
|
+
quietUntil = at + WHEEL_COOLDOWN;
|
|
165
|
+
pulled = 0;
|
|
166
|
+
wheelPull = 0;
|
|
167
|
+
wheelSpeed = 0;
|
|
168
|
+
lastWheel = 0;
|
|
169
|
+
document.body.style.userSelect = '';
|
|
170
|
+
if (dismiss) onDismiss();
|
|
171
|
+
else restore();
|
|
172
|
+
}
|
|
173
|
+
function stepWheel(event: WheelEvent) {
|
|
174
|
+
const amount = -deltaOf(event);
|
|
175
|
+
const elapsed = lastWheel ? Math.max(1, event.timeStamp - lastWheel) : 16;
|
|
176
|
+
lastWheel = event.timeStamp;
|
|
177
|
+
wheelSpeed = wheelSpeed * 0.4 + (amount / elapsed) * 0.6;
|
|
178
|
+
wheelPull = Math.max(0, wheelPull + amount);
|
|
179
|
+
follow(0, wheelPull);
|
|
180
|
+
if ((wheelSpeed > WHEEL_SPEED && pulled > WHEEL_MIN) || pulled > DISMISS) {
|
|
181
|
+
endWheel(true, event.timeStamp);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
clearTimeout(idle);
|
|
185
|
+
idle = setTimeout(() => wheeling && endWheel(false, performance.now()), WHEEL_IDLE);
|
|
186
|
+
}
|
|
187
|
+
function onWheelCapture(event: WheelEvent) {
|
|
188
|
+
if (!wheeling) return;
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
event.stopPropagation();
|
|
191
|
+
stepWheel(event);
|
|
192
|
+
}
|
|
193
|
+
const onWheel = (event: WheelEvent) => {
|
|
194
|
+
if (wheeling || reduced() || !canDrag()) return;
|
|
195
|
+
const up = deltaOf(event) < 0;
|
|
196
|
+
const scroller = scrollerOf(event.target, surface);
|
|
197
|
+
const canScroll = scroller
|
|
198
|
+
? up
|
|
199
|
+
? scroller.scrollTop > 0
|
|
200
|
+
: scroller.scrollTop + scroller.clientHeight < scroller.scrollHeight - 1
|
|
201
|
+
: false;
|
|
202
|
+
if (canScroll || !up || event.timeStamp < quietUntil) return;
|
|
203
|
+
if (event.target instanceof Element && event.target.closest('[data-mk-no-drag], [popover]')) return;
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
wheeling = true;
|
|
206
|
+
wheelPull = 0;
|
|
207
|
+
wheelSpeed = 0;
|
|
208
|
+
lastWheel = 0;
|
|
209
|
+
begin();
|
|
210
|
+
mode = null;
|
|
211
|
+
window.addEventListener('wheel', onWheelCapture, { passive: false, capture: true });
|
|
212
|
+
stepWheel(event);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
surface.addEventListener('pointerdown', onPointerDown);
|
|
216
|
+
window.addEventListener('pointermove', onPointerMove, { passive: false });
|
|
217
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
218
|
+
window.addEventListener('pointercancel', onPointerUp);
|
|
219
|
+
surface.addEventListener('wheel', onWheel, { passive: false });
|
|
220
|
+
return () => {
|
|
221
|
+
surface.removeEventListener('pointerdown', onPointerDown);
|
|
222
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
223
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
224
|
+
window.removeEventListener('pointercancel', onPointerUp);
|
|
225
|
+
surface.removeEventListener('wheel', onWheel);
|
|
226
|
+
window.removeEventListener('wheel', onWheelCapture, { capture: true });
|
|
227
|
+
clearTimeout(idle);
|
|
228
|
+
document.removeEventListener('selectstart', noSelect);
|
|
229
|
+
document.body.style.userSelect = '';
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { reduced } from '../core/reduced';
|
|
2
|
+
import { TIMING } from '../core/timing';
|
|
3
|
+
import { warnOnce } from '../core/types';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Play Stepbro's utility animations (css/effects/anim.css) from code: animate()
|
|
7
|
+
* restarts one even when it is already playing and resolves when it ends;
|
|
8
|
+
* leave() plays an exit and then removes (or hides) the element.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const ANIMATIONS = [
|
|
12
|
+
'out',
|
|
13
|
+
'out-left',
|
|
14
|
+
'out-bottom',
|
|
15
|
+
'collapse',
|
|
16
|
+
'highlight',
|
|
17
|
+
'shake',
|
|
18
|
+
'shake-strong',
|
|
19
|
+
'fade',
|
|
20
|
+
'wipe',
|
|
21
|
+
'progress-in',
|
|
22
|
+
] as const;
|
|
23
|
+
export type AnimationName = (typeof ANIMATIONS)[number];
|
|
24
|
+
export type ExitName = 'out' | 'out-left' | 'out-bottom' | 'collapse';
|
|
25
|
+
|
|
26
|
+
const EXITS = new Set<AnimationName>(['out', 'out-left', 'out-bottom', 'collapse']);
|
|
27
|
+
|
|
28
|
+
export interface AnimateOptions {
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function animate(el: Element, name: AnimationName, { signal }: AnimateOptions = {}): Promise<void> {
|
|
33
|
+
const className = `mk-anim-${name}`;
|
|
34
|
+
if (!ANIMATIONS.includes(name)) {
|
|
35
|
+
warnOnce(`animate: unknown animation "${String(name)}" (use ${ANIMATIONS.join(', ')})`);
|
|
36
|
+
return Promise.resolve();
|
|
37
|
+
}
|
|
38
|
+
if (reduced()) return Promise.resolve();
|
|
39
|
+
const element = el as HTMLElement;
|
|
40
|
+
if (name === 'collapse') element.style.setProperty('--mk-collapse-from', `${element.offsetHeight}px`);
|
|
41
|
+
// Off, reflow, on: the browser starts the animation over.
|
|
42
|
+
element.classList.remove(className);
|
|
43
|
+
void element.offsetWidth;
|
|
44
|
+
element.classList.add(className);
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const finish = (keep: boolean) => {
|
|
47
|
+
element.removeEventListener('animationend', onEnd);
|
|
48
|
+
signal?.removeEventListener('abort', onAbort);
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
if (!keep) element.classList.remove(className);
|
|
51
|
+
resolve();
|
|
52
|
+
};
|
|
53
|
+
const onEnd = (event: Event) => {
|
|
54
|
+
if ((event as AnimationEvent).animationName === `mk-${name}`) finish(EXITS.has(name));
|
|
55
|
+
};
|
|
56
|
+
const onAbort = () => finish(false);
|
|
57
|
+
element.addEventListener('animationend', onEnd);
|
|
58
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
59
|
+
const timer = setTimeout(() => finish(EXITS.has(name)), TIMING.effects[name] * 1000 + 100);
|
|
60
|
+
if (signal?.aborted) onAbort();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LeaveOptions {
|
|
65
|
+
/** Remove the element (default) or hide it. */
|
|
66
|
+
remove?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function leave(el: Element, name: ExitName, { remove = true }: LeaveOptions = {}): Promise<void> {
|
|
70
|
+
const focused = el.contains(document.activeElement) ? (document.activeElement as HTMLElement) : null;
|
|
71
|
+
await animate(el, name);
|
|
72
|
+
focused?.blur();
|
|
73
|
+
if (remove) {
|
|
74
|
+
el.remove();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
(el as HTMLElement).hidden = true;
|
|
78
|
+
el.classList.remove(`mk-anim-${name}`);
|
|
79
|
+
(el as HTMLElement).style.removeProperty('--mk-collapse-from');
|
|
80
|
+
if (el.getAttribute('style') === '') el.removeAttribute('style');
|
|
81
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TIMING } from '../core/timing';
|
|
2
|
+
import { flowIn } from './flow';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Named entrances from Stepbro (money/replication-catalog.json): the same
|
|
6
|
+
* flowIn with each surface's own scale, blur and rhythm.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Navigation menu options: grow from 90 % out of 4 px blur, last first, after 0.1 s. */
|
|
10
|
+
export const flowInMenu = (targets: gsap.TweenTarget) =>
|
|
11
|
+
flowIn(targets, { style: 2, startScale: 0.9, blur: 4, reverse: true, autoAlpha: false, ...TIMING.menu });
|
|
12
|
+
|
|
13
|
+
/** Theme picker swatches: pop in from half size, 10 ms apart. */
|
|
14
|
+
export const flowInPalette = (targets: gsap.TweenTarget) =>
|
|
15
|
+
flowIn(targets, { style: 2, startScale: 0.5, blur: 4, autoAlpha: false, ...TIMING.palette });
|
|
16
|
+
|
|
17
|
+
/** Form cards (`[data-flow-item]`): grow from 90 % out of 8 px blur. */
|
|
18
|
+
export const flowInForm = (targets: gsap.TweenTarget) =>
|
|
19
|
+
flowIn(targets, { style: 2, startScale: 0.9, blur: 8, ...TIMING.form });
|
|
20
|
+
|
|
21
|
+
/** Landing pieces: grow from 85 % out of 24 px blur, after 0.15 s. */
|
|
22
|
+
export const flowInLanding = (targets: gsap.TweenTarget) =>
|
|
23
|
+
flowIn(targets, { style: 2, startScale: 0.85, blur: 24, ...TIMING.landing });
|
|
24
|
+
|
|
25
|
+
/** Long-form content (`[data-flow]`, markdown blocks): style 2, 0.6 s, 60 ms apart. */
|
|
26
|
+
export const flowInContent = (root: Element) =>
|
|
27
|
+
flowIn(root, { style: 2, target: '[data-flow], .markdown-content > *', ...TIMING.content });
|