@liiift-studio/magnettype 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +33 -0
- package/dist/index.js +270 -145
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# magnetType
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@liiift-studio/magnettype) [](https://opensource.org/licenses/MIT) [](https://github.com/Liiift-Studio/type-tools)
|
|
4
|
+
|
|
3
5
|
CSS `font-variation-settings` applies a single value to the whole element — there is no native way to drive axis values per word from cursor proximity, or to selectively widen visually confusable characters for legibility. magnetType adds both.
|
|
4
6
|
|
|
5
7
|
**[magnettype.com](https://magnettype.com)** · [npm](https://www.npmjs.com/package/@liiift-studio/magnettype) · [GitHub](https://github.com/Liiift-Studio/MagnetType)
|
|
@@ -138,6 +140,7 @@ const legibilityOpts: MagnetTypeOptions = {
|
|
|
138
140
|
| `falloff` | `'quadratic'` | *(field mode only)* Falloff curve. `'linear'` — strength decreases linearly with distance. `'quadratic'` — strength decreases as distance², giving a tighter hot zone and a sharper peak feel |
|
|
139
141
|
| `magnetMode` | `'attract'` | *(field mode only)* `'attract'` — words near the cursor approach `peakValue`. `'repel'` — words near the cursor stay at `restValue`; words farther away approach `peakValue` |
|
|
140
142
|
| `wdthBoost` | `6` | *(legibility mode only)* `wdth` axis units added to confusable characters, scaled by risk level. Risk 3 characters (`i l 1 I`) receive the full boost; risk 2 characters (`r 0 O`) receive ⅔; risk 1 characters (`n m o b d p q c e`) receive ⅓ |
|
|
143
|
+
| `transitionMs` | `0` | Duration in milliseconds for the CSS transition back to rest values when the cursor leaves (or a touch ends). `0` preserves the existing instant snap. When > 0, `font-variation-settings` animates back over the given duration using `ease` easing. The transition is cleared immediately on the next `mousemove`/`touchmove` so live tracking is not delayed |
|
|
141
144
|
| `as` | `'p'` | HTML element to render, e.g. `'h1'`, `'div'`, `'span'`. Accepts any valid React element type. *(React component only)* |
|
|
142
145
|
|
|
143
146
|
---
|
|
@@ -185,12 +188,10 @@ The package itself has zero runtime dependencies. Do not remove this entry.
|
|
|
185
188
|
|
|
186
189
|
## Future improvements
|
|
187
190
|
|
|
188
|
-
- **Touch support** — `touchmove` events for mobile field mode; currently `mousemove`-only, so the effect is desktop-exclusive
|
|
189
|
-
- **Smooth transition on stop** — animate axis values back to `restValue` over a short duration when the cursor leaves, rather than snapping on the next rAF tick
|
|
190
191
|
- **Custom confusable table** — allow callers to pass their own `Record<string, number>` to override or extend the built-in character risk map for language- or font-specific tuning
|
|
191
192
|
- **Axis clamping** — optional per-axis min/max clamp to prevent axis values from exceeding a font's supported range, avoiding undefined browser rendering behaviour
|
|
192
193
|
- **SSR hydration** — pre-render legibility mode markup on the server so boosted characters are present from first paint without a client-side flash
|
|
193
194
|
|
|
194
195
|
---
|
|
195
196
|
|
|
196
|
-
Current version:
|
|
197
|
+
Current version: 0.1.3
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const M=require("react"),K=require("react/jsx-runtime"),G={i:3,l:3,1:3,I:3,r:2,n:1,m:1,0:2,O:2,o:1,b:1,d:1,p:1,q:1,c:1,e:1},R={word:"mt-word",char:"mt-char",probe:"mt-probe"},Y={axes:{wght:[300,500]},radius:120,falloff:"quadratic",magnetMode:"attract",wdthBoost:6,scope:"document"};function _(t,o=[]){return t.nodeType===Node.TEXT_NODE?o.push(t):t.childNodes.forEach(r=>_(r,o)),o}function H(t,o,r){if(!t||t==="normal")return`"${o}" ${r}`;const f=new RegExp(`(["'])${o}\\1\\s+[\\d.eE+-]+`),c=`"${o}" ${r}`;return f.test(t)?t.replace(f,c):`${t}, ${c}`}function J(t,o){let r=t;for(const[f,c]of Object.entries(o))r=H(r,f,c);return r}function U(t,o,r){if(o.opacity!==void 0){const[f,c]=o.opacity;t.style.opacity=String(f+(c-f)*r)}o.italic===!0&&(t.style.fontStyle=r>.5?"italic":"")}function P(t,o){o.opacity!==void 0&&(t.style.opacity=String(o.opacity[0])),o.italic===!0&&(t.style.fontStyle="")}function z(t){const o=t.cloneNode(!0),r=o.querySelectorAll(`.${R.word}, .${R.char}`);return Array.from(r).reverse().forEach(c=>{const u=c.parentNode;if(u){for(;c.firstChild;)u.insertBefore(c.firstChild,c);u.removeChild(c)}}),o.innerHTML}function ot(t,o){t.innerHTML=o}function Q(t,o,r={}){if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return t.innerHTML=o,()=>{};const f=r.wdthBoost??Y.wdthBoost,c=r.radius??Y.radius,u=r.falloff??Y.falloff,T=r.scope??Y.scope,v=r.props,m=r.transitionMs??0,S=window.scrollY;t.innerHTML=o;const L=getComputedStyle(t).fontVariationSettings,b=L.match(/"wdth"\s+([\d.eE+-]+)/),y=b?parseFloat(b[1]):100,A=_(t),w=[];for(const e of A){const n=e.textContent??"";if(!n||!n.split("").some(a=>a in G))continue;const h=document.createDocumentFragment();for(const a of n){const B=G[a];if(B===void 0){const E=h.lastChild;E&&E.nodeType===Node.TEXT_NODE?E.textContent+=a:h.appendChild(document.createTextNode(a))}else{const E=document.createElement("span");E.className=R.char,E.style.fontVariationSettings=H(L,"wdth",y),E.textContent=a,h.appendChild(E),w.push({span:E,riskLevel:B})}}e.parentNode.replaceChild(h,e)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-S)>2&&window.scrollTo({top:S,behavior:"instant"})}),w.length===0)return()=>{};v&&w.forEach(({span:e})=>P(e,v));let p=-9999,q=-9999,g=!1,i=0,V=!0,s=null;function d(){if(!V)return;if(!g){w.forEach(({span:n})=>{m>0&&(n.style.transition=`font-variation-settings ${m}ms ease`),n.style.fontVariationSettings=H(L,"wdth",y),v&&P(n,v)}),m>0&&(s!==null&&clearTimeout(s),s=setTimeout(()=>{w.forEach(({span:n})=>{n.style.transition=""}),s=null},m)),i=0;return}const e=w.map(({span:n})=>n.getBoundingClientRect());w.forEach(({span:n,riskLevel:x},h)=>{n.style.transition="";const a=e[h],B=a.left+a.width/2,E=a.top+a.height/2,j=Math.sqrt((p-B)**2+(q-E)**2),O=Math.max(0,1-j/c),$=u==="quadratic"?O*O:O,X=f*(x/3)*$;n.style.fontVariationSettings=H(L,"wdth",y+X),v&&U(n,v,$)}),i=requestAnimationFrame(d)}function N(e){p=e.clientX,q=e.clientY,g||(g=!0),i===0&&(i=requestAnimationFrame(d))}function C(){g=!1,i===0&&(i=requestAnimationFrame(d))}function F(e){e.touches.length!==0&&(p=e.touches[0].clientX,q=e.touches[0].clientY,g||(g=!0),i===0&&(i=requestAnimationFrame(d)))}function k(){g=!1,i===0&&(i=requestAnimationFrame(d))}const l=T==="document"?document:t;return l.addEventListener("mousemove",N),l.addEventListener("mouseleave",C),l.addEventListener("touchmove",F,{passive:!0}),l.addEventListener("touchend",k),()=>{V=!1,cancelAnimationFrame(i),s!==null&&clearTimeout(s),l.removeEventListener("mousemove",N),l.removeEventListener("mouseleave",C),l.removeEventListener("touchmove",F),l.removeEventListener("touchend",k),t.innerHTML=o}}function Z(t,o,r={}){if(typeof window>"u")return()=>{};if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return t.innerHTML=o,()=>{};const f=r.axes??Y.axes,c=r.radius??Y.radius,u=r.falloff??Y.falloff,T=r.magnetMode??Y.magnetMode,v=r.scope??Y.scope,m=r.props,S=r.transitionMs??0,L=window.scrollY;t.innerHTML=o;const b=_(t),y=[];for(const e of b){const n=e.textContent??"";if(!n.trim())continue;const x=n.split(/(\S+)/),h=document.createDocumentFragment();for(let a=0;a<x.length;a+=2){const B=x[a],E=x[a+1];if(!E)continue;const O=x[a+3]===void 0?x[a+2]??"":"",$=document.createElement("span");$.className=R.word,$.textContent=B+E+O,h.appendChild($),y.push($)}e.parentNode.replaceChild(h,e)}if(requestAnimationFrame(()=>{typeof window<"u"&&Math.abs(window.scrollY-L)>2&&window.scrollTo({top:L,behavior:"instant"})}),y.length===0)return()=>{};const A=getComputedStyle(t).fontVariationSettings,w=J(A,Object.fromEntries(Object.entries(f).map(([e,[n]])=>[e,n])));y.forEach(e=>{e.style.fontVariationSettings=w,m&&P(e,m)});let p=-9999,q=-9999,g=!1,i=0,V=!0,s=null;function d(){if(!V)return;if(!g){y.forEach(n=>{S>0&&(n.style.transition=`font-variation-settings ${S}ms ease`),n.style.fontVariationSettings=w,m&&P(n,m)}),S>0&&(s!==null&&clearTimeout(s),s=setTimeout(()=>{y.forEach(n=>{n.style.transition=""}),s=null},S)),i=0;return}const e=y.map(n=>n.getBoundingClientRect());y.forEach((n,x)=>{n.style.transition="";const h=e[x],a=h.left+h.width/2,B=h.top+h.height/2,E=Math.sqrt((p-a)**2+(q-B)**2),j=Math.max(0,1-E/c),O=u==="quadratic"?j*j:j,$=T==="repel"?1-O:O,X={};for(const D of Object.keys(f)){const[I,nt]=f[D]??[300,500];X[D]=I+(nt-I)*$}n.style.fontVariationSettings=J(A,X),m&&U(n,m,O)}),i=requestAnimationFrame(d)}function N(e){p=e.clientX,q=e.clientY,g||(g=!0),i===0&&(i=requestAnimationFrame(d))}function C(){g=!1,i===0&&(i=requestAnimationFrame(d))}function F(e){e.touches.length!==0&&(p=e.touches[0].clientX,q=e.touches[0].clientY,g||(g=!0),i===0&&(i=requestAnimationFrame(d)))}function k(){g=!1,i===0&&(i=requestAnimationFrame(d))}const l=v==="document"?document:t;return l.addEventListener("mousemove",N),l.addEventListener("mouseleave",C),l.addEventListener("touchmove",F,{passive:!0}),l.addEventListener("touchend",k),()=>{V=!1,cancelAnimationFrame(i),s!==null&&clearTimeout(s),l.removeEventListener("mousemove",N),l.removeEventListener("mouseleave",C),l.removeEventListener("touchmove",F),l.removeEventListener("touchend",k),t.innerHTML=o}}function W(t){const o=M.useRef(null),r=M.useRef(null),f=M.useRef(t);f.current=t;const c=M.useRef(null),u=t.mode??"field",{axes:T,radius:v,falloff:m,magnetMode:S,wdthBoost:L,scope:b}=t,y=T?JSON.stringify(T):void 0,A=t.props?JSON.stringify(t.props):void 0,w=M.useCallback(()=>{const p=o.current;if(!p)return;r.current===null&&(r.current=z(p)),c.current&&(c.current(),c.current=null),(f.current.mode??"field")==="field"?c.current=Z(p,r.current,f.current):c.current=Q(p,r.current,f.current)},[u,y,v,m,S,L,b,A]);return M.useLayoutEffect(()=>(w(),()=>{c.current&&(c.current(),c.current=null)}),[w]),M.useEffect(()=>{document.fonts.ready.then(w)},[w]),o}const tt=M.forwardRef(function({children:o,as:r="p",className:f,style:c,...u},T){const v=W(u),m=M.useCallback(S=>{v.current=S,typeof T=="function"?T(S):T&&(T.current=S)},[T]);return K.jsx(r,{ref:m,className:f,style:c,children:o})});tt.displayName="MagnetTypeText";const et=M.forwardRef(function({children:o,as:r="p",className:f,style:c,minWeight:u=300,maxWeight:T=600,proximityRadius:v,spreadRadius:m,fixedAxes:S={}},L){const b=M.useRef(null),y=M.useRef(null),A=M.useRef([]),w=M.useCallback(s=>{b.current=s,typeof L=="function"?L(s):L&&(L.current=s)},[L]);function p(s){const d=[`'wght' ${s.toFixed(0)}`];for(const[N,C]of Object.entries(S))d.push(`'${N}' ${C}`);return d.join(", ")}M.useEffect(()=>{var k;if(!m)return;const s=b.current;if(!s)return;const d=[],N=document.createTreeWalker(s,NodeFilter.SHOW_TEXT),C=[];let F;for(;F=N.nextNode();)C.push(F);for(const l of C){const e=l.textContent??"";if(!/\S/.test(e))continue;const n=e.split(/(\s+)/),x=document.createDocumentFragment();for(const h of n)if(h!=="")if(/^\s+$/.test(h))x.appendChild(document.createTextNode(h));else{const a=document.createElement("span");a.style.fontVariationSettings=p(u),a.textContent=h,d.push(a),x.appendChild(a)}(k=l.parentNode)==null||k.replaceChild(x,l)}A.current=d},[]);function q(s,d){const N=b.current;if(!N)return;const C=N.getBoundingClientRect(),F=Math.max(C.left-s,0,s-C.right),k=Math.max(C.top-d,0,d-C.bottom),l=Math.sqrt(F*F+k*k);if(v!==void 0&&!m){const n=1-(1-Math.max(0,1-l/v))**2;N.style.fontVariationSettings=p(u+(T-u)*n);return}if(m){if(v!==void 0&&l>v){N.style.fontVariationSettings=p(u);for(const e of A.current)e.style.fontVariationSettings=p(u);return}for(const e of A.current){const n=e.getBoundingClientRect(),x=(n.left+n.right)/2,h=(n.top+n.bottom)/2,a=Math.sqrt((s-x)**2+(d-h)**2),E=1-(1-Math.max(0,1-a/m))**2;e.style.fontVariationSettings=p(u+(T-u)*E)}}}const g=M.useCallback(s=>{y.current={x:s.clientX,y:s.clientY},q(s.clientX,s.clientY)},[u,T,v,m]),i=M.useCallback(()=>{y.current&&q(y.current.x,y.current.y)},[u,T,v,m]),V=M.useCallback(()=>{y.current=null;const s=b.current;s&&(s.style.fontVariationSettings=p(u));for(const d of A.current)d.style.fontVariationSettings=p(u)},[u]);return M.useEffect(()=>(window.addEventListener("mousemove",g,{passive:!0}),window.addEventListener("scroll",i,{passive:!0,capture:!0}),document.documentElement.addEventListener("mouseleave",V),()=>{window.removeEventListener("mousemove",g),window.removeEventListener("scroll",i,{capture:!0}),document.documentElement.removeEventListener("mouseleave",V)}),[g,i,V]),K.jsx(r,{ref:w,className:f,style:{fontVariationSettings:p(u),...c},children:o})});et.displayName="MagnetBlock";exports.MAGNET_TYPE_CLASSES=R;exports.MagnetBlock=et;exports.MagnetTypeText=tt;exports.applyMagnetType=Q;exports.getCleanHTML=z;exports.removeMagnetType=ot;exports.startMagnetType=Z;exports.useMagnetType=W;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { default as default_2 } from 'react';
|
|
2
|
+
import { ForwardRefExoticComponent } from 'react';
|
|
3
|
+
import { RefAttributes } from 'react';
|
|
2
4
|
import { RefObject } from 'react';
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -38,6 +40,28 @@ export declare const MAGNET_TYPE_CLASSES: {
|
|
|
38
40
|
readonly probe: "mt-probe";
|
|
39
41
|
};
|
|
40
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Drop-in block element with cursor-proximity variable font weight variation.
|
|
45
|
+
* Accepts any ReactNode — use spreadRadius for per-word spread, proximityRadius
|
|
46
|
+
* for a whole-element proximity gate, or combine both.
|
|
47
|
+
*/
|
|
48
|
+
export declare const MagnetBlock: ForwardRefExoticComponent<MagnetBlockProps & RefAttributes<HTMLElement>>;
|
|
49
|
+
|
|
50
|
+
export declare interface MagnetBlockProps {
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
/** HTML element to render. Default: 'p' */
|
|
53
|
+
as?: React.ElementType;
|
|
54
|
+
className?: string;
|
|
55
|
+
style?: React.CSSProperties;
|
|
56
|
+
minWeight?: number;
|
|
57
|
+
maxWeight?: number;
|
|
58
|
+
/** Pixel distance from the element edge within which the whole-element effect activates */
|
|
59
|
+
proximityRadius?: number;
|
|
60
|
+
/** Pixel distance from the cursor within which each word's weight rises to max */
|
|
61
|
+
spreadRadius?: number;
|
|
62
|
+
fixedAxes?: Record<string, number>;
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
/** Whether cursor proximity attracts toward peak or repels toward rest */
|
|
42
66
|
export declare type MagnetModeType = 'attract' | 'repel';
|
|
43
67
|
|
|
@@ -104,6 +128,15 @@ export declare interface MagnetTypeOptions {
|
|
|
104
128
|
* Risk 3 characters receive wdthBoost × (3/3) = full boost at peak.
|
|
105
129
|
*/
|
|
106
130
|
wdthBoost?: number;
|
|
131
|
+
/**
|
|
132
|
+
* Duration in milliseconds for the CSS transition back to rest values when the
|
|
133
|
+
* cursor leaves (mouseleave / touchend). Default: 0 (instant snap, no transition).
|
|
134
|
+
*
|
|
135
|
+
* When > 0, sets `transition: font-variation-settings <transitionMs>ms ease` on
|
|
136
|
+
* each span at leave time, then removes the transition property after the duration.
|
|
137
|
+
* On mousemove / touchmove the transition is cleared so live tracking is not delayed.
|
|
138
|
+
*/
|
|
139
|
+
transitionMs?: number;
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useRef as
|
|
2
|
-
import { jsx as
|
|
3
|
-
const
|
|
1
|
+
import { useRef as k, useCallback as j, useLayoutEffect as tt, useEffect as _, forwardRef as z } from "react";
|
|
2
|
+
import { jsx as Q } from "react/jsx-runtime";
|
|
3
|
+
const G = {
|
|
4
4
|
i: 3,
|
|
5
5
|
l: 3,
|
|
6
6
|
1: 3,
|
|
@@ -22,14 +22,14 @@ const D = {
|
|
|
22
22
|
c: 1,
|
|
23
23
|
e: 1
|
|
24
24
|
// similar bowls
|
|
25
|
-
},
|
|
25
|
+
}, I = {
|
|
26
26
|
/** Applied to each word span in field mode */
|
|
27
27
|
word: "mt-word",
|
|
28
28
|
/** Applied to each character span in legibility mode */
|
|
29
29
|
char: "mt-char",
|
|
30
30
|
/** Applied to measurement probe spans (never in final output) */
|
|
31
31
|
probe: "mt-probe"
|
|
32
|
-
},
|
|
32
|
+
}, Y = {
|
|
33
33
|
axes: { wght: [300, 500] },
|
|
34
34
|
radius: 120,
|
|
35
35
|
falloff: "quadratic",
|
|
@@ -37,196 +37,321 @@ const D = {
|
|
|
37
37
|
wdthBoost: 6,
|
|
38
38
|
scope: "document"
|
|
39
39
|
};
|
|
40
|
-
function
|
|
41
|
-
return t.nodeType === Node.TEXT_NODE ?
|
|
40
|
+
function J(t, o = []) {
|
|
41
|
+
return t.nodeType === Node.TEXT_NODE ? o.push(t) : t.childNodes.forEach((r) => J(r, o)), o;
|
|
42
42
|
}
|
|
43
|
-
function
|
|
44
|
-
if (!t || t === "normal") return `"${
|
|
45
|
-
const
|
|
46
|
-
return
|
|
43
|
+
function D(t, o, r) {
|
|
44
|
+
if (!t || t === "normal") return `"${o}" ${r}`;
|
|
45
|
+
const l = new RegExp(`(["'])${o}\\1\\s+[\\d.eE+-]+`), c = `"${o}" ${r}`;
|
|
46
|
+
return l.test(t) ? t.replace(l, c) : `${t}, ${c}`;
|
|
47
47
|
}
|
|
48
|
-
function
|
|
49
|
-
let
|
|
50
|
-
for (const [
|
|
51
|
-
|
|
52
|
-
return
|
|
48
|
+
function R(t, o) {
|
|
49
|
+
let r = t;
|
|
50
|
+
for (const [l, c] of Object.entries(o))
|
|
51
|
+
r = D(r, l, c);
|
|
52
|
+
return r;
|
|
53
53
|
}
|
|
54
|
-
function
|
|
55
|
-
if (
|
|
56
|
-
const [
|
|
57
|
-
t.style.opacity = String(
|
|
54
|
+
function Z(t, o, r) {
|
|
55
|
+
if (o.opacity !== void 0) {
|
|
56
|
+
const [l, c] = o.opacity;
|
|
57
|
+
t.style.opacity = String(l + (c - l) * r);
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
o.italic === !0 && (t.style.fontStyle = r > 0.5 ? "italic" : "");
|
|
60
60
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
61
|
+
function P(t, o) {
|
|
62
|
+
o.opacity !== void 0 && (t.style.opacity = String(o.opacity[0])), o.italic === !0 && (t.style.fontStyle = "");
|
|
63
63
|
}
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
`.${
|
|
64
|
+
function et(t) {
|
|
65
|
+
const o = t.cloneNode(!0), r = o.querySelectorAll(
|
|
66
|
+
`.${I.word}, .${I.char}`
|
|
67
67
|
);
|
|
68
|
-
return Array.from(
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
for (;
|
|
72
|
-
|
|
68
|
+
return Array.from(r).reverse().forEach((c) => {
|
|
69
|
+
const u = c.parentNode;
|
|
70
|
+
if (u) {
|
|
71
|
+
for (; c.firstChild; ) u.insertBefore(c.firstChild, c);
|
|
72
|
+
u.removeChild(c);
|
|
73
73
|
}
|
|
74
|
-
}),
|
|
74
|
+
}), o.innerHTML;
|
|
75
75
|
}
|
|
76
|
-
function
|
|
77
|
-
t.innerHTML =
|
|
76
|
+
function ut(t, o) {
|
|
77
|
+
t.innerHTML = o;
|
|
78
78
|
}
|
|
79
|
-
function
|
|
79
|
+
function nt(t, o, r = {}) {
|
|
80
80
|
if (typeof window > "u") return () => {
|
|
81
81
|
};
|
|
82
82
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches)
|
|
83
|
-
return t.innerHTML =
|
|
83
|
+
return t.innerHTML = o, () => {
|
|
84
84
|
};
|
|
85
|
-
const
|
|
86
|
-
t.innerHTML =
|
|
87
|
-
const
|
|
88
|
-
for (const
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
91
|
-
const
|
|
92
|
-
for (const
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
const
|
|
96
|
-
|
|
85
|
+
const l = r.wdthBoost ?? Y.wdthBoost, c = r.radius ?? Y.radius, u = r.falloff ?? Y.falloff, w = r.scope ?? Y.scope, v = r.props, m = r.transitionMs ?? 0, x = window.scrollY;
|
|
86
|
+
t.innerHTML = o;
|
|
87
|
+
const L = getComputedStyle(t).fontVariationSettings, N = L.match(/"wdth"\s+([\d.eE+-]+)/), y = N ? parseFloat(N[1]) : 100, A = J(t), M = [];
|
|
88
|
+
for (const e of A) {
|
|
89
|
+
const n = e.textContent ?? "";
|
|
90
|
+
if (!n || !n.split("").some((a) => a in G)) continue;
|
|
91
|
+
const h = document.createDocumentFragment();
|
|
92
|
+
for (const a of n) {
|
|
93
|
+
const $ = G[a];
|
|
94
|
+
if ($ === void 0) {
|
|
95
|
+
const E = h.lastChild;
|
|
96
|
+
E && E.nodeType === Node.TEXT_NODE ? E.textContent += a : h.appendChild(document.createTextNode(a));
|
|
97
97
|
} else {
|
|
98
|
-
const
|
|
99
|
-
|
|
98
|
+
const E = document.createElement("span");
|
|
99
|
+
E.className = I.char, E.style.fontVariationSettings = D(L, "wdth", y), E.textContent = a, h.appendChild(E), M.push({ span: E, riskLevel: $ });
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
e.parentNode.replaceChild(h, e);
|
|
103
103
|
}
|
|
104
104
|
if (requestAnimationFrame(() => {
|
|
105
|
-
typeof window < "u" && Math.abs(window.scrollY -
|
|
106
|
-
}),
|
|
105
|
+
typeof window < "u" && Math.abs(window.scrollY - x) > 2 && window.scrollTo({ top: x, behavior: "instant" });
|
|
106
|
+
}), M.length === 0) return () => {
|
|
107
107
|
};
|
|
108
|
-
|
|
109
|
-
let
|
|
110
|
-
function
|
|
111
|
-
if (!
|
|
112
|
-
if (!
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}),
|
|
108
|
+
v && M.forEach(({ span: e }) => P(e, v));
|
|
109
|
+
let p = -9999, q = -9999, g = !1, i = 0, V = !0, s = null;
|
|
110
|
+
function d() {
|
|
111
|
+
if (!V) return;
|
|
112
|
+
if (!g) {
|
|
113
|
+
M.forEach(({ span: n }) => {
|
|
114
|
+
m > 0 && (n.style.transition = `font-variation-settings ${m}ms ease`), n.style.fontVariationSettings = D(L, "wdth", y), v && P(n, v);
|
|
115
|
+
}), m > 0 && (s !== null && clearTimeout(s), s = setTimeout(() => {
|
|
116
|
+
M.forEach(({ span: n }) => {
|
|
117
|
+
n.style.transition = "";
|
|
118
|
+
}), s = null;
|
|
119
|
+
}, m)), i = 0;
|
|
116
120
|
return;
|
|
117
121
|
}
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
const e = M.map(({ span: n }) => n.getBoundingClientRect());
|
|
123
|
+
M.forEach(({ span: n, riskLevel: T }, h) => {
|
|
124
|
+
n.style.transition = "";
|
|
125
|
+
const a = e[h], $ = a.left + a.width / 2, E = a.top + a.height / 2, X = Math.sqrt((p - $) ** 2 + (q - E) ** 2), B = Math.max(0, 1 - X / c), O = u === "quadratic" ? B * B : B, H = l * (T / 3) * O;
|
|
126
|
+
n.style.fontVariationSettings = D(L, "wdth", y + H), v && Z(n, v, O);
|
|
127
|
+
}), i = requestAnimationFrame(d);
|
|
123
128
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
129
|
+
function C(e) {
|
|
130
|
+
p = e.clientX, q = e.clientY, g || (g = !0), i === 0 && (i = requestAnimationFrame(d));
|
|
126
131
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
132
|
+
function S() {
|
|
133
|
+
g = !1, i === 0 && (i = requestAnimationFrame(d));
|
|
129
134
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
function F(e) {
|
|
136
|
+
e.touches.length !== 0 && (p = e.touches[0].clientX, q = e.touches[0].clientY, g || (g = !0), i === 0 && (i = requestAnimationFrame(d)));
|
|
137
|
+
}
|
|
138
|
+
function b() {
|
|
139
|
+
g = !1, i === 0 && (i = requestAnimationFrame(d));
|
|
140
|
+
}
|
|
141
|
+
const f = w === "document" ? document : t;
|
|
142
|
+
return f.addEventListener("mousemove", C), f.addEventListener("mouseleave", S), f.addEventListener("touchmove", F, { passive: !0 }), f.addEventListener("touchend", b), () => {
|
|
143
|
+
V = !1, cancelAnimationFrame(i), s !== null && clearTimeout(s), f.removeEventListener("mousemove", C), f.removeEventListener("mouseleave", S), f.removeEventListener("touchmove", F), f.removeEventListener("touchend", b), t.innerHTML = o;
|
|
133
144
|
};
|
|
134
145
|
}
|
|
135
|
-
function
|
|
146
|
+
function ot(t, o, r = {}) {
|
|
136
147
|
if (typeof window > "u") return () => {
|
|
137
148
|
};
|
|
138
149
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches)
|
|
139
|
-
return t.innerHTML =
|
|
150
|
+
return t.innerHTML = o, () => {
|
|
140
151
|
};
|
|
141
|
-
const
|
|
142
|
-
t.innerHTML =
|
|
143
|
-
const
|
|
144
|
-
for (const
|
|
145
|
-
const
|
|
146
|
-
if (!
|
|
147
|
-
const
|
|
148
|
-
for (let
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
151
|
-
const
|
|
152
|
-
|
|
152
|
+
const l = r.axes ?? Y.axes, c = r.radius ?? Y.radius, u = r.falloff ?? Y.falloff, w = r.magnetMode ?? Y.magnetMode, v = r.scope ?? Y.scope, m = r.props, x = r.transitionMs ?? 0, L = window.scrollY;
|
|
153
|
+
t.innerHTML = o;
|
|
154
|
+
const N = J(t), y = [];
|
|
155
|
+
for (const e of N) {
|
|
156
|
+
const n = e.textContent ?? "";
|
|
157
|
+
if (!n.trim()) continue;
|
|
158
|
+
const T = n.split(/(\S+)/), h = document.createDocumentFragment();
|
|
159
|
+
for (let a = 0; a < T.length; a += 2) {
|
|
160
|
+
const $ = T[a], E = T[a + 1];
|
|
161
|
+
if (!E) continue;
|
|
162
|
+
const B = T[a + 3] === void 0 ? T[a + 2] ?? "" : "", O = document.createElement("span");
|
|
163
|
+
O.className = I.word, O.textContent = $ + E + B, h.appendChild(O), y.push(O);
|
|
153
164
|
}
|
|
154
|
-
|
|
165
|
+
e.parentNode.replaceChild(h, e);
|
|
155
166
|
}
|
|
156
167
|
if (requestAnimationFrame(() => {
|
|
157
|
-
typeof window < "u" && Math.abs(window.scrollY -
|
|
158
|
-
}),
|
|
168
|
+
typeof window < "u" && Math.abs(window.scrollY - L) > 2 && window.scrollTo({ top: L, behavior: "instant" });
|
|
169
|
+
}), y.length === 0) return () => {
|
|
159
170
|
};
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
Object.fromEntries(Object.entries(
|
|
171
|
+
const A = getComputedStyle(t).fontVariationSettings, M = R(
|
|
172
|
+
A,
|
|
173
|
+
Object.fromEntries(Object.entries(l).map(([e, [n]]) => [e, n]))
|
|
163
174
|
);
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
y.forEach((e) => {
|
|
176
|
+
e.style.fontVariationSettings = M, m && P(e, m);
|
|
166
177
|
});
|
|
167
|
-
let
|
|
168
|
-
function
|
|
169
|
-
if (!
|
|
170
|
-
if (!
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}),
|
|
178
|
+
let p = -9999, q = -9999, g = !1, i = 0, V = !0, s = null;
|
|
179
|
+
function d() {
|
|
180
|
+
if (!V) return;
|
|
181
|
+
if (!g) {
|
|
182
|
+
y.forEach((n) => {
|
|
183
|
+
x > 0 && (n.style.transition = `font-variation-settings ${x}ms ease`), n.style.fontVariationSettings = M, m && P(n, m);
|
|
184
|
+
}), x > 0 && (s !== null && clearTimeout(s), s = setTimeout(() => {
|
|
185
|
+
y.forEach((n) => {
|
|
186
|
+
n.style.transition = "";
|
|
187
|
+
}), s = null;
|
|
188
|
+
}, x)), i = 0;
|
|
174
189
|
return;
|
|
175
190
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
191
|
+
const e = y.map((n) => n.getBoundingClientRect());
|
|
192
|
+
y.forEach((n, T) => {
|
|
193
|
+
n.style.transition = "";
|
|
194
|
+
const h = e[T], a = h.left + h.width / 2, $ = h.top + h.height / 2, E = Math.sqrt((p - a) ** 2 + (q - $) ** 2), X = Math.max(0, 1 - E / c), B = u === "quadratic" ? X * X : X, O = w === "repel" ? 1 - B : B, H = {};
|
|
195
|
+
for (const K of Object.keys(l)) {
|
|
196
|
+
const [U, W] = l[K] ?? [300, 500];
|
|
197
|
+
H[K] = U + (W - U) * O;
|
|
182
198
|
}
|
|
183
|
-
|
|
184
|
-
}),
|
|
199
|
+
n.style.fontVariationSettings = R(A, H), m && Z(n, m, B);
|
|
200
|
+
}), i = requestAnimationFrame(d);
|
|
201
|
+
}
|
|
202
|
+
function C(e) {
|
|
203
|
+
p = e.clientX, q = e.clientY, g || (g = !0), i === 0 && (i = requestAnimationFrame(d));
|
|
185
204
|
}
|
|
186
|
-
function
|
|
187
|
-
g =
|
|
205
|
+
function S() {
|
|
206
|
+
g = !1, i === 0 && (i = requestAnimationFrame(d));
|
|
188
207
|
}
|
|
189
|
-
function
|
|
190
|
-
|
|
208
|
+
function F(e) {
|
|
209
|
+
e.touches.length !== 0 && (p = e.touches[0].clientX, q = e.touches[0].clientY, g || (g = !0), i === 0 && (i = requestAnimationFrame(d)));
|
|
191
210
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
function b() {
|
|
212
|
+
g = !1, i === 0 && (i = requestAnimationFrame(d));
|
|
213
|
+
}
|
|
214
|
+
const f = v === "document" ? document : t;
|
|
215
|
+
return f.addEventListener("mousemove", C), f.addEventListener("mouseleave", S), f.addEventListener("touchmove", F, { passive: !0 }), f.addEventListener("touchend", b), () => {
|
|
216
|
+
V = !1, cancelAnimationFrame(i), s !== null && clearTimeout(s), f.removeEventListener("mousemove", C), f.removeEventListener("mouseleave", S), f.removeEventListener("touchmove", F), f.removeEventListener("touchend", b), t.innerHTML = o;
|
|
195
217
|
};
|
|
196
218
|
}
|
|
197
|
-
function
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
|
|
204
|
-
}, [
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
}), [
|
|
208
|
-
document.fonts.ready.then(
|
|
209
|
-
}, [
|
|
219
|
+
function rt(t) {
|
|
220
|
+
const o = k(null), r = k(null), l = k(t);
|
|
221
|
+
l.current = t;
|
|
222
|
+
const c = k(null), u = t.mode ?? "field", { axes: w, radius: v, falloff: m, magnetMode: x, wdthBoost: L, scope: N } = t, y = w ? JSON.stringify(w) : void 0, A = t.props ? JSON.stringify(t.props) : void 0, M = j(() => {
|
|
223
|
+
const p = o.current;
|
|
224
|
+
if (!p) return;
|
|
225
|
+
r.current === null && (r.current = et(p)), c.current && (c.current(), c.current = null), (l.current.mode ?? "field") === "field" ? c.current = ot(p, r.current, l.current) : c.current = nt(p, r.current, l.current);
|
|
226
|
+
}, [u, y, v, m, x, L, N, A]);
|
|
227
|
+
return tt(() => (M(), () => {
|
|
228
|
+
c.current && (c.current(), c.current = null);
|
|
229
|
+
}), [M]), _(() => {
|
|
230
|
+
document.fonts.ready.then(M);
|
|
231
|
+
}, [M]), o;
|
|
210
232
|
}
|
|
211
|
-
const
|
|
212
|
-
function({ children:
|
|
213
|
-
const
|
|
214
|
-
(
|
|
215
|
-
|
|
233
|
+
const st = z(
|
|
234
|
+
function({ children: o, as: r = "p", className: l, style: c, ...u }, w) {
|
|
235
|
+
const v = rt(u), m = j(
|
|
236
|
+
(x) => {
|
|
237
|
+
v.current = x, typeof w == "function" ? w(x) : w && (w.current = x);
|
|
238
|
+
},
|
|
239
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
240
|
+
[w]
|
|
241
|
+
);
|
|
242
|
+
return /* @__PURE__ */ Q(r, { ref: m, className: l, style: c, children: o });
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
st.displayName = "MagnetTypeText";
|
|
246
|
+
const ct = z(
|
|
247
|
+
function({
|
|
248
|
+
children: o,
|
|
249
|
+
as: r = "p",
|
|
250
|
+
className: l,
|
|
251
|
+
style: c,
|
|
252
|
+
minWeight: u = 300,
|
|
253
|
+
maxWeight: w = 600,
|
|
254
|
+
proximityRadius: v,
|
|
255
|
+
spreadRadius: m,
|
|
256
|
+
fixedAxes: x = {}
|
|
257
|
+
}, L) {
|
|
258
|
+
const N = k(null), y = k(null), A = k([]), M = j(
|
|
259
|
+
(s) => {
|
|
260
|
+
N.current = s, typeof L == "function" ? L(s) : L && (L.current = s);
|
|
216
261
|
},
|
|
217
262
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
218
|
-
[
|
|
263
|
+
[L]
|
|
264
|
+
);
|
|
265
|
+
function p(s) {
|
|
266
|
+
const d = [`'wght' ${s.toFixed(0)}`];
|
|
267
|
+
for (const [C, S] of Object.entries(x)) d.push(`'${C}' ${S}`);
|
|
268
|
+
return d.join(", ");
|
|
269
|
+
}
|
|
270
|
+
_(() => {
|
|
271
|
+
var b;
|
|
272
|
+
if (!m) return;
|
|
273
|
+
const s = N.current;
|
|
274
|
+
if (!s) return;
|
|
275
|
+
const d = [], C = document.createTreeWalker(s, NodeFilter.SHOW_TEXT), S = [];
|
|
276
|
+
let F;
|
|
277
|
+
for (; F = C.nextNode(); ) S.push(F);
|
|
278
|
+
for (const f of S) {
|
|
279
|
+
const e = f.textContent ?? "";
|
|
280
|
+
if (!/\S/.test(e)) continue;
|
|
281
|
+
const n = e.split(/(\s+)/), T = document.createDocumentFragment();
|
|
282
|
+
for (const h of n)
|
|
283
|
+
if (h !== "")
|
|
284
|
+
if (/^\s+$/.test(h))
|
|
285
|
+
T.appendChild(document.createTextNode(h));
|
|
286
|
+
else {
|
|
287
|
+
const a = document.createElement("span");
|
|
288
|
+
a.style.fontVariationSettings = p(u), a.textContent = h, d.push(a), T.appendChild(a);
|
|
289
|
+
}
|
|
290
|
+
(b = f.parentNode) == null || b.replaceChild(T, f);
|
|
291
|
+
}
|
|
292
|
+
A.current = d;
|
|
293
|
+
}, []);
|
|
294
|
+
function q(s, d) {
|
|
295
|
+
const C = N.current;
|
|
296
|
+
if (!C) return;
|
|
297
|
+
const S = C.getBoundingClientRect(), F = Math.max(S.left - s, 0, s - S.right), b = Math.max(S.top - d, 0, d - S.bottom), f = Math.sqrt(F * F + b * b);
|
|
298
|
+
if (v !== void 0 && !m) {
|
|
299
|
+
const n = 1 - (1 - Math.max(0, 1 - f / v)) ** 2;
|
|
300
|
+
C.style.fontVariationSettings = p(u + (w - u) * n);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (m) {
|
|
304
|
+
if (v !== void 0 && f > v) {
|
|
305
|
+
C.style.fontVariationSettings = p(u);
|
|
306
|
+
for (const e of A.current) e.style.fontVariationSettings = p(u);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
for (const e of A.current) {
|
|
310
|
+
const n = e.getBoundingClientRect(), T = (n.left + n.right) / 2, h = (n.top + n.bottom) / 2, a = Math.sqrt((s - T) ** 2 + (d - h) ** 2), E = 1 - (1 - Math.max(0, 1 - a / m)) ** 2;
|
|
311
|
+
e.style.fontVariationSettings = p(u + (w - u) * E);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const g = j(
|
|
316
|
+
(s) => {
|
|
317
|
+
y.current = { x: s.clientX, y: s.clientY }, q(s.clientX, s.clientY);
|
|
318
|
+
},
|
|
319
|
+
[u, w, v, m]
|
|
320
|
+
), i = j(
|
|
321
|
+
() => {
|
|
322
|
+
y.current && q(y.current.x, y.current.y);
|
|
323
|
+
},
|
|
324
|
+
[u, w, v, m]
|
|
325
|
+
), V = j(
|
|
326
|
+
() => {
|
|
327
|
+
y.current = null;
|
|
328
|
+
const s = N.current;
|
|
329
|
+
s && (s.style.fontVariationSettings = p(u));
|
|
330
|
+
for (const d of A.current) d.style.fontVariationSettings = p(u);
|
|
331
|
+
},
|
|
332
|
+
[u]
|
|
333
|
+
);
|
|
334
|
+
return _(() => (window.addEventListener("mousemove", g, { passive: !0 }), window.addEventListener("scroll", i, { passive: !0, capture: !0 }), document.documentElement.addEventListener("mouseleave", V), () => {
|
|
335
|
+
window.removeEventListener("mousemove", g), window.removeEventListener("scroll", i, { capture: !0 }), document.documentElement.removeEventListener("mouseleave", V);
|
|
336
|
+
}), [g, i, V]), /* @__PURE__ */ Q(
|
|
337
|
+
r,
|
|
338
|
+
{
|
|
339
|
+
ref: M,
|
|
340
|
+
className: l,
|
|
341
|
+
style: { fontVariationSettings: p(u), ...c },
|
|
342
|
+
children: o
|
|
343
|
+
}
|
|
219
344
|
);
|
|
220
|
-
return /* @__PURE__ */ G(n, { ref: d, className: c, style: o, children: e });
|
|
221
345
|
}
|
|
222
346
|
);
|
|
223
|
-
|
|
347
|
+
ct.displayName = "MagnetBlock";
|
|
224
348
|
export {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
349
|
+
I as MAGNET_TYPE_CLASSES,
|
|
350
|
+
ct as MagnetBlock,
|
|
351
|
+
st as MagnetTypeText,
|
|
352
|
+
nt as applyMagnetType,
|
|
353
|
+
et as getCleanHTML,
|
|
354
|
+
ut as removeMagnetType,
|
|
355
|
+
ot as startMagnetType,
|
|
356
|
+
rt as useMagnetType
|
|
232
357
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/magnettype",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Cursor-field per-word variable font axis variation and per-character legibility mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "^19.0.0",
|
|
40
40
|
"@vitejs/plugin-react": "^4.0.0",
|
|
41
|
-
"happy-dom": "^
|
|
42
|
-
"next": "16.2.
|
|
41
|
+
"happy-dom": "^20.9.0",
|
|
42
|
+
"next": "^16.2.6",
|
|
43
43
|
"react": "^19.0.0",
|
|
44
44
|
"typescript": "^5.0.0",
|
|
45
45
|
"vite": "^6.0.0",
|
|
@@ -83,5 +83,8 @@
|
|
|
83
83
|
"sideEffects": false,
|
|
84
84
|
"publishConfig": {
|
|
85
85
|
"access": "public"
|
|
86
|
+
},
|
|
87
|
+
"overrides": {
|
|
88
|
+
"postcss": ">=8.5.10"
|
|
86
89
|
}
|
|
87
90
|
}
|