masoneffect 1.0.27 β†’ 1.0.29

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # MasonEffect ![npm](https://img.shields.io/npm/dy/masoneffect)
2
2
 
3
- **Release version 1.0.27**
3
+ **Release version 1.0.29**
4
4
 
5
- A library that provides particle morphing effects. It can be used with React, Vue, and vanilla JavaScript.
5
+ A library that provides particle morphing effects. It can be used with React, Vue, Svelte, and vanilla JavaScript.
6
6
 
7
7
  ## Installation
8
8
 
@@ -47,7 +47,7 @@ effect.scatter();
47
47
  #### Using CDN (UMD)
48
48
 
49
49
  ```html
50
- <script src="https://cdn.jsdelivr.net/npm/masoneffect@1.0.27/dist/index.umd.min.js"></script>
50
+ <script src="https://cdn.jsdelivr.net/npm/masoneffect@1.0.29/dist/index.umd.min.js"></script>
51
51
  <script>
52
52
  const container = document.getElementById('my-container');
53
53
  const effect = new MasonEffect(container, {
@@ -189,6 +189,67 @@ const onReady = (instance) => {
189
189
  </template>
190
190
  ```
191
191
 
192
+ ### Svelte
193
+
194
+ ```svelte
195
+ <script lang="ts">
196
+ import MasonEffect from 'masoneffect/svelte';
197
+ import type { MasonEffect as MasonEffectType } from 'masoneffect';
198
+
199
+ let effectRef: MasonEffect | null = null;
200
+
201
+ const handleMorph = () => {
202
+ // Change text
203
+ effectRef?.morph({ text: 'Morphed!' });
204
+ };
205
+
206
+ const handleScatter = () => {
207
+ // Return particles to initial position
208
+ effectRef?.scatter();
209
+ };
210
+
211
+ const handleChangeText = () => {
212
+ const texts = ['Hello', 'World', 'Mason', 'Effect', 'Line 1\nLine 2'];
213
+ const randomText = texts[Math.floor(Math.random() * texts.length)];
214
+ effectRef?.morph({ text: randomText });
215
+ };
216
+
217
+ const handleChangeWithOptions = () => {
218
+ // Change text along with other properties
219
+ effectRef?.morph({
220
+ text: 'New Text',
221
+ particleColor: '#ff00ff',
222
+ maxParticles: 3000,
223
+ pointSize: 1.0,
224
+ });
225
+ };
226
+
227
+ const onReady = (instance: MasonEffectType) => {
228
+ console.log('Ready!', instance);
229
+ };
230
+ </script>
231
+
232
+ <div style="width: 100%; height: 70vh; background: #000; display: flex; flex-direction: column">
233
+ <div style="flex: 1; position: relative; min-height: 400px">
234
+ <MasonEffect
235
+ bind:this={effectRef}
236
+ text="Hello Svelte"
237
+ particleColor="#00ff88"
238
+ maxParticles={2000}
239
+ on:ready={onReady}
240
+ />
241
+ </div>
242
+ <div style="padding: 20px; display: flex; gap: 10px">
243
+ <button on:click={handleMorph}>Morph</button>
244
+ <button on:click={handleScatter}>Scatter</button>
245
+ <button on:click={handleChangeText}>Change Text</button>
246
+ <button on:click={handleChangeWithOptions}>Change with Options</button>
247
+ </div>
248
+ </div>
249
+ ```
250
+
251
+ **Note**: In Svelte, you can access component methods using `bind:this`. The component exposes `morph()`, `scatter()`, `updateConfig()`, and `destroy()` methods. Make sure to specify an explicit size for the container.
252
+
192
253
  ## API
193
254
 
194
255
  ### Options
@@ -250,7 +311,7 @@ Destroys the instance and cleans up resources.
250
311
  - πŸ–±οΈ Mouse interaction support (repel/attract)
251
312
  - πŸ“± Responsive design
252
313
  - ⚑ High-performance Canvas rendering
253
- - πŸ”§ Supports React, Vue, and vanilla JS (including CDN)
314
+ - πŸ”§ Supports React, Vue, Svelte, and vanilla JS (including CDN)
254
315
  - 🎯 Includes TypeScript type definitions
255
316
  - πŸ’Ύ Automatic obfuscation and optimization in production builds
256
317
  - πŸ”„ Scatter effect that returns to initial position
@@ -283,6 +344,7 @@ Running the build will generate the following files:
283
344
  - **Core library**: `dist/index.mjs` (ESM), `dist/index.cjs` (CommonJS), `dist/index.d.ts` (TypeScript types)
284
345
  - **React component**: `dist/react/index.mjs`, `dist/react/index.cjs`, `dist/react/index.d.ts`
285
346
  - **Vue component**: `dist/vue/index.mjs`, `dist/vue/index.cjs`, `dist/vue/index.d.ts`
347
+ - **Svelte component**: `dist/svelte/index.mjs`, `dist/svelte/index.cjs`, `dist/svelte/index.d.ts`
286
348
  - **UMD build**: `dist/index.umd.min.js` (for CDN usage)
287
349
 
288
350
  When installed via npm, the appropriate module format is automatically selected based on your bundler. The UMD files (`index.umd.min.js`) can be used directly in browsers via CDN or script tags.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Svelte component entry point
