@dofy/native-emoji-motion 0.0.0-stage → 0.2.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 +18 -0
- package/LICENSE +21 -0
- package/README.md +255 -2
- package/UNICODE-LICENSE.txt +39 -0
- package/package.json +128 -4
- package/src/EmojiMotion.svelte +22 -0
- package/src/EmojiMotion.svelte.d.ts +13 -0
- package/src/EmojiMotionLite.svelte +22 -0
- package/src/element-core.js +31 -0
- package/src/element-lite.js +7 -0
- package/src/element.d.ts +3 -0
- package/src/element.js +7 -0
- package/src/index.d.ts +25 -0
- package/src/index.js +8 -0
- package/src/lite.d.ts +2 -0
- package/src/lite.js +5 -0
- package/src/presets.js +206 -0
- package/src/react-core.js +20 -0
- package/src/react-lite.js +4 -0
- package/src/react.d.ts +14 -0
- package/src/react.js +4 -0
- package/src/registry.js +183 -0
- package/src/svelte-core.js +15 -0
- package/src/svelte-lite.js +3 -0
- package/src/svelte.d.ts +3 -0
- package/src/svelte.js +3 -0
- package/src/unicode-data.js +5 -0
- package/src/unicode.js +54 -0
- package/src/vue-core.js +30 -0
- package/src/vue-lite.js +3 -0
- package/src/vue.d.ts +13 -0
- package/src/vue.js +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-11-13
|
|
4
|
+
|
|
5
|
+
- Expand the handcrafted collection to 161 emoji and add five reviewed motion families.
|
|
6
|
+
- Add configurable spring and sway motion builders and composable SVG decorations.
|
|
7
|
+
- Support custom families with explicit variant aliases and user-defined priority.
|
|
8
|
+
- Improve the demo with family previews, persistent preferences and clearer examples.
|
|
9
|
+
|
|
10
|
+
## 0.1.0 — 2026-11-13
|
|
11
|
+
|
|
12
|
+
Initial public release.
|
|
13
|
+
|
|
14
|
+
- Animate native emoji using handcrafted presets or automatic Unicode 18.0 classification.
|
|
15
|
+
- Customize emoji, group and classification motions with user-defined rules taking priority.
|
|
16
|
+
- Support skin tone variants, optional SVG decorations and reduced-motion preferences.
|
|
17
|
+
- Provide components for React, Vue and Svelte, plus a framework-independent web component.
|
|
18
|
+
- Offer preset-only lite entry points and TypeScript declarations.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emoji Motion contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,256 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Emoji Motion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Small, expressive animations for native emoji. A framework independent library with Unicode based automatic classification, reusable motion presets and explicit per emoji overrides.
|
|
4
|
+
|
|
5
|
+
## Collection at a glance
|
|
6
|
+
|
|
7
|
+
The current repository and demo contain **161 handcrafted emoji presets** and **5 reviewed motion families** with **24 additional base emoji**, across **10 categories**. They use **27 reusable motions**; **24 base emoji** have optional SVG effects. Skin tones and other variants are additional sequences, and similar emoji may share the same motion.
|
|
8
|
+
|
|
9
|
+
The default entry also recognizes **5,235 Unicode Emoji 18.0 sequences**, including qualified forms and variants, and applies a category based fallback when there is no handcrafted preset or reviewed family match. These collection counts describe version 0.2.0.
|
|
10
|
+
|
|
11
|
+
## Try the demo
|
|
12
|
+
|
|
13
|
+
[Explore the live demo](https://emoji-motion.phpz.org/) or run it locally:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Open http://127.0.0.1:4173. Hover, focus or tap a card; use the filters to explore the groups. The demo imports the same public entry point as an application. Use **Try any emoji** to preview automatic classification. The gallery starts with a curated selection from across the groups. Choose **All presets** for the handcrafted list, **Motion families** for reviewed family members, **With effects** for decorative SVG layers, or select a semantic group. Choose a framework and display mode, then select an emoji to copy its component. The examples show how to place an emoji in a sentence or on its own line.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install @dofy/native-emoji-motion
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The package is ESM and uses the browser Web Animations API. React, Vue and Svelte are optional peers: install the framework used by your application.
|
|
28
|
+
|
|
29
|
+
## Components
|
|
30
|
+
|
|
31
|
+
Use an emoji directly as a component. `display` is `inline` by default and can be set to `block`; all components animate on hover, focus and click and use built in SVG effects when available. Use these imports in a bundled app. React, Vue and Svelte are optional peers, so the core stays framework independent.
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
import { EmojiMotion } from '@dofy/native-emoji-motion/react';
|
|
35
|
+
|
|
36
|
+
<p>Hello <EmojiMotion emoji="👋" skinTone="medium" /></p>
|
|
37
|
+
<EmojiMotion emoji="📡" display="block" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<script setup>
|
|
42
|
+
import { EmojiMotion } from '@dofy/native-emoji-motion/vue';
|
|
43
|
+
</script>
|
|
44
|
+
<template>
|
|
45
|
+
<p>Hello <EmojiMotion emoji="👋" skin-tone="medium" /></p>
|
|
46
|
+
<EmojiMotion emoji="📡" display="block" />
|
|
47
|
+
</template>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```svelte
|
|
51
|
+
<script>
|
|
52
|
+
import EmojiMotion from '@dofy/native-emoji-motion/svelte/EmojiMotion.svelte';
|
|
53
|
+
</script>
|
|
54
|
+
<p>Hello <EmojiMotion emoji="👋" skinTone="medium" /></p>
|
|
55
|
+
<EmojiMotion emoji="📡" display="block" />
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For plain HTML or other frameworks, import `@dofy/native-emoji-motion/element` once in a bundled entry module, then use `<emoji-motion emoji="👋" display="block"></emoji-motion>`. The web component can be used with Angular or any other framework that supports custom elements. It registers itself when the module is imported. Its attributes include `emoji`, `skin-tone`, `display`, `label` and `decoration="false"`; assign the `registry` property to use a custom registry.
|
|
59
|
+
|
|
60
|
+
React, Vue and Svelte components accept `emoji`, `skinTone`, `display`, `decoration`, `label`, `events`, and a custom `registry`. For a custom interaction target, React accepts `triggerRef` and Vue/Svelte accept `trigger`. Components clean up their listeners when updated or unmounted. A global skin tone set on a registry is read when a component renders; to update an already mounted component, update its `skinTone` prop or cause a framework rerender.
|
|
61
|
+
|
|
62
|
+
Components do not set a mouse cursor: the emoji follows its surrounding context. Set `cursor: pointer` on a button or other control when the emoji is part of an action.
|
|
63
|
+
|
|
64
|
+
## Low level API
|
|
65
|
+
|
|
66
|
+
Import the package in an ESM application:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import { emojiMotion } from '@dofy/native-emoji-motion';
|
|
70
|
+
|
|
71
|
+
// Keep the emoji in its own inline-block element. The card can be the trigger.
|
|
72
|
+
const dispose = emojiMotion.bind(emojiElement, '👋', { trigger: card });
|
|
73
|
+
// When the component is removed:
|
|
74
|
+
dispose();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or play once manually: `emojiMotion.play(emojiElement, '❤️')`. `resolve(glyph)` returns the entry, group and selected motion, or `undefined` when the input cannot be recognized. `list()` returns canonical entries; `groups()` lists groups. `bind` listens to `pointerenter` and `focus` by default. Pass `events: ['click']` or any DOM event names to change this. Make the trigger keyboard accessible; a `<button>` is a good default. The element being animated should have `display: inline-block`, and should be dedicated to the emoji because Web Animations controls its transform.
|
|
78
|
+
|
|
79
|
+
Motion is skipped when the user prefers reduced motion. `play` returns `null` for unknown emoji, reduced motion, or an element without Web Animations support; otherwise it returns the browser's `Animation`. A repeated play cancels the previous one on the same element. `ignoreReducedMotion` is an explicit opt out and should only be used with the user's consent.
|
|
80
|
+
|
|
81
|
+
## Automatic classification
|
|
82
|
+
|
|
83
|
+
The default entry recognizes emoji using data generated from Unicode Emoji 18.0, including fully qualified, minimally qualified, and unqualified sequences. A known emoji is classified by its Unicode group and subgroup and receives a conservative reusable motion; existing handcrafted presets retain their special animations and optional decorations. Exact Unicode sequences, including flags and ZWJ sequences, are used rather than guessing category from a code point range. `resolve()` exposes `source`, `unicodeGroup` and `unicodeSubgroup` where available:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
import { emojiMotion } from '@dofy/native-emoji-motion';
|
|
87
|
+
|
|
88
|
+
emojiMotion.resolve('🐶'); // source: 'unicode', subgroup: 'animal-mammal', motion: 'bounce'
|
|
89
|
+
emojiMotion.resolve('👩🏽🚀').glyph; // 👩🏽🚀
|
|
90
|
+
emojiMotion.resolve('📡').source; // 'preset', including its signal decoration
|
|
91
|
+
emojiMotion.resolve('hello'); // undefined
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The precedence is: **user emoji and aliases → user subgroup or group classification rule → user group default → handcrafted preset → generated Unicode classification → conservative fallback**. User emoji entries always take precedence over user classification rules. Within user classifications, subgroup rules take precedence over group rules. An explicitly supplied group default overrides handcrafted and automatic motions for that group; a more specific user emoji or classification rule can override it. User-defined motions replace built in motions with the same ID. User classification rules also override matching handcrafted presets.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { createEmojiMotion } from '@dofy/native-emoji-motion';
|
|
98
|
+
|
|
99
|
+
const custom = createEmojiMotion({
|
|
100
|
+
classifications: {
|
|
101
|
+
groups: { 'Animals & Nature': { group: 'nature', motion: 'float' } },
|
|
102
|
+
subgroups: { 'animal-bird': { group: 'nature', motion: 'swing' } },
|
|
103
|
+
fallback: { group: 'other', motion: 'bounce' },
|
|
104
|
+
},
|
|
105
|
+
emoji: { '🐶': { group: 'hearts', name: 'My dog', motion: 'pulse' } },
|
|
106
|
+
});
|
|
107
|
+
// 🐶 uses pulse, 🐦 uses swing, and other animals use float.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
A generated Unicode name and subgroup are used for uncustomized emoji. `list()` remains the small set of handcrafted and explicitly added entries; it is not a list of the entire Unicode dataset. Generated classifications do not automatically create SVG decorations. Unknown sequences newer than the pinned dataset receive a generic fallback only if the JavaScript engine recognizes them as emoji; otherwise they remain unrecognized. Detection does not guarantee that the system font can render the glyph. Standalone components and arbitrary text are not treated as emoji.
|
|
111
|
+
|
|
112
|
+
The `defaults: false` setting disables both built in presets and automatic classification unless `autoDetect: true` is explicitly supplied; with no built in groups or motions, the caller must define the groups and motions used by its rules. Set `autoDetect: false` to disable automatic classification while keeping built in presets.
|
|
113
|
+
|
|
114
|
+
## Smaller preset only entry
|
|
115
|
+
|
|
116
|
+
Use the lite entry to omit the Unicode classifier and its data. Unknown emoji do not automatically animate; user-defined entries and handcrafted presets still work. The matching framework entries also omit Unicode data:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { createEmojiMotion } from '@dofy/native-emoji-motion/lite';
|
|
120
|
+
import { EmojiMotion as ReactEmoji } from '@dofy/native-emoji-motion/react/lite';
|
|
121
|
+
import { EmojiMotion as VueEmoji } from '@dofy/native-emoji-motion/vue/lite';
|
|
122
|
+
import '@dofy/native-emoji-motion/element/lite';
|
|
123
|
+
// Svelte: import EmojiMotion from '@dofy/native-emoji-motion/svelte/EmojiMotionLite.svelte';
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Import either the full or lite web component entry on a page, not both: both register the same `emoji-motion` tag and the first registration wins. To keep a framework bundle small, use its lite entry rather than passing a lite registry to a full component entry, which still imports the full classifier.
|
|
127
|
+
|
|
128
|
+
Unicode data is generated with `npm run generate:unicode` from a version-pinned, checksum-verified official file. The data is covered by [UNICODE-LICENSE.txt](./UNICODE-LICENSE.txt).
|
|
129
|
+
|
|
130
|
+
## Skin tone
|
|
131
|
+
|
|
132
|
+
A registry can specify a default and change it later. `resolve` returns the glyph to render; animation never changes your element's text automatically.
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
import { createEmojiMotion } from '@dofy/native-emoji-motion';
|
|
136
|
+
|
|
137
|
+
const motion = createEmojiMotion({ defaultSkinTone: 'medium' });
|
|
138
|
+
motion.resolve('👋').glyph; // 👋🏽
|
|
139
|
+
motion.resolve('👋', { skinTone: 'dark' }).glyph; // 👋🏿
|
|
140
|
+
motion.setDefaultSkinTone('light');
|
|
141
|
+
motion.resolve('👋').glyph; // 👋🏻
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The available values are `default`, `light`, `medium-light`, `medium`, `medium-dark` and `dark`. Explicitly supplied skin tone variants retain their tone unless a per-call override is supplied. Automatic skin tones are applied only where an official single-tone variant is unambiguous; mixed-tone sequences stay unchanged rather than being rewritten. Emoji without a skin tone mapping stay unchanged. When adding custom emoji, supply `skinTones` as an explicit map from the tone names to complete Unicode sequences, including any joiners; the library does not guess modifier placement. If the default tone changes, update rendered text by resolving again.
|
|
145
|
+
|
|
146
|
+
## Extend
|
|
147
|
+
|
|
148
|
+
An isolated registry combines built ins and your definitions; existing IDs can be replaced. Set `defaults: false` to use only your own definitions. Emoji variants are explicit rather than guessed, so skin tone and multi code point sequences remain under your control.
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { createEmojiMotion } from '@dofy/native-emoji-motion';
|
|
152
|
+
|
|
153
|
+
const motion = createEmojiMotion({
|
|
154
|
+
motions: {
|
|
155
|
+
wiggle: {
|
|
156
|
+
keyframes: [
|
|
157
|
+
{ transform: 'rotate(0deg)' },
|
|
158
|
+
{ transform: 'rotate(12deg)' },
|
|
159
|
+
{ transform: 'rotate(0deg)' },
|
|
160
|
+
],
|
|
161
|
+
options: { duration: 600, easing: 'ease-out' },
|
|
162
|
+
origin: '50% 80%',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
groups: { creatures: { label: 'Creatures', motion: 'wiggle' } },
|
|
166
|
+
emoji: { '🐙': { name: 'Octopus', group: 'creatures' } },
|
|
167
|
+
});
|
|
168
|
+
motion.bind(emojiElement, '🐙', { trigger: card });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Groups select a default motion; an emoji entry can override it with `motion`, and `variants` lists exact alternate sequences that inherit its settings. References and duplicate variants are validated when a registry is created. Definitions are ordinary JavaScript objects; do not mutate exported built in data. Create a new registry to change configuration. API types are included.
|
|
172
|
+
|
|
173
|
+
### Motion families
|
|
174
|
+
|
|
175
|
+
Families apply a shared motion to explicitly reviewed members. Built in families cover balls, plants, drinks, flying animals and people in poses. They do not assume every emoji in a Unicode subgroup moves alike. `resolve()` reports `source: 'family'` and `familyId` for family matches; `list()` remains the handcrafted emoji list, while `families()` returns family definitions.
|
|
176
|
+
|
|
177
|
+
Use the configurable spring and sway builders to create reusable motions. A family can adjust Web Animations timing with `animation`, and combine named SVG effects with `decorations`:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { createEmojiMotion, createSpringMotion } from '@dofy/native-emoji-motion';
|
|
181
|
+
|
|
182
|
+
const motion = createEmojiMotion({
|
|
183
|
+
motions: { smallBounce: createSpringMotion({ height: 14, rebounds: 2, duration: 900 }) },
|
|
184
|
+
families: {
|
|
185
|
+
snacks: {
|
|
186
|
+
label: 'Snacks', group: 'food', motion: 'smallBounce',
|
|
187
|
+
members: ['🍪', '🍩'],
|
|
188
|
+
animation: { duration: 800 },
|
|
189
|
+
decorations: ['impact', 'sparkle'],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Pass `motion` as the component's `registry` property to use this configuration. `createSwayMotion({ angle, duration, origin })` provides a direction neutral sway. Effects remain optional and require a decoration container when using the low level API. Each effect keeps its own anchor unless the caller explicitly overrides it.
|
|
196
|
+
|
|
197
|
+
Custom emoji definitions take priority, followed by custom Unicode classifications and custom families. Built in single emoji presets precede built in families; remaining emoji use automatic classification. A custom group default overrides built in presets and families, while an explicitly defined custom family keeps its chosen motion. A custom family with the same ID replaces the built in family. Unicode variants and unambiguous skin tones are resolved by the full entry; for the lite entry, supply explicit `aliases: { '🧎🏽': '🧎' }` mappings when a family needs variants. Mixed tone sequences are never guessed or rewritten.
|
|
198
|
+
|
|
199
|
+
## Optional decorative effects
|
|
200
|
+
|
|
201
|
+
Emoji can also reference a decoration preset. The built in 📡 example adds a neutral signal halo around the whole glyph; 🔔 and 🎵 add notes, and ✨ adds sparkles. Decorations are opt in and render inside a container you provide. Make the container `position: relative` and size it to the glyph. Effects do not change the emoji itself.
|
|
202
|
+
|
|
203
|
+
```html
|
|
204
|
+
<button id="antenna"><span class="emoji-box"><span id="dish">📡</span></span></button>
|
|
205
|
+
<style>.emoji-box { position: relative; display: inline-block } #dish { display: inline-block }</style>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
emojiMotion.bind(document.querySelector('#dish'), '📡', {
|
|
210
|
+
trigger: document.querySelector('#antenna'),
|
|
211
|
+
decoration: {
|
|
212
|
+
container: document.querySelector('.emoji-box'),
|
|
213
|
+
// Optional: adjust for the font actually used by your application.
|
|
214
|
+
anchor: { x: '50%', y: '50%' },
|
|
215
|
+
flipX: false,
|
|
216
|
+
rotate: 0,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`anchor` is relative to the container. `flipX` and `rotate` affect the decoration, never the glyph. For asymmetric effects, select explicit settings for the actual font or environment you support; OS detection alone cannot reliably determine the font or its direction. The default signal effect is centered rather than tied to a specific antenna position. When no decoration container is supplied, only the emoji moves. Decoration layers are removed after playback, on replay and on cleanup; reduced motion skips both.
|
|
222
|
+
|
|
223
|
+
Add custom `decorations` in `createEmojiMotion`: each named preset has an `anchor` and `layers`, each with CSS `style`, Web Animations `keyframes` and `options`. Optionally provide `svg: { viewBox, elements }`, where each element has a `tag` and `attrs`; without `svg`, a styled span is used. Reference its name using the emoji entry's `decoration` field. Layer styles are trusted application code, and the library does not bundle images or fonts.
|
|
224
|
+
|
|
225
|
+
For example, this adds an SVG sparkle to a custom group:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
const custom = createEmojiMotion({
|
|
229
|
+
decorations: {
|
|
230
|
+
star: {
|
|
231
|
+
anchor: { x: '50%', y: '50%' },
|
|
232
|
+
layers: [{
|
|
233
|
+
svg: { viewBox: '0 0 100 100', elements: [
|
|
234
|
+
{ tag: 'circle', attrs: { cx: '50', cy: '20', r: '5', fill: 'gold' } },
|
|
235
|
+
] },
|
|
236
|
+
style: { width: '160%', height: '160%' },
|
|
237
|
+
keyframes: [
|
|
238
|
+
{ opacity: 1, transform: 'translate(-50%, -50%) scale(.6)' },
|
|
239
|
+
{ opacity: 0, transform: 'translate(-50%, -50%) scale(1.2)' },
|
|
240
|
+
],
|
|
241
|
+
options: { duration: 700 },
|
|
242
|
+
}],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
emoji: { '🌟': { name: 'Glowing star', group: 'symbols', decoration: 'star' } },
|
|
246
|
+
});
|
|
247
|
+
custom.bind(emojiElement, '🌟', { trigger: card, decoration: { container: emojiBox } });
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Platform boundaries
|
|
251
|
+
|
|
252
|
+
The library animates the entire native glyph, not individual parts. Rendering, supported emoji, sizes and font fallback vary by operating system, browser and installed fonts. Unsupported sequences may appear as missing glyphs or multiple characters. This library deliberately ships no emoji images or fonts and does not replace unsupported glyphs. Test the target browser, OS and font combinations for your product. Keyframes use relative transforms, but the visual pivot can still vary between fonts.
|
|
253
|
+
|
|
254
|
+
## Development and release
|
|
255
|
+
|
|
256
|
+
`npm test` runs the registry and lifecycle tests; `npm run build:demo` creates the isolated static site in `dist/`; `npm run pack:check` previews package contents. The core uses ESM and the browser Web Animations API; it does not require a framework. See [RELEASE.md](https://github.com/dofy/emoji-motion/blob/main/RELEASE.md) for the release checklist.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
UNICODE LICENSE V3
|
|
2
|
+
|
|
3
|
+
COPYRIGHT AND PERMISSION NOTICE
|
|
4
|
+
|
|
5
|
+
Copyright © 1991-2026 Unicode, Inc.
|
|
6
|
+
|
|
7
|
+
NOTICE TO USER: Carefully read the following legal agreement. BY
|
|
8
|
+
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
|
9
|
+
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
|
10
|
+
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
|
11
|
+
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
14
|
+
copy of data files and any associated documentation (the "Data Files") or
|
|
15
|
+
software and any associated documentation (the "Software") to deal in the
|
|
16
|
+
Data Files or Software without restriction, including without limitation
|
|
17
|
+
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
|
18
|
+
copies of the Data Files or Software, and to permit persons to whom the
|
|
19
|
+
Data Files or Software are furnished to do so, provided that either (a)
|
|
20
|
+
this copyright and permission notice appear with all copies of the Data
|
|
21
|
+
Files or Software, or (b) this copyright and permission notice appear in
|
|
22
|
+
associated Documentation.
|
|
23
|
+
|
|
24
|
+
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
25
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
26
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
|
27
|
+
THIRD PARTY RIGHTS.
|
|
28
|
+
|
|
29
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
|
30
|
+
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
|
31
|
+
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
32
|
+
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
33
|
+
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
|
34
|
+
FILES OR SOFTWARE.
|
|
35
|
+
|
|
36
|
+
Except as contained in this notice, the name of a copyright holder shall
|
|
37
|
+
not be used in advertising or otherwise to promote the sale, use or other
|
|
38
|
+
dealings in these Data Files or Software without prior written
|
|
39
|
+
authorization of the copyright holder.
|
package/package.json
CHANGED
|
@@ -1,6 +1,130 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dofy/native-emoji-motion",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Expressive, extensible animations for native emoji with optional SVG effects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"import": "./src/index.js",
|
|
10
|
+
"default": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./react": {
|
|
13
|
+
"types": "./src/react.d.ts",
|
|
14
|
+
"import": "./src/react.js"
|
|
15
|
+
},
|
|
16
|
+
"./vue": {
|
|
17
|
+
"types": "./src/vue.d.ts",
|
|
18
|
+
"import": "./src/vue.js"
|
|
19
|
+
},
|
|
20
|
+
"./svelte": {
|
|
21
|
+
"types": "./src/svelte.d.ts",
|
|
22
|
+
"import": "./src/svelte.js"
|
|
23
|
+
},
|
|
24
|
+
"./element": {
|
|
25
|
+
"types": "./src/element.d.ts",
|
|
26
|
+
"import": "./src/element.js"
|
|
27
|
+
},
|
|
28
|
+
"./svelte/EmojiMotion.svelte": {
|
|
29
|
+
"types": "./src/EmojiMotion.svelte.d.ts",
|
|
30
|
+
"svelte": "./src/EmojiMotion.svelte",
|
|
31
|
+
"default": "./src/EmojiMotion.svelte"
|
|
32
|
+
},
|
|
33
|
+
"./lite": {
|
|
34
|
+
"types": "./src/lite.d.ts",
|
|
35
|
+
"import": "./src/lite.js",
|
|
36
|
+
"default": "./src/lite.js"
|
|
37
|
+
},
|
|
38
|
+
"./react/lite": {
|
|
39
|
+
"types": "./src/react.d.ts",
|
|
40
|
+
"import": "./src/react-lite.js"
|
|
41
|
+
},
|
|
42
|
+
"./vue/lite": {
|
|
43
|
+
"types": "./src/vue.d.ts",
|
|
44
|
+
"import": "./src/vue-lite.js"
|
|
45
|
+
},
|
|
46
|
+
"./svelte/lite": {
|
|
47
|
+
"types": "./src/svelte.d.ts",
|
|
48
|
+
"import": "./src/svelte-lite.js"
|
|
49
|
+
},
|
|
50
|
+
"./svelte/EmojiMotionLite.svelte": {
|
|
51
|
+
"types": "./src/EmojiMotion.svelte.d.ts",
|
|
52
|
+
"svelte": "./src/EmojiMotionLite.svelte",
|
|
53
|
+
"default": "./src/EmojiMotionLite.svelte"
|
|
54
|
+
},
|
|
55
|
+
"./element/lite": {
|
|
56
|
+
"types": "./src/element.d.ts",
|
|
57
|
+
"import": "./src/element-lite.js"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"types": "./src/index.d.ts",
|
|
61
|
+
"files": [
|
|
62
|
+
"src",
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE",
|
|
65
|
+
"UNICODE-LICENSE.txt",
|
|
66
|
+
"CHANGELOG.md"
|
|
67
|
+
],
|
|
68
|
+
"scripts": {
|
|
69
|
+
"dev": "node demo/server.js",
|
|
70
|
+
"test": "node --test",
|
|
71
|
+
"pack:check": "npm pack --dry-run",
|
|
72
|
+
"build:demo": "node scripts/build-demo.js",
|
|
73
|
+
"prepublishOnly": "npm test && npm run test:types",
|
|
74
|
+
"test:types": "tsc --noEmit --strict --skipLibCheck false --module nodenext --moduleResolution nodenext --target es2022 type-tests/consumer.mts",
|
|
75
|
+
"generate:unicode": "node scripts/generate-unicode.mjs"
|
|
76
|
+
},
|
|
77
|
+
"engines": {
|
|
78
|
+
"node": ">=18"
|
|
79
|
+
},
|
|
80
|
+
"license": "MIT",
|
|
81
|
+
"keywords": [
|
|
82
|
+
"emoji",
|
|
83
|
+
"emoji-animation",
|
|
84
|
+
"animation",
|
|
85
|
+
"web-animations",
|
|
86
|
+
"svg",
|
|
87
|
+
"skin-tone",
|
|
88
|
+
"zero-dependencies"
|
|
89
|
+
],
|
|
90
|
+
"sideEffects": [
|
|
91
|
+
"./src/element.js",
|
|
92
|
+
"./src/element-lite.js"
|
|
93
|
+
],
|
|
94
|
+
"homepage": "https://emoji-motion.phpz.org",
|
|
95
|
+
"repository": {
|
|
96
|
+
"type": "git",
|
|
97
|
+
"url": "git+https://github.com/dofy/emoji-motion.git"
|
|
98
|
+
},
|
|
99
|
+
"bugs": {
|
|
100
|
+
"url": "https://github.com/dofy/emoji-motion/issues"
|
|
101
|
+
},
|
|
102
|
+
"devDependencies": {
|
|
103
|
+
"@types/react": "^19.3.0",
|
|
104
|
+
"@types/react-dom": "^19.3.0",
|
|
105
|
+
"jsdom": "^26.1.0",
|
|
106
|
+
"prismjs": "^1.30.0",
|
|
107
|
+
"react": "^19.3.1",
|
|
108
|
+
"react-dom": "^19.3.1",
|
|
109
|
+
"svelte": "^5.57.3",
|
|
110
|
+
"svelte4": "npm:svelte@^4.2.20",
|
|
111
|
+
"typescript": "~5.9.3",
|
|
112
|
+
"vue": "^3.5.45"
|
|
113
|
+
},
|
|
114
|
+
"peerDependencies": {
|
|
115
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
116
|
+
"svelte": "^4.0.0 || ^5.0.0",
|
|
117
|
+
"vue": "^3.3.0"
|
|
118
|
+
},
|
|
119
|
+
"peerDependenciesMeta": {
|
|
120
|
+
"react": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
123
|
+
"vue": {
|
|
124
|
+
"optional": true
|
|
125
|
+
},
|
|
126
|
+
"svelte": {
|
|
127
|
+
"optional": true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { animateEmoji } from './svelte.js';
|
|
3
|
+
import { emojiMotion } from './index.js';
|
|
4
|
+
export let emoji;
|
|
5
|
+
export let skinTone = undefined;
|
|
6
|
+
export let registry = emojiMotion;
|
|
7
|
+
export let decoration = true;
|
|
8
|
+
export let display = 'inline';
|
|
9
|
+
export let trigger = undefined;
|
|
10
|
+
export let label = undefined;
|
|
11
|
+
export let events = ['pointerenter', 'focus', 'click'];
|
|
12
|
+
let wrapper;
|
|
13
|
+
$: resolved = registry.resolve(emoji, { skinTone });
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<span bind:this={wrapper} role="img" aria-label={label ?? resolved?.name ?? emoji}
|
|
17
|
+
tabindex={trigger ? -1 : 0} style:display={display === 'block' ? 'block' : 'inline-block'}>
|
|
18
|
+
<span style="display: inline-block; position: relative">
|
|
19
|
+
<span aria-hidden="true" style="display: inline-block"
|
|
20
|
+
use:animateEmoji={{ emoji, skinTone, registry, trigger: trigger ?? wrapper, decoration, events }}>{resolved?.glyph ?? emoji}</span>
|
|
21
|
+
</span>
|
|
22
|
+
</span>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from 'svelte';
|
|
2
|
+
import type { EmojiMotion as Registry, SkinTone } from './index.js';
|
|
3
|
+
export interface EmojiMotionProps {
|
|
4
|
+
emoji: string;
|
|
5
|
+
skinTone?: SkinTone;
|
|
6
|
+
registry?: Registry;
|
|
7
|
+
decoration?: boolean;
|
|
8
|
+
display?: 'inline' | 'block';
|
|
9
|
+
trigger?: EventTarget;
|
|
10
|
+
label?: string;
|
|
11
|
+
events?: string[];
|
|
12
|
+
}
|
|
13
|
+
export default class EmojiMotion extends SvelteComponentTyped<EmojiMotionProps> {}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { animateEmoji } from './svelte-lite.js';
|
|
3
|
+
import { emojiMotion } from './lite.js';
|
|
4
|
+
export let emoji;
|
|
5
|
+
export let skinTone = undefined;
|
|
6
|
+
export let registry = emojiMotion;
|
|
7
|
+
export let decoration = true;
|
|
8
|
+
export let display = 'inline';
|
|
9
|
+
export let trigger = undefined;
|
|
10
|
+
export let label = undefined;
|
|
11
|
+
export let events = ['pointerenter', 'focus', 'click'];
|
|
12
|
+
let wrapper;
|
|
13
|
+
$: resolved = registry.resolve(emoji, { skinTone });
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<span bind:this={wrapper} role="img" aria-label={label ?? resolved?.name ?? emoji}
|
|
17
|
+
tabindex={trigger ? -1 : 0} style:display={display === 'block' ? 'block' : 'inline-block'}>
|
|
18
|
+
<span style="display: inline-block; position: relative">
|
|
19
|
+
<span aria-hidden="true" style="display: inline-block"
|
|
20
|
+
use:animateEmoji={{ emoji, skinTone, registry, trigger: trigger ?? wrapper, decoration, events }}>{resolved?.glyph ?? emoji}</span>
|
|
21
|
+
</span>
|
|
22
|
+
</span>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const BaseElement = globalThis.HTMLElement ?? class {};
|
|
2
|
+
/** Framework-independent web component. Import this module to register <emoji-motion>. */
|
|
3
|
+
export function createEmojiMotionElement(emojiMotion) {
|
|
4
|
+
return class EmojiMotionElement extends BaseElement {
|
|
5
|
+
static observedAttributes = ['emoji', 'skin-tone', 'display', 'decoration', 'label'];
|
|
6
|
+
#registry = emojiMotion;
|
|
7
|
+
#dispose = () => {};
|
|
8
|
+
get registry() { return this.#registry; }
|
|
9
|
+
set registry(value) { this.#registry = value ?? emojiMotion; if (this.isConnected) this.#render(); }
|
|
10
|
+
connectedCallback() { this.#render(); }
|
|
11
|
+
disconnectedCallback() { this.#dispose(); }
|
|
12
|
+
attributeChangedCallback() { if (this.isConnected) this.#render(); }
|
|
13
|
+
#render() {
|
|
14
|
+
this.#dispose();
|
|
15
|
+
const emoji = this.getAttribute('emoji') ?? '';
|
|
16
|
+
const skinTone = this.getAttribute('skin-tone') ?? undefined;
|
|
17
|
+
const result = this.#registry.resolve(emoji, { skinTone });
|
|
18
|
+
this.style.display = this.getAttribute('display') === 'block' ? 'block' : 'inline-block';
|
|
19
|
+
this.setAttribute('role', 'img');
|
|
20
|
+
this.setAttribute('aria-label', this.getAttribute('label') ?? result?.name ?? emoji);
|
|
21
|
+
if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
|
|
22
|
+
const visual = this.ownerDocument.createElement('span');
|
|
23
|
+
Object.assign(visual.style, { display: 'inline-block', position: 'relative' });
|
|
24
|
+
const glyph = this.ownerDocument.createElement('span');
|
|
25
|
+
Object.assign(glyph.style, { display: 'inline-block' });
|
|
26
|
+
glyph.setAttribute('aria-hidden', 'true'); glyph.textContent = result?.glyph ?? emoji;
|
|
27
|
+
visual.append(glyph); this.replaceChildren(visual);
|
|
28
|
+
this.#dispose = this.#registry.bind(glyph, emoji, { trigger: this, events: ['pointerenter', 'focus', 'click'], skinTone, ...(this.getAttribute('decoration') !== 'false' ? { decoration: { container: visual } } : {}) });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { emojiMotion } from './lite.js';
|
|
2
|
+
import { createEmojiMotionElement } from './element-core.js';
|
|
3
|
+
export const EmojiMotionElement = createEmojiMotionElement(emojiMotion);
|
|
4
|
+
export function defineEmojiMotion() {
|
|
5
|
+
if (globalThis.customElements && !customElements.get('emoji-motion')) customElements.define('emoji-motion', EmojiMotionElement);
|
|
6
|
+
}
|
|
7
|
+
defineEmojiMotion();
|
package/src/element.d.ts
ADDED
package/src/element.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { emojiMotion } from './index.js';
|
|
2
|
+
import { createEmojiMotionElement } from './element-core.js';
|
|
3
|
+
export const EmojiMotionElement = createEmojiMotionElement(emojiMotion);
|
|
4
|
+
export function defineEmojiMotion() {
|
|
5
|
+
if (globalThis.customElements && !customElements.get('emoji-motion')) customElements.define('emoji-motion', EmojiMotionElement);
|
|
6
|
+
}
|
|
7
|
+
defineEmojiMotion();
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Motion { keyframes: Keyframe[]; options?: KeyframeAnimationOptions; origin?: string }
|
|
2
|
+
export interface Group { label: string; description?: string; motion: string }
|
|
3
|
+
export interface Anchor { x: string; y: string }
|
|
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[] } }
|
|
6
|
+
export interface Decoration { anchor: Anchor; layers: DecorationLayer[] }
|
|
7
|
+
export type SkinTone = 'default' | 'light' | 'medium-light' | 'medium' | 'medium-dark' | 'dark';
|
|
8
|
+
export interface EmojiEntry { group: string; name: string; motion?: string; decoration?: string; variants?: string[]; skinTones?: Partial<Record<Exclude<SkinTone, 'default'>, string>> }
|
|
9
|
+
export interface ClassificationRule { group: string; motion?: string }
|
|
10
|
+
export interface MotionFamily { label: string; group: string; motion: string; members: string[]; aliases?: Record<string, string>; decoration?: string; decorations?: string[]; animation?: KeyframeAnimationOptions }
|
|
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
|
+
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[] }
|
|
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
|
+
export interface EmojiMotion { resolve(glyph: string, options?: { skinTone?: SkinTone }): Resolution | undefined; setDefaultSkinTone(tone: SkinTone): void; getDefaultSkinTone(): SkinTone; play(element: Element, glyph: string, options?: PlayOptions): Animation | null; bind(element: Element, glyph: string, options?: BindOptions): () => void; list(): string[]; groups(): (Group & { id: string })[]; families(): (MotionFamily & { id: string })[] }
|
|
16
|
+
export declare function createEmojiMotion(config?: Config): EmojiMotion;
|
|
17
|
+
export declare const motions: Record<string, Motion>;
|
|
18
|
+
export declare const groups: Record<string, Group>;
|
|
19
|
+
export declare const emoji: Record<string, EmojiEntry>;
|
|
20
|
+
export declare const emojiMotion: EmojiMotion;
|
|
21
|
+
export declare const decorations: Record<string, Decoration>;
|
|
22
|
+
export declare const families: Record<string, MotionFamily>;
|
|
23
|
+
export declare function createSpringMotion(options?: { height?: number; rebounds?: 0 | 1 | 2; duration?: number }): Motion;
|
|
24
|
+
export declare function createSwayMotion(options?: { angle?: number; duration?: number; origin?: string }): Motion;
|
|
25
|
+
export declare const unicodeVersion: string;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createRegistry } from './registry.js';
|
|
2
|
+
import { lookupUnicode, looksLikeEmoji } from './unicode.js';
|
|
3
|
+
|
|
4
|
+
const classifier = { lookup: lookupUnicode, looksLikeEmoji };
|
|
5
|
+
export function createEmojiMotion(config = {}) { return createRegistry(config, classifier); }
|
|
6
|
+
export { motions, groups, emoji, decorations, families, createSpringMotion, createSwayMotion } from './presets.js';
|
|
7
|
+
export { unicodeVersion } from './unicode.js';
|
|
8
|
+
export const emojiMotion = createEmojiMotion();
|
package/src/lite.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { createEmojiMotion, emojiMotion, motions, groups, emoji, decorations, families, createSpringMotion, createSwayMotion } from './index.js';
|
|
2
|
+
export type { Motion, MotionFamily, Group, Anchor, SvgShape, DecorationLayer, Decoration, SkinTone, EmojiEntry, Config, ClassificationRule, PlayOptions, BindOptions, Resolution, EmojiMotion } from './index.js';
|
package/src/lite.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createRegistry } from './registry.js';
|
|
2
|
+
/** Preset-only entry: does not load Unicode classification data. */
|
|
3
|
+
export function createEmojiMotion(config = {}) { return createRegistry(config); }
|
|
4
|
+
export { motions, groups, emoji, decorations, families, createSpringMotion, createSwayMotion } from './presets.js';
|
|
5
|
+
export const emojiMotion = createEmojiMotion();
|