magic-canvas-text 1.0.0 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +129 -46
  2. package/package.json +1 -1
  3. package/src/index.js +133 -135
package/README.md CHANGED
@@ -1,36 +1,49 @@
1
- # ✨ Magic canvas text
1
+ # ✨ Magic Canvas Text
2
2
 
3
- **Magic canvas text** is a lightweight npm library for rendering animated, interactive particle-based text using the HTML5 canvas.
4
- It supports mouse and touch interactions, gradients, multiple animation start modes, and mobile-optimized behavior.
3
+ **Magic Canvas Text** is a lightweight npm library for rendering animated, interactive particle-based text using the HTML5 canvas.
4
+ It supports mouse and touch interactions, gradients, multiple animation start modes, and mobileoptimized behavior.
5
5
 
6
- Ideal for landing pages, hero sections, and playful UI elements.
6
+ Perfect for landing pages, hero sections, and playful UI elements.
7
7
 
8
8
  ---
9
9
 
10
10
  ## 📦 Installation
11
11
 
12
- Install the package via npm:
12
+ Install the package via npm or yarn:
13
13
 
14
14
  ```bash
15
15
  npm install magic-canvas-text
16
- ````
16
+ ```
17
+
17
18
  ```bash
18
19
  yarn add magic-canvas-text
19
20
  ```
20
21
 
22
+ ---
23
+
21
24
  ## 🚀 Usage
22
- Import
23
- import { initializeText } from "magic-canvas-text;
24
25
 
25
26
  ### HTML
26
27
 
27
28
  Create a container element where the canvas will be injected:
28
29
 
30
+ ```html
29
31
  <div class="text-con"></div>
32
+ ```
33
+
34
+ > ⚠️ The container should be empty and have a defined width & height.
35
+
36
+ ---
37
+
38
+ ### JavaScript
39
+
40
+ ```js
41
+ import { initializeText } from "magic-canvas-text";
42
+
43
+ const element = document.querySelector(".your-class");
30
44
 
31
- ### JS
32
- initializeText({
33
- textContainerClass: "text-con",
45
+ const magicText = initializeText({
46
+ element,
34
47
  text: "Magic Text",
35
48
  fontSize: 100,
36
49
  fontSizeMobile: 30,
@@ -44,62 +57,132 @@ initializeText({
44
57
  colorOne: "#ff0000",
45
58
  colorTwo: "#00ff00",
46
59
  colorThree: "#0000ff",
47
- startMode: "auto",
60
+ startMode: "random",
48
61
  });
62
+ ```
63
+
64
+ ### ✅ Important API Change
65
+
66
+ Magic Canvas Text now **expects a DOM element**, not a class name or selector string.
49
67
 
50
- ### ## 🔧 Configuration Options
68
+ This makes the API:
51
69
 
52
- | Option | Type | Required | Notes |
53
- |------|------|----------|-------|
54
- | `textContainerClass` | `string` | ✅ Yes | Must match an existing DOM element and an empty one|
55
- | `text` | `string` | ❌ No | Defaults to `"Magic Text"` |
56
- | `fontSize` | `number` | ✅ Yes | Required for proper font rendering |
57
- | `fontSizeMobile` | `number` | ✅ Yes | Required for mobile rendering |
58
- | `textColor` | `string` | ⚠️ Conditional | Defaults to `#000000` |
59
- | `bgColor` | `string` | ❌ No | Defaults to `#ffffff` |
60
- | `effectColorApplied` | `boolean` | ⚠️ Recommended | Enables hover color effect |
61
- | `effectColor` | `string` | ⚠️ Conditional | Required when `effectColorApplied === true` |
62
- | `effectRadius` | `number` | ❌ No | Defaults to `80` (mobile capped at `100`) |
63
- | `duration` | `number` | ❌ No | Defaults internally to `0.05` |
64
- | `gradient` | `boolean` | ⚠️ Recommended | Enables gradient text |
65
- | `colorOne` | `string` | ⚠️ Conditional | Required when `gradient === true` |
66
- | `colorTwo` | `string` | ⚠️ Conditional | Required when `gradient === true` |
67
- | `colorThree` | `string` | ⚠️ Conditional | Required when `gradient === true` |
68
- | `startMode` | `string` | ❌ No | Defaults to `random` |
70
+ * more predictable
71
+ * framework‑friendly (React, Vue, Svelte)
72
+ * safer against double initialization
69
73
 