3
+ * Exports the compiled Svelte component
4
+ */
5
+ export { default } from './MasonEffect.svelte';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/svelte/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1 @@
1
+ "use strict";var t=Object.defineProperty,e=(e,i,n)=>((e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n)(e,"symbol"!=typeof i?i+"":i,n);const i=require("svelte");function n(){}function s(t){return t()}function o(){return Object.create(null)}function h(t){t.forEach(s)}function a(t){return"function"==typeof t}function r(t,e){return t!=t?e==e:t!==e||t&&"object"==typeof t||"function"==typeof t}function l(t){t.parentNode&&t.parentNode.removeChild(t)}function c(t,e,i){null==i?t.removeAttribute(e):t.getAttribute(e)!==i&&t.setAttribute(e,i)}let f;function d(t){f=t}const u=[],p=[];let g=[];const m=[],v=Promise.resolve();let x=!1;function y(t){g.push(t)}const w=new Set;let b=0;function $(){if(0!==b)return;const t=f;do{try{for(;b<u.length;){const t=u[b];b++,d(t),M(t.$$)}}catch(e){throw u.length=0,b=0,e}for(d(null),u.length=0,b=0;p.length;)p.pop()();for(let t=0;t<g.length;t+=1){const e=g[t];w.has(e)||(w.add(e),e())}g.length=0}while(u.length);for(;m.length;)m.pop()();x=!1,w.clear(),d(t)}function M(t){if(null!==t.fragment){t.update(),h(t.before_update);const e=t.dirty;t.dirty=[-1],t.fragment&&t.fragment.p(t.ctx,e),t.after_update.forEach(y)}}const R=new Set;function C(t,e){const i=t.$$;null!==i.fragment&&(!function(t){const e=[],i=[];g.forEach(n=>-1===t.indexOf(n)?e.push(n):i.push(n)),i.forEach(t=>t()),g=e}(i.after_update),h(i.on_destroy),i.fragment&&i.fragment.d(e),i.on_destroy=i.fragment=null,i.ctx=[])}function S(t,e){-1===t.$$.dirty[0]&&(u.push(t),x||(x=!0,v.then($)),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<<e%31}function P(t,e,i,r,c,u,p=null,g=[-1]){const m=f;d(t);const v=t.$$={fragment:null,ctx:[],props:u,update:n,not_equal:c,bound:o(),on_mount:[],on_destroy:[],on_disconnect:[],before_update:[],after_update:[],context:new Map(e.context||(m?m.$$.context:[])),callbacks:o(),dirty:g,skip_bound:!1,root:e.target||m.$$.root};p&&p(v.root);let x=!1;if(v.ctx=i?i(t,e.props||{},(e,i,...n)=>{const s=n.length?n[0]:i;return v.ctx&&c(v.ctx[e],v.ctx[e]=s)&&(!v.skip_bound&&v.bound[e]&&v.bound[e](s),x&&S(t,e)),i}):[],v.update(),x=!0,h(v.before_update),v.fragment=!!r&&r(v.ctx),e.target){if(e.hydrate){const t=(M=e.target,Array.from(M.childNodes));v.fragment&&v.fragment.l(t),t.forEach(l)}else v.fragment&&v.fragment.c();e.intro&&((w=t.$$.fragment)&&w.i&&(R.delete(w),w.i(b))),function(t,e,i){const{fragment:n,after_update:o}=t.$$;n&&n.m(e,i),y(()=>{const e=t.$$.on_mount.map(s).filter(a);t.$$.on_destroy?t.$$.on_destroy.push(...e):h(e),t.$$.on_mount=[]}),o.forEach(y)}(t,e.target,e.anchor),$()}var w,b,M;d(m)}class _{constructor(){e(this,"$$"),e(this,"$$set")}$destroy(){C(this,1),this.$destroy=n}$on(t,e){if(!a(e))return n;const i=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return i.push(e),()=>{const t=i.indexOf(e);-1!==t&&i.splice(t,1)}}$set(t){var e;this.$$set&&(e=t,0!==Object.keys(e).length)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}function z(t,e){let i=null;return function(...n){null!==i&&clearTimeout(i),i=setTimeout(()=>{i=null,t.apply(this,n)},e)}}"undefined"!=typeof window&&(window.__svelte||(window.__svelte={v:new Set})).v.add("4");class E{constructor(t,e={}){if(this.container="string"==typeof t?document.querySelector(t):t,!this.container)throw new Error("Container element not found");this.config={text:e.text||"mason effect",densityStep:e.densityStep??2,maxParticles:e.maxParticles??3200,pointSize:e.pointSize??.5,ease:e.ease??.05,repelRadius:e.repelRadius??150,repelStrength:e.repelStrength??1,particleColor:e.particleColor||"#fff",fontFamily:e.fontFamily||"Inter, system-ui, Arial",fontSize:e.fontSize||null,width:e.width||null,height:e.height||null,devicePixelRatio:e.devicePixelRatio??null,onReady:e.onReady||null,onUpdate:e.onUpdate||null},this.canvas=document.createElement("canvas");const i=this.canvas.getContext("2d");if(!i)throw new Error("Canvas context not available");this.ctx=i,this.container.appendChild(this.canvas),this.canvas.style.display="block",this.offCanvas=document.createElement("canvas");const n=this.offCanvas.getContext("2d");if(!n)throw new Error("Offscreen canvas context not available");this.offCtx=n,this.W=0,this.H=0,this.DPR=this.config.devicePixelRatio||Math.min(window.devicePixelRatio||1,1.8),this.particles=[],this.mouse={x:0,y:0,down:!1},this.animationId=null,this.isRunning=!1,this.isVisible=!1,this.intersectionObserver=null,this.debounceDelay=e.debounceDelay??150;const s=this.handleResize.bind(this);this.handleResize=z(s,this.debounceDelay),this.handleMouseMove=this.handleMouseMove.bind(this),this.handleMouseLeave=this.handleMouseLeave.bind(this),this.handleMouseDown=this.handleMouseDown.bind(this),this.handleMouseUp=this.handleMouseUp.bind(this),this._debouncedMorph=z(this._morphInternal.bind(this),this.debounceDelay),this._debouncedUpdateConfig=z(this._updateConfigInternal.bind(this),this.debounceDelay),this.init()}init(){this.resize(),this.setupEventListeners(),this.setupIntersectionObserver(),this.config.onReady&&this.config.onReady(this)}setupIntersectionObserver(){if("undefined"==typeof window||void 0===window.IntersectionObserver)return this.isVisible=!0,void this.start();this.intersectionObserver||(this.intersectionObserver=new IntersectionObserver(t=>{for(const e of t)e.target===this.container&&(e.isIntersecting?(this.isVisible=!0,this.start()):(this.isVisible=!1,this.stop()))},{threshold:.1}),this.intersectionObserver.observe(this.container))}resize(){const t=this.config.width||this.container.clientWidth||window.innerWidth,e=this.config.height||this.container.clientHeight||.7*window.innerHeight;if(t<=0||e<=0)return;this.W=Math.floor(t*this.DPR),this.H=Math.floor(e*this.DPR);const i=4096;if(this.W>i||this.H>i){const t=Math.min(i/this.W,i/this.H);this.W=Math.floor(this.W*t),this.H=Math.floor(this.H*t),this.DPR=this.DPR*t}this.canvas.width=this.W,this.canvas.height=this.H,this.canvas.style.width=t+"px",this.canvas.style.height=e+"px",this.W>0&&this.H>0&&(this.buildTargets(),this.particles.length||this.initParticles())}measureTextFit(t,e,i,n){this.offCtx.font=`400 ${t}px ${this.config.fontFamily}`;const s=e.split("\n"),o=t,h=.1*t,a=.05*t;let r=0;for(const c of s){if(0===c.length)continue;const t=this.offCtx.measureText(c).width+a*(c.length>0?c.length-1:0);r=Math.max(r,t)}const l=s.length>0?o*s.length+h*(s.length-1):o;return{width:r,height:l,fits:r<=i&&l<=n}}findOptimalFontSize(t,e,i,n){if(this.measureTextFit(n,t,e,i).fits)return n;if(n<=12)return 12;let s=12,o=n,h=12;for(;s<=o;){const n=Math.floor((s+o)/2);this.measureTextFit(n,t,e,i).fits?(h=n,s=n+1):o=n-1}return h}buildTargets(){if(this.W<=0||this.H<=0)return;const t=this.config.text;this.offCanvas.width=this.W,this.offCanvas.height=this.H,this.offCtx.clearRect(0,0,this.offCanvas.width,this.offCanvas.height);const e=Math.min(this.W,this.H),i=this.config.fontSize||Math.max(80,Math.floor(.18*e)),n=this.W-80,s=this.H-80,o=this.findOptimalFontSize(t,n,s,i);this.offCtx.fillStyle="#ffffff",this.offCtx.textAlign="center",this.offCtx.textBaseline="middle",this.offCtx.font=`400 ${o}px ${this.config.fontFamily}`;const h=t.split("\n"),a=o,r=.1*o,l=.05*o,c=h.length>0?a*h.length+r*(h.length-1):a;let f=this.H/2-c/2+a/2;for(const g of h){if(0===g.length){f+=a+r;continue}const t=g.split(""),e=this.offCtx.measureText(g).width+l*(t.length-1);let i=this.W/2-e/2;for(const n of t)this.offCtx.fillText(n,i+this.offCtx.measureText(n).width/2,f),i+=this.offCtx.measureText(n).width+l;f+=a+r}const d=Math.max(2,this.config.densityStep),u=this.offCtx.getImageData(0,0,this.W,this.H).data,p=[];for(let g=0;g<this.H;g+=d)for(let t=0;t<this.W;t+=d){const e=4*(g*this.W+t);u[e]+u[e+1]+u[e+2]>600&&p.push({x:t,y:g})}for(;p.length>this.config.maxParticles;)p.splice(Math.floor(Math.random()*p.length),1);if(this.particles.length<p.length){const t=p.length-this.particles.length;for(let e=0;e<t;e++)this.particles.push(this.makeParticle())}else this.particles.length>p.length&&(this.particles.length=p.length);for(let g=0;g<this.particles.length;g++){const t=this.particles[g],e=p[g];t.tx=e.x,t.ty=e.y}}makeParticle(){const t=Math.random()*this.W,e=Math.random()*this.H;return{x:t,y:e,vx:0,vy:0,tx:t,ty:e,initialX:t,initialY:e,j:Math.random()*Math.PI*2}}initParticles(){for(const t of this.particles){const e=Math.random()*this.W,i=Math.random()*this.H;t.x=e,t.y=i,t.vx=t.vy=0,t.initialX=e,t.initialY=i}}scatter(){for(const t of this.particles)void 0!==t.initialX&&void 0!==t.initialY?(t.tx=t.initialX,t.ty=t.initialY):(t.initialX=t.x,t.initialY=t.y,t.tx=t.initialX,t.ty=t.initialY)}morph(t){this._debouncedMorph(t)}_morphInternal(t){if(0!==this.W&&0!==this.H||this.resize(),"string"==typeof t)this.config.text=t,this.buildTargets();else if(t&&"object"==typeof t){const e=void 0!==t.text;this.config={...this.config,...t},e&&this.buildTargets()}else this.buildTargets()}update(){this.ctx.clearRect(0,0,this.W,this.H);for(const e of this.particles){let t=(e.tx-e.x)*this.config.ease,i=(e.ty-e.y)*this.config.ease;if(this.mouse.x||this.mouse.y){const n=e.x-this.mouse.x,s=e.y-this.mouse.y,o=n*n+s*s,h=this.config.repelRadius*this.DPR;if(o<h*h){const e=Math.sqrt(o)+1e-4,a=(this.mouse.down?-1:1)*this.config.repelStrength*(1-e/h);t+=n/e*a*6,i+=s/e*a*6}}e.j+=2,t+=.05*Math.cos(e.j),i+=.05*Math.sin(1.3*e.j),e.vx=(e.vx+t)*Math.random(),e.vy=(e.vy+i)*Math.random(),e.x+=e.vx,e.y+=e.vy}this.ctx.fillStyle=this.config.particleColor;const t=this.config.pointSize*this.DPR;for(const e of this.particles)this.ctx.beginPath(),this.ctx.arc(e.x,e.y,t,0,2*Math.PI),this.ctx.fill();this.config.onUpdate&&this.config.onUpdate(this)}animate(){this.isRunning&&(this.update(),this.animationId=requestAnimationFrame(()=>this.animate()))}start(){this.isRunning||(this.isRunning=!0,this.animate())}stop(){this.isRunning=!1,this.animationId&&(cancelAnimationFrame(this.animationId),this.animationId=null)}setupEventListeners(){window.addEventListener("resize",this.handleResize),this.canvas.addEventListener("mousemove",this.handleMouseMove),this.canvas.addEventListener("mouseleave",this.handleMouseLeave),this.canvas.addEventListener("mousedown",this.handleMouseDown),window.addEventListener("mouseup",this.handleMouseUp)}removeEventListeners(){window.removeEventListener("resize",this.handleResize),this.canvas.removeEventListener("mousemove",this.handleMouseMove),this.canvas.removeEventListener("mouseleave",this.handleMouseLeave),this.canvas.removeEventListener("mousedown",this.handleMouseDown),window.removeEventListener("mouseup",this.handleMouseUp)}handleResize(){this.resize()}handleMouseMove(t){const e=this.canvas.getBoundingClientRect();this.mouse.x=(t.clientX-e.left)*this.DPR,this.mouse.y=(t.clientY-e.top)*this.DPR}handleMouseLeave(){this.mouse.x=this.mouse.y=0}handleMouseDown(){this.mouse.down=!0}handleMouseUp(){this.mouse.down=!1}updateConfig(t){this._debouncedUpdateConfig(t)}_updateConfigInternal(t){this.config={...this.config,...t},t.text&&this.buildTargets()}destroy(){this.stop(),this.removeEventListeners(),this.intersectionObserver&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null),this.canvas&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas)}}function D(t){let e;return{c(){var i;i="div",e=document.createElement(i),c(e,"class",t[0]),c(e,"style",t[1])},m(i,n){!function(t,e,i){t.insertBefore(e,i||null)}(i,e,n),t[21](e)},p(t,[i]){1&i&&c(e,"class",t[0]),2&i&&c(e,"style",t[1])},i:n,o:n,d(i){i&&l(e),t[21](null)}}}function W(t,e,n){let{text:s="mason effect"}=e,{densityStep:o=2}=e,{maxParticles:h=3200}=e,{pointSize:a=.5}=e,{ease:r=.05}=e,{repelRadius:l=150}=e,{repelStrength:c=1}=e,{particleColor:f="#fff"}=e,{fontFamily:d="Inter, system-ui, Arial"}=e,{fontSize:u=null}=e,{width:g=null}=e,{height:m=null}=e,{devicePixelRatio:v=null}=e,{className:x=""}=e,{style:y={}}=e;const w=i.createEventDispatcher();let b,$=null;return i.onMount(()=>{if(!b)return;n(20,$=new E(b,{text:s,densityStep:o,maxParticles:h,pointSize:a,ease:r,repelRadius:l,repelStrength:c,particleColor:f,fontFamily:d,fontSize:u,width:g,height:m,devicePixelRatio:v,onReady:t=>{w("ready",t)},onUpdate:t=>{w("update",t)}}))}),i.onDestroy(()=>{$&&($.destroy(),n(20,$=null))}),t.$$set=t=>{"text"in t&&n(3,s=t.text),"densityStep"in t&&n(4,o=t.densityStep),"maxParticles"in t&&n(5,h=t.maxParticles),"pointSize"in t&&n(6,a=t.pointSize),"ease"in t&&n(7,r=t.ease),"repelRadius"in t&&n(8,l=t.repelRadius),"repelStrength"in t&&n(9,c=t.repelStrength),"particleColor"in t&&n(10,f=t.particleColor),"fontFamily"in t&&n(11,d=t.fontFamily),"fontSize"in t&&n(12,u=t.fontSize),"width"in t&&n(13,g=t.width),"height"in t&&n(14,m=t.height),"devicePixelRatio"in t&&n(15,v=t.devicePixelRatio),"className"in t&&n(0,x=t.className),"style"in t&&n(1,y=t.style)},t.$$.update=()=>{1114104&t.$$.dirty&&$&&$.updateConfig({text:s,densityStep:o,maxParticles:h,pointSize:a,ease:r,repelRadius:l,repelStrength:c,particleColor:f,fontFamily:d,fontSize:u,width:g,height:m,devicePixelRatio:v})},[x,y,b,s,o,h,a,r,l,c,f,d,u,g,m,v,function(t){$&&$.morph(t)},function(){$&&$.scatter()},function(t){$&&$.updateConfig(t)},function(){$&&($.destroy(),n(20,$=null))},$,function(t){p[t?"unshift":"push"](()=>{b=t,n(2,b)})}]}module.exports=class extends _{constructor(t){super(),P(this,t,W,D,r,{text:3,densityStep:4,maxParticles:5,pointSize:6,ease:7,repelRadius:8,repelStrength:9,particleColor:10,fontFamily:11,fontSize:12,width:13,height:14,devicePixelRatio:15,className:0,style:1,morph:16,scatter:17,updateConfig:18,destroy:19})}get morph(){return this.$$.ctx[16]}get scatter(){return this.$$.ctx[17]}get updateConfig(){return this.$$.ctx[18]}get destroy(){return this.$$.ctx[19]}};
@@ -0,0 +1,104 @@
1
+ declare class MasonEffect {
2
+ container: HTMLElement;
3
+ config: Required<Omit<MasonEffectOptions, 'onReady' | 'onUpdate' | 'debounceDelay'>> & {
4
+ onReady: MasonEffectOptions['onReady'];
5
+ onUpdate: MasonEffectOptions['onUpdate'];
6
+ };
7
+ canvas: HTMLCanvasElement;
8
+ ctx: CanvasRenderingContext2D;
9
+ offCanvas: HTMLCanvasElement;
10
+ offCtx: CanvasRenderingContext2D;
11
+ W: number;
12
+ H: number;
13
+ DPR: number;
14
+ particles: Particle[];
15
+ mouse: {
16
+ x: number;
17
+ y: number;
18
+ down: boolean;
19
+ };
20
+ animationId: number | null;
21
+ isRunning: boolean;
22
+ isVisible: boolean;
23
+ intersectionObserver: IntersectionObserver | null;
24
+ debounceDelay: number;
25
+ _debouncedMorph: (textOrOptions?: string | Partial<MasonEffectOptions> | null) => void;
26
+ _debouncedUpdateConfig: (newConfig: Partial<MasonEffectOptions>) => void;
27
+ constructor(container: HTMLElement | string, options?: MasonEffectOptions);
28
+ init(): void;
29
+ setupIntersectionObserver(): void;
30
+ resize(): void;
31
+ /**
32
+ * ν…μŠ€νŠΈκ°€ μ˜μ—­ μ•ˆμ— λ“€μ–΄κ°€λŠ”μ§€ ν™•μΈν•˜λŠ” 헬퍼 ν•¨μˆ˜ (μ€„λ°”κΏˆ 지원)
33
+ * @param fontSize 확인할 폰트 크기
34
+ * @param text ν…μŠ€νŠΈ (\n으둜 μ€„λ°”κΏˆ ꡬ뢄)
35
+ * @param maxWidth μ΅œλŒ€ λ„ˆλΉ„
36
+ * @param maxHeight μ΅œλŒ€ 높이
37
+ * @returns { width: number, height: number, fits: boolean }
38
+ */
39
+ private measureTextFit;
40
+ /**
41
+ * 이진 검색을 μ‚¬μš©ν•˜μ—¬ μ μ ˆν•œ 폰트 크기λ₯Ό μ°ΎλŠ” μ΅œμ ν™”λœ ν•¨μˆ˜
42
+ * 반볡 횟수λ₯Ό O(log n)으둜 쀄여 μ„±λŠ₯ κ°œμ„  (μ΅œλŒ€ 15회 반볡, κΈ°μ‘΄ μ΅œλŒ€ 100νšŒμ—μ„œ λŒ€ν­ κ°μ†Œ)
43
+ */
44
+ private findOptimalFontSize;
45
+ buildTargets(): void;
46
+ makeParticle(): Particle;
47
+ initParticles(): void;
48
+ scatter(): void;
49
+ morph(textOrOptions?: string | Partial<MasonEffectOptions> | null): void;
50
+ _morphInternal(textOrOptions?: string | Partial<MasonEffectOptions> | null): void;
51
+ update(): void;
52
+ animate(): void;
53
+ start(): void;
54
+ stop(): void;
55
+ setupEventListeners(): void;
56
+ removeEventListeners(): void;
57
+ handleResize(): void;
58
+ handleMouseMove(e: MouseEvent): void;
59
+ handleMouseLeave(): void;
60
+ handleMouseDown(): void;
61
+ handleMouseUp(): void;
62
+ updateConfig(newConfig: Partial<MasonEffectOptions>): void;
63
+ _updateConfigInternal(newConfig: Partial<MasonEffectOptions>): void;
64
+ destroy(): void;
65
+ }
66
+ export { MasonEffect }
67
+ export default MasonEffect;
68
+
69
+ /**
70
+ * MasonEffect - νŒŒν‹°ν΄ λͺ¨ν•‘ 효과 라이브러리
71
+ * 바닐라 JS μ½”μ–΄ 클래슀
72
+ */
73
+ export declare interface MasonEffectOptions {
74
+ text?: string;
75
+ densityStep?: number;
76
+ maxParticles?: number;
77
+ pointSize?: number;
78
+ ease?: number;
79
+ repelRadius?: number;
80
+ repelStrength?: number;
81
+ particleColor?: string;
82
+ fontFamily?: string;
83
+ fontSize?: number | null;
84
+ width?: number | null;
85
+ height?: number | null;
86
+ devicePixelRatio?: number | null;
87
+ debounceDelay?: number;
88
+ onReady?: (instance: MasonEffect) => void;
89
+ onUpdate?: (instance: MasonEffect) => void;
90
+ }
91
+
92
+ export declare interface Particle {
93
+ x: number;
94
+ y: number;
95
+ vx: number;
96
+ vy: number;
97
+ tx: number;
98
+ ty: number;
99
+ initialX?: number;
100
+ initialY?: number;
101
+ j: number;
102
+ }
103
+
104
+ export { }
@@ -0,0 +1,924 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { createEventDispatcher, onMount, onDestroy } from "svelte";
5
+ function noop() {
6
+ }
7
+ function run(fn) {
8
+ return fn();
9
+ }
10
+ function blank_object() {
11
+ return /* @__PURE__ */ Object.create(null);
12
+ }
13
+ function run_all(fns) {
14
+ fns.forEach(run);
15
+ }
16
+ function is_function(thing) {
17
+ return typeof thing === "function";
18
+ }
19
+ function safe_not_equal(a, b) {
20
+ return a != a ? b == b : a !== b || a && typeof a === "object" || typeof a === "function";
21
+ }
22
+ function is_empty(obj) {
23
+ return Object.keys(obj).length === 0;
24
+ }
25
+ function insert(target, node, anchor) {
26
+ target.insertBefore(node, anchor || null);
27
+ }
28
+ function detach(node) {
29
+ if (node.parentNode) {
30
+ node.parentNode.removeChild(node);
31
+ }
32
+ }
33
+ function element(name) {
34
+ return document.createElement(name);
35
+ }
36
+ function attr(node, attribute, value) {
37
+ if (value == null) node.removeAttribute(attribute);
38
+ else if (node.getAttribute(attribute) !== value) node.setAttribute(attribute, value);
39
+ }
40
+ function children(element2) {
41
+ return Array.from(element2.childNodes);
42
+ }
43
+ let current_component;
44
+ function set_current_component(component) {
45
+ current_component = component;
46
+ }
47
+ const dirty_components = [];
48
+ const binding_callbacks = [];
49
+ let render_callbacks = [];
50
+ const flush_callbacks = [];
51
+ const resolved_promise = /* @__PURE__ */ Promise.resolve();
52
+ let update_scheduled = false;
53
+ function schedule_update() {
54
+ if (!update_scheduled) {
55
+ update_scheduled = true;
56
+ resolved_promise.then(flush);
57
+ }
58
+ }
59
+ function add_render_callback(fn) {
60
+ render_callbacks.push(fn);
61
+ }
62
+ const seen_callbacks = /* @__PURE__ */ new Set();
63
+ let flushidx = 0;
64
+ function flush() {
65
+ if (flushidx !== 0) {
66
+ return;
67
+ }
68
+ const saved_component = current_component;
69
+ do {
70
+ try {
71
+ while (flushidx < dirty_components.length) {
72
+ const component = dirty_components[flushidx];
73
+ flushidx++;
74
+ set_current_component(component);
75
+ update(component.$$);
76
+ }
77
+ } catch (e) {
78
+ dirty_components.length = 0;
79
+ flushidx = 0;
80
+ throw e;
81
+ }
82
+ set_current_component(null);
83
+ dirty_components.length = 0;
84
+ flushidx = 0;
85
+ while (binding_callbacks.length) binding_callbacks.pop()();
86
+ for (let i = 0; i < render_callbacks.length; i += 1) {
87
+ const callback = render_callbacks[i];
88
+ if (!seen_callbacks.has(callback)) {
89
+ seen_callbacks.add(callback);
90
+ callback();
91
+ }
92
+ }
93
+ render_callbacks.length = 0;
94
+ } while (dirty_components.length);
95
+ while (flush_callbacks.length) {
96
+ flush_callbacks.pop()();
97
+ }
98
+ update_scheduled = false;
99
+ seen_callbacks.clear();
100
+ set_current_component(saved_component);
101
+ }
102
+ function update($$) {
103
+ if ($$.fragment !== null) {
104
+ $$.update();
105
+ run_all($$.before_update);
106
+ const dirty = $$.dirty;
107
+ $$.dirty = [-1];
108
+ $$.fragment && $$.fragment.p($$.ctx, dirty);
109
+ $$.after_update.forEach(add_render_callback);
110
+ }
111
+ }
112
+ function flush_render_callbacks(fns) {
113
+ const filtered = [];
114
+ const targets = [];
115
+ render_callbacks.forEach((c) => fns.indexOf(c) === -1 ? filtered.push(c) : targets.push(c));
116
+ targets.forEach((c) => c());
117
+ render_callbacks = filtered;
118
+ }
119
+ const outroing = /* @__PURE__ */ new Set();
120
+ function transition_in(block, local) {
121
+ if (block && block.i) {
122
+ outroing.delete(block);
123
+ block.i(local);
124
+ }
125
+ }
126
+ function mount_component(component, target, anchor) {
127
+ const { fragment, after_update } = component.$$;
128
+ fragment && fragment.m(target, anchor);
129
+ add_render_callback(() => {
130
+ const new_on_destroy = component.$$.on_mount.map(run).filter(is_function);
131
+ if (component.$$.on_destroy) {
132
+ component.$$.on_destroy.push(...new_on_destroy);
133
+ } else {
134
+ run_all(new_on_destroy);
135
+ }
136
+ component.$$.on_mount = [];
137
+ });
138
+ after_update.forEach(add_render_callback);
139
+ }
140
+ function destroy_component(component, detaching) {
141
+ const $$ = component.$$;
142
+ if ($$.fragment !== null) {
143
+ flush_render_callbacks($$.after_update);
144
+ run_all($$.on_destroy);
145
+ $$.fragment && $$.fragment.d(detaching);
146
+ $$.on_destroy = $$.fragment = null;
147
+ $$.ctx = [];
148
+ }
149
+ }
150
+ function make_dirty(component, i) {
151
+ if (component.$$.dirty[0] === -1) {
152
+ dirty_components.push(component);
153
+ schedule_update();
154
+ component.$$.dirty.fill(0);
155
+ }
156
+ component.$$.dirty[i / 31 | 0] |= 1 << i % 31;
157
+ }
158
+ function init(component, options, instance, create_fragment2, not_equal, props, append_styles = null, dirty = [-1]) {
159
+ const parent_component = current_component;
160
+ set_current_component(component);
161
+ const $$ = component.$$ = {
162
+ fragment: null,
163
+ ctx: [],
164
+ // state
165
+ props,
166
+ update: noop,
167
+ not_equal,
168
+ bound: blank_object(),
169
+ // lifecycle
170
+ on_mount: [],
171
+ on_destroy: [],
172
+ on_disconnect: [],
173
+ before_update: [],
174
+ after_update: [],
175
+ context: new Map(options.context || (parent_component ? parent_component.$$.context : [])),
176
+ // everything else
177
+ callbacks: blank_object(),
178
+ dirty,
179
+ skip_bound: false,
180
+ root: options.target || parent_component.$$.root
181
+ };
182
+ append_styles && append_styles($$.root);
183
+ let ready = false;
184
+ $$.ctx = instance ? instance(component, options.props || {}, (i, ret, ...rest) => {
185
+ const value = rest.length ? rest[0] : ret;
186
+ if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
187
+ if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
188
+ if (ready) make_dirty(component, i);
189
+ }
190
+ return ret;
191
+ }) : [];
192
+ $$.update();
193
+ ready = true;
194
+ run_all($$.before_update);
195
+ $$.fragment = create_fragment2 ? create_fragment2($$.ctx) : false;
196
+ if (options.target) {
197
+ if (options.hydrate) {
198
+ const nodes = children(options.target);
199
+ $$.fragment && $$.fragment.l(nodes);
200
+ nodes.forEach(detach);
201
+ } else {
202
+ $$.fragment && $$.fragment.c();
203
+ }
204
+ if (options.intro) transition_in(component.$$.fragment);
205
+ mount_component(component, options.target, options.anchor);
206
+ flush();
207
+ }
208
+ set_current_component(parent_component);
209
+ }
210
+ class SvelteComponent {
211
+ constructor() {
212
+ /**
213
+ * ### PRIVATE API
214
+ *
215
+ * Do not use, may change at any time
216
+ *
217
+ * @type {any}
218
+ */
219
+ __publicField(this, "$$");
220
+ /**
221
+ * ### PRIVATE API
222
+ *
223
+ * Do not use, may change at any time
224
+ *
225
+ * @type {any}
226
+ */
227
+ __publicField(this, "$$set");
228
+ }
229
+ /** @returns {void} */
230
+ $destroy() {
231
+ destroy_component(this, 1);
232
+ this.$destroy = noop;
233
+ }
234
+ /**
235
+ * @template {Extract<keyof Events, string>} K
236
+ * @param {K} type
237
+ * @param {((e: Events[K]) => void) | null | undefined} callback
238
+ * @returns {() => void}
239
+ */
240
+ $on(type, callback) {
241
+ if (!is_function(callback)) {
242
+ return noop;
243
+ }
244
+ const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
245
+ callbacks.push(callback);
246
+ return () => {
247
+ const index = callbacks.indexOf(callback);
248
+ if (index !== -1) callbacks.splice(index, 1);
249
+ };
250
+ }
251
+ /**
252
+ * @param {Partial<Props>} props
253
+ * @returns {void}
254
+ */
255
+ $set(props) {
256
+ if (this.$$set && !is_empty(props)) {
257
+ this.$$.skip_bound = true;
258
+ this.$$set(props);
259
+ this.$$.skip_bound = false;
260
+ }
261
+ }
262
+ }
263
+ const PUBLIC_VERSION = "4";
264
+ if (typeof window !== "undefined")
265
+ (window.__svelte || (window.__svelte = { v: /* @__PURE__ */ new Set() })).v.add(PUBLIC_VERSION);
266
+ function debounce(func, wait) {
267
+ let timeout = null;
268
+ return function executedFunction(...args) {
269
+ const later = () => {
270
+ timeout = null;
271
+ func.apply(this, args);
272
+ };
273
+ if (timeout !== null) {
274
+ clearTimeout(timeout);
275
+ }
276
+ timeout = setTimeout(later, wait);
277
+ };
278
+ }
279
+ class MasonEffect {
280
+ constructor(container, options = {}) {
281
+ this.container = typeof container === "string" ? document.querySelector(container) : container;
282
+ if (!this.container) {
283
+ throw new Error("Container element not found");
284
+ }
285
+ this.config = {
286
+ text: options.text || "mason effect",
287
+ densityStep: options.densityStep ?? 2,
288
+ maxParticles: options.maxParticles ?? 3200,
289
+ pointSize: options.pointSize ?? 0.5,
290
+ ease: options.ease ?? 0.05,
291
+ repelRadius: options.repelRadius ?? 150,
292
+ repelStrength: options.repelStrength ?? 1,
293
+ particleColor: options.particleColor || "#fff",
294
+ fontFamily: options.fontFamily || "Inter, system-ui, Arial",
295
+ fontSize: options.fontSize || null,
296
+ width: options.width || null,
297
+ height: options.height || null,
298
+ devicePixelRatio: options.devicePixelRatio ?? null,
299
+ onReady: options.onReady || null,
300
+ onUpdate: options.onUpdate || null
301
+ };
302
+ this.canvas = document.createElement("canvas");
303
+ const ctx = this.canvas.getContext("2d");
304
+ if (!ctx) {
305
+ throw new Error("Canvas context not available");
306
+ }
307
+ this.ctx = ctx;
308
+ this.container.appendChild(this.canvas);
309
+ this.canvas.style.display = "block";
310
+ this.offCanvas = document.createElement("canvas");
311
+ const offCtx = this.offCanvas.getContext("2d");
312
+ if (!offCtx) {
313
+ throw new Error("Offscreen canvas context not available");
314
+ }
315
+ this.offCtx = offCtx;
316
+ this.W = 0;
317
+ this.H = 0;
318
+ this.DPR = this.config.devicePixelRatio || Math.min(window.devicePixelRatio || 1, 1.8);
319
+ this.particles = [];
320
+ this.mouse = { x: 0, y: 0, down: false };
321
+ this.animationId = null;
322
+ this.isRunning = false;
323
+ this.isVisible = false;
324
+ this.intersectionObserver = null;
325
+ this.debounceDelay = options.debounceDelay ?? 150;
326
+ const boundHandleResize = this.handleResize.bind(this);
327
+ this.handleResize = debounce(boundHandleResize, this.debounceDelay);
328
+ this.handleMouseMove = this.handleMouseMove.bind(this);
329
+ this.handleMouseLeave = this.handleMouseLeave.bind(this);
330
+ this.handleMouseDown = this.handleMouseDown.bind(this);
331
+ this.handleMouseUp = this.handleMouseUp.bind(this);
332
+ this._debouncedMorph = debounce(this._morphInternal.bind(this), this.debounceDelay);
333
+ this._debouncedUpdateConfig = debounce(this._updateConfigInternal.bind(this), this.debounceDelay);
334
+ this.init();
335
+ }
336
+ init() {
337
+ this.resize();
338
+ this.setupEventListeners();
339
+ this.setupIntersectionObserver();
340
+ if (this.config.onReady) {
341
+ this.config.onReady(this);
342
+ }
343
+ }
344
+ setupIntersectionObserver() {
345
+ if (typeof window === "undefined" || typeof window.IntersectionObserver === "undefined") {
346
+ this.isVisible = true;
347
+ this.start();
348
+ return;
349
+ }
350
+ if (this.intersectionObserver) {
351
+ return;
352
+ }
353
+ this.intersectionObserver = new IntersectionObserver(
354
+ (entries) => {
355
+ for (const entry of entries) {
356
+ if (entry.target !== this.container) continue;
357
+ if (entry.isIntersecting) {
358
+ this.isVisible = true;
359
+ this.start();
360
+ } else {
361
+ this.isVisible = false;
362
+ this.stop();
363
+ }
364
+ }
365
+ },
366
+ {
367
+ threshold: 0.1
368
+ // 10% 이상 보일 λ•Œ λ™μž‘
369
+ }
370
+ );
371
+ this.intersectionObserver.observe(this.container);
372
+ }
373
+ resize() {
374
+ const width = this.config.width || this.container.clientWidth || window.innerWidth;
375
+ const height = this.config.height || this.container.clientHeight || window.innerHeight * 0.7;
376
+ if (width <= 0 || height <= 0) {
377
+ return;
378
+ }
379
+ this.W = Math.floor(width * this.DPR);
380
+ this.H = Math.floor(height * this.DPR);
381
+ const MAX_CANVAS_SIZE = 4096;
382
+ if (this.W > MAX_CANVAS_SIZE || this.H > MAX_CANVAS_SIZE) {
383
+ const scale = Math.min(MAX_CANVAS_SIZE / this.W, MAX_CANVAS_SIZE / this.H);
384
+ this.W = Math.floor(this.W * scale);
385
+ this.H = Math.floor(this.H * scale);
386
+ this.DPR = this.DPR * scale;
387
+ }
388
+ this.canvas.width = this.W;
389
+ this.canvas.height = this.H;
390
+ this.canvas.style.width = width + "px";
391
+ this.canvas.style.height = height + "px";
392
+ if (this.W > 0 && this.H > 0) {
393
+ this.buildTargets();
394
+ if (!this.particles.length) {
395
+ this.initParticles();
396
+ }
397
+ }
398
+ }
399
+ /**
400
+ * ν…μŠ€νŠΈκ°€ μ˜μ—­ μ•ˆμ— λ“€μ–΄κ°€λŠ”μ§€ ν™•μΈν•˜λŠ” 헬퍼 ν•¨μˆ˜ (μ€„λ°”κΏˆ 지원)
401
+ * @param fontSize 확인할 폰트 크기
402
+ * @param text ν…μŠ€νŠΈ (\n으둜 μ€„λ°”κΏˆ ꡬ뢄)
403
+ * @param maxWidth μ΅œλŒ€ λ„ˆλΉ„
404
+ * @param maxHeight μ΅œλŒ€ 높이
405
+ * @returns { width: number, height: number, fits: boolean }
406
+ */
407
+ measureTextFit(fontSize, text, maxWidth, maxHeight) {
408
+ this.offCtx.font = `400 ${fontSize}px ${this.config.fontFamily}`;
409
+ const lines = text.split("\n");
410
+ const lineHeight = fontSize;
411
+ const lineSpacing = fontSize * 0.1;
412
+ const spacing = fontSize * 0.05;
413
+ let maxLineWidth = 0;
414
+ for (const line of lines) {
415
+ if (line.length === 0) continue;
416
+ const textWidth = this.offCtx.measureText(line).width;
417
+ const totalWidth = textWidth + spacing * (line.length > 0 ? line.length - 1 : 0);
418
+ maxLineWidth = Math.max(maxLineWidth, totalWidth);
419
+ }
420
+ const totalHeight = lines.length > 0 ? lineHeight * lines.length + lineSpacing * (lines.length - 1) : lineHeight;
421
+ return {
422
+ width: maxLineWidth,
423
+ height: totalHeight,
424
+ fits: maxLineWidth <= maxWidth && totalHeight <= maxHeight
425
+ };
426
+ }
427
+ /**
428
+ * 이진 검색을 μ‚¬μš©ν•˜μ—¬ μ μ ˆν•œ 폰트 크기λ₯Ό μ°ΎλŠ” μ΅œμ ν™”λœ ν•¨μˆ˜
429
+ * 반볡 횟수λ₯Ό O(log n)으둜 쀄여 μ„±λŠ₯ κ°œμ„  (μ΅œλŒ€ 15회 반볡, κΈ°μ‘΄ μ΅œλŒ€ 100νšŒμ—μ„œ λŒ€ν­ κ°μ†Œ)
430
+ */
431
+ findOptimalFontSize(text, maxWidth, maxHeight, initialFontSize) {
432
+ const minFontSize = 12;
433
+ const initialMeasure = this.measureTextFit(initialFontSize, text, maxWidth, maxHeight);
434
+ if (initialMeasure.fits) {
435
+ return initialFontSize;
436
+ }
437
+ if (initialFontSize <= minFontSize) {
438
+ return minFontSize;
439
+ }
440
+ let low = minFontSize;
441
+ let high = initialFontSize;
442
+ let bestSize = minFontSize;
443
+ while (low <= high) {
444
+ const mid = Math.floor((low + high) / 2);
445
+ const measure = this.measureTextFit(mid, text, maxWidth, maxHeight);
446
+ if (measure.fits) {
447
+ bestSize = mid;
448
+ low = mid + 1;
449
+ } else {
450
+ high = mid - 1;
451
+ }
452
+ }
453
+ return bestSize;
454
+ }
455
+ buildTargets() {
456
+ if (this.W <= 0 || this.H <= 0) {
457
+ return;
458
+ }
459
+ const text = this.config.text;
460
+ this.offCanvas.width = this.W;
461
+ this.offCanvas.height = this.H;
462
+ this.offCtx.clearRect(0, 0, this.offCanvas.width, this.offCanvas.height);
463
+ const base = Math.min(this.W, this.H);
464
+ const initialFontSize = this.config.fontSize || Math.max(80, Math.floor(base * 0.18));
465
+ const padding = 40;
466
+ const maxWidth = this.W - padding * 2;
467
+ const maxHeight = this.H - padding * 2;
468
+ const fontSize = this.findOptimalFontSize(text, maxWidth, maxHeight, initialFontSize);
469
+ this.offCtx.fillStyle = "#ffffff";
470
+ this.offCtx.textAlign = "center";
471
+ this.offCtx.textBaseline = "middle";
472
+ this.offCtx.font = `400 ${fontSize}px ${this.config.fontFamily}`;
473
+ const lines = text.split("\n");
474
+ const lineHeight = fontSize;
475
+ const lineSpacing = fontSize * 0.1;
476
+ const spacing = fontSize * 0.05;
477
+ const totalTextHeight = lines.length > 0 ? lineHeight * lines.length + lineSpacing * (lines.length - 1) : lineHeight;
478
+ let startY = this.H / 2 - totalTextHeight / 2 + lineHeight / 2;
479
+ for (const line of lines) {
480
+ if (line.length === 0) {
481
+ startY += lineHeight + lineSpacing;
482
+ continue;
483
+ }
484
+ const chars = line.split("");
485
+ const totalWidth = this.offCtx.measureText(line).width + spacing * (chars.length - 1);
486
+ let x = this.W / 2 - totalWidth / 2;
487
+ for (const ch of chars) {
488
+ this.offCtx.fillText(ch, x + this.offCtx.measureText(ch).width / 2, startY);
489
+ x += this.offCtx.measureText(ch).width + spacing;
490
+ }
491
+ startY += lineHeight + lineSpacing;
492
+ }
493
+ const step = Math.max(2, this.config.densityStep);
494
+ const img = this.offCtx.getImageData(0, 0, this.W, this.H).data;
495
+ const targets = [];
496
+ for (let y = 0; y < this.H; y += step) {
497
+ for (let x = 0; x < this.W; x += step) {
498
+ const i = (y * this.W + x) * 4;
499
+ if (img[i] + img[i + 1] + img[i + 2] > 600) {
500
+ targets.push({ x, y });
501
+ }
502
+ }
503
+ }
504
+ while (targets.length > this.config.maxParticles) {
505
+ targets.splice(Math.floor(Math.random() * targets.length), 1);
506
+ }
507
+ if (this.particles.length < targets.length) {
508
+ const need = targets.length - this.particles.length;
509
+ for (let i = 0; i < need; i++) {
510
+ this.particles.push(this.makeParticle());
511
+ }
512
+ } else if (this.particles.length > targets.length) {
513
+ this.particles.length = targets.length;
514
+ }
515
+ for (let i = 0; i < this.particles.length; i++) {
516
+ const p = this.particles[i];
517
+ const t = targets[i];
518
+ p.tx = t.x;
519
+ p.ty = t.y;
520
+ }
521
+ }
522
+ makeParticle() {
523
+ const sx = Math.random() * this.W;
524
+ const sy = Math.random() * this.H;
525
+ return {
526
+ x: sx,
527
+ y: sy,
528
+ vx: 0,
529
+ vy: 0,
530
+ tx: sx,
531
+ ty: sy,
532
+ initialX: sx,
533
+ // 초기 μœ„μΉ˜ μ €μž₯ (scatter μ‹œ λŒμ•„κ°ˆ μœ„μΉ˜)
534
+ initialY: sy,
535
+ j: Math.random() * Math.PI * 2
536
+ };
537
+ }
538
+ initParticles() {
539
+ for (const p of this.particles) {
540
+ const sx = Math.random() * this.W;
541
+ const sy = Math.random() * this.H;
542
+ p.x = sx;
543
+ p.y = sy;
544
+ p.vx = p.vy = 0;
545
+ p.initialX = sx;
546
+ p.initialY = sy;
547
+ }
548
+ }
549
+ scatter() {
550
+ for (const p of this.particles) {
551
+ if (p.initialX !== void 0 && p.initialY !== void 0) {
552
+ p.tx = p.initialX;
553
+ p.ty = p.initialY;
554
+ } else {
555
+ p.initialX = p.x;
556
+ p.initialY = p.y;
557
+ p.tx = p.initialX;
558
+ p.ty = p.initialY;
559
+ }
560
+ }
561
+ }
562
+ morph(textOrOptions) {
563
+ this._debouncedMorph(textOrOptions);
564
+ }
565
+ _morphInternal(textOrOptions) {
566
+ if (this.W === 0 || this.H === 0) {
567
+ this.resize();
568
+ }
569
+ if (typeof textOrOptions === "string") {
570
+ this.config.text = textOrOptions;
571
+ this.buildTargets();
572
+ } else if (textOrOptions && typeof textOrOptions === "object") {
573
+ const needsRebuild = textOrOptions.text !== void 0;
574
+ this.config = { ...this.config, ...textOrOptions };
575
+ if (needsRebuild) {
576
+ this.buildTargets();
577
+ }
578
+ } else {
579
+ this.buildTargets();
580
+ }
581
+ }
582
+ update() {
583
+ this.ctx.clearRect(0, 0, this.W, this.H);
584
+ for (const p of this.particles) {
585
+ let ax = (p.tx - p.x) * this.config.ease;
586
+ let ay = (p.ty - p.y) * this.config.ease;
587
+ if (this.mouse.x || this.mouse.y) {
588
+ const dx = p.x - this.mouse.x;
589
+ const dy = p.y - this.mouse.y;
590
+ const d2 = dx * dx + dy * dy;
591
+ const r2 = this.config.repelRadius * this.DPR;
592
+ if (d2 < r2 * r2) {
593
+ const d = Math.sqrt(d2) + 1e-4;
594
+ const f = (this.mouse.down ? -1 : 1) * this.config.repelStrength * (1 - d / r2);
595
+ ax += dx / d * f * 6;
596
+ ay += dy / d * f * 6;
597
+ }
598
+ }
599
+ p.j += 2;
600
+ ax += Math.cos(p.j) * 0.05;
601
+ ay += Math.sin(p.j * 1.3) * 0.05;
602
+ p.vx = (p.vx + ax) * Math.random();
603
+ p.vy = (p.vy + ay) * Math.random();
604
+ p.x += p.vx;
605
+ p.y += p.vy;
606
+ }
607
+ this.ctx.fillStyle = this.config.particleColor;
608
+ const r = this.config.pointSize * this.DPR;
609
+ for (const p of this.particles) {
610
+ this.ctx.beginPath();
611
+ this.ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
612
+ this.ctx.fill();
613
+ }
614
+ if (this.config.onUpdate) {
615
+ this.config.onUpdate(this);
616
+ }
617
+ }
618
+ animate() {
619
+ if (!this.isRunning) return;
620
+ this.update();
621
+ this.animationId = requestAnimationFrame(() => this.animate());
622
+ }
623
+ start() {
624
+ if (this.isRunning) return;
625
+ this.isRunning = true;
626
+ this.animate();
627
+ }
628
+ stop() {
629
+ this.isRunning = false;
630
+ if (this.animationId) {
631
+ cancelAnimationFrame(this.animationId);
632
+ this.animationId = null;
633
+ }
634
+ }
635
+ setupEventListeners() {
636
+ window.addEventListener("resize", this.handleResize);
637
+ this.canvas.addEventListener("mousemove", this.handleMouseMove);
638
+ this.canvas.addEventListener("mouseleave", this.handleMouseLeave);
639
+ this.canvas.addEventListener("mousedown", this.handleMouseDown);
640
+ window.addEventListener("mouseup", this.handleMouseUp);
641
+ }
642
+ removeEventListeners() {
643
+ window.removeEventListener("resize", this.handleResize);
644
+ this.canvas.removeEventListener("mousemove", this.handleMouseMove);
645
+ this.canvas.removeEventListener("mouseleave", this.handleMouseLeave);
646
+ this.canvas.removeEventListener("mousedown", this.handleMouseDown);
647
+ window.removeEventListener("mouseup", this.handleMouseUp);
648
+ }
649
+ handleResize() {
650
+ this.resize();
651
+ }
652
+ handleMouseMove(e) {
653
+ const rect = this.canvas.getBoundingClientRect();
654
+ this.mouse.x = (e.clientX - rect.left) * this.DPR;
655
+ this.mouse.y = (e.clientY - rect.top) * this.DPR;
656
+ }
657
+ handleMouseLeave() {
658
+ this.mouse.x = this.mouse.y = 0;
659
+ }
660
+ handleMouseDown() {
661
+ this.mouse.down = true;
662
+ }
663
+ handleMouseUp() {
664
+ this.mouse.down = false;
665
+ }
666
+ // μ„€μ • μ—…λ°μ΄νŠΈ
667
+ updateConfig(newConfig) {
668
+ this._debouncedUpdateConfig(newConfig);
669
+ }
670
+ _updateConfigInternal(newConfig) {
671
+ this.config = { ...this.config, ...newConfig };
672
+ if (newConfig.text) {
673
+ this.buildTargets();
674
+ }
675
+ }
676
+ // 파괴 및 정리
677
+ destroy() {
678
+ this.stop();
679
+ this.removeEventListeners();
680
+ if (this.intersectionObserver) {
681
+ this.intersectionObserver.disconnect();
682
+ this.intersectionObserver = null;
683
+ }
684
+ if (this.canvas && this.canvas.parentNode) {
685
+ this.canvas.parentNode.removeChild(this.canvas);
686
+ }
687
+ }
688
+ }
689
+ function create_fragment(ctx) {
690
+ let div;
691
+ return {
692
+ c() {
693
+ div = element("div");
694
+ attr(
695
+ div,
696
+ "class",
697
+ /*className*/
698
+ ctx[0]
699
+ );
700
+ attr(
701
+ div,
702
+ "style",
703
+ /*style*/
704
+ ctx[1]
705
+ );
706
+ },
707
+ m(target, anchor) {
708
+ insert(target, div, anchor);
709
+ ctx[21](div);
710
+ },
711
+ p(ctx2, [dirty]) {
712
+ if (dirty & /*className*/
713
+ 1) {
714
+ attr(
715
+ div,
716
+ "class",
717
+ /*className*/
718
+ ctx2[0]
719
+ );
720
+ }
721
+ if (dirty & /*style*/
722
+ 2) {
723
+ attr(
724
+ div,
725
+ "style",
726
+ /*style*/
727
+ ctx2[1]
728
+ );
729
+ }
730
+ },
731
+ i: noop,
732
+ o: noop,
733
+ d(detaching) {
734
+ if (detaching) {
735
+ detach(div);
736
+ }
737
+ ctx[21](null);
738
+ }
739
+ };
740
+ }
741
+ function instance_1($$self, $$props, $$invalidate) {
742
+ let { text = "mason effect" } = $$props;
743
+ let { densityStep = 2 } = $$props;
744
+ let { maxParticles = 3200 } = $$props;
745
+ let { pointSize = 0.5 } = $$props;
746
+ let { ease = 0.05 } = $$props;
747
+ let { repelRadius = 150 } = $$props;
748
+ let { repelStrength = 1 } = $$props;
749
+ let { particleColor = "#fff" } = $$props;
750
+ let { fontFamily = "Inter, system-ui, Arial" } = $$props;
751
+ let { fontSize = null } = $$props;
752
+ let { width = null } = $$props;
753
+ let { height = null } = $$props;
754
+ let { devicePixelRatio = null } = $$props;
755
+ let { className = "" } = $$props;
756
+ let { style = {} } = $$props;
757
+ const dispatch = createEventDispatcher();
758
+ let container;
759
+ let instance = null;
760
+ onMount(() => {
761
+ if (!container) return;
762
+ const options = {
763
+ text,
764
+ densityStep,
765
+ maxParticles,
766
+ pointSize,
767
+ ease,
768
+ repelRadius,
769
+ repelStrength,
770
+ particleColor,
771
+ fontFamily,
772
+ fontSize,
773
+ width,
774
+ height,
775
+ devicePixelRatio,
776
+ onReady: (inst) => {
777
+ dispatch("ready", inst);
778
+ },
779
+ onUpdate: (inst) => {
780
+ dispatch("update", inst);
781
+ }
782
+ };
783
+ $$invalidate(20, instance = new MasonEffect(container, options));
784
+ });
785
+ onDestroy(() => {
786
+ if (instance) {
787
+ instance.destroy();
788
+ $$invalidate(20, instance = null);
789
+ }
790
+ });
791
+ function morph(textOrOptions) {
792
+ if (instance) {
793
+ instance.morph(textOrOptions);
794
+ }
795
+ }
796
+ function scatter() {
797
+ if (instance) {
798
+ instance.scatter();
799
+ }
800
+ }
801
+ function updateConfig(config) {
802
+ if (instance) {
803
+ instance.updateConfig(config);
804
+ }
805
+ }
806
+ function destroy() {
807
+ if (instance) {
808
+ instance.destroy();
809
+ $$invalidate(20, instance = null);
810
+ }
811
+ }
812
+ function div_binding($$value) {
813
+ binding_callbacks[$$value ? "unshift" : "push"](() => {
814
+ container = $$value;
815
+ $$invalidate(2, container);
816
+ });
817
+ }
818
+ $$self.$$set = ($$props2) => {
819
+ if ("text" in $$props2) $$invalidate(3, text = $$props2.text);
820
+ if ("densityStep" in $$props2) $$invalidate(4, densityStep = $$props2.densityStep);
821
+ if ("maxParticles" in $$props2) $$invalidate(5, maxParticles = $$props2.maxParticles);
822
+ if ("pointSize" in $$props2) $$invalidate(6, pointSize = $$props2.pointSize);
823
+ if ("ease" in $$props2) $$invalidate(7, ease = $$props2.ease);
824
+ if ("repelRadius" in $$props2) $$invalidate(8, repelRadius = $$props2.repelRadius);
825
+ if ("repelStrength" in $$props2) $$invalidate(9, repelStrength = $$props2.repelStrength);
826
+ if ("particleColor" in $$props2) $$invalidate(10, particleColor = $$props2.particleColor);
827
+ if ("fontFamily" in $$props2) $$invalidate(11, fontFamily = $$props2.fontFamily);
828
+ if ("fontSize" in $$props2) $$invalidate(12, fontSize = $$props2.fontSize);
829
+ if ("width" in $$props2) $$invalidate(13, width = $$props2.width);
830
+ if ("height" in $$props2) $$invalidate(14, height = $$props2.height);
831
+ if ("devicePixelRatio" in $$props2) $$invalidate(15, devicePixelRatio = $$props2.devicePixelRatio);
832
+ if ("className" in $$props2) $$invalidate(0, className = $$props2.className);
833
+ if ("style" in $$props2) $$invalidate(1, style = $$props2.style);
834
+ };
835
+ $$self.$$.update = () => {
836
+ if ($$self.$$.dirty & /*instance, text, densityStep, maxParticles, pointSize, ease, repelRadius, repelStrength, particleColor, fontFamily, fontSize, width, height, devicePixelRatio*/
837
+ 1114104) {
838
+ {
839
+ if (instance) {
840
+ instance.updateConfig({
841
+ text,
842
+ densityStep,
843
+ maxParticles,
844
+ pointSize,
845
+ ease,
846
+ repelRadius,
847
+ repelStrength,
848
+ particleColor,
849
+ fontFamily,
850
+ fontSize,
851
+ width,
852
+ height,
853
+ devicePixelRatio
854
+ });
855
+ }
856
+ }
857
+ }
858
+ };
859
+ return [
860
+ className,
861
+ style,
862
+ container,
863
+ text,
864
+ densityStep,
865
+ maxParticles,
866
+ pointSize,
867
+ ease,
868
+ repelRadius,
869
+ repelStrength,
870
+ particleColor,
871
+ fontFamily,
872
+ fontSize,
873
+ width,
874
+ height,
875
+ devicePixelRatio,
876
+ morph,
877
+ scatter,
878
+ updateConfig,
879
+ destroy,
880
+ instance,
881
+ div_binding
882
+ ];
883
+ }
884
+ class MasonEffect_1 extends SvelteComponent {
885
+ constructor(options) {
886
+ super();
887
+ init(this, options, instance_1, create_fragment, safe_not_equal, {
888
+ text: 3,
889
+ densityStep: 4,
890
+ maxParticles: 5,
891
+ pointSize: 6,
892
+ ease: 7,
893
+ repelRadius: 8,
894
+ repelStrength: 9,
895
+ particleColor: 10,
896
+ fontFamily: 11,
897
+ fontSize: 12,
898
+ width: 13,
899
+ height: 14,
900
+ devicePixelRatio: 15,
901
+ className: 0,
902
+ style: 1,
903
+ morph: 16,
904
+ scatter: 17,
905
+ updateConfig: 18,
906
+ destroy: 19
907
+ });
908
+ }
909
+ get morph() {
910
+ return this.$$.ctx[16];
911
+ }
912
+ get scatter() {
913
+ return this.$$.ctx[17];
914
+ }
915
+ get updateConfig() {
916
+ return this.$$.ctx[18];
917
+ }
918
+ get destroy() {
919
+ return this.$$.ctx[19];
920
+ }
921
+ }
922
+ export {
923
+ MasonEffect_1 as default
924
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "masoneffect",
3
- "version": "1.0.27",
4
- "description": "νŒŒν‹°ν΄ λͺ¨ν•‘ 효과λ₯Ό μ œκ³΅ν•˜λŠ” 라이브러리 - React, Vue, 바닐라 JS 지원",
3
+ "version": "1.0.29",
4
+ "description": "A library that provides particle morphing effects. It can be used with React, Vue, Svelte, and vanilla JavaScript.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
@@ -20,13 +20,20 @@
20
20
  "import": "./dist/vue/index.mjs",
21
21
  "require": "./dist/vue/index.cjs",
22
22
  "types": "./dist/vue/index.d.ts"
23
+ },
24
+ "./svelte": {
25
+ "import": "./dist/svelte/index.mjs",
26
+ "require": "./dist/svelte/index.cjs",
27
+ "types": "./dist/svelte/index.d.ts"
23
28
  }
24
29
  },
25
30
  "files": [
26
31
  "dist"
27
32
  ],
28
33
  "scripts": {
29
- "build": "rollup -c rollup.config.mjs && vite build && node scripts/generate-react-types.js",
34
+ "build": "rollup -c rollup.config.mjs && npm run build:vue && npm run build:svelte && node scripts/generate-react-types.js",
35
+ "build:vue": "vite build --config vite.config.vue.mjs",
36
+ "build:svelte": "vite build --config vite.config.svelte.mjs",
30
37
  "dev": "rollup -c rollup.config.mjs -w",
31
38
  "serve": "npx http-server . -p 8080 -o --cors",
32
39
  "dev:example": "npm run serve",
@@ -57,12 +64,15 @@
57
64
  "@rollup/plugin-node-resolve": "^15.2.3",
58
65
  "@rollup/plugin-terser": "^0.4.4",
59
66
  "@rollup/plugin-typescript": "^11.1.5",
67
+ "@sveltejs/vite-plugin-svelte": "^3.0.0",
60
68
  "@types/react": "^19.2.3",
61
69
  "@types/react-dom": "^19.2.3",
62
70
  "@vitejs/plugin-vue": "^5.0.0",
63
71
  "react": "^19.2.0",
64
72
  "react-dom": "^19.2.0",
65
73
  "rollup": "^4.9.0",
74
+ "svelte": "^4.0.0",
75
+ "svelte-preprocess": "^6.0.3",
66
76
  "tslib": "^2.8.1",
67
77
  "typescript": "^5.3.3",
68
78
  "vite": "^5.0.0",
@@ -70,6 +80,7 @@
70
80
  },
71
81
  "peerDependencies": {
72
82
  "react": ">=16.8.0 || >=19.0.0-0",
83
+ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
73
84
  "vue": ">=3.0.0"
74
85
  },
75
86
  "peerDependenciesMeta": {
@@ -78,6 +89,9 @@
78
89
  },
79
90
  "vue": {
80
91
  "optional": true
92
+ },
93
+ "svelte": {
94
+ "optional": true
81
95
  }
82
96
  }
83
97
  }