@sarthak03dot/romantic-animations 1.0.2 → 1.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/README.md +310 -23
- package/dist/icon.png +0 -0
- package/dist/romantic-animations.es.js +727 -0
- package/dist/romantic-animations.es.js.map +1 -0
- package/dist/romantic-animations.umd.js +10 -0
- package/dist/romantic-animations.umd.js.map +1 -0
- package/package.json +35 -11
- package/src/animations/butterfly.js +92 -0
- package/src/animations/confetti.js +92 -0
- package/src/animations/fireworks.js +112 -0
- package/src/animations/floatingHearts.js +86 -32
- package/src/animations/floatingOrbs.js +76 -0
- package/src/animations/heartBurst.js +106 -49
- package/src/animations/heartTrail.js +82 -38
- package/src/animations/loveRain.js +71 -0
- package/src/animations/magicDust.js +87 -0
- package/src/animations/shootingStars.js +82 -0
- package/src/animations/sparkles.js +93 -0
- package/src/animations/starField.js +100 -0
- package/src/core/engine.js +76 -12
- package/src/index.js +218 -13
- package/gitignore +0 -23
- package/vite.config.js +0 -13
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
density: 0.18,
|
|
5
|
+
colors: ['#ff6b8a','#ff4d6d','#ffd6ff','#e7c6ff','#c77dff','#48cae4','#ffe66d','#06d6a0'],
|
|
6
|
+
minSize: 6,
|
|
7
|
+
maxSize: 14,
|
|
8
|
+
minSpeed: 1.5,
|
|
9
|
+
maxSpeed: 4,
|
|
10
|
+
gravity: 0.06,
|
|
11
|
+
drag: 0.99,
|
|
12
|
+
shapes: ['rect', 'circle', 'ribbon'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function confetti(canvas, userOptions = {}) {
|
|
16
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
17
|
+
const ctx = canvas.getContext('2d');
|
|
18
|
+
const pieces = [];
|
|
19
|
+
let running = true;
|
|
20
|
+
|
|
21
|
+
function createPiece() {
|
|
22
|
+
const size = opts.minSize + Math.random() * (opts.maxSize - opts.minSize);
|
|
23
|
+
const speed = opts.minSpeed + Math.random() * (opts.maxSpeed - opts.minSpeed);
|
|
24
|
+
return {
|
|
25
|
+
x: Math.random() * canvas.width,
|
|
26
|
+
y: -size * 2,
|
|
27
|
+
w: size,
|
|
28
|
+
h: size * (0.4 + Math.random() * 0.8),
|
|
29
|
+
vx: (Math.random() - 0.5) * 3,
|
|
30
|
+
vy: speed,
|
|
31
|
+
angle: Math.random() * Math.PI * 2,
|
|
32
|
+
spin: (Math.random() - 0.5) * 0.15,
|
|
33
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
34
|
+
alpha: 0.8 + Math.random() * 0.2,
|
|
35
|
+
shape: opts.shapes[Math.floor(Math.random() * opts.shapes.length)],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function drawPiece(p) {
|
|
40
|
+
ctx.save();
|
|
41
|
+
ctx.globalAlpha = p.alpha;
|
|
42
|
+
ctx.fillStyle = p.color;
|
|
43
|
+
ctx.strokeStyle = p.color;
|
|
44
|
+
ctx.translate(p.x, p.y);
|
|
45
|
+
ctx.rotate(p.angle);
|
|
46
|
+
|
|
47
|
+
if (p.shape === 'circle') {
|
|
48
|
+
ctx.beginPath();
|
|
49
|
+
ctx.ellipse(0, 0, p.w / 2, p.h / 2, 0, 0, Math.PI * 2);
|
|
50
|
+
ctx.fill();
|
|
51
|
+
} else if (p.shape === 'ribbon') {
|
|
52
|
+
ctx.beginPath();
|
|
53
|
+
ctx.moveTo(-p.w / 2, 0);
|
|
54
|
+
ctx.quadraticCurveTo(0, -p.h, p.w / 2, 0);
|
|
55
|
+
ctx.quadraticCurveTo(0, p.h, -p.w / 2, 0);
|
|
56
|
+
ctx.fill();
|
|
57
|
+
} else {
|
|
58
|
+
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
|
59
|
+
}
|
|
60
|
+
ctx.restore();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function animate() {
|
|
64
|
+
if (!running) return;
|
|
65
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
66
|
+
|
|
67
|
+
if (Math.random() < opts.density) pieces.push(createPiece());
|
|
68
|
+
|
|
69
|
+
for (let i = pieces.length - 1; i >= 0; i--) {
|
|
70
|
+
const p = pieces[i];
|
|
71
|
+
p.vy += opts.gravity;
|
|
72
|
+
p.vx *= opts.drag;
|
|
73
|
+
p.vy *= opts.drag;
|
|
74
|
+
p.x += p.vx;
|
|
75
|
+
p.y += p.vy;
|
|
76
|
+
p.angle += p.spin;
|
|
77
|
+
|
|
78
|
+
drawPiece(p);
|
|
79
|
+
|
|
80
|
+
if (p.y > canvas.height + 20) pieces.splice(i, 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
requestAnimationFrame(animate);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
animate();
|
|
87
|
+
|
|
88
|
+
return function stop() {
|
|
89
|
+
running = false;
|
|
90
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
interval: 1200, // ms between auto-launches
|
|
5
|
+
trailLength: 28,
|
|
6
|
+
particleCount: 80,
|
|
7
|
+
colors: ['#ff6b8a','#ff4d6d','#ffd6ff','#e7c6ff','#ffe66d','#06d6a0','#48cae4','#ffffff'],
|
|
8
|
+
gravity: 0.09,
|
|
9
|
+
decay: 0.014,
|
|
10
|
+
glow: true,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function fireworks(canvas, userOptions = {}) {
|
|
14
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
15
|
+
const ctx = canvas.getContext('2d');
|
|
16
|
+
let running = true;
|
|
17
|
+
|
|
18
|
+
const rockets = []; // { x, y, vx, vy, trail[], color }
|
|
19
|
+
const particles = []; // burst particles
|
|
20
|
+
|
|
21
|
+
function launchRocket() {
|
|
22
|
+
const x = canvas.width * (0.2 + Math.random() * 0.6);
|
|
23
|
+
const targetY = canvas.height * (0.1 + Math.random() * 0.4);
|
|
24
|
+
const dy = targetY - canvas.height;
|
|
25
|
+
const speed = 8 + Math.random() * 5;
|
|
26
|
+
const color = opts.colors[Math.floor(Math.random() * opts.colors.length)];
|
|
27
|
+
rockets.push({ x, y: canvas.height, vy: -Math.abs(speed), targetY, trail: [], color });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function burst(x, y, color) {
|
|
31
|
+
for (let i = 0; i < opts.particleCount; i++) {
|
|
32
|
+
const angle = Math.random() * Math.PI * 2;
|
|
33
|
+
const speed = 1 + Math.random() * 5;
|
|
34
|
+
particles.push({
|
|
35
|
+
x, y,
|
|
36
|
+
vx: Math.cos(angle) * speed,
|
|
37
|
+
vy: Math.sin(angle) * speed,
|
|
38
|
+
alpha: 1,
|
|
39
|
+
decay: opts.decay * (0.7 + Math.random() * 0.6),
|
|
40
|
+
size: 2 + Math.random() * 3,
|
|
41
|
+
color,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Auto-launch interval
|
|
47
|
+
const iv = setInterval(() => { if (running) launchRocket(); }, opts.interval);
|
|
48
|
+
launchRocket(); // fire one immediately
|
|
49
|
+
|
|
50
|
+
function animate() {
|
|
51
|
+
if (!running) return;
|
|
52
|
+
|
|
53
|
+
// Clear canvas instead of using a solid fill to keep background transparent
|
|
54
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
55
|
+
|
|
56
|
+
// Draw rockets
|
|
57
|
+
for (let i = rockets.length - 1; i >= 0; i--) {
|
|
58
|
+
const r = rockets[i];
|
|
59
|
+
r.y += r.vy;
|
|
60
|
+
r.trail.push({ x: r.x, y: r.y });
|
|
61
|
+
if (r.trail.length > opts.trailLength) r.trail.shift();
|
|
62
|
+
|
|
63
|
+
// Draw trail
|
|
64
|
+
for (let t = 0; t < r.trail.length; t++) {
|
|
65
|
+
const alpha = (t / r.trail.length) * 0.8;
|
|
66
|
+
ctx.save();
|
|
67
|
+
ctx.globalAlpha = alpha;
|
|
68
|
+
if (opts.glow) { ctx.shadowColor = r.color; ctx.shadowBlur = 6; }
|
|
69
|
+
ctx.fillStyle = r.color;
|
|
70
|
+
ctx.beginPath();
|
|
71
|
+
ctx.arc(r.trail[t].x, r.trail[t].y, 2.5 * (t / r.trail.length), 0, Math.PI * 2);
|
|
72
|
+
ctx.fill();
|
|
73
|
+
ctx.restore();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (r.y <= r.targetY) {
|
|
77
|
+
burst(r.x, r.y, r.color);
|
|
78
|
+
rockets.splice(i, 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Draw burst particles
|
|
83
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
84
|
+
const p = particles[i];
|
|
85
|
+
p.x += p.vx;
|
|
86
|
+
p.y += p.vy;
|
|
87
|
+
p.vy += opts.gravity;
|
|
88
|
+
p.alpha -= p.decay;
|
|
89
|
+
|
|
90
|
+
ctx.save();
|
|
91
|
+
ctx.globalAlpha = Math.max(0, p.alpha);
|
|
92
|
+
if (opts.glow) { ctx.shadowColor = p.color; ctx.shadowBlur = p.size * 2; }
|
|
93
|
+
ctx.fillStyle = p.color;
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
96
|
+
ctx.fill();
|
|
97
|
+
ctx.restore();
|
|
98
|
+
|
|
99
|
+
if (p.alpha <= 0) particles.splice(i, 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
requestAnimationFrame(animate);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
animate();
|
|
106
|
+
|
|
107
|
+
return function stop() {
|
|
108
|
+
running = false;
|
|
109
|
+
clearInterval(iv);
|
|
110
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -1,35 +1,89 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
count: 0.12, // hearts spawned per frame (probability)
|
|
5
|
+
minSize: 14,
|
|
6
|
+
maxSize: 32,
|
|
7
|
+
minSpeed: 0.8,
|
|
8
|
+
maxSpeed: 2.4,
|
|
9
|
+
colors: ['#ff6b8a', '#ff4d6d', '#ff85a1', '#ffc2d1', '#ff0a54', '#ff477e'],
|
|
10
|
+
wobble: true, // horizontal sine drift
|
|
11
|
+
glow: true,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Draw a proper heart shape centred at (cx, cy) with given radius.
|
|
16
|
+
*/
|
|
17
|
+
function drawHeartShape(ctx, cx, cy, r, color, alpha = 1, glow = false) {
|
|
18
|
+
ctx.save();
|
|
19
|
+
ctx.globalAlpha = alpha;
|
|
20
|
+
if (glow) {
|
|
21
|
+
ctx.shadowColor = color;
|
|
22
|
+
ctx.shadowBlur = r * 1.2;
|
|
23
|
+
}
|
|
24
|
+
ctx.fillStyle = color;
|
|
25
|
+
ctx.beginPath();
|
|
26
|
+
ctx.moveTo(cx, cy + r * 0.3);
|
|
27
|
+
// left lobe
|
|
28
|
+
ctx.bezierCurveTo(cx - r * 1.1, cy - r * 0.5, cx - r * 1.6, cy + r * 0.5, cx, cy + r * 1.4);
|
|
29
|
+
// right lobe
|
|
30
|
+
ctx.bezierCurveTo(cx + r * 1.6, cy + r * 0.5, cx + r * 1.1, cy - r * 0.5, cx, cy + r * 0.3);
|
|
31
|
+
ctx.fill();
|
|
32
|
+
ctx.restore();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function floatingHearts(canvas, userOptions = {}) {
|
|
36
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
37
|
+
const ctx = canvas.getContext('2d');
|
|
38
|
+
const hearts = [];
|
|
39
|
+
let running = true;
|
|
40
|
+
let frame = 0;
|
|
41
|
+
|
|
42
|
+
function createHeart() {
|
|
43
|
+
const size = opts.minSize + Math.random() * (opts.maxSize - opts.minSize);
|
|
44
|
+
return {
|
|
45
|
+
x: Math.random() * canvas.width,
|
|
46
|
+
y: canvas.height + size * 2,
|
|
47
|
+
size,
|
|
48
|
+
speed: opts.minSpeed + Math.random() * (opts.maxSpeed - opts.minSpeed),
|
|
49
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
50
|
+
alpha: 0.7 + Math.random() * 0.3,
|
|
51
|
+
wobbleOffset: Math.random() * Math.PI * 2,
|
|
52
|
+
wobbleSpeed: 0.02 + Math.random() * 0.03,
|
|
53
|
+
wobbleAmount: 0.5 + Math.random() * 1.5,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function animate() {
|
|
58
|
+
if (!running) return;
|
|
59
|
+
frame++;
|
|
60
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
61
|
+
|
|
62
|
+
if (Math.random() < opts.count) hearts.push(createHeart());
|
|
63
|
+
|
|
64
|
+
for (let i = hearts.length - 1; i >= 0; i--) {
|
|
65
|
+
const h = hearts[i];
|
|
66
|
+
h.y -= h.speed;
|
|
67
|
+
h.wobbleOffset += h.wobbleSpeed;
|
|
68
|
+
const xOffset = opts.wobble ? Math.sin(h.wobbleOffset) * h.wobbleAmount * h.size * 0.5 : 0;
|
|
69
|
+
|
|
70
|
+
// Fade out near top
|
|
71
|
+
const fadeAlpha = Math.min(h.alpha, h.y / (canvas.height * 0.2));
|
|
72
|
+
if (fadeAlpha <= 0 || h.y < -h.size * 3) {
|
|
73
|
+
hearts.splice(i, 1);
|
|
74
|
+
continue;
|
|
29
75
|
}
|
|
30
|
-
|
|
76
|
+
|
|
77
|
+
drawHeartShape(ctx, h.x + xOffset, h.y, h.size, h.color, Math.max(0, fadeAlpha), opts.glow);
|
|
31
78
|
}
|
|
32
|
-
|
|
33
|
-
animate
|
|
79
|
+
|
|
80
|
+
requestAnimationFrame(animate);
|
|
34
81
|
}
|
|
35
|
-
|
|
82
|
+
|
|
83
|
+
animate();
|
|
84
|
+
|
|
85
|
+
return function stop() {
|
|
86
|
+
running = false;
|
|
87
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
orbCount: 15,
|
|
5
|
+
minSize: 50,
|
|
6
|
+
maxSize: 150,
|
|
7
|
+
colors: ['#ff4d6d', '#c77dff', '#48cae4', '#ffe66d'],
|
|
8
|
+
speed: 0.5,
|
|
9
|
+
glow: true,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function floatingOrbs(canvas, userOptions = {}) {
|
|
13
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
14
|
+
const ctx = canvas.getContext('2d');
|
|
15
|
+
const orbs = [];
|
|
16
|
+
let running = true;
|
|
17
|
+
|
|
18
|
+
function createOrb() {
|
|
19
|
+
const size = opts.minSize + Math.random() * (opts.maxSize - opts.minSize);
|
|
20
|
+
return {
|
|
21
|
+
x: Math.random() * canvas.width,
|
|
22
|
+
y: Math.random() * canvas.height,
|
|
23
|
+
size,
|
|
24
|
+
vx: (Math.random() - 0.5) * opts.speed,
|
|
25
|
+
vy: (Math.random() - 0.5) * opts.speed,
|
|
26
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
27
|
+
alpha: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < opts.orbCount; i++) {
|
|
32
|
+
const orb = createOrb();
|
|
33
|
+
orb.alpha = Math.random() * 0.5 + 0.1;
|
|
34
|
+
orbs.push(orb);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function animate() {
|
|
38
|
+
if (!running) return;
|
|
39
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < orbs.length; i++) {
|
|
42
|
+
const b = orbs[i];
|
|
43
|
+
b.x += b.vx;
|
|
44
|
+
b.y += b.vy;
|
|
45
|
+
|
|
46
|
+
// Bounce off walls
|
|
47
|
+
if (b.x < -b.size) b.x = canvas.width + b.size;
|
|
48
|
+
if (b.x > canvas.width + b.size) b.x = -b.size;
|
|
49
|
+
if (b.y < -b.size) b.y = canvas.height + b.size;
|
|
50
|
+
if (b.y > canvas.height + b.size) b.y = -b.size;
|
|
51
|
+
|
|
52
|
+
ctx.save();
|
|
53
|
+
ctx.globalAlpha = b.alpha;
|
|
54
|
+
ctx.globalCompositeOperation = 'screen';
|
|
55
|
+
|
|
56
|
+
const gradient = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.size);
|
|
57
|
+
gradient.addColorStop(0, b.color);
|
|
58
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
|
59
|
+
|
|
60
|
+
ctx.fillStyle = gradient;
|
|
61
|
+
ctx.beginPath();
|
|
62
|
+
ctx.arc(b.x, b.y, b.size, 0, Math.PI * 2);
|
|
63
|
+
ctx.fill();
|
|
64
|
+
ctx.restore();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
requestAnimationFrame(animate);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
animate();
|
|
71
|
+
|
|
72
|
+
return function stop() {
|
|
73
|
+
running = false;
|
|
74
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -1,56 +1,113 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
count: 20, // hearts per burst
|
|
5
|
+
minSize: 8,
|
|
6
|
+
maxSize: 20,
|
|
7
|
+
minSpeed: 2,
|
|
8
|
+
maxSpeed: 7,
|
|
9
|
+
gravity: 0.08,
|
|
10
|
+
decay: 0.018,
|
|
11
|
+
colors: ['#ff0a54', '#ff477e', '#ff7096', '#ff85a1', '#fbb1bd', '#ff4d6d'],
|
|
12
|
+
glow: true,
|
|
13
|
+
symbols: ['heart'], // 'heart' | 'star' | 'sparkle'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function drawSymbol(ctx, type, cx, cy, r, color, alpha, glow) {
|
|
17
|
+
ctx.save();
|
|
18
|
+
ctx.globalAlpha = Math.max(0, alpha);
|
|
19
|
+
if (glow) { ctx.shadowColor = color; ctx.shadowBlur = r * 2; }
|
|
20
|
+
ctx.fillStyle = color;
|
|
21
|
+
|
|
22
|
+
if (type === 'star') {
|
|
23
|
+
// 5-point star
|
|
24
|
+
ctx.beginPath();
|
|
25
|
+
for (let i = 0; i < 5; i++) {
|
|
26
|
+
const outer = (Math.PI / 2) + (i * 2 * Math.PI) / 5;
|
|
27
|
+
const inner = outer + Math.PI / 5;
|
|
28
|
+
if (i === 0) ctx.moveTo(cx + r * Math.cos(outer), cy - r * Math.sin(outer));
|
|
29
|
+
else ctx.lineTo(cx + r * Math.cos(outer), cy - r * Math.sin(outer));
|
|
30
|
+
ctx.lineTo(cx + (r * 0.4) * Math.cos(inner), cy - (r * 0.4) * Math.sin(inner));
|
|
31
|
+
}
|
|
32
|
+
ctx.closePath();
|
|
33
|
+
ctx.fill();
|
|
34
|
+
} else if (type === 'sparkle') {
|
|
35
|
+
// 4-point sparkle
|
|
36
|
+
for (let i = 0; i < 4; i++) {
|
|
37
|
+
const a = (i * Math.PI) / 2;
|
|
38
|
+
ctx.beginPath();
|
|
39
|
+
ctx.ellipse(cx + Math.cos(a) * r * 0.5, cy + Math.sin(a) * r * 0.5, r * 0.18, r * 0.7, a, 0, Math.PI * 2);
|
|
40
|
+
ctx.fill();
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// heart
|
|
44
|
+
ctx.beginPath();
|
|
45
|
+
ctx.moveTo(cx, cy + r * 0.3);
|
|
46
|
+
ctx.bezierCurveTo(cx - r * 1.1, cy - r * 0.5, cx - r * 1.6, cy + r * 0.5, cx, cy + r * 1.4);
|
|
47
|
+
ctx.bezierCurveTo(cx + r * 1.6, cy + r * 0.5, cx + r * 1.1, cy - r * 0.5, cx, cy + r * 0.3);
|
|
48
|
+
ctx.fill();
|
|
49
|
+
}
|
|
50
|
+
ctx.restore();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function heartBurst(canvas, userOptions = {}) {
|
|
54
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
55
|
+
const ctx = canvas.getContext('2d');
|
|
56
|
+
const particles = [];
|
|
57
|
+
let running = true;
|
|
58
|
+
|
|
59
|
+
function spawnBurst(x, y) {
|
|
60
|
+
for (let i = 0; i < opts.count; i++) {
|
|
6
61
|
const angle = Math.random() * Math.PI * 2;
|
|
7
|
-
const speed =
|
|
8
|
-
|
|
9
|
-
x,
|
|
10
|
-
|
|
11
|
-
size: 10 + Math.random() * 5,
|
|
62
|
+
const speed = opts.minSpeed + Math.random() * (opts.maxSpeed - opts.minSpeed);
|
|
63
|
+
particles.push({
|
|
64
|
+
x, y,
|
|
65
|
+
size: opts.minSize + Math.random() * (opts.maxSize - opts.minSize),
|
|
12
66
|
vx: Math.cos(angle) * speed,
|
|
13
67
|
vy: Math.sin(angle) * speed,
|
|
14
68
|
alpha: 1,
|
|
15
|
-
decay: 0.
|
|
16
|
-
|
|
69
|
+
decay: opts.decay * (0.8 + Math.random() * 0.4),
|
|
70
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
71
|
+
symbol: opts.symbols[Math.floor(Math.random() * opts.symbols.length)],
|
|
72
|
+
});
|
|
17
73
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
h.y += h.vy;
|
|
45
|
-
h.alpha -= h.decay;
|
|
46
|
-
drawHeart(h);
|
|
47
|
-
if (h.alpha <= 0) burst.splice(i, 1);
|
|
48
|
-
}
|
|
49
|
-
if (burst.length === 0) bursts.splice(b, 1);
|
|
50
|
-
}
|
|
51
|
-
requestAnimationFrame(animate);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const onClick = (e) => {
|
|
77
|
+
const rect = canvas.getBoundingClientRect();
|
|
78
|
+
spawnBurst(e.clientX - rect.left, e.clientY - rect.top);
|
|
79
|
+
};
|
|
80
|
+
const onTouch = (e) => {
|
|
81
|
+
const rect = canvas.getBoundingClientRect();
|
|
82
|
+
Array.from(e.changedTouches).forEach((t) => spawnBurst(t.clientX - rect.left, t.clientY - rect.top));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
window.addEventListener('click', onClick);
|
|
86
|
+
window.addEventListener('touchend', onTouch, { passive: true });
|
|
87
|
+
|
|
88
|
+
function animate() {
|
|
89
|
+
if (!running) return;
|
|
90
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
91
|
+
|
|
92
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
93
|
+
const p = particles[i];
|
|
94
|
+
p.x += p.vx;
|
|
95
|
+
p.y += p.vy;
|
|
96
|
+
p.vy += opts.gravity; // gravity
|
|
97
|
+
p.alpha -= p.decay;
|
|
98
|
+
drawSymbol(ctx, p.symbol, p.x, p.y, p.size, p.color, p.alpha, opts.glow);
|
|
99
|
+
if (p.alpha <= 0) particles.splice(i, 1);
|
|
52
100
|
}
|
|
53
|
-
|
|
54
|
-
animate
|
|
101
|
+
|
|
102
|
+
requestAnimationFrame(animate);
|
|
55
103
|
}
|
|
56
|
-
|
|
104
|
+
|
|
105
|
+
animate();
|
|
106
|
+
|
|
107
|
+
return function stop() {
|
|
108
|
+
running = false;
|
|
109
|
+
window.removeEventListener('click', onClick);
|
|
110
|
+
window.removeEventListener('touchend', onTouch);
|
|
111
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
112
|
+
};
|
|
113
|
+
}
|