masoneffect 0.1.19 → 0.1.21
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/dist/index.cjs +1 -0
- package/dist/index.mjs +1 -0
- package/dist/react/MasonEffect.cjs +1 -552
- package/dist/react/MasonEffect.d.ts.map +1 -1
- package/dist/react/MasonEffect.mjs +1 -0
- package/dist/react/index.cjs +7 -0
- package/dist/react/index.mjs +1 -0
- package/dist/react/react/MasonEffect.d.ts.map +1 -1
- 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} +209 -242
- package/package.json +16 -12
- package/dist/index.esm.js +0 -407
- package/dist/index.esm.js.map +0 -1
- package/dist/index.esm.min.js +0 -1
- package/dist/index.js +0 -412
- package/dist/index.js.map +0 -1
- package/dist/index.min.js +0 -1
- package/dist/index.umd.js +0 -416
- 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 -550
- 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/dist/react/index.js +0 -549
- package/src/index.ts +0 -14
- package/src/index.umd.ts +0 -10
- package/src/react/MasonEffect.tsx +0 -235
- package/src/react/index.ts +0 -4
- 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,27 +100,20 @@ 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
|
-
|
|
204
|
-
// 최소 크기 보장
|
|
205
112
|
if (width <= 0 || height <= 0) {
|
|
206
113
|
return;
|
|
207
114
|
}
|
|
208
|
-
|
|
209
115
|
this.W = Math.floor(width * this.DPR);
|
|
210
116
|
this.H = Math.floor(height * this.DPR);
|
|
211
|
-
|
|
212
|
-
// 캔버스 크기 제한 (메모리 오류 방지)
|
|
213
|
-
// getImageData는 최대 약 268MB (4096x4096x4)까지 지원
|
|
214
117
|
const MAX_CANVAS_SIZE = 4096;
|
|
215
118
|
if (this.W > MAX_CANVAS_SIZE || this.H > MAX_CANVAS_SIZE) {
|
|
216
119
|
const scale = Math.min(MAX_CANVAS_SIZE / this.W, MAX_CANVAS_SIZE / this.H);
|
|
@@ -218,13 +121,10 @@ export class MasonEffect {
|
|
|
218
121
|
this.H = Math.floor(this.H * scale);
|
|
219
122
|
this.DPR = this.DPR * scale;
|
|
220
123
|
}
|
|
221
|
-
|
|
222
124
|
this.canvas.width = this.W;
|
|
223
125
|
this.canvas.height = this.H;
|
|
224
|
-
this.canvas.style.width = width +
|
|
225
|
-
this.canvas.style.height = height +
|
|
226
|
-
|
|
227
|
-
// 크기가 유효할 때만 buildTargets 실행
|
|
126
|
+
this.canvas.style.width = width + "px";
|
|
127
|
+
this.canvas.style.height = height + "px";
|
|
228
128
|
if (this.W > 0 && this.H > 0) {
|
|
229
129
|
this.buildTargets();
|
|
230
130
|
if (!this.particles.length) {
|
|
@@ -232,56 +132,42 @@ export class MasonEffect {
|
|
|
232
132
|
}
|
|
233
133
|
}
|
|
234
134
|
}
|
|
235
|
-
|
|
236
|
-
buildTargets(): void {
|
|
237
|
-
// 크기 검증
|
|
135
|
+
buildTargets() {
|
|
238
136
|
if (this.W <= 0 || this.H <= 0) {
|
|
239
137
|
return;
|
|
240
138
|
}
|
|
241
|
-
|
|
242
139
|
const text = this.config.text;
|
|
243
140
|
this.offCanvas.width = this.W;
|
|
244
141
|
this.offCanvas.height = this.H;
|
|
245
142
|
this.offCtx.clearRect(0, 0, this.offCanvas.width, this.offCanvas.height);
|
|
246
|
-
|
|
247
143
|
const base = Math.min(this.W, this.H);
|
|
248
144
|
const fontSize = this.config.fontSize || Math.max(80, Math.floor(base * 0.18));
|
|
249
|
-
this.offCtx.fillStyle =
|
|
250
|
-
this.offCtx.textAlign =
|
|
251
|
-
this.offCtx.textBaseline =
|
|
145
|
+
this.offCtx.fillStyle = "#ffffff";
|
|
146
|
+
this.offCtx.textAlign = "center";
|
|
147
|
+
this.offCtx.textBaseline = "middle";
|
|
252
148
|
this.offCtx.font = `400 ${fontSize}px ${this.config.fontFamily}`;
|
|
253
|
-
|
|
254
|
-
// 글자 간격 계산 및 그리기
|
|
255
|
-
const chars = text.split('');
|
|
149
|
+
const chars = text.split("");
|
|
256
150
|
const spacing = fontSize * 0.05;
|
|
257
151
|
const totalWidth = this.offCtx.measureText(text).width + spacing * (chars.length - 1);
|
|
258
152
|
let x = this.W / 2 - totalWidth / 2;
|
|
259
|
-
|
|
260
153
|
for (const ch of chars) {
|
|
261
154
|
this.offCtx.fillText(ch, x + this.offCtx.measureText(ch).width / 2, this.H / 2);
|
|
262
155
|
x += this.offCtx.measureText(ch).width + spacing;
|
|
263
156
|
}
|
|
264
|
-
|
|
265
|
-
// 픽셀 샘플링
|
|
266
157
|
const step = Math.max(2, this.config.densityStep);
|
|
267
158
|
const img = this.offCtx.getImageData(0, 0, this.W, this.H).data;
|
|
268
|
-
const targets
|
|
269
|
-
|
|
159
|
+
const targets = [];
|
|
270
160
|
for (let y = 0; y < this.H; y += step) {
|
|
271
|
-
for (let
|
|
272
|
-
const i = (y * this.W +
|
|
161
|
+
for (let x2 = 0; x2 < this.W; x2 += step) {
|
|
162
|
+
const i = (y * this.W + x2) * 4;
|
|
273
163
|
if (img[i] + img[i + 1] + img[i + 2] > 600) {
|
|
274
|
-
targets.push({ x, y });
|
|
164
|
+
targets.push({ x: x2, y });
|
|
275
165
|
}
|
|
276
166
|
}
|
|
277
167
|
}
|
|
278
|
-
|
|
279
|
-
// 파티클 수 제한
|
|
280
168
|
while (targets.length > this.config.maxParticles) {
|
|
281
169
|
targets.splice(Math.floor(Math.random() * targets.length), 1);
|
|
282
170
|
}
|
|
283
|
-
|
|
284
|
-
// 파티클 수 조정
|
|
285
171
|
if (this.particles.length < targets.length) {
|
|
286
172
|
const need = targets.length - this.particles.length;
|
|
287
173
|
for (let i = 0; i < need; i++) {
|
|
@@ -290,8 +176,6 @@ export class MasonEffect {
|
|
|
290
176
|
} else if (this.particles.length > targets.length) {
|
|
291
177
|
this.particles.length = targets.length;
|
|
292
178
|
}
|
|
293
|
-
|
|
294
|
-
// 목표 좌표 할당
|
|
295
179
|
for (let i = 0; i < this.particles.length; i++) {
|
|
296
180
|
const p = this.particles[i];
|
|
297
181
|
const t = targets[i];
|
|
@@ -299,9 +183,7 @@ export class MasonEffect {
|
|
|
299
183
|
p.ty = t.y;
|
|
300
184
|
}
|
|
301
185
|
}
|
|
302
|
-
|
|
303
|
-
makeParticle(): Particle {
|
|
304
|
-
// 캔버스 전체에 골고루 분포 (여백 없이)
|
|
186
|
+
makeParticle() {
|
|
305
187
|
const sx = Math.random() * this.W;
|
|
306
188
|
const sy = Math.random() * this.H;
|
|
307
189
|
return {
|
|
@@ -311,35 +193,29 @@ export class MasonEffect {
|
|
|
311
193
|
vy: 0,
|
|
312
194
|
tx: sx,
|
|
313
195
|
ty: sy,
|
|
314
|
-
initialX: sx,
|
|
196
|
+
initialX: sx,
|
|
197
|
+
// 초기 위치 저장 (scatter 시 돌아갈 위치)
|
|
315
198
|
initialY: sy,
|
|
316
|
-
j: Math.random() * Math.PI * 2
|
|
199
|
+
j: Math.random() * Math.PI * 2
|
|
317
200
|
};
|
|
318
201
|
}
|
|
319
|
-
|
|
320
|
-
initParticles(): void {
|
|
321
|
-
// 캔버스 전체에 골고루 분포 (여백 없이)
|
|
202
|
+
initParticles() {
|
|
322
203
|
for (const p of this.particles) {
|
|
323
204
|
const sx = Math.random() * this.W;
|
|
324
205
|
const sy = Math.random() * this.H;
|
|
325
206
|
p.x = sx;
|
|
326
207
|
p.y = sy;
|
|
327
208
|
p.vx = p.vy = 0;
|
|
328
|
-
// 초기 위치 저장 (scatter 시 돌아갈 위치)
|
|
329
209
|
p.initialX = sx;
|
|
330
210
|
p.initialY = sy;
|
|
331
211
|
}
|
|
332
212
|
}
|
|
333
|
-
|
|
334
|
-
scatter(): void {
|
|
335
|
-
// 각 파티클을 초기 위치로 돌아가도록 설정
|
|
213
|
+
scatter() {
|
|
336
214
|
for (const p of this.particles) {
|
|
337
|
-
|
|
338
|
-
if (p.initialX !== undefined && p.initialY !== undefined) {
|
|
215
|
+
if (p.initialX !== void 0 && p.initialY !== void 0) {
|
|
339
216
|
p.tx = p.initialX;
|
|
340
217
|
p.ty = p.initialY;
|
|
341
218
|
} else {
|
|
342
|
-
// 초기 위치가 없으면 현재 위치를 초기 위치로 저장
|
|
343
219
|
p.initialX = p.x;
|
|
344
220
|
p.initialY = p.y;
|
|
345
221
|
p.tx = p.initialX;
|
|
@@ -347,71 +223,51 @@ export class MasonEffect {
|
|
|
347
223
|
}
|
|
348
224
|
}
|
|
349
225
|
}
|
|
350
|
-
|
|
351
|
-
morph(textOrOptions?: string | Partial<MasonEffectOptions> | null): void {
|
|
352
|
-
// 즉시 실행이 필요한 경우 (예: 초기화 시)를 위해 내부 메서드 직접 호출
|
|
353
|
-
// 일반적인 경우에는 디바운스 적용
|
|
226
|
+
morph(textOrOptions) {
|
|
354
227
|
this._debouncedMorph(textOrOptions);
|
|
355
228
|
}
|
|
356
|
-
|
|
357
|
-
_morphInternal(textOrOptions?: string | Partial<MasonEffectOptions> | null): void {
|
|
358
|
-
// W와 H가 0이면 resize 먼저 실행
|
|
229
|
+
_morphInternal(textOrOptions) {
|
|
359
230
|
if (this.W === 0 || this.H === 0) {
|
|
360
231
|
this.resize();
|
|
361
232
|
}
|
|
362
|
-
|
|
363
|
-
if (typeof textOrOptions === 'string') {
|
|
364
|
-
// 문자열인 경우: 기존 동작 유지
|
|
233
|
+
if (typeof textOrOptions === "string") {
|
|
365
234
|
this.config.text = textOrOptions;
|
|
366
235
|
this.buildTargets();
|
|
367
|
-
} else if (textOrOptions && typeof textOrOptions ===
|
|
368
|
-
|
|
369
|
-
const needsRebuild = textOrOptions.text !== undefined;
|
|
236
|
+
} else if (textOrOptions && typeof textOrOptions === "object") {
|
|
237
|
+
const needsRebuild = textOrOptions.text !== void 0;
|
|
370
238
|
this.config = { ...this.config, ...textOrOptions };
|
|
371
239
|
if (needsRebuild) {
|
|
372
240
|
this.buildTargets();
|
|
373
241
|
}
|
|
374
242
|
} else {
|
|
375
|
-
// null이거나 undefined인 경우: 현재 텍스트로 재빌드
|
|
376
243
|
this.buildTargets();
|
|
377
244
|
}
|
|
378
245
|
}
|
|
379
|
-
|
|
380
|
-
update(): void {
|
|
246
|
+
update() {
|
|
381
247
|
this.ctx.clearRect(0, 0, this.W, this.H);
|
|
382
|
-
|
|
383
248
|
for (const p of this.particles) {
|
|
384
|
-
// 목표 좌표로 당기는 힘
|
|
385
249
|
let ax = (p.tx - p.x) * this.config.ease;
|
|
386
250
|
let ay = (p.ty - p.y) * this.config.ease;
|
|
387
|
-
|
|
388
|
-
// 마우스 반발/흡입
|
|
389
251
|
if (this.mouse.x || this.mouse.y) {
|
|
390
252
|
const dx = p.x - this.mouse.x;
|
|
391
253
|
const dy = p.y - this.mouse.y;
|
|
392
254
|
const d2 = dx * dx + dy * dy;
|
|
393
|
-
const
|
|
394
|
-
if (d2 <
|
|
395
|
-
const d = Math.sqrt(d2) +
|
|
396
|
-
const f = (this.mouse.down ? -1 : 1) * this.config.repelStrength * (1 - d /
|
|
397
|
-
ax +=
|
|
398
|
-
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;
|
|
399
261
|
}
|
|
400
262
|
}
|
|
401
|
-
|
|
402
|
-
// 진동 효과
|
|
403
263
|
p.j += 2;
|
|
404
264
|
ax += Math.cos(p.j) * 0.05;
|
|
405
265
|
ay += Math.sin(p.j * 1.3) * 0.05;
|
|
406
|
-
|
|
407
|
-
// 속도와 위치 업데이트
|
|
408
266
|
p.vx = (p.vx + ax) * Math.random();
|
|
409
267
|
p.vy = (p.vy + ay) * Math.random();
|
|
410
268
|
p.x += p.vx;
|
|
411
269
|
p.y += p.vy;
|
|
412
270
|
}
|
|
413
|
-
|
|
414
|
-
// 파티클 그리기
|
|
415
271
|
this.ctx.fillStyle = this.config.particleColor;
|
|
416
272
|
const r = this.config.pointSize * this.DPR;
|
|
417
273
|
for (const p of this.particles) {
|
|
@@ -419,85 +275,70 @@ export class MasonEffect {
|
|
|
419
275
|
this.ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
|
|
420
276
|
this.ctx.fill();
|
|
421
277
|
}
|
|
422
|
-
|
|
423
278
|
if (this.config.onUpdate) {
|
|
424
279
|
this.config.onUpdate(this);
|
|
425
280
|
}
|
|
426
281
|
}
|
|
427
|
-
|
|
428
|
-
animate(): void {
|
|
282
|
+
animate() {
|
|
429
283
|
if (!this.isRunning) return;
|
|
430
284
|
this.update();
|
|
431
285
|
this.animationId = requestAnimationFrame(() => this.animate());
|
|
432
286
|
}
|
|
433
|
-
|
|
434
|
-
start(): void {
|
|
287
|
+
start() {
|
|
435
288
|
if (this.isRunning) return;
|
|
436
289
|
this.isRunning = true;
|
|
437
290
|
this.animate();
|
|
438
291
|
}
|
|
439
|
-
|
|
440
|
-
stop(): void {
|
|
292
|
+
stop() {
|
|
441
293
|
this.isRunning = false;
|
|
442
294
|
if (this.animationId) {
|
|
443
295
|
cancelAnimationFrame(this.animationId);
|
|
444
296
|
this.animationId = null;
|
|
445
297
|
}
|
|
446
298
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this.canvas.addEventListener(
|
|
451
|
-
this.canvas.addEventListener(
|
|
452
|
-
|
|
453
|
-
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);
|
|
454
305
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
this.canvas.removeEventListener(
|
|
459
|
-
this.canvas.removeEventListener(
|
|
460
|
-
|
|
461
|
-
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);
|
|
462
312
|
}
|
|
463
|
-
|
|
464
|
-
handleResize(): void {
|
|
313
|
+
handleResize() {
|
|
465
314
|
this.resize();
|
|
466
315
|
}
|
|
467
|
-
|
|
468
|
-
handleMouseMove(e: MouseEvent): void {
|
|
316
|
+
handleMouseMove(e) {
|
|
469
317
|
const rect = this.canvas.getBoundingClientRect();
|
|
470
318
|
this.mouse.x = (e.clientX - rect.left) * this.DPR;
|
|
471
319
|
this.mouse.y = (e.clientY - rect.top) * this.DPR;
|
|
472
320
|
}
|
|
473
|
-
|
|
474
|
-
handleMouseLeave(): void {
|
|
321
|
+
handleMouseLeave() {
|
|
475
322
|
this.mouse.x = this.mouse.y = 0;
|
|
476
323
|
}
|
|
477
|
-
|
|
478
|
-
handleMouseDown(): void {
|
|
324
|
+
handleMouseDown() {
|
|
479
325
|
this.mouse.down = true;
|
|
480
326
|
}
|
|
481
|
-
|
|
482
|
-
handleMouseUp(): void {
|
|
327
|
+
handleMouseUp() {
|
|
483
328
|
this.mouse.down = false;
|
|
484
329
|
}
|
|
485
|
-
|
|
486
330
|
// 설정 업데이트
|
|
487
|
-
updateConfig(newConfig
|
|
488
|
-
// 디바운스 적용
|
|
331
|
+
updateConfig(newConfig) {
|
|
489
332
|
this._debouncedUpdateConfig(newConfig);
|
|
490
333
|
}
|
|
491
|
-
|
|
492
|
-
_updateConfigInternal(newConfig: Partial<MasonEffectOptions>): void {
|
|
334
|
+
_updateConfigInternal(newConfig) {
|
|
493
335
|
this.config = { ...this.config, ...newConfig };
|
|
494
336
|
if (newConfig.text) {
|
|
495
337
|
this.buildTargets();
|
|
496
338
|
}
|
|
497
339
|
}
|
|
498
|
-
|
|
499
340
|
// 파괴 및 정리
|
|
500
|
-
destroy()
|
|
341
|
+
destroy() {
|
|
501
342
|
this.stop();
|
|
502
343
|
this.removeEventListeners();
|
|
503
344
|
if (this.intersectionObserver) {
|
|
@@ -509,7 +350,133 @@ export class MasonEffect {
|
|
|
509
350
|
}
|
|
510
351
|
}
|
|
511
352
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
+
};
|