@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
|
@@ -1,41 +1,85 @@
|
|
|
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
|
-
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
minSize: 6,
|
|
5
|
+
maxSize: 16,
|
|
6
|
+
decay: 0.025,
|
|
7
|
+
colors: ['#ff6b8a', '#ff4d6d', '#ff85a1', '#ffc2d1', '#c9184a'],
|
|
8
|
+
glow: true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function drawHeart(ctx, cx, cy, r, color, alpha, glow) {
|
|
12
|
+
ctx.save();
|
|
13
|
+
ctx.globalAlpha = Math.max(0, alpha);
|
|
14
|
+
if (glow) {
|
|
15
|
+
ctx.shadowColor = color;
|
|
16
|
+
ctx.shadowBlur = r * 2;
|
|
17
|
+
}
|
|
18
|
+
ctx.fillStyle = color;
|
|
19
|
+
ctx.beginPath();
|
|
20
|
+
ctx.moveTo(cx, cy + r * 0.3);
|
|
21
|
+
ctx.bezierCurveTo(cx - r * 1.1, cy - r * 0.5, cx - r * 1.6, cy + r * 0.5, cx, cy + r * 1.4);
|
|
22
|
+
ctx.bezierCurveTo(cx + r * 1.6, cy + r * 0.5, cx + r * 1.1, cy - r * 0.5, cx, cy + r * 0.3);
|
|
23
|
+
ctx.fill();
|
|
24
|
+
ctx.restore();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function heartTrail(canvas, userOptions = {}) {
|
|
28
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
29
|
+
const ctx = canvas.getContext('2d');
|
|
30
|
+
const hearts = [];
|
|
31
|
+
let running = true;
|
|
32
|
+
|
|
33
|
+
function addHeart(x, y) {
|
|
34
|
+
hearts.push({
|
|
35
|
+
x,
|
|
36
|
+
y,
|
|
37
|
+
size: opts.minSize + Math.random() * (opts.maxSize - opts.minSize),
|
|
38
|
+
alpha: 0.9 + Math.random() * 0.1,
|
|
39
|
+
decay: opts.decay * (0.8 + Math.random() * 0.4),
|
|
40
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
41
|
+
vy: -(0.3 + Math.random() * 0.6), // drift upward
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Mouse support
|
|
46
|
+
const onMouseMove = (e) => {
|
|
47
|
+
const rect = canvas.getBoundingClientRect();
|
|
48
|
+
addHeart(e.clientX - rect.left, e.clientY - rect.top);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Touch support
|
|
52
|
+
const onTouchMove = (e) => {
|
|
53
|
+
const rect = canvas.getBoundingClientRect();
|
|
54
|
+
Array.from(e.touches).forEach((t) => {
|
|
55
|
+
addHeart(t.clientX - rect.left, t.clientY - rect.top);
|
|
27
56
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
60
|
+
window.addEventListener('touchmove', onTouchMove, { passive: true });
|
|
61
|
+
|
|
62
|
+
function animate() {
|
|
63
|
+
if (!running) return;
|
|
64
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
65
|
+
|
|
66
|
+
for (let i = hearts.length - 1; i >= 0; i--) {
|
|
67
|
+
const h = hearts[i];
|
|
68
|
+
h.y += h.vy;
|
|
69
|
+
drawHeart(ctx, h.x, h.y, h.size, h.color, h.alpha, opts.glow);
|
|
70
|
+
h.alpha -= h.decay;
|
|
71
|
+
if (h.alpha <= 0) hearts.splice(i, 1);
|
|
37
72
|
}
|
|
38
|
-
|
|
39
|
-
animate
|
|
73
|
+
|
|
74
|
+
requestAnimationFrame(animate);
|
|
40
75
|
}
|
|
41
|
-
|
|
76
|
+
|
|
77
|
+
animate();
|
|
78
|
+
|
|
79
|
+
return function stop() {
|
|
80
|
+
running = false;
|
|
81
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
82
|
+
window.removeEventListener('touchmove', onTouchMove);
|
|
83
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
density: 0.15, // probability of a new drop per frame
|
|
5
|
+
symbols: ['❤', '💕', '✨', '💖', '💗', '⭐', '×'],
|
|
6
|
+
minSize: 12,
|
|
7
|
+
maxSize: 28,
|
|
8
|
+
minSpeed: 1,
|
|
9
|
+
maxSpeed: 3.5,
|
|
10
|
+
colors: ['#ff6b8a', '#ff4d6d', '#ffc2d1', '#ff85a1', '#ff0a54', '#a2d2ff'],
|
|
11
|
+
opacity: 0.85,
|
|
12
|
+
glow: true,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function loveRain(canvas, userOptions = {}) {
|
|
16
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
17
|
+
const ctx = canvas.getContext('2d');
|
|
18
|
+
const drops = [];
|
|
19
|
+
let running = true;
|
|
20
|
+
|
|
21
|
+
function createDrop() {
|
|
22
|
+
const size = opts.minSize + Math.random() * (opts.maxSize - opts.minSize);
|
|
23
|
+
return {
|
|
24
|
+
x: Math.random() * canvas.width,
|
|
25
|
+
y: -size * 2,
|
|
26
|
+
size,
|
|
27
|
+
speed: opts.minSpeed + Math.random() * (opts.maxSpeed - opts.minSpeed),
|
|
28
|
+
symbol: opts.symbols[Math.floor(Math.random() * opts.symbols.length)],
|
|
29
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
30
|
+
alpha: 0.4 + Math.random() * 0.6,
|
|
31
|
+
angle: (Math.random() - 0.5) * 0.4, // slight tilt
|
|
32
|
+
wobble: Math.random() * Math.PI * 2,
|
|
33
|
+
wobbleSpeed: 0.02 + Math.random() * 0.02,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function animate() {
|
|
38
|
+
if (!running) return;
|
|
39
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
40
|
+
|
|
41
|
+
if (Math.random() < opts.density) drops.push(createDrop());
|
|
42
|
+
|
|
43
|
+
for (let i = drops.length - 1; i >= 0; i--) {
|
|
44
|
+
const d = drops[i];
|
|
45
|
+
d.y += d.speed;
|
|
46
|
+
d.wobble += d.wobbleSpeed;
|
|
47
|
+
const xOff = Math.sin(d.wobble) * d.size * 0.3;
|
|
48
|
+
|
|
49
|
+
ctx.save();
|
|
50
|
+
ctx.globalAlpha = d.alpha * opts.opacity;
|
|
51
|
+
ctx.font = `${d.size}px serif`;
|
|
52
|
+
ctx.fillStyle = d.color;
|
|
53
|
+
if (opts.glow) { ctx.shadowColor = d.color; ctx.shadowBlur = d.size * 0.8; }
|
|
54
|
+
ctx.translate(d.x + xOff, d.y);
|
|
55
|
+
ctx.rotate(d.angle);
|
|
56
|
+
ctx.fillText(d.symbol, 0, 0);
|
|
57
|
+
ctx.restore();
|
|
58
|
+
|
|
59
|
+
if (d.y > canvas.height + d.size * 2) drops.splice(i, 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
requestAnimationFrame(animate);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
animate();
|
|
66
|
+
|
|
67
|
+
return function stop() {
|
|
68
|
+
running = false;
|
|
69
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
particleCount: 150,
|
|
5
|
+
minSize: 1,
|
|
6
|
+
maxSize: 4,
|
|
7
|
+
colors: ['#ffd6ff', '#e7c6ff', '#c77dff', '#ffb3c1', '#ffffff'],
|
|
8
|
+
speed: 0.8,
|
|
9
|
+
glow: true,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function magicDust(canvas, userOptions = {}) {
|
|
13
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
14
|
+
const ctx = canvas.getContext('2d');
|
|
15
|
+
const dusts = [];
|
|
16
|
+
let running = true;
|
|
17
|
+
let time = 0;
|
|
18
|
+
|
|
19
|
+
function createDust() {
|
|
20
|
+
return {
|
|
21
|
+
x: Math.random() * canvas.width,
|
|
22
|
+
y: Math.random() * canvas.height,
|
|
23
|
+
size: opts.minSize + Math.random() * (opts.maxSize - opts.minSize),
|
|
24
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
25
|
+
angle: Math.random() * Math.PI * 2,
|
|
26
|
+
orbitRadius: 20 + Math.random() * 80,
|
|
27
|
+
orbitSpeed: (0.01 + Math.random() * 0.03) * (Math.random() > 0.5 ? 1 : -1),
|
|
28
|
+
centerX: Math.random() * canvas.width,
|
|
29
|
+
centerY: canvas.height + 50, // Start below and move up
|
|
30
|
+
upwardSpeed: opts.speed + Math.random() * 1.5,
|
|
31
|
+
alpha: 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < opts.particleCount; i++) {
|
|
36
|
+
dusts.push(createDust());
|
|
37
|
+
// Scatter initial positions so they aren't all at the bottom
|
|
38
|
+
dusts[i].centerY = Math.random() * canvas.height;
|
|
39
|
+
dusts[i].alpha = Math.random();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function animate() {
|
|
43
|
+
if (!running) return;
|
|
44
|
+
time++;
|
|
45
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < dusts.length; i++) {
|
|
48
|
+
const d = dusts[i];
|
|
49
|
+
d.angle += d.orbitSpeed;
|
|
50
|
+
d.centerY -= d.upwardSpeed;
|
|
51
|
+
|
|
52
|
+
// Swirling calculation
|
|
53
|
+
d.x = d.centerX + Math.cos(d.angle) * d.orbitRadius + Math.sin(time * 0.01 + d.angle) * 30;
|
|
54
|
+
d.y = d.centerY + Math.sin(d.angle) * (d.orbitRadius * 0.5);
|
|
55
|
+
|
|
56
|
+
if (d.alpha < 1 && d.centerY > 0) d.alpha += 0.01;
|
|
57
|
+
|
|
58
|
+
// Reset when they reach top
|
|
59
|
+
if (d.y < -50) {
|
|
60
|
+
Object.assign(d, createDust());
|
|
61
|
+
d.centerY = canvas.height + 50;
|
|
62
|
+
d.centerX = Math.random() * canvas.width;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ctx.save();
|
|
66
|
+
ctx.globalAlpha = d.alpha;
|
|
67
|
+
if (opts.glow) {
|
|
68
|
+
ctx.shadowColor = d.color;
|
|
69
|
+
ctx.shadowBlur = d.size * 3;
|
|
70
|
+
}
|
|
71
|
+
ctx.fillStyle = d.color;
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
ctx.arc(d.x, d.y, d.size, 0, Math.PI * 2);
|
|
74
|
+
ctx.fill();
|
|
75
|
+
ctx.restore();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
requestAnimationFrame(animate);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
animate();
|
|
82
|
+
|
|
83
|
+
return function stop() {
|
|
84
|
+
running = false;
|
|
85
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
density: 0.02,
|
|
5
|
+
minSpeed: 10,
|
|
6
|
+
maxSpeed: 25,
|
|
7
|
+
colors: ['#ffffff', '#e7c6ff', '#48cae4', '#ffe66d'],
|
|
8
|
+
glow: true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function shootingStars(canvas, userOptions = {}) {
|
|
12
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
13
|
+
const ctx = canvas.getContext('2d');
|
|
14
|
+
const stars = [];
|
|
15
|
+
let running = true;
|
|
16
|
+
|
|
17
|
+
function createStar() {
|
|
18
|
+
return {
|
|
19
|
+
x: Math.random() * canvas.width * 1.5,
|
|
20
|
+
y: -50,
|
|
21
|
+
length: 50 + Math.random() * 100,
|
|
22
|
+
thickness: 1 + Math.random() * 2,
|
|
23
|
+
speed: opts.minSpeed + Math.random() * (opts.maxSpeed - opts.minSpeed),
|
|
24
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
25
|
+
angle: (Math.PI / 4) + (Math.random() * 0.2 - 0.1), // Roughly 45 degrees
|
|
26
|
+
opacity: 1,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function animate() {
|
|
31
|
+
if (!running) return;
|
|
32
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
33
|
+
|
|
34
|
+
if (Math.random() < opts.density) {
|
|
35
|
+
stars.push(createStar());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let i = stars.length - 1; i >= 0; i--) {
|
|
39
|
+
const s = stars[i];
|
|
40
|
+
const vx = -Math.cos(s.angle) * s.speed;
|
|
41
|
+
const vy = Math.sin(s.angle) * s.speed;
|
|
42
|
+
|
|
43
|
+
s.x += vx;
|
|
44
|
+
s.y += vy;
|
|
45
|
+
s.opacity -= 0.01;
|
|
46
|
+
|
|
47
|
+
ctx.save();
|
|
48
|
+
ctx.globalAlpha = Math.max(0, s.opacity);
|
|
49
|
+
if (opts.glow) {
|
|
50
|
+
ctx.shadowColor = s.color;
|
|
51
|
+
ctx.shadowBlur = s.thickness * 4;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const grad = ctx.createLinearGradient(s.x, s.y, s.x - vx * (s.length / s.speed), s.y - vy * (s.length / s.speed));
|
|
55
|
+
grad.addColorStop(0, s.color);
|
|
56
|
+
grad.addColorStop(1, 'rgba(255,255,255,0)');
|
|
57
|
+
|
|
58
|
+
ctx.strokeStyle = grad;
|
|
59
|
+
ctx.lineWidth = s.thickness;
|
|
60
|
+
ctx.lineCap = 'round';
|
|
61
|
+
|
|
62
|
+
ctx.beginPath();
|
|
63
|
+
ctx.moveTo(s.x, s.y);
|
|
64
|
+
ctx.lineTo(s.x - vx * (s.length / s.speed), s.y - vy * (s.length / s.speed));
|
|
65
|
+
ctx.stroke();
|
|
66
|
+
ctx.restore();
|
|
67
|
+
|
|
68
|
+
if (s.opacity <= 0 || s.x < -100 || s.y > canvas.height + 100) {
|
|
69
|
+
stars.splice(i, 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
requestAnimationFrame(animate);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
animate();
|
|
77
|
+
|
|
78
|
+
return function stop() {
|
|
79
|
+
running = false;
|
|
80
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
count: 80, // number of sparkles alive at once
|
|
5
|
+
minSize: 2,
|
|
6
|
+
maxSize: 6,
|
|
7
|
+
speed: 0.5,
|
|
8
|
+
twinkleSpeed: 0.04,
|
|
9
|
+
colors: ['#fff', '#ffe4e8', '#ffb3c1', '#ff85a1', '#ffd6ff', '#e7c6ff'],
|
|
10
|
+
glow: true,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function sparkles(canvas, userOptions = {}) {
|
|
14
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
15
|
+
const ctx = canvas.getContext('2d');
|
|
16
|
+
const stars = [];
|
|
17
|
+
let running = true;
|
|
18
|
+
|
|
19
|
+
function createStar() {
|
|
20
|
+
return {
|
|
21
|
+
x: Math.random() * canvas.width,
|
|
22
|
+
y: Math.random() * canvas.height,
|
|
23
|
+
size: opts.minSize + Math.random() * (opts.maxSize - opts.minSize),
|
|
24
|
+
alpha: Math.random(),
|
|
25
|
+
alphaDir: Math.random() > 0.5 ? 1 : -1,
|
|
26
|
+
twinkleSpeed: opts.twinkleSpeed * (0.5 + Math.random()),
|
|
27
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
28
|
+
vx: (Math.random() - 0.5) * opts.speed,
|
|
29
|
+
vy: (Math.random() - 0.5) * opts.speed,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Populate initial stars
|
|
34
|
+
for (let i = 0; i < opts.count; i++) stars.push(createStar());
|
|
35
|
+
|
|
36
|
+
function drawStar(s) {
|
|
37
|
+
ctx.save();
|
|
38
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, s.alpha));
|
|
39
|
+
if (opts.glow) { ctx.shadowColor = s.color; ctx.shadowBlur = s.size * 3; }
|
|
40
|
+
ctx.fillStyle = s.color;
|
|
41
|
+
|
|
42
|
+
// Cross / sparkle shape
|
|
43
|
+
const r = s.size;
|
|
44
|
+
ctx.beginPath();
|
|
45
|
+
for (let i = 0; i < 4; i++) {
|
|
46
|
+
const a = (i * Math.PI) / 2;
|
|
47
|
+
ctx.ellipse(
|
|
48
|
+
s.x + Math.cos(a) * r * 0.35,
|
|
49
|
+
s.y + Math.sin(a) * r * 0.35,
|
|
50
|
+
r * 0.15, r * 0.7, a, 0, Math.PI * 2
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
ctx.fill();
|
|
54
|
+
|
|
55
|
+
// Tiny centre dot
|
|
56
|
+
ctx.beginPath();
|
|
57
|
+
ctx.arc(s.x, s.y, r * 0.2, 0, Math.PI * 2);
|
|
58
|
+
ctx.fill();
|
|
59
|
+
ctx.restore();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function animate() {
|
|
63
|
+
if (!running) return;
|
|
64
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < stars.length; i++) {
|
|
67
|
+
const s = stars[i];
|
|
68
|
+
s.x += s.vx;
|
|
69
|
+
s.y += s.vy;
|
|
70
|
+
s.alpha += s.alphaDir * s.twinkleSpeed;
|
|
71
|
+
|
|
72
|
+
if (s.alpha >= 1) { s.alpha = 1; s.alphaDir = -1; }
|
|
73
|
+
else if (s.alpha <= 0) { s.alpha = 0; s.alphaDir = 1; }
|
|
74
|
+
|
|
75
|
+
// Wrap edges
|
|
76
|
+
if (s.x < -10) s.x = canvas.width + 10;
|
|
77
|
+
if (s.x > canvas.width + 10) s.x = -10;
|
|
78
|
+
if (s.y < -10) s.y = canvas.height + 10;
|
|
79
|
+
if (s.y > canvas.height + 10) s.y = -10;
|
|
80
|
+
|
|
81
|
+
drawStar(s);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
requestAnimationFrame(animate);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
animate();
|
|
88
|
+
|
|
89
|
+
return function stop() {
|
|
90
|
+
running = false;
|
|
91
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mergeOptions } from '../core/engine.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
starCount: 120,
|
|
5
|
+
speed: 0.4,
|
|
6
|
+
colors: ['#ffffff', '#ffe4e8', '#ffc2d1', '#e7c6ff', '#a2d2ff'],
|
|
7
|
+
minSize: 1,
|
|
8
|
+
maxSize: 3.5,
|
|
9
|
+
twinkle: true,
|
|
10
|
+
connectDist: 100, // draw faint lines between close stars
|
|
11
|
+
connectOpacity: 0.08,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function starField(canvas, userOptions = {}) {
|
|
15
|
+
const opts = mergeOptions(DEFAULTS, userOptions);
|
|
16
|
+
const ctx = canvas.getContext('2d');
|
|
17
|
+
const stars = [];
|
|
18
|
+
let running = true;
|
|
19
|
+
|
|
20
|
+
function createStar(randomY = false) {
|
|
21
|
+
return {
|
|
22
|
+
x: Math.random() * canvas.width,
|
|
23
|
+
y: randomY ? Math.random() * canvas.height : Math.random() * canvas.height,
|
|
24
|
+
size: opts.minSize + Math.random() * (opts.maxSize - opts.minSize),
|
|
25
|
+
alpha: 0.3 + Math.random() * 0.7,
|
|
26
|
+
alphaDir: Math.random() > 0.5 ? 1 : -1,
|
|
27
|
+
twinkleSpeed: 0.008 + Math.random() * 0.015,
|
|
28
|
+
vx: (Math.random() - 0.5) * opts.speed,
|
|
29
|
+
vy: (Math.random() - 0.5) * opts.speed,
|
|
30
|
+
color: opts.colors[Math.floor(Math.random() * opts.colors.length)],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < opts.starCount; i++) stars.push(createStar(true));
|
|
35
|
+
|
|
36
|
+
function animate() {
|
|
37
|
+
if (!running) return;
|
|
38
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
39
|
+
|
|
40
|
+
// Draw connections
|
|
41
|
+
if (opts.connectDist > 0) {
|
|
42
|
+
for (let i = 0; i < stars.length; i++) {
|
|
43
|
+
for (let j = i + 1; j < stars.length; j++) {
|
|
44
|
+
const dx = stars[i].x - stars[j].x;
|
|
45
|
+
const dy = stars[i].y - stars[j].y;
|
|
46
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
47
|
+
if (dist < opts.connectDist) {
|
|
48
|
+
ctx.save();
|
|
49
|
+
ctx.globalAlpha = opts.connectOpacity * (1 - dist / opts.connectDist);
|
|
50
|
+
ctx.strokeStyle = '#ffffff';
|
|
51
|
+
ctx.lineWidth = 0.5;
|
|
52
|
+
ctx.beginPath();
|
|
53
|
+
ctx.moveTo(stars[i].x, stars[i].y);
|
|
54
|
+
ctx.lineTo(stars[j].x, stars[j].y);
|
|
55
|
+
ctx.stroke();
|
|
56
|
+
ctx.restore();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Draw stars
|
|
63
|
+
for (let i = 0; i < stars.length; i++) {
|
|
64
|
+
const s = stars[i];
|
|
65
|
+
s.x += s.vx;
|
|
66
|
+
s.y += s.vy;
|
|
67
|
+
|
|
68
|
+
if (opts.twinkle) {
|
|
69
|
+
s.alpha += s.alphaDir * s.twinkleSpeed;
|
|
70
|
+
if (s.alpha >= 1) { s.alpha = 1; s.alphaDir = -1; }
|
|
71
|
+
if (s.alpha <= 0.1) { s.alpha = 0.1; s.alphaDir = 1; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Wrap
|
|
75
|
+
if (s.x < -5) s.x = canvas.width + 5;
|
|
76
|
+
if (s.x > canvas.width + 5) s.x = -5;
|
|
77
|
+
if (s.y < -5) s.y = canvas.height + 5;
|
|
78
|
+
if (s.y > canvas.height + 5) s.y = -5;
|
|
79
|
+
|
|
80
|
+
ctx.save();
|
|
81
|
+
ctx.globalAlpha = s.alpha;
|
|
82
|
+
ctx.shadowColor = s.color;
|
|
83
|
+
ctx.shadowBlur = s.size * 3;
|
|
84
|
+
ctx.fillStyle = s.color;
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
|
|
87
|
+
ctx.fill();
|
|
88
|
+
ctx.restore();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
requestAnimationFrame(animate);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
animate();
|
|
95
|
+
|
|
96
|
+
return function stop() {
|
|
97
|
+
running = false;
|
|
98
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/core/engine.js
CHANGED
|
@@ -1,13 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
/**
|
|
2
|
+
* romantic-animations — core engine
|
|
3
|
+
* Handles canvas creation, sizing, resize observation, and cleanup.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
zIndex: 0,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialises a canvas inside the given container element.
|
|
12
|
+
*
|
|
13
|
+
* @param {string|HTMLElement} containerIdOrEl – element id OR element reference
|
|
14
|
+
* @param {object} userOptions – optional overrides
|
|
15
|
+
* @returns {{ canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D,
|
|
16
|
+
* options: object, destroy: Function }}
|
|
17
|
+
*/
|
|
18
|
+
export function initCanvas(containerIdOrEl, userOptions = {}) {
|
|
19
|
+
const options = Object.assign({}, DEFAULT_OPTIONS, userOptions);
|
|
20
|
+
|
|
21
|
+
const container =
|
|
22
|
+
typeof containerIdOrEl === 'string'
|
|
23
|
+
? document.getElementById(containerIdOrEl)
|
|
24
|
+
: containerIdOrEl;
|
|
25
|
+
|
|
26
|
+
if (!container) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`[romantic-animations] Container "${containerIdOrEl}" not found in the DOM.`
|
|
29
|
+
);
|
|
12
30
|
}
|
|
13
|
-
|
|
31
|
+
|
|
32
|
+
// Remove any pre-existing canvas we created (clean slate when re-triggering)
|
|
33
|
+
const old = container.querySelector('canvas[data-ra]');
|
|
34
|
+
if (old) old.remove();
|
|
35
|
+
|
|
36
|
+
const canvas = document.createElement('canvas');
|
|
37
|
+
canvas.setAttribute('data-ra', '1');
|
|
38
|
+
canvas.style.cssText = `
|
|
39
|
+
position: fixed;
|
|
40
|
+
top: 0;
|
|
41
|
+
left: 0;
|
|
42
|
+
width: 100vw;
|
|
43
|
+
height: 100vh;
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
z-index: ${options.zIndex};
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const resize = () => {
|
|
49
|
+
canvas.width = window.innerWidth;
|
|
50
|
+
canvas.height = window.innerHeight;
|
|
51
|
+
};
|
|
52
|
+
resize();
|
|
53
|
+
|
|
54
|
+
container.style.position = container.style.position || 'relative';
|
|
55
|
+
container.appendChild(canvas);
|
|
56
|
+
|
|
57
|
+
// Keep canvas sized to container
|
|
58
|
+
const ro = new ResizeObserver(resize);
|
|
59
|
+
ro.observe(container);
|
|
60
|
+
|
|
61
|
+
const ctx = canvas.getContext('2d');
|
|
62
|
+
|
|
63
|
+
/** Tear-down helper – call the returned destroy() to stop & clean up */
|
|
64
|
+
function destroy() {
|
|
65
|
+
ro.disconnect();
|
|
66
|
+
canvas.remove();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { canvas, ctx, options, destroy };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Merge user options with animation-specific defaults.
|
|
74
|
+
*/
|
|
75
|
+
export function mergeOptions(defaults, userOptions = {}) {
|
|
76
|
+
return Object.assign({}, defaults, userOptions);
|
|
77
|
+
}
|