@tombcato/smart-ticker 1.0.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/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/TickerCore-DDdYkrsq.js +244 -0
- package/dist/TickerCore-DDdYkrsq.js.map +1 -0
- package/dist/TickerCore-DSrG8V7Z.cjs +243 -0
- package/dist/TickerCore-DSrG8V7Z.cjs.map +1 -0
- package/dist/index.cjs +153 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +1 -0
- package/dist/logo-dark.svg +7 -0
- package/dist/logo.svg +7 -0
- package/dist/style.css +53 -0
- package/dist/vue-demo.html +542 -0
- package/dist/vue.cjs +148 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.ts +122 -0
- package/dist/vue.js +149 -0
- package/dist/vue.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Vue Ticker Demo</title>
|
|
8
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Inter:wght@400;500;600;700&display=swap"
|
|
11
|
+
rel="stylesheet">
|
|
12
|
+
<style>
|
|
13
|
+
:root {
|
|
14
|
+
--bg: #0f172a;
|
|
15
|
+
--bg-card: #1e293b;
|
|
16
|
+
--text: #f8fafc;
|
|
17
|
+
--text-muted: #94a3b8;
|
|
18
|
+
--accent: #6366f1;
|
|
19
|
+
--accent-glow: rgba(99, 102, 241, 0.4);
|
|
20
|
+
--border: #334155;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
background-color: var(--bg);
|
|
25
|
+
color: var(--text);
|
|
26
|
+
font-family: 'Inter', sans-serif;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
margin: 0;
|
|
33
|
+
overflow-x: hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Price Hero Styles */
|
|
37
|
+
.price-hero {
|
|
38
|
+
width: 100%;
|
|
39
|
+
max-width: 1000px;
|
|
40
|
+
padding: 2rem;
|
|
41
|
+
text-align: center;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.price-label {
|
|
45
|
+
font-size: 1rem;
|
|
46
|
+
color: var(--text-muted);
|
|
47
|
+
margin-bottom: 0.5rem;
|
|
48
|
+
text-transform: uppercase;
|
|
49
|
+
letter-spacing: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.price-value {
|
|
53
|
+
font-family: 'JetBrains Mono', monospace;
|
|
54
|
+
font-size: 5rem;
|
|
55
|
+
font-weight: 700;
|
|
56
|
+
color: var(--text);
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
margin-bottom: 3rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.price-currency {
|
|
64
|
+
font-size: 3rem;
|
|
65
|
+
margin-right: 0.25rem;
|
|
66
|
+
color: var(--text-muted);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (max-width: 768px) {
|
|
70
|
+
.price-value {
|
|
71
|
+
font-size: 3rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.price-currency {
|
|
75
|
+
font-size: 2rem;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Controls */
|
|
80
|
+
.price-controls {
|
|
81
|
+
display: flex;
|
|
82
|
+
gap: 2rem;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
flex-wrap: wrap;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.control-group {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 0.5rem;
|
|
92
|
+
background: rgba(30, 41, 59, 0.5);
|
|
93
|
+
padding: 1rem;
|
|
94
|
+
border-radius: 12px;
|
|
95
|
+
border: 1px solid var(--border);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.control-label {
|
|
99
|
+
font-size: 0.8rem;
|
|
100
|
+
color: var(--text-muted);
|
|
101
|
+
text-transform: uppercase;
|
|
102
|
+
letter-spacing: 1px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.control-buttons {
|
|
106
|
+
display: flex;
|
|
107
|
+
gap: 0.4rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.control-buttons button {
|
|
111
|
+
padding: 0.4rem 0.8rem;
|
|
112
|
+
background: rgba(255, 255, 255, 0.05);
|
|
113
|
+
border: 1px solid var(--border);
|
|
114
|
+
border-radius: 6px;
|
|
115
|
+
color: var(--text);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-size: 0.8rem;
|
|
118
|
+
font-family: 'Inter', sans-serif;
|
|
119
|
+
transition: all 0.2s;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.control-buttons button:hover {
|
|
123
|
+
background: rgba(255, 255, 255, 0.1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.control-buttons button.active {
|
|
127
|
+
background: var(--accent);
|
|
128
|
+
border-color: var(--accent);
|
|
129
|
+
box-shadow: 0 0 10px var(--accent-glow);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Ticker Component Styles */
|
|
133
|
+
.ticker {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
font-family: 'JetBrains Mono', monospace;
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
line-height: 1.2;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.ticker-column {
|
|
142
|
+
display: inline-block;
|
|
143
|
+
height: 1.2em;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
position: relative;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.ticker-char {
|
|
149
|
+
position: absolute;
|
|
150
|
+
top: 0;
|
|
151
|
+
left: 0;
|
|
152
|
+
right: 0;
|
|
153
|
+
height: 1.2em;
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: center;
|
|
157
|
+
white-space: pre;
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
160
|
+
</head>
|
|
161
|
+
|
|
162
|
+
<body>
|
|
163
|
+
<div id="app">
|
|
164
|
+
<div class="price-hero">
|
|
165
|
+
<div class="price-label">价格</div>
|
|
166
|
+
<div class="price-value">
|
|
167
|
+
<span class="price-currency">$</span>
|
|
168
|
+
<ticker-component :value="heroPrice" :duration="animDuration" :easing="easing" :char-width="charWidth"
|
|
169
|
+
:character-lists="['0123456789.,']"></ticker-component>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="price-controls">
|
|
173
|
+
<div class="control-group">
|
|
174
|
+
<span class="control-label">字符宽度</span>
|
|
175
|
+
<div class="control-buttons">
|
|
176
|
+
<button v-for="w in widthOptions" :key="w" :class="{ active: charWidth === w }"
|
|
177
|
+
@click="charWidth = w">
|
|
178
|
+
{{ w }}x
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="control-group">
|
|
183
|
+
<span class="control-label">动画时长</span>
|
|
184
|
+
<div class="control-buttons">
|
|
185
|
+
<button v-for="d in durationOptions" :key="d" :class="{ active: animDuration === d }"
|
|
186
|
+
@click="animDuration = d">
|
|
187
|
+
{{ d }}ms
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="control-group">
|
|
192
|
+
<span class="control-label">缓动曲线</span>
|
|
193
|
+
<div class="control-buttons">
|
|
194
|
+
<button v-for="e in easingOptions" :key="e.name" :class="{ active: easing === e.name }"
|
|
195
|
+
@click="easing = e.name">
|
|
196
|
+
{{ e.label }}
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<script>
|
|
205
|
+
const { createApp, ref, computed, watch, onUnmounted, onMounted } = Vue;
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Constants & Logic
|
|
209
|
+
// ============================================================================
|
|
210
|
+
const EMPTY_CHAR = '\0';
|
|
211
|
+
|
|
212
|
+
const easingFunctions = {
|
|
213
|
+
linear: (t) => t,
|
|
214
|
+
easeIn: (t) => t * t,
|
|
215
|
+
easeOut: (t) => 1 - (1 - t) * (1 - t),
|
|
216
|
+
easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
|
|
217
|
+
bounce: (t) => {
|
|
218
|
+
const n1 = 7.5625, d1 = 2.75;
|
|
219
|
+
if (t < 1 / d1) return n1 * t * t;
|
|
220
|
+
else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
221
|
+
else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
222
|
+
else return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
class TickerCharacterList {
|
|
227
|
+
constructor(characterList) {
|
|
228
|
+
const charsArray = characterList.split('');
|
|
229
|
+
const length = charsArray.length;
|
|
230
|
+
this.numOriginalCharacters = length;
|
|
231
|
+
this.characterIndicesMap = new Map();
|
|
232
|
+
for (let i = 0; i < length; i++) this.characterIndicesMap.set(charsArray[i], i);
|
|
233
|
+
this.characterList = new Array(length * 2 + 1);
|
|
234
|
+
this.characterList[0] = EMPTY_CHAR;
|
|
235
|
+
for (let i = 0; i < length; i++) {
|
|
236
|
+
this.characterList[1 + i] = charsArray[i];
|
|
237
|
+
this.characterList[1 + length + i] = charsArray[i];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
getCharacterIndices(start, end, direction) {
|
|
241
|
+
let startIndex = this.getIndexOfChar(start);
|
|
242
|
+
let endIndex = this.getIndexOfChar(end);
|
|
243
|
+
if (startIndex < 0 || endIndex < 0) return null;
|
|
244
|
+
if (direction === 'DOWN') {
|
|
245
|
+
if (end === EMPTY_CHAR) endIndex = this.characterList.length;
|
|
246
|
+
else if (endIndex < startIndex) endIndex += this.numOriginalCharacters;
|
|
247
|
+
} else if (direction === 'UP') {
|
|
248
|
+
if (startIndex < endIndex) startIndex += this.numOriginalCharacters;
|
|
249
|
+
} else if (direction === 'ANY') {
|
|
250
|
+
if (start !== EMPTY_CHAR && end !== EMPTY_CHAR) {
|
|
251
|
+
if (endIndex < startIndex) {
|
|
252
|
+
const nonWrap = startIndex - endIndex;
|
|
253
|
+
const wrap = this.numOriginalCharacters - startIndex + endIndex;
|
|
254
|
+
if (wrap < nonWrap) endIndex += this.numOriginalCharacters;
|
|
255
|
+
} else if (startIndex < endIndex) {
|
|
256
|
+
const nonWrap = endIndex - startIndex;
|
|
257
|
+
const wrap = this.numOriginalCharacters - endIndex + startIndex;
|
|
258
|
+
if (wrap < nonWrap) startIndex += this.numOriginalCharacters;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { startIndex, endIndex };
|
|
263
|
+
}
|
|
264
|
+
getSupportedCharacters() { return new Set(this.characterIndicesMap.keys()); }
|
|
265
|
+
getCharacterList() { return this.characterList; }
|
|
266
|
+
getIndexOfChar(c) {
|
|
267
|
+
if (c === EMPTY_CHAR) return 0;
|
|
268
|
+
if (this.characterIndicesMap.has(c)) return this.characterIndicesMap.get(c) + 1;
|
|
269
|
+
return -1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const ACTION_SAME = 0, ACTION_INSERT = 1, ACTION_DELETE = 2;
|
|
274
|
+
function computeColumnActions(source, target, supported) {
|
|
275
|
+
let si = 0, ti = 0;
|
|
276
|
+
const actions = [];
|
|
277
|
+
while (true) {
|
|
278
|
+
const endS = si === source.length;
|
|
279
|
+
const endT = ti === target.length;
|
|
280
|
+
if (endS && endT) break;
|
|
281
|
+
if (endS) { for (; ti < target.length; ti++) actions.push(ACTION_INSERT); break; }
|
|
282
|
+
if (endT) { for (; si < source.length; si++) actions.push(ACTION_DELETE); break; }
|
|
283
|
+
const sSupp = supported.has(source[si]);
|
|
284
|
+
const tSupp = supported.has(target[ti]);
|
|
285
|
+
if (sSupp && tSupp) {
|
|
286
|
+
let se = si + 1, te = ti + 1;
|
|
287
|
+
while (se < source.length && supported.has(source[se])) se++;
|
|
288
|
+
while (te < target.length && supported.has(target[te])) te++;
|
|
289
|
+
const sLen = se - si, tLen = te - ti;
|
|
290
|
+
if (sLen === tLen) for (let i = 0; i < sLen; i++) actions.push(ACTION_SAME);
|
|
291
|
+
else {
|
|
292
|
+
const matrix = Array(sLen + 1).fill(null).map(() => Array(tLen + 1).fill(0));
|
|
293
|
+
for (let i = 0; i <= sLen; i++) matrix[i][0] = i;
|
|
294
|
+
for (let j = 0; j <= tLen; j++) matrix[0][j] = j;
|
|
295
|
+
for (let r = 1; r <= sLen; r++) {
|
|
296
|
+
for (let c = 1; c <= tLen; c++) {
|
|
297
|
+
const cost = source[si + r - 1] === target[ti + c - 1] ? 0 : 1;
|
|
298
|
+
matrix[r][c] = Math.min(matrix[r - 1][c] + 1, matrix[r][c - 1] + 1, matrix[r - 1][c - 1] + cost);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const result = [];
|
|
302
|
+
let r = sLen, c = tLen;
|
|
303
|
+
while (r > 0 || c > 0) {
|
|
304
|
+
if (r === 0) { result.push(ACTION_INSERT); c--; }
|
|
305
|
+
else if (c === 0) { result.push(ACTION_DELETE); r--; }
|
|
306
|
+
else {
|
|
307
|
+
const ins = matrix[r][c - 1], del = matrix[r - 1][c], rep = matrix[r - 1][c - 1];
|
|
308
|
+
if (ins < del && ins < rep) { result.push(ACTION_INSERT); c--; }
|
|
309
|
+
else if (del < rep) { result.push(ACTION_DELETE); r--; }
|
|
310
|
+
else { result.push(ACTION_SAME); r--; c--; }
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (let i = result.length - 1; i >= 0; i--) actions.push(result[i]);
|
|
314
|
+
}
|
|
315
|
+
si = se; ti = te;
|
|
316
|
+
} else if (sSupp) { actions.push(ACTION_INSERT); ti++; }
|
|
317
|
+
else if (tSupp) { actions.push(ACTION_DELETE); si++; }
|
|
318
|
+
else { actions.push(ACTION_SAME); si++; ti++; }
|
|
319
|
+
}
|
|
320
|
+
return actions;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function createColumn() {
|
|
324
|
+
return {
|
|
325
|
+
currentChar: EMPTY_CHAR, targetChar: EMPTY_CHAR, charList: null,
|
|
326
|
+
startIndex: 0, endIndex: 0, sourceWidth: 0, currentWidth: 0, targetWidth: 0,
|
|
327
|
+
directionAdj: 1, prevDelta: 0, currDelta: 0,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function setTarget(col, target, lists, dir) {
|
|
332
|
+
const c = { ...col };
|
|
333
|
+
c.targetChar = target;
|
|
334
|
+
c.sourceWidth = c.currentWidth;
|
|
335
|
+
c.targetWidth = target === EMPTY_CHAR ? 0 : 1;
|
|
336
|
+
let found = false;
|
|
337
|
+
for (const list of lists) {
|
|
338
|
+
const indices = list.getCharacterIndices(c.currentChar, target, dir);
|
|
339
|
+
if (indices) {
|
|
340
|
+
c.charList = list.getCharacterList();
|
|
341
|
+
c.startIndex = indices.startIndex;
|
|
342
|
+
c.endIndex = indices.endIndex;
|
|
343
|
+
found = true;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!found) {
|
|
348
|
+
c.charList = c.currentChar === target ? [c.currentChar] : [c.currentChar, target];
|
|
349
|
+
c.startIndex = 0;
|
|
350
|
+
c.endIndex = c.currentChar === target ? 0 : 1;
|
|
351
|
+
}
|
|
352
|
+
c.directionAdj = c.endIndex >= c.startIndex ? 1 : -1;
|
|
353
|
+
c.prevDelta = c.currDelta;
|
|
354
|
+
c.currDelta = 0;
|
|
355
|
+
return c;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function applyProgress(col, progress, forceUpdate = false) {
|
|
359
|
+
const c = { ...col };
|
|
360
|
+
const total = Math.abs(c.endIndex - c.startIndex);
|
|
361
|
+
const pos = progress * total;
|
|
362
|
+
const offset = pos - Math.floor(pos);
|
|
363
|
+
const additional = c.prevDelta * (1 - progress);
|
|
364
|
+
const delta = offset * c.directionAdj + additional;
|
|
365
|
+
const charIdx = c.startIndex + Math.floor(pos) * c.directionAdj;
|
|
366
|
+
if (progress >= 1) {
|
|
367
|
+
c.currentChar = c.targetChar;
|
|
368
|
+
c.currDelta = 0;
|
|
369
|
+
c.prevDelta = 0;
|
|
370
|
+
} else if (forceUpdate && c.charList && charIdx >= 0 && charIdx < c.charList.length) {
|
|
371
|
+
c.currentChar = c.charList[charIdx];
|
|
372
|
+
c.currDelta = delta;
|
|
373
|
+
}
|
|
374
|
+
c.currentWidth = c.sourceWidth + (c.targetWidth - c.sourceWidth) * progress;
|
|
375
|
+
return { col: c, charIdx, delta };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const TickerComponent = {
|
|
379
|
+
template: `
|
|
380
|
+
<div class="ticker" :class="className">
|
|
381
|
+
<div v-for="(col, i) in renderedColumns" :key="i" class="ticker-column" :style="{ width: col.width + 'em' }">
|
|
382
|
+
<div v-for="charObj in col.chars" :key="charObj.key" class="ticker-char" :style="{ transform: 'translateY(' + charObj.offset + 'em)' }">
|
|
383
|
+
{{ charObj.char === '${EMPTY_CHAR}' ? '\u00A0' : charObj.char }}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
`,
|
|
388
|
+
props: {
|
|
389
|
+
value: { type: String, required: true },
|
|
390
|
+
characterLists: { type: Array, default: () => ['0123456789'] },
|
|
391
|
+
duration: { type: Number, default: 500 },
|
|
392
|
+
direction: { type: String, default: 'ANY' },
|
|
393
|
+
easing: { type: String, default: 'easeInOut' },
|
|
394
|
+
className: { type: String, default: '' },
|
|
395
|
+
charWidth: { type: Number, default: 1 },
|
|
396
|
+
},
|
|
397
|
+
setup(props) {
|
|
398
|
+
const columns = ref([]);
|
|
399
|
+
const progress = ref(1);
|
|
400
|
+
let animId;
|
|
401
|
+
|
|
402
|
+
const lists = computed(() => props.characterLists.map(s => new TickerCharacterList(s)));
|
|
403
|
+
const supported = computed(() => {
|
|
404
|
+
const set = new Set();
|
|
405
|
+
lists.value.forEach(l => l.getSupportedCharacters().forEach(c => set.add(c)));
|
|
406
|
+
return set;
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
watch(() => props.value, (newValue, oldValue) => {
|
|
410
|
+
if (newValue === oldValue) return;
|
|
411
|
+
if (animId) { cancelAnimationFrame(animId); animId = undefined; }
|
|
412
|
+
|
|
413
|
+
let currentCols = columns.value;
|
|
414
|
+
if (progress.value < 1 && progress.value > 0) {
|
|
415
|
+
currentCols = currentCols.map(c => applyProgress(c, progress.value, true).col);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const targetChars = newValue.split('');
|
|
419
|
+
const sourceChars = currentCols.map(c => c.currentChar);
|
|
420
|
+
const actions = computeColumnActions(sourceChars, targetChars, supported.value);
|
|
421
|
+
let ci = 0, ti = 0;
|
|
422
|
+
const result = [];
|
|
423
|
+
const validCols = currentCols.filter(c => c.currentWidth > 0);
|
|
424
|
+
|
|
425
|
+
for (const action of actions) {
|
|
426
|
+
if (action === ACTION_INSERT) result.push(setTarget(createColumn(), targetChars[ti++], lists.value, props.direction));
|
|
427
|
+
else if (action === ACTION_SAME) {
|
|
428
|
+
const existing = validCols[ci++] || createColumn();
|
|
429
|
+
result.push(setTarget(existing, targetChars[ti++], lists.value, props.direction));
|
|
430
|
+
} else {
|
|
431
|
+
const existing = validCols[ci++] || createColumn();
|
|
432
|
+
result.push(setTarget(existing, EMPTY_CHAR, lists.value, props.direction));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
columns.value = result;
|
|
437
|
+
progress.value = 0;
|
|
438
|
+
const start = performance.now();
|
|
439
|
+
const dur = props.duration;
|
|
440
|
+
const easeFn = easingFunctions[props.easing] || easingFunctions.linear;
|
|
441
|
+
let lastUpdate = 0;
|
|
442
|
+
|
|
443
|
+
const animate = (now) => {
|
|
444
|
+
const linearP = Math.min((now - start) / dur, 1);
|
|
445
|
+
const p = easeFn(linearP);
|
|
446
|
+
if (now - lastUpdate >= 16 || linearP >= 1) {
|
|
447
|
+
lastUpdate = now;
|
|
448
|
+
progress.value = p;
|
|
449
|
+
}
|
|
450
|
+
if (linearP < 1) animId = requestAnimationFrame(animate);
|
|
451
|
+
else {
|
|
452
|
+
const final = columns.value.map(c => applyProgress(c, 1).col).filter(c => c.currentWidth > 0);
|
|
453
|
+
columns.value = final;
|
|
454
|
+
progress.value = 1;
|
|
455
|
+
animId = undefined;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
animId = requestAnimationFrame(animate);
|
|
459
|
+
}, { immediate: true });
|
|
460
|
+
|
|
461
|
+
onUnmounted(() => { if (animId) cancelAnimationFrame(animId); });
|
|
462
|
+
|
|
463
|
+
const charHeight = 1.2;
|
|
464
|
+
const renderedColumns = computed(() => {
|
|
465
|
+
return columns.value.map(col => {
|
|
466
|
+
const { charIdx, delta } = applyProgress(col, progress.value);
|
|
467
|
+
const logicalWidth = col.sourceWidth + (col.targetWidth - col.sourceWidth) * progress.value;
|
|
468
|
+
const width = logicalWidth * props.charWidth * 0.8;
|
|
469
|
+
if (width <= 0) return { width, chars: [] };
|
|
470
|
+
const chars = [];
|
|
471
|
+
const list = col.charList || [];
|
|
472
|
+
const deltaEm = delta * charHeight;
|
|
473
|
+
const add = (idx, offsetMod, keyPrefix) => {
|
|
474
|
+
if (idx >= 0 && idx < list.length) {
|
|
475
|
+
chars.push({ key: keyPrefix + '-' + idx, char: list[idx], offset: deltaEm + offsetMod });
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
add(charIdx, 0, 'c');
|
|
479
|
+
add(charIdx + 1, -charHeight, 'n');
|
|
480
|
+
add(charIdx - 1, charHeight, 'p');
|
|
481
|
+
return { width, chars };
|
|
482
|
+
}).filter(c => c.width > 0);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return { renderedColumns, EMPTY_CHAR };
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
createApp({
|
|
490
|
+
components: { TickerComponent },
|
|
491
|
+
setup() {
|
|
492
|
+
const priceSequence = ['73.18', '76.58', '173.50', '9.1'];
|
|
493
|
+
const heroPrice = ref(priceSequence[0]);
|
|
494
|
+
const animDuration = ref(800);
|
|
495
|
+
const easing = ref('easeInOut');
|
|
496
|
+
const charWidth = ref(1);
|
|
497
|
+
const durationOptions = [400, 800, 1200];
|
|
498
|
+
const widthOptions = [0.8, 1, 1.2];
|
|
499
|
+
const easingOptions = [
|
|
500
|
+
{ name: 'linear', label: '线性' },
|
|
501
|
+
{ name: 'easeInOut', label: '先加后减' },
|
|
502
|
+
{ name: 'bounce', label: '回弹' },
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
let interval;
|
|
506
|
+
|
|
507
|
+
watch(animDuration, (newDur) => {
|
|
508
|
+
if (interval) clearInterval(interval);
|
|
509
|
+
startInterval();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const startInterval = () => {
|
|
513
|
+
let priceIdx = 0;
|
|
514
|
+
interval = setInterval(() => {
|
|
515
|
+
priceIdx = (priceIdx + 1) % priceSequence.length;
|
|
516
|
+
heroPrice.value = priceSequence[priceIdx];
|
|
517
|
+
}, animDuration.value + 1000);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
onMounted(() => {
|
|
521
|
+
startInterval();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
onUnmounted(() => {
|
|
525
|
+
if (interval) clearInterval(interval);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
heroPrice,
|
|
530
|
+
animDuration,
|
|
531
|
+
easing,
|
|
532
|
+
charWidth,
|
|
533
|
+
durationOptions,
|
|
534
|
+
widthOptions,
|
|
535
|
+
easingOptions
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}).mount('#app');
|
|
539
|
+
</script>
|
|
540
|
+
</body>
|
|
541
|
+
|
|
542
|
+
</html>
|
package/dist/vue.cjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const vue = require("vue");
|
|
4
|
+
const TickerCore = require("./TickerCore-DSrG8V7Z.cjs");
|
|
5
|
+
const charHeight = 1.2;
|
|
6
|
+
const _sfc_main = /* @__PURE__ */ vue.defineComponent({
|
|
7
|
+
__name: "Ticker",
|
|
8
|
+
props: {
|
|
9
|
+
value: { type: String, required: true },
|
|
10
|
+
characterLists: { type: Array, default: () => ["0123456789"] },
|
|
11
|
+
duration: { type: Number, default: 500 },
|
|
12
|
+
direction: { type: String, default: "ANY" },
|
|
13
|
+
easing: { type: String, default: "easeInOut" },
|
|
14
|
+
className: { type: String, default: "" }
|
|
15
|
+
},
|
|
16
|
+
setup(__props) {
|
|
17
|
+
const props = __props;
|
|
18
|
+
const columns = vue.ref([]);
|
|
19
|
+
const progress = vue.ref(1);
|
|
20
|
+
let animId;
|
|
21
|
+
const lists = vue.computed(() => props.characterLists.map((s) => new TickerCore.TickerCharacterList(s)));
|
|
22
|
+
const supported = vue.computed(() => {
|
|
23
|
+
const set = /* @__PURE__ */ new Set();
|
|
24
|
+
lists.value.forEach((l) => l.getSupportedCharacters().forEach((c) => set.add(c)));
|
|
25
|
+
return set;
|
|
26
|
+
});
|
|
27
|
+
vue.watch(() => props.value, (newValue, oldValue) => {
|
|
28
|
+
if (newValue === oldValue) return;
|
|
29
|
+
if (animId) {
|
|
30
|
+
cancelAnimationFrame(animId);
|
|
31
|
+
animId = void 0;
|
|
32
|
+
}
|
|
33
|
+
let currentCols = columns.value;
|
|
34
|
+
if (progress.value < 1 && progress.value > 0) {
|
|
35
|
+
currentCols = currentCols.map((c) => TickerCore.applyProgress(c, progress.value, true).col);
|
|
36
|
+
}
|
|
37
|
+
const targetChars = newValue.split("");
|
|
38
|
+
const sourceChars = currentCols.map((c) => c.currentChar);
|
|
39
|
+
const actions = TickerCore.computeColumnActions(sourceChars, targetChars, supported.value);
|
|
40
|
+
let ci = 0, ti = 0;
|
|
41
|
+
const result = [];
|
|
42
|
+
const validCols = currentCols.filter((c) => c.currentWidth > 0);
|
|
43
|
+
for (const action of actions) {
|
|
44
|
+
if (action === TickerCore.ACTION_INSERT) {
|
|
45
|
+
result.push(TickerCore.setTarget(TickerCore.createColumn(), targetChars[ti++], lists.value, props.direction));
|
|
46
|
+
} else if (action === TickerCore.ACTION_SAME) {
|
|
47
|
+
const existing = validCols[ci++] || TickerCore.createColumn();
|
|
48
|
+
result.push(TickerCore.setTarget(existing, targetChars[ti++], lists.value, props.direction));
|
|
49
|
+
} else {
|
|
50
|
+
const existing = validCols[ci++] || TickerCore.createColumn();
|
|
51
|
+
result.push(TickerCore.setTarget(existing, TickerCore.EMPTY_CHAR, lists.value, props.direction));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
columns.value = result;
|
|
55
|
+
progress.value = 0;
|
|
56
|
+
const start = performance.now();
|
|
57
|
+
const dur = props.duration;
|
|
58
|
+
const easeFn = TickerCore.easingFunctions[props.easing] || TickerCore.easingFunctions.linear;
|
|
59
|
+
let lastUpdate = 0;
|
|
60
|
+
const animate = (now) => {
|
|
61
|
+
const linearP = Math.min((now - start) / dur, 1);
|
|
62
|
+
const p = easeFn(linearP);
|
|
63
|
+
const shouldUpdate = now - lastUpdate >= 16 || linearP >= 1;
|
|
64
|
+
if (shouldUpdate) {
|
|
65
|
+
lastUpdate = now;
|
|
66
|
+
progress.value = p;
|
|
67
|
+
}
|
|
68
|
+
if (linearP < 1) {
|
|
69
|
+
animId = requestAnimationFrame(animate);
|
|
70
|
+
} else {
|
|
71
|
+
const final = columns.value.map((c) => TickerCore.applyProgress(c, 1).col).filter((c) => c.currentWidth > 0);
|
|
72
|
+
columns.value = final;
|
|
73
|
+
progress.value = 1;
|
|
74
|
+
animId = void 0;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
animId = requestAnimationFrame(animate);
|
|
78
|
+
}, { immediate: true });
|
|
79
|
+
vue.onUnmounted(() => {
|
|
80
|
+
if (animId) cancelAnimationFrame(animId);
|
|
81
|
+
});
|
|
82
|
+
const renderedColumns = vue.computed(() => {
|
|
83
|
+
return columns.value.map((col) => {
|
|
84
|
+
const { charIdx, delta } = TickerCore.applyProgress(col, progress.value);
|
|
85
|
+
const width = col.sourceWidth + (col.targetWidth - col.sourceWidth) * progress.value;
|
|
86
|
+
if (width <= 0) return { width, chars: [] };
|
|
87
|
+
const chars = [];
|
|
88
|
+
const list = col.charList || [];
|
|
89
|
+
const deltaEm = delta * charHeight;
|
|
90
|
+
const add = (idx, offsetMod, keyPrefix) => {
|
|
91
|
+
if (idx >= 0 && idx < list.length) {
|
|
92
|
+
chars.push({
|
|
93
|
+
key: `${keyPrefix}-${idx}`,
|
|
94
|
+
char: list[idx],
|
|
95
|
+
offset: deltaEm + offsetMod
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
add(charIdx, 0, "c");
|
|
100
|
+
add(charIdx + 1, -charHeight, "n");
|
|
101
|
+
add(charIdx - 1, charHeight, "p");
|
|
102
|
+
return { width, chars };
|
|
103
|
+
}).filter((c) => c.width > 0);
|
|
104
|
+
});
|
|
105
|
+
return (_ctx, _cache) => {
|
|
106
|
+
return vue.openBlock(), vue.createElementBlock("div", {
|
|
107
|
+
class: vue.normalizeClass(["ticker", __props.className])
|
|
108
|
+
}, [
|
|
109
|
+
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(renderedColumns.value, (col, i) => {
|
|
110
|
+
return vue.openBlock(), vue.createElementBlock("div", {
|
|
111
|
+
key: i,
|
|
112
|
+
class: "ticker-column",
|
|
113
|
+
style: vue.normalizeStyle({ width: `${col.width}em` })
|
|
114
|
+
}, [
|
|
115
|
+
(vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(col.chars, (charObj) => {
|
|
116
|
+
return vue.openBlock(), vue.createElementBlock("div", {
|
|
117
|
+
key: charObj.key,
|
|
118
|
+
class: "ticker-char",
|
|
119
|
+
style: vue.normalizeStyle({ transform: `translateY(${charObj.offset}em)` })
|
|
120
|
+
}, vue.toDisplayString(charObj.char === vue.unref(TickerCore.EMPTY_CHAR) ? " " : charObj.char), 5);
|
|
121
|
+
}), 128))
|
|
122
|
+
], 4);
|
|
123
|
+
}), 128))
|
|
124
|
+
], 2);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const _export_sfc = (sfc, props) => {
|
|
129
|
+
const target = sfc.__vccOpts || sfc;
|
|
130
|
+
for (const [key, val] of props) {
|
|
131
|
+
target[key] = val;
|
|
132
|
+
}
|
|
133
|
+
return target;
|
|
134
|
+
};
|
|
135
|
+
const Ticker = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d7db53e5"]]);
|
|
136
|
+
exports.ACTION_DELETE = TickerCore.ACTION_DELETE;
|
|
137
|
+
exports.ACTION_INSERT = TickerCore.ACTION_INSERT;
|
|
138
|
+
exports.ACTION_SAME = TickerCore.ACTION_SAME;
|
|
139
|
+
exports.EMPTY_CHAR = TickerCore.EMPTY_CHAR;
|
|
140
|
+
exports.TickerCharacterList = TickerCore.TickerCharacterList;
|
|
141
|
+
exports.TickerUtils = TickerCore.TickerUtils;
|
|
142
|
+
exports.applyProgress = TickerCore.applyProgress;
|
|
143
|
+
exports.computeColumnActions = TickerCore.computeColumnActions;
|
|
144
|
+
exports.createColumn = TickerCore.createColumn;
|
|
145
|
+
exports.easingFunctions = TickerCore.easingFunctions;
|
|
146
|
+
exports.setTarget = TickerCore.setTarget;
|
|
147
|
+
exports.Ticker = Ticker;
|
|
148
|
+
//# sourceMappingURL=vue.cjs.map
|