@luay_abbas/magic-text 1.0.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.
Files changed (3) hide show
  1. package/README.md +101 -0
  2. package/package.json +28 -0
  3. package/src/index.js +250 -0
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # โœจ Magic Text Canvas
2
+
3
+ **Magic Text Canvas** 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.
5
+
6
+ Ideal for landing pages, hero sections, and playful UI elements.
7
+
8
+ ---
9
+
10
+ ## ๐Ÿ“ฆ Installation
11
+
12
+ Install the package via npm:
13
+
14
+ ```bash
15
+ npm install magic-text-canvas
16
+ ````
17
+ ```bash
18
+ yarn add magic-text-canvas
19
+ ```
20
+
21
+ ## ๐Ÿš€ Usage
22
+ Import
23
+ import { initializeText } from "magic-text-canvas";
24
+
25
+ ### HTML
26
+
27
+ Create a container element where the canvas will be injected:
28
+
29
+ <div class="text-con"></div>
30
+
31
+ ### JS
32
+ initializeText({
33
+ textContainerClass: "text-con",
34
+ text: "Magic Text",
35
+ fontSize: 100,
36
+ fontSizeMobile: 30,
37
+ textColor: "#ffffff",
38
+ bgColor: "#000000",
39
+ effectColorApplied: true,
40
+ effectColor: "#ff0000",
41
+ effectRadius: 85,
42
+ duration: 0.03,
43
+ gradient: false,
44
+ colorOne: "#ff0000",
45
+ colorTwo: "#00ff00",
46
+ colorThree: "#0000ff",
47
+ startMode: "auto",
48
+ });
49
+
50
+ ### ## ๐Ÿ”ง Configuration Options
51
+
52
+ | Option | Type | Required | Notes |
53
+ |------|------|----------|-------|
54
+ | `textContainerClass` | `string` | โœ… Yes | Must match an existing DOM element |
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` |
69
+
70
+
71
+ ### ๐ŸŽฌ Start Modes
72
+
73
+ - random โ€“ particles spawn at random positions
74
+
75
+ - left โ€“ particles animate in from the left
76
+
77
+ - center โ€“ particles animate from the center
78
+
79
+ - bottom โ€“ particles animate from below
80
+
81
+ ### ๐Ÿงน Cleanup
82
+
83
+ To remove the canvas, animation loop, and event listeners:
84
+
85
+ const magicText = initializeText({ ... });
86
+
87
+ // later
88
+ magicText.destroy();
89
+
90
+
91
+ ### ๐Ÿ“ฑ Mobile Support
92
+
93
+ - Touch events supported
94
+
95
+ - Optimized interaction radius for performance
96
+
97
+ - Separate mobile font size configuration
98
+
99
+ ### ๐Ÿ“„ License
100
+
101
+ MIT License
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@luay_abbas/magic-text",
3
+ "version": "1.0.0",
4
+ "description": "An interactive particle-based text animation library using HTML5 Canvas.",
5
+ "main": "src/index.js",
6
+ "files": [
7
+ "dist/",
8
+ "src/",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "scripts": {
13
+ "build": "webpack --mode production",
14
+ "build:dev": "webpack --mode development"
15
+ },
16
+ "keywords": [
17
+ "magic-text",
18
+ "canvas",
19
+ "particle-text",
20
+ "text-animation",
21
+ "javascript",
22
+ "interactive",
23
+ "hover-effect"
24
+ ],
25
+ "author": "Luay Abbas",
26
+ "license": "MIT",
27
+ "sideEffects": false
28
+ }
package/src/index.js ADDED
@@ -0,0 +1,250 @@
1
+ // Initialize Magic Text
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,
14
+ colorOne,
15
+ colorTwo,
16
+ colorThree,
17
+ startMode,
18
+ }) {
19
+ // App values
20
+ const gap = 1;
21
+ let animationId;
22
+ let destroyed = false;
23
+ const particles = [];
24
+ const mouse = {
25
+ x: null,
26
+ y: null,
27
+ radius: effectRadius,
28
+ };
29
+ // Check device
30
+ if (typeof window === "undefined") return;
31
+ const isMobile =
32
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
33
+ navigator.userAgent,
34
+ );
35
+
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");
42
+ const canvas = document.createElement("canvas");
43
+ canvas.classList.add("magic-text-canvas");
44
+ textContainer.appendChild(canvas);
45
+ 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
+
52
+ // Create particles from text
53
+ class Particle {
54
+ constructor(ctx, x, y, color, startMode) {
55
+ this.ctx = ctx;
56
+ this.originX = x;
57
+ this.originY = y;
58
+ const start = getStartPosition(startMode, x, y);
59
+ this.x = start.x;
60
+ this.y = start.y;
61
+
62
+ this.color = color;
63
+ this.baseColor = color;
64
+ this.secondColor = effectColor || "#0088ff";
65
+ this.size = gap;
66
+ this.ease = Math.random() * 0.1 + (duration || 0.05);
67
+ this.pushX = 0;
68
+ this.pushY = 0;
69
+ this.friction = 0.9;
70
+ }
71
+
72
+ update() {
73
+ let isHovering = false;
74
+ if (mouse.x !== null) {
75
+ const dx = this.x - mouse.x;
76
+ 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;
81
+ const angle = Math.atan2(dy, dx);
82
+ this.pushX += Math.cos(angle) * force * 7 * this.ease * 5;
83
+ 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
+ ) {
90
+ this.color = this.secondColor;
91
+ }
92
+ }
93
+ }
94
+
95
+ if (!isHovering) {
96
+ this.color = this.baseColor;
97
+ }
98
+ // Apply push force
99
+ this.pushX *= this.friction;
100
+ this.pushY *= this.friction;
101
+
102
+ this.x += this.pushX;
103
+ this.y += this.pushY;
104
+
105
+ // Ease back to original text position
106
+ this.x += (this.originX - this.x) * this.ease;
107
+ this.y += (this.originY - this.y) * this.ease;
108
+ }
109
+
110
+ draw() {
111
+ this.ctx.fillStyle = this.color;
112
+ this.ctx.fillRect(this.x, this.y, this.size, this.size);
113
+ }
114
+ }
115
+ // Draw Text on Canvas
116
+ function drawText() {
117
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
118
+ const fontFamily = "Arial Black, sans-serif";
119
+ const activeFontSize = isMobile ? fontSizeMobile : fontSize;
120
+ ctx.font = `italic bold ${activeFontSize}px ${fontFamily}`;
121
+ ctx.textAlign = "center";
122
+ ctx.textBaseline = "middle";
123
+ if (gradient && colorOne && colorTwo && colorThree) {
124
+ const grad = ctx.createLinearGradient(0, 0, canvasWidth, canvasHeight);
125
+ grad.addColorStop(0.3, colorOne);
126
+ grad.addColorStop(0.5, colorTwo);
127
+ grad.addColorStop(0.8, colorThree);
128
+ ctx.fillStyle = grad;
129
+ } else {
130
+ ctx.fillStyle = textColor || "#000000";
131
+ }
132
+ ctx.fillText(text || "Magic Text", canvasWidth / 2, canvasHeight / 2);
133
+ }
134
+ function createParticlesFromText() {
135
+ mouse.radius = effectRadius || 80;
136
+ if (isMobile && mouse.radius > 100) {
137
+ mouse.radius = 100;
138
+ }
139
+ particles.length = 0;
140
+ startMode = startMode || "random";
141
+ 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));
155
+ }
156
+ }
157
+ }
158
+ }
159
+ // Animate
160
+ function animate() {
161
+ if (destroyed) return;
162
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
163
+ particles.forEach((particle) => {
164
+ particle.update();
165
+ particle.draw();
166
+ });
167
+
168
+ animationId = requestAnimationFrame(animate);
169
+ }
170
+ // Modes
171
+ function getStartPosition(mode, originX, originY) {
172
+ switch (mode) {
173
+ case "center":
174
+ return {
175
+ x: canvasWidth / 2,
176
+ y: canvasHeight / 2,
177
+ };
178
+ case "bottom":
179
+ return {
180
+ x: originX,
181
+ y: canvasHeight + 20,
182
+ };
183
+
184
+ case "left":
185
+ return {
186
+ x: -20,
187
+ y: originY,
188
+ };
189
+
190
+ case "random":
191
+ default:
192
+ return {
193
+ x: Math.random() * canvasWidth,
194
+ y: Math.random() * canvasHeight,
195
+ };
196
+ }
197
+ }
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) {
215
+ const rect = canvas.getBoundingClientRect();
216
+ const touch = e.touches[0];
217
+ mouse.x = touch.clientX - rect.left;
218
+ mouse.y = touch.clientY - rect.top;
219
+ }
220
+ function onTouchEnd() {
221
+ mouse.x = null;
222
+ mouse.y = null;
223
+ }
224
+
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
231
+ function destroy() {
232
+ if (destroyed) return;
233
+ 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
+
242
+ canvas.remove();
243
+ particles.length = 0;
244
+ }
245
+ // Initial Setup
246
+ createParticlesFromText();
247
+ animate();
248
+ return { destroy };
249
+ }
250
+ export { initializeText };