@yewdiel21/motion-kit 0.9.0 → 0.10.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 +3 -35
- package/README.md +93 -81
- package/dist/auto/index.d.ts +22 -3
- package/dist/auto/index.js +43 -5
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-KLY5BRRI.js → chunk-34EZ7DF4.js} +1 -1
- package/dist/chunk-34EZ7DF4.js.map +1 -0
- package/dist/{chunk-RFDIZYFX.js → chunk-DOOMULVV.js} +20 -20
- package/dist/chunk-DOOMULVV.js.map +1 -0
- package/dist/{chunk-ABNRDDO7.js → chunk-I3DXF2KQ.js} +2 -2
- package/dist/{chunk-Q4LVFNLN.js → chunk-IKQ5FODN.js} +4 -4
- package/dist/chunk-IKQ5FODN.js.map +1 -0
- package/dist/{chunk-2BQGFOA7.js → chunk-J6OCFNGU.js} +2 -2
- package/dist/chunk-J6OCFNGU.js.map +1 -0
- package/dist/{chunk-6OJAP6ZL.js → chunk-QFBOHPWZ.js} +3 -3
- package/dist/chunk-QFBOHPWZ.js.map +1 -0
- package/dist/{chunk-J27KH2A2.js → chunk-RVSKEBIC.js} +1 -1
- package/dist/chunk-RVSKEBIC.js.map +1 -0
- package/dist/{chunk-34XYOXHG.js → chunk-TFNRIZHU.js} +1615 -12
- package/dist/chunk-TFNRIZHU.js.map +1 -0
- package/dist/{chunk-FS2HJVVY.js → chunk-XAKC2QY7.js} +1204 -5
- package/dist/chunk-XAKC2QY7.js.map +1 -0
- package/dist/css/components/calendar.css +159 -0
- package/dist/css/components/chat-background.css +2 -2
- package/dist/css/components/chat.css +1 -1
- package/dist/css/components/cursor.css +3 -3
- package/dist/css/components/dialog.css +3 -3
- package/dist/css/components/hover-card.css +14 -0
- package/dist/css/components/input-otp.css +10 -0
- package/dist/css/components/island.css +1 -1
- package/dist/css/components/menu.css +1 -1
- package/dist/css/components/nav.css +1 -5
- package/dist/css/components/pagination.css +11 -0
- package/dist/css/components/quantity.css +16 -0
- package/dist/css/components/slot-picker.css +126 -0
- package/dist/css/components/steps.css +2 -2
- package/dist/css/components/studio-dock.css +35 -0
- package/dist/css/components/theme-picker.css +1 -1
- package/dist/css/components/toast.css +1 -1
- package/dist/css/components/trailer.css +37 -0
- package/dist/css/controls.css +78 -13
- package/dist/css/effects.css +8 -8
- package/dist/css/fonts.css +1 -1
- package/dist/css/themes/black-green.css +1 -1
- package/dist/css/themes/black.css +1 -1
- package/dist/css/themes/blue.css +1 -1
- package/dist/css/themes/brown.css +1 -1
- package/dist/css/themes/cold-blue.css +1 -1
- package/dist/css/themes/funny.css +1 -1
- package/dist/css/themes/gray.css +1 -1
- package/dist/css/themes/green.css +1 -1
- package/dist/css/themes/modern-1.css +1 -1
- package/dist/css/themes/neogreen.css +1 -1
- package/dist/css/themes/new.css +1 -1
- package/dist/css/themes/new2.css +1 -1
- package/dist/css/themes/oled.css +1 -1
- package/dist/css/themes/orange.css +1 -1
- package/dist/css/themes/pastel-pink.css +1 -1
- package/dist/css/themes/pink.css +1 -1
- package/dist/css/themes/purple.css +1 -1
- package/dist/css/themes/red.css +1 -1
- package/dist/css/themes/super-blue.css +1 -1
- package/dist/css/themes.css +19 -19
- package/dist/css/tokens.css +9 -9
- package/dist/elements/index.d.ts +75 -7
- package/dist/elements/index.js +172 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-CUr9QijM.d.ts → engine-GDGwQnJE.d.ts} +2 -2
- package/dist/gl/index.js +3 -3
- package/dist/index.css +1076 -63
- package/dist/index.d.ts +17 -17
- package/dist/index.js +49 -7
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +139 -40
- package/dist/react/index.js +355 -8
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BM4Ls0g5.d.ts → reduced-BmBzKH94.d.ts} +215 -18
- package/dist/{reveal-WFUWDrei.d.ts → reveal-jsX-uwUz.d.ts} +233 -24
- package/dist/{store-Bqy_obnD.d.ts → store-DYb3tTsZ.d.ts} +5 -5
- package/dist/theme/generate.d.ts +1 -1
- package/dist/theme/generate.js +2 -2
- package/dist/theme/generate.js.map +1 -1
- package/dist/theme/index.d.ts +2 -2
- package/dist/theme/index.js +2 -2
- package/dist/{theme-BFMoCzi6.d.ts → theme-Bjf9qIIy.d.ts} +1 -1
- package/package.json +2 -2
- package/registry.json +230 -156
- package/skills/motion-kit/SKILL.md +209 -189
- package/skills/motion-kit/reference/components.md +5 -5
- package/skills/motion-kit/reference/design.md +14 -14
- package/skills/motion-kit/reference/recipes.md +48 -49
- package/src/auto/auto-init.ts +38 -0
- package/src/components/calendar/README.md +82 -0
- package/src/components/calendar/calendar.css +158 -0
- package/src/components/calendar/element.ts +27 -0
- package/src/components/calendar/engine.ts +412 -0
- package/src/components/calendar/react.tsx +69 -0
- package/src/components/chat/README.md +1 -3
- package/src/components/chat/chat.css +1 -1
- package/src/components/chat/engine.ts +1 -1
- package/src/components/chat/react.tsx +1 -1
- package/src/components/chat-background/README.md +1 -3
- package/src/components/chat-background/audio.ts +1 -1
- package/src/components/chat-background/chat-background.css +2 -2
- package/src/components/chat-background/engine.ts +1 -1
- package/src/components/chat-background/react.tsx +1 -1
- package/src/components/cursor/README.md +3 -5
- package/src/components/cursor/cursor.css +3 -3
- package/src/components/cursor/element.ts +2 -2
- package/src/components/cursor/engine.ts +5 -5
- package/src/components/cursor/react.tsx +1 -1
- package/src/components/dialog/README.md +4 -8
- package/src/components/dialog/dialog.css +3 -3
- package/src/components/dialog/engine.ts +2 -2
- package/src/components/dialog/react.tsx +1 -1
- package/src/components/hover-card/README.md +16 -0
- package/src/components/hover-card/element.ts +8 -0
- package/src/components/hover-card/engine.ts +69 -0
- package/src/components/hover-card/hover-card.css +13 -0
- package/src/components/hover-card/react.tsx +19 -0
- package/src/components/input-otp/README.md +18 -0
- package/src/components/input-otp/element.ts +14 -0
- package/src/components/input-otp/engine.ts +106 -0
- package/src/components/input-otp/input-otp.css +9 -0
- package/src/components/input-otp/react.tsx +21 -0
- package/src/components/island/README.md +7 -7
- package/src/components/island/engine.ts +3 -3
- package/src/components/island/island.css +1 -1
- package/src/components/island/react.tsx +3 -3
- package/src/components/menu/README.md +2 -4
- package/src/components/menu/engine.ts +1 -1
- package/src/components/menu/menu.css +1 -1
- package/src/components/menu/react.tsx +1 -1
- package/src/components/nav/README.md +12 -14
- package/src/components/nav/engine.ts +5 -5
- package/src/components/nav/nav.css +1 -5
- package/src/components/nav/react.tsx +3 -3
- package/src/components/pagination/README.md +17 -0
- package/src/components/pagination/element.ts +14 -0
- package/src/components/pagination/engine.ts +102 -0
- package/src/components/pagination/pagination.css +10 -0
- package/src/components/pagination/react.tsx +22 -0
- package/src/components/quantity/README.md +20 -0
- package/src/components/quantity/element.ts +14 -0
- package/src/components/quantity/engine.ts +85 -0
- package/src/components/quantity/quantity.css +15 -0
- package/src/components/quantity/react.tsx +21 -0
- package/src/components/slot-picker/README.md +68 -0
- package/src/components/slot-picker/element.ts +27 -0
- package/src/components/slot-picker/engine.ts +412 -0
- package/src/components/slot-picker/react.tsx +76 -0
- package/src/components/slot-picker/slot-picker.css +125 -0
- package/src/components/steps/README.md +1 -3
- package/src/components/steps/engine.ts +3 -3
- package/src/components/steps/react.tsx +2 -2
- package/src/components/steps/steps.css +2 -2
- package/src/components/studio-dock/README.md +44 -0
- package/src/components/studio-dock/element.ts +11 -0
- package/src/components/studio-dock/engine.ts +212 -0
- package/src/components/studio-dock/react.tsx +23 -0
- package/src/components/studio-dock/studio-dock.css +34 -0
- package/src/components/theme-picker/README.md +3 -5
- package/src/components/theme-picker/engine.ts +2 -2
- package/src/components/theme-picker/react.tsx +1 -1
- package/src/components/theme-picker/theme-picker.css +1 -1
- package/src/components/toast/README.md +1 -3
- package/src/components/toast/engine.ts +2 -2
- package/src/components/toast/react.tsx +1 -1
- package/src/components/toast/store.ts +1 -2
- package/src/components/toast/toast.css +1 -1
- package/src/components/trailer/README.md +28 -0
- package/src/components/trailer/element.ts +13 -0
- package/src/components/trailer/engine.ts +189 -0
- package/src/components/trailer/react.tsx +16 -0
- package/src/components/trailer/trailer.css +36 -0
- package/src/core/channels.ts +3 -3
- package/src/core/curves.ts +3 -3
- package/src/core/dates.ts +98 -0
- package/src/core/gsap.ts +2 -2
- package/src/core/loop.ts +1 -1
- package/src/core/math.ts +1 -1
- package/src/core/physics-spring.ts +2 -2
- package/src/core/scroll-track.ts +2 -4
- package/src/core/spring.ts +1 -1
- package/src/core/timing.ts +21 -22
- package/src/css/controls/README.md +24 -20
- package/src/css/controls/burger.css +1 -1
- package/src/css/controls/button.css +4 -4
- package/src/css/controls/checkbox.css +1 -1
- package/src/css/controls/chip.css +1 -1
- package/src/css/controls/field.css +2 -2
- package/src/css/controls/glass.css +4 -4
- package/src/css/controls/status.css +63 -0
- package/src/css/default-theme.css +1 -1
- package/src/css/effects/README.md +19 -21
- package/src/css/effects/anim.css +1 -1
- package/src/css/effects/fade.css +1 -1
- package/src/css/effects/float.css +1 -1
- package/src/css/effects/frost.css +1 -1
- package/src/css/effects/grain.css +1 -1
- package/src/css/effects/loading.css +1 -1
- package/src/css/effects/success.css +1 -1
- package/src/css/effects/text.css +1 -1
- package/src/css/fonts.css +1 -1
- package/src/css/states.css +1 -1
- package/src/css/themes/black-green.css +1 -1
- package/src/css/themes/black.css +1 -1
- package/src/css/themes/blue.css +1 -1
- package/src/css/themes/brown.css +1 -1
- package/src/css/themes/cold-blue.css +1 -1
- package/src/css/themes/funny.css +1 -1
- package/src/css/themes/gray.css +1 -1
- package/src/css/themes/green.css +1 -1
- package/src/css/themes/modern-1.css +1 -1
- package/src/css/themes/neogreen.css +1 -1
- package/src/css/themes/new.css +1 -1
- package/src/css/themes/new2.css +1 -1
- package/src/css/themes/oled.css +1 -1
- package/src/css/themes/orange.css +1 -1
- package/src/css/themes/pastel-pink.css +1 -1
- package/src/css/themes/pink.css +1 -1
- package/src/css/themes/purple.css +1 -1
- package/src/css/themes/red.css +1 -1
- package/src/css/themes/super-blue.css +1 -1
- package/src/css/tokens.css +8 -8
- package/src/elements/index.ts +24 -0
- package/src/gl/liquid-blend/README.md +3 -3
- package/src/gl/matcap/README.md +3 -3
- package/src/gl/particle-hero/README.md +1 -1
- package/src/gl/particle-hero/create-particle-hero.ts +3 -5
- package/src/gl/particle-hero/shaders.ts +1 -1
- package/src/gl/shader-blur/README.md +3 -3
- package/src/gl/shader-grain/README.md +3 -3
- package/src/index.ts +25 -0
- package/src/motion/chat.ts +2 -2
- package/src/motion/confetti.ts +1 -1
- package/src/motion/count.ts +1 -1
- package/src/motion/details.ts +1 -1
- package/src/motion/drag-dismiss.ts +1 -1
- package/src/motion/effects.ts +1 -1
- package/src/motion/entrances.ts +1 -1
- package/src/motion/floating.ts +1 -1
- package/src/motion/flow.ts +2 -2
- package/src/motion/indicator.ts +1 -1
- package/src/motion/layout.ts +1 -1
- package/src/motion/liquid.ts +1 -1
- package/src/motion/morph.ts +2 -2
- package/src/motion/reveal.ts +1 -1
- package/src/motion/sheet.ts +1 -1
- package/src/motion/surface.ts +1 -1
- package/src/react/animated-number.tsx +2 -2
- package/src/react/flip-group.tsx +1 -1
- package/src/react/frost.tsx +1 -1
- package/src/react/index.ts +21 -0
- package/src/react/slot-text.tsx +1 -1
- package/src/react/use-action-button.ts +6 -0
- package/src/react/use-card-dock.ts +1 -1
- package/src/react/use-cascade.ts +1 -1
- package/src/react/use-contrast.ts +1 -1
- package/src/react/use-depth-grid.ts +1 -1
- package/src/react/use-editorial-reveal.ts +7 -0
- package/src/react/use-fill-text.ts +1 -1
- package/src/react/use-film-hero.ts +6 -0
- package/src/react/use-font-cycle.ts +7 -0
- package/src/react/use-frame-zoom.ts +1 -1
- package/src/react/use-image-zoom.ts +5 -0
- package/src/react/use-joined-cta.ts +5 -0
- package/src/react/use-letters.ts +1 -1
- package/src/react/use-local-clock.ts +5 -0
- package/src/react/use-marquee.ts +1 -1
- package/src/react/use-mask-text.ts +1 -1
- package/src/react/use-parallax.ts +1 -1
- package/src/react/use-particle-hero.ts +1 -1
- package/src/react/use-scroll-dock.ts +5 -0
- package/src/react/use-snap-strip.ts +1 -1
- package/src/react/use-sticker.ts +5 -0
- package/src/react/use-studio-dock.ts +8 -0
- package/src/react/use-text-roll.ts +1 -1
- package/src/react/use-thumbnail-nav.ts +7 -0
- package/src/react/use-trailer.ts +6 -0
- package/src/react/use-weight-hover.ts +5 -0
- package/src/recipes/action-button/README.md +31 -0
- package/src/recipes/action-button/action-button.css +113 -0
- package/src/recipes/action-button/create-action-button.ts +56 -0
- package/src/recipes/card-dock/README.md +1 -1
- package/src/recipes/card-dock/card-dock.css +1 -1
- package/src/recipes/card-dock/create-card-dock.ts +2 -2
- package/src/recipes/cascade/README.md +1 -3
- package/src/recipes/cascade/create-cascade.ts +3 -3
- package/src/recipes/contrast/README.md +0 -2
- package/src/recipes/contrast/create-contrast.ts +1 -1
- package/src/recipes/counter/README.md +0 -2
- package/src/recipes/counter/create-counter.ts +1 -1
- package/src/recipes/curved-carousel/README.md +0 -2
- package/src/recipes/curved-carousel/create-curved-carousel.ts +2 -2
- package/src/recipes/curved-carousel/curved-carousel.css +1 -1
- package/src/recipes/depth-grid/README.md +0 -2
- package/src/recipes/depth-grid/create-depth-grid.ts +3 -4
- package/src/recipes/details/README.md +0 -2
- package/src/recipes/details/create-details.ts +1 -1
- package/src/recipes/dock-pill/README.md +3 -3
- package/src/recipes/drag-ruler/README.md +3 -3
- package/src/recipes/editorial-reveal/README.md +35 -0
- package/src/recipes/editorial-reveal/create-editorial-reveal.ts +91 -0
- package/src/recipes/editorial-reveal/editorial-reveal.css +17 -0
- package/src/recipes/fill-text/README.md +1 -1
- package/src/recipes/fill-text/create-fill-text.ts +1 -1
- package/src/recipes/film-hero/README.md +23 -0
- package/src/recipes/film-hero/create-film-hero.ts +65 -0
- package/src/recipes/film-hero/film-hero.css +29 -0
- package/src/recipes/fit-text/README.md +0 -2
- package/src/recipes/flow/README.md +0 -2
- package/src/recipes/flow/create-flow.ts +1 -1
- package/src/recipes/font-cycle/README.md +11 -0
- package/src/recipes/font-cycle/create-font-cycle.ts +126 -0
- package/src/recipes/font-cycle/font-cycle.css +36 -0
- package/src/recipes/frame-zoom/README.md +1 -3
- package/src/recipes/frame-zoom/create-frame-zoom.ts +2 -2
- package/src/recipes/image-zoom/README.md +16 -0
- package/src/recipes/image-zoom/create-image-zoom.ts +27 -0
- package/src/recipes/image-zoom/image-zoom.css +7 -0
- package/src/recipes/infinite-reel/README.md +3 -3
- package/src/recipes/joined-cta/README.md +13 -0
- package/src/recipes/joined-cta/create-joined-cta.ts +22 -0
- package/src/recipes/joined-cta/joined-cta.css +17 -0
- package/src/recipes/letters/README.md +2 -4
- package/src/recipes/letters/create-letters.ts +3 -3
- package/src/recipes/local-clock/README.md +31 -0
- package/src/recipes/local-clock/create-local-clock.ts +61 -0
- package/src/recipes/marquee/README.md +0 -2
- package/src/recipes/marquee/create-marquee.ts +3 -3
- package/src/recipes/marquee/marquee.css +1 -1
- package/src/recipes/mask-text/README.md +3 -5
- package/src/recipes/mask-text/create-mask-text.ts +4 -4
- package/src/recipes/parallax/README.md +1 -3
- package/src/recipes/parallax/create-parallax.ts +1 -1
- package/src/recipes/pile/README.md +0 -2
- package/src/recipes/pile/create-pile.ts +1 -1
- package/src/recipes/pile/pile-math.ts +1 -1
- package/src/recipes/pile/pile.css +1 -1
- package/src/recipes/pill/README.md +0 -2
- package/src/recipes/pill/pill.css +2 -2
- package/src/recipes/pop/README.md +0 -2
- package/src/recipes/pop/create-pop.ts +1 -1
- package/src/recipes/radial-menu/README.md +0 -2
- package/src/recipes/radial-menu/create-radial-menu.ts +1 -1
- package/src/recipes/radial-menu/radial-menu.css +1 -1
- package/src/recipes/reflow/README.md +3 -5
- package/src/recipes/reflow/create-reflow.ts +1 -1
- package/src/recipes/repel-bento/README.md +3 -3
- package/src/recipes/reveal/README.md +0 -2
- package/src/recipes/reveal/create-reveal.ts +1 -1
- package/src/recipes/rotator/README.md +3 -3
- package/src/recipes/scroll-dock/README.md +46 -0
- package/src/recipes/scroll-dock/create-scroll-dock.ts +72 -0
- package/src/recipes/scroll-dock/scroll-dock.css +70 -0
- package/src/recipes/slot-text/README.md +0 -2
- package/src/recipes/slot-text/create-slot-text.ts +1 -1
- package/src/recipes/snap-strip/README.md +1 -1
- package/src/recipes/snap-strip/create-snap-strip.ts +5 -6
- package/src/recipes/snap-strip/snap-strip.css +1 -1
- package/src/recipes/sortable/README.md +3 -5
- package/src/recipes/sortable/create-sortable.ts +4 -4
- package/src/recipes/sortable/sortable.css +1 -1
- package/src/recipes/spring-cards/README.md +0 -2
- package/src/recipes/spring-cards/create-spring-cards.ts +1 -1
- package/src/recipes/spring-cards/spring-cards.css +1 -1
- package/src/recipes/sticker/README.md +59 -0
- package/src/recipes/sticker/create-sticker.ts +490 -0
- package/src/recipes/sticker/sticker.css +112 -0
- package/src/recipes/stretch-footer/README.md +0 -2
- package/src/recipes/stretch-footer/create-stretch-footer.ts +1 -1
- package/src/recipes/stretch-footer/stretch-footer.css +1 -1
- package/src/recipes/tabs/README.md +0 -2
- package/src/recipes/tabs/create-tabs.ts +1 -1
- package/src/recipes/text-roll/README.md +1 -3
- package/src/recipes/text-roll/create-text-roll.ts +3 -3
- package/src/recipes/text-roll/text-roll.css +1 -1
- package/src/recipes/thumbnail-nav/README.md +16 -0
- package/src/recipes/thumbnail-nav/create-thumbnail-nav.ts +165 -0
- package/src/recipes/thumbnail-nav/thumbnail-nav.css +108 -0
- package/src/recipes/ticket-menu/README.md +3 -3
- package/src/recipes/weight-hover/README.md +37 -0
- package/src/recipes/weight-hover/create-weight-hover.ts +136 -0
- package/src/recipes/weight-hover/weight-hover.css +7 -0
- package/src/recipes/words/README.md +0 -2
- package/src/recipes/words/create-words.ts +1 -1
- package/src/theme/generate.ts +1 -1
- package/src/theme/presets.ts +2 -2
- package/src/theme/theme.ts +1 -2
- package/dist/chunk-2BQGFOA7.js.map +0 -1
- package/dist/chunk-34XYOXHG.js.map +0 -1
- package/dist/chunk-6OJAP6ZL.js.map +0 -1
- package/dist/chunk-FS2HJVVY.js.map +0 -1
- package/dist/chunk-J27KH2A2.js.map +0 -1
- package/dist/chunk-KLY5BRRI.js.map +0 -1
- package/dist/chunk-Q4LVFNLN.js.map +0 -1
- package/dist/chunk-RFDIZYFX.js.map +0 -1
- /package/dist/{chunk-ABNRDDO7.js.map → chunk-I3DXF2KQ.js.map} +0 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { reduced } from '../../core/reduced';
|
|
2
|
+
import { createScope } from '../../core/scope';
|
|
3
|
+
import { inert, type Instance } from '../../core/types';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Sticker (DOVV Studio hero and contact): a die-cut holographic sticker.
|
|
7
|
+
* - Holo: the foil and the shine follow the mouse over the sticker (lerp .08);
|
|
8
|
+
* without a mouse they drift on their own (sin t/1600, cos t/2100).
|
|
9
|
+
* - Peel: drag from any point and the nearest edge lifts. The stuck part is the
|
|
10
|
+
* art clipped to one side of the fold line; the flap is a copy reflected across
|
|
11
|
+
* it. Released early, it sticks back (380 ms, ease-out cubic). Past `detachAt`
|
|
12
|
+
* of the area (or a long drag) it comes off: it follows the pointer (lerp .22),
|
|
13
|
+
* grows 8 % and leans with the speed (±16°). Let go, it falls with gravity
|
|
14
|
+
* (1600 px/s²) and spin, and comes back to its place after `returnDelay`.
|
|
15
|
+
* Kit additions over the reference: the press freezes the current (hover) pose so
|
|
16
|
+
* nothing jumps, a press lifts the edge a little, pulling away also peels (the
|
|
17
|
+
* outward part of the drag folds the edge back), the corner cannot stretch past
|
|
18
|
+
* the opposite edge, and the back is a shaded paper face with a soft shadow.
|
|
19
|
+
* Entrance (pop .4 → 1, .7 s) and hover (scale 1.08, counter-rotation) are CSS.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface StickerOptions {
|
|
23
|
+
/** Drag to peel the sticker off. */
|
|
24
|
+
peel?: boolean;
|
|
25
|
+
/** Pointer-driven foil and shine. */
|
|
26
|
+
holo?: boolean;
|
|
27
|
+
/** Share of the area lifted before the sticker comes off (0–1). */
|
|
28
|
+
detachAt?: number;
|
|
29
|
+
/** Drag length (px, unscaled) that takes it off whatever the area; default max(80, half the longest side). */
|
|
30
|
+
detachDistance?: number;
|
|
31
|
+
/** Time hidden after falling before it comes back, ms. */
|
|
32
|
+
returnDelay?: number;
|
|
33
|
+
onPeel?: () => void;
|
|
34
|
+
onReturn?: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StickerInstance extends Instance {
|
|
38
|
+
/** idle | peeling | carried | falling | away */
|
|
39
|
+
readonly state: StickerState;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type StickerState = 'idle' | 'peeling' | 'carried' | 'falling' | 'away';
|
|
43
|
+
|
|
44
|
+
type Point = { x: number; y: number };
|
|
45
|
+
|
|
46
|
+
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3;
|
|
47
|
+
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));
|
|
48
|
+
|
|
49
|
+
/** Part of a convex polygon on the side where (p − m)·n ≥ 0 (one Sutherland–Hodgman pass). */
|
|
50
|
+
export function clipHalfPlane(poly: Point[], m: Point, n: Point): Point[] {
|
|
51
|
+
const side = (p: Point) => (p.x - m.x) * n.x + (p.y - m.y) * n.y;
|
|
52
|
+
const out: Point[] = [];
|
|
53
|
+
for (let i = 0; i < poly.length; i++) {
|
|
54
|
+
const a = poly[i];
|
|
55
|
+
const b = poly[(i + 1) % poly.length];
|
|
56
|
+
const sa = side(a);
|
|
57
|
+
const sb = side(b);
|
|
58
|
+
if (sa >= 0) out.push(a);
|
|
59
|
+
if ((sa >= 0) !== (sb >= 0)) {
|
|
60
|
+
const t = sa / (sa - sb);
|
|
61
|
+
out.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const area = (poly: Point[]) => {
|
|
68
|
+
let sum = 0;
|
|
69
|
+
for (let i = 0; i < poly.length; i++) {
|
|
70
|
+
const a = poly[i];
|
|
71
|
+
const b = poly[(i + 1) % poly.length];
|
|
72
|
+
sum += a.x * b.y - b.x * a.y;
|
|
73
|
+
}
|
|
74
|
+
return Math.abs(sum) / 2;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const polygon = (poly: Point[]) => (poly.length < 3 ? 'polygon(0 0, 0 0, 0 0)' : `polygon(${poly.map((p) => `${p.x.toFixed(2)}px ${p.y.toFixed(2)}px`).join(', ')})`);
|
|
78
|
+
|
|
79
|
+
export function createSticker(root: HTMLElement | null, options: StickerOptions = {}): StickerInstance {
|
|
80
|
+
if (!root) return inert({ state: 'idle' as StickerState });
|
|
81
|
+
const scope = createScope();
|
|
82
|
+
const still = reduced();
|
|
83
|
+
const detachAt = clamp(Number.isFinite(options.detachAt) ? options.detachAt! : 0.3, 0.05, 1);
|
|
84
|
+
const returnDelay = Math.max(0, Number.isFinite(options.returnDelay) ? options.returnDelay! : 2500);
|
|
85
|
+
let state: StickerState = 'idle';
|
|
86
|
+
const frames = new Set<number>();
|
|
87
|
+
const raf = (fn: FrameRequestCallback) => {
|
|
88
|
+
const id = requestAnimationFrame((time) => { frames.delete(id); fn(time); });
|
|
89
|
+
frames.add(id);
|
|
90
|
+
};
|
|
91
|
+
scope.add(() => { frames.forEach(cancelAnimationFrame); frames.clear(); });
|
|
92
|
+
|
|
93
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
94
|
+
scope.attr(root, 'style', root.getAttribute('style'));
|
|
95
|
+
if (options.holo !== false) scope.attr(root, 'data-mk-holo', '');
|
|
96
|
+
|
|
97
|
+
// root > peel > top > (original children); the back is a reflected copy made on demand.
|
|
98
|
+
const original = [...root.childNodes];
|
|
99
|
+
const peelBox = document.createElement('div');
|
|
100
|
+
peelBox.className = 'mk-sticker__peel';
|
|
101
|
+
const top = document.createElement('div');
|
|
102
|
+
top.className = 'mk-sticker__top';
|
|
103
|
+
top.append(...original);
|
|
104
|
+
peelBox.append(top);
|
|
105
|
+
root.append(peelBox);
|
|
106
|
+
scope.add(() => root.replaceChildren(...original));
|
|
107
|
+
let back: HTMLElement | null = null;
|
|
108
|
+
|
|
109
|
+
/* ── Holo ───────────────────────────────────────────── */
|
|
110
|
+
if (options.holo !== false) {
|
|
111
|
+
const holo = { x: 0.5, y: 0.5 };
|
|
112
|
+
let mouse: Point | null = null;
|
|
113
|
+
let visible = typeof IntersectionObserver !== 'function';
|
|
114
|
+
const hoverless = typeof matchMedia === 'function' && matchMedia('(hover: none)').matches;
|
|
115
|
+
root.style.setProperty('--mk-sticker-x', '0.5');
|
|
116
|
+
root.style.setProperty('--mk-sticker-y', '0.5');
|
|
117
|
+
if (!still) {
|
|
118
|
+
scope.on(window, 'pointermove', (event) => {
|
|
119
|
+
const e = event as PointerEvent;
|
|
120
|
+
if (e.pointerType === 'mouse') mouse = { x: e.clientX, y: e.clientY };
|
|
121
|
+
}, { passive: true });
|
|
122
|
+
scope.on(document, 'pointerleave', () => { mouse = null; });
|
|
123
|
+
if (typeof IntersectionObserver === 'function') {
|
|
124
|
+
scope.observe(new IntersectionObserver(([entry]) => {
|
|
125
|
+
const was = visible;
|
|
126
|
+
visible = entry.isIntersecting;
|
|
127
|
+
if (visible && !was) raf(loop);
|
|
128
|
+
})).observe(root);
|
|
129
|
+
}
|
|
130
|
+
const loop = (time: number) => {
|
|
131
|
+
if (scope.disposed || !visible) return;
|
|
132
|
+
const box = top.getBoundingClientRect();
|
|
133
|
+
let x: number;
|
|
134
|
+
let y: number;
|
|
135
|
+
if (mouse && !hoverless && box.width && box.height) {
|
|
136
|
+
x = clamp((mouse.x - box.left) / box.width, -0.5, 1.5);
|
|
137
|
+
y = clamp((mouse.y - box.top) / box.height, -0.5, 1.5);
|
|
138
|
+
} else {
|
|
139
|
+
x = 0.5 + Math.sin(time / 1600 + box.left / 200) * 0.6;
|
|
140
|
+
y = 0.5 + Math.cos(time / 2100 + box.top / 200) * 0.6;
|
|
141
|
+
}
|
|
142
|
+
holo.x += (x - holo.x) * 0.08;
|
|
143
|
+
holo.y += (y - holo.y) * 0.08;
|
|
144
|
+
root.style.setProperty('--mk-sticker-x', holo.x.toFixed(4));
|
|
145
|
+
root.style.setProperty('--mk-sticker-y', holo.y.toFixed(4));
|
|
146
|
+
raf(loop);
|
|
147
|
+
};
|
|
148
|
+
if (visible) raf(loop);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (options.peel === false) return { get state() { return state; }, destroy: () => scope.dispose() };
|
|
153
|
+
|
|
154
|
+
/* ── Peel geometry (local, unrotated coordinates of the peel box) ───── */
|
|
155
|
+
let w = 0;
|
|
156
|
+
let h = 0;
|
|
157
|
+
let corner: Point = { x: 0, y: 0 };
|
|
158
|
+
let current: Point = { x: 0, y: 0 };
|
|
159
|
+
let grabbed: Point = { x: 0, y: 0 };
|
|
160
|
+
let lifted = 0;
|
|
161
|
+
|
|
162
|
+
let anchor: Point = { x: 0, y: 0 };
|
|
163
|
+
let reach = 0;
|
|
164
|
+
let outward: Point = { x: 1, y: 0 };
|
|
165
|
+
let detachDistance = 80;
|
|
166
|
+
|
|
167
|
+
// The pose at the press (hover tilt and scale included) is frozen while peeling,
|
|
168
|
+
// so nothing jumps under the pointer; local coordinates use that pose.
|
|
169
|
+
let pose = { angle: 0, scale: 1, prop: 1 };
|
|
170
|
+
let frozen: { transform: string; transition: string } | null = null;
|
|
171
|
+
const readPose = () => {
|
|
172
|
+
const style = getComputedStyle(root);
|
|
173
|
+
const prop = parseFloat(style.scale) || 1;
|
|
174
|
+
const matrix = /^matrix\(([^)]+)\)$/.exec(style.transform || '');
|
|
175
|
+
if (matrix) {
|
|
176
|
+
const [a, b] = matrix[1].split(',').map(Number);
|
|
177
|
+
return { angle: Math.atan2(b, a), scale: Math.hypot(a, b) || 1, prop, transform: style.transform };
|
|
178
|
+
}
|
|
179
|
+
const angle = (parseFloat(style.getPropertyValue('--mk-sticker-rot')) || 0) * (Math.PI / 180);
|
|
180
|
+
return { angle, scale: 1, prop, transform: `rotate(${angle}rad)` };
|
|
181
|
+
};
|
|
182
|
+
const freeze = () => {
|
|
183
|
+
const now = readPose();
|
|
184
|
+
pose = now;
|
|
185
|
+
if (!frozen) frozen = { transform: root.style.transform, transition: root.style.transition };
|
|
186
|
+
root.style.transition = 'none';
|
|
187
|
+
root.style.transform = now.transform;
|
|
188
|
+
};
|
|
189
|
+
const unfreeze = () => {
|
|
190
|
+
if (!frozen) return;
|
|
191
|
+
root.style.transform = frozen.transform;
|
|
192
|
+
root.style.transition = frozen.transition;
|
|
193
|
+
frozen = null;
|
|
194
|
+
};
|
|
195
|
+
scope.add(unfreeze);
|
|
196
|
+
const toLocal = (clientX: number, clientY: number): Point => {
|
|
197
|
+
const box = peelBox.getBoundingClientRect();
|
|
198
|
+
const a = -pose.angle;
|
|
199
|
+
const s = pose.scale * pose.prop;
|
|
200
|
+
const dx = (clientX - (box.left + box.width / 2)) / s;
|
|
201
|
+
const dy = (clientY - (box.top + box.height / 2)) / s;
|
|
202
|
+
return { x: dx * Math.cos(a) - dy * Math.sin(a) + w / 2, y: dx * Math.sin(a) + dy * Math.cos(a) + h / 2 };
|
|
203
|
+
};
|
|
204
|
+
/*
|
|
205
|
+
* Which corner lifts. The reference projects the press onto the outline, which on a
|
|
206
|
+
* long pill pressed near the middle picks a point on a long edge and folds the whole
|
|
207
|
+
* strip. The kit uses the box's real corners (a diagonal curl): near the border, the
|
|
208
|
+
* corner closest to the press; from the middle, it waits for the first few pixels of
|
|
209
|
+
* drag and lifts the corner opposite to that direction, so it travels with the hand.
|
|
210
|
+
*/
|
|
211
|
+
const CORNERS = () => [{ x: 0, y: 0 }, { x: w, y: 0 }, { x: w, y: h }, { x: 0, y: h }];
|
|
212
|
+
const nearBorder = (p: Point) => Math.min(p.x, w - p.x, p.y, h - p.y) < Math.min(w, h) * 0.35;
|
|
213
|
+
const closestCorner = (p: Point) => CORNERS().reduce((a, b) => (Math.hypot(b.x - p.x, b.y - p.y) < Math.hypot(a.x - p.x, a.y - p.y) ? b : a));
|
|
214
|
+
const cornerAgainst = (d: Point) => CORNERS().reduce((a, b) => ((b.x - w / 2) * -d.x + (b.y - h / 2) * -d.y > (a.x - w / 2) * -d.x + (a.y - h / 2) * -d.y + 1e-6 ? b : a));
|
|
215
|
+
let choosing = false;
|
|
216
|
+
const useCorner = (c: Point) => {
|
|
217
|
+
choosing = false;
|
|
218
|
+
corner = c;
|
|
219
|
+
anchor = { x: w - c.x, y: h - c.y };
|
|
220
|
+
reach = Math.hypot(c.x - anchor.x, c.y - anchor.y);
|
|
221
|
+
const ox = c.x - w / 2;
|
|
222
|
+
const oy = c.y - h / 2;
|
|
223
|
+
outward = { x: ox / (Math.hypot(ox, oy) || 1), y: oy / (Math.hypot(ox, oy) || 1) };
|
|
224
|
+
current = { ...c };
|
|
225
|
+
};
|
|
226
|
+
const ensureBack = () => {
|
|
227
|
+
if (back) return back;
|
|
228
|
+
// The shadow sits on the outer layer: a filter on the clipped face would be clipped with it.
|
|
229
|
+
back = document.createElement('div');
|
|
230
|
+
back.className = 'mk-sticker__back';
|
|
231
|
+
back.setAttribute('aria-hidden', 'true');
|
|
232
|
+
const face = document.createElement('div');
|
|
233
|
+
face.className = 'mk-sticker__back-face';
|
|
234
|
+
const copy = top.cloneNode(true) as HTMLElement;
|
|
235
|
+
copy.querySelectorAll('[id]').forEach((el) => el.removeAttribute('id'));
|
|
236
|
+
face.append(...copy.childNodes);
|
|
237
|
+
back.append(face);
|
|
238
|
+
peelBox.append(back);
|
|
239
|
+
return back;
|
|
240
|
+
};
|
|
241
|
+
const flat = () => {
|
|
242
|
+
top.style.clipPath = '';
|
|
243
|
+
if (back) back.style.visibility = 'hidden';
|
|
244
|
+
root.removeAttribute('data-mk-peeling');
|
|
245
|
+
lifted = 0;
|
|
246
|
+
unfreeze();
|
|
247
|
+
};
|
|
248
|
+
scope.add(() => { top.style.clipPath = ''; back?.remove(); back = null; });
|
|
249
|
+
|
|
250
|
+
const fold = (p: Point) => {
|
|
251
|
+
current = p;
|
|
252
|
+
const dx = p.x - corner.x;
|
|
253
|
+
const dy = p.y - corner.y;
|
|
254
|
+
const length = Math.hypot(dx, dy);
|
|
255
|
+
if (length < 0.5) {
|
|
256
|
+
top.style.clipPath = '';
|
|
257
|
+
if (back) back.style.visibility = 'hidden';
|
|
258
|
+
lifted = 0;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const n = { x: dx / length, y: dy / length };
|
|
262
|
+
const m = { x: (corner.x + p.x) / 2, y: (corner.y + p.y) / 2 };
|
|
263
|
+
const rect = [{ x: 0, y: 0 }, { x: w, y: 0 }, { x: w, y: h }, { x: 0, y: h }];
|
|
264
|
+
const stuck = clipHalfPlane(rect, m, n);
|
|
265
|
+
const flap = clipHalfPlane(rect, m, { x: -n.x, y: -n.y });
|
|
266
|
+
lifted = area(flap) / (w * h || 1);
|
|
267
|
+
top.style.clipPath = polygon(stuck);
|
|
268
|
+
const b = ensureBack();
|
|
269
|
+
// Reflection across the fold line: X' = (I − 2nnᵀ)X + 2(m·n)n.
|
|
270
|
+
const d = m.x * n.x + m.y * n.y;
|
|
271
|
+
b.style.transform = `matrix(${1 - 2 * n.x * n.x}, ${-2 * n.x * n.y}, ${-2 * n.x * n.y}, ${1 - 2 * n.y * n.y}, ${2 * d * n.x}, ${2 * d * n.y})`;
|
|
272
|
+
(b.firstElementChild as HTMLElement).style.clipPath = polygon(flap);
|
|
273
|
+
// Shading runs from the fold (bright) to the lifted tip, in the face's own coordinates.
|
|
274
|
+
b.style.setProperty('--mk-sticker-fold', `${(Math.atan2(-n.x, n.y) * 180) / Math.PI}deg`);
|
|
275
|
+
b.style.visibility = flap.length >= 3 ? 'visible' : 'hidden';
|
|
276
|
+
};
|
|
277
|
+
const NUDGE = 8;
|
|
278
|
+
/**
|
|
279
|
+
* Peel point for a pointer at `at` (local). The fold deepens with the distance
|
|
280
|
+
* dragged in any direction (pulling away peels too, instead of flipping the whole
|
|
281
|
+
* sticker); the sideways part only tilts the fold (at most ~35°), so grabbing the
|
|
282
|
+
* end of a long pill never lifts the whole strip. The paper does not stretch.
|
|
283
|
+
*/
|
|
284
|
+
const pull = (at: Point): Point => {
|
|
285
|
+
const dx = at.x - grabbed.x;
|
|
286
|
+
const dy = at.y - grabbed.y;
|
|
287
|
+
const depth = Math.hypot(dx, dy);
|
|
288
|
+
const perp = { x: -outward.y, y: outward.x };
|
|
289
|
+
const side = clamp(dx * perp.x + dy * perp.y, -depth * 0.7, depth * 0.7) * 0.6;
|
|
290
|
+
let p = {
|
|
291
|
+
x: corner.x - outward.x * (NUDGE + depth) + perp.x * side,
|
|
292
|
+
y: corner.y - outward.y * (NUDGE + depth) + perp.y * side,
|
|
293
|
+
};
|
|
294
|
+
const ax = p.x - anchor.x;
|
|
295
|
+
const ay = p.y - anchor.y;
|
|
296
|
+
const length = Math.hypot(ax, ay);
|
|
297
|
+
if (length > reach) p = { x: anchor.x + (ax * reach) / length, y: anchor.y + (ay * reach) / length };
|
|
298
|
+
return p;
|
|
299
|
+
};
|
|
300
|
+
// A new press cancels a sticker that is still settling back.
|
|
301
|
+
let generation = 0;
|
|
302
|
+
const settle = (from: Point, duration: number, done?: () => void) => {
|
|
303
|
+
const start = performance.now();
|
|
304
|
+
const mine = generation;
|
|
305
|
+
// performance.now(), not the rAF timestamp: some environments use another time base.
|
|
306
|
+
const step = () => {
|
|
307
|
+
if (scope.disposed || mine !== generation) return;
|
|
308
|
+
const t = Math.min(1, (performance.now() - start) / duration);
|
|
309
|
+
const e = easeOutCubic(t);
|
|
310
|
+
fold({ x: from.x + (corner.x - from.x) * e, y: from.y + (corner.y - from.y) * e });
|
|
311
|
+
if (t < 1) raf(step);
|
|
312
|
+
else { flat(); done?.(); }
|
|
313
|
+
};
|
|
314
|
+
raf(step);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/* ── Carry and fall ─────────────────────────────────── */
|
|
318
|
+
const velocity = { x: 0, y: 0 };
|
|
319
|
+
let last = { x: 0, y: 0, t: 0 };
|
|
320
|
+
const track = (e: PointerEvent) => {
|
|
321
|
+
const now = performance.now();
|
|
322
|
+
const frames60 = Math.max(1, (now - last.t) / 16);
|
|
323
|
+
velocity.x = velocity.x * 0.5 + ((e.clientX - last.x) / frames60) * 0.5;
|
|
324
|
+
velocity.y = velocity.y * 0.5 + ((e.clientY - last.y) / frames60) * 0.5;
|
|
325
|
+
last = { x: e.clientX, y: e.clientY, t: now };
|
|
326
|
+
};
|
|
327
|
+
let home: { parent: Node; next: Node | null; style: string | null } | null = null;
|
|
328
|
+
const carry = { x: 0, y: 0, tx: 0, ty: 0, offX: 0, offY: 0, lift: 0, tilt: 0, base: 0, from: 1, to: 1.08 };
|
|
329
|
+
const carryScale = () => carry.from + (carry.to - carry.from) * carry.lift;
|
|
330
|
+
const paint = () => {
|
|
331
|
+
root.style.left = `${carry.x}px`;
|
|
332
|
+
root.style.top = `${carry.y}px`;
|
|
333
|
+
root.style.transform = `rotate(${carry.base + carry.tilt}deg) scale(${carryScale()})`;
|
|
334
|
+
root.style.filter = `drop-shadow(0 ${8 + 14 * carry.lift}px ${14 + 12 * carry.lift}px rgb(0 0 0 / ${0.16 + 0.14 * carry.lift}))`;
|
|
335
|
+
};
|
|
336
|
+
const follow = () => {
|
|
337
|
+
if (state !== 'carried' || scope.disposed) return;
|
|
338
|
+
carry.x += (carry.tx - carry.x) * 0.22;
|
|
339
|
+
carry.y += (carry.ty - carry.y) * 0.22;
|
|
340
|
+
carry.lift += (1 - carry.lift) * 0.15;
|
|
341
|
+
carry.tilt += (clamp(velocity.x * 1.2, -16, 16) - carry.tilt) * 0.15;
|
|
342
|
+
paint();
|
|
343
|
+
raf(follow);
|
|
344
|
+
};
|
|
345
|
+
const detach = (e: PointerEvent) => {
|
|
346
|
+
state = 'carried';
|
|
347
|
+
root.removeAttribute('data-mk-peeling');
|
|
348
|
+
root.setAttribute('data-mk-carried', '');
|
|
349
|
+
settle({ ...current }, 160);
|
|
350
|
+
const box = root.getBoundingClientRect();
|
|
351
|
+
// Home keeps the consumer's own inline styles, not the frozen pose.
|
|
352
|
+
unfreeze();
|
|
353
|
+
carry.base = pose.angle * (180 / Math.PI);
|
|
354
|
+
carry.from = pose.scale;
|
|
355
|
+
carry.to = Math.max(1, pose.scale) + 0.08;
|
|
356
|
+
home = { parent: root.parentNode!, next: root.nextSibling, style: root.getAttribute('style') };
|
|
357
|
+
const width = root.offsetWidth;
|
|
358
|
+
const height = root.offsetHeight;
|
|
359
|
+
document.body.append(root);
|
|
360
|
+
// Pin the size: a --mk-sticker-scale inherited from the old parent does not reach <body>.
|
|
361
|
+
Object.assign(root.style, { position: 'fixed', right: 'auto', bottom: 'auto', margin: '0', translate: 'none', scale: String(pose.prop), zIndex: '999', animation: 'none', transition: 'none' });
|
|
362
|
+
carry.x = box.left + box.width / 2 - width / 2;
|
|
363
|
+
carry.y = box.top + box.height / 2 - height / 2;
|
|
364
|
+
carry.offX = e.clientX - carry.x;
|
|
365
|
+
carry.offY = e.clientY - carry.y;
|
|
366
|
+
carry.tx = carry.x;
|
|
367
|
+
carry.ty = carry.y;
|
|
368
|
+
carry.lift = 0;
|
|
369
|
+
carry.tilt = 0;
|
|
370
|
+
paint();
|
|
371
|
+
options.onPeel?.();
|
|
372
|
+
raf(follow);
|
|
373
|
+
};
|
|
374
|
+
const goHome = () => {
|
|
375
|
+
if (!home) return;
|
|
376
|
+
const { parent, next, style } = home;
|
|
377
|
+
home = null;
|
|
378
|
+
parent.insertBefore(root, next && next.parentNode === parent ? next : null);
|
|
379
|
+
if (style === null) root.removeAttribute('style');
|
|
380
|
+
else root.setAttribute('style', style);
|
|
381
|
+
root.style.animationDelay = '0s';
|
|
382
|
+
root.removeAttribute('data-mk-carried');
|
|
383
|
+
root.removeAttribute('data-mk-away');
|
|
384
|
+
state = 'idle';
|
|
385
|
+
options.onReturn?.();
|
|
386
|
+
};
|
|
387
|
+
scope.add(goHome);
|
|
388
|
+
const away = () => {
|
|
389
|
+
state = 'away';
|
|
390
|
+
root.setAttribute('data-mk-away', '');
|
|
391
|
+
scope.timeout(goHome, returnDelay);
|
|
392
|
+
};
|
|
393
|
+
const fall = () => {
|
|
394
|
+
state = 'falling';
|
|
395
|
+
root.style.pointerEvents = 'none';
|
|
396
|
+
if (still) { away(); return; }
|
|
397
|
+
let x = 0;
|
|
398
|
+
let y = 0;
|
|
399
|
+
let angle = carry.base + carry.tilt;
|
|
400
|
+
let vx = clamp(velocity.x * 40, -900, 900);
|
|
401
|
+
let vy = clamp(velocity.y * 40, -500, 600) - 120;
|
|
402
|
+
const spin = vx * 0.35 + (Math.random() - 0.5) * 120;
|
|
403
|
+
const startY = carry.y;
|
|
404
|
+
let then = performance.now();
|
|
405
|
+
const step = () => {
|
|
406
|
+
if (scope.disposed || state !== 'falling') return;
|
|
407
|
+
const time = performance.now();
|
|
408
|
+
const dt = Math.min(0.05, Math.max(0, time - then) / 1000);
|
|
409
|
+
then = time;
|
|
410
|
+
vy += 1600 * dt;
|
|
411
|
+
x += vx * dt;
|
|
412
|
+
y += vy * dt;
|
|
413
|
+
vx *= 1 - 0.6 * dt;
|
|
414
|
+
angle += spin * dt;
|
|
415
|
+
root.style.transform = `translate(${x}px, ${y}px) rotate(${angle}deg) scale(${carryScale()})`;
|
|
416
|
+
if (startY + y < innerHeight + 40) raf(step);
|
|
417
|
+
else away();
|
|
418
|
+
};
|
|
419
|
+
raf(step);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/* ── Pointer ───────────────────────────────────────── */
|
|
423
|
+
let pointerId: number | null = null;
|
|
424
|
+
scope.on(root, 'pointerdown', (event) => {
|
|
425
|
+
const e = event as PointerEvent;
|
|
426
|
+
if (state !== 'idle' || e.button !== 0) return;
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
generation++;
|
|
429
|
+
freeze();
|
|
430
|
+
state = 'peeling';
|
|
431
|
+
root.setAttribute('data-mk-peeling', '');
|
|
432
|
+
pointerId = e.pointerId;
|
|
433
|
+
root.setPointerCapture?.(e.pointerId);
|
|
434
|
+
w = top.offsetWidth;
|
|
435
|
+
h = top.offsetHeight;
|
|
436
|
+
peelBox.style.width = `${w}px`;
|
|
437
|
+
peelBox.style.height = `${h}px`;
|
|
438
|
+
const at = toLocal(e.clientX, e.clientY);
|
|
439
|
+
grabbed = at;
|
|
440
|
+
detachDistance = Number.isFinite(options.detachDistance) ? Math.max(1, options.detachDistance!) : Math.max(80, Math.max(w, h) / 2);
|
|
441
|
+
last = { x: e.clientX, y: e.clientY, t: performance.now() };
|
|
442
|
+
velocity.x = velocity.y = 0;
|
|
443
|
+
if (nearBorder(at)) {
|
|
444
|
+
useCorner(closestCorner(at));
|
|
445
|
+
fold(pull(at));
|
|
446
|
+
} else {
|
|
447
|
+
// Nothing lifts until the drag says which way; settling from here is a no-op.
|
|
448
|
+
choosing = true;
|
|
449
|
+
corner = current = { ...at };
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
scope.on(root, 'pointermove', (event) => {
|
|
453
|
+
const e = event as PointerEvent;
|
|
454
|
+
if (state !== 'peeling' || e.pointerId !== pointerId) return;
|
|
455
|
+
track(e);
|
|
456
|
+
const at = toLocal(e.clientX, e.clientY);
|
|
457
|
+
if (choosing) {
|
|
458
|
+
const dx = at.x - grabbed.x;
|
|
459
|
+
const dy = at.y - grabbed.y;
|
|
460
|
+
const moved = Math.hypot(dx, dy);
|
|
461
|
+
if (moved < 4) return;
|
|
462
|
+
useCorner(cornerAgainst({ x: dx / moved, y: dy / moved }));
|
|
463
|
+
}
|
|
464
|
+
fold(pull(at));
|
|
465
|
+
if (lifted > detachAt || Math.hypot(at.x - grabbed.x, at.y - grabbed.y) > detachDistance) detach(e);
|
|
466
|
+
});
|
|
467
|
+
scope.on(document, 'pointermove', (event) => {
|
|
468
|
+
const e = event as PointerEvent;
|
|
469
|
+
if (state !== 'carried' || e.pointerId !== pointerId) return;
|
|
470
|
+
track(e);
|
|
471
|
+
carry.tx = e.clientX - carry.offX;
|
|
472
|
+
carry.ty = e.clientY - carry.offY;
|
|
473
|
+
});
|
|
474
|
+
const release = (event: Event) => {
|
|
475
|
+
const e = event as PointerEvent;
|
|
476
|
+
if (e.pointerId !== pointerId) return;
|
|
477
|
+
if (state === 'peeling') {
|
|
478
|
+
state = 'idle';
|
|
479
|
+
settle({ ...current }, 380);
|
|
480
|
+
} else if (state === 'carried') fall();
|
|
481
|
+
};
|
|
482
|
+
scope.on(document, 'pointerup', release);
|
|
483
|
+
scope.on(document, 'pointercancel', release);
|
|
484
|
+
scope.add(() => { peelBox.style.width = ''; peelBox.style.height = ''; });
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
get state() { return state; },
|
|
488
|
+
destroy: () => scope.dispose(),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* sticker — die-cut holographic sticker (DOVV Studio). Place it with position/left/top; tilt it with --mk-sticker-rot. */
|
|
2
|
+
.mk-sticker {
|
|
3
|
+
--mk-sticker-rot: 0deg;
|
|
4
|
+
--mk-sticker-foil: linear-gradient(115deg, #ed4a1c, #ff6a3d 18%, #ff8f5e 34%, #ff4f7a 52%, #ed4a1c 68%, #ff7a3d 84%, #ed4a1c);
|
|
5
|
+
--mk-sticker-ink: #fff;
|
|
6
|
+
display: inline-block;
|
|
7
|
+
cursor: grab;
|
|
8
|
+
touch-action: none;
|
|
9
|
+
user-select: none;
|
|
10
|
+
-webkit-user-select: none;
|
|
11
|
+
scale: var(--mk-sticker-scale, 1);
|
|
12
|
+
transform: rotate(var(--mk-sticker-rot));
|
|
13
|
+
filter: drop-shadow(0 8px 14px rgb(0 0 0 / 0.16));
|
|
14
|
+
transition: transform 0.4s var(--mk-ease-slot), filter 0.3s;
|
|
15
|
+
animation: mk-sticker-pop 0.7s var(--mk-ease-slot) backwards;
|
|
16
|
+
animation-delay: var(--mk-sticker-delay, 0s);
|
|
17
|
+
}
|
|
18
|
+
@media (hover: hover) {
|
|
19
|
+
.mk-sticker:hover { transform: rotate(calc(var(--mk-sticker-rot) * -0.6)) scale(1.08); }
|
|
20
|
+
}
|
|
21
|
+
/* The engine freezes the pose it had at the press (inline transform), so there is no snap here. */
|
|
22
|
+
.mk-sticker[data-mk-peeling],
|
|
23
|
+
.mk-sticker[data-mk-peeling]:hover {
|
|
24
|
+
transition: none;
|
|
25
|
+
cursor: grabbing;
|
|
26
|
+
}
|
|
27
|
+
.mk-sticker[data-mk-carried] { cursor: grabbing; }
|
|
28
|
+
.mk-sticker[data-mk-away] { visibility: hidden; }
|
|
29
|
+
@keyframes mk-sticker-pop {
|
|
30
|
+
0% { opacity: 0; scale: 0.4; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Peel layers: the top is clipped at the fold; the back is a reflected copy whose
|
|
34
|
+
face is clipped (inner) while the shadow lives on the outer layer, outside the clip. */
|
|
35
|
+
.mk-sticker__peel { position: relative; display: inline-block; vertical-align: top; }
|
|
36
|
+
.mk-sticker__top { display: block; }
|
|
37
|
+
.mk-sticker__back {
|
|
38
|
+
position: absolute;
|
|
39
|
+
left: 0;
|
|
40
|
+
top: 0;
|
|
41
|
+
width: 100%;
|
|
42
|
+
height: 100%;
|
|
43
|
+
transform-origin: 0 0;
|
|
44
|
+
visibility: hidden;
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
filter: drop-shadow(0 0 1.5px rgb(0 0 0 / 0.3)) drop-shadow(0 0 7px rgb(0 0 0 / 0.22));
|
|
47
|
+
}
|
|
48
|
+
.mk-sticker__back-face { width: 100%; height: 100%; }
|
|
49
|
+
/* Paper backing: brightest along the fold, a little grey towards the lifted tip. */
|
|
50
|
+
.mk-sticker:not([data-mk-sticker-back='silhouette']) .mk-sticker__back-face :is(.mk-sticker__art, .mk-sticker__pill, .mk-sticker__badge) {
|
|
51
|
+
/* The gradient runs under the die-cut border too, so the back reads as one piece of paper. */
|
|
52
|
+
background: linear-gradient(var(--mk-sticker-fold, 90deg), #fff 0%, #f6f6f4 40%, #e2e2de 100%) border-box !important;
|
|
53
|
+
color: transparent !important;
|
|
54
|
+
border-color: transparent !important;
|
|
55
|
+
}
|
|
56
|
+
.mk-sticker__back-face :is(.mk-sticker__art, .mk-sticker__pill, .mk-sticker__badge) * { visibility: hidden; }
|
|
57
|
+
.mk-sticker__back-face :is(.mk-sticker__art, .mk-sticker__pill, .mk-sticker__badge)::after { display: none; }
|
|
58
|
+
/* Free-form art (an <img> or <svg> shape): a white silhouette of it instead. */
|
|
59
|
+
.mk-sticker[data-mk-sticker-back='silhouette'] .mk-sticker__back-face { filter: brightness(0) invert(0.95); }
|
|
60
|
+
|
|
61
|
+
/* Art: a pill, a round badge or any element with .mk-sticker__art. The white die-cut border is part of the art. */
|
|
62
|
+
.mk-sticker__art,
|
|
63
|
+
.mk-sticker__pill,
|
|
64
|
+
.mk-sticker__badge {
|
|
65
|
+
position: relative;
|
|
66
|
+
display: block;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
isolation: isolate;
|
|
69
|
+
color: var(--mk-sticker-ink);
|
|
70
|
+
border: 4px solid #fff;
|
|
71
|
+
}
|
|
72
|
+
.mk-sticker__pill {
|
|
73
|
+
padding: 0.6em 1.1em;
|
|
74
|
+
border-radius: 999px;
|
|
75
|
+
font-weight: 800;
|
|
76
|
+
font-size: 1.15rem;
|
|
77
|
+
letter-spacing: -0.02em;
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
}
|
|
80
|
+
.mk-sticker__badge {
|
|
81
|
+
display: grid;
|
|
82
|
+
place-items: center;
|
|
83
|
+
width: var(--mk-sticker-size, 7rem);
|
|
84
|
+
aspect-ratio: 1;
|
|
85
|
+
border-radius: 50%;
|
|
86
|
+
font-weight: 800;
|
|
87
|
+
text-align: center;
|
|
88
|
+
}
|
|
89
|
+
.mk-sticker__art { border-radius: var(--mk-sticker-radius, 24px); }
|
|
90
|
+
.mk-sticker[data-mk-holo] :is(.mk-sticker__art, .mk-sticker__pill, .mk-sticker__badge) {
|
|
91
|
+
background: var(--mk-sticker-foil);
|
|
92
|
+
background-size: 220% 220%;
|
|
93
|
+
background-position: calc(var(--mk-sticker-x, 0.5) * 100%) calc(var(--mk-sticker-y, 0.5) * 100%);
|
|
94
|
+
}
|
|
95
|
+
.mk-sticker[data-mk-holo] :is(.mk-sticker__art, .mk-sticker__pill, .mk-sticker__badge)::after {
|
|
96
|
+
content: '';
|
|
97
|
+
position: absolute;
|
|
98
|
+
inset: 0;
|
|
99
|
+
z-index: -1;
|
|
100
|
+
pointer-events: none;
|
|
101
|
+
background: radial-gradient(circle at calc(var(--mk-sticker-x, 0.5) * 100%) calc(var(--mk-sticker-y, 0.5) * 100%), rgb(255 255 255 / 0.7), transparent 65%);
|
|
102
|
+
mix-blend-mode: soft-light;
|
|
103
|
+
opacity: 0.9;
|
|
104
|
+
}
|
|
105
|
+
.mk-sticker--pink { --mk-sticker-foil: linear-gradient(115deg, #ff7ac6, #c084fc 22%, #ffb3e1 44%, #7dd3fc 66%, #ff7ac6 84%, #f0abfc); --mk-sticker-ink: #3d0a26; }
|
|
106
|
+
.mk-sticker--blue { --mk-sticker-foil: linear-gradient(115deg, #2e90fa, #7c5cff 25%, #38bdf8 50%, #2e90fa 75%, #a78bfa); --mk-sticker-ink: #fff; }
|
|
107
|
+
.mk-sticker--lime { --mk-sticker-foil: linear-gradient(115deg, #c6f432, #fef08a 25%, #5eead4 50%, #a3e635 75%, #c6f432); --mk-sticker-ink: #1d2a00; }
|
|
108
|
+
.mk-sticker--gold { --mk-sticker-foil: linear-gradient(115deg, #facc15, #fde68a 25%, #f59e0b 50%, #fef3c7 70%, #eab308); --mk-sticker-ink: #1a1a1a; }
|
|
109
|
+
|
|
110
|
+
@media (prefers-reduced-motion: reduce) {
|
|
111
|
+
.mk-sticker { animation: none; transition: none; }
|
|
112
|
+
}
|
|
@@ -6,7 +6,7 @@ import { createScope } from '../../core/scope';
|
|
|
6
6
|
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
|
-
*
|
|
9
|
+
* stretch-footer: fixed behind the page and revealed by a 100dvh
|
|
10
10
|
* "phantom" at the end of the flow. Smoothed scroll g = damp(g, scrollY, λ)
|
|
11
11
|
* drives the title's scaleY (0 → 1) and lifts the footer from "title on the
|
|
12
12
|
* bottom edge" to its resting place.
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Un indicador se desliza hasta la pestaña elegida en 0.45 s con la curva signature. Si la lista hace scroll, centra la pestaña. Flechas (según orientación), Inicio y Fin cambian la selección.
|
|
4
4
|
|
|
5
|
-
Origen: Stepbro — PrettyTabs (`tabs`, `tabs-indicator` en el kit: `Flip.fit`, 0.45 s, `prettyTabsSlide`).
|
|
6
|
-
|
|
7
5
|
## Vanilla / Astro
|
|
8
6
|
|
|
9
7
|
```html
|
|
@@ -5,7 +5,7 @@ import { TIMING } from '../../core/timing';
|
|
|
5
5
|
import { slideIndicator } from '../../motion/indicator';
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
|
-
*
|
|
8
|
+
* Tabs: an indicator slides onto the selected tab in 0.45 s on
|
|
9
9
|
* the signature curve; a scrolling tab list centres the selection. Arrow keys
|
|
10
10
|
* (by orientation), Home and End move the selection.
|
|
11
11
|
*/
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Un título que gira como una tragaperras: cada letra es un carrete (la letra y una copia debajo) que da entre 9 y 15 vueltas al azar; todas frenan con `expo.out` en 2 s, 40 ms entre letras en orden aleatorio, y se quedan en la misma letra. Puede repetirse cada 6 s mientras se ve y no hay scroll (espera 1 s de calma).
|
|
4
4
|
|
|
5
|
-
Origen: Siena Film Foundation, títulos de película de la portada (copia `cloneText2`, `L.utils.random(8, 14)` repeticiones).
|
|
6
|
-
|
|
7
5
|
## HTML / Astro
|
|
8
6
|
|
|
9
7
|
```html
|
|
@@ -27,7 +25,7 @@ const roll = useTextRoll(ref, { trigger: 'manual' });
|
|
|
27
25
|
| Opción | Por defecto | |
|
|
28
26
|
|---|---|---|
|
|
29
27
|
| `trigger` | `'load'` | `'scroll'`, `'hover'`, `'manual'` |
|
|
30
|
-
| `interval` | `0` | ms entre repeticiones
|
|
28
|
+
| `interval` | `0` | ms entre repeticiones |
|
|
31
29
|
| `turns` | `[8, 14]` | repeticiones de la vuelta de cada letra |
|
|
32
30
|
|
|
33
31
|
## Accesibilidad
|