@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.
- package/README.md +101 -0
- package/package.json +28 -0
- 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 };
|