@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,74 @@
|
|
|
1
|
+
import { entrances } from '../../core/reduced';
|
|
2
|
+
import { createScope } from '../../core/scope';
|
|
3
|
+
import { TIMING } from '../../core/timing';
|
|
4
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Siena's marquee: the first child (the group) is copied until the strip is
|
|
8
|
+
* full, plus one; every copy slides its own length in a linear loop, so the
|
|
9
|
+
* strip never shows a seam. Speed: 2 s per item in the group (at most 14).
|
|
10
|
+
* Copies are aria-hidden and inert: readers and keyboards meet the group once.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface MarqueeOptions {
|
|
14
|
+
/** 'x' (default) or 'y' (Siena's vertical strip). */
|
|
15
|
+
axis?: 'x' | 'y';
|
|
16
|
+
/** Seconds per item. */
|
|
17
|
+
speed?: number;
|
|
18
|
+
reverse?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MarqueeInstance extends Instance {
|
|
22
|
+
/** Recount the copies (after the content or size changed). */
|
|
23
|
+
refresh(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createMarquee(root: HTMLElement | null, options: MarqueeOptions = {}): MarqueeInstance {
|
|
27
|
+
const group = (root?.firstElementChild as HTMLElement | null) ?? null;
|
|
28
|
+
if (!root || !group) {
|
|
29
|
+
warnOnce('marquee: needs a group inside');
|
|
30
|
+
return inert({ refresh() {} });
|
|
31
|
+
}
|
|
32
|
+
const t = TIMING.marquee;
|
|
33
|
+
const { axis = 'x', speed = t.perItem, reverse = false } = options;
|
|
34
|
+
const scope = createScope();
|
|
35
|
+
const classes = ['mk-marquee', `mk-marquee--${axis}`, reverse && 'mk-marquee--reverse'].filter(Boolean).join(' ');
|
|
36
|
+
scope.attr(root, 'class', [root.getAttribute('class'), classes].filter(Boolean).join(' '));
|
|
37
|
+
const items = Math.min(group.children.length || 1, t.maxItems);
|
|
38
|
+
root.style.setProperty('--mk-marquee-duration', `${items * speed}s`);
|
|
39
|
+
scope.add(() => {
|
|
40
|
+
root.style.removeProperty('--mk-marquee-duration');
|
|
41
|
+
if (root.getAttribute('style') === '') root.removeAttribute('style');
|
|
42
|
+
});
|
|
43
|
+
let copies: HTMLElement[] = [];
|
|
44
|
+
|
|
45
|
+
const refresh = () => {
|
|
46
|
+
for (const copy of copies.splice(0)) copy.remove();
|
|
47
|
+
if (!entrances()) return;
|
|
48
|
+
const size = axis === 'x' ? group.offsetWidth : group.offsetHeight;
|
|
49
|
+
const room = axis === 'x' ? root.clientWidth : root.clientHeight;
|
|
50
|
+
// Unmeasurable (hidden, not laid out yet): five copies, like Siena.
|
|
51
|
+
const count = size > 0 ? Math.max(1, Math.ceil(room / size)) + 1 : 5;
|
|
52
|
+
for (let i = 0; i < count; i++) {
|
|
53
|
+
const copy = group.cloneNode(true) as HTMLElement;
|
|
54
|
+
copy.setAttribute('aria-hidden', 'true');
|
|
55
|
+
copy.inert = true;
|
|
56
|
+
for (const el of copy.querySelectorAll('[id]')) el.removeAttribute('id');
|
|
57
|
+
root.append(copy);
|
|
58
|
+
copies.push(copy);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
refresh();
|
|
62
|
+
scope.add(() => {
|
|
63
|
+
for (const copy of copies.splice(0)) copy.remove();
|
|
64
|
+
});
|
|
65
|
+
root.dataset.mkReady = '';
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
refresh,
|
|
69
|
+
destroy() {
|
|
70
|
+
scope.dispose();
|
|
71
|
+
delete root.dataset.mkReady;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* marquee — Siena's strip: every copy of the group slides its own length in a linear loop. */
|
|
2
|
+
.mk-marquee {
|
|
3
|
+
display: flex;
|
|
4
|
+
gap: var(--mk-marquee-gap, 0px);
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.mk-marquee--y {
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.mk-marquee > * {
|
|
13
|
+
flex: none;
|
|
14
|
+
animation: mk-marquee-x var(--mk-marquee-duration, 10s) linear infinite;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.mk-marquee--y > * {
|
|
18
|
+
animation-name: mk-marquee-y;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.mk-marquee--reverse > * {
|
|
22
|
+
animation-direction: reverse;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes mk-marquee-x {
|
|
26
|
+
to {
|
|
27
|
+
transform: translateX(calc(-100% - var(--mk-marquee-gap, 0px)));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes mk-marquee-y {
|
|
32
|
+
to {
|
|
33
|
+
transform: translateY(calc(-100% - var(--mk-marquee-gap, 0px)));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@media (prefers-reduced-motion: reduce) {
|
|
38
|
+
.mk-marquee > * {
|
|
39
|
+
animation: none;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# mask-text
|
|
2
|
+
|
|
3
|
+
El texto sale de su máscara: cada línea (o palabra, o letra) vive en un recorte y sube desde abajo (`yPercent 100 → 0`, 1.2 s `expo.out`, retardo 0.1 s, 20 ms entre piezas) cuando el 80 % del bloque está en pantalla. Al salir de la pantalla vuelve a esconderse, así que se repite cada vez que regresa (salvo `once`). SplitText lo vuelve a partir al cambiar el tamaño o al cargar la fuente, algo que OFFFICE no hacía.
|
|
4
|
+
|
|
5
|
+
Origen: OFFFICE (offficestud.io) `masked-type`; Siena usa el mismo gesto en los títulos de película partiendo de 120 %.
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<h2 data-mk="mask-text">Office for future furnishing</h2>
|
|
11
|
+
<p data-mk="mask-text" data-mk-by="words" data-mk-once>Texto que entra palabra a palabra.</p>
|
|
12
|
+
<h1 data-mk="mask-text" data-mk-y="120" data-mk-trigger="load">Savoy</h1>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## React
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const ref = useRef<HTMLHeadingElement>(null);
|
|
19
|
+
const text = useMaskText(ref, { by: 'lines' });
|
|
20
|
+
<h2 ref={ref} data-mk="mask-text">Office for future furnishing</h2>
|
|
21
|
+
// text.current?.hide() antes de cambiar de página
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## API
|
|
25
|
+
|
|
26
|
+
`createMaskText(el, { by, y, duration, ease, delay, stagger, trigger, once })` → `{ play(), hide(), split, destroy }`.
|
|
27
|
+
|
|
28
|
+
| Opción | Por defecto | |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `by` | `'lines'` | `'words'`, `'chars'` |
|
|
31
|
+
| `y` | `100` | % desde el que sube cada pieza |
|
|
32
|
+
| `trigger` | `'scroll'` | `'load'`, `'manual'` |
|
|
33
|
+
| `once` | `false` | no vuelve a esconderse al salir |
|
|
34
|
+
|
|
35
|
+
`hide()` es la salida de OFFFICE: todas bajan a su recorte con 2 ms entre piezas.
|
|
36
|
+
|
|
37
|
+
## Accesibilidad
|
|
38
|
+
|
|
39
|
+
La raíz recibe `aria-label` con el texto completo y las piezas quedan ocultas a lectores (SplitText `aria: 'auto'`); `destroy()` lo deshace. Con movimiento reducido no se parte ni se esconde nada.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { gsap, SplitText, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { entrances } from '../../core/reduced';
|
|
3
|
+
import { TIMING } from '../../core/timing';
|
|
4
|
+
import { shows, thresholds } from '../../core/visible';
|
|
5
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* OFFFICE's masked type (and Siena's case titles): the text is split into lines,
|
|
9
|
+
* words or letters, each inside its own clip, and rises from below it
|
|
10
|
+
* (yPercent 100 → 0, 1.2 s expo.out, 0.1 s delay, 20 ms apart) once 80 % of it
|
|
11
|
+
* is on screen. It hides again when it leaves the screen (unless `once`), so it
|
|
12
|
+
* plays every time it comes back. SplitText re-splits on resize and font load.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface MaskTextOptions {
|
|
16
|
+
by?: 'lines' | 'words' | 'chars';
|
|
17
|
+
/** Start offset in % of each piece (Siena's case titles: 120). */
|
|
18
|
+
y?: number;
|
|
19
|
+
duration?: number;
|
|
20
|
+
ease?: string;
|
|
21
|
+
delay?: number;
|
|
22
|
+
stagger?: number;
|
|
23
|
+
/** 'scroll' (default) plays when 80 % is visible; 'load' now; 'manual' waits for play(). */
|
|
24
|
+
trigger?: 'scroll' | 'load' | 'manual';
|
|
25
|
+
/** Play once and stay (OFFFICE's data-a-mod). */
|
|
26
|
+
once?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MaskTextInstance extends Instance {
|
|
30
|
+
play(): void;
|
|
31
|
+
/** OFFFICE's exit: back down into the clips, 2 ms apart. */
|
|
32
|
+
hide(): void;
|
|
33
|
+
readonly split: SplitText | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CLASSES = { lines: 'mk-mask-line', words: 'mk-mask-word', chars: 'mk-mask-char' };
|
|
37
|
+
const TYPES = { lines: 'lines', words: 'words', chars: 'words,chars' };
|
|
38
|
+
|
|
39
|
+
export function createMaskText(el: HTMLElement | null, options: MaskTextOptions = {}): MaskTextInstance {
|
|
40
|
+
if (!el) {
|
|
41
|
+
warnOnce('mask-text: missing element');
|
|
42
|
+
return inert({ play() {}, hide() {}, split: null });
|
|
43
|
+
}
|
|
44
|
+
const t = TIMING.maskText;
|
|
45
|
+
const {
|
|
46
|
+
by = 'lines',
|
|
47
|
+
y = t.y,
|
|
48
|
+
duration = t.duration,
|
|
49
|
+
ease = 'expo.out',
|
|
50
|
+
delay = t.delay,
|
|
51
|
+
stagger = t.stagger,
|
|
52
|
+
trigger = 'scroll',
|
|
53
|
+
once = false,
|
|
54
|
+
} = options;
|
|
55
|
+
if (!entrances()) {
|
|
56
|
+
el.dataset.mkReady = '';
|
|
57
|
+
return {
|
|
58
|
+
play() {},
|
|
59
|
+
hide() {},
|
|
60
|
+
split: null,
|
|
61
|
+
destroy() {
|
|
62
|
+
delete el.dataset.mkReady;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
registerMotion();
|
|
67
|
+
let shown = false;
|
|
68
|
+
let tween: gsap.core.Tween | null = null;
|
|
69
|
+
let observer: IntersectionObserver | null = null;
|
|
70
|
+
|
|
71
|
+
const split = SplitText.create(el, {
|
|
72
|
+
type: TYPES[by],
|
|
73
|
+
mask: by,
|
|
74
|
+
autoSplit: true,
|
|
75
|
+
aria: 'auto',
|
|
76
|
+
linesClass: CLASSES.lines,
|
|
77
|
+
wordsClass: CLASSES.words,
|
|
78
|
+
charsClass: CLASSES.chars,
|
|
79
|
+
// A new split (resize, fonts) keeps the state the text was in.
|
|
80
|
+
onSplit: (self: SplitText) => {
|
|
81
|
+
tween?.kill();
|
|
82
|
+
gsap.set(self[by], { yPercent: shown ? 0 : y });
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const pieces = () => split[by] as HTMLElement[];
|
|
86
|
+
gsap.set(pieces(), { yPercent: y });
|
|
87
|
+
|
|
88
|
+
const play = () => {
|
|
89
|
+
shown = true;
|
|
90
|
+
tween?.kill();
|
|
91
|
+
tween = gsap.to(pieces(), { yPercent: 0, duration, ease, delay, stagger: { each: stagger, from: 'start' }, overwrite: true });
|
|
92
|
+
};
|
|
93
|
+
const hide = () => {
|
|
94
|
+
shown = false;
|
|
95
|
+
tween?.kill();
|
|
96
|
+
tween = gsap.to(pieces(), { yPercent: y, duration, ease, stagger: { each: t.exitStagger }, overwrite: true });
|
|
97
|
+
};
|
|
98
|
+
const reset = () => {
|
|
99
|
+
shown = false;
|
|
100
|
+
tween?.kill();
|
|
101
|
+
gsap.set(pieces(), { yPercent: y });
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (trigger === 'load') play();
|
|
105
|
+
else if (trigger === 'scroll' && typeof IntersectionObserver === 'function') {
|
|
106
|
+
observer = new IntersectionObserver(
|
|
107
|
+
(entries) => {
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
// 80 % visible — or, for text taller than the screen, 80 % of the screen.
|
|
110
|
+
if (shows(entry, t.threshold)) {
|
|
111
|
+
if (!shown) play();
|
|
112
|
+
if (once) observer?.disconnect();
|
|
113
|
+
} else if (!entry.isIntersecting && shown && !once) reset();
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{ threshold: thresholds(t.threshold), rootMargin: '10px' },
|
|
117
|
+
);
|
|
118
|
+
observer.observe(el);
|
|
119
|
+
}
|
|
120
|
+
el.dataset.mkReady = '';
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
play,
|
|
124
|
+
hide,
|
|
125
|
+
split,
|
|
126
|
+
destroy() {
|
|
127
|
+
observer?.disconnect();
|
|
128
|
+
tween?.kill();
|
|
129
|
+
gsap.killTweensOf(pieces());
|
|
130
|
+
split.revert();
|
|
131
|
+
delete el.dataset.mkReady;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# parallax
|
|
2
|
+
|
|
3
|
+
La foto se mueve dentro de su marco mientras el marco cruza la pantalla: escalada a 1.2 (1.3 con `amount` distinto de 1, salvo en el móvil), viaja de −10 % a +10 % de su altura × `amount`. Sigue al scroll de la página sin retraso (Siena suavizaba la página entera con Lenis; con scroll nativo un suavizado propio deja la foto arrastrándose detrás del marco — `lerp: .1` si lo quieres). Una foto arriba del todo de la página cuenta desde `top top`, así que empieza en −10 %.
|
|
4
|
+
|
|
5
|
+
Origen: Siena Film Foundation, fotos de las páginas de película y About (`translateY(-10·a + p·20·a %) scale(1.2)`).
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<figure data-mk="parallax" style="height: 70vh">
|
|
11
|
+
<img src="foto.jpg" alt="…" style="width: 100%; height: 100%; object-fit: cover">
|
|
12
|
+
</figure>
|
|
13
|
+
<figure data-mk="parallax" data-mk-amount="2">…</figure>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
El marco recorta (`overflow: clip`, lo pone el motor). El objetivo es `[data-mk-parallax-target]`, si no la primera `img`, `video` o `picture`, si no el primer hijo.
|
|
17
|
+
|
|
18
|
+
## React
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
const ref = useRef<HTMLElement>(null);
|
|
22
|
+
useParallax(ref, { amount: 1 });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
`createParallax(root, { amount, lerp, scroll })` → `{ seek(p), destroy }`. Con `scroll: false` solo se mueve con `seek`.
|
|
28
|
+
|
|
29
|
+
## Accesibilidad
|
|
30
|
+
|
|
31
|
+
Con movimiento reducido la foto queda centrada y quieta.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { trackScroll, type ScrollTrack } from '../../core/scroll-track';
|
|
4
|
+
import { TIMING } from '../../core/timing';
|
|
5
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Siena's photo parallax: the frame clips, the picture inside is scaled up
|
|
9
|
+
* (1.2, or 1.3 for a stronger amount on wide screens) and travels from −10 % to
|
|
10
|
+
* +10 % of its height × amount while the frame crosses the screen (a frame at
|
|
11
|
+
* the top of the page counts from "top top", so it starts at −10 %). Target:
|
|
12
|
+
* [data-mk-parallax-target], else the first img / video / picture, else the
|
|
13
|
+
* first child.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface ParallaxOptions {
|
|
17
|
+
amount?: number;
|
|
18
|
+
lerp?: number;
|
|
19
|
+
/** Follow the scroll (default). Off: only seek() moves it. */
|
|
20
|
+
scroll?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ParallaxInstance extends Instance {
|
|
24
|
+
/** Show the state at progress 0…1. */
|
|
25
|
+
seek(progress: number): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createParallax(root: HTMLElement | null, options: ParallaxOptions = {}): ParallaxInstance {
|
|
29
|
+
const target =
|
|
30
|
+
root?.querySelector<HTMLElement>('[data-mk-parallax-target]') ??
|
|
31
|
+
root?.querySelector<HTMLElement>(':scope > img, :scope > video, :scope > picture') ??
|
|
32
|
+
(root?.firstElementChild as HTMLElement | null) ??
|
|
33
|
+
null;
|
|
34
|
+
if (!root || !target) {
|
|
35
|
+
warnOnce('parallax: needs an element with a picture inside');
|
|
36
|
+
return inert({ seek() {} });
|
|
37
|
+
}
|
|
38
|
+
const t = TIMING.parallax;
|
|
39
|
+
const { amount = 1, lerp = t.lerp, scroll = true } = options;
|
|
40
|
+
const phone = typeof matchMedia === 'function' && matchMedia('(max-width: 767px)').matches;
|
|
41
|
+
const scale = amount !== 1 && !phone ? t.strongScale : t.scale;
|
|
42
|
+
registerMotion();
|
|
43
|
+
// Not an entrance: it hides nothing, so only reduced motion stills it.
|
|
44
|
+
const moving = !reduced();
|
|
45
|
+
const originalTransform = target.style.transform;
|
|
46
|
+
const originalOverflow = root.style.overflow;
|
|
47
|
+
root.style.overflow = 'clip';
|
|
48
|
+
|
|
49
|
+
const render = (progress: number) => {
|
|
50
|
+
const p = moving ? progress : 0.5;
|
|
51
|
+
const y = Number((-t.travel * amount + p * 2 * t.travel * amount).toFixed(3));
|
|
52
|
+
target.style.transform = `translateY(${y}%) scale(${scale})`;
|
|
53
|
+
};
|
|
54
|
+
let track: ScrollTrack | null = null;
|
|
55
|
+
if (moving && scroll) {
|
|
56
|
+
const top = root.getBoundingClientRect().top + (window.scrollY || 0);
|
|
57
|
+
track = trackScroll(root, { start: top < 20 ? 'top top' : 'top bottom', lerp, onUpdate: render });
|
|
58
|
+
} else render(0.5);
|
|
59
|
+
root.dataset.mkReady = '';
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
seek(progress: number) {
|
|
63
|
+
if (track) track.set(progress);
|
|
64
|
+
else render(progress);
|
|
65
|
+
},
|
|
66
|
+
destroy() {
|
|
67
|
+
track?.destroy();
|
|
68
|
+
target.style.transform = originalTransform;
|
|
69
|
+
root.style.overflow = originalOverflow;
|
|
70
|
+
for (const el of [target, root]) if (el.getAttribute('style') === '') el.removeAttribute('style');
|
|
71
|
+
delete root.dataset.mkReady;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# pile
|
|
2
|
+
|
|
3
|
+
Un proyecto como pila de 2–3 capas giradas que escalan con rebote al entrar en pantalla y reaccionan al hover.
|
|
4
|
+
|
|
5
|
+
Origen: spencergabor.com — Overview / GridEntry.
|
|
6
|
+
|
|
7
|
+
## Vanilla / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<button class="mk-pile" data-mk="pile">
|
|
11
|
+
<span class="mk-pile__stack">
|
|
12
|
+
<!-- capas traseras primero; style generado con pileStyle(pileLayers(n, seed)[i]) -->
|
|
13
|
+
<span class="mk-pile__layer" style="--mk-rot:-3.1;--mk-px:6.2;--mk-py:10">
|
|
14
|
+
<span class="mk-pile__media"><span class="mk-pile__shadow"></span><span class="mk-pile__plate"></span></span>
|
|
15
|
+
</span>
|
|
16
|
+
<span class="mk-pile__layer">
|
|
17
|
+
<span class="mk-pile__media"><span class="mk-pile__shadow"></span><img src="cover.jpg" alt=""></span>
|
|
18
|
+
</span>
|
|
19
|
+
</span>
|
|
20
|
+
<span>Proyecto · 2026</span>
|
|
21
|
+
</button>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Con `autoInit()` (de `@yewdiel21/motion-kit/auto`) las opciones van como `data-mk-<opción>` en kebab-case. Sin `autoInit`: `createPile(el, opciones)`.
|
|
25
|
+
|
|
26
|
+
## React
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
30
|
+
usePile(ref);
|
|
31
|
+
const layers = pileLayers(2, index); // determinista: igual en servidor y cliente
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Opciones
|
|
35
|
+
|
|
36
|
+
| Opción | Por defecto | Qué hace |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `threshold` | `.25` | Fracción visible para disparar la entrada |
|
|
39
|
+
|
|
40
|
+
## Controles
|
|
41
|
+
|
|
42
|
+
`destroy()`; utilidades `pileLayers(count, seed)` y `pileStyle(layer)`
|
|
43
|
+
|
|
44
|
+
## CSS
|
|
45
|
+
|
|
46
|
+
La entrada (escala .7 → 1 y sombra) es CSS con `--mk-ease-elastic`, activada por `data-mk-inview`.
|
|
47
|
+
|
|
48
|
+
Con `prefers-reduced-motion: reduce` la receta deja el contenido en su estado final, sin animar.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createScope } from '../../core/scope';
|
|
2
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
3
|
+
import { shows, thresholds } from '../../core/visible';
|
|
4
|
+
import type { PileLayer } from './pile-math';
|
|
5
|
+
|
|
6
|
+
export { pileLayers, type PileLayer } from './pile-math';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* spencergabor.com project grid: a pile of two or three rotated layers that
|
|
10
|
+
* scale in (CSS, elastic) once the pile scrolls into view. The rotations are
|
|
11
|
+
* generated once with `pileLayers()` and written into the markup with
|
|
12
|
+
* `pileStyle()`, so server and client render the same pile.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface PileOptions {
|
|
16
|
+
/** Fraction of the pile that must be visible before it scales in. */
|
|
17
|
+
threshold?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type PileInstance = Instance;
|
|
21
|
+
|
|
22
|
+
/** Inline style for a back layer: `style={pileStyle(layer)}`. */
|
|
23
|
+
export const pileStyle = (layer: PileLayer) =>
|
|
24
|
+
`--mk-rot:${layer.rotation.toFixed(2)};--mk-px:${layer.offsetX.toFixed(2)};--mk-py:${layer.offsetY}`;
|
|
25
|
+
|
|
26
|
+
export function createPile(root: HTMLElement | null, options: PileOptions = {}): PileInstance {
|
|
27
|
+
if (!root) {
|
|
28
|
+
warnOnce('pile: missing root');
|
|
29
|
+
return inert();
|
|
30
|
+
}
|
|
31
|
+
const scope = createScope();
|
|
32
|
+
const threshold = Math.min(1, Math.max(0, options.threshold ?? 0.25));
|
|
33
|
+
const io = scope.observe(
|
|
34
|
+
new IntersectionObserver(
|
|
35
|
+
([entry]) => {
|
|
36
|
+
if (!entry || !shows(entry, threshold)) return;
|
|
37
|
+
root.dataset.mkInview = '';
|
|
38
|
+
io.disconnect();
|
|
39
|
+
},
|
|
40
|
+
{ threshold: thresholds(threshold) },
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
io.observe(root);
|
|
44
|
+
root.dataset.mkReady = '';
|
|
45
|
+
scope.add(() => {
|
|
46
|
+
delete root.dataset.mkInview;
|
|
47
|
+
delete root.dataset.mkReady;
|
|
48
|
+
});
|
|
49
|
+
return { destroy: () => scope.dispose() };
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface PileLayer {
|
|
2
|
+
rotation: number;
|
|
3
|
+
offsetX: number;
|
|
4
|
+
offsetY: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function mulberry32(seed: number) {
|
|
8
|
+
let a = seed >>> 0;
|
|
9
|
+
return () => {
|
|
10
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
11
|
+
let t = a;
|
|
12
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
13
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
14
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Spencer's stack transforms, seeded so SSR output is stable. Layer 0 is the back. */
|
|
19
|
+
export function pileLayers(count: number, seed: number): PileLayer[] {
|
|
20
|
+
const rand = mulberry32(seed + 1);
|
|
21
|
+
const range = (min: number, max: number) => rand() * (max - min) + min;
|
|
22
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
23
|
+
rotation: range(2, 5) * ((i % 2) * 2 - 1),
|
|
24
|
+
offsetX: range(4, 10) * (rand() < 0.5 ? -1 : 1),
|
|
25
|
+
offsetY: (count - i) * 5,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* pile — stack recipe from spencergabor.com (GridEntry) */
|
|
2
|
+
|
|
3
|
+
.mk-pile {
|
|
4
|
+
display: grid;
|
|
5
|
+
gap: 2.25rem;
|
|
6
|
+
width: 100%;
|
|
7
|
+
padding: 0;
|
|
8
|
+
border: 0;
|
|
9
|
+
background: none;
|
|
10
|
+
color: inherit;
|
|
11
|
+
text-align: left;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.mk-pile__stack {
|
|
15
|
+
display: grid;
|
|
16
|
+
aspect-ratio: 4 / 3;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.mk-pile__layer {
|
|
20
|
+
position: relative;
|
|
21
|
+
grid-area: 1 / 1;
|
|
22
|
+
border-radius: var(--mk-radius-card);
|
|
23
|
+
rotate: calc(var(--mk-rot, 0) * 0.6deg);
|
|
24
|
+
translate: calc(var(--mk-px, 0) * 0.3%) calc(var(--mk-py, 0) * 0.3%);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.mk-pile__media {
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset: 0;
|
|
30
|
+
display: block;
|
|
31
|
+
border-radius: inherit;
|
|
32
|
+
transition: scale 0.8s var(--mk-ease-elastic);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.mk-pile__shadow {
|
|
36
|
+
position: absolute;
|
|
37
|
+
inset: 0;
|
|
38
|
+
border-radius: inherit;
|
|
39
|
+
background: rgb(0 0 0 / 0.05);
|
|
40
|
+
box-shadow: var(--mk-shadow-soft);
|
|
41
|
+
transition: opacity 0.8s var(--mk-ease-elastic);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.mk-pile__media > img,
|
|
45
|
+
.mk-pile__plate {
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset: 0;
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
border-radius: inherit;
|
|
51
|
+
object-fit: cover;
|
|
52
|
+
background: var(--mk-surface);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media (any-hover: hover) {
|
|
56
|
+
.mk-pile:hover .mk-pile__layer:nth-last-child(1) .mk-pile__media { scale: 1.03; }
|
|
57
|
+
.mk-pile:hover .mk-pile__layer:nth-last-child(2) .mk-pile__media { scale: 1.02; }
|
|
58
|
+
.mk-pile:hover .mk-pile__layer:nth-last-child(3) .mk-pile__media { scale: 1.01; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.mk-pile:active .mk-pile__layer:nth-last-child(1) .mk-pile__media { scale: 0.97; }
|
|
62
|
+
.mk-pile:active .mk-pile__layer:nth-last-child(2) .mk-pile__media { scale: 0.98; }
|
|
63
|
+
.mk-pile:active .mk-pile__layer:nth-last-child(3) .mk-pile__media { scale: 0.99; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# pill
|
|
2
|
+
|
|
3
|
+
Botón píldora con escala elástica al pasar (1.05 + sombra) y al pulsar (.95), y `.mk-stretch` para enlaces que se estiran al pasar.
|
|
4
|
+
|
|
5
|
+
Origen: spencergabor.com — Button.vue.
|
|
6
|
+
|
|
7
|
+
## Vanilla / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<button class="mk-pill">Enviar mensaje</button>
|
|
11
|
+
<a class="mk-stretch" href="#">GitHub</a>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Solo CSS: importa `@yewdiel21/motion-kit/css`.
|
|
15
|
+
|
|
16
|
+
## React
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<button className="mk-pill">Enviar</button>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Controles
|
|
23
|
+
|
|
24
|
+
— (solo CSS)
|
|
25
|
+
|
|
26
|
+
## CSS
|
|
27
|
+
|
|
28
|
+
Colores con `--mk-pill-bg` y `--mk-pill-fg`.
|
|
29
|
+
|
|
30
|
+
Con `prefers-reduced-motion: reduce` la receta deja el contenido en su estado final, sin animar.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* pill — spencergabor.com button: elastic scale and shadow */
|
|
2
|
+
|
|
3
|
+
.mk-pill {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: 0.6rem;
|
|
7
|
+
padding: 1rem 1.75rem;
|
|
8
|
+
border: 0;
|
|
9
|
+
border-radius: 999px;
|
|
10
|
+
background: var(--mk-pill-bg, #0a0a0a);
|
|
11
|
+
color: var(--mk-pill-fg, #fff);
|
|
12
|
+
font: inherit;
|
|
13
|
+
line-height: 1;
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
transition:
|
|
16
|
+
scale 0.8s var(--mk-ease-elastic),
|
|
17
|
+
box-shadow 0.8s var(--mk-ease-elastic),
|
|
18
|
+
opacity 0.2s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.mk-pill:active {
|
|
22
|
+
scale: 0.95;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.mk-pill:disabled {
|
|
26
|
+
opacity: 0.5;
|
|
27
|
+
cursor: progress;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@media (any-hover: hover) {
|
|
31
|
+
.mk-pill:hover {
|
|
32
|
+
scale: 1.05;
|
|
33
|
+
box-shadow: var(--mk-shadow);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Stretch on hover (spencergabor.com footer links) */
|
|
38
|
+
.mk-stretch {
|
|
39
|
+
display: inline-block;
|
|
40
|
+
transform-origin: 50% 100%;
|
|
41
|
+
transition: transform 1s var(--mk-ease-elastic-hover);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@media (any-hover: hover) {
|
|
45
|
+
.mk-stretch:hover {
|
|
46
|
+
transform: scaleY(1.2);
|
|
47
|
+
}
|
|
48
|
+
}
|