@dofy/native-emoji-motion 0.6.4 → 0.7.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/CHANGELOG.md +9 -0
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/EmojiMotion.svelte +1 -1
- package/src/EmojiMotionLite.svelte +1 -1
- package/src/element-core.js +1 -1
- package/src/enhance.js +1 -1
- package/src/index.d.ts +2 -2
- package/src/presets.js +150 -49
- package/src/react-core.js +1 -1
- package/src/registry.js +23 -3
- package/src/vue-core.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 — 2026-11-14
|
|
4
|
+
|
|
5
|
+
- **Behavior change:** emoji that use the bounce, hop or spring motions, including most animals and faces, now move with the new rhythm. The game die, alarm clock, cartwheeling person and turtle use new motions, and the hammer leaves the hand tools family.
|
|
6
|
+
- Give springs, bounces, hops and pop a gravity-aware rhythm with anticipation, per-segment easing and squash on landing.
|
|
7
|
+
- Add toss, somersault, alarmRing, peek and slam motions for dice, cartwheels, alarm clocks and telephones, peeking faces and the hammer.
|
|
8
|
+
- Burst sparkles one after another, and add a scan effect for screens and documents.
|
|
9
|
+
- Let decoration layers paint behind the glyph, and read built-in effect colors from CSS custom properties.
|
|
10
|
+
- `bind()` ignores touch hover, mouse focus and repeats within its new `cooldown` (260 ms by default).
|
|
11
|
+
|
|
3
12
|
## 0.6.4 — 2026-11-14
|
|
4
13
|
|
|
5
14
|
- Add ripple, splash and wake effects for water.
|
package/README.md
CHANGED
|
@@ -24,13 +24,13 @@ Bring 👋, ❤️, 📡 and more to life in React, Vue, Svelte or plain HTML. E
|
|
|
24
24
|
|
|
25
25
|
## 📊 Collection at a glance
|
|
26
26
|
|
|
27
|
-
| In version 0.
|
|
27
|
+
| In version 0.7.0 | Count | What it covers |
|
|
28
28
|
| --- | ---: | --- |
|
|
29
|
-
| Handcrafted presets |
|
|
30
|
-
| Reviewed motion families |
|
|
29
|
+
| Handcrafted presets | 182 | Individually configured base emoji |
|
|
30
|
+
| Reviewed motion families | 34 | 223 additional base emoji sharing reviewed motions |
|
|
31
31
|
| Categories | 10 | Groups used to organize the collection |
|
|
32
|
-
| Reusable motions |
|
|
33
|
-
| Emoji with optional SVG effects |
|
|
32
|
+
| Reusable motions | 58 | Motions shared by presets and families |
|
|
33
|
+
| Emoji with optional SVG effects | 106 | Base emoji with decorative effects |
|
|
34
34
|
| Recognized Unicode sequences | 5,235 | Unicode Emoji 18.0 sequences, including skin tones and other variants |
|
|
35
35
|
|
|
36
36
|
Skin tones and other variants count as additional Unicode sequences. Similar emoji may share a motion. Recognized emoji without a preset or reviewed family receive a category-based fallback.
|
package/package.json
CHANGED
package/src/EmojiMotion.svelte
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
<span bind:this={wrapper} data-emoji-motion="component" role="img" aria-label={label ?? resolved?.name ?? emoji}
|
|
16
16
|
tabindex={trigger ? -1 : 0}>
|
|
17
|
-
<span style="display: inline-block; position: relative">
|
|
17
|
+
<span style="display: inline-block; position: relative; isolation: isolate">
|
|
18
18
|
<span aria-hidden="true" style="display: inline-block"
|
|
19
19
|
use:animateEmoji={{ emoji, skinTone, registry, trigger: trigger ?? wrapper, decoration, events }}>{resolved?.glyph ?? emoji}</span>
|
|
20
20
|
</span>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
<span bind:this={wrapper} data-emoji-motion="component" role="img" aria-label={label ?? resolved?.name ?? emoji}
|
|
16
16
|
tabindex={trigger ? -1 : 0}>
|
|
17
|
-
<span style="display: inline-block; position: relative">
|
|
17
|
+
<span style="display: inline-block; position: relative; isolation: isolate">
|
|
18
18
|
<span aria-hidden="true" style="display: inline-block"
|
|
19
19
|
use:animateEmoji={{ emoji, skinTone, registry, trigger: trigger ?? wrapper, decoration, events }}>{resolved?.glyph ?? emoji}</span>
|
|
20
20
|
</span>
|
package/src/element-core.js
CHANGED
|
@@ -19,7 +19,7 @@ export function createEmojiMotionElement(emojiMotion) {
|
|
|
19
19
|
this.setAttribute('aria-label', this.getAttribute('label') ?? result?.name ?? emoji);
|
|
20
20
|
if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
|
|
21
21
|
const visual = this.ownerDocument.createElement('span');
|
|
22
|
-
Object.assign(visual.style, { display: 'inline-block', position: 'relative' });
|
|
22
|
+
Object.assign(visual.style, { display: 'inline-block', position: 'relative', isolation: 'isolate' });
|
|
23
23
|
const glyph = this.ownerDocument.createElement('span');
|
|
24
24
|
Object.assign(glyph.style, { display: 'inline-block' });
|
|
25
25
|
glyph.setAttribute('aria-hidden', 'true'); glyph.textContent = result?.glyph ?? emoji;
|
package/src/enhance.js
CHANGED
|
@@ -36,7 +36,7 @@ export function enhance(registry, options = {}) {
|
|
|
36
36
|
if (index > cursor) fragment.append(node.ownerDocument.createTextNode(text.slice(cursor, index)));
|
|
37
37
|
const wrapper = node.ownerDocument.createElement('span');
|
|
38
38
|
wrapper.setAttribute('data-emoji-motion', 'enhanced');
|
|
39
|
-
Object.assign(wrapper.style, { display: 'inline-block', position: 'relative' });
|
|
39
|
+
Object.assign(wrapper.style, { display: 'inline-block', position: 'relative', isolation: 'isolate' });
|
|
40
40
|
const inner = node.ownerDocument.createElement('span');
|
|
41
41
|
inner.style.display = 'inline-block';
|
|
42
42
|
inner.textContent = glyph;
|
package/src/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export interface Motion { keyframes: Keyframe[]; options?: KeyframeAnimationOpti
|
|
|
2
2
|
export interface Group { label: string; description?: string; motion: string }
|
|
3
3
|
export interface Anchor { x: string; y: string }
|
|
4
4
|
export interface SvgShape { tag: 'path' | 'circle' | 'ellipse' | 'rect' | 'line' | 'polyline' | 'polygon'; attrs: Record<string, string> }
|
|
5
|
-
export interface DecorationLayer { style: Partial<CSSStyleDeclaration>; keyframes: Keyframe[]; options?: KeyframeAnimationOptions; svg?: { viewBox: string; elements: SvgShape[] } }
|
|
5
|
+
export interface DecorationLayer { /** Paint behind the glyph; the container must create a stacking context, for example with isolation: isolate. */ behind?: boolean; style: Partial<CSSStyleDeclaration>; keyframes: Keyframe[]; options?: KeyframeAnimationOptions; svg?: { viewBox: string; elements: SvgShape[] } }
|
|
6
6
|
export interface Decoration { anchor: Anchor; layers: DecorationLayer[] }
|
|
7
7
|
export type SkinTone = 'default' | 'light' | 'medium-light' | 'medium' | 'medium-dark' | 'dark';
|
|
8
8
|
export interface EmojiEntry { group: string; name: string; motion?: string; decoration?: string; variants?: string[]; skinTones?: Partial<Record<Exclude<SkinTone, 'default'>, string>> }
|
|
@@ -10,7 +10,7 @@ export interface ClassificationRule { group: string; motion?: string }
|
|
|
10
10
|
export interface MotionFamily { label: string; group: string; motion: string; members: string[]; aliases?: Record<string, string>; decoration?: string; decorations?: string[]; animation?: KeyframeAnimationOptions }
|
|
11
11
|
export interface Config { defaults?: boolean; autoDetect?: boolean; classifications?: { groups?: Record<string, ClassificationRule>; subgroups?: Record<string, ClassificationRule>; fallback?: ClassificationRule }; defaultSkinTone?: SkinTone; motions?: Record<string, Motion>; decorations?: Record<string, Decoration>; groups?: Record<string, Group>; emoji?: Record<string, EmojiEntry>; families?: Record<string, MotionFamily> }
|
|
12
12
|
export interface PlayOptions { ignoreReducedMotion?: boolean; skinTone?: SkinTone; animation?: KeyframeAnimationOptions; decoration?: { container: HTMLElement; anchor?: Anchor; flipX?: boolean; rotate?: number } }
|
|
13
|
-
export interface BindOptions extends PlayOptions { trigger?: EventTarget; events?: string[] }
|
|
13
|
+
export interface BindOptions extends PlayOptions { trigger?: EventTarget; events?: string[]; /** Milliseconds during which repeated triggers are ignored. Defaults to 260. */ cooldown?: number }
|
|
14
14
|
export interface Resolution extends Omit<EmojiEntry, 'group' | 'motion'> { glyph: string; canonical: string; skinTone: SkinTone; groupId: string; group: Group; motionId: string; motion: Motion; decorationPreset?: Decoration; decorationPresets: Decoration[]; familyId?: string; animation?: KeyframeAnimationOptions; source: 'user' | 'user-classification' | 'preset' | 'family' | 'unicode' | 'fallback'; unicodeGroup?: string; unicodeSubgroup?: string }
|
|
15
15
|
export interface EnhanceOptions { root?: string | Element | Document | DocumentFragment; exclude?: string; enabled?: boolean; observe?: boolean; decoration?: boolean; events?: string[] }
|
|
16
16
|
export interface EmojiEnhancer { enable(): void; disable(): void; cleanup(): void }
|
package/src/presets.js
CHANGED
|
@@ -3,15 +3,27 @@ function boundedNumber(name, value, min, max) {
|
|
|
3
3
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) throw new RangeError(`${name} must be between ${min} and ${max}`);
|
|
4
4
|
return value;
|
|
5
5
|
}
|
|
6
|
+
// Gravity easings: rising slows towards the top, falling speeds up into the landing.
|
|
7
|
+
const rise = 'cubic-bezier(.2,.7,.35,1)';
|
|
8
|
+
const fall = 'cubic-bezier(.6,0,.85,.45)';
|
|
9
|
+
const crouch = 'cubic-bezier(.3,0,.6,1)';
|
|
10
|
+
const settle = 'ease-out';
|
|
6
11
|
/** Build a direction-neutral bounce; height is a percentage of glyph height. */
|
|
7
12
|
export function createSpringMotion({ height = 24, rebounds = 1, duration = 900 } = {}) {
|
|
8
13
|
boundedNumber('height', height, 0, 60); boundedNumber('duration', duration, 1, 10000);
|
|
9
14
|
if (!Number.isInteger(rebounds) || rebounds < 0 || rebounds > 2) throw new RangeError('rebounds must be 0, 1, or 2');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
// Crouch before take-off, stretch while rising, squash on landing.
|
|
16
|
+
const frames = [
|
|
17
|
+
{ transform: 'translateY(0) scale(1)', offset: 0, easing: crouch },
|
|
18
|
+
{ transform: 'translateY(0) scale(1.08,.92)', offset: .12, easing: rise },
|
|
19
|
+
{ transform: `translateY(-${height}%) scale(.96,1.05)`, offset: .4, easing: fall },
|
|
20
|
+
{ transform: 'translateY(0) scale(1.12,.88)', offset: .6, easing: rebounds ? rise : settle }
|
|
21
|
+
];
|
|
22
|
+
if (rebounds >= 1) frames.push({ transform: `translateY(-${height * .28}%) scale(.98,1.02)`, offset: .74, easing: fall }, { transform: 'translateY(0) scale(1.04,.96)', offset: .85, easing: rebounds === 2 ? rise : settle });
|
|
23
|
+
if (rebounds === 2) frames.push({ transform: `translateY(-${height * .1}%) scale(1)`, offset: .92, easing: fall }, { transform: 'translateY(0) scale(1.01,.99)', offset: .97, easing: settle });
|
|
24
|
+
if (!rebounds) frames.push({ transform: 'translateY(0) scale(.98,1.02)', offset: .8, easing: settle });
|
|
13
25
|
frames.push({ transform: 'translateY(0) scale(1)', offset: 1 });
|
|
14
|
-
return { keyframes: frames, options: { duration, easing:
|
|
26
|
+
return { keyframes: frames, options: { duration, easing: 'linear' }, origin: '50% 90%' };
|
|
15
27
|
}
|
|
16
28
|
/** Build a balanced sway; no assumption about the native glyph's direction. */
|
|
17
29
|
export function createSwayMotion({ angle = 8, duration = 1200, origin = '50% 90%' } = {}) {
|
|
@@ -23,7 +35,7 @@ export const motions = {
|
|
|
23
35
|
glide: { keyframes: [{ transform: 'translate(0, 0) rotate(0deg)' }, { transform: 'translate(-12%, 8%) rotate(-9deg)', offset: .25 }, { transform: 'translate(12%, -22%) rotate(9deg)', offset: .6 }, { transform: 'translate(0, 0) rotate(0deg)' }], options: { duration: 1050, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
24
36
|
ride: { keyframes: [{ transform: 'translateY(0) rotate(0deg)' }, { transform: 'translateY(-15%) rotate(-6deg)', offset: .24 }, { transform: 'translateY(0) rotate(4deg)', offset: .48 }, { transform: 'translateY(-9%) rotate(-4deg)', offset: .68 }, { transform: 'translateY(0) rotate(0deg)' }], options: { duration: 950, easing: 'cubic-bezier(.22,1,.36,1)' }, origin: '50% 80%' },
|
|
25
37
|
wave: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(18deg)' }, { transform: 'rotate(-12deg)' }, { transform: 'rotate(16deg)' }, { transform: 'rotate(-7deg)' }, { transform: 'rotate(0deg)' }], options: { duration: 850, easing: 'cubic-bezier(.22,1,.36,1)' }, origin: '70% 75%' },
|
|
26
|
-
bounce: {
|
|
38
|
+
bounce: { ...createSpringMotion({ height: 22, rebounds: 0, duration: 700 }), origin: '50% 85%' },
|
|
27
39
|
pulse: { keyframes: [{ transform: 'scale(1)' }, { transform: 'scale(1.25)', offset: .28 }, { transform: 'scale(.96)', offset: .5 }, { transform: 'scale(1.16)', offset: .68 }, { transform: 'scale(1)' }], options: { duration: 750, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
28
40
|
wobble: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(-13deg)' }, { transform: 'rotate(10deg)' }, { transform: 'rotate(-6deg)' }, { transform: 'rotate(3deg)' }, { transform: 'rotate(0deg)' }], options: { duration: 700, easing: 'cubic-bezier(.22,1,.36,1)' }, origin: '50% 80%' },
|
|
29
41
|
spin: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }], options: { duration: 800, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
@@ -32,14 +44,14 @@ export const motions = {
|
|
|
32
44
|
swing: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(18deg)' }, { transform: 'rotate(-15deg)' }, { transform: 'rotate(9deg)' }, { transform: 'rotate(-4deg)' }, { transform: 'rotate(0deg)' }], options: { duration: 850, easing: 'cubic-bezier(.22,1,.36,1)' }, origin: '50% 10%' },
|
|
33
45
|
float: { keyframes: [{ transform: 'translateY(0) rotate(0deg)' }, { transform: 'translateY(-18%) rotate(-8deg)', offset: .4 }, { transform: 'translateY(-10%) rotate(6deg)', offset: .7 }, { transform: 'translateY(0) rotate(0deg)' }], options: { duration: 1000, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
34
46
|
shake: { keyframes: [{ transform: 'translateX(0)' }, { transform: 'translateX(-10%) rotate(-8deg)' }, { transform: 'translateX(10%) rotate(8deg)' }, { transform: 'translateX(-6%) rotate(-5deg)' }, { transform: 'translateX(4%) rotate(3deg)' }, { transform: 'translateX(0)' }], options: { duration: 650, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
35
|
-
hop:
|
|
47
|
+
hop: createSpringMotion({ height: 28, rebounds: 1, duration: 850 }),
|
|
36
48
|
flutter: { keyframes: [{ transform: 'translateY(0) rotate(0deg)' }, { transform: 'translateY(-12%) rotate(-8deg)' }, { transform: 'translateY(-21%) rotate(8deg)' }, { transform: 'translateY(-10%) rotate(-5deg)' }, { transform: 'translateY(0) rotate(0deg)' }], options: { duration: 900, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
37
49
|
sway: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(-9deg)' }, { transform: 'rotate(8deg)' }, { transform: 'rotate(-5deg)' }, { transform: 'rotate(0deg)' }], options: { duration: 1100, easing: 'cubic-bezier(.22,1,.36,1)' }, origin: '50% 95%' },
|
|
38
50
|
flip: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(185deg)', offset: .5 }, { transform: 'rotate(360deg)' }], options: { duration: 1000, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
39
51
|
sip: { keyframes: [{ transform: 'translateY(0) rotate(0deg)' }, { transform: 'translateY(-8%) rotate(-6deg)', offset: .35 }, { transform: 'translateY(-8%) rotate(6deg)', offset: .65 }, { transform: 'translateY(0) rotate(0deg)' }], options: { duration: 1000, easing: 'cubic-bezier(.22,1,.36,1)' }, origin: '50% 80%' },
|
|
40
52
|
twirl: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(-10deg)' }, { transform: 'rotate(12deg)' }, { transform: 'rotate(-6deg)' }, { transform: 'rotate(0deg)' }], options: { duration: 950, easing: 'cubic-bezier(.22,1,.36,1)' }, origin: '50% 80%' },
|
|
41
53
|
nod: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(8deg)', offset: .3 }, { transform: 'rotate(-4deg)', offset: .6 }, { transform: 'rotate(0deg)' }], options: { duration: 850, easing: 'cubic-bezier(.22,1,.36,1)' }, origin: '50% 85%' },
|
|
42
|
-
pop: { keyframes: [{ transform: 'scale(1)' }, { transform: 'scale(.
|
|
54
|
+
pop: { keyframes: [{ transform: 'scale(1)', easing: crouch }, { transform: 'scale(.84)', offset: .18, easing: rise }, { transform: 'scale(1.2)', offset: .45, easing: 'ease-in-out' }, { transform: 'scale(.95)', offset: .68, easing: settle }, { transform: 'scale(1.04)', offset: .84, easing: settle }, { transform: 'scale(1)' }], options: { duration: 750, easing: 'linear' } },
|
|
43
55
|
bob: { keyframes: [{ transform: 'translateY(0) rotate(0deg)' }, { transform: 'translateY(-12%) rotate(-4deg)', offset: .3 }, { transform: 'translateY(4%) rotate(4deg)', offset: .65 }, { transform: 'translateY(0) rotate(0deg)' }], options: { duration: 1000, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
44
56
|
breathe: { keyframes: [{ transform: 'scale(1)' }, { transform: 'scale(1.09,1.05)', offset: .48 }, { transform: 'scale(1)' }], options: { duration: 1200, easing: 'cubic-bezier(.22,1,.36,1)' } },
|
|
45
57
|
spring: createSpringMotion(),
|
|
@@ -62,7 +74,7 @@ export const motions = {
|
|
|
62
74
|
orbitFloat: { keyframes: [{ transform: 'translateY(0) scale(1)' }, { transform: 'translateY(-9%) scale(1.04)', offset: .45 }, { transform: 'translateY(0) scale(1)' }], options: { duration: 1200, easing: curve } },
|
|
63
75
|
writingTilt: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(-5deg)', offset: .3 }, { transform: 'rotate(5deg)', offset: .65 }, { transform: 'rotate(0deg)' }], options: { duration: 1000, easing: curve }, origin: '50% 65%' },
|
|
64
76
|
gardenBounce: { keyframes: [{ transform: 'translateY(0) rotate(0deg)' }, { transform: 'translateY(-6%) rotate(-3deg)', offset: .32 }, { transform: 'translateY(-3%) rotate(3deg)', offset: .65 }, { transform: 'translateY(0) rotate(0deg)' }], options: { duration: 1050, easing: curve }, origin: '50% 85%' },
|
|
65
|
-
animalHop:
|
|
77
|
+
animalHop: createSpringMotion({ height: 11, rebounds: 0, duration: 900 }),
|
|
66
78
|
fabricSway: createSwayMotion({ angle: 6, duration: 1250, origin: '50% 15%' }),
|
|
67
79
|
toolWiggle: createSwayMotion({ angle: 7, duration: 900, origin: '50% 70%' }),
|
|
68
80
|
bookPeek: { keyframes: [{ transform: 'perspective(500px) rotateY(0deg) translateY(0)' }, { transform: 'perspective(500px) rotateY(-20deg) translateY(-3%)', offset: .32 }, { transform: 'perspective(500px) rotateY(20deg) translateY(-3%)', offset: .68 }, { transform: 'perspective(500px) rotateY(0deg) translateY(0)' }], options: { duration: 1050, easing: curve } },
|
|
@@ -72,6 +84,64 @@ export const motions = {
|
|
|
72
84
|
clockTick: { keyframes: [{ transform: 'rotate(0deg)' }, { transform: 'rotate(-3deg)', offset: .25 }, { transform: 'rotate(3deg)', offset: .5 }, { transform: 'rotate(-2deg)', offset: .75 }, { transform: 'rotate(0deg)' }], options: { duration: 1200, easing: curve }, origin: '50% 50%' },
|
|
73
85
|
gentleFall: { keyframes: [{ transform: 'translateY(0) scale(1)' }, { transform: 'translateY(7%) scale(.98,1.02)', offset: .45 }, { transform: 'translateY(0) scale(1)' }], options: { duration: 1100, easing: curve } },
|
|
74
86
|
softSettle: { keyframes: [{ transform: 'translateY(0) scale(1)' }, { transform: 'translateY(-5%) scale(1)', offset: .35 }, { transform: 'translateY(0) scale(1.03,.97)', offset: .7 }, { transform: 'translateY(0) scale(1)' }], options: { duration: 950, easing: curve }, origin: '50% 90%' },
|
|
87
|
+
// Rhythm motions: a beat of anticipation, the move, a held pause, then a reversal into rest.
|
|
88
|
+
// Tumble up, hang near the top, drop flat, overshoot and settle back.
|
|
89
|
+
toss: { keyframes: [
|
|
90
|
+
{ transform: 'translateY(0) rotate(0deg) scale(1)', easing: crouch },
|
|
91
|
+
{ transform: 'translateY(4%) rotate(-10deg) scale(1.12,.88)', offset: .12, easing: 'cubic-bezier(.15,.75,.35,1)' },
|
|
92
|
+
{ transform: 'translateY(-56%) rotate(190deg) scale(1.06)', offset: .44, easing: 'cubic-bezier(.45,.05,.55,.95)' },
|
|
93
|
+
{ transform: 'translateY(-50%) rotate(255deg) scale(1.06)', offset: .56, easing: fall },
|
|
94
|
+
{ transform: 'translateY(0) rotate(360deg) scale(1.14,.86)', offset: .76, easing: settle },
|
|
95
|
+
{ transform: 'translateY(-4%) rotate(366deg) scale(.98,1.02)', offset: .86, easing: settle },
|
|
96
|
+
{ transform: 'translateY(0) rotate(357deg) scale(1)', offset: .93, easing: settle },
|
|
97
|
+
{ transform: 'translateY(0) rotate(360deg) scale(1)' }
|
|
98
|
+
], options: { duration: 1000, easing: 'linear' }, origin: '50% 55%' },
|
|
99
|
+
// A full roll whose slowest part is the top, so the turn seems to hang before landing.
|
|
100
|
+
somersault: { keyframes: [
|
|
101
|
+
{ transform: 'translateY(0) rotate(0deg) scale(1)', easing: crouch },
|
|
102
|
+
{ transform: 'translateY(5%) rotate(0deg) scale(1.1,.9)', offset: .14, easing: rise },
|
|
103
|
+
{ transform: 'translateY(-40%) rotate(-150deg) scale(1)', offset: .36, easing: 'linear' },
|
|
104
|
+
{ transform: 'translateY(-44%) rotate(-215deg) scale(1)', offset: .5, easing: fall },
|
|
105
|
+
{ transform: 'translateY(0) rotate(-360deg) scale(1.12,.88)', offset: .72, easing: settle },
|
|
106
|
+
{ transform: 'translateY(-3%) rotate(-360deg) scale(.98,1.02)', offset: .86, easing: settle },
|
|
107
|
+
{ transform: 'translateY(0) rotate(-360deg) scale(1)' }
|
|
108
|
+
], options: { duration: 1100, easing: 'linear' }, origin: '50% 60%' },
|
|
109
|
+
// Two bursts of quick shaking separated by a held breath, like an alarm clock.
|
|
110
|
+
alarmRing: { keyframes: [
|
|
111
|
+
{ transform: 'translateY(0) rotate(0deg)', easing: 'ease-out' },
|
|
112
|
+
{ transform: 'translateY(-8%) rotate(-16deg)', offset: .06 },
|
|
113
|
+
{ transform: 'translateY(-8%) rotate(16deg)', offset: .12 },
|
|
114
|
+
{ transform: 'translateY(-8%) rotate(-15deg)', offset: .18 },
|
|
115
|
+
{ transform: 'translateY(-8%) rotate(15deg)', offset: .24 },
|
|
116
|
+
{ transform: 'translateY(-8%) rotate(-9deg)', offset: .3, easing: 'ease-out' },
|
|
117
|
+
{ transform: 'translateY(-7%) rotate(0deg)', offset: .36 },
|
|
118
|
+
{ transform: 'translateY(-6%) rotate(0deg)', offset: .5 },
|
|
119
|
+
{ transform: 'translateY(-6%) rotate(-12deg)', offset: .56 },
|
|
120
|
+
{ transform: 'translateY(-6%) rotate(12deg)', offset: .62 },
|
|
121
|
+
{ transform: 'translateY(-6%) rotate(-8deg)', offset: .68 },
|
|
122
|
+
{ transform: 'translateY(-5%) rotate(6deg)', offset: .74, easing: 'ease-out' },
|
|
123
|
+
{ transform: 'translateY(0) rotate(-2deg)', offset: .86, easing: settle },
|
|
124
|
+
{ transform: 'translateY(0) rotate(0deg)' }
|
|
125
|
+
], options: { duration: 1100, easing: 'linear' }, origin: '50% 60%' },
|
|
126
|
+
// Duck out of sight, wait, then pop back with a look one way and the other.
|
|
127
|
+
peek: { keyframes: [
|
|
128
|
+
{ transform: 'translateY(0) scale(1) rotate(0deg)', opacity: 1, easing: 'cubic-bezier(.5,0,.75,0)' },
|
|
129
|
+
{ transform: 'translateY(8%) scale(.6) rotate(0deg)', opacity: .2, offset: .16, easing: 'linear' },
|
|
130
|
+
{ transform: 'translateY(8%) scale(.6) rotate(0deg)', opacity: .2, offset: .4, easing: rise },
|
|
131
|
+
{ transform: 'translateY(-6%) scale(1.08) rotate(10deg)', opacity: 1, offset: .58, easing: 'ease-in-out' },
|
|
132
|
+
{ transform: 'translateY(-4%) scale(1.04) rotate(-7deg)', opacity: 1, offset: .74, easing: settle },
|
|
133
|
+
{ transform: 'translateY(0) scale(1) rotate(2deg)', opacity: 1, offset: .88, easing: settle },
|
|
134
|
+
{ transform: 'translateY(0) scale(1) rotate(0deg)', opacity: 1 }
|
|
135
|
+
], options: { duration: 1100, easing: 'linear' } },
|
|
136
|
+
// Lift and hold at the top, then come down hard and squash on impact.
|
|
137
|
+
slam: { keyframes: [
|
|
138
|
+
{ transform: 'translateY(0) rotate(0deg) scale(1)', easing: rise },
|
|
139
|
+
{ transform: 'translateY(-30%) rotate(-10deg) scale(1.06)', offset: .34, easing: 'linear' },
|
|
140
|
+
{ transform: 'translateY(-32%) rotate(-12deg) scale(1.06)', offset: .44, easing: fall },
|
|
141
|
+
{ transform: 'translateY(3%) rotate(0deg) scale(1.18,.82)', offset: .56, easing: settle },
|
|
142
|
+
{ transform: 'translateY(-4%) rotate(2deg) scale(.96,1.04)', offset: .72, easing: settle },
|
|
143
|
+
{ transform: 'translateY(0) rotate(0deg) scale(1)' }
|
|
144
|
+
], options: { duration: 900, easing: 'linear' }, origin: '50% 90%' },
|
|
75
145
|
steps: { keyframes: [{ transform: 'translateY(0)' }, { transform: 'translateY(-7%)', offset: .25 }, { transform: 'translateY(0)', offset: .5 }, { transform: 'translateY(-5%)', offset: .75 }, { transform: 'translateY(0)' }], options: { duration: 1000, easing: curve }, origin: '50% 90%' }
|
|
76
146
|
};
|
|
77
147
|
|
|
@@ -117,7 +187,7 @@ export const families = {
|
|
|
117
187
|
vegetables: { label: 'Vegetables', group: 'food', motion: 'gardenBounce', members: ['🥕', '🥦', '🥬', '🥒', '🌽', '🫑', '🍆', '🧅', '🧄', '🥔'] },
|
|
118
188
|
landAnimals: { label: 'Land animals', group: 'nature', motion: 'animalHop', members: ['🐕', '🐈', '🐖', '🐑', '🐐', '🐄', '🐎', '🦌', '🐘', '🦒'] },
|
|
119
189
|
clothing: { label: 'Clothing', group: 'objects', motion: 'fabricSway', members: ['👕', '👖', '🧥', '👗', '🩳', '👚', '👔'] },
|
|
120
|
-
handTools: { label: 'Hand tools', group: 'objects', motion: 'toolWiggle', members: ['
|
|
190
|
+
handTools: { label: 'Hand tools', group: 'objects', motion: 'toolWiggle', members: ['🪓', '🪚', '🪛', '🔧', '🪠', '🔩'] },
|
|
121
191
|
books: { label: 'Books', group: 'objects', motion: 'bookPeek', members: ['📕', '📗', '📘', '📙', '📔', '📓', '📒'] },
|
|
122
192
|
handGestures: { label: 'Hand gestures', group: 'gestures', motion: 'handEmphasis', members: ['🤏', '🫳', '🫴', '🫲', '🫱', '🫷', '🫸', '🤌', '🫵'], aliases: familyToneAliases(['🤏', '🫳', '🫴', '🫲', '🫱', '🫷', '🫸', '🤌', '🫵']) },
|
|
123
193
|
vessels: { label: 'Boats', group: 'travel', motion: 'vesselRock', members: ['🚤', '🛥️', '🛳️', '⛴️', '🛶'], aliases: { '🛥': '🛥️', '🛳': '🛳️', '⛴': '⛴️' }, decoration: 'wake' },
|
|
@@ -134,6 +204,9 @@ export const families = {
|
|
|
134
204
|
poses: { label: 'People in poses', group: 'people', motion: 'pose', members: ['🧎', '🧍', '🙇'], aliases: familyToneAliases(['🧎', '🧍', '🙇']) },
|
|
135
205
|
professionals: { label: 'Professionals', group: 'people', motion: 'pose', members: roleMembers, aliases: roleAliases },
|
|
136
206
|
walkers: { label: 'Walking and running', group: 'people', motion: 'steps', members: walkerMembers, aliases: walkerAliases },
|
|
207
|
+
ringers: { label: 'Ringing things', group: 'objects', motion: 'alarmRing', members: ['☎️', '📞', '🛎️'], aliases: { '☎': '☎️', '🛎': '🛎️' }, decoration: 'sound' },
|
|
208
|
+
peekers: { label: 'Peeking faces', group: 'faces', motion: 'peek', members: ['🫣', '🙈', '🙉', '🙊'] },
|
|
209
|
+
screens: { label: 'Screens and documents', group: 'objects', motion: 'steady', members: ['💻', '🖥️', '📠', '🧾', '📋'], aliases: { '🖥': '🖥️' }, decoration: 'scan' },
|
|
137
210
|
swimmers: { label: 'Swimmers', group: 'people', motion: 'bob', members: gendered('🏊'), aliases: genderedToneAliases(gendered('🏊')), decoration: 'ripple' },
|
|
138
211
|
surfers: { label: 'Surfers', group: 'people', motion: 'ride', members: gendered('🏄'), aliases: genderedToneAliases(gendered('🏄')), decoration: 'splash' },
|
|
139
212
|
waterPolo: { label: 'Water polo', group: 'people', motion: 'bob', members: gendered('🤽'), aliases: genderedToneAliases(gendered('🤽')), decoration: 'splash' },
|
|
@@ -178,7 +251,7 @@ export const emoji = {
|
|
|
178
251
|
'☕': { group: 'food', name: 'Hot beverage', motion: 'sip', decoration: 'steam' }, '🍵': { group: 'food', name: 'Teacup', motion: 'sip', decoration: 'steam' }, '🍕': { group: 'food', name: 'Pizza', motion: 'bounce' }, '🍦': { group: 'food', name: 'Soft ice cream', motion: 'sway' },
|
|
179
252
|
'🏀': { group: 'activities', name: 'Basketball', motion: 'hop' }, '⚽': { group: 'activities', name: 'Soccer ball', motion: 'hop' }, '🎈': { group: 'activities', name: 'Balloon', motion: 'float' }, '🎉': { group: 'activities', name: 'Party popper', motion: 'bounce', decoration: 'sparkle' },
|
|
180
253
|
'💃': { group: 'people', name: 'Woman dancing', motion: 'twirl', skinTones: skinTones('💃') }, '🕺': { group: 'people', name: 'Man dancing', motion: 'twirl', skinTones: skinTones('🕺') },
|
|
181
|
-
'⏳': { group: 'objects', name: 'Hourglass', motion: 'hourglassTurn' }, '⏰': { group: 'objects', name: 'Alarm clock', motion: '
|
|
254
|
+
'⏳': { group: 'objects', name: 'Hourglass', motion: 'hourglassTurn' }, '⏰': { group: 'objects', name: 'Alarm clock', motion: 'alarmRing', decoration: 'sound' }, '🕯️': { group: 'objects', name: 'Candle', motion: 'steady', decoration: 'flicker', variants: ['🕯'] },
|
|
182
255
|
|
|
183
256
|
// Additional handcrafted entries share motions where their physical action fits.
|
|
184
257
|
'😀': { group: 'faces', name: 'Grinning face', motion: 'bounce' }, '🤣': { group: 'faces', name: 'Rolling on the floor laughing', motion: 'shake' },
|
|
@@ -191,7 +264,7 @@ export const emoji = {
|
|
|
191
264
|
'🫰': { group: 'gestures', name: 'Hand with index finger and thumb crossed', motion: 'pop', skinTones: skinTones('🫰') },
|
|
192
265
|
'🐱': { group: 'nature', name: 'Cat face', motion: 'nod' }, '🐻': { group: 'nature', name: 'Bear', motion: 'nod' }, '🐼': { group: 'nature', name: 'Panda', motion: 'sway' },
|
|
193
266
|
'🐸': { group: 'nature', name: 'Frog', motion: 'hop' }, '🐧': { group: 'nature', name: 'Penguin', motion: 'sway' }, '🐥': { group: 'nature', name: 'Front facing baby chick', motion: 'hop' },
|
|
194
|
-
'🐢': { group: 'nature', name: 'Turtle', motion: '
|
|
267
|
+
'🐢': { group: 'nature', name: 'Turtle', motion: 'peek' }, '🐟': { group: 'nature', name: 'Fish', motion: 'bob' }, '🐬': { group: 'nature', name: 'Dolphin', motion: 'hop', decoration: 'splash' },
|
|
195
268
|
'🐙': { group: 'nature', name: 'Octopus', motion: 'twirl' }, '🌵': { group: 'nature', name: 'Cactus', motion: 'sway' }, '🌲': { group: 'nature', name: 'Evergreen tree', motion: 'sway' },
|
|
196
269
|
'🌼': { group: 'nature', name: 'Blossom flower', motion: 'sway', decoration: 'petals' }, '❄️': { group: 'nature', name: 'Snowflake', motion: 'float', variants: ['❄'] }, '☁️': { group: 'nature', name: 'Cloud', motion: 'float', variants: ['☁'] },
|
|
197
270
|
'🍎': { group: 'food', name: 'Red apple', motion: 'nod' }, '🍉': { group: 'food', name: 'Watermelon', motion: 'wobble' }, '🍓': { group: 'food', name: 'Strawberry', motion: 'bounce' },
|
|
@@ -202,7 +275,7 @@ export const emoji = {
|
|
|
202
275
|
'🎨': { group: 'activities', name: 'Artist palette', motion: 'twirl' }, '🎸': { group: 'activities', name: 'Guitar', motion: 'sway', decoration: 'sound' },
|
|
203
276
|
'🥁': { group: 'activities', name: 'Drum', motion: 'shake', decoration: 'sound' }, '🎮': { group: 'activities', name: 'Game controller', motion: 'wobble' }, '🪁': { group: 'activities', name: 'Kite', motion: 'flutter' },
|
|
204
277
|
'🙋': { group: 'people', name: 'Person raising hand', motion: 'wave', skinTones: skinTones('🙋') }, '🧘': { group: 'people', name: 'Person in lotus position', motion: 'breathe', skinTones: skinTones('🧘') },
|
|
205
|
-
'🏃': { group: 'people', name: 'Person running', motion: 'hop', skinTones: skinTones('🏃') }, '🤸': { group: 'people', name: 'Person cartwheeling', motion: '
|
|
278
|
+
'🏃': { group: 'people', name: 'Person running', motion: 'hop', skinTones: skinTones('🏃') }, '🤸': { group: 'people', name: 'Person cartwheeling', motion: 'somersault', skinTones: skinTones('🤸') },
|
|
206
279
|
'🧑🍳': { group: 'people', name: 'Cook', motion: 'nod', skinTones: personSkinTones('🍳') }, '🧑🎨': { group: 'people', name: 'Artist', motion: 'twirl', skinTones: personSkinTones('🎨') },
|
|
207
280
|
'🧑🚀': { group: 'people', name: 'Astronaut', motion: 'float', skinTones: personSkinTones('🚀') }, '🧑💻': { group: 'people', name: 'Technologist', motion: 'nod', skinTones: personSkinTones('💻') },
|
|
208
281
|
'📱': { group: 'objects', name: 'Mobile phone', motion: 'shake' }, '📚': { group: 'objects', name: 'Books', motion: 'nod' }, '✉️': { group: 'objects', name: 'Envelope', motion: 'bob', variants: ['✉'] },
|
|
@@ -229,41 +302,63 @@ export const emoji = {
|
|
|
229
302
|
'🤿': { group: 'activities', name: 'Diving mask', motion: 'underwater', decoration: 'bubbles' },
|
|
230
303
|
'🛟': { group: 'travel', name: 'Ring buoy', motion: 'vesselRock', decoration: 'wake' },
|
|
231
304
|
'🪙': { group: 'objects', name: 'Coin', motion: 'flipHorizontal' },
|
|
232
|
-
'
|
|
305
|
+
'🔨': { group: 'objects', name: 'Hammer', motion: 'slam', decoration: 'impact' },
|
|
306
|
+
'🔢': { group: 'symbols', name: 'Input numbers', motion: 'toss' },
|
|
307
|
+
'🪃': { group: 'activities', name: 'Boomerang', motion: 'toss' },
|
|
308
|
+
'🥏': { group: 'activities', name: 'Flying disc', motion: 'toss' },
|
|
309
|
+
'📳': { group: 'symbols', name: 'Vibration mode', motion: 'alarmRing' },
|
|
310
|
+
'🎲': { group: 'activities', name: 'Game die', motion: 'toss' }
|
|
233
311
|
};
|
|
234
312
|
|
|
235
|
-
// Declarative SVG layers never alter the native glyph.
|
|
236
|
-
//
|
|
237
|
-
|
|
313
|
+
// Declarative SVG layers never alter the native glyph. Colors read a CSS custom
|
|
314
|
+
// property such as --emoji-motion-water and fall back to the listed value.
|
|
315
|
+
// Anchors are relative to the consumer's glyph container and may be overridden
|
|
316
|
+
// for a particular font.
|
|
317
|
+
const signalShape = { viewBox: '0 0 100 100', elements: [{ tag: 'circle', attrs: { cx: '50', cy: '50', r: '29', fill: 'none', stroke: 'var(--emoji-motion-signal, #7c8ee8)', 'stroke-width': '2.5' } }] };
|
|
238
318
|
const notesShape = { viewBox: '0 0 100 100', elements: [
|
|
239
|
-
{ tag: 'path', attrs: { d: 'M24 37c-7 3-7 8-3 10s9-1 9-5V23l12-3v15c-7 3-7 8-3 10s9-1 9-5V12l-24 6z', fill: '#b780c8' } },
|
|
240
|
-
{ tag: 'path', attrs: { d: 'M75 67c-5 2-5 6-2 7s7-1 7-4V54l8-2v12c-5 2-5 6-2 7s7-1 7-4V46l-18 5z', fill: '#dc8f72' } }
|
|
241
|
-
] };
|
|
242
|
-
const sparkleShape = { viewBox: '0 0 100 100', elements: [
|
|
243
|
-
{ tag: 'path', attrs: { d: 'M18 9c1.5 9 3 10.5 12 12-9 1.5-10.5 3-12 12-1.5-9-3-10.5-12-12 9-1.5 10.5-3 12-12z', fill: '#e0a63e' } },
|
|
244
|
-
{ tag: 'path', attrs: { d: 'M80 58c1 6 2 7 8 8-6 1-7 2-8 8-1-6-2-7-8-8 6-1 7-2 8-8z', fill: '#d787ad' } }
|
|
319
|
+
{ tag: 'path', attrs: { d: 'M24 37c-7 3-7 8-3 10s9-1 9-5V23l12-3v15c-7 3-7 8-3 10s9-1 9-5V12l-24 6z', fill: 'var(--emoji-motion-note, #b780c8)' } },
|
|
320
|
+
{ tag: 'path', attrs: { d: 'M75 67c-5 2-5 6-2 7s7-1 7-4V54l8-2v12c-5 2-5 6-2 7s7-1 7-4V46l-18 5z', fill: 'var(--emoji-motion-note-accent, #dc8f72)' } }
|
|
245
321
|
] };
|
|
246
322
|
const centered = { width: '160%', height: '160%', overflow: 'visible' };
|
|
323
|
+
// A four point star of radius r centered on (cx, cy) in viewBox units.
|
|
324
|
+
const star = (cx, cy, r) => { const k = r / 12; const f = value => +(value * k).toFixed(2); return `M${cx} ${cy - r}c${f(1.5)} ${f(9)} ${f(3)} ${f(10.5)} ${f(12)} ${f(12)}-${f(9)} ${f(1.5)}-${f(10.5)} ${f(3)}-${f(12)} ${f(12)}-${f(1.5)}-${f(9)}-${f(3)}-${f(10.5)}-${f(12)}-${f(12)} ${f(9)}-${f(1.5)} ${f(10.5)}-${f(3)} ${f(12)}-${f(12)}z`; };
|
|
325
|
+
// Sparkles burst one after another: a quick pop outwards, then a slower drift as they fade.
|
|
326
|
+
const sparkleBurst = [[24, 24, 9, -7, -7, 'sparkle'], [78, 28, 6, 6, -6, 'sparkle-accent'], [82, 70, 7, 6, 5, 'sparkle'], [20, 66, 5, -6, 3, 'sparkle-accent']].map(([cx, cy, r, dx, dy, color], index) => ({
|
|
327
|
+
svg: { viewBox: '0 0 100 100', elements: [{ tag: 'path', attrs: { d: star(cx, cy, r), fill: `var(--emoji-motion-${color}, ${color === 'sparkle' ? '#e0a63e' : '#d787ad'})` } }] },
|
|
328
|
+
style: { ...centered, transformOrigin: `${cx}% ${cy}%` },
|
|
329
|
+
keyframes: [
|
|
330
|
+
{ opacity: 0, transform: 'translate(-50%, -50%) scale(.3) rotate(0deg)', easing: 'cubic-bezier(.2,.8,.3,1)' },
|
|
331
|
+
{ opacity: 1, transform: `translate(${-50 + dx * .4}%, ${-50 + dy * .4}%) scale(1.1) rotate(20deg)`, offset: .3, easing: 'ease-out' },
|
|
332
|
+
{ opacity: 0, transform: `translate(${-50 + dx}%, ${-50 + dy}%) scale(.4) rotate(45deg)` }
|
|
333
|
+
],
|
|
334
|
+
options: { duration: 700, delay: index * 60, easing: 'linear', fill: 'backwards' }
|
|
335
|
+
}));
|
|
336
|
+
// The scan line sweeps down, rests a beat at the bottom, then glides back and fades.
|
|
337
|
+
const scanShape = { viewBox: '0 0 100 100', elements: [
|
|
338
|
+
{ tag: 'line', attrs: { x1: '24', y1: '22', x2: '76', y2: '22', stroke: 'var(--emoji-motion-scan, #76b6a4)', 'stroke-width': '7', 'stroke-linecap': 'round', opacity: '.25' } },
|
|
339
|
+
{ tag: 'line', attrs: { x1: '24', y1: '22', x2: '76', y2: '22', stroke: 'var(--emoji-motion-scan, #76b6a4)', 'stroke-width': '2.5', 'stroke-linecap': 'round' } }
|
|
340
|
+
] };
|
|
247
341
|
const steamShape = { viewBox: '0 0 100 100', elements: [
|
|
248
|
-
{ tag: 'path', attrs: { d: 'M38 30c-5-5 5-8 0-14M50 27c-5-5 5-8 0-14M62 30c-5-5 5-8 0-14', fill: 'none', stroke: '#a29aa5', 'stroke-width': '2.5', 'stroke-linecap': 'round' } }
|
|
342
|
+
{ tag: 'path', attrs: { d: 'M38 30c-5-5 5-8 0-14M50 27c-5-5 5-8 0-14M62 30c-5-5 5-8 0-14', fill: 'none', stroke: 'var(--emoji-motion-steam, #a29aa5)', 'stroke-width': '2.5', 'stroke-linecap': 'round' } }
|
|
249
343
|
] };
|
|
250
344
|
// Water effects sit at the waterline below the glyph and spread symmetrically.
|
|
251
345
|
// y is the waterline in viewBox units: at the body for swimmers, below the hull for boats.
|
|
252
|
-
const rippleRings = y => [0, 320].map(delay => ({
|
|
253
|
-
|
|
346
|
+
const rippleRings = (y, behind = false) => [0, 320].map(delay => ({
|
|
347
|
+
behind,
|
|
348
|
+
svg: { viewBox: '0 0 100 100', elements: [{ tag: 'ellipse', attrs: { cx: '50', cy: String(y), rx: '30', ry: '6', fill: 'none', stroke: 'var(--emoji-motion-water, #7cafd0)', 'stroke-width': '2.5' } }] },
|
|
254
349
|
style: { ...centered, transformOrigin: `50% ${y}%` },
|
|
255
350
|
keyframes: [{ opacity: .8, transform: 'translate(-50%, -50%) scale(.5)' }, { opacity: 0, transform: 'translate(-50%, -50%) scale(1.25)' }],
|
|
256
351
|
options: { duration: 1100, delay, easing: 'cubic-bezier(.25,.6,.4,1)', fill: 'backwards' }
|
|
257
352
|
}));
|
|
258
353
|
const splashShape = { viewBox: '0 0 100 100', elements: [
|
|
259
|
-
{ tag: 'path', attrs: { d: 'M39 70l-9-8M45 67l-3-10M61 70l9-8M55 67l3-10', fill: 'none', stroke: '#7cafd0', 'stroke-width': '2.5', 'stroke-linecap': 'round' } },
|
|
260
|
-
{ tag: 'circle', attrs: { cx: '25', cy: '56', r: '2.6', fill: '#7cafd0' } },
|
|
261
|
-
{ tag: 'circle', attrs: { cx: '39', cy: '50', r: '2', fill: '#7cafd0' } },
|
|
262
|
-
{ tag: 'circle', attrs: { cx: '75', cy: '56', r: '2.6', fill: '#7cafd0' } },
|
|
263
|
-
{ tag: 'circle', attrs: { cx: '61', cy: '50', r: '2', fill: '#7cafd0' } }
|
|
354
|
+
{ tag: 'path', attrs: { d: 'M39 70l-9-8M45 67l-3-10M61 70l9-8M55 67l3-10', fill: 'none', stroke: 'var(--emoji-motion-water, #7cafd0)', 'stroke-width': '2.5', 'stroke-linecap': 'round' } },
|
|
355
|
+
{ tag: 'circle', attrs: { cx: '25', cy: '56', r: '2.6', fill: 'var(--emoji-motion-water, #7cafd0)' } },
|
|
356
|
+
{ tag: 'circle', attrs: { cx: '39', cy: '50', r: '2', fill: 'var(--emoji-motion-water, #7cafd0)' } },
|
|
357
|
+
{ tag: 'circle', attrs: { cx: '75', cy: '56', r: '2.6', fill: 'var(--emoji-motion-water, #7cafd0)' } },
|
|
358
|
+
{ tag: 'circle', attrs: { cx: '61', cy: '50', r: '2', fill: 'var(--emoji-motion-water, #7cafd0)' } }
|
|
264
359
|
] };
|
|
265
360
|
const rainShape = { viewBox: '0 0 100 100', elements: [
|
|
266
|
-
{ tag: 'path', attrs: { d: 'M30 67v6M50 72v6M70 67v6', fill: 'none', stroke: '#7c9fca', 'stroke-width': '3', 'stroke-linecap': 'round' } }
|
|
361
|
+
{ tag: 'path', attrs: { d: 'M30 67v6M50 72v6M70 67v6', fill: 'none', stroke: 'var(--emoji-motion-water, #7c9fca)', 'stroke-width': '3', 'stroke-linecap': 'round' } }
|
|
267
362
|
] };
|
|
268
363
|
// Symmetric rays sit around the glyph rather than assuming a bulb, lens or flame position.
|
|
269
364
|
const lightShape = color => ({ viewBox: '0 0 100 100', elements: [
|
|
@@ -271,30 +366,31 @@ const lightShape = color => ({ viewBox: '0 0 100 100', elements: [
|
|
|
271
366
|
] });
|
|
272
367
|
export const decorations = {
|
|
273
368
|
bubbles: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: { viewBox: '0 0 100 100', elements: [
|
|
274
|
-
{ tag: 'circle', attrs: { cx: '21', cy: '46', r: '3', fill: 'none', stroke: '#7cafd0', 'stroke-width': '2' } },
|
|
275
|
-
{ tag: 'circle', attrs: { cx: '76', cy: '38', r: '4', fill: 'none', stroke: '#7cafd0', 'stroke-width': '2' } },
|
|
276
|
-
{ tag: 'circle', attrs: { cx: '80', cy: '60', r: '2', fill: 'none', stroke: '#7cafd0', 'stroke-width': '2' } }
|
|
369
|
+
{ tag: 'circle', attrs: { cx: '21', cy: '46', r: '3', fill: 'none', stroke: 'var(--emoji-motion-water, #7cafd0)', 'stroke-width': '2' } },
|
|
370
|
+
{ tag: 'circle', attrs: { cx: '76', cy: '38', r: '4', fill: 'none', stroke: 'var(--emoji-motion-water, #7cafd0)', 'stroke-width': '2' } },
|
|
371
|
+
{ tag: 'circle', attrs: { cx: '80', cy: '60', r: '2', fill: 'none', stroke: 'var(--emoji-motion-water, #7cafd0)', 'stroke-width': '2' } }
|
|
277
372
|
] }, style: centered, keyframes: [{ opacity: 0, transform: 'translate(-50%, -43%)' }, { opacity: .85, transform: 'translate(-50%, -50%)', offset: .35 }, { opacity: 0, transform: 'translate(-50%, -64%)' }], options: { duration: 1100, easing: curve } }] },
|
|
278
|
-
alertLight: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: lightShape('#df625e'), style: centered,
|
|
373
|
+
alertLight: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: lightShape('var(--emoji-motion-alert, #df625e)'), style: centered,
|
|
279
374
|
keyframes: [{ opacity: 0, transform: 'translate(-50%, -50%) scale(.85)' }, { opacity: .9, transform: 'translate(-50%, -50%) scale(1)', offset: .3 }, { opacity: 0, transform: 'translate(-50%, -50%) scale(1.12)' }],
|
|
280
375
|
options: { duration: 900, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
281
376
|
}] },
|
|
282
|
-
glow: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: lightShape('#e0a63e'), style: centered,
|
|
377
|
+
glow: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: lightShape('var(--emoji-motion-glow, #e0a63e)'), style: centered,
|
|
283
378
|
keyframes: [{ opacity: 0, transform: 'translate(-50%, -50%) scale(.85)' }, { opacity: .9, transform: 'translate(-50%, -50%) scale(1)', offset: .35 }, { opacity: 0, transform: 'translate(-50%, -50%) scale(1.08)' }],
|
|
284
379
|
options: { duration: 850, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
285
380
|
}] },
|
|
286
|
-
flicker: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: lightShape('#df985c'), style: centered,
|
|
381
|
+
flicker: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: lightShape('var(--emoji-motion-flame, #df985c)'), style: centered,
|
|
287
382
|
keyframes: [{ opacity: 0, transform: 'translate(-50%, -50%) scale(.9)' }, { opacity: .7, transform: 'translate(-50%, -50%) scale(1)', offset: .3 }, { opacity: .3, transform: 'translate(-50%, -50%) scale(.95)', offset: .5 }, { opacity: .75, transform: 'translate(-50%, -50%) scale(1.03)', offset: .7 }, { opacity: 0, transform: 'translate(-50%, -50%) scale(1)' }],
|
|
288
383
|
options: { duration: 1100, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
289
384
|
}] },
|
|
290
|
-
flash: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: lightShape('#a5b7df'), style: centered,
|
|
385
|
+
flash: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: lightShape('var(--emoji-motion-flash, #a5b7df)'), style: centered,
|
|
291
386
|
keyframes: [{ opacity: 0, transform: 'translate(-50%, -50%) scale(.75)' }, { opacity: 1, transform: 'translate(-50%, -50%) scale(1)', offset: .18 }, { opacity: 0, transform: 'translate(-50%, -50%) scale(1.2)', offset: .52 }, { opacity: 0, transform: 'translate(-50%, -50%) scale(1.2)' }],
|
|
292
387
|
options: { duration: 800, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
293
388
|
}] },
|
|
294
|
-
impact: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: { viewBox: '0 0 100 100', elements: [{ tag: 'path', attrs: { d: 'M28 75l-5 4M72 75l5 4M50 79v6', fill: 'none', stroke: '#d49b70', 'stroke-width': '3', 'stroke-linecap': 'round' } }] }, style: centered,
|
|
295
|
-
|
|
389
|
+
impact: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: { viewBox: '0 0 100 100', elements: [{ tag: 'path', attrs: { d: 'M28 75l-5 4M72 75l5 4M50 79v6', fill: 'none', stroke: 'var(--emoji-motion-impact, #d49b70)', 'stroke-width': '3', 'stroke-linecap': 'round' } }] }, style: centered,
|
|
390
|
+
// A linear timeline keeps the burst on the landing frame instead of drifting early.
|
|
391
|
+
keyframes: [{ opacity: 0, transform: 'translate(-50%, -50%) scale(.8)' }, { opacity: 0, transform: 'translate(-50%, -50%) scale(.8)', offset: .54, easing: 'ease-out' }, { opacity: .9, transform: 'translate(-50%, -50%) scale(1)', offset: .62, easing: settle }, { opacity: 0, transform: 'translate(-50%, -50%) scale(1.12)' }], options: { duration: 900, easing: 'linear' }
|
|
296
392
|
}] },
|
|
297
|
-
petals: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: { viewBox: '0 0 100 100', elements: [{ tag: 'ellipse', attrs: { cx: '24', cy: '33', rx: '3', ry: '5', fill: '#d89cb9' } }, { tag: 'ellipse', attrs: { cx: '76', cy: '38', rx: '3', ry: '5', fill: '#d89cb9' } }] }, style: centered,
|
|
393
|
+
petals: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: { viewBox: '0 0 100 100', elements: [{ tag: 'ellipse', attrs: { cx: '24', cy: '33', rx: '3', ry: '5', fill: 'var(--emoji-motion-petal, #d89cb9)' } }, { tag: 'ellipse', attrs: { cx: '76', cy: '38', rx: '3', ry: '5', fill: 'var(--emoji-motion-petal, #d89cb9)' } }] }, style: centered,
|
|
298
394
|
keyframes: [{ opacity: 0, transform: 'translate(-50%, -58%) rotate(-8deg)' }, { opacity: .8, transform: 'translate(-50%, -50%) rotate(0deg)', offset: .35 }, { opacity: 0, transform: 'translate(-50%, -35%) rotate(8deg)' }], options: { duration: 1200, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
299
395
|
}] },
|
|
300
396
|
steam: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: steamShape, style: centered,
|
|
@@ -306,7 +402,8 @@ export const decorations = {
|
|
|
306
402
|
options: { duration: 950, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
307
403
|
}] },
|
|
308
404
|
ripple: { anchor: { x: '50%', y: '50%' }, layers: rippleRings(66) },
|
|
309
|
-
|
|
405
|
+
// The hull hides the far side of its wake.
|
|
406
|
+
wake: { anchor: { x: '50%', y: '50%' }, layers: rippleRings(75, true) },
|
|
310
407
|
splash: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: splashShape, style: { ...centered, transformOrigin: '50% 70%' },
|
|
311
408
|
keyframes: [{ opacity: 0, transform: 'translate(-50%, -50%) scale(.55)' }, { opacity: .9, transform: 'translate(-50%, -50%) scale(1)', offset: .35 }, { opacity: 0, transform: 'translate(-50%, -54%) scale(1.15)' }],
|
|
312
409
|
options: { duration: 850, easing: curve }
|
|
@@ -326,11 +423,15 @@ export const decorations = {
|
|
|
326
423
|
options: { duration: 900, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
327
424
|
}]
|
|
328
425
|
},
|
|
329
|
-
sparkle: {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
426
|
+
sparkle: { anchor: { x: '50%', y: '50%' }, layers: sparkleBurst },
|
|
427
|
+
scan: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: scanShape, style: centered,
|
|
428
|
+
keyframes: [
|
|
429
|
+
{ opacity: 0, transform: 'translate(-50%, -50%)', easing: 'ease-out' },
|
|
430
|
+
{ opacity: 1, transform: 'translate(-50%, -48%)', offset: .1, easing: 'ease-in-out' },
|
|
431
|
+
{ opacity: 1, transform: 'translate(-50%, -14%)', offset: .5, easing: 'linear' },
|
|
432
|
+
{ opacity: 1, transform: 'translate(-50%, -14%)', offset: .62, easing: 'ease-in-out' },
|
|
433
|
+
{ opacity: 0, transform: 'translate(-50%, -40%)' }
|
|
434
|
+
],
|
|
435
|
+
options: { duration: 900, easing: 'linear' }
|
|
436
|
+
}] }
|
|
336
437
|
};
|
package/src/react-core.js
CHANGED
|
@@ -14,7 +14,7 @@ export function createEmojiMotionComponent(emojiMotion) {
|
|
|
14
14
|
return registry.bind(glyph.current, emoji, { trigger: triggerRef?.current ?? wrapper.current, events, skinTone, ...(decoration ? { decoration: { container: visual.current } } : {}) });
|
|
15
15
|
}, [registry, emoji, skinTone, decoration, events, triggerRef]);
|
|
16
16
|
return createElement('span', { ...attributes, className, ref: wrapper, 'data-emoji-motion': 'component', role: 'img', 'aria-label': label ?? result?.name ?? emoji, tabIndex: attributes.tabIndex ?? (triggerRef ? -1 : 0), style },
|
|
17
|
-
createElement('span', { ref: visual, style: { display: 'inline-block', position: 'relative' } },
|
|
17
|
+
createElement('span', { ref: visual, style: { display: 'inline-block', position: 'relative', isolation: 'isolate' } },
|
|
18
18
|
createElement('span', { ref: glyph, 'aria-hidden': 'true', style: { display: 'inline-block' } }, result?.glyph ?? emoji)));
|
|
19
19
|
}
|
|
20
20
|
}
|
package/src/registry.js
CHANGED
|
@@ -159,11 +159,17 @@ export function createRegistry(config = {}, classifier) {
|
|
|
159
159
|
node.setAttribute('viewBox', layer.svg.viewBox);
|
|
160
160
|
for (const shape of layer.svg.elements) {
|
|
161
161
|
const child = container.ownerDocument.createElementNS('http://www.w3.org/2000/svg', shape.tag);
|
|
162
|
-
for (const [key, value] of Object.entries(shape.attrs))
|
|
162
|
+
for (const [key, value] of Object.entries(shape.attrs)) {
|
|
163
|
+
// Presentation attributes cannot read custom properties, so a themed
|
|
164
|
+
// color goes through style and keeps its fallback as the attribute.
|
|
165
|
+
const themed = typeof value === 'string' && value.match(/^var\(\s*--[\w-]+\s*,\s*(.+)\)$/);
|
|
166
|
+
child.setAttribute(key, themed ? themed[1].trim() : value);
|
|
167
|
+
if (themed) child.style[key] = value;
|
|
168
|
+
}
|
|
163
169
|
node.appendChild(child);
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
|
-
Object.assign(node.style, { position: 'absolute', left: '0', top: '0', opacity: '0', ...layer.style });
|
|
172
|
+
Object.assign(node.style, { position: 'absolute', left: '0', top: '0', opacity: '0', ...(layer.behind ? { zIndex: '-1' } : {}), ...layer.style });
|
|
167
173
|
decorationRoot.appendChild(node);
|
|
168
174
|
decorationAnimations.push(node.animate(layer.keyframes, layer.options));
|
|
169
175
|
}
|
|
@@ -176,7 +182,21 @@ export function createRegistry(config = {}, classifier) {
|
|
|
176
182
|
function bind(element, glyph, options = {}) {
|
|
177
183
|
const trigger = options.trigger ?? element;
|
|
178
184
|
const events = options.events ?? ['pointerenter', 'focus'];
|
|
179
|
-
const
|
|
185
|
+
const cooldown = options.cooldown ?? 260;
|
|
186
|
+
let lastStart = -Infinity;
|
|
187
|
+
const handler = event => {
|
|
188
|
+
// A tap fires pointerenter and click together; let the click play it once.
|
|
189
|
+
if (event?.type === 'pointerenter' && event.pointerType === 'touch') return;
|
|
190
|
+
// A mouse click also focuses the trigger; only keyboard focus replays.
|
|
191
|
+
if ((event?.type === 'focus' || event?.type === 'focusin') && typeof event.target?.matches === 'function') {
|
|
192
|
+
try { if (!event.target.matches(':focus-visible')) return; } catch { /* Older engines without :focus-visible. */ }
|
|
193
|
+
}
|
|
194
|
+
// Quick repeated triggers would restart the motion before it reads.
|
|
195
|
+
const now = globalThis.performance?.now?.() ?? Date.now();
|
|
196
|
+
if (now - lastStart < cooldown) return;
|
|
197
|
+
lastStart = now;
|
|
198
|
+
play(element, glyph, options);
|
|
199
|
+
};
|
|
180
200
|
for (const event of events) trigger.addEventListener(event, handler);
|
|
181
201
|
return () => { for (const event of events) trigger.removeEventListener(event, handler); active.get(element)?.dispose(); active.delete(element); };
|
|
182
202
|
}
|
package/src/vue-core.js
CHANGED
|
@@ -22,7 +22,7 @@ export function createEmojiMotionComponent(emojiMotion) {
|
|
|
22
22
|
return () => {
|
|
23
23
|
const result = props.registry.resolve(props.emoji, { skinTone: props.skinTone });
|
|
24
24
|
return h('span', { ...attrs, ref: wrapper, 'data-emoji-motion': 'component', role: 'img', 'aria-label': props.label ?? result?.name ?? props.emoji, tabindex: attrs.tabindex ?? (props.trigger ? -1 : 0) },
|
|
25
|
-
h('span', { ref: visual, style: { display: 'inline-block', position: 'relative' } },
|
|
25
|
+
h('span', { ref: visual, style: { display: 'inline-block', position: 'relative', isolation: 'isolate' } },
|
|
26
26
|
h('span', { ref: glyph, 'aria-hidden': 'true', style: { display: 'inline-block' } }, result?.glyph ?? props.emoji)));
|
|
27
27
|
};
|
|
28
28
|
}
|