craft-library 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +78 -2
- package/dist/cdn.js +3 -0
- package/dist/craft.js +1278 -0
- package/dist/elements.js +36 -0
- package/dist/glassnav.js +231 -0
- package/dist/index.js +43 -0
- package/dist/like.js +67 -0
- package/dist/magnet.js +308 -0
- package/dist/style.js +11 -0
- package/package.json +46 -4
package/dist/magnet.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// Magnetic icon · Craft Library, by Sara Lobo. magnet.css and magnet.js from the library, unchanged, around a mount.
|
|
2
|
+
import { css } from './style.js';
|
|
3
|
+
const CSS = "/* Magnetic icon · Craft Library, by Sara Lobo\n A dark disc with a brushed-metal rim and a search glyph (Figma: 40×40). Near the cursor the rim acts like metal\n near a strong magnet: the disc is drawn toward the pointer, the strained rim creases and trembles, its tip cracks\n into shards, and small voxels break off and fly to the pointer. Pairs with magnet.js.\n Everything is drawn in a 40-unit box, so --mag-size scales the whole piece, the pull included. */\n\n.mag {\n all: unset;\n position: relative;\n display: inline-grid;\n place-items: center;\n width: var(--mag-size, 40px);\n height: var(--mag-size, 40px);\n border-radius: 50%;\n color: #fff;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n transition: transform .2s cubic-bezier(.2, .8, .2, 1);\n}\n.mag:active {\n transform: scale(.94);\n}\n.mag:focus-visible {\n outline: 2px solid #9DB8FF;\n outline-offset: 4px;\n}\n/* the disc and its rim, redrawn by magnet.js; the stretch reaches past the button */\n.mag-ring {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n overflow: visible;\n pointer-events: none;\n}\n/* the shards and voxels: 46 units of room on every side of the 40-unit button (46 / 40 = 115%) */\n.mag-dust {\n position: absolute;\n left: -115%;\n top: -115%;\n width: 330%;\n height: 330%;\n pointer-events: none;\n}\n.mag-icon {\n position: relative;\n display: block;\n width: 50%;\n height: 50%;\n}\n";
|
|
4
|
+
const MARKUP = "<svg class=\"mag-ring\" viewBox=\"0 0 40 40\" aria-hidden=\"true\"><defs><linearGradient id=\"magMetal\" x1=\"32\" y1=\"3.5\" x2=\"20\" y2=\"40\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#999\"></stop><stop offset=\".452\" stop-color=\"#fff\"></stop><stop offset=\"1\" stop-color=\"#A2A2A2\"></stop></linearGradient><radialGradient id=\"magFade\" cx=\"20\" cy=\"20\" r=\"40\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".506\" stop-color=\"#fff\"></stop><stop offset=\".507\" stop-color=\"#000\"></stop></radialGradient><mask id=\"magMask\" maskUnits=\"userSpaceOnUse\" x=\"-30\" y=\"-30\" width=\"100\" height=\"100\"><rect x=\"-30\" y=\"-30\" width=\"100\" height=\"100\" fill=\"url(#magFade)\"></rect></mask></defs><path class=\"mag-rim\" mask=\"url(#magMask)\" d=\"M39.25 20C39.25 30.6315 30.6315 39.25 20 39.25C9.36852 39.25 .75 30.6315 .75 20C.75 9.36852 9.36852 .75 20 .75C30.6315 .75 39.25 9.36852 39.25 20Z\" fill=\"#fff\" fill-opacity=\".2\" stroke=\"url(#magMetal)\" stroke-width=\"1.5\"></path></svg><svg class=\"mag-icon\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\"><path d=\"M17.5 17.5L13.8807 13.8807M13.8807 13.8807C15.0871 12.6743 15.8333 11.0076 15.8333 9.16667C15.8333 5.48477 12.8486 2.5 9.16667 2.5C5.48477 2.5 2.5 5.48477 2.5 9.16667C2.5 12.8486 5.48477 15.8333 9.16667 15.8333C11.0076 15.8333 12.6743 15.0871 13.8807 13.8807Z\" stroke=\"currentColor\" stroke-width=\"1.66667\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg>";
|
|
5
|
+
let api;
|
|
6
|
+
function engine() {
|
|
7
|
+
if (api) return api;
|
|
8
|
+
const window = {};
|
|
9
|
+
// Magnetic icon · Craft Library, by Sara Lobo
|
|
10
|
+
// Every .mag on the page reads the pointer and its rim follows it while it is near: the disc is drawn toward it, the
|
|
11
|
+
// strained rim creases and trembles, the tip cracks into shards and voxels break off and fly to the pointer. Let go,
|
|
12
|
+
// and the rim springs back with a wobble while the loose voxels return to it. Pairs with magnet.css.
|
|
13
|
+
// Buttons added later: magnetize(root). To steer one without a mouse (a demo, a tour): magnetPoint(button, x, y) with
|
|
14
|
+
// a point on screen, and magnetPoint(button, null) to hand it back to the pointer.
|
|
15
|
+
(function () {
|
|
16
|
+
var C = 20, R = 19.25, N = 96; // the rim's centre and radius in the 40-unit box, and how many points draw it
|
|
17
|
+
var REACH = 44; // how far past the rim the magnet is felt
|
|
18
|
+
var PULL = 15, FALL = 1.1; // the furthest the rim reaches out, and how fast the pull drops with distance
|
|
19
|
+
var BODY = 0.3, SHIFT = 0.14; // how much of the disc goes with the tip, and how far the disc itself is drawn
|
|
20
|
+
var JAG = 0.18; // how deep the strained rim creases
|
|
21
|
+
var DENT = 3.5; // the furthest the rim is drawn in while the pointer is over the button
|
|
22
|
+
var RATE = 90, MAXDUST = 90; // voxels shed per second at full pull, and at most at once
|
|
23
|
+
var ROOM = 46; // room for the shards and voxels on every side of the button (matches .mag-dust)
|
|
24
|
+
var pointer = null, mags = [], raf = 0, last = 0, uid = 0;
|
|
25
|
+
|
|
26
|
+
if (matchMedia('(prefers-reduced-motion: reduce)').matches) { // less motion: the button stays as drawn
|
|
27
|
+
window.magnetize = window.magnetPoint = function () {};
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clamp(x, a, b) { return Math.max(a, Math.min(b, x)); }
|
|
32
|
+
function wrap(a) { return Math.atan2(Math.sin(a), Math.cos(a)); }
|
|
33
|
+
function bell(x, s) { return Math.exp(-x * x / (2 * s * s)); }
|
|
34
|
+
function spike(x, s) { return Math.exp((0.16 - Math.sqrt(x * x + 0.0256)) / s); } // a rounded peak with flared shoulders, like metal drawn out
|
|
35
|
+
function hash(x, y) { var h = Math.sin(x * 127.1 + y * 311.7) * 43758.5453; return h - Math.floor(h); }
|
|
36
|
+
function n2(x) { return Math.round(x * 100) / 100; }
|
|
37
|
+
// a quick, smooth random shiver for point i, between -0.5 and 0.5
|
|
38
|
+
function tremble(i, now) { var f = now / 1000 * 14, k = Math.floor(f), u = f - k; u = u * u * (3 - 2 * u); return hash(i, k) * (1 - u) + hash(i, k + 1) * u - 0.5; }
|
|
39
|
+
|
|
40
|
+
// the rim's radius at angle t, around the disc's (drawn) centre: pulled out toward the pointer, the disc going with it,
|
|
41
|
+
// or pressed in; the far side gives a little
|
|
42
|
+
function width(m) { return 0.55 - 0.25 * clamp(m.d / PULL, 0, 1); }
|
|
43
|
+
function radius(m, t) {
|
|
44
|
+
var dt = wrap(t - m.a), d = m.d;
|
|
45
|
+
return R + (d > 0 ? d * (spike(dt, width(m)) + BODY * bell(dt, 1)) : d * bell(dt, 0.7)) - 0.12 * d * bell(wrap(dt - Math.PI), 1.3);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// the rim as one closed curve: smooth where it is calm (Catmull-Rom turned into cubic Béziers), creased into straight,
|
|
49
|
+
// trembling facets where the magnet strains it
|
|
50
|
+
function rimPath(m, now) {
|
|
51
|
+
var pts = [], strain = clamp((Math.abs(m.d) + 3 * m.heat) / PULL, 0, 1), i, t, r, env, sharp, n, p, a, b, c, e;
|
|
52
|
+
for (i = 0; i < N; i++) {
|
|
53
|
+
t = i / N * 2 * Math.PI;
|
|
54
|
+
r = radius(m, t);
|
|
55
|
+
env = bell(wrap(t - m.a), 0.6) * strain;
|
|
56
|
+
sharp = env > 0.12;
|
|
57
|
+
if (sharp && i % 3) continue;
|
|
58
|
+
if (sharp) r += 2 * JAG * PULL * env * tremble(i, now);
|
|
59
|
+
pts.push([m.cx + r * Math.cos(t), m.cy + r * Math.sin(t), sharp]);
|
|
60
|
+
}
|
|
61
|
+
n = pts.length;
|
|
62
|
+
p = 'M' + n2(pts[0][0]) + ' ' + n2(pts[0][1]);
|
|
63
|
+
for (i = 0; i < n; i++) {
|
|
64
|
+
a = pts[(i + n - 1) % n]; b = pts[i]; c = pts[(i + 1) % n]; e = pts[(i + 2) % n];
|
|
65
|
+
p += b[2] || c[2] ? 'L' + n2(c[0]) + ' ' + n2(c[1])
|
|
66
|
+
: 'C' + n2(b[0] + (c[0] - a[0]) / 6) + ' ' + n2(b[1] + (c[1] - a[1]) / 6) + ' ' +
|
|
67
|
+
n2(c[0] - (e[0] - b[0]) / 6) + ' ' + n2(c[1] - (e[1] - b[1]) / 6) + ' ' + n2(c[0]) + ' ' + n2(c[1]);
|
|
68
|
+
}
|
|
69
|
+
return p + 'Z';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// where the rim wants to be: outside, it reaches toward the pointer (never past it); over the button, it is drawn in
|
|
73
|
+
function aim(m) {
|
|
74
|
+
var p = m.ghost || pointer;
|
|
75
|
+
if (!p) return null;
|
|
76
|
+
var k = 40 / m.box.width;
|
|
77
|
+
var x = (p.x - m.box.left) * k - C, y = (p.y - m.box.top) * k - C;
|
|
78
|
+
var dist = Math.hypot(x, y), edge = dist - R, near = 1 - Math.max(edge, 0) / REACH;
|
|
79
|
+
if (edge > REACH) return null;
|
|
80
|
+
var d = edge >= 0
|
|
81
|
+
? Math.min(edge * 0.9 / (1 + BODY), PULL * Math.pow(1 - edge / REACH, FALL))
|
|
82
|
+
: -Math.min(-edge * 0.85, DENT * clamp(dist / R, 0, 1));
|
|
83
|
+
// heat: how hard the field strains the metal, strongest at the rim and calm at the very centre
|
|
84
|
+
return { d: d, a: Math.atan2(y, x), x: x, y: y, dist: dist, near: near, heat: edge >= 0 ? near * near : clamp(dist / R, 0, 1) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// one tick of the springs; true once everything has come to rest
|
|
88
|
+
function step(m, dt) {
|
|
89
|
+
var t = m.t = aim(m), to = t ? t.d : 0, heat = t ? t.heat : 0, turn = 0, k, c, ix = 0, iy = 0, e;
|
|
90
|
+
// a spring toward the target; looser once the magnet lets go, so the metal overshoots and wobbles back
|
|
91
|
+
if (t) { k = 420; c = 30; } else { k = 260; c = 9; }
|
|
92
|
+
m.v += (k * (to - m.d) - c * m.v) * dt;
|
|
93
|
+
m.d += m.v * dt;
|
|
94
|
+
m.heat += (heat - m.heat) * (1 - Math.exp(-dt * 6));
|
|
95
|
+
if (t) {
|
|
96
|
+
// the stretched side turns toward the pointer; close to the centre the direction jitters, so it turns slower there
|
|
97
|
+
turn = wrap(t.a - m.a);
|
|
98
|
+
m.a += turn * (1 - Math.exp(-dt * 22 * clamp(t.dist / 8, 0, 1)));
|
|
99
|
+
// the glyph is drawn a little toward the pointer too
|
|
100
|
+
e = 1.4 * t.near * t.near / Math.max(t.dist, 1);
|
|
101
|
+
ix = t.x * e; iy = t.y * e;
|
|
102
|
+
}
|
|
103
|
+
e = 1 - Math.exp(-dt * 14);
|
|
104
|
+
m.ix += (ix - m.ix) * e; m.iy += (iy - m.iy) * e;
|
|
105
|
+
// the disc itself is drawn toward the magnet
|
|
106
|
+
e = SHIFT * Math.abs(m.d);
|
|
107
|
+
m.cx = C + e * Math.cos(m.a); m.cy = C + e * Math.sin(m.a);
|
|
108
|
+
return Math.abs(to - m.d) < 0.02 && Math.abs(m.v) < 0.05 && Math.abs(turn) < 0.002 && Math.abs(heat - m.heat) < 0.01 &&
|
|
109
|
+
Math.abs(ix - m.ix) < 0.01 && Math.abs(iy - m.iy) < 0.01;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// voxels break off where the magnet pulls hardest: more the further the rim is pulled, and more while the pointer moves
|
|
113
|
+
function shed(m, dt) {
|
|
114
|
+
var t = m.t, pull, th, r, x, y, dx, dy, l;
|
|
115
|
+
if (!t) { m.acc = 0; return; }
|
|
116
|
+
pull = m.d > 0 ? clamp(m.d / PULL, 0, 1) : 0.5 * clamp(-m.d / DENT, 0, 1);
|
|
117
|
+
m.acc += dt * RATE * Math.pow(pull, 1.4) * (0.35 + 0.65 * clamp(m.pace / 160, 0, 1));
|
|
118
|
+
for (; m.acc >= 1; m.acc--) {
|
|
119
|
+
if (m.dust.length >= MAXDUST) continue;
|
|
120
|
+
th = m.a + (Math.random() + Math.random() - 1) * width(m) * 1.4;
|
|
121
|
+
r = radius(m, th) - Math.random() * 1.2;
|
|
122
|
+
if (m.d > 0) r += 0.35 * m.d * clamp((r - R) / (m.d * (1 + BODY)), 0, 1); // off the shards, which have pulled away
|
|
123
|
+
x = m.cx + r * Math.cos(th); y = m.cy + r * Math.sin(th);
|
|
124
|
+
dx = C + t.x - x; dy = C + t.y - y; l = Math.hypot(dx, dy) || 1;
|
|
125
|
+
m.dust.push({ x: x, y: y, vx: dx / l * 12, vy: dy / l * 12, hx: C + R * Math.cos(th), hy: C + R * Math.sin(th),
|
|
126
|
+
s: 1.2 + Math.random() * 1.6, k: 1, age: 0, life: 0.6 + Math.random() * 0.6, back: Math.random() < 0.3,
|
|
127
|
+
spin: Math.random() < 0.5 ? -1 : 1, c: Math.random() < 0.35 ? 190 : 255 });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// loose voxels: drawn to the pointer and swallowed there; once the magnet lets go (and for the few that hold on), back to the rim
|
|
132
|
+
function drift(m, dt) {
|
|
133
|
+
var t = m.t, e = 1 - Math.exp(-dt * 5), k, p, home, tx, ty, dx, dy, dist, sp, sw;
|
|
134
|
+
for (k = m.dust.length - 1; k >= 0; k--) {
|
|
135
|
+
p = m.dust[k];
|
|
136
|
+
p.age += dt;
|
|
137
|
+
home = !t || (p.back && p.age > 0.18);
|
|
138
|
+
if (home && !p.ret) { p.ret = true; p.life = Math.max(p.life, p.age + 0.5); }
|
|
139
|
+
tx = home ? p.hx : C + t.x; ty = home ? p.hy : C + t.y;
|
|
140
|
+
dx = tx - p.x; dy = ty - p.y; dist = Math.hypot(dx, dy) || 0.001;
|
|
141
|
+
sp = (home ? 18 : 30) + 420 / (dist + 6);
|
|
142
|
+
sw = p.spin * sp * 0.4 * clamp(dist / 20, 0, 1); // a slight swirl, like filings along a field line
|
|
143
|
+
p.vx += (dx / dist * sp - dy / dist * sw - p.vx) * e;
|
|
144
|
+
p.vy += (dy / dist * sp + dx / dist * sw - p.vy) * e;
|
|
145
|
+
p.x += p.vx * dt; p.y += p.vy * dt;
|
|
146
|
+
p.k = 0.45 + 0.55 * clamp(dist / 12, 0, 1);
|
|
147
|
+
if (dist < (home ? 1 : 2)) p.age = Math.max(p.age, p.life - 0.1); // swallowed by the magnet, or back in the metal
|
|
148
|
+
if (p.age >= p.life) m.dust.splice(k, 1);
|
|
149
|
+
}
|
|
150
|
+
return m.dust.length > 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// where the drawn rim gives way to the shards: a clean crack line a little under halfway up the stretch
|
|
154
|
+
function crack(m) { var d = Math.max(m.d, 0); return [R + 1 + 0.45 * d, 0.04 * d + 0.001]; }
|
|
155
|
+
|
|
156
|
+
// the cracked tip: past the crack line the rim splits into shards that pull away toward the pointer, the ones nearest
|
|
157
|
+
// the point the most, each turned a little; as the rim springs back they close up again
|
|
158
|
+
function drawShards(m, g) {
|
|
159
|
+
if (m.d < 1) return;
|
|
160
|
+
var f = crack(m)[0], top = radius(m, m.a) - f, cuts = [], lo, hi, x, k, i, t0, t1, tc, r0, r1, ro, rm, q;
|
|
161
|
+
if (top < 0.5) return;
|
|
162
|
+
for (lo = 0; lo > -Math.PI && radius(m, m.a + lo) > f; lo -= 0.02);
|
|
163
|
+
for (hi = 0; hi < Math.PI && radius(m, m.a + hi) > f; hi += 0.02);
|
|
164
|
+
// the cracks ride with the tip, so the shards keep their shapes as it turns
|
|
165
|
+
for (x = -(0.08 + 0.06 * hash(9, 1)), k = 0; x > lo; x -= 0.15 + 0.15 * hash(k++, 2)) cuts.unshift(x);
|
|
166
|
+
cuts.unshift(lo);
|
|
167
|
+
for (x = 0.08 + 0.06 * hash(9, 3), k = 0; x < hi; x += 0.15 + 0.15 * hash(k++, 4)) cuts.push(x);
|
|
168
|
+
cuts.push(hi);
|
|
169
|
+
function at(t, r) { return [m.cx + r * Math.cos(m.a + t), m.cy + r * Math.sin(m.a + t)]; }
|
|
170
|
+
for (i = 0; i < cuts.length - 1; i++) {
|
|
171
|
+
t0 = cuts[i] + 0.012; t1 = cuts[i + 1] - 0.012;
|
|
172
|
+
if (t1 - t0 < 0.03) continue;
|
|
173
|
+
tc = (t0 + t1) / 2; r0 = radius(m, m.a + t0); r1 = radius(m, m.a + t1); ro = radius(m, m.a + tc);
|
|
174
|
+
q = clamp((ro - f) / top, 0, 1);
|
|
175
|
+
if (ro - f > 6) { // the long piece at the point breaks once more across
|
|
176
|
+
rm = f + (ro - f) * (0.4 + 0.2 * hash(i, 7));
|
|
177
|
+
shard(m, g, i * 2, q * 0.5, [at(t0, f), at(tc, f), at(t1, f), at(t1, Math.min(rm, r1)), at(t0, Math.min(rm, r0))], 0);
|
|
178
|
+
shard(m, g, i * 2 + 1, q, [at(t0, Math.min(rm, r0)), at(t1, Math.min(rm, r1)), at(t1, r1), at(tc, ro), at(t0, r0)], 2);
|
|
179
|
+
} else shard(m, g, i * 2, q, [at(t0, f), at(tc, f), at(t1, f), at(t1, r1), at(tc, ro), at(t0, r0)], 3);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// one shard; its rim edge starts at point `rim` of its outline (0: it carries none)
|
|
183
|
+
function shard(m, g, k, q, pts, rim) {
|
|
184
|
+
var cx = 0, cy = 0, l = m.d * (0.04 + 0.45 * Math.pow(q, 1.5)) * (0.8 + 0.4 * hash(k, 1)), ux, uy, tx, ty, dl, i;
|
|
185
|
+
pts.forEach(function (p) { cx += p[0] / pts.length; cy += p[1] / pts.length; });
|
|
186
|
+
// pulled partly outward, partly straight at the pointer
|
|
187
|
+
ux = cx - m.cx; uy = cy - m.cy; dl = Math.hypot(ux, uy) || 1; ux /= dl; uy /= dl;
|
|
188
|
+
if (m.t) { tx = C + m.t.x - cx; ty = C + m.t.y - cy; dl = Math.hypot(tx, ty) || 1; ux = (ux + tx / dl) / 2; uy = (uy + ty / dl) / 2; }
|
|
189
|
+
g.save();
|
|
190
|
+
g.translate(cx + ux * l, cy + uy * l); g.rotate((hash(k, 3) - 0.5) * 0.5 * q); g.translate(-cx, -cy);
|
|
191
|
+
g.beginPath(); g.moveTo(pts[0][0], pts[0][1]);
|
|
192
|
+
for (i = 1; i < pts.length; i++) g.lineTo(pts[i][0], pts[i][1]);
|
|
193
|
+
g.closePath();
|
|
194
|
+
g.fillStyle = 'rgba(255,255,255,0.2)'; g.fill();
|
|
195
|
+
g.lineWidth = 0.5; g.lineJoin = 'miter'; g.strokeStyle = 'rgba(255,255,255,0.2)'; g.stroke(); // the fresh break catches the light
|
|
196
|
+
if (rim) { // the piece of metal rim it carries
|
|
197
|
+
g.beginPath(); g.moveTo(pts[rim][0], pts[rim][1]);
|
|
198
|
+
for (i = rim + 1; i < pts.length; i++) g.lineTo(pts[i][0], pts[i][1]);
|
|
199
|
+
g.lineWidth = 1.5; g.strokeStyle = 'rgba(236,236,236,0.95)'; g.stroke();
|
|
200
|
+
}
|
|
201
|
+
g.restore();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function draw(m, now) {
|
|
205
|
+
var s = m.box.width / 40, dpr = Math.min(2, devicePixelRatio || 1), size = Math.round((40 + 2 * ROOM) * s * dpr), f = crack(m), g = m.g;
|
|
206
|
+
m.rim.setAttribute('d', rimPath(m, now));
|
|
207
|
+
// the drawn rim gives way at the crack line, where the shards take over
|
|
208
|
+
m.grad.setAttribute('cx', n2(m.cx)); m.grad.setAttribute('cy', n2(m.cy));
|
|
209
|
+
m.stops[0].setAttribute('offset', (f[0] / 40).toFixed(4));
|
|
210
|
+
m.stops[1].setAttribute('offset', ((f[0] + f[1]) / 40).toFixed(4));
|
|
211
|
+
if (m.icon) m.icon.style.transform = 'translate(' + n2((m.ix + m.cx - C) * s) + 'px,' + n2((m.iy + m.cy - C) * s) + 'px)';
|
|
212
|
+
if (m.cv.width !== size) m.cv.width = m.cv.height = size;
|
|
213
|
+
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
214
|
+
g.clearRect(0, 0, size, size);
|
|
215
|
+
g.setTransform(s * dpr, 0, 0, s * dpr, ROOM * s * dpr, ROOM * s * dpr);
|
|
216
|
+
drawShards(m, g);
|
|
217
|
+
m.dust.forEach(function (p) {
|
|
218
|
+
// never under 2 screen pixels, so the voxels still read on a small button
|
|
219
|
+
var q = p.age / p.life, a = 0.9 * Math.min(1, p.age / 0.06) * (1 - q * q * q), z = Math.max(p.s, 2 / s) * p.k;
|
|
220
|
+
g.fillStyle = 'rgba(' + p.c + ',' + p.c + ',' + p.c + ',' + n2(a) + ')';
|
|
221
|
+
g.fillRect(p.x - z / 2, p.y - z / 2, z, z);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function frame(now) {
|
|
226
|
+
var dt = Math.min((now - last) / 1000, 1 / 30), rest = true;
|
|
227
|
+
last = now;
|
|
228
|
+
mags = mags.filter(function (m) { return m.el.isConnected; }); // buttons taken off the page let go
|
|
229
|
+
mags.forEach(function (m) {
|
|
230
|
+
m.box = m.el.getBoundingClientRect();
|
|
231
|
+
if (!m.box.width) return;
|
|
232
|
+
// how fast the magnet moves, in units a second: a quick pass tears off more than a slow one
|
|
233
|
+
var p = m.ghost || pointer;
|
|
234
|
+
if (p && m.lp && dt > 0) m.pace += (Math.hypot(p.x - m.lp.x, p.y - m.lp.y) * 40 / m.box.width / dt - m.pace) * (1 - Math.exp(-dt * 8));
|
|
235
|
+
else m.pace *= Math.exp(-dt * 4);
|
|
236
|
+
m.lp = p ? { x: p.x, y: p.y } : null;
|
|
237
|
+
// two half steps keep the springs steady when frames come in slow
|
|
238
|
+
var still = step(m, dt / 2) & step(m, dt / 2);
|
|
239
|
+
shed(m, dt);
|
|
240
|
+
var live = drift(m, dt) || !still || (m.t && (Math.abs(m.d) > 0.5 || m.heat > 0.05));
|
|
241
|
+
if (live) { rest = false; draw(m, now); m.still = false; }
|
|
242
|
+
else if (!m.still) { draw(m, now); m.still = true; }
|
|
243
|
+
});
|
|
244
|
+
raf = rest ? 0 : requestAnimationFrame(frame);
|
|
245
|
+
}
|
|
246
|
+
function wake() { if (!raf) { last = performance.now(); raf = requestAnimationFrame(frame); } }
|
|
247
|
+
|
|
248
|
+
function wire(el) {
|
|
249
|
+
var svg = el.querySelector('.mag-ring'), icon = el.querySelector('.mag-icon'), cv;
|
|
250
|
+
if (el.__mag || !svg) return;
|
|
251
|
+
// each button gets its own ids, so several can live on one page
|
|
252
|
+
uid++;
|
|
253
|
+
svg.querySelectorAll('[id]').forEach(function (def) {
|
|
254
|
+
var id = def.id + '-' + uid;
|
|
255
|
+
svg.querySelectorAll('[fill],[stroke],[mask]').forEach(function (s) {
|
|
256
|
+
['fill', 'stroke', 'mask'].forEach(function (at) { if (s.getAttribute(at) === 'url(#' + def.id + ')') s.setAttribute(at, 'url(#' + id + ')'); });
|
|
257
|
+
});
|
|
258
|
+
def.id = id;
|
|
259
|
+
});
|
|
260
|
+
cv = document.createElement('canvas');
|
|
261
|
+
cv.className = 'mag-dust';
|
|
262
|
+
cv.setAttribute('aria-hidden', 'true');
|
|
263
|
+
el.insertBefore(cv, icon);
|
|
264
|
+
el.__mag = { el: el, rim: svg.querySelector('.mag-rim'), grad: svg.querySelector('radialGradient'), stops: svg.querySelectorAll('radialGradient stop'),
|
|
265
|
+
icon: icon, cv: cv, g: cv.getContext('2d'), dust: [], acc: 0, pace: 0, lp: null, ghost: null,
|
|
266
|
+
d: 0, v: 0, a: -Math.PI / 2, heat: 0, cx: C, cy: C, ix: 0, iy: 0, t: null, still: true };
|
|
267
|
+
mags.push(el.__mag);
|
|
268
|
+
}
|
|
269
|
+
window.magnetize = function (root) {
|
|
270
|
+
root = root || document;
|
|
271
|
+
if (root.matches && root.matches('.mag')) wire(root);
|
|
272
|
+
root.querySelectorAll('.mag').forEach(wire);
|
|
273
|
+
};
|
|
274
|
+
window.magnetPoint = function (el, x, y) {
|
|
275
|
+
wire(el);
|
|
276
|
+
if (!el.__mag) return;
|
|
277
|
+
el.__mag.ghost = x == null ? null : { x: x, y: y };
|
|
278
|
+
wake();
|
|
279
|
+
};
|
|
280
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { window.magnetize(); });
|
|
281
|
+
else window.magnetize();
|
|
282
|
+
|
|
283
|
+
function track(e) { pointer = { x: e.clientX, y: e.clientY }; wake(); }
|
|
284
|
+
function drop() { pointer = null; wake(); }
|
|
285
|
+
addEventListener('pointermove', track, { passive: true });
|
|
286
|
+
addEventListener('pointerdown', track, { passive: true });
|
|
287
|
+
addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') drop(); });
|
|
288
|
+
addEventListener('pointercancel', drop);
|
|
289
|
+
addEventListener('pointerout', function (e) { if (!e.relatedTarget) drop(); });
|
|
290
|
+
addEventListener('blur', drop);
|
|
291
|
+
addEventListener('scroll', wake, { passive: true }); // the page moves under a still pointer
|
|
292
|
+
})();
|
|
293
|
+
|
|
294
|
+
return (api = window);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// host: the element the button goes in. o: { label, size (px or any CSS length), onClick }
|
|
298
|
+
export function mountMagnet(host, o = {}) {
|
|
299
|
+
css('craft-magnet', CSS);
|
|
300
|
+
const M = engine();
|
|
301
|
+
host.innerHTML = '<button class="mag" type="button">' + MARKUP + '</button>';
|
|
302
|
+
const btn = host.firstChild;
|
|
303
|
+
btn.setAttribute('aria-label', o.label || 'Search');
|
|
304
|
+
if (o.size) btn.style.setProperty('--mag-size', typeof o.size === 'number' ? o.size + 'px' : String(o.size));
|
|
305
|
+
if (o.onClick) btn.addEventListener('click', o.onClick);
|
|
306
|
+
M.magnetize(btn);
|
|
307
|
+
return { button: btn, point: (x, y) => M.magnetPoint(btn, x, y), destroy() { host.innerHTML = ''; } };
|
|
308
|
+
}
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// each piece's CSS goes into the page once, the first time one is mounted
|
|
2
|
+
const done = new Set();
|
|
3
|
+
export function css(id, text) {
|
|
4
|
+
if (done.has(id) || typeof document === 'undefined') return;
|
|
5
|
+
done.add(id);
|
|
6
|
+
if (document.getElementById(id)) return;
|
|
7
|
+
const s = document.createElement('style');
|
|
8
|
+
s.id = id;
|
|
9
|
+
s.textContent = text;
|
|
10
|
+
document.head.appendChild(s);
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "craft-library",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Craft Library by Sara Lobo: a liquid glass nav bar, a bursting heart and a magnetic icon, for React or plain HTML",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./elements": "./dist/elements.js",
|
|
11
|
+
"./craft.js": "./dist/craft.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"./dist/elements.js",
|
|
19
|
+
"./dist/craft.js"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"react": {
|
|
26
|
+
"optional": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"esbuild": "0.24.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node build.mjs",
|
|
34
|
+
"prepublishOnly": "node build.mjs"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"react",
|
|
38
|
+
"web-components",
|
|
39
|
+
"liquid-glass",
|
|
40
|
+
"animation",
|
|
41
|
+
"micro-interaction",
|
|
42
|
+
"navbar",
|
|
43
|
+
"like-button"
|
|
44
|
+
],
|
|
45
|
+
"author": "Sara Lobo",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"homepage": "https://claude.ai/artifact/LBBWCfEHJGvfBSynV3wDuR"
|
|
48
|
+
}
|