masoneffect 0.1.18 → 0.1.20
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 +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +1 -0
- package/dist/react/MasonEffect.cjs +1 -526
- package/dist/react/MasonEffect.d.ts.map +1 -1
- package/dist/react/MasonEffect.mjs +1 -0
- package/dist/react/core/index.d.ts +0 -1
- package/dist/react/core/index.d.ts.map +1 -1
- package/dist/react/index.d.ts +2 -8
- package/dist/react/index.mjs +1 -0
- package/dist/react/index.umd.d.ts +0 -1
- package/dist/react/react/MasonEffect.d.ts +0 -1
- package/dist/react/react/MasonEffect.d.ts.map +1 -1
- package/dist/react/react/index.d.ts +2 -0
- package/dist/react/react/index.d.ts.map +1 -0
- package/dist/react/vue/index.d.ts +5 -0
- package/dist/react/vue/index.d.ts.map +1 -0
- package/dist/vue/index.cjs +1 -0
- package/dist/vue/index.d.ts +90 -0
- package/{src/core/index.ts → dist/vue/index.mjs} +227 -237
- package/package.json +14 -15
- package/dist/index.esm.js +0 -387
- package/dist/index.esm.js.map +0 -1
- package/dist/index.esm.min.js +0 -1
- package/dist/index.js +0 -392
- package/dist/index.js.map +0 -1
- package/dist/index.min.js +0 -1
- package/dist/index.umd.js +0 -396
- package/dist/index.umd.js.map +0 -1
- package/dist/index.umd.min.js +0 -1
- package/dist/react/MasonEffect.cjs.map +0 -1
- package/dist/react/MasonEffect.js +0 -524
- package/dist/react/MasonEffect.js.map +0 -1
- package/dist/react/MasonEffect.min.cjs +0 -1
- package/dist/react/MasonEffect.min.js +0 -1
- package/src/index.ts +0 -14
- package/src/index.umd.ts +0 -10
- package/src/react/MasonEffect.tsx +0 -228
- package/src/react/index.js +0 -2
- package/src/vue/MasonEffect.vue +0 -140
|
@@ -1,46 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export interface MasonEffectOptions {
|
|
7
|
-
text?: string;
|
|
8
|
-
densityStep?: number;
|
|
9
|
-
maxParticles?: number;
|
|
10
|
-
pointSize?: number;
|
|
11
|
-
ease?: number;
|
|
12
|
-
repelRadius?: number;
|
|
13
|
-
repelStrength?: number;
|
|
14
|
-
particleColor?: string;
|
|
15
|
-
fontFamily?: string;
|
|
16
|
-
fontSize?: number | null;
|
|
17
|
-
width?: number | null;
|
|
18
|
-
height?: number | null;
|
|
19
|
-
devicePixelRatio?: number | null;
|
|
20
|
-
debounceDelay?: number;
|
|
21
|
-
onReady?: (instance: MasonEffect) => void;
|
|
22
|
-
onUpdate?: (instance: MasonEffect) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface Particle {
|
|
26
|
-
x: number;
|
|
27
|
-
y: number;
|
|
28
|
-
vx: number;
|
|
29
|
-
vy: number;
|
|
30
|
-
tx: number;
|
|
31
|
-
ty: number;
|
|
32
|
-
initialX?: number;
|
|
33
|
-
initialY?: number;
|
|
34
|
-
j: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 디바운스 유틸리티 함수
|
|
38
|
-
function debounce<T extends (...args: any[]) => any>(
|
|
39
|
-
func: T,
|
|
40
|
-
wait: number
|
|
41
|
-
): (...args: Parameters<T>) => void {
|
|
42
|
-
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
43
|
-
return function executedFunction(...args: Parameters<T>) {
|
|
1
|
+
import { defineComponent, ref, watch, onMounted, onBeforeUnmount, createElementBlock, openBlock, normalizeStyle, normalizeClass } from "vue";
|
|
2
|
+
function debounce(func, wait) {
|
|
3
|
+
let timeout = null;
|
|
4
|
+
return function executedFunction(...args) {
|
|
44
5
|
const later = () => {
|
|
45
6
|
timeout = null;
|
|
46
7
|
func.apply(this, args);
|
|
@@ -51,78 +12,43 @@ function debounce<T extends (...args: any[]) => any>(
|
|
|
51
12
|
timeout = setTimeout(later, wait);
|
|
52
13
|
};
|
|
53
14
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
config: Required<Omit<MasonEffectOptions, 'onReady' | 'onUpdate' | 'debounceDelay'>> & {
|
|
58
|
-
onReady: MasonEffectOptions['onReady'];
|
|
59
|
-
onUpdate: MasonEffectOptions['onUpdate'];
|
|
60
|
-
};
|
|
61
|
-
canvas: HTMLCanvasElement;
|
|
62
|
-
ctx: CanvasRenderingContext2D;
|
|
63
|
-
offCanvas: HTMLCanvasElement;
|
|
64
|
-
offCtx: CanvasRenderingContext2D;
|
|
65
|
-
W: number;
|
|
66
|
-
H: number;
|
|
67
|
-
DPR: number;
|
|
68
|
-
particles: Particle[];
|
|
69
|
-
mouse: { x: number; y: number; down: boolean };
|
|
70
|
-
animationId: number | null;
|
|
71
|
-
isRunning: boolean;
|
|
72
|
-
isVisible: boolean;
|
|
73
|
-
intersectionObserver: IntersectionObserver | null;
|
|
74
|
-
debounceDelay: number;
|
|
75
|
-
_debouncedMorph!: (textOrOptions?: string | Partial<MasonEffectOptions> | null) => void;
|
|
76
|
-
_debouncedUpdateConfig!: (newConfig: Partial<MasonEffectOptions>) => void;
|
|
77
|
-
|
|
78
|
-
constructor(container: HTMLElement | string, options: MasonEffectOptions = {}) {
|
|
79
|
-
// 컨테이너 요소
|
|
80
|
-
this.container = typeof container === 'string'
|
|
81
|
-
? document.querySelector(container) as HTMLElement
|
|
82
|
-
: container;
|
|
83
|
-
|
|
15
|
+
class MasonEffect {
|
|
16
|
+
constructor(container, options = {}) {
|
|
17
|
+
this.container = typeof container === "string" ? document.querySelector(container) : container;
|
|
84
18
|
if (!this.container) {
|
|
85
|
-
throw new Error(
|
|
19
|
+
throw new Error("Container element not found");
|
|
86
20
|
}
|
|
87
|
-
|
|
88
|
-
// 설정값들
|
|
89
21
|
this.config = {
|
|
90
|
-
text: options.text ||
|
|
22
|
+
text: options.text || "mason effect",
|
|
91
23
|
densityStep: options.densityStep ?? 2,
|
|
92
24
|
maxParticles: options.maxParticles ?? 3200,
|
|
93
25
|
pointSize: options.pointSize ?? 0.5,
|
|
94
26
|
ease: options.ease ?? 0.05,
|
|
95
27
|
repelRadius: options.repelRadius ?? 150,
|
|
96
28
|
repelStrength: options.repelStrength ?? 1,
|
|
97
|
-
particleColor: options.particleColor ||
|
|
98
|
-
fontFamily: options.fontFamily ||
|
|
29
|
+
particleColor: options.particleColor || "#fff",
|
|
30
|
+
fontFamily: options.fontFamily || "Inter, system-ui, Arial",
|
|
99
31
|
fontSize: options.fontSize || null,
|
|
100
32
|
width: options.width || null,
|
|
101
33
|
height: options.height || null,
|
|
102
34
|
devicePixelRatio: options.devicePixelRatio ?? null,
|
|
103
35
|
onReady: options.onReady || null,
|
|
104
|
-
onUpdate: options.onUpdate || null
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.canvas = document.createElement('canvas');
|
|
109
|
-
const ctx = this.canvas.getContext('2d');
|
|
36
|
+
onUpdate: options.onUpdate || null
|
|
37
|
+
};
|
|
38
|
+
this.canvas = document.createElement("canvas");
|
|
39
|
+
const ctx = this.canvas.getContext("2d");
|
|
110
40
|
if (!ctx) {
|
|
111
|
-
throw new Error(
|
|
41
|
+
throw new Error("Canvas context not available");
|
|
112
42
|
}
|
|
113
43
|
this.ctx = ctx;
|
|
114
44
|
this.container.appendChild(this.canvas);
|
|
115
|
-
this.canvas.style.display =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.offCanvas = document.createElement('canvas');
|
|
119
|
-
const offCtx = this.offCanvas.getContext('2d');
|
|
45
|
+
this.canvas.style.display = "block";
|
|
46
|
+
this.offCanvas = document.createElement("canvas");
|
|
47
|
+
const offCtx = this.offCanvas.getContext("2d");
|
|
120
48
|
if (!offCtx) {
|
|
121
|
-
throw new Error(
|
|
49
|
+
throw new Error("Offscreen canvas context not available");
|
|
122
50
|
}
|
|
123
51
|
this.offCtx = offCtx;
|
|
124
|
-
|
|
125
|
-
// 상태
|
|
126
52
|
this.W = 0;
|
|
127
53
|
this.H = 0;
|
|
128
54
|
this.DPR = this.config.devicePixelRatio || Math.min(window.devicePixelRatio || 1, 1.8);
|
|
@@ -132,54 +58,38 @@ export class MasonEffect {
|
|
|
132
58
|
this.isRunning = false;
|
|
133
59
|
this.isVisible = false;
|
|
134
60
|
this.intersectionObserver = null;
|
|
135
|
-
|
|
136
|
-
// 디바운스 설정 (ms)
|
|
137
61
|
this.debounceDelay = options.debounceDelay ?? 150;
|
|
138
|
-
|
|
139
|
-
// 이벤트 핸들러 바인딩 (디바운스 적용 전에 바인딩)
|
|
140
62
|
const boundHandleResize = this.handleResize.bind(this);
|
|
141
63
|
this.handleResize = debounce(boundHandleResize, this.debounceDelay);
|
|
142
64
|
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
143
65
|
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
|
144
66
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
145
67
|
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
146
|
-
|
|
147
|
-
// morph와 updateConfig를 위한 디바운스된 내부 메서드
|
|
148
68
|
this._debouncedMorph = debounce(this._morphInternal.bind(this), this.debounceDelay);
|
|
149
69
|
this._debouncedUpdateConfig = debounce(this._updateConfigInternal.bind(this), this.debounceDelay);
|
|
150
|
-
|
|
151
|
-
// 초기화
|
|
152
70
|
this.init();
|
|
153
71
|
}
|
|
154
|
-
|
|
155
|
-
init(): void {
|
|
72
|
+
init() {
|
|
156
73
|
this.resize();
|
|
157
74
|
this.setupEventListeners();
|
|
158
75
|
this.setupIntersectionObserver();
|
|
159
|
-
|
|
160
76
|
if (this.config.onReady) {
|
|
161
77
|
this.config.onReady(this);
|
|
162
78
|
}
|
|
163
79
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// IntersectionObserver가 지원되지 않는 환경에서는 항상 재생
|
|
167
|
-
if (typeof window === 'undefined' || typeof window.IntersectionObserver === 'undefined') {
|
|
80
|
+
setupIntersectionObserver() {
|
|
81
|
+
if (typeof window === "undefined" || typeof window.IntersectionObserver === "undefined") {
|
|
168
82
|
this.isVisible = true;
|
|
169
83
|
this.start();
|
|
170
84
|
return;
|
|
171
85
|
}
|
|
172
|
-
|
|
173
|
-
// 이미 설정되어 있다면 다시 만들지 않음
|
|
174
86
|
if (this.intersectionObserver) {
|
|
175
87
|
return;
|
|
176
88
|
}
|
|
177
|
-
|
|
178
89
|
this.intersectionObserver = new IntersectionObserver(
|
|
179
90
|
(entries) => {
|
|
180
91
|
for (const entry of entries) {
|
|
181
92
|
if (entry.target !== this.container) continue;
|
|
182
|
-
|
|
183
93
|
if (entry.isIntersecting) {
|
|
184
94
|
this.isVisible = true;
|
|
185
95
|
this.start();
|
|
@@ -190,75 +100,74 @@ export class MasonEffect {
|
|
|
190
100
|
}
|
|
191
101
|
},
|
|
192
102
|
{
|
|
193
|
-
threshold: 0.1
|
|
103
|
+
threshold: 0.1
|
|
104
|
+
// 10% 이상 보일 때 동작
|
|
194
105
|
}
|
|
195
106
|
);
|
|
196
|
-
|
|
197
107
|
this.intersectionObserver.observe(this.container);
|
|
198
108
|
}
|
|
199
|
-
|
|
200
|
-
resize(): void {
|
|
109
|
+
resize() {
|
|
201
110
|
const width = this.config.width || this.container.clientWidth || window.innerWidth;
|
|
202
111
|
const height = this.config.height || this.container.clientHeight || window.innerHeight * 0.7;
|
|
203
|
-
|
|
112
|
+
if (width <= 0 || height <= 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
204
115
|
this.W = Math.floor(width * this.DPR);
|
|
205
116
|
this.H = Math.floor(height * this.DPR);
|
|
206
|
-
|
|
117
|
+
const MAX_CANVAS_SIZE = 4096;
|
|
118
|
+
if (this.W > MAX_CANVAS_SIZE || this.H > MAX_CANVAS_SIZE) {
|
|
119
|
+
const scale = Math.min(MAX_CANVAS_SIZE / this.W, MAX_CANVAS_SIZE / this.H);
|
|
120
|
+
this.W = Math.floor(this.W * scale);
|
|
121
|
+
this.H = Math.floor(this.H * scale);
|
|
122
|
+
this.DPR = this.DPR * scale;
|
|
123
|
+
}
|
|
207
124
|
this.canvas.width = this.W;
|
|
208
125
|
this.canvas.height = this.H;
|
|
209
|
-
this.canvas.style.width = width +
|
|
210
|
-
this.canvas.style.height = height +
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
126
|
+
this.canvas.style.width = width + "px";
|
|
127
|
+
this.canvas.style.height = height + "px";
|
|
128
|
+
if (this.W > 0 && this.H > 0) {
|
|
129
|
+
this.buildTargets();
|
|
130
|
+
if (!this.particles.length) {
|
|
131
|
+
this.initParticles();
|
|
132
|
+
}
|
|
215
133
|
}
|
|
216
134
|
}
|
|
217
|
-
|
|
218
|
-
|
|
135
|
+
buildTargets() {
|
|
136
|
+
if (this.W <= 0 || this.H <= 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
219
139
|
const text = this.config.text;
|
|
220
140
|
this.offCanvas.width = this.W;
|
|
221
141
|
this.offCanvas.height = this.H;
|
|
222
142
|
this.offCtx.clearRect(0, 0, this.offCanvas.width, this.offCanvas.height);
|
|
223
|
-
|
|
224
143
|
const base = Math.min(this.W, this.H);
|
|
225
144
|
const fontSize = this.config.fontSize || Math.max(80, Math.floor(base * 0.18));
|
|
226
|
-
this.offCtx.fillStyle =
|
|
227
|
-
this.offCtx.textAlign =
|
|
228
|
-
this.offCtx.textBaseline =
|
|
145
|
+
this.offCtx.fillStyle = "#ffffff";
|
|
146
|
+
this.offCtx.textAlign = "center";
|
|
147
|
+
this.offCtx.textBaseline = "middle";
|
|
229
148
|
this.offCtx.font = `400 ${fontSize}px ${this.config.fontFamily}`;
|
|
230
|
-
|
|
231
|
-
// 글자 간격 계산 및 그리기
|
|
232
|
-
const chars = text.split('');
|
|
149
|
+
const chars = text.split("");
|
|
233
150
|
const spacing = fontSize * 0.05;
|
|
234
151
|
const totalWidth = this.offCtx.measureText(text).width + spacing * (chars.length - 1);
|
|
235
152
|
let x = this.W / 2 - totalWidth / 2;
|
|
236
|
-
|
|
237
153
|
for (const ch of chars) {
|
|
238
154
|
this.offCtx.fillText(ch, x + this.offCtx.measureText(ch).width / 2, this.H / 2);
|
|
239
155
|
x += this.offCtx.measureText(ch).width + spacing;
|
|
240
156
|
}
|
|
241
|
-
|
|
242
|
-
// 픽셀 샘플링
|
|
243
157
|
const step = Math.max(2, this.config.densityStep);
|
|
244
158
|
const img = this.offCtx.getImageData(0, 0, this.W, this.H).data;
|
|
245
|
-
const targets
|
|
246
|
-
|
|
159
|
+
const targets = [];
|
|
247
160
|
for (let y = 0; y < this.H; y += step) {
|
|
248
|
-
for (let
|
|
249
|
-
const i = (y * this.W +
|
|
161
|
+
for (let x2 = 0; x2 < this.W; x2 += step) {
|
|
162
|
+
const i = (y * this.W + x2) * 4;
|
|
250
163
|
if (img[i] + img[i + 1] + img[i + 2] > 600) {
|
|
251
|
-
targets.push({ x, y });
|
|
164
|
+
targets.push({ x: x2, y });
|
|
252
165
|
}
|
|
253
166
|
}
|
|
254
167
|
}
|
|
255
|
-
|
|
256
|
-
// 파티클 수 제한
|
|
257
168
|
while (targets.length > this.config.maxParticles) {
|
|
258
169
|
targets.splice(Math.floor(Math.random() * targets.length), 1);
|
|
259
170
|
}
|
|
260
|
-
|
|
261
|
-
// 파티클 수 조정
|
|
262
171
|
if (this.particles.length < targets.length) {
|
|
263
172
|
const need = targets.length - this.particles.length;
|
|
264
173
|
for (let i = 0; i < need; i++) {
|
|
@@ -267,8 +176,6 @@ export class MasonEffect {
|
|
|
267
176
|
} else if (this.particles.length > targets.length) {
|
|
268
177
|
this.particles.length = targets.length;
|
|
269
178
|
}
|
|
270
|
-
|
|
271
|
-
// 목표 좌표 할당
|
|
272
179
|
for (let i = 0; i < this.particles.length; i++) {
|
|
273
180
|
const p = this.particles[i];
|
|
274
181
|
const t = targets[i];
|
|
@@ -276,9 +183,7 @@ export class MasonEffect {
|
|
|
276
183
|
p.ty = t.y;
|
|
277
184
|
}
|
|
278
185
|
}
|
|
279
|
-
|
|
280
|
-
makeParticle(): Particle {
|
|
281
|
-
// 캔버스 전체에 골고루 분포 (여백 없이)
|
|
186
|
+
makeParticle() {
|
|
282
187
|
const sx = Math.random() * this.W;
|
|
283
188
|
const sy = Math.random() * this.H;
|
|
284
189
|
return {
|
|
@@ -288,35 +193,29 @@ export class MasonEffect {
|
|
|
288
193
|
vy: 0,
|
|
289
194
|
tx: sx,
|
|
290
195
|
ty: sy,
|
|
291
|
-
initialX: sx,
|
|
196
|
+
initialX: sx,
|
|
197
|
+
// 초기 위치 저장 (scatter 시 돌아갈 위치)
|
|
292
198
|
initialY: sy,
|
|
293
|
-
j: Math.random() * Math.PI * 2
|
|
199
|
+
j: Math.random() * Math.PI * 2
|
|
294
200
|
};
|
|
295
201
|
}
|
|
296
|
-
|
|
297
|
-
initParticles(): void {
|
|
298
|
-
// 캔버스 전체에 골고루 분포 (여백 없이)
|
|
202
|
+
initParticles() {
|
|
299
203
|
for (const p of this.particles) {
|
|
300
204
|
const sx = Math.random() * this.W;
|
|
301
205
|
const sy = Math.random() * this.H;
|
|
302
206
|
p.x = sx;
|
|
303
207
|
p.y = sy;
|
|
304
208
|
p.vx = p.vy = 0;
|
|
305
|
-
// 초기 위치 저장 (scatter 시 돌아갈 위치)
|
|
306
209
|
p.initialX = sx;
|
|
307
210
|
p.initialY = sy;
|
|
308
211
|
}
|
|
309
212
|
}
|
|
310
|
-
|
|
311
|
-
scatter(): void {
|
|
312
|
-
// 각 파티클을 초기 위치로 돌아가도록 설정
|
|
213
|
+
scatter() {
|
|
313
214
|
for (const p of this.particles) {
|
|
314
|
-
|
|
315
|
-
if (p.initialX !== undefined && p.initialY !== undefined) {
|
|
215
|
+
if (p.initialX !== void 0 && p.initialY !== void 0) {
|
|
316
216
|
p.tx = p.initialX;
|
|
317
217
|
p.ty = p.initialY;
|
|
318
218
|
} else {
|
|
319
|
-
// 초기 위치가 없으면 현재 위치를 초기 위치로 저장
|
|
320
219
|
p.initialX = p.x;
|
|
321
220
|
p.initialY = p.y;
|
|
322
221
|
p.tx = p.initialX;
|
|
@@ -324,71 +223,51 @@ export class MasonEffect {
|
|
|
324
223
|
}
|
|
325
224
|
}
|
|
326
225
|
}
|
|
327
|
-
|
|
328
|
-
morph(textOrOptions?: string | Partial<MasonEffectOptions> | null): void {
|
|
329
|
-
// 즉시 실행이 필요한 경우 (예: 초기화 시)를 위해 내부 메서드 직접 호출
|
|
330
|
-
// 일반적인 경우에는 디바운스 적용
|
|
226
|
+
morph(textOrOptions) {
|
|
331
227
|
this._debouncedMorph(textOrOptions);
|
|
332
228
|
}
|
|
333
|
-
|
|
334
|
-
_morphInternal(textOrOptions?: string | Partial<MasonEffectOptions> | null): void {
|
|
335
|
-
// W와 H가 0이면 resize 먼저 실행
|
|
229
|
+
_morphInternal(textOrOptions) {
|
|
336
230
|
if (this.W === 0 || this.H === 0) {
|
|
337
231
|
this.resize();
|
|
338
232
|
}
|
|
339
|
-
|
|
340
|
-
if (typeof textOrOptions === 'string') {
|
|
341
|
-
// 문자열인 경우: 기존 동작 유지
|
|
233
|
+
if (typeof textOrOptions === "string") {
|
|
342
234
|
this.config.text = textOrOptions;
|
|
343
235
|
this.buildTargets();
|
|
344
|
-
} else if (textOrOptions && typeof textOrOptions ===
|
|
345
|
-
|
|
346
|
-
const needsRebuild = textOrOptions.text !== undefined;
|
|
236
|
+
} else if (textOrOptions && typeof textOrOptions === "object") {
|
|
237
|
+
const needsRebuild = textOrOptions.text !== void 0;
|
|
347
238
|
this.config = { ...this.config, ...textOrOptions };
|
|
348
239
|
if (needsRebuild) {
|
|
349
240
|
this.buildTargets();
|
|
350
241
|
}
|
|
351
242
|
} else {
|
|
352
|
-
// null이거나 undefined인 경우: 현재 텍스트로 재빌드
|
|
353
243
|
this.buildTargets();
|
|
354
244
|
}
|
|
355
245
|
}
|
|
356
|
-
|
|
357
|
-
update(): void {
|
|
246
|
+
update() {
|
|
358
247
|
this.ctx.clearRect(0, 0, this.W, this.H);
|
|
359
|
-
|
|
360
248
|
for (const p of this.particles) {
|
|
361
|
-
// 목표 좌표로 당기는 힘
|
|
362
249
|
let ax = (p.tx - p.x) * this.config.ease;
|
|
363
250
|
let ay = (p.ty - p.y) * this.config.ease;
|
|
364
|
-
|
|
365
|
-
// 마우스 반발/흡입
|
|
366
251
|
if (this.mouse.x || this.mouse.y) {
|
|
367
252
|
const dx = p.x - this.mouse.x;
|
|
368
253
|
const dy = p.y - this.mouse.y;
|
|
369
254
|
const d2 = dx * dx + dy * dy;
|
|
370
|
-
const
|
|
371
|
-
if (d2 <
|
|
372
|
-
const d = Math.sqrt(d2) +
|
|
373
|
-
const f = (this.mouse.down ? -1 : 1) * this.config.repelStrength * (1 - d /
|
|
374
|
-
ax +=
|
|
375
|
-
ay +=
|
|
255
|
+
const r2 = this.config.repelRadius * this.DPR;
|
|
256
|
+
if (d2 < r2 * r2) {
|
|
257
|
+
const d = Math.sqrt(d2) + 1e-4;
|
|
258
|
+
const f = (this.mouse.down ? -1 : 1) * this.config.repelStrength * (1 - d / r2);
|
|
259
|
+
ax += dx / d * f * 6;
|
|
260
|
+
ay += dy / d * f * 6;
|
|
376
261
|
}
|
|
377
262
|
}
|
|
378
|
-
|
|
379
|
-
// 진동 효과
|
|
380
263
|
p.j += 2;
|
|
381
264
|
ax += Math.cos(p.j) * 0.05;
|
|
382
265
|
ay += Math.sin(p.j * 1.3) * 0.05;
|
|
383
|
-
|
|
384
|
-
// 속도와 위치 업데이트
|
|
385
266
|
p.vx = (p.vx + ax) * Math.random();
|
|
386
267
|
p.vy = (p.vy + ay) * Math.random();
|
|
387
268
|
p.x += p.vx;
|
|
388
269
|
p.y += p.vy;
|
|
389
270
|
}
|
|
390
|
-
|
|
391
|
-
// 파티클 그리기
|
|
392
271
|
this.ctx.fillStyle = this.config.particleColor;
|
|
393
272
|
const r = this.config.pointSize * this.DPR;
|
|
394
273
|
for (const p of this.particles) {
|
|
@@ -396,85 +275,70 @@ export class MasonEffect {
|
|
|
396
275
|
this.ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
|
|
397
276
|
this.ctx.fill();
|
|
398
277
|
}
|
|
399
|
-
|
|
400
278
|
if (this.config.onUpdate) {
|
|
401
279
|
this.config.onUpdate(this);
|
|
402
280
|
}
|
|
403
281
|
}
|
|
404
|
-
|
|
405
|
-
animate(): void {
|
|
282
|
+
animate() {
|
|
406
283
|
if (!this.isRunning) return;
|
|
407
284
|
this.update();
|
|
408
285
|
this.animationId = requestAnimationFrame(() => this.animate());
|
|
409
286
|
}
|
|
410
|
-
|
|
411
|
-
start(): void {
|
|
287
|
+
start() {
|
|
412
288
|
if (this.isRunning) return;
|
|
413
289
|
this.isRunning = true;
|
|
414
290
|
this.animate();
|
|
415
291
|
}
|
|
416
|
-
|
|
417
|
-
stop(): void {
|
|
292
|
+
stop() {
|
|
418
293
|
this.isRunning = false;
|
|
419
294
|
if (this.animationId) {
|
|
420
295
|
cancelAnimationFrame(this.animationId);
|
|
421
296
|
this.animationId = null;
|
|
422
297
|
}
|
|
423
298
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
this.canvas.addEventListener(
|
|
428
|
-
this.canvas.addEventListener(
|
|
429
|
-
|
|
430
|
-
window.addEventListener('mouseup', this.handleMouseUp);
|
|
299
|
+
setupEventListeners() {
|
|
300
|
+
window.addEventListener("resize", this.handleResize);
|
|
301
|
+
this.canvas.addEventListener("mousemove", this.handleMouseMove);
|
|
302
|
+
this.canvas.addEventListener("mouseleave", this.handleMouseLeave);
|
|
303
|
+
this.canvas.addEventListener("mousedown", this.handleMouseDown);
|
|
304
|
+
window.addEventListener("mouseup", this.handleMouseUp);
|
|
431
305
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
this.canvas.removeEventListener(
|
|
436
|
-
this.canvas.removeEventListener(
|
|
437
|
-
|
|
438
|
-
window.removeEventListener('mouseup', this.handleMouseUp);
|
|
306
|
+
removeEventListeners() {
|
|
307
|
+
window.removeEventListener("resize", this.handleResize);
|
|
308
|
+
this.canvas.removeEventListener("mousemove", this.handleMouseMove);
|
|
309
|
+
this.canvas.removeEventListener("mouseleave", this.handleMouseLeave);
|
|
310
|
+
this.canvas.removeEventListener("mousedown", this.handleMouseDown);
|
|
311
|
+
window.removeEventListener("mouseup", this.handleMouseUp);
|
|
439
312
|
}
|
|
440
|
-
|
|
441
|
-
handleResize(): void {
|
|
313
|
+
handleResize() {
|
|
442
314
|
this.resize();
|
|
443
315
|
}
|
|
444
|
-
|
|
445
|
-
handleMouseMove(e: MouseEvent): void {
|
|
316
|
+
handleMouseMove(e) {
|
|
446
317
|
const rect = this.canvas.getBoundingClientRect();
|
|
447
318
|
this.mouse.x = (e.clientX - rect.left) * this.DPR;
|
|
448
319
|
this.mouse.y = (e.clientY - rect.top) * this.DPR;
|
|
449
320
|
}
|
|
450
|
-
|
|
451
|
-
handleMouseLeave(): void {
|
|
321
|
+
handleMouseLeave() {
|
|
452
322
|
this.mouse.x = this.mouse.y = 0;
|
|
453
323
|
}
|
|
454
|
-
|
|
455
|
-
handleMouseDown(): void {
|
|
324
|
+
handleMouseDown() {
|
|
456
325
|
this.mouse.down = true;
|
|
457
326
|
}
|
|
458
|
-
|
|
459
|
-
handleMouseUp(): void {
|
|
327
|
+
handleMouseUp() {
|
|
460
328
|
this.mouse.down = false;
|
|
461
329
|
}
|
|
462
|
-
|
|
463
330
|
// 설정 업데이트
|
|
464
|
-
updateConfig(newConfig
|
|
465
|
-
// 디바운스 적용
|
|
331
|
+
updateConfig(newConfig) {
|
|
466
332
|
this._debouncedUpdateConfig(newConfig);
|
|
467
333
|
}
|
|
468
|
-
|
|
469
|
-
_updateConfigInternal(newConfig: Partial<MasonEffectOptions>): void {
|
|
334
|
+
_updateConfigInternal(newConfig) {
|
|
470
335
|
this.config = { ...this.config, ...newConfig };
|
|
471
336
|
if (newConfig.text) {
|
|
472
337
|
this.buildTargets();
|
|
473
338
|
}
|
|
474
339
|
}
|
|
475
|
-
|
|
476
340
|
// 파괴 및 정리
|
|
477
|
-
destroy()
|
|
341
|
+
destroy() {
|
|
478
342
|
this.stop();
|
|
479
343
|
this.removeEventListeners();
|
|
480
344
|
if (this.intersectionObserver) {
|
|
@@ -486,7 +350,133 @@ export class MasonEffect {
|
|
|
486
350
|
}
|
|
487
351
|
}
|
|
488
352
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
353
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
354
|
+
__name: "MasonEffect",
|
|
355
|
+
props: {
|
|
356
|
+
className: { default: "" },
|
|
357
|
+
style: { default: () => ({}) },
|
|
358
|
+
text: { default: "mason effect" },
|
|
359
|
+
densityStep: { default: 2 },
|
|
360
|
+
maxParticles: { default: 3200 },
|
|
361
|
+
pointSize: { default: 0.5 },
|
|
362
|
+
ease: { default: 0.05 },
|
|
363
|
+
repelRadius: { default: 150 },
|
|
364
|
+
repelStrength: { default: 1 },
|
|
365
|
+
particleColor: { default: "#fff" },
|
|
366
|
+
fontFamily: { default: "Inter, system-ui, Arial" },
|
|
367
|
+
fontSize: { default: null },
|
|
368
|
+
width: { default: null },
|
|
369
|
+
height: { default: null },
|
|
370
|
+
devicePixelRatio: { default: null },
|
|
371
|
+
debounceDelay: {},
|
|
372
|
+
onReady: { type: Function, default: null },
|
|
373
|
+
onUpdate: { type: Function, default: null }
|
|
374
|
+
},
|
|
375
|
+
emits: ["ready", "update"],
|
|
376
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
377
|
+
const props = __props;
|
|
378
|
+
const emit = __emit;
|
|
379
|
+
const container = ref(null);
|
|
380
|
+
let instance = null;
|
|
381
|
+
const init = () => {
|
|
382
|
+
if (!container.value) return;
|
|
383
|
+
const options = {
|
|
384
|
+
text: props.text,
|
|
385
|
+
densityStep: props.densityStep,
|
|
386
|
+
maxParticles: props.maxParticles,
|
|
387
|
+
pointSize: props.pointSize,
|
|
388
|
+
ease: props.ease,
|
|
389
|
+
repelRadius: props.repelRadius,
|
|
390
|
+
repelStrength: props.repelStrength,
|
|
391
|
+
particleColor: props.particleColor,
|
|
392
|
+
fontFamily: props.fontFamily,
|
|
393
|
+
fontSize: props.fontSize,
|
|
394
|
+
width: props.width,
|
|
395
|
+
height: props.height,
|
|
396
|
+
devicePixelRatio: props.devicePixelRatio,
|
|
397
|
+
onReady: (inst) => {
|
|
398
|
+
if (props.onReady) props.onReady(inst);
|
|
399
|
+
emit("ready", inst);
|
|
400
|
+
},
|
|
401
|
+
onUpdate: (inst) => {
|
|
402
|
+
if (props.onUpdate) props.onUpdate(inst);
|
|
403
|
+
emit("update", inst);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
instance = new MasonEffect(container.value, options);
|
|
407
|
+
};
|
|
408
|
+
watch(
|
|
409
|
+
() => [
|
|
410
|
+
props.text,
|
|
411
|
+
props.densityStep,
|
|
412
|
+
props.maxParticles,
|
|
413
|
+
props.pointSize,
|
|
414
|
+
props.ease,
|
|
415
|
+
props.repelRadius,
|
|
416
|
+
props.repelStrength,
|
|
417
|
+
props.particleColor,
|
|
418
|
+
props.fontFamily,
|
|
419
|
+
props.fontSize,
|
|
420
|
+
props.width,
|
|
421
|
+
props.height,
|
|
422
|
+
props.devicePixelRatio
|
|
423
|
+
],
|
|
424
|
+
() => {
|
|
425
|
+
if (instance) {
|
|
426
|
+
instance.updateConfig({
|
|
427
|
+
text: props.text,
|
|
428
|
+
densityStep: props.densityStep,
|
|
429
|
+
maxParticles: props.maxParticles,
|
|
430
|
+
pointSize: props.pointSize,
|
|
431
|
+
ease: props.ease,
|
|
432
|
+
repelRadius: props.repelRadius,
|
|
433
|
+
repelStrength: props.repelStrength,
|
|
434
|
+
particleColor: props.particleColor,
|
|
435
|
+
fontFamily: props.fontFamily,
|
|
436
|
+
fontSize: props.fontSize,
|
|
437
|
+
width: props.width,
|
|
438
|
+
height: props.height,
|
|
439
|
+
devicePixelRatio: props.devicePixelRatio
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
onMounted(() => {
|
|
445
|
+
init();
|
|
446
|
+
});
|
|
447
|
+
onBeforeUnmount(() => {
|
|
448
|
+
if (instance) {
|
|
449
|
+
instance.destroy();
|
|
450
|
+
instance = null;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
__expose({
|
|
454
|
+
morph: (textOrOptions) => {
|
|
455
|
+
if (instance) instance.morph(textOrOptions);
|
|
456
|
+
},
|
|
457
|
+
scatter: () => {
|
|
458
|
+
if (instance) instance.scatter();
|
|
459
|
+
},
|
|
460
|
+
updateConfig: (config) => {
|
|
461
|
+
if (instance) instance.updateConfig(config);
|
|
462
|
+
},
|
|
463
|
+
destroy: () => {
|
|
464
|
+
if (instance) {
|
|
465
|
+
instance.destroy();
|
|
466
|
+
instance = null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
return (_ctx, _cache) => {
|
|
471
|
+
return openBlock(), createElementBlock("div", {
|
|
472
|
+
ref_key: "container",
|
|
473
|
+
ref: container,
|
|
474
|
+
class: normalizeClass(__props.className),
|
|
475
|
+
style: normalizeStyle(__props.style)
|
|
476
|
+
}, null, 6);
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
export {
|
|
481
|
+
_sfc_main as default
|
|
482
|
+
};
|