74
+ ---
70
75
 
71
- ### 🎬 Start Modes
76
+ ## 🔧 Configuration Options
77
+
78
+ | Option | Type | Required | Description |
79
+ | -------------------- | ------------- | -------------- | ---------------------------------------------------- |
80
+ | `element` | `HTMLElement` | ✅ Yes | Target element where the canvas will be mounted |
81
+ | `text` | `string` | ❌ No | Text to render (default: `"Magic Text"`) |
82
+ | `fontSize` | `number` | ❌ No | Desktop font size (default: `100`) |
83
+ | `fontSizeMobile` | `number` | ❌ No | Mobile font size (default: `30`) |
84
+ | `textColor` | `string` | ❌ No | Solid text color (default: `#000000`) |
85
+ | `bgColor` | `string` | ❌ No | Canvas background color (default: `#ffffff`) |
86
+ | `effectColorApplied` | `boolean` | ❌ No | Enables hover color effect |
87
+ | `effectColor` | `string` | ⚠️ Conditional | Required if `effectColorApplied === true` |
88
+ | `effectRadius` | `number` | ❌ No | Interaction radius (default: `80`, mobile max `100`) |
89
+ | `duration` | `number` | ❌ No | Particle easing speed (default: `0.05`) |
90
+ | `gradient` | `boolean` | ❌ No | Enables gradient text |
91
+ | `colorOne` | `string` | ⚠️ Conditional | Required when `gradient === true` |
92
+ | `colorTwo` | `string` | ⚠️ Conditional | Required when `gradient === true` |
93
+ | `colorThree` | `string` | ⚠️ Conditional | Required when `gradient === true` |
94
+ | `startMode` | `string` | ❌ No | Particle start animation mode (default: `random`) |
72
95
 
73
- - random – particles spawn at random positions
96
+ ---
74
97
 
75
- - left particles animate in from the left
98
+ ## 🎬 Start Modes
76
99
 
77
- - center – particles animate from the center
100
+ * `random` – particles spawn at random positions
101
+ * `left` – particles animate in from the left
102
+ * `center` – particles animate from the center
103
+ * `top` – particles animate in from top
104
+ * `bottom` – particles animate in from below
78
105
 
79
- - bottom – particles animate from below
106
+ ---
80
107
 
81
- ### 🧹 Cleanup
108
+ ## 🧹 Cleanup
82
109
 
83
- To remove the canvas, animation loop, and event listeners:
110
+ Each initialization returns an instance with a `destroy()` method.
84
111
 
85
- const magicText = initializeText({ ... });
112
+ ```js
113
+ const magicText = initializeText({ element, text: "Hello" });
86
114
 
87
115
  // later
88
116
  magicText.destroy();
117
+ ```
118
+
119
+ This removes:
120
+
121
+ * the canvas
122
+ * animation loop
123
+ * event listeners
124
+ * internal instance reference
125
+
126
+ ---
89
127
 
128
+ ## 📱 Mobile Support
90
129
 
91
- ### 📱 Mobile Support
130
+ * Touch interaction support
131
+ * Optimized interaction radius
132
+ * Separate mobile font sizing
92
133
 
93
- - Touch events supported
134
+ ---
135
+
136
+ ## 🧩 Framework Usage
137
+
138
+ ### React
139
+
140
+ ```js
141
+ const ref = useRef(null);
142
+
143
+ useEffect(() => {
144
+ const instance = initializeText({
145
+ element: ref.current,
146
+ text: "React Magic",
147
+ });
148
+
149
+ return () => instance?.destroy();
150
+ }, []);
151
+ ```
152
+
153
+ ### Vue
154
+
155
+ ```js
156
+ const title = ref(null);
157
+
158
+ onMounted(() => {
159
+ initializeText({
160
+ element: title.value,
161
+ text: "Vue Magic",
162
+ });
163
+ });
164
+ ```
165
+
166
+ ---
94
167
 
