@dofy/native-emoji-motion 0.6.0 → 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 +15 -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 +179 -48
- 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,20 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.6.4 — 2026-11-14
|
|
13
|
+
|
|
14
|
+
- Add ripple, splash and wake effects for water.
|
|
15
|
+
- Add families for swimmers, surfers, water polo players and rowers, covering their gender and skin tone variants.
|
|
16
|
+
- Boats, the ring buoy, the water wave, the dolphin and the diving mask now use water effects.
|
|
17
|
+
|
|
3
18
|
## 0.6.0 — 2026-11-14
|
|
4
19
|
|
|
5
20
|
- Add motion families for vegetables, land animals, clothing, tools, books, hand gestures, boats and sweet treats.
|
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
|
|
|
@@ -101,11 +171,14 @@ const rolePeople = ['🧑', '👨', '👩'];
|
|
|
101
171
|
const roleSuffixes = ['⚕️', '🎓', '🏫', '🌾', '🔧', '🔬', '🚒', '⚖️', '✈️', '🎤'];
|
|
102
172
|
const roleMembers = roleSuffixes.flatMap(suffix => rolePeople.map(person => `${person}${suffix}`));
|
|
103
173
|
const roleAliases = Object.fromEntries(roleSuffixes.flatMap(suffix => rolePeople.flatMap(person => Object.values(skinTones(person)).map(variant => [`${variant}${suffix}`, `${person}${suffix}`]))));
|
|
104
|
-
|
|
105
|
-
const
|
|
174
|
+
// Gendered sequences take the modifier on the person, before the joiner.
|
|
175
|
+
const genderedToneAliases = glyphs => Object.fromEntries(glyphs.flatMap(glyph => {
|
|
106
176
|
const [person, suffix = ''] = glyph.split('');
|
|
107
177
|
return Object.values(skinTones(person)).map(variant => [suffix ? `${variant}${suffix}` : variant, glyph]);
|
|
108
178
|
}));
|
|
179
|
+
const gendered = person => [person, `${person}♂️`, `${person}♀️`];
|
|
180
|
+
const walkerMembers = ['🚶', '🚶♂️', '🚶♀️', '🏃♂️', '🏃♀️'];
|
|
181
|
+
const walkerAliases = genderedToneAliases(walkerMembers);
|
|
109
182
|
// Members are reviewed explicitly; no entire Unicode subgroup is assumed to move alike.
|
|
110
183
|
export const families = {
|
|
111
184
|
clocks: { label: 'Clocks', group: 'objects', motion: 'clockTick', members: ['⌚', '🕰️', '⏲️', '🕛', '🕧'], aliases: { '🕰': '🕰️', '⏲': '⏲️' } },
|
|
@@ -114,10 +187,10 @@ export const families = {
|
|
|
114
187
|
vegetables: { label: 'Vegetables', group: 'food', motion: 'gardenBounce', members: ['🥕', '🥦', '🥬', '🥒', '🌽', '🫑', '🍆', '🧅', '🧄', '🥔'] },
|
|
115
188
|
landAnimals: { label: 'Land animals', group: 'nature', motion: 'animalHop', members: ['🐕', '🐈', '🐖', '🐑', '🐐', '🐄', '🐎', '🦌', '🐘', '🦒'] },
|
|
116
189
|
clothing: { label: 'Clothing', group: 'objects', motion: 'fabricSway', members: ['👕', '👖', '🧥', '👗', '🩳', '👚', '👔'] },
|
|
117
|
-
handTools: { label: 'Hand tools', group: 'objects', motion: 'toolWiggle', members: ['
|
|
190
|
+
handTools: { label: 'Hand tools', group: 'objects', motion: 'toolWiggle', members: ['🪓', '🪚', '🪛', '🔧', '🪠', '🔩'] },
|
|
118
191
|
books: { label: 'Books', group: 'objects', motion: 'bookPeek', members: ['📕', '📗', '📘', '📙', '📔', '📓', '📒'] },
|
|
119
192
|
handGestures: { label: 'Hand gestures', group: 'gestures', motion: 'handEmphasis', members: ['🤏', '🫳', '🫴', '🫲', '🫱', '🫷', '🫸', '🤌', '🫵'], aliases: familyToneAliases(['🤏', '🫳', '🫴', '🫲', '🫱', '🫷', '🫸', '🤌', '🫵']) },
|
|
120
|
-
vessels: { label: 'Boats', group: 'travel', motion: 'vesselRock', members: ['🚤', '🛥️', '🛳️', '⛴️', '🛶'], aliases: { '🛥': '🛥️', '🛳': '🛳️', '⛴': '⛴️' } },
|
|
193
|
+
vessels: { label: 'Boats', group: 'travel', motion: 'vesselRock', members: ['🚤', '🛥️', '🛳️', '⛴️', '🛶'], aliases: { '🛥': '🛥️', '🛳': '🛳️', '⛴': '⛴️' }, decoration: 'wake' },
|
|
121
194
|
treats: { label: 'Sweet treats', group: 'food', motion: 'treatPop', members: ['🍬', '🍭', '🍫', '🍮', '🧁', '🍨'], decoration: 'sparkle' },
|
|
122
195
|
fruit: { label: 'Fruit', group: 'food', motion: 'fruitBounce', members: ['🍊', '🍋', '🍐', '🍑', '🍇', '🫐', '🍒', '🥝', '🍍', '🥭'], decoration: 'sparkle' },
|
|
123
196
|
animalFaces: { label: 'Animal faces', group: 'nature', motion: 'curiousTilt', members: ['🐶', '🦊', '🐹', '🦝', '🐨', '🐯', '🐭', '🐻❄️'] },
|
|
@@ -131,6 +204,13 @@ export const families = {
|
|
|
131
204
|
poses: { label: 'People in poses', group: 'people', motion: 'pose', members: ['🧎', '🧍', '🙇'], aliases: familyToneAliases(['🧎', '🧍', '🙇']) },
|
|
132
205
|
professionals: { label: 'Professionals', group: 'people', motion: 'pose', members: roleMembers, aliases: roleAliases },
|
|
133
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' },
|
|
210
|
+
swimmers: { label: 'Swimmers', group: 'people', motion: 'bob', members: gendered('🏊'), aliases: genderedToneAliases(gendered('🏊')), decoration: 'ripple' },
|
|
211
|
+
surfers: { label: 'Surfers', group: 'people', motion: 'ride', members: gendered('🏄'), aliases: genderedToneAliases(gendered('🏄')), decoration: 'splash' },
|
|
212
|
+
waterPolo: { label: 'Water polo', group: 'people', motion: 'bob', members: gendered('🤽'), aliases: genderedToneAliases(gendered('🤽')), decoration: 'splash' },
|
|
213
|
+
rowers: { label: 'Rowers', group: 'people', motion: 'vesselRock', members: gendered('🚣'), aliases: genderedToneAliases(gendered('🚣')), decoration: 'wake' },
|
|
134
214
|
seaLife: { label: 'Sea life', group: 'nature', motion: 'underwater', members: ['🐠', '🐡', '🦈', '🐋', '🐳', '🦭'], decoration: 'bubbles' },
|
|
135
215
|
bakery: { label: 'Bakery', group: 'food', motion: 'pastryBounce', members: ['🍞', '🥐', '🥖', '🥨', '🥯', '🥞', '🧇'] },
|
|
136
216
|
rail: { label: 'Rail vehicles', group: 'travel', motion: 'railRide', members: ['🚄', '🚅', '🚆', '🚇', '🚈', '🚊', '🚝', '🚞'] },
|
|
@@ -163,15 +243,15 @@ export const emoji = {
|
|
|
163
243
|
'🔍': { group: 'objects', name: 'Magnifying glass', motion: 'search' }, '🔔': { group: 'objects', name: 'Bell', motion: 'swing', decoration: 'sound' }, '🎁': { group: 'objects', name: 'Gift', motion: 'shake' }, '📡': { group: 'objects', name: 'Satellite antenna', motion: 'wobble', decoration: 'signal' }, '⏱️': { group: 'objects', name: 'Stopwatch', motion: 'swing', variants: ['⏱'] },
|
|
164
244
|
'💡': { group: 'objects', name: 'Light bulb', motion: 'breathe', decoration: 'glow' }, '📷': { group: 'objects', name: 'Camera', motion: 'bounce', decoration: 'flash' }, '🔑': { group: 'objects', name: 'Key', motion: 'wobble' }, '📌': { group: 'objects', name: 'Pushpin', motion: 'bounce' }, '📦': { group: 'objects', name: 'Package', motion: 'shake' }, '🧭': { group: 'objects', name: 'Compass', motion: 'wobble' },
|
|
165
245
|
'🌸': { group: 'nature', name: 'Blossom', decoration: 'petals' }, '🍃': { group: 'nature', name: 'Leaves' }, '☀️': { group: 'nature', name: 'Sun', motion: 'spin', decoration: 'glow', variants: ['☀'] },
|
|
166
|
-
'🌻': { group: 'nature', name: 'Sunflower', motion: 'leafSway' }, '🌈': { group: 'nature', name: 'Rainbow', motion: 'float' }, '🌙': { group: 'nature', name: 'Crescent moon', motion: 'float' }, '⭐': { group: 'nature', name: 'Star', motion: 'pulse', decoration: 'sparkle' }, '🔥': { group: 'nature', name: 'Fire', motion: 'wobble', decoration: 'flicker' }, '🌊': { group: 'nature', name: 'Water wave', motion: 'float' },
|
|
246
|
+
'🌻': { group: 'nature', name: 'Sunflower', motion: 'leafSway' }, '🌈': { group: 'nature', name: 'Rainbow', motion: 'float' }, '🌙': { group: 'nature', name: 'Crescent moon', motion: 'float' }, '⭐': { group: 'nature', name: 'Star', motion: 'pulse', decoration: 'sparkle' }, '🔥': { group: 'nature', name: 'Fire', motion: 'wobble', decoration: 'flicker' }, '🌊': { group: 'nature', name: 'Water wave', motion: 'float', decoration: 'splash' },
|
|
167
247
|
'🚀': { group: 'travel', name: 'Rocket' }, '✈️': { group: 'travel', name: 'Airplane', motion: 'glide', variants: ['✈'] }, '🚲': { group: 'travel', name: 'Bicycle', motion: 'ride' },
|
|
168
|
-
'🚗': { group: 'travel', name: 'Car', motion: 'ride' }, '🚂': { group: 'travel', name: 'Locomotive', motion: 'ride' }, '🛸': { group: 'travel', name: 'Flying saucer', motion: 'float' }, '🚢': { group: 'travel', name: 'Ship', motion: '
|
|
248
|
+
'🚗': { group: 'travel', name: 'Car', motion: 'ride' }, '🚂': { group: 'travel', name: 'Locomotive', motion: 'ride' }, '🛸': { group: 'travel', name: 'Flying saucer', motion: 'float' }, '🚢': { group: 'travel', name: 'Ship', motion: 'vesselRock', decoration: 'wake' },
|
|
169
249
|
'✨': { group: 'symbols', name: 'Sparkles', motion: 'pulse', decoration: 'sparkle' }, '⚙️': { group: 'symbols', name: 'Gear', variants: ['⚙'] }, '🎯': { group: 'symbols', name: 'Target', motion: 'bounce' }, '🎵': { group: 'symbols', name: 'Musical note', motion: 'swing', decoration: 'sound' }, '💤': { group: 'symbols', name: 'Sleeping symbol', motion: 'float' }, '💥': { group: 'symbols', name: 'Collision', motion: 'pulse' }, '❗': { group: 'symbols', name: 'Exclamation mark', motion: 'bounce' }, '❓': { group: 'symbols', name: 'Question mark', motion: 'wobble' }, '✅': { group: 'symbols', name: 'Check mark', motion: 'bounce' },
|
|
170
250
|
'🐰': { group: 'nature', name: 'Rabbit face', motion: 'hop' }, '🦋': { group: 'nature', name: 'Butterfly', motion: 'flutter' }, '🐝': { group: 'nature', name: 'Bee', motion: 'flutter' }, '🌱': { group: 'nature', name: 'Seedling', motion: 'sway' }, '🌧️': { group: 'nature', name: 'Cloud with rain', motion: 'float', decoration: 'rain', variants: ['🌧'] },
|
|
171
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' },
|
|
172
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' },
|
|
173
253
|
'💃': { group: 'people', name: 'Woman dancing', motion: 'twirl', skinTones: skinTones('💃') }, '🕺': { group: 'people', name: 'Man dancing', motion: 'twirl', skinTones: skinTones('🕺') },
|
|
174
|
-
'⏳': { 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: ['🕯'] },
|
|
175
255
|
|
|
176
256
|
// Additional handcrafted entries share motions where their physical action fits.
|
|
177
257
|
'😀': { group: 'faces', name: 'Grinning face', motion: 'bounce' }, '🤣': { group: 'faces', name: 'Rolling on the floor laughing', motion: 'shake' },
|
|
@@ -184,7 +264,7 @@ export const emoji = {
|
|
|
184
264
|
'🫰': { group: 'gestures', name: 'Hand with index finger and thumb crossed', motion: 'pop', skinTones: skinTones('🫰') },
|
|
185
265
|
'🐱': { group: 'nature', name: 'Cat face', motion: 'nod' }, '🐻': { group: 'nature', name: 'Bear', motion: 'nod' }, '🐼': { group: 'nature', name: 'Panda', motion: 'sway' },
|
|
186
266
|
'🐸': { group: 'nature', name: 'Frog', motion: 'hop' }, '🐧': { group: 'nature', name: 'Penguin', motion: 'sway' }, '🐥': { group: 'nature', name: 'Front facing baby chick', motion: 'hop' },
|
|
187
|
-
'🐢': { 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' },
|
|
188
268
|
'🐙': { group: 'nature', name: 'Octopus', motion: 'twirl' }, '🌵': { group: 'nature', name: 'Cactus', motion: 'sway' }, '🌲': { group: 'nature', name: 'Evergreen tree', motion: 'sway' },
|
|
189
269
|
'🌼': { group: 'nature', name: 'Blossom flower', motion: 'sway', decoration: 'petals' }, '❄️': { group: 'nature', name: 'Snowflake', motion: 'float', variants: ['❄'] }, '☁️': { group: 'nature', name: 'Cloud', motion: 'float', variants: ['☁'] },
|
|
190
270
|
'🍎': { group: 'food', name: 'Red apple', motion: 'nod' }, '🍉': { group: 'food', name: 'Watermelon', motion: 'wobble' }, '🍓': { group: 'food', name: 'Strawberry', motion: 'bounce' },
|
|
@@ -195,15 +275,14 @@ export const emoji = {
|
|
|
195
275
|
'🎨': { group: 'activities', name: 'Artist palette', motion: 'twirl' }, '🎸': { group: 'activities', name: 'Guitar', motion: 'sway', decoration: 'sound' },
|
|
196
276
|
'🥁': { group: 'activities', name: 'Drum', motion: 'shake', decoration: 'sound' }, '🎮': { group: 'activities', name: 'Game controller', motion: 'wobble' }, '🪁': { group: 'activities', name: 'Kite', motion: 'flutter' },
|
|
197
277
|
'🙋': { group: 'people', name: 'Person raising hand', motion: 'wave', skinTones: skinTones('🙋') }, '🧘': { group: 'people', name: 'Person in lotus position', motion: 'breathe', skinTones: skinTones('🧘') },
|
|
198
|
-
'🏃': { group: 'people', name: 'Person running', motion: 'hop', skinTones: skinTones('🏃') }, '🤸': { group: 'people', name: 'Person cartwheeling', motion: '
|
|
199
|
-
'🏊': { group: 'people', name: 'Person swimming', motion: 'bob', skinTones: skinTones('🏊') }, '🏄': { group: 'people', name: 'Person surfing', motion: 'bob', skinTones: skinTones('🏄') },
|
|
278
|
+
'🏃': { group: 'people', name: 'Person running', motion: 'hop', skinTones: skinTones('🏃') }, '🤸': { group: 'people', name: 'Person cartwheeling', motion: 'somersault', skinTones: skinTones('🤸') },
|
|
200
279
|
'🧑🍳': { group: 'people', name: 'Cook', motion: 'nod', skinTones: personSkinTones('🍳') }, '🧑🎨': { group: 'people', name: 'Artist', motion: 'twirl', skinTones: personSkinTones('🎨') },
|
|
201
280
|
'🧑🚀': { group: 'people', name: 'Astronaut', motion: 'float', skinTones: personSkinTones('🚀') }, '🧑💻': { group: 'people', name: 'Technologist', motion: 'nod', skinTones: personSkinTones('💻') },
|
|
202
281
|
'📱': { group: 'objects', name: 'Mobile phone', motion: 'shake' }, '📚': { group: 'objects', name: 'Books', motion: 'nod' }, '✉️': { group: 'objects', name: 'Envelope', motion: 'bob', variants: ['✉'] },
|
|
203
282
|
'💌': { group: 'objects', name: 'Love letter', motion: 'bob', decoration: 'sparkle' }, '🔒': { group: 'objects', name: 'Locked', motion: 'wobble' }, '🔓': { group: 'objects', name: 'Unlocked', motion: 'pop' },
|
|
204
283
|
'🔦': { group: 'objects', name: 'Flashlight', motion: 'nod', decoration: 'flash' }, '🪄': { group: 'objects', name: 'Magic wand', motion: 'twirl', decoration: 'sparkle' }, '🎤': { group: 'objects', name: 'Microphone', motion: 'swing', decoration: 'sound' },
|
|
205
284
|
'🚌': { group: 'travel', name: 'Bus', motion: 'ride' }, '🚕': { group: 'travel', name: 'Taxi', motion: 'ride' }, '🚁': { group: 'travel', name: 'Helicopter', motion: 'bob' },
|
|
206
|
-
'🎡': { group: 'travel', name: 'Ferris wheel', motion: 'steady' }, '⛵': { group: 'travel', name: 'Sailboat', motion: '
|
|
285
|
+
'🎡': { group: 'travel', name: 'Ferris wheel', motion: 'steady' }, '⛵': { group: 'travel', name: 'Sailboat', motion: 'vesselRock', decoration: 'wake' }, '🎠': { group: 'travel', name: 'Carousel horse', motion: 'carousel' },
|
|
207
286
|
// Direction-neutral lighting for other objects that naturally emit or reflect light.
|
|
208
287
|
'📸': { group: 'objects', name: 'Camera with flash', motion: 'bounce', decoration: 'flash' },
|
|
209
288
|
'🪔': { group: 'objects', name: 'Oil lamp', motion: 'sway', decoration: 'flicker' },
|
|
@@ -219,27 +298,67 @@ export const emoji = {
|
|
|
219
298
|
'💎': { group: 'objects', name: 'Gem stone', motion: 'pulse', decoration: 'sparkle' },
|
|
220
299
|
'🪩': { group: 'objects', name: 'Mirror ball', motion: 'spin', decoration: 'sparkle' },
|
|
221
300
|
'🌠': { group: 'nature', name: 'Shooting star', motion: 'float', decoration: 'sparkle' },
|
|
301
|
+
// Water gear borrows the sea life and boat motions with matching water effects.
|
|
302
|
+
'🤿': { group: 'activities', name: 'Diving mask', motion: 'underwater', decoration: 'bubbles' },
|
|
303
|
+
'🛟': { group: 'travel', name: 'Ring buoy', motion: 'vesselRock', decoration: 'wake' },
|
|
222
304
|
'🪙': { group: 'objects', name: 'Coin', motion: 'flipHorizontal' },
|
|
223
|
-
'
|
|
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' }
|
|
224
311
|
};
|
|
225
312
|
|
|
226
|
-
// Declarative SVG layers never alter the native glyph.
|
|
227
|
-
//
|
|
228
|
-
|
|
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' } }] };
|
|
229
318
|
const notesShape = { viewBox: '0 0 100 100', elements: [
|
|
230
|
-
{ 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' } },
|
|
231
|
-
{ 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' } }
|
|
232
|
-
] };
|
|
233
|
-
const sparkleShape = { viewBox: '0 0 100 100', elements: [
|
|
234
|
-
{ 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' } },
|
|
235
|
-
{ 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)' } }
|
|
236
321
|
] };
|
|
237
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
|
+
] };
|
|
238
341
|
const steamShape = { viewBox: '0 0 100 100', elements: [
|
|
239
|
-
{ 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' } }
|
|
343
|
+
] };
|
|
344
|
+
// Water effects sit at the waterline below the glyph and spread symmetrically.
|
|
345
|
+
// y is the waterline in viewBox units: at the body for swimmers, below the hull for boats.
|
|
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' } }] },
|
|
349
|
+
style: { ...centered, transformOrigin: `50% ${y}%` },
|
|
350
|
+
keyframes: [{ opacity: .8, transform: 'translate(-50%, -50%) scale(.5)' }, { opacity: 0, transform: 'translate(-50%, -50%) scale(1.25)' }],
|
|
351
|
+
options: { duration: 1100, delay, easing: 'cubic-bezier(.25,.6,.4,1)', fill: 'backwards' }
|
|
352
|
+
}));
|
|
353
|
+
const splashShape = { viewBox: '0 0 100 100', elements: [
|
|
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)' } }
|
|
240
359
|
] };
|
|
241
360
|
const rainShape = { viewBox: '0 0 100 100', elements: [
|
|
242
|
-
{ 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' } }
|
|
243
362
|
] };
|
|
244
363
|
// Symmetric rays sit around the glyph rather than assuming a bulb, lens or flame position.
|
|
245
364
|
const lightShape = color => ({ viewBox: '0 0 100 100', elements: [
|
|
@@ -247,30 +366,31 @@ const lightShape = color => ({ viewBox: '0 0 100 100', elements: [
|
|
|
247
366
|
] });
|
|
248
367
|
export const decorations = {
|
|
249
368
|
bubbles: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: { viewBox: '0 0 100 100', elements: [
|
|
250
|
-
{ tag: 'circle', attrs: { cx: '21', cy: '46', r: '3', fill: 'none', stroke: '#7cafd0', 'stroke-width': '2' } },
|
|
251
|
-
{ tag: 'circle', attrs: { cx: '76', cy: '38', r: '4', fill: 'none', stroke: '#7cafd0', 'stroke-width': '2' } },
|
|
252
|
-
{ 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' } }
|
|
253
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 } }] },
|
|
254
|
-
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,
|
|
255
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)' }],
|
|
256
375
|
options: { duration: 900, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
257
376
|
}] },
|
|
258
|
-
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,
|
|
259
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)' }],
|
|
260
379
|
options: { duration: 850, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
261
380
|
}] },
|
|
262
|
-
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,
|
|
263
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)' }],
|
|
264
383
|
options: { duration: 1100, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
265
384
|
}] },
|
|
266
|
-
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,
|
|
267
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)' }],
|
|
268
387
|
options: { duration: 800, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
269
388
|
}] },
|
|
270
|
-
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,
|
|
271
|
-
|
|
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' }
|
|
272
392
|
}] },
|
|
273
|
-
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,
|
|
274
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)' }
|
|
275
395
|
}] },
|
|
276
396
|
steam: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: steamShape, style: centered,
|
|
@@ -281,6 +401,13 @@ export const decorations = {
|
|
|
281
401
|
keyframes: [{ opacity: 0, transform: 'translate(-50%, -55%)' }, { opacity: .9, transform: 'translate(-50%, -50%)', offset: .3 }, { opacity: 0, transform: 'translate(-50%, -40%)' }],
|
|
282
402
|
options: { duration: 950, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
283
403
|
}] },
|
|
404
|
+
ripple: { anchor: { x: '50%', y: '50%' }, layers: rippleRings(66) },
|
|
405
|
+
// The hull hides the far side of its wake.
|
|
406
|
+
wake: { anchor: { x: '50%', y: '50%' }, layers: rippleRings(75, true) },
|
|
407
|
+
splash: { anchor: { x: '50%', y: '50%' }, layers: [{ svg: splashShape, style: { ...centered, transformOrigin: '50% 70%' },
|
|
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)' }],
|
|
409
|
+
options: { duration: 850, easing: curve }
|
|
410
|
+
}] },
|
|
284
411
|
signal: {
|
|
285
412
|
anchor: { x: '50%', y: '50%' },
|
|
286
413
|
layers: [0, 180, 360].map(delay => ({
|
|
@@ -296,11 +423,15 @@ export const decorations = {
|
|
|
296
423
|
options: { duration: 900, easing: 'cubic-bezier(.22,1,.36,1)' }
|
|
297
424
|
}]
|
|
298
425
|
},
|
|
299
|
-
sparkle: {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
}] }
|
|
306
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
|
}
|