@sarthak03dot/romantic-animations 1.2.0 → 1.2.3
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 +332 -327
- package/dist/romantic-animations.es.js.map +1 -1
- package/dist/romantic-animations.umd.js.map +1 -1
- package/package.json +54 -54
- package/src/animations/butterfly.js +92 -92
- package/src/animations/confetti.js +92 -92
- package/src/animations/fireworks.js +112 -112
- package/src/animations/floatingHearts.js +89 -89
- package/src/animations/floatingOrbs.js +76 -76
- package/src/animations/heartBurst.js +113 -113
- package/src/animations/heartTrail.js +85 -85
- package/src/animations/loveRain.js +71 -71
- package/src/animations/magicDust.js +87 -87
- package/src/animations/shootingStars.js +82 -82
- package/src/animations/sparkles.js +93 -93
- package/src/animations/starField.js +100 -100
- package/src/core/engine.js +77 -77
- package/src/index.js +224 -224
|
@@ -1,89 +1,89 @@
|
|
|
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;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
drawHeartShape(ctx, h.x + xOffset, h.y, h.size, h.color, Math.max(0, fadeAlpha), opts.glow);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
requestAnimationFrame(animate);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
animate();
|
|
84
|
-
|
|
85
|
-
return function stop() {
|
|
86
|
-
running = false;
|
|
87
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
88
|
-
};
|
|
89
|
-
}
|
|
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;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
drawHeartShape(ctx, h.x + xOffset, h.y, h.size, h.color, Math.max(0, fadeAlpha), opts.glow);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
requestAnimationFrame(animate);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
animate();
|
|
84
|
+
|
|
85
|
+
return function stop() {
|
|
86
|
+
running = false;
|
|
87
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -1,76 +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
|
+
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,113 +1,113 @@
|
|
|
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++) {
|
|
61
|
-
const angle = Math.random() * Math.PI * 2;
|
|
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),
|
|
66
|
-
vx: Math.cos(angle) * speed,
|
|
67
|
-
vy: Math.sin(angle) * speed,
|
|
68
|
-
alpha: 1,
|
|
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
|
-
});
|
|
73
|
-
}
|
|
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);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
requestAnimationFrame(animate);
|
|
103
|
-
}
|
|
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
|
-
}
|
|
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++) {
|
|
61
|
+
const angle = Math.random() * Math.PI * 2;
|
|
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),
|
|
66
|
+
vx: Math.cos(angle) * speed,
|
|
67
|
+
vy: Math.sin(angle) * speed,
|
|
68
|
+
alpha: 1,
|
|
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
|
+
});
|
|
73
|
+
}
|
|
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);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
requestAnimationFrame(animate);
|
|
103
|
+
}
|
|
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
|
+
}
|