95
- - Optimized interaction radius for performance
168
+ ## 🌐 Demo
169
+
170
+ [Magic Canvas Text Demo](https://luayabbas1981.github.io/magic-text/)
171
+
172
+ ---
96
173
 
97
- - Separate mobile font size configuration
174
+ ## 📦 Github
98
175
 
99
- ### Links
176
+ [magic-canvas-text on npm](https://github.com/Luayabbas1981/magic-canvas-text)
100
177
 
101
- -github [Magic canvas text...](https://github.com/Luayabbas1981/magic-canvas-text)
178
+ ---
179
+
180
+ ## 👤 Portfolio
181
+
182
+ [Portfolio](https://luay-portfolio.interflowcode.de/)
183
+
184
+ ---
102
185
 
103
- ### 📄 License
186
+ ## 📄 License
104
187
 
105
- MIT License
188
+ MIT License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-canvas-text",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "An interactive particle-based text animation library using HTML5 Canvas.",
5
5
  "main": "src/index.js",
6
6
  "files": [
package/src/index.js CHANGED
@@ -1,250 +1,248 @@
1
1
  // Initialize Magic Text
2
2
  function initializeText({
3
- textContainerClass,
4
- text,
5
- fontSize,
6
- fontSizeMobile,
7
- textColor,
8
- bgColor,
9
- effectColorApplied,
10
- effectColor,
11
- effectRadius,
12
- duration,
13
- gradient,
3
+ element,
4
+ text = "Magic Text",
5
+ fontSize = 100,
6
+ fontSizeMobile = 30,
7
+ textColor = "#000",
8
+ bgColor = "#fff",
9
+ effectColorApplied = false,
10
+ effectColor = "#0088ff",
11
+ effectRadius = 80,
12
+ duration = 0.05,
13
+ gradient = false,
14
14
  colorOne,
15
15
  colorTwo,
16
16
  colorThree,
17
- startMode,
17
+ startMode = "random",
18
18
  }) {
19
- // App values
19
+ if (typeof window === "undefined") return;
20
+
21
+ if (!(element instanceof HTMLElement)) {
22
+ console.error("MagicText: element must be a DOM element");
23
+ return;
24
+ }
25
+
26
+ // prevent double init
27
+ if (element._magicTextInstance) {
28
+ return element._magicTextInstance;
29
+ }
30
+
20
31
  const gap = 1;
21
32
  let animationId;
22
33
  let destroyed = false;
23
34
  const particles = [];
35
+
24
36
  const mouse = {
25
37
  x: null,
26
38
  y: null,
27
39
  radius: effectRadius,
28
40
  };
29
- // Check device
30
- if (typeof window === "undefined") return;
41
+
31
42
  const isMobile =
32
43
  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
33
44
  navigator.userAgent,
34
45
  );
35
46
 
36
- const textContainer = document.querySelector(`.${textContainerClass}`);
37
- if (!textContainer) {
38
- console.error(`No element found with class name "${textContainerClass}".`);
39
- return;
40
- }
41
- textContainer.classList.add("magic-text-container");
47
+ element.classList.add("magic-text-container");
48
+
42
49
  const canvas = document.createElement("canvas");
43
50
  canvas.classList.add("magic-text-canvas");
44
- textContainer.appendChild(canvas);
51
+ element.appendChild(canvas);
52
+
45
53
  const ctx = canvas.getContext("2d");
46
- canvas.width = textContainer.clientWidth;
47
- canvas.height = textContainer.clientHeight;
48
- const canvasWidth = canvas.width;
49
- const canvasHeight = canvas.height;
50
- canvas.style.backgroundColor = bgColor || "#ffffff";
51
54
 
52
- // Create particles from text
55
+ function resizeCanvas() {
56
+ canvas.width = element.clientWidth;
57
+ canvas.height = element.clientHeight;
58
+ }
59
+
60
+ resizeCanvas();
61
+ canvas.style.backgroundColor = bgColor;
62
+
63
+ const canvasWidth = () => canvas.width;
64
+ const canvasHeight = () => canvas.height;
65
+
66
+ // ---------- Particle ----------
53
67
  class Particle {
54
- constructor(ctx, x, y, color, startMode) {
55
- this.ctx = ctx;
68
+ constructor(x, y, color) {
56
69
  this.originX = x;
57
70
  this.originY = y;
71
+
58
72
  const start = getStartPosition(startMode, x, y);
59
73
  this.x = start.x;
60
74
  this.y = start.y;
61
75
 
62
76
  this.color = color;
63
77
  this.baseColor = color;
64
- this.secondColor = effectColor || "#0088ff";
78
+ this.secondColor = effectColor;
65
79
  this.size = gap;
66
- this.ease = Math.random() * 0.1 + (duration || 0.05);
80
+ this.ease = Math.random() * 0.1 + duration;
67
81
  this.pushX = 0;
68
82
  this.pushY = 0;
69
83
  this.friction = 0.9;
70
84
  }
71
85
 
72
86
  update() {
73
- let isHovering = false;
87
+ let hovering = false;
88
+
74
89
  if (mouse.x !== null) {
75
90
  const dx = this.x - mouse.x;
76
91
  const dy = this.y - mouse.y;
77
- const distance = Math.sqrt(dx * dx + dy * dy);
78
- if (distance < mouse.radius) {
79
- isHovering = true;
80
- const force = (mouse.radius - distance) / mouse.radius;
92
+ const dist = Math.sqrt(dx * dx + dy * dy);
93
+
94
+ if (dist < mouse.radius) {
95
+ hovering = true;
96
+ const force = (mouse.radius - dist) / mouse.radius;
81
97
  const angle = Math.atan2(dy, dx);
98
+
82
99
  this.pushX += Math.cos(angle) * force * 7 * this.ease * 5;
83
100
  this.pushY += Math.sin(angle) * force * 7 * this.ease * 5;
84
- if (
85
- effectColorApplied &&
86
- effectColor &&
87
- this.originX > mouse.x &&
88
- this.originY > mouse.y
89
- ) {
101
+
102
+ if (effectColorApplied) {
90
103
  this.color = this.secondColor;
91
104
  }
92
105
  }
93
106
  }
94
107
 
95
- if (!isHovering) {
108
+ if (!hovering) {
96
109
  this.color = this.baseColor;
97
110
  }
98
- // Apply push force
111
+
99
112
  this.pushX *= this.friction;
100
113
  this.pushY *= this.friction;
101
114
 
102
115
  this.x += this.pushX;
103
116
  this.y += this.pushY;
104
117
 
105
- // Ease back to original text position
106
118
  this.x += (this.originX - this.x) * this.ease;
107
119
  this.y += (this.originY - this.y) * this.ease;
108
120
  }
109
121
 
110
122
  draw() {
111
- this.ctx.fillStyle = this.color;
112
- this.ctx.fillRect(this.x, this.y, this.size, this.size);
123
+ ctx.fillStyle = this.color;
124
+ ctx.fillRect(this.x, this.y, this.size, this.size);
113
125
  }
114
126
  }
115
- // Draw Text on Canvas
127
+
128
+ // ---------- Text ----------
116
129
  function drawText() {
117
- ctx.clearRect(0, 0, canvasWidth, canvasHeight);
118
- const fontFamily = "Arial Black, sans-serif";
130
+ ctx.clearRect(0, 0, canvasWidth(), canvasHeight());
131
+
119
132
  const activeFontSize = isMobile ? fontSizeMobile : fontSize;
120
- ctx.font = `italic bold ${activeFontSize}px ${fontFamily}`;
133
+ ctx.font = `italic bold ${activeFontSize}px Arial Black, sans-serif`;
121
134
  ctx.textAlign = "center";
122
135
  ctx.textBaseline = "middle";
136
+
123
137
  if (gradient && colorOne && colorTwo && colorThree) {
124
- const grad = ctx.createLinearGradient(0, 0, canvasWidth, canvasHeight);
138
+ const grad = ctx.createLinearGradient(
139
+ 0,
140
+ 0,
141
+ canvasWidth(),
142
+ canvasHeight(),
143
+ );
125
144
  grad.addColorStop(0.3, colorOne);
126
145
  grad.addColorStop(0.5, colorTwo);
127
146
  grad.addColorStop(0.8, colorThree);
128
147
  ctx.fillStyle = grad;
129
148
  } else {
130
- ctx.fillStyle = textColor || "#000000";
149
+ ctx.fillStyle = textColor;
131
150
  }
132
- ctx.fillText(text || "Magic Text", canvasWidth / 2, canvasHeight / 2);
151
+
152
+ ctx.fillText(text, canvasWidth() / 2, canvasHeight() / 2);
133
153
  }
154
+
134
155
  function createParticlesFromText() {
135
- mouse.radius = effectRadius || 80;
136
- if (isMobile && mouse.radius > 100) {
137
- mouse.radius = 100;
138
- }
156
+ mouse.radius = isMobile ? Math.min(effectRadius, 100) : effectRadius;
139
157
  particles.length = 0;
140
- startMode = startMode || "random";
158
+
141
159
  drawText();
142
- const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
143
- const pixels = imageData.data;
144
- ctx.clearRect(0, 0, canvasWidth, canvasHeight);
145
- for (let y = 0; y < canvasHeight; y += gap) {
146
- for (let x = 0; x < canvasWidth; x += gap) {
147
- const index = (y * canvasWidth + x) * 4;
148
- const alpha = pixels[index + 3];
149
- if (alpha > 0) {
150
- const r = pixels[index];
151
- const g = pixels[index + 1];
152
- const b = pixels[index + 2];
153
- const color = `rgb(${r}, ${g}, ${b})`;
154
- particles.push(new Particle(ctx, x, y, color, startMode));
160
+
161
+ const imageData = ctx.getImageData(0, 0, canvasWidth(), canvasHeight());
162
+ ctx.clearRect(0, 0, canvasWidth(), canvasHeight());
163
+
164
+ for (let y = 0; y < canvasHeight(); y += gap) {
165
+ for (let x = 0; x < canvasWidth(); x += gap) {
166
+ const i = (y * canvasWidth() + x) * 4;
167
+ if (imageData.data[i + 3] > 0) {
168
+ const r = imageData.data[i];
169
+ const g = imageData.data[i + 1];
170
+ const b = imageData.data[i + 2];
171
+ particles.push(new Particle(x, y, `rgb(${r},${g},${b})`));
155
172
  }
156
173
  }
157
174
  }
158
175
  }
159
- // Animate
176
+
160
177
  function animate() {
161
178
  if (destroyed) return;
162
- ctx.clearRect(0, 0, canvasWidth, canvasHeight);
163
- particles.forEach((particle) => {
164
- particle.update();
165
- particle.draw();
179
+
180
+ ctx.clearRect(0, 0, canvasWidth(), canvasHeight());
181
+ particles.forEach((p) => {
182
+ p.update();
183
+ p.draw();
166
184
  });
167
185
 
168
186
  animationId = requestAnimationFrame(animate);
169
187
  }
170
- // Modes
171
- function getStartPosition(mode, originX, originY) {
188
+
189
+ function getStartPosition(mode, ox, oy) {
172
190
  switch (mode) {
173
191
  case "center":
174
- return {
175
- x: canvasWidth / 2,
176
- y: canvasHeight / 2,
177
- };
192
+ return { x: canvasWidth() / 2, y: canvasHeight() / 2 };
178
193
  case "bottom":
179
- return {
180
- x: originX,
181
- y: canvasHeight + 20,
182
- };
183
-
194
+ return { x: ox, y: canvasHeight() + 20 };
195
+ case "top":
196
+ return { x: ox, y: -20 };
184
197
  case "left":
185
- return {
186
- x: -20,
187
- y: originY,
188
- };
189
-
190
- case "random":
198
+ return { x: -20, y: oy };
191
199
  default:
192
200
  return {
193
- x: Math.random() * canvasWidth,
194
- y: Math.random() * canvasHeight,
201
+ x: Math.random() * canvasWidth(),
202
+ y: Math.random() * canvasHeight(),
195
203
  };
196
204
  }
197
205
  }
198
- // Mouse events
199
- function onMouseMove(e) {
200
- const rect = canvas.getBoundingClientRect();
201
- mouse.x = e.clientX - rect.left;
202
- mouse.y = e.clientY - rect.top;
203
- }
204
- function onMouseLeave() {
205
- mouse.x = null;
206
- mouse.y = null;
207
- }
208
- function onTouchStart(e) {
209
- const rect = canvas.getBoundingClientRect();
210
- const touch = e.touches[0];
211
- mouse.x = touch.clientX - rect.left;
212
- mouse.y = touch.clientY - rect.top;
213
- }
214
- function onTouchMove(e) {
206
+
207
+ // ---------- Events ----------
208
+ function updateMouse(e, touch = false) {
215
209
  const rect = canvas.getBoundingClientRect();
216
- const touch = e.touches[0];
217
- mouse.x = touch.clientX - rect.left;
218
- mouse.y = touch.clientY - rect.top;
210
+ const p = touch ? e.touches[0] : e;
211
+ mouse.x = p.clientX - rect.left;
212
+ mouse.y = p.clientY - rect.top;
219
213
  }
220
- function onTouchEnd() {
221
- mouse.x = null;
222
- mouse.y = null;
214
+
215
+ function resetMouse() {
216
+ mouse.x = mouse.y = null;
223
217
  }
224
218
 
225
- canvas.addEventListener("mousemove", onMouseMove);
226
- canvas.addEventListener("mouseleave", onMouseLeave);
227
- canvas.addEventListener("touchstart", onTouchStart, { passive: true });
228
- canvas.addEventListener("touchmove", onTouchMove, { passive: true });
229
- canvas.addEventListener("touchend", onTouchEnd);
230
- // Cleanup function
219
+ canvas.addEventListener("mousemove", updateMouse);
220
+ canvas.addEventListener("mouseleave", resetMouse);
221
+ canvas.addEventListener("touchstart", (e) => updateMouse(e, true), {
222
+ passive: true,
223
+ });
224
+ canvas.addEventListener("touchmove", (e) => updateMouse(e, true), {
225
+ passive: true,
226
+ });
227
+ canvas.addEventListener("touchend", resetMouse);
228
+
229
+ // ---------- Destroy ----------
231
230
  function destroy() {
232
231
  if (destroyed) return;
233
232
  destroyed = true;
234
- cancelAnimationFrame(animationId);
235
-
236
- canvas.removeEventListener("mousemove", onMouseMove);
237
- canvas.removeEventListener("mouseleave", onMouseLeave);
238
- canvas.removeEventListener("touchstart", onTouchStart);
239
- canvas.removeEventListener("touchmove", onTouchMove);
240
- canvas.removeEventListener("touchend", onTouchEnd);
241
233
 
234
+ cancelAnimationFrame(animationId);
242
235
  canvas.remove();
243
236
  particles.length = 0;
237
+
238
+ delete element._magicTextInstance;
244
239
  }
245
- // Initial Setup
240
+
241
+ // ---------- Init ----------
246
242
  createParticlesFromText();
247
243
  animate();
248
- return { destroy };
244
+
245
+ element._magicTextInstance = { destroy };
246
+ return element._magicTextInstance;
249
247
  }
250
248
  export { initializeText };