Skip to content

Commit 71c537e

Browse files
committed
v1.1.0: 新增 prefix/suffix/disabled/onAnimationEnd/自定义easing,修复 emoji 宽度和首次动画
1 parent 565c74c commit 71c537e

12 files changed

Lines changed: 274 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Changelog
2+
3+
## [1.1.0] - 2026-01-04
4+
5+
### ✨ 新功能
6+
- **`prefix` / `suffix`** — 静态前后缀支持,不参与滚动动画
7+
- **`disabled`** — 禁用动画,直接显示最终值
8+
- **`onAnimationEnd` / `@animation-end`** — 动画结束回调
9+
- **自定义 easing 函数** — 支持传入 `(t: number) => number` 函数
10+
- **`animateOnMount`** — 控制首次加载是否播放动画(默认 `false`
11+
12+
### 🐛 修复
13+
- **Emoji 宽度检测** — 使用 `codePointAt` 正确处理高位 Unicode(emoji)
14+
- **扩展拉丁字符误判** — 如 `ĀāĒē` 不再被误判为全角字符
15+
- **Vue 首次渲染动画** — 首次加载时不再自动播放动画
16+
17+
### ⚡ 性能优化
18+
- **提取 `isFW`/`getW` 函数** — 避免每帧重复创建函数对象
19+
- **React 外部化配置增强** — 添加 `react-dom/client` 和正则匹配,解决 React 19 兼容问题
20+
21+
### 📝 文档
22+
- 更新 API Props 表格
23+
- 同步 `vue-demo.html` 所有功能
24+
25+
---
26+
27+
## [1.0.4] - 2025-12-28
28+
29+
### ✨ 初始发布
30+
- Levenshtein diff 算法智能文本差异
31+
- React 18+ / Vue 3+ 双框架支持
32+
- 内置 `linear``easeInOut``bounce` 等缓动函数
33+
- 中英文、数字、符号混合滚动
34+
- 动画中断平滑过渡

CHANGELOG_EN.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Changelog
2+
3+
## [1.1.0] - 2026-01-04
4+
5+
### ✨ New Features
6+
- **`prefix` / `suffix`** — Static prefix/suffix support (not animated)
7+
- **`disabled`** — Skip animation, display final value immediately
8+
- **`onAnimationEnd` / `@animation-end`** — Callback when animation completes
9+
- **Custom easing function** — Pass `(t: number) => number` directly
10+
- **`animateOnMount`** — Control initial render animation (default `false`)
11+
12+
### 🐛 Bug Fixes
13+
- **Emoji width detection** — Use `codePointAt` for high Unicode (emoji) support
14+
- **Extended Latin misdetection** — Characters like `ĀāĒē` no longer misidentified as full-width
15+
- **Vue initial animation** — No animation on first render by default
16+
17+
### ⚡ Performance
18+
- **Extract `isFW`/`getW` functions** — Avoid recreating functions per frame
19+
- **Enhanced React externalization** — Add `react-dom/client` and regex patterns for React 19 compatibility
20+
21+
### 📝 Documentation
22+
- Updated API Props table
23+
- Synced all features to `vue-demo.html`
24+
25+
---
26+
27+
## [1.0.4] - 2025-12-28
28+
29+
### ✨ Initial Release
30+
- Levenshtein diff algorithm for smart text diffing
31+
- React 18+ / Vue 3+ dual framework support
32+
- Built-in `linear`, `easeInOut`, `bounce` easing functions
33+
- CJK, numbers, symbols, emoji mixed scrolling
34+
- Smooth animation interruption handling

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,16 @@ const price = ref('73.18');
143143
|------|------|--------|------|
144144
| `value` | `string` | - | 要显示的文本值(必填) |
145145
| `duration` | `number` | `500` | 动画持续时间(毫秒) |
146-
| `easing` | `string` | `'easeInOut'` | 缓动函数:`linear``easeIn``easeOut``easeInOut``bounce` |
146+
| `easing` | `EasingName \| function` | `'easeInOut'` | 缓动函数:`linear``easeIn``easeOut``easeInOut``bounce`,或自定义 `(t: number) => number` |
147147
| `direction` | `string` | `'ANY'` | 滚动方向:`UP``DOWN``ANY`(自动选择最短路径) |
148148
| `charWidth` | `number` | `1` | 字符宽度倍率(基准为 0.8em) |
149149
| `characterLists` | `string[]` | `['0123456789']` | 支持的字符列表 |
150150
| `className` | `string` | `''` | 自定义 CSS 类名 |
151+
| `animateOnMount` | `boolean` | `false` | 首次加载时是否播放动画 |
152+
| `disabled` | `boolean` | `false` | 禁用动画,直接显示最终值 |
153+
| `prefix` | `string` | - | 静态前缀(不参与滚动动画) |
154+
| `suffix` | `string` | - | 静态后缀(不参与滚动动画) |
155+
| `onAnimationEnd` | `() => void` | - | 动画结束回调(Vue: `@animation-end`|
151156

152157
### 内置字符列表
153158

@@ -239,5 +244,9 @@ smart-ticker/
239244
- **框架**: React 18 / Vue 3
240245
- **样式**: CSS Variables + 响应式设计
241246

247+
## 📝 更新日志
248+
249+
查看 [CHANGELOG.md](./CHANGELOG.md) 了解版本更新详情。
250+
242251
## 📄 License
243252
MIT

README_EN.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,16 @@ The component uses the system monospace stack by default. To use a custom font (
140140
|------|------|--------|------|
141141
| `value` | `string` | - | The text value to display (Required) |
142142
| `duration` | `number` | `500` | Animation duration (ms) |
143-
| `easing` | `string` | `'easeInOut'` | Easing function: `linear`, `easeIn`, `easeOut`, `easeInOut`, `bounce` |
143+
| `easing` | `EasingName \| function` | `'easeInOut'` | Easing: `linear`, `easeIn`, `easeOut`, `easeInOut`, `bounce`, or custom `(t: number) => number` |
144144
| `direction` | `string` | `'ANY'` | Scroll direction: `UP`, `DOWN`, `ANY` (shortest path) |
145145
| `charWidth` | `number` | `1` | Character width multiplier (base 0.8em) |
146146
| `characterLists` | `string[]` | `['0123456789']` | Allowed character sets |
147147
| `className` | `string` | `''` | Custom CSS class name |
148+
| `animateOnMount` | `boolean` | `false` | Animate on initial render |
149+
| `disabled` | `boolean` | `false` | Disable animation, show final value immediately |
150+
| `prefix` | `string` | - | Static prefix (not animated) |
151+
| `suffix` | `string` | - | Static suffix (not animated) |
152+
| `onAnimationEnd` | `() => void` | - | Callback when animation ends (Vue: `@animation-end`) |
148153

149154
### Built-in Character Lists
150155

@@ -237,6 +242,10 @@ smart-ticker/
237242
- **Frameworks**: React 18 / Vue 3
238243
- **Styling**: CSS Variables + Responsive Design
239244

245+
## 📝 Changelog
246+
247+
See [CHANGELOG_EN.md](./CHANGELOG_EN.md) for version history.
248+
240249
## 📄 License
241250

242251
MIT

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tombcato/smart-ticker",
3-
"version": "1.0.4",
3+
"version": "1.1.0",
44
"description": "Smart text animation component with diff-based character scrollin",
55
"type": "module",
66
"main": "./dist/index.cjs",

public/vue-demo.html

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -385,23 +385,29 @@
385385
const TickerComponent = {
386386
template: `
387387
<div class="ticker" :class="className">
388-
<div v-for="(col, i) in renderedColumns" :key="i" class="ticker-column" :style="{ width: col.width + 'em' }">
388+
<span v-if="prefix" class="ticker-prefix">{{ prefix }}</span>
389+
<div v-for="(col, i) in renderedColumns" :key="i" class="ticker-column" :style="{ width: col.width + 'em', opacity: col.opacity }">
389390
<div v-for="charObj in col.chars" :key="charObj.key" class="ticker-char" :style="{ transform: 'translateY(' + charObj.offset + 'em)' }">
390391
{{ charObj.char === '${EMPTY_CHAR}' ? '\u00A0' : charObj.char }}
391392
</div>
392393
</div>
394+
<span v-if="suffix" class="ticker-suffix">{{ suffix }}</span>
393395
</div>
394396
`,
395397
props: {
396398
value: { type: String, required: true },
397399
characterLists: { type: Array, default: () => ['0123456789'] },
398400
duration: { type: Number, default: 500 },
399401
direction: { type: String, default: 'ANY' },
400-
easing: { type: String, default: 'easeInOut' },
402+
easing: { type: [String, Function], default: 'easeInOut' },
401403
className: { type: String, default: '' },
402404
charWidth: { type: Number, default: 1 },
405+
disabled: { type: Boolean, default: false },
406+
prefix: { type: String, default: '' },
407+
suffix: { type: String, default: '' },
403408
},
404-
setup(props) {
409+
emits: ['animationEnd'],
410+
setup(props, { emit }) {
405411
const columns = ref([]);
406412
const progress = ref(1);
407413
let animId;
@@ -441,10 +447,21 @@
441447
}
442448

443449
columns.value = result;
450+
// disabled 跳过动画
451+
if (props.disabled) {
452+
progress.value = 1;
453+
const final = result.map(c => applyProgress(c, 1).col).filter(c => c.currentWidth > 0);
454+
columns.value = final;
455+
return;
456+
}
457+
444458
progress.value = 0;
445459
const start = performance.now();
446460
const dur = props.duration;
447-
const easeFn = easingFunctions[props.easing] || easingFunctions.linear;
461+
// 支持自定义 easing 函数
462+
const easeFn = typeof props.easing === 'function'
463+
? props.easing
464+
: easingFunctions[props.easing] || easingFunctions.linear;
448465
let lastUpdate = 0;
449466

450467
const animate = (now) => {
@@ -460,10 +477,24 @@
460477
columns.value = final;
461478
progress.value = 1;
462479
animId = undefined;
480+
// emit animationEnd
481+
emit('animationEnd');
463482
}
464483
};
465484
animId = requestAnimationFrame(animate);
466-
}, { immediate: true });
485+
}, { immediate: false }); // 首次不触发动画
486+
487+
// 首次渲染:直接显示初始值
488+
const initValue = props.value;
489+
if (initValue) {
490+
const targetChars = initValue.split('');
491+
const result = targetChars.map(char => {
492+
const col = setTarget(createColumn(), char, lists.value, props.direction);
493+
return applyProgress(col, 1).col;
494+
}).filter(c => c.currentWidth > 0);
495+
columns.value = result;
496+
progress.value = 1;
497+
}
467498

468499
onUnmounted(() => { if (animId) cancelAnimationFrame(animId); });
469500

@@ -473,10 +504,19 @@
473504
const { charIdx, delta } = applyProgress(col, progress.value);
474505
const logicalWidth = col.sourceWidth + (col.targetWidth - col.sourceWidth) * progress.value;
475506

476-
// Full-width character interpolation logic
507+
// Full-width character detection (emoji + CJK)
477508
const list = col.charList || [];
478-
const isFW = (c) => c && c.length > 0 && c.charCodeAt(0) > 255;
479-
const getW = (c) => isFW(c) ? 1.1 : 0.8;
509+
const isFW = (c) => {
510+
if (!c || c.length === 0) return false;
511+
const code = c.codePointAt(0) || 0;
512+
return (
513+
(code >= 0x3000 && code <= 0x9FFF) || // CJK
514+
(code >= 0xAC00 && code <= 0xD7AF) || // 韩文
515+
(code >= 0xFF00 && code <= 0xFFEF) || // 全角 ASCII
516+
(code >= 0x1F300 && code <= 0x1FAFF) // Emoji
517+
);
518+
};
519+
const getW = (c) => isFW(c) ? 1.25 : 0.8;
480520

481521
const startChar = list[col.startIndex] || '';
482522
const endChar = list[col.endIndex] || '';
@@ -498,7 +538,13 @@
498538
add(charIdx, 0, 'c');
499539
add(charIdx + 1, -charHeight, 'n');
500540
add(charIdx - 1, charHeight, 'p');
501-
return { width, chars };
541+
542+
// Alpha 渐变:新增/删除列
543+
const isDeleting = col.targetWidth === 0 && col.sourceWidth > 0;
544+
const isInserting = col.sourceWidth === 0 && col.targetWidth > 0;
545+
const opacity = isDeleting ? 1 - progress.value : isInserting ? progress.value : 1;
546+
547+
return { width, chars, opacity };
502548
}).filter(c => c.width > 0);
503549
});
504550

0 commit comments

Comments
 (0)