@sarthak03dot/romantic-animations 1.0.3 → 1.2.1

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.
@@ -1,41 +1,85 @@
1
- export function heartTrail(canvas) {
2
- const ctx = canvas.getContext("2d");
3
- const hearts = [];
4
-
5
- function createHeart(x, y) {
6
- return {
7
- x,
8
- y,
9
- size: 8 + Math.random() * 5,
10
- alpha: 1,
11
- decay: 0.02
12
- };
13
- }
14
-
15
- function drawHeart(h) {
16
- ctx.fillStyle = `rgba(255, 105, 180, ${h.alpha})`;
17
- ctx.beginPath();
18
- ctx.moveTo(h.x, h.y);
19
- ctx.bezierCurveTo(h.x + h.size / 2, h.y - h.size, h.x + h.size, h.y + h.size / 3, h.x, h.y + h.size);
20
- ctx.bezierCurveTo(h.x - h.size, h.y + h.size / 3, h.x - h.size / 2, h.y - h.size, h.x, h.y);
21
- ctx.fill();
22
- }
23
-
24
- canvas.addEventListener("mousemove", (e) => {
25
- const rect = canvas.getBoundingClientRect();
26
- hearts.push(createHeart(e.clientX - rect.left, e.clientY - rect.top));
27
- });
28
-
29
- function animate() {
30
- ctx.clearRect(0, 0, canvas.width, canvas.height);
31
- for (let i = hearts.length - 1; i >= 0; i--) {
32
- drawHeart(hearts[i]);
33
- hearts[i].alpha -= hearts[i].decay;
34
- if (hearts[i].alpha <= 0) hearts.splice(i, 1);
35
- }
36
- requestAnimationFrame(animate);
37
- }
38
-
39
- animate();
40
- }
41
-
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);
56
+ });
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);
72
+ }
73
+
74
+ requestAnimationFrame(animate);
75
+ }
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
+ }
@@ -1,13 +1,77 @@
1
- export function initCanvas(containerId) {
2
- const container = document.getElementById(containerId);
3
- const canvas = document.createElement('canvas');
4
- canvas.width = container.offsetWidth;
5
- canvas.height = container.offsetHeight;
6
- canvas.style.position = 'absolute';
7
- canvas.style.top = 0;
8
- canvas.style.left = 0;
9
- canvas.style.pointerEvents = 'none';
10
- container.appendChild(canvas);
11
- return canvas;
12
- }
13
-
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
+ );
30
+ }
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
+ }