Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/animations/animations.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ThemeIcon } from '../../../common/themables.js';
7
import * as dom from '../../dom.js';
8
9
export const enum ClickAnimation {
10
Confetti = 1,
11
FloatingIcons = 2,
12
PulseWave = 3,
13
RadiantLines = 4,
14
}
15
16
const confettiColors = [
17
'#007acc',
18
'#005a9e',
19
'#0098ff',
20
'#4fc3f7',
21
'#64b5f6',
22
'#42a5f5',
23
];
24
25
let activeOverlay: HTMLElement | undefined;
26
27
/**
28
* Creates a fixed-positioned overlay centered on the given element.
29
*/
30
function createOverlay(element: HTMLElement): { overlay: HTMLElement; cx: number; cy: number } | undefined {
31
if (activeOverlay) {
32
return undefined;
33
}
34
35
const rect = element.getBoundingClientRect();
36
const ownerDocument = dom.getWindow(element).document;
37
38
const overlay = dom.$('.animation-overlay');
39
overlay.style.position = 'fixed';
40
overlay.style.left = `${rect.left}px`;
41
overlay.style.top = `${rect.top}px`;
42
overlay.style.width = `${rect.width}px`;
43
overlay.style.height = `${rect.height}px`;
44
overlay.style.pointerEvents = 'none';
45
overlay.style.overflow = 'visible';
46
overlay.style.zIndex = '10000';
47
48
ownerDocument.body.appendChild(overlay);
49
activeOverlay = overlay;
50
51
return { overlay, cx: rect.width / 2, cy: rect.height / 2 };
52
}
53
54
/**
55
* Cleans up the overlay after specified period.
56
*/
57
function cleanupOverlay(duration: number) {
58
setTimeout(() => {
59
if (activeOverlay) {
60
activeOverlay.remove();
61
activeOverlay = undefined;
62
}
63
}, duration);
64
}
65
66
/**
67
* Bounce the element with a given scale and optional rotation.
68
*/
69
export function bounceElement(element: HTMLElement, opts: { scale?: number[]; rotate?: number[]; translateY?: number[]; duration?: number }) {
70
const frames: Keyframe[] = [];
71
72
const steps = Math.max(opts.scale?.length ?? 0, opts.rotate?.length ?? 0, opts.translateY?.length ?? 0);
73
if (steps === 0) {
74
return;
75
}
76
77
for (let i = 0; i < steps; i++) {
78
const frame: Keyframe = { offset: steps === 1 ? 1 : i / (steps - 1) };
79
let transformParts = '';
80
81
const scale = opts.scale?.[i];
82
if (scale !== undefined) {
83
transformParts += `scale(${scale})`;
84
}
85
86
const rotate = opts.rotate?.[i];
87
if (rotate !== undefined) {
88
transformParts += ` rotate(${rotate}deg)`;
89
}
90
91
const translateY = opts.translateY?.[i];
92
if (translateY !== undefined) {
93
transformParts += ` translateY(${translateY}px)`;
94
}
95
96
if (transformParts) {
97
frame.transform = transformParts.trim();
98
}
99
frames.push(frame);
100
}
101
102
element.animate(frames, {
103
duration: opts.duration ?? 350,
104
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
105
fill: 'forwards',
106
});
107
}
108
109
/**
110
* Confetti: small particles burst outward in a circle from the element center,
111
* with an expanding ring.
112
*/
113
export function triggerConfettiAnimation(element: HTMLElement) {
114
const result = createOverlay(element);
115
if (!result) {
116
return;
117
}
118
119
const { overlay, cx, cy } = result;
120
const rect = element.getBoundingClientRect();
121
122
// Element bounce
123
bounceElement(element, {
124
scale: [1, 1.3, 1],
125
rotate: [0, -10, 10, 0],
126
duration: 350,
127
});
128
129
// Confetti particles
130
const particleCount = 10;
131
for (let i = 0; i < particleCount; i++) {
132
const size = 3 + (i % 3) * 1.5;
133
const angle = (i * 36 * Math.PI) / 180;
134
const distance = 35;
135
const particleOpacity = 0.6 + (i % 4) * 0.1;
136
137
const part = dom.$('.animation-particle');
138
part.style.position = 'absolute';
139
part.style.width = `${size}px`;
140
part.style.height = `${size}px`;
141
part.style.borderRadius = '50%';
142
part.style.backgroundColor = confettiColors[i % confettiColors.length];
143
part.style.left = `${cx - size / 2}px`;
144
part.style.top = `${cy - size / 2}px`;
145
overlay.appendChild(part);
146
147
const tx = Math.cos(angle) * distance;
148
const ty = Math.sin(angle) * distance;
149
150
part.animate([
151
{ opacity: 0, transform: 'scale(0) translate(0, 0)' },
152
{ opacity: particleOpacity, transform: `scale(1) translate(${tx * 0.5}px, ${ty * 0.5}px)`, offset: 0.3 },
153
{ opacity: particleOpacity, transform: `scale(1) translate(${tx}px, ${ty}px)`, offset: 0.7 },
154
{ opacity: 0, transform: `scale(0) translate(${tx}px, ${ty}px)` },
155
], {
156
duration: 1100,
157
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
158
fill: 'forwards',
159
});
160
}
161
162
// Expanding ring
163
const ring = dom.$('.animation-particle');
164
ring.style.position = 'absolute';
165
ring.style.left = '0';
166
ring.style.top = '0';
167
ring.style.width = `${rect.width}px`;
168
ring.style.height = `${rect.height}px`;
169
ring.style.borderRadius = '50%';
170
ring.style.border = '2px solid var(--vscode-focusBorder, #007acc)';
171
ring.style.boxSizing = 'border-box';
172
overlay.appendChild(ring);
173
174
ring.animate([
175
{ transform: 'scale(1)', opacity: 1 },
176
{ transform: 'scale(2)', opacity: 0 },
177
], {
178
duration: 800,
179
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
180
fill: 'forwards',
181
});
182
183
cleanupOverlay(2000);
184
}
185
186
/**
187
* Floating Icons: small icons float upward from the element.
188
*/
189
export function triggerFloatingIconsAnimation(element: HTMLElement, icon: ThemeIcon) {
190
const result = createOverlay(element);
191
if (!result) {
192
return;
193
}
194
195
const { overlay, cx, cy } = result;
196
const rect = element.getBoundingClientRect();
197
198
// Element bounce upward
199
bounceElement(element, {
200
translateY: [0, -6, 0],
201
duration: 350,
202
});
203
204
// Floating icons
205
const iconCount = 6;
206
for (let i = 0; i < iconCount; i++) {
207
const size = 12 + (i % 3) * 2;
208
const iconEl = dom.$('.animation-particle');
209
iconEl.style.position = 'absolute';
210
iconEl.style.left = `${cx}px`;
211
iconEl.style.top = `${cy}px`;
212
iconEl.style.fontSize = `${size}px`;
213
iconEl.style.lineHeight = '1';
214
iconEl.style.color = 'var(--vscode-focusBorder, #007acc)';
215
iconEl.classList.add(...ThemeIcon.asClassNameArray(icon));
216
overlay.appendChild(iconEl);
217
218
const driftX = (Math.random() - 0.5) * 50;
219
const floatY = -50 - (i % 3) * 10;
220
const rotate1 = (Math.random() - 0.5) * 20;
221
const rotate2 = (Math.random() - 0.5) * 40;
222
223
iconEl.animate([
224
{ opacity: 0, transform: `translate(-50%, -50%) scale(0) rotate(${rotate1}deg)` },
225
{ opacity: 1, transform: `translate(calc(-50% + ${driftX * 0.3}px), calc(-50% + ${floatY * 0.3}px)) scale(1) rotate(${(rotate1 + rotate2) * 0.3}deg)`, offset: 0.3 },
226
{ opacity: 1, transform: `translate(calc(-50% + ${driftX * 0.7}px), calc(-50% + ${floatY * 0.7}px)) scale(1) rotate(${(rotate1 + rotate2) * 0.7}deg)`, offset: 0.7 },
227
{ opacity: 0, transform: `translate(calc(-50% + ${driftX}px), calc(-50% + ${floatY}px)) scale(0.8) rotate(${rotate2}deg)` },
228
], {
229
duration: 800 + (i % 3) * 200,
230
delay: i * 80,
231
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
232
fill: 'forwards',
233
});
234
}
235
236
// Expanding ring
237
const ring = dom.$('.animation-particle');
238
ring.style.position = 'absolute';
239
ring.style.left = '0';
240
ring.style.top = '0';
241
ring.style.width = `${rect.width}px`;
242
ring.style.height = `${rect.height}px`;
243
ring.style.borderRadius = '50%';
244
ring.style.border = '2px solid var(--vscode-focusBorder, #007acc)';
245
ring.style.boxSizing = 'border-box';
246
overlay.appendChild(ring);
247
248
ring.animate([
249
{ transform: 'scale(1)', opacity: 1 },
250
{ transform: 'scale(2)', opacity: 0 },
251
], {
252
duration: 500,
253
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
254
fill: 'forwards',
255
});
256
257
cleanupOverlay(2000);
258
}
259
260
/**
261
* Pulse Wave: expanding rings and sparkle dots radiate from the element center.
262
*/
263
export function triggerPulseWaveAnimation(element: HTMLElement) {
264
const result = createOverlay(element);
265
if (!result) {
266
return;
267
}
268
269
const { overlay, cx, cy } = result;
270
const rect = element.getBoundingClientRect();
271
272
// Element bounce with slight rotation
273
bounceElement(element, {
274
scale: [1, 1.1, 1],
275
rotate: [0, -12, 0],
276
duration: 400,
277
});
278
279
// Expanding rings
280
for (let i = 0; i < 2; i++) {
281
const ring = dom.$('.animation-particle');
282
ring.style.position = 'absolute';
283
ring.style.left = '0';
284
ring.style.top = '0';
285
ring.style.width = `${rect.width}px`;
286
ring.style.height = `${rect.height}px`;
287
ring.style.borderRadius = '50%';
288
ring.style.border = '2px solid var(--vscode-focusBorder, #007acc)';
289
ring.style.boxSizing = 'border-box';
290
overlay.appendChild(ring);
291
292
ring.animate([
293
{ transform: 'scale(0.8)', opacity: 0 },
294
{ transform: 'scale(0.8)', opacity: 0.6, offset: 0.01 },
295
{ transform: 'scale(2.5)', opacity: 0 },
296
], {
297
duration: 800,
298
delay: i * 150,
299
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
300
fill: 'forwards',
301
});
302
}
303
304
// Sparkle dots
305
for (let i = 0; i < 6; i++) {
306
const angle = (i * 60 * Math.PI) / 180;
307
const distance = 30 + (i % 2) * 10;
308
const size = 3.5;
309
310
const dot = dom.$('.animation-particle');
311
dot.style.position = 'absolute';
312
dot.style.width = `${size}px`;
313
dot.style.height = `${size}px`;
314
dot.style.borderRadius = '50%';
315
dot.style.backgroundColor = '#0098ff';
316
dot.style.left = `${cx - size / 2}px`;
317
dot.style.top = `${cy - size / 2}px`;
318
overlay.appendChild(dot);
319
320
const tx = Math.cos(angle) * distance;
321
const ty = Math.sin(angle) * distance;
322
323
dot.animate([
324
{ opacity: 0, transform: 'scale(0) translate(0, 0)' },
325
{ opacity: 1, transform: `scale(1) translate(${tx}px, ${ty}px)`, offset: 0.5 },
326
{ opacity: 0, transform: `scale(0) translate(${tx}px, ${ty}px)` },
327
], {
328
duration: 600,
329
delay: 100 + i * 50,
330
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
331
fill: 'forwards',
332
});
333
}
334
335
// Background glow
336
const glow = dom.$('.animation-particle');
337
glow.style.position = 'absolute';
338
glow.style.left = '0';
339
glow.style.top = '0';
340
glow.style.width = `${rect.width}px`;
341
glow.style.height = `${rect.height}px`;
342
glow.style.borderRadius = '50%';
343
glow.style.backgroundColor = 'var(--vscode-focusBorder, #007acc)';
344
overlay.appendChild(glow);
345
346
glow.animate([
347
{ transform: 'scale(0.9)', opacity: 0 },
348
{ transform: 'scale(0.9)', opacity: 0.5, offset: 0.01 },
349
{ transform: 'scale(1.5)', opacity: 0 },
350
], {
351
duration: 500,
352
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
353
fill: 'forwards',
354
});
355
356
cleanupOverlay(2000);
357
}
358
359
/**
360
* Radiant Lines: lines and dots emanate outward from the element center.
361
*/
362
export function triggerRadiantLinesAnimation(element: HTMLElement) {
363
const result = createOverlay(element);
364
if (!result) {
365
return;
366
}
367
368
const { overlay, cx, cy } = result;
369
370
// Element scale bounce
371
bounceElement(element, {
372
scale: [1, 1.15, 1],
373
duration: 350,
374
});
375
376
// Dots at offset angles
377
for (let i = 0; i < 8; i++) {
378
const size = 3;
379
const dotOpacity = 0.7;
380
const angle = ((i * 45 + 22.5) * Math.PI) / 180;
381
const startDistance = 14;
382
const endDistance = 30;
383
384
const dot = dom.$('.animation-particle');
385
dot.style.position = 'absolute';
386
dot.style.width = `${size}px`;
387
dot.style.height = `${size}px`;
388
dot.style.borderRadius = '50%';
389
dot.style.backgroundColor = 'var(--vscode-editor-foreground, #ffffff)';
390
dot.style.left = `${cx - size / 2}px`;
391
dot.style.top = `${cy - size / 2}px`;
392
overlay.appendChild(dot);
393
394
const startX = Math.cos(angle) * startDistance;
395
const startY = Math.sin(angle) * startDistance;
396
const endX = Math.cos(angle) * endDistance;
397
const endY = Math.sin(angle) * endDistance;
398
399
dot.animate([
400
{ opacity: 0, transform: `scale(0) translate(${startX}px, ${startY}px)` },
401
{ opacity: dotOpacity, transform: `scale(1.2) translate(${(startX + endX) / 2}px, ${(startY + endY) / 2}px)`, offset: 0.25 },
402
{ opacity: dotOpacity, transform: `scale(1) translate(${endX * 0.8}px, ${endY * 0.8}px)`, offset: 0.5 },
403
{ opacity: dotOpacity * 0.5, transform: `scale(1) translate(${endX}px, ${endY}px)`, offset: 0.75 },
404
{ opacity: 0, transform: `scale(0.5) translate(${endX}px, ${endY}px)` },
405
], {
406
duration: 1100,
407
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
408
fill: 'forwards',
409
});
410
}
411
412
// Radiant lines
413
for (let i = 0; i < 8; i++) {
414
const angleDeg = i * 45;
415
416
const lineWrapper = dom.$('.animation-particle');
417
lineWrapper.style.position = 'absolute';
418
lineWrapper.style.left = `${cx}px`;
419
lineWrapper.style.top = `${cy}px`;
420
lineWrapper.style.width = '0';
421
lineWrapper.style.height = '0';
422
lineWrapper.style.transform = `rotate(${angleDeg}deg)`;
423
overlay.appendChild(lineWrapper);
424
425
const line = dom.$('.animation-particle');
426
line.style.position = 'absolute';
427
line.style.width = '2px';
428
line.style.height = '10px';
429
line.style.backgroundColor = 'var(--vscode-focusBorder, #007acc)';
430
line.style.left = '-1px';
431
line.style.top = '-22px';
432
line.style.transformOrigin = 'bottom center';
433
lineWrapper.appendChild(line);
434
435
line.animate([
436
{ transform: 'scale(1, 0)', opacity: 0.6 },
437
{ transform: 'scale(1, 1)', opacity: 0.6, offset: 0.2 },
438
{ transform: 'scale(1, 1)', opacity: 0.6, offset: 0.6 },
439
{ transform: 'scale(1, 1)', opacity: 0.6, offset: 0.8 },
440
{ transform: 'scale(0, 0.3)', opacity: 0 },
441
], {
442
duration: 1200,
443
delay: 150,
444
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
445
fill: 'forwards',
446
});
447
}
448
449
cleanupOverlay(2000);
450
}
451
452
/**
453
* Triggers the specified click animation on the element.
454
* @param element The target element to animate.
455
* @param animation The type of click animation to trigger.
456
* @param icon Optional icon for animations that require it (e.g., FloatingIcons).
457
*/
458
export function triggerClickAnimation(element: HTMLElement, animation: ClickAnimation, icon?: ThemeIcon) {
459
switch (animation) {
460
case ClickAnimation.Confetti:
461
triggerConfettiAnimation(element);
462
break;
463
case ClickAnimation.FloatingIcons:
464
if (icon) {
465
triggerFloatingIconsAnimation(element, icon);
466
}
467
break;
468
case ClickAnimation.PulseWave:
469
triggerPulseWaveAnimation(element);
470
break;
471
case ClickAnimation.RadiantLines:
472
triggerRadiantLinesAnimation(element);
473
break;
474
}
475
}
476
477