@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,31 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createChatBackground, type ChatBackgroundInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/** <mk-chat-background energy="0" scale="1"></mk-chat-background> inside any positioned container. */
|
|
5
|
+
export class MkChatBackgroundElement extends MkElement<ChatBackgroundInstance> {
|
|
6
|
+
static options: Record<string, OptionType> = {};
|
|
7
|
+
static get observedAttributes() {
|
|
8
|
+
return ['energy', 'scale'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
protected create() {
|
|
12
|
+
return createChatBackground(this, { energy: Number(this.getAttribute('energy')) || 0, scale: Number(this.getAttribute('scale')) || 1 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Energy changes every frame while recording: update the engine instead of rebuilding it.
|
|
16
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
17
|
+
if (previous === next || !this.instance) return;
|
|
18
|
+
if (name === 'energy') this.instance.setEnergy(Number(next) || 0);
|
|
19
|
+
if (name === 'scale') this.instance.setScale(Number(next) || 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setEnergy(energy: number) {
|
|
23
|
+
this.instance?.setEnergy(energy);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
busy(on: boolean) {
|
|
27
|
+
this.instance?.busy(on);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const defineChatBackground = () => defineElement('mk-chat-background', MkChatBackgroundElement);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { TIMING } from '../../core/timing';
|
|
5
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Stepbro's PrettyBackground: two soft blobs (primary-container and
|
|
9
|
+
* tertiary-container, 800 px radial gradients, saturate 150 %) orbit behind the
|
|
10
|
+
* chat and a grain sits on top. Energy (0…1, e.g. the microphone's) speeds the
|
|
11
|
+
* orbits up (10 − 3.5·e s and 15 − 4.5·e s), brightens the blobs (.5 + .15·e) and
|
|
12
|
+
* scales them ((1 + .1·e)·scale). Onboarding's "busy" state pushes it to energy
|
|
13
|
+
* 1.6 and scale 1.8 in 0.8 s (power2.out) and back in 0.6 s (power2.inOut).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface ChatBackgroundOptions {
|
|
17
|
+
energy?: number;
|
|
18
|
+
scale?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ChatBackgroundInstance extends Instance {
|
|
22
|
+
setEnergy(energy: number): void;
|
|
23
|
+
setScale(scale: number): void;
|
|
24
|
+
/** Onboarding's busy state: energy 1.6 / scale 1.8, or back to rest. */
|
|
25
|
+
busy(on: boolean): void;
|
|
26
|
+
readonly energy: number;
|
|
27
|
+
readonly scale: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const VARS = ['--mk-chat-orbit-1', '--mk-chat-orbit-2', '--mk-chat-blob-opacity', '--mk-chat-scale'];
|
|
31
|
+
const instances = new WeakMap<Element, ChatBackgroundInstance>();
|
|
32
|
+
|
|
33
|
+
/** The background instance running on `el` (steps and chat use it to drive the energy). */
|
|
34
|
+
export const chatBackgroundOf = (el: Element | null | undefined) => (el ? (instances.get(el) ?? null) : null);
|
|
35
|
+
|
|
36
|
+
export function createChatBackground(root: HTMLElement | null, options: ChatBackgroundOptions = {}): ChatBackgroundInstance {
|
|
37
|
+
if (!root) {
|
|
38
|
+
warnOnce('chat-background: missing root');
|
|
39
|
+
return inert({ setEnergy() {}, setScale() {}, busy() {}, energy: 0, scale: 1 });
|
|
40
|
+
}
|
|
41
|
+
registerMotion();
|
|
42
|
+
const scope = createScope();
|
|
43
|
+
const state = { energy: options.energy ?? 0, scale: options.scale ?? 1 };
|
|
44
|
+
let noise = root.querySelector<HTMLElement>(':scope > .mk-chat-background__noise');
|
|
45
|
+
if (!noise) {
|
|
46
|
+
noise = document.createElement('div');
|
|
47
|
+
noise.className = 'mk-chat-background__noise';
|
|
48
|
+
noise.setAttribute('aria-hidden', 'true');
|
|
49
|
+
root.append(noise);
|
|
50
|
+
const created = noise;
|
|
51
|
+
scope.add(() => created.remove());
|
|
52
|
+
}
|
|
53
|
+
scope.attr(root, 'aria-hidden', 'true');
|
|
54
|
+
|
|
55
|
+
const paint = () => {
|
|
56
|
+
const e = Math.max(0, state.energy);
|
|
57
|
+
root.style.setProperty('--mk-chat-orbit-1', `${Math.max(1, 10 - 3.5 * e).toFixed(3)}s`);
|
|
58
|
+
root.style.setProperty('--mk-chat-orbit-2', `${Math.max(1, 15 - 4.5 * e).toFixed(3)}s`);
|
|
59
|
+
root.style.setProperty('--mk-chat-blob-opacity', (0.5 + 0.15 * e).toFixed(3));
|
|
60
|
+
root.style.setProperty('--mk-chat-scale', ((1 + 0.1 * e) * state.scale).toFixed(3));
|
|
61
|
+
};
|
|
62
|
+
scope.add(() => {
|
|
63
|
+
gsap.killTweensOf(state);
|
|
64
|
+
for (const name of VARS) root.style.removeProperty(name);
|
|
65
|
+
if (root.getAttribute('style') === '') root.removeAttribute('style');
|
|
66
|
+
});
|
|
67
|
+
paint();
|
|
68
|
+
|
|
69
|
+
const instance: ChatBackgroundInstance = {
|
|
70
|
+
setEnergy(energy: number) {
|
|
71
|
+
state.energy = Number.isFinite(energy) ? energy : 0;
|
|
72
|
+
paint();
|
|
73
|
+
},
|
|
74
|
+
setScale(scale: number) {
|
|
75
|
+
state.scale = Number.isFinite(scale) ? scale : 1;
|
|
76
|
+
paint();
|
|
77
|
+
},
|
|
78
|
+
busy(on: boolean) {
|
|
79
|
+
const to = on ? { energy: 1.6, scale: 1.8 } : { energy: 0, scale: 1 };
|
|
80
|
+
gsap.killTweensOf(state);
|
|
81
|
+
if (reduced()) {
|
|
82
|
+
Object.assign(state, to);
|
|
83
|
+
paint();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
gsap.to(state, {
|
|
87
|
+
...to,
|
|
88
|
+
duration: on ? TIMING.onboarding.busy : TIMING.onboarding.idle,
|
|
89
|
+
ease: on ? 'power2.out' : 'power2.inOut',
|
|
90
|
+
onUpdate: paint,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
get energy() {
|
|
94
|
+
return state.energy;
|
|
95
|
+
},
|
|
96
|
+
get scale() {
|
|
97
|
+
return state.scale;
|
|
98
|
+
},
|
|
99
|
+
destroy() {
|
|
100
|
+
instances.delete(root);
|
|
101
|
+
scope.dispose();
|
|
102
|
+
delete root.dataset.mkReady;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
instances.set(root, instance);
|
|
106
|
+
root.dataset.mkReady = '';
|
|
107
|
+
return instance;
|
|
108
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, type CSSProperties } from 'react';
|
|
4
|
+
import { useIsomorphicLayoutEffect } from '../../react/use-recipe';
|
|
5
|
+
import { createChatBackground, type ChatBackgroundInstance } from './engine';
|
|
6
|
+
|
|
7
|
+
export interface ChatBackgroundProps {
|
|
8
|
+
/** 0…1 (the microphone's, see audioEnergy); onboarding's busy state goes to 1.6. */
|
|
9
|
+
energy?: number;
|
|
10
|
+
scale?: number;
|
|
11
|
+
className?: string;
|
|
12
|
+
style?: CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Stepbro's chat background. Put it first inside a positioned container. */
|
|
16
|
+
export function ChatBackground({ energy = 0, scale = 1, className, style }: ChatBackgroundProps) {
|
|
17
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
18
|
+
const instance = useRef<ChatBackgroundInstance | null>(null);
|
|
19
|
+
useIsomorphicLayoutEffect(() => {
|
|
20
|
+
const created = createChatBackground(ref.current, { energy, scale });
|
|
21
|
+
instance.current = created;
|
|
22
|
+
return () => {
|
|
23
|
+
created.destroy();
|
|
24
|
+
instance.current = null;
|
|
25
|
+
};
|
|
26
|
+
// Created once; energy and scale are pushed below.
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
|
+
}, []);
|
|
29
|
+
useIsomorphicLayoutEffect(() => {
|
|
30
|
+
instance.current?.setEnergy(energy);
|
|
31
|
+
}, [energy]);
|
|
32
|
+
useIsomorphicLayoutEffect(() => {
|
|
33
|
+
instance.current?.setScale(scale);
|
|
34
|
+
}, [scale]);
|
|
35
|
+
return <div ref={ref} className={['mk-chat-background', className].filter(Boolean).join(' ')} style={style} />;
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# cursor
|
|
2
|
+
|
|
3
|
+
Dos cursores:
|
|
4
|
+
|
|
5
|
+
- **`chip`** (Dousan Miao, por defecto): el cursor del sistema se queda. Sobre un elemento con `data-mk-cursor="Texto"` aparece un chip de cristal (`.mk-glass`) con el texto que sigue al puntero con un resorte (Motion 800/35) y entra desde `scale .8` (500/30). `data-mk-cursor-shape="circle"` lo convierte en un círculo de 96 px. Se queda a 10 px de los bordes de la ventana. El texto es del color acento sobre fondos claros y blanco sobre oscuros (luminancia `(299R + 587G + 114B) / 1000 > 180`, como Dousan).
|
|
6
|
+
- **`dot`** (Siena Film Foundation): dentro de su zona un círculo sustituye al cursor y lo sigue (20 % de la distancia por frame) con una palabra (`text` o el `data-mk-cursor` de lo que hay debajo). Al presionar encoge a .7, la palabra cae y entran dos flechas (0.2–0.5 s, `easeOutQuint`). `data-mk-cursor-hide` lo oculta sobre enlaces o botones.
|
|
7
|
+
|
|
8
|
+
Los dos se apagan en pantallas táctiles y con movimiento reducido.
|
|
9
|
+
|
|
10
|
+
Origen: dousanmiao.com (cursor `kd`), siena.film (`[data-cursor]`, lerp .2).
|
|
11
|
+
|
|
12
|
+
## HTML / Astro
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<!-- chip: una vez por página -->
|
|
16
|
+
<mk-cursor></mk-cursor>
|
|
17
|
+
<a href="/p/1" data-mk-cursor="View project" data-mk-cursor-shape="circle">…</a>
|
|
18
|
+
<button data-mk-cursor="Copiar" data-mk-cursor-icon="content_copy">…</button>
|
|
19
|
+
|
|
20
|
+
<!-- dot: dentro de su zona -->
|
|
21
|
+
<section class="reel">
|
|
22
|
+
<mk-cursor variant="dot" text="Scroll" arrows="y"></mk-cursor>
|
|
23
|
+
<a href="/films/savoy" data-mk-cursor-hide>Savoy</a>
|
|
24
|
+
</section>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Sin custom element: `createCursor(capa, { variant, zone, text, arrows })` sobre un `div` vacío.
|
|
28
|
+
|
|
29
|
+
## React
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<Cursor /> // en el layout
|
|
33
|
+
<section><Cursor variant="dot" text="Drag" arrows="x" /></section>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
`createCursor(host, opciones)` → `{ visible, label, position, tone, update(), destroy }`.
|
|
39
|
+
|
|
40
|
+
| Opción | Por defecto | |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `variant` | `'chip'` | `'dot'` |
|
|
43
|
+
| `zone` | chip: el documento; dot: el padre | dónde funciona |
|
|
44
|
+
| `text` | — | dot: la palabra por defecto |
|
|
45
|
+
| `arrows` | `'y'` | dot: `'x'`, `'none'` |
|
|
46
|
+
|
|
47
|
+
## Variables CSS
|
|
48
|
+
|
|
49
|
+
`--mk-cursor-light-ink` (texto del chip sobre claro; por defecto `primary`), `--mk-cursor-circle` (96px), `--mk-cursor-size` (88px), `--mk-cursor-fill`, `--mk-cursor-ink`.
|
|
50
|
+
|
|
51
|
+
## Accesibilidad
|
|
52
|
+
|
|
53
|
+
La capa es decorativa (`aria-hidden`, sin puntero); el texto del chip repite lo que el enlace ya dice, así que el enlace necesita su propio nombre accesible. El `dot` oculta el cursor del sistema solo en su zona.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/* cursor — Dousan's glass chip and Siena's dot. The layer follows the pointer; the engine writes its transform. */
|
|
2
|
+
.mk-cursor {
|
|
3
|
+
position: fixed;
|
|
4
|
+
top: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
z-index: 2147483000;
|
|
7
|
+
pointer-events: none;
|
|
8
|
+
will-change: transform;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* chip (Dousan): a .mk-glass label; ink follows the background under it. */
|
|
12
|
+
.mk-cursor__chip {
|
|
13
|
+
--mk-glass-fg: #fff;
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 6px;
|
|
17
|
+
padding: 8px 16px;
|
|
18
|
+
font-family: var(--mk-font);
|
|
19
|
+
font-size: 0.875rem;
|
|
20
|
+
font-weight: 500;
|
|
21
|
+
line-height: 1;
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
transition: color 0.15s;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.mk-cursor__chip[data-mk-tone='light'] {
|
|
27
|
+
--mk-glass-fg: var(--mk-cursor-light-ink, var(--md-sys-color-primary));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.mk-cursor__chip--circle {
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
width: var(--mk-cursor-circle, 96px);
|
|
34
|
+
height: var(--mk-cursor-circle, 96px);
|
|
35
|
+
padding: 8px;
|
|
36
|
+
line-height: 1.15;
|
|
37
|
+
text-align: center;
|
|
38
|
+
white-space: normal;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.mk-cursor__icon {
|
|
42
|
+
font-size: 0.875rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.mk-cursor__icon:empty {
|
|
46
|
+
display: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* dot (Siena): a disc with a word and two arrows that slide in when pressed. */
|
|
50
|
+
.mk-cursor--dot {
|
|
51
|
+
--mk-cursor-size: 88px;
|
|
52
|
+
transition: opacity 0.15s var(--mk-ease-quint);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.mk-cursor__dot {
|
|
56
|
+
position: relative;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
gap: 4px;
|
|
62
|
+
width: var(--mk-cursor-size);
|
|
63
|
+
height: var(--mk-cursor-size);
|
|
64
|
+
color: var(--mk-cursor-ink, var(--md-sys-color-background));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.mk-cursor--dot[data-mk-arrows='x'] .mk-cursor__dot {
|
|
68
|
+
flex-direction: row;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.mk-cursor__circle {
|
|
72
|
+
position: absolute;
|
|
73
|
+
inset: 0;
|
|
74
|
+
border-radius: 50%;
|
|
75
|
+
background: var(--mk-cursor-fill, var(--md-sys-color-on-background));
|
|
76
|
+
transition: transform 0.5s var(--mk-ease-quint);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.mk-cursor__text {
|
|
80
|
+
position: relative;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
font-family: var(--mk-font);
|
|
83
|
+
font-size: 12px;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
line-height: 0.9;
|
|
86
|
+
letter-spacing: 0.04em;
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.mk-cursor__word {
|
|
91
|
+
display: block;
|
|
92
|
+
transition: transform 0.5s var(--mk-ease-quint);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.mk-cursor__arrow {
|
|
96
|
+
position: relative;
|
|
97
|
+
display: grid;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.mk-cursor__arrow > svg {
|
|
102
|
+
transition: transform 0.3s var(--mk-ease-quint);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.mk-cursor__arrow--start > svg {
|
|
106
|
+
transform: translateY(130%);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.mk-cursor__arrow--end > svg {
|
|
110
|
+
transform: translateY(-130%) rotate(180deg);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.mk-cursor--dot[data-mk-arrows='x'] .mk-cursor__arrow--start > svg {
|
|
114
|
+
transform: translateX(130%) rotate(-90deg);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.mk-cursor--dot[data-mk-arrows='x'] .mk-cursor__arrow--end > svg {
|
|
118
|
+
transform: translateX(-130%) rotate(90deg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.mk-cursor[data-mk-pressed] .mk-cursor__circle {
|
|
122
|
+
transform: scale(0.7);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.mk-cursor[data-mk-pressed] .mk-cursor__word {
|
|
126
|
+
transform: translateY(150%);
|
|
127
|
+
transition-duration: 0.2s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.mk-cursor[data-mk-pressed] .mk-cursor__arrow > svg {
|
|
131
|
+
transition-delay: 0.1s;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.mk-cursor[data-mk-pressed] .mk-cursor__arrow--start > svg {
|
|
135
|
+
transform: translateY(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.mk-cursor[data-mk-pressed] .mk-cursor__arrow--end > svg {
|
|
139
|
+
transform: translateY(0) rotate(180deg);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.mk-cursor--dot[data-mk-arrows='x'][data-mk-pressed] .mk-cursor__arrow--start > svg {
|
|
143
|
+
transform: translateX(0) rotate(-90deg);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.mk-cursor--dot[data-mk-arrows='x'][data-mk-pressed] .mk-cursor__arrow--end > svg {
|
|
147
|
+
transform: translateX(0) rotate(90deg);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.mk-cursor--dot[data-mk-hidden] {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.mk-cursor--dot[data-mk-hidden] .mk-cursor__circle {
|
|
155
|
+
transform: scale(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* The dot replaces the system cursor inside its zone. */
|
|
159
|
+
[data-mk-cursor-zone],
|
|
160
|
+
[data-mk-cursor-zone] * {
|
|
161
|
+
cursor: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
mk-cursor {
|
|
165
|
+
display: block;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@media (pointer: coarse), (prefers-reduced-motion: reduce) {
|
|
169
|
+
.mk-cursor {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
[data-mk-cursor-zone],
|
|
174
|
+
[data-mk-cursor-zone] * {
|
|
175
|
+
cursor: auto;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createCursor, type CursorInstance, type CursorOptions, type CursorVariant } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-cursor></mk-cursor> Dousan's glass chip (once per page)
|
|
6
|
+
* <mk-cursor variant="dot" text="Scroll" arrows="y"></mk-cursor> Siena's dot, inside its parent
|
|
7
|
+
* zone="#hero" picks another zone.
|
|
8
|
+
*/
|
|
9
|
+
export class MkCursorElement extends MkElement<CursorInstance> {
|
|
10
|
+
static options: Record<string, OptionType> = { variant: 'string', text: 'string', arrows: 'string', zone: 'string' };
|
|
11
|
+
|
|
12
|
+
protected create(options: Record<string, unknown>) {
|
|
13
|
+
const zone = typeof options.zone === 'string' ? document.querySelector(options.zone) : undefined;
|
|
14
|
+
return createCursor(this, {
|
|
15
|
+
variant: options.variant as CursorVariant | undefined,
|
|
16
|
+
text: options.text as string | undefined,
|
|
17
|
+
arrows: options.arrows as CursorOptions['arrows'],
|
|
18
|
+
zone,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const defineCursor = () => defineElement('mk-cursor', MkCursorElement);
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { subscribe } from '../../core/loop';
|
|
3
|
+
import { motionSpring } from '../../core/physics-spring';
|
|
4
|
+
import { reduced } from '../../core/reduced';
|
|
5
|
+
import { createScope } from '../../core/scope';
|
|
6
|
+
import { TIMING } from '../../core/timing';
|
|
7
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
8
|
+
import { ICON_PATHS } from '../icons';
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Two cursors.
|
|
12
|
+
* chip (Dousan): the system cursor stays; over [data-mk-cursor="Label"] a glass
|
|
13
|
+
* chip with the label follows the pointer on a Motion 800/35 spring, popping in
|
|
14
|
+
* from scale .8 (500/30); data-mk-cursor-shape="circle" makes it a 96 px disc;
|
|
15
|
+
* it stays 10 px inside the window; its ink is the accent on light backgrounds
|
|
16
|
+
* and white on dark ones (Dousan's luminance test).
|
|
17
|
+
* dot (Siena): inside its zone a disc replaces the cursor and eases after it
|
|
18
|
+
* (lerp .2 a frame) with a word (data-mk-cursor or `text`); pressing shrinks it
|
|
19
|
+
* to .7, drops the word and slides two arrows in; [data-mk-cursor-hide] hides it.
|
|
20
|
+
* Off on touch screens and with reduced motion.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type CursorVariant = 'chip' | 'dot';
|
|
24
|
+
|
|
25
|
+
export interface CursorOptions {
|
|
26
|
+
variant?: CursorVariant;
|
|
27
|
+
/** Where it works. chip: the document; dot: the host's parent. */
|
|
28
|
+
zone?: Element | null;
|
|
29
|
+
/** dot: the word when nothing under the pointer gives one. */
|
|
30
|
+
text?: string;
|
|
31
|
+
/** dot: arrows shown while pressed. */
|
|
32
|
+
arrows?: 'x' | 'y' | 'none';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CursorInstance extends Instance {
|
|
36
|
+
readonly visible: boolean;
|
|
37
|
+
readonly label: string;
|
|
38
|
+
readonly position: { x: number; y: number };
|
|
39
|
+
readonly tone: 'light' | 'dark';
|
|
40
|
+
/** One frame (dt = 1 at 60 Hz); the shared ticker calls it. */
|
|
41
|
+
update(dt?: number): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const svg = (extra = '') =>
|
|
45
|
+
`<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"${extra}><path d="${ICON_PATHS.send}"/></svg>`;
|
|
46
|
+
|
|
47
|
+
/** Dousan: the first background with alpha > .1 up the tree; light when (299R + 587G + 114B) / 1000 > 180. */
|
|
48
|
+
function backgroundTone(el: Element | null): 'light' | 'dark' {
|
|
49
|
+
if (!el) return 'light';
|
|
50
|
+
if (el.closest('img, video, picture, canvas')) return 'dark';
|
|
51
|
+
for (let node: Element | null = el; node; node = node.parentElement) {
|
|
52
|
+
const match = getComputedStyle(node).backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
53
|
+
if (!match) continue;
|
|
54
|
+
const alpha = match[4] === undefined ? 1 : parseFloat(match[4]);
|
|
55
|
+
if (alpha <= 0.1) continue;
|
|
56
|
+
const luminance = (Number(match[1]) * 299 + Number(match[2]) * 587 + Number(match[3]) * 114) / 1000;
|
|
57
|
+
return luminance > TIMING.cursor.light ? 'light' : 'dark';
|
|
58
|
+
}
|
|
59
|
+
return 'light';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createCursor(host: HTMLElement | null, options: CursorOptions = {}): CursorInstance {
|
|
63
|
+
const variant = options.variant ?? 'chip';
|
|
64
|
+
const idle = inert<CursorInstance>({ visible: false, label: '', position: { x: 0, y: 0 }, tone: 'light', update() {} });
|
|
65
|
+
if (!host) {
|
|
66
|
+
warnOnce('cursor: missing element');
|
|
67
|
+
return idle;
|
|
68
|
+
}
|
|
69
|
+
const coarse = typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches;
|
|
70
|
+
if (coarse || reduced()) return idle;
|
|
71
|
+
registerMotion();
|
|
72
|
+
const scope = createScope();
|
|
73
|
+
const t = TIMING.cursor;
|
|
74
|
+
const zone = options.zone ?? (variant === 'dot' ? (host.parentElement ?? document.body) : document.documentElement);
|
|
75
|
+
scope.attr(host, 'class', [host.getAttribute('class'), 'mk-cursor', `mk-cursor--${variant}`].filter(Boolean).join(' '));
|
|
76
|
+
scope.attr(host, 'aria-hidden', 'true');
|
|
77
|
+
|
|
78
|
+
const pointer = { x: -100, y: -100, placed: false };
|
|
79
|
+
const position = { x: -100, y: -100 };
|
|
80
|
+
const velocity = { x: 0, y: 0 };
|
|
81
|
+
let visible = false;
|
|
82
|
+
let label = '';
|
|
83
|
+
let tone: 'light' | 'dark' = 'light';
|
|
84
|
+
const made: Element[] = [];
|
|
85
|
+
|
|
86
|
+
// ── Markup ────────────────────────────────────────────────────────────────
|
|
87
|
+
let chip: HTMLElement | null = null;
|
|
88
|
+
let chipLabel: HTMLElement | null = null;
|
|
89
|
+
let chipIcon: HTMLElement | null = null;
|
|
90
|
+
let word: HTMLElement | null = null;
|
|
91
|
+
const size = { width: 0, height: 0 };
|
|
92
|
+
if (variant === 'chip') {
|
|
93
|
+
chip = document.createElement('span');
|
|
94
|
+
chip.className = 'mk-cursor__chip mk-glass';
|
|
95
|
+
chip.innerHTML =
|
|
96
|
+
'<span class="mk-glass__shadow" aria-hidden="true"></span><span class="mk-cursor__icon mk-icon"></span><span class="mk-cursor__label"></span>';
|
|
97
|
+
chipIcon = chip.querySelector('.mk-cursor__icon');
|
|
98
|
+
chipLabel = chip.querySelector('.mk-cursor__label');
|
|
99
|
+
host.append(chip);
|
|
100
|
+
made.push(chip);
|
|
101
|
+
gsap.set(chip, { autoAlpha: 0, scale: 0.8 });
|
|
102
|
+
} else {
|
|
103
|
+
const arrows = options.arrows ?? 'y';
|
|
104
|
+
scope.attr(host, 'data-mk-arrows', arrows);
|
|
105
|
+
scope.attr(zone, 'data-mk-cursor-zone', '');
|
|
106
|
+
const dot = document.createElement('span');
|
|
107
|
+
dot.className = 'mk-cursor__dot';
|
|
108
|
+
dot.innerHTML =
|
|
109
|
+
arrows === 'none'
|
|
110
|
+
? '<span class="mk-cursor__circle"></span><span class="mk-cursor__text"><span class="mk-cursor__word"></span></span>'
|
|
111
|
+
: `<span class="mk-cursor__circle"></span><span class="mk-cursor__arrow mk-cursor__arrow--start">${svg()}</span><span class="mk-cursor__text"><span class="mk-cursor__word"></span></span><span class="mk-cursor__arrow mk-cursor__arrow--end">${svg()}</span>`;
|
|
112
|
+
word = dot.querySelector('.mk-cursor__word');
|
|
113
|
+
if (word) word.textContent = options.text ?? '';
|
|
114
|
+
host.append(dot);
|
|
115
|
+
made.push(dot);
|
|
116
|
+
scope.attr(host, 'data-mk-hidden', '');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Chip ──────────────────────────────────────────────────────────────────
|
|
120
|
+
const appear = motionSpring(...TIMING.springs.cursorAppear);
|
|
121
|
+
const showChip = (next: HTMLElement | null) => {
|
|
122
|
+
if (!chip || !chipLabel || !chipIcon) return;
|
|
123
|
+
const text = next?.dataset.mkCursor ?? '';
|
|
124
|
+
if (text) {
|
|
125
|
+
label = text;
|
|
126
|
+
chipLabel.textContent = text;
|
|
127
|
+
chipIcon.textContent = next?.dataset.mkCursorIcon ?? '';
|
|
128
|
+
chip.classList.toggle('mk-cursor__chip--circle', next?.dataset.mkCursorShape === 'circle');
|
|
129
|
+
const box = chip.getBoundingClientRect();
|
|
130
|
+
size.width = box.width;
|
|
131
|
+
size.height = box.height;
|
|
132
|
+
}
|
|
133
|
+
if (Boolean(text) === visible) return;
|
|
134
|
+
visible = Boolean(text);
|
|
135
|
+
if (!visible) label = '';
|
|
136
|
+
gsap.to(chip, {
|
|
137
|
+
autoAlpha: visible ? 1 : 0,
|
|
138
|
+
scale: visible ? 1 : 0.8,
|
|
139
|
+
duration: appear.duration,
|
|
140
|
+
ease: appear.ease,
|
|
141
|
+
overwrite: 'auto',
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// ── Dot ───────────────────────────────────────────────────────────────────
|
|
146
|
+
let inZone = false;
|
|
147
|
+
let overHide = false;
|
|
148
|
+
const syncDot = () => scope.attr(host, 'data-mk-hidden', inZone && !overHide ? null : '');
|
|
149
|
+
const wordFor = (target: Element | null) => {
|
|
150
|
+
if (!word) return;
|
|
151
|
+
const source = target?.closest<HTMLElement>('[data-mk-cursor]');
|
|
152
|
+
word.textContent = source && zone.contains(source) ? (source.dataset.mkCursor ?? '') : (options.text ?? '');
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ── Events ────────────────────────────────────────────────────────────────
|
|
156
|
+
const place = (event: PointerEvent) => {
|
|
157
|
+
if (event.pointerType === 'touch') return;
|
|
158
|
+
pointer.x = event.clientX;
|
|
159
|
+
pointer.y = event.clientY;
|
|
160
|
+
if (!pointer.placed) {
|
|
161
|
+
pointer.placed = true;
|
|
162
|
+
if (variant === 'dot') {
|
|
163
|
+
position.x = pointer.x;
|
|
164
|
+
position.y = pointer.y;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
scope.on(window, 'pointermove', (event) => place(event as PointerEvent), { passive: true });
|
|
169
|
+
const overTarget = variant === 'chip' ? document : zone;
|
|
170
|
+
scope.on(overTarget, 'pointerover', (event) => {
|
|
171
|
+
const target = event.target as Element | null;
|
|
172
|
+
if (variant === 'chip') {
|
|
173
|
+
const source = target?.closest<HTMLElement>('[data-mk-cursor]') ?? null;
|
|
174
|
+
showChip(source && zone.contains(source) ? source : null);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
overHide = Boolean(target?.closest('[data-mk-cursor-hide]'));
|
|
178
|
+
wordFor(target);
|
|
179
|
+
syncDot();
|
|
180
|
+
});
|
|
181
|
+
if (variant === 'dot') {
|
|
182
|
+
scope.on(zone, 'pointerenter', (event) => {
|
|
183
|
+
place(event as PointerEvent);
|
|
184
|
+
inZone = true;
|
|
185
|
+
visible = true;
|
|
186
|
+
syncDot();
|
|
187
|
+
});
|
|
188
|
+
scope.on(zone, 'pointerleave', () => {
|
|
189
|
+
inZone = false;
|
|
190
|
+
visible = false;
|
|
191
|
+
scope.attr(host, 'data-mk-pressed', null);
|
|
192
|
+
syncDot();
|
|
193
|
+
});
|
|
194
|
+
scope.on(zone, 'pointerdown', () => scope.attr(host, 'data-mk-pressed', ''));
|
|
195
|
+
scope.on(window, 'pointerup', () => scope.attr(host, 'data-mk-pressed', null));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Frame ─────────────────────────────────────────────────────────────────
|
|
199
|
+
const [stiffness, damping] = TIMING.springs.cursorFollow;
|
|
200
|
+
const update = (dt = 1) => {
|
|
201
|
+
if (variant === 'chip') {
|
|
202
|
+
// Dousan keeps the chip inside the window, 10 px from its edges.
|
|
203
|
+
const halfW = size.width / 2 + t.gutter;
|
|
204
|
+
const halfH = size.height / 2 + t.gutter;
|
|
205
|
+
const tx = size.width ? Math.min(Math.max(pointer.x, halfW), innerWidth - halfW) : pointer.x;
|
|
206
|
+
const ty = size.height ? Math.min(Math.max(pointer.y, halfH), innerHeight - halfH) : pointer.y;
|
|
207
|
+
// Motion's spring, integrated in small steps.
|
|
208
|
+
const steps = Math.max(1, Math.ceil(dt * 4));
|
|
209
|
+
const h = dt / 60 / steps;
|
|
210
|
+
for (let i = 0; i < steps; i++) {
|
|
211
|
+
velocity.x += (stiffness * (tx - position.x) - damping * velocity.x) * h;
|
|
212
|
+
velocity.y += (stiffness * (ty - position.y) - damping * velocity.y) * h;
|
|
213
|
+
position.x += velocity.x * h;
|
|
214
|
+
position.y += velocity.y * h;
|
|
215
|
+
}
|
|
216
|
+
if (visible && typeof document.elementFromPoint === 'function') {
|
|
217
|
+
tone = backgroundTone(document.elementFromPoint(pointer.x, pointer.y));
|
|
218
|
+
if (chip && chip.dataset.mkTone !== tone) chip.dataset.mkTone = tone;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
const step = 1 - Math.pow(1 - t.dotLerp, dt);
|
|
222
|
+
position.x += (pointer.x - position.x) * step;
|
|
223
|
+
position.y += (pointer.y - position.y) * step;
|
|
224
|
+
}
|
|
225
|
+
host.style.transform = `translate3d(${position.x.toFixed(2)}px, ${position.y.toFixed(2)}px, 0) translate(-50%, -50%)`;
|
|
226
|
+
};
|
|
227
|
+
scope.add(subscribe(update));
|
|
228
|
+
update(0);
|
|
229
|
+
host.dataset.mkReady = '';
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
get visible() {
|
|
233
|
+
return visible;
|
|
234
|
+
},
|
|
235
|
+
get label() {
|
|
236
|
+
return label;
|
|
237
|
+
},
|
|
238
|
+
get position() {
|
|
239
|
+
return { ...position };
|
|
240
|
+
},
|
|
241
|
+
get tone() {
|
|
242
|
+
return tone;
|
|
243
|
+
},
|
|
244
|
+
update,
|
|
245
|
+
destroy() {
|
|
246
|
+
if (chip) gsap.killTweensOf(chip);
|
|
247
|
+
scope.dispose();
|
|
248
|
+
for (const node of made.splice(0)) node.remove();
|
|
249
|
+
host.style.removeProperty('transform');
|
|
250
|
+
if (host.getAttribute('style') === '') host.removeAttribute('style');
|
|
251
|
+
delete host.dataset.mkReady;
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|