craft-library 0.2.0 → 0.4.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 +45 -3
- package/dist/cdn.js +4 -4
- package/dist/craft.js +442 -2
- package/dist/elements.js +32 -3
- package/dist/index.js +34 -1
- package/dist/piggy-load.js +2 -0
- package/dist/piggy-mount.js +27 -0
- package/dist/piggy.cdn.js +633 -0
- package/dist/piggy.js +633 -0
- package/dist/thinking.js +243 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Craft Library
|
|
2
2
|
|
|
3
|
-
Interface pieces by [Sara Lobo](https://www.saralobodesign.com/), designed, tested and polished one detail at a time: a liquid glass nav bar, a bursting heart, a magnetic icon
|
|
3
|
+
Interface pieces by [Sara Lobo](https://www.saralobodesign.com/), designed, tested and polished one detail at a time: a liquid glass nav bar, a bursting heart, a magnetic icon, an iridescent 3D card, a glass piggy bank, and two "thinking" indicators, the Magic sphere and the Sweep. Each one is the exact piece from the library, with its own CSS and motion, for React 18+ or plain HTML. The only dependency is three.js, for the two 3D pieces, and it loads only when one of them is on the page.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install craft-library
|
|
@@ -80,9 +80,48 @@ import { IridescentCard } from 'craft-library';
|
|
|
80
80
|
|
|
81
81
|
It is WebGL (three.js 0.169, which comes with the package and loads the first time a card mounts), so one card per screen is best. It is made for a dark background; black is best. The Visa mark is a trademark of Visa Inc., there to show a card: pass your own `logo` for anything real.
|
|
82
82
|
|
|
83
|
+
## Glass piggy bank
|
|
84
|
+
|
|
85
|
+
A congratulations moment in 3D, made for a phone screen: a frosted glass piggy bank hops in from the right with a full turn, lands on its hind feet, nearly tips over and clacks back down, rocking like the rigid glass it is. Coins pop into the air above it and drop in through the slot; each one knocks the glass, and its eyes close a little more until they curve, happy. It plays the first time it comes into view; a tap plays it again.
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
import { PiggyBank } from 'craft-library';
|
|
89
|
+
|
|
90
|
+
<PiggyBank height={560} onDone={() => console.log('saved!')} />
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Prop | | |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `height` | `number \| string` | The box the moment fills: 100% wide (at least 280px) and this tall. Default `560`. |
|
|
96
|
+
| `autoplay` | `boolean` | `false` waits instead of playing when it comes into view. Default `true`. |
|
|
97
|
+
| `replay` | `any` | Change it (a counter, say) to play the moment again. |
|
|
98
|
+
| `interactive` | `boolean` | `false` turns off tap-to-replay and the drag that turns it once it is still. Default `true`. |
|
|
99
|
+
| `onDone` | `() => void` | After each play, when its eyes open again. |
|
|
100
|
+
| `label` | `string` | The accessible name. |
|
|
101
|
+
|
|
102
|
+
It is WebGL (three.js 0.169, which comes with the package and loads the first time it mounts), so one per screen is best. It is made for a dark background; black is best.
|
|
103
|
+
|
|
104
|
+
## Magic sphere and Sweep
|
|
105
|
+
|
|
106
|
+
Two "thinking" indicators for the wait while an answer is being written: a ball of dots, drawn on a canvas, with the word beside it. The **Magic sphere** is a Rubik's cube on a ball, one grey tone per face: its slices turn and scramble the pieces, retrace their steps until every piece is home, and the ball lights up for a moment while a light passes through the word. In the **Sweep**, a wave crosses a dotted globe from left to right, lifting the dots it passes, runs on through the word at the same speed, and rests.
|
|
107
|
+
|
|
108
|
+
```jsx
|
|
109
|
+
import { MagicSphere, SweepSphere } from 'craft-library';
|
|
110
|
+
|
|
111
|
+
{isThinking && <MagicSphere />}
|
|
112
|
+
{isThinking && <SweepSphere label="Searching" />}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Prop | | |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `label` | `string` | The word beside the ball. Default `"Thinking"`; `""` shows the ball alone (still announced as "Thinking"). |
|
|
118
|
+
| `size` | `number \| string` | The ball, px or any CSS length. Default `32`. Under 64px it draws more, finer dots, made to sit beside text; larger, it shows the full detail. |
|
|
119
|
+
|
|
120
|
+
Both are made for a dark background: the dots and the word are light. On a light one, set `--thinking-ink` (the dots and the light in the word) and `--thinking-dim` (the word at rest) on it or a parent. The element is a `role="status"`.
|
|
121
|
+
|
|
83
122
|
## Plain HTML
|
|
84
123
|
|
|
85
|
-
One script registers
|
|
124
|
+
One script registers seven tags. Same pieces, same props as attributes.
|
|
86
125
|
|
|
87
126
|
```html
|
|
88
127
|
<script src="https://cdn.jsdelivr.net/npm/craft-library@0/dist/craft.js"></script>
|
|
@@ -91,12 +130,15 @@ One script registers four tags. Same pieces, same props as attributes.
|
|
|
91
130
|
<like-button></like-button> <!-- "change" event: e.detail.liked -->
|
|
92
131
|
<magnetic-icon size="40"></magnetic-icon>
|
|
93
132
|
<iridescent-card></iridescent-card> <!-- "cardturn" event: e.detail.face; 100% wide, 480px tall unless you size it -->
|
|
133
|
+
<piggy-bank></piggy-bank> <!-- "piggydone" event after each play; el.play() replays; 100% wide, 560px tall unless you size it -->
|
|
134
|
+
<magic-sphere></magic-sphere> <!-- label="Thinking" (default; label="" for the ball alone), size="32" -->
|
|
135
|
+
<sweep-sphere></sweep-sphere>
|
|
94
136
|
```
|
|
95
137
|
|
|
96
138
|
With a bundler and no React: `import 'craft-library/elements'`.
|
|
97
139
|
|
|
98
140
|
## Motion and access
|
|
99
141
|
|
|
100
|
-
Every item is a real `<button>` with an accessible name; the nav marks the chosen one with `aria-current` and moves with the arrow keys. The card is a `role="button"` that Enter, Space and the arrow keys turn over. With `prefers-reduced-motion`, the heart only changes colour, the nav only slides, the magnet stays still
|
|
142
|
+
Every item is a real `<button>` with an accessible name; the nav marks the chosen one with `aria-current` and moves with the arrow keys. The card is a `role="button"` that Enter, Space and the arrow keys turn over; the piggy bank is a labelled image. With `prefers-reduced-motion`, the heart only changes colour, the nav only slides, the magnet stays still, the card swaps faces with a fade, with no entrance and no breathing, the piggy bank stays in place while the coins appear inside it and its eyes close, and the thinking balls hold still and only breathe.
|
|
101
143
|
|
|
102
144
|
MIT · [Craft Library](https://claude.ai/artifact/LBBWCfEHJGvfBSynV3wDuR)
|
package/dist/cdn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// the one-file build for a <script> tag: registers the
|
|
2
|
-
//
|
|
3
|
-
import { mountLike, mountMagnet, mountGlassNav, mountIridescentCard } from './elements.js';
|
|
4
|
-
window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard };
|
|
1
|
+
// the one-file build for a <script> tag: registers the seven tags and exposes the mount functions (the 3D pieces' scenes
|
|
2
|
+
// are fetched from next to this file the first time one is mounted)
|
|
3
|
+
import { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking } from './elements.js';
|
|
4
|
+
window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
|
package/dist/craft.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Craft Library, by Sara Lobo · liquid glass nav bar, bursting heart, magnetic icon, iridescent card · <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card> */
|
|
1
|
+
/* Craft Library, by Sara Lobo · liquid glass nav bar, bursting heart, magnetic icon, iridescent card, glass piggy bank, magic sphere, sweep · <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere>, <sweep-sphere> */
|
|
2
2
|
(() => {
|
|
3
3
|
// dist/style.js
|
|
4
4
|
var done = /* @__PURE__ */ new Set();
|
|
@@ -1355,6 +1355,426 @@
|
|
|
1355
1355
|
};
|
|
1356
1356
|
}
|
|
1357
1357
|
|
|
1358
|
+
// src/piggy-load-cdn.js
|
|
1359
|
+
var me2 = typeof document !== "undefined" && document.currentScript ? document.currentScript.src : "";
|
|
1360
|
+
var base2 = me2 ? me2.slice(0, me2.lastIndexOf("/") + 1) : "https://cdn.jsdelivr.net/npm/craft-library@0/dist/";
|
|
1361
|
+
var loadPiggy = () => import(base2 + "piggy.cdn.js");
|
|
1362
|
+
|
|
1363
|
+
// dist/piggy-mount.js
|
|
1364
|
+
var LABEL2 = "A glass piggy bank hops in, coins drop in through its slot and it closes its eyes, happy.";
|
|
1365
|
+
function mountPiggyBank(host, o = {}) {
|
|
1366
|
+
let pig = null, gone = false, wantPlay = false;
|
|
1367
|
+
const opts = { label: o.label || LABEL2 };
|
|
1368
|
+
if (o.autoplay === false) opts.autoplay = false;
|
|
1369
|
+
if (o.interactive === false) opts.interactive = false;
|
|
1370
|
+
const done2 = () => {
|
|
1371
|
+
if (o.onDone) o.onDone();
|
|
1372
|
+
};
|
|
1373
|
+
host.addEventListener("piggydone", done2);
|
|
1374
|
+
const ready = loadPiggy().then((m) => {
|
|
1375
|
+
if (gone) return null;
|
|
1376
|
+
pig = m.mountPiggy(host, opts);
|
|
1377
|
+
if (wantPlay) pig.play();
|
|
1378
|
+
return pig.ready.then(() => pig);
|
|
1379
|
+
});
|
|
1380
|
+
return {
|
|
1381
|
+
ready,
|
|
1382
|
+
play: () => {
|
|
1383
|
+
if (pig) pig.play();
|
|
1384
|
+
else wantPlay = true;
|
|
1385
|
+
},
|
|
1386
|
+
destroy() {
|
|
1387
|
+
gone = true;
|
|
1388
|
+
host.removeEventListener("piggydone", done2);
|
|
1389
|
+
if (pig) pig.destroy();
|
|
1390
|
+
pig = null;
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// dist/thinking.js
|
|
1396
|
+
var CSS4 = `/* Thinking orbs \xB7 Craft Library, by Sara Lobo
|
|
1397
|
+
A ball of dots and the word beside it, for the wait while an answer is being written. thinking.js draws the ball
|
|
1398
|
+
and, for the Sweep, carries the ball's wave on through the word. Made for dark backgrounds: on a light one, set
|
|
1399
|
+
--thinking-ink (the dots and the light in the word) and --thinking-dim (the word at rest). */
|
|
1400
|
+
|
|
1401
|
+
.thinking {
|
|
1402
|
+
--thinking-size: 32px;
|
|
1403
|
+
--thinking-ink: #EDEDED;
|
|
1404
|
+
--thinking-dim: #8A8A8A;
|
|
1405
|
+
position: relative;
|
|
1406
|
+
display: inline-flex;
|
|
1407
|
+
align-items: center;
|
|
1408
|
+
gap: 10px;
|
|
1409
|
+
vertical-align: middle;
|
|
1410
|
+
}
|
|
1411
|
+
/* the ball: drawn by thinking.js at this size. Under 64px it uses more, finer dots, made to sit beside text */
|
|
1412
|
+
.thinking-orb {
|
|
1413
|
+
display: block;
|
|
1414
|
+
flex: none;
|
|
1415
|
+
width: var(--thinking-size);
|
|
1416
|
+
height: var(--thinking-size);
|
|
1417
|
+
}
|
|
1418
|
+
/* the word: a light passes through it, left to right, on a loop */
|
|
1419
|
+
.thinking-label {
|
|
1420
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', system-ui, sans-serif;
|
|
1421
|
+
font-size: 14px;
|
|
1422
|
+
line-height: normal;
|
|
1423
|
+
letter-spacing: -.005em;
|
|
1424
|
+
white-space: nowrap;
|
|
1425
|
+
-webkit-font-smoothing: antialiased;
|
|
1426
|
+
text-rendering: optimizeLegibility;
|
|
1427
|
+
color: transparent;
|
|
1428
|
+
-webkit-background-clip: text;
|
|
1429
|
+
background-clip: text;
|
|
1430
|
+
background-image: linear-gradient(90deg, var(--thinking-dim) 0 35%, var(--thinking-ink) 50%, var(--thinking-dim) 65% 100%);
|
|
1431
|
+
background-size: 250% 100%;
|
|
1432
|
+
animation: thinking-shine 2.6s linear infinite;
|
|
1433
|
+
}
|
|
1434
|
+
@keyframes thinking-shine {
|
|
1435
|
+
from {
|
|
1436
|
+
background-position: 100% 0;
|
|
1437
|
+
}
|
|
1438
|
+
to {
|
|
1439
|
+
background-position: 0 0;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
/* the Sweep's word is lit by the ball's own wave: thinking.js moves --thinking-x across it */
|
|
1443
|
+
.thinking[data-kind="sweep"] .thinking-label {
|
|
1444
|
+
--thinking-x: -999px;
|
|
1445
|
+
background-image: linear-gradient(90deg, var(--thinking-dim) calc(var(--thinking-x) - 26px), var(--thinking-ink) var(--thinking-x), var(--thinking-dim) calc(var(--thinking-x) + 26px));
|
|
1446
|
+
background-size: auto;
|
|
1447
|
+
animation: none;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/* less motion: the ball holds still and only breathes; the word stays plain */
|
|
1451
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1452
|
+
.thinking-label {
|
|
1453
|
+
animation: none;
|
|
1454
|
+
background: none;
|
|
1455
|
+
color: var(--thinking-dim);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
`;
|
|
1459
|
+
var api4;
|
|
1460
|
+
function engine4() {
|
|
1461
|
+
if (api4) return api4;
|
|
1462
|
+
const window2 = {};
|
|
1463
|
+
(function() {
|
|
1464
|
+
var TAU = Math.PI * 2, SMALL = 64;
|
|
1465
|
+
var SPEED = 85, REST = 0.55, HALO = 26;
|
|
1466
|
+
var reduce = matchMedia("(prefers-reduced-motion: reduce)");
|
|
1467
|
+
var all = [], running = false, last = 0;
|
|
1468
|
+
function clamp01(x) {
|
|
1469
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
1470
|
+
}
|
|
1471
|
+
function smooth(a, b, x) {
|
|
1472
|
+
var t = clamp01((x - a) / (b - a));
|
|
1473
|
+
return t * t * (3 - 2 * t);
|
|
1474
|
+
}
|
|
1475
|
+
function ease(t) {
|
|
1476
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
1477
|
+
}
|
|
1478
|
+
function painter() {
|
|
1479
|
+
var LEVELS = 16, d = [], n = [];
|
|
1480
|
+
for (var i = 0; i < LEVELS; i++) {
|
|
1481
|
+
d.push([]);
|
|
1482
|
+
n.push(0);
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
gain: 1,
|
|
1486
|
+
dot: function(x, y, r, a) {
|
|
1487
|
+
a *= this.gain;
|
|
1488
|
+
if (a <= 0.02) return;
|
|
1489
|
+
var b = Math.min(LEVELS - 1, a * LEVELS | 0), arr = d[b], k = n[b];
|
|
1490
|
+
arr[k] = x;
|
|
1491
|
+
arr[k + 1] = y;
|
|
1492
|
+
arr[k + 2] = r;
|
|
1493
|
+
n[b] = k + 3;
|
|
1494
|
+
},
|
|
1495
|
+
flush: function(c, ink) {
|
|
1496
|
+
c.fillStyle = ink;
|
|
1497
|
+
for (var b = 0; b < LEVELS; b++) {
|
|
1498
|
+
if (!n[b]) continue;
|
|
1499
|
+
var arr = d[b];
|
|
1500
|
+
c.globalAlpha = (b + 1) / LEVELS;
|
|
1501
|
+
c.beginPath();
|
|
1502
|
+
for (var i2 = 0; i2 < n[b]; i2 += 3) {
|
|
1503
|
+
c.moveTo(arr[i2] + arr[i2 + 2], arr[i2 + 1]);
|
|
1504
|
+
c.arc(arr[i2], arr[i2 + 1], arr[i2 + 2], 0, TAU);
|
|
1505
|
+
}
|
|
1506
|
+
c.fill();
|
|
1507
|
+
n[b] = 0;
|
|
1508
|
+
}
|
|
1509
|
+
c.globalAlpha = 1;
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
var CAM = 3.2, LX = -0.42, LY = -0.62, LZ = -0.66;
|
|
1514
|
+
function view(yaw, pitch) {
|
|
1515
|
+
return { cy: Math.cos(yaw), sy: Math.sin(yaw), cp: Math.cos(pitch), sp: Math.sin(pitch) };
|
|
1516
|
+
}
|
|
1517
|
+
function turn(V, x, y, z, o) {
|
|
1518
|
+
var x1 = x * V.cy - z * V.sy, z1 = x * V.sy + z * V.cy;
|
|
1519
|
+
o[0] = x1;
|
|
1520
|
+
o[1] = y * V.cp - z1 * V.sp;
|
|
1521
|
+
o[2] = y * V.sp + z1 * V.cp;
|
|
1522
|
+
}
|
|
1523
|
+
function persp(x, y, z, o) {
|
|
1524
|
+
var f = CAM / (CAM + z);
|
|
1525
|
+
o[3] = x * f;
|
|
1526
|
+
o[4] = y * f;
|
|
1527
|
+
o[5] = f;
|
|
1528
|
+
}
|
|
1529
|
+
function lit(nx, ny, nz) {
|
|
1530
|
+
return (0.32 + 0.68 * Math.max(0, nx * LX + ny * LY + nz * LZ)) * (0.07 + 0.93 * smooth(0.28, -0.3, nz));
|
|
1531
|
+
}
|
|
1532
|
+
var TONES = [1, 0.5, 0.78, 0.3, 0.62, 0.18];
|
|
1533
|
+
function spin(q, axis, cs, sn) {
|
|
1534
|
+
var b = (axis + 1) % 3, c = (axis + 2) % 3, qb = q[b], qc = q[c];
|
|
1535
|
+
q[b] = qb * cs - qc * sn;
|
|
1536
|
+
q[c] = qb * sn + qc * cs;
|
|
1537
|
+
}
|
|
1538
|
+
function sphere(small) {
|
|
1539
|
+
var per = small ? 2 : 4, COUNT = 5, SLOT = 0.5, DUR = 0.42, HOLD = 1.1, R0 = 0.8, D = [], v = [0, 0, 0], o = [0, 0, 0, 0, 0, 0];
|
|
1540
|
+
for (var a = 0; a < 3; a++) for (var sg = 1; sg >= -1; sg -= 2) {
|
|
1541
|
+
var s = TONES[a * 2 + (sg > 0 ? 0 : 1)], b = (a + 1) % 3, c = (a + 2) % 3;
|
|
1542
|
+
for (var su = -1; su <= 1; su++) for (var sv = -1; sv <= 1; sv++) for (var j = 0; j < per; j++) for (var k = 0; k < per; k++) {
|
|
1543
|
+
var pos = [0, 0, 0], cub = [0, 0, 0];
|
|
1544
|
+
pos[a] = sg;
|
|
1545
|
+
cub[a] = sg;
|
|
1546
|
+
cub[b] = su;
|
|
1547
|
+
cub[c] = sv;
|
|
1548
|
+
pos[b] = su * 2 / 3 + ((j + 0.5) / per * 2 - 1) * 0.26;
|
|
1549
|
+
pos[c] = sv * 2 / 3 + ((k + 0.5) / per * 2 - 1) * 0.26;
|
|
1550
|
+
D.push({ pos, cub, s });
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
var t = 0, seq = [], mi = 0, mt = 0;
|
|
1554
|
+
function plan() {
|
|
1555
|
+
var sc = [], prev = -1;
|
|
1556
|
+
while (sc.length < COUNT) {
|
|
1557
|
+
var axis = Math.random() * 3 | 0, layer = (Math.random() * 3 | 0) - 1, key = axis * 3 + layer + 1;
|
|
1558
|
+
if (key === prev) continue;
|
|
1559
|
+
prev = key;
|
|
1560
|
+
sc.push({ axis, layer, dir: Math.random() < 0.5 ? -1 : 1 });
|
|
1561
|
+
}
|
|
1562
|
+
seq = sc.concat(sc.slice().reverse().map(function(m) {
|
|
1563
|
+
return { axis: m.axis, layer: m.layer, dir: -m.dir };
|
|
1564
|
+
}));
|
|
1565
|
+
}
|
|
1566
|
+
plan();
|
|
1567
|
+
return {
|
|
1568
|
+
time: function() {
|
|
1569
|
+
return t;
|
|
1570
|
+
},
|
|
1571
|
+
step: function(dt) {
|
|
1572
|
+
t += dt;
|
|
1573
|
+
mt += dt;
|
|
1574
|
+
if (mi < seq.length) {
|
|
1575
|
+
if (mt >= SLOT) {
|
|
1576
|
+
var m = seq[mi];
|
|
1577
|
+
D.forEach(function(d) {
|
|
1578
|
+
if (d.cub[m.axis] === m.layer) {
|
|
1579
|
+
spin(d.pos, m.axis, 0, m.dir);
|
|
1580
|
+
spin(d.cub, m.axis, 0, m.dir);
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
mi++;
|
|
1584
|
+
mt -= SLOT;
|
|
1585
|
+
}
|
|
1586
|
+
} else if (mt >= HOLD) {
|
|
1587
|
+
plan();
|
|
1588
|
+
mi = 0;
|
|
1589
|
+
mt -= HOLD;
|
|
1590
|
+
}
|
|
1591
|
+
},
|
|
1592
|
+
draw: function(p, u) {
|
|
1593
|
+
var V = view(0.72 + 0.2 * Math.sin(t * TAU / 15), 0.5 + 0.07 * Math.sin(t * TAU / 21));
|
|
1594
|
+
var m = mi < seq.length ? seq[mi] : null;
|
|
1595
|
+
var th = m ? m.dir * Math.PI / 2 * ease(clamp01(mt / DUR)) : 0, cs = Math.cos(th), sn = Math.sin(th);
|
|
1596
|
+
var glow = m ? 0 : Math.sin(Math.PI * clamp01(mt / HOLD)), rb = R0 * (1 + 0.03 * glow);
|
|
1597
|
+
for (var i = 0; i < D.length; i++) {
|
|
1598
|
+
var d = D[i];
|
|
1599
|
+
v[0] = d.pos[0];
|
|
1600
|
+
v[1] = d.pos[1];
|
|
1601
|
+
v[2] = d.pos[2];
|
|
1602
|
+
if (m && d.cub[m.axis] === m.layer) spin(v, m.axis, cs, sn);
|
|
1603
|
+
var k2 = rb / Math.hypot(v[0], v[1], v[2]);
|
|
1604
|
+
turn(V, v[0] * k2, v[1] * k2, v[2] * k2, o);
|
|
1605
|
+
var shade = lit(o[0] / rb, o[1] / rb, o[2] / rb);
|
|
1606
|
+
persp(o[0], o[1], o[2], o);
|
|
1607
|
+
p.dot(u.cx + o[3] * u.R, u.cy + o[4] * u.R, u.rad((small ? 0.5 : 1.05) * o[5]), shade * (0.25 + 0.75 * (d.s + (1 - d.s) * 0.6 * glow)));
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
function sweep(small, front) {
|
|
1613
|
+
var LAT = small ? 11 : 15, dens = small ? 26 : 40, R0 = 0.74, W = small ? 0.28 : 0.24, D = [], o = [0, 0, 0, 0, 0, 0];
|
|
1614
|
+
for (var a = 1; a < LAT; a++) {
|
|
1615
|
+
var lat = Math.PI * a / LAT, y = -Math.cos(lat), r = Math.sin(lat), n = Math.max(5, Math.round(r * dens));
|
|
1616
|
+
for (var b = 0; b < n; b++) {
|
|
1617
|
+
var th = TAU * b / n + a * 0.37;
|
|
1618
|
+
D.push([Math.cos(th) * r, y, Math.sin(th) * r]);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
front = front || function(t2) {
|
|
1622
|
+
return -1.3 + 2.6 * (t2 % 1.75) / 1.2;
|
|
1623
|
+
};
|
|
1624
|
+
var t = 0;
|
|
1625
|
+
return {
|
|
1626
|
+
time: function() {
|
|
1627
|
+
return t;
|
|
1628
|
+
},
|
|
1629
|
+
step: function(dt) {
|
|
1630
|
+
t += dt;
|
|
1631
|
+
},
|
|
1632
|
+
draw: function(p, u) {
|
|
1633
|
+
var V = view(0.35 * Math.sin(t * TAU / 17), 0.3 + 0.07 * Math.sin(t * TAU / 23)), f = front(t);
|
|
1634
|
+
for (var i = 0; i < D.length; i++) {
|
|
1635
|
+
turn(V, D[i][0], D[i][1], D[i][2], o);
|
|
1636
|
+
var nx = o[0], ny = o[1], nz = o[2], q = (nx * R0 - f) / W, w = Math.exp(-q * q), rho = R0 * (1 + 0.17 * w);
|
|
1637
|
+
persp(nx * rho, ny * rho, nz * rho, o);
|
|
1638
|
+
p.dot(u.cx + o[3] * u.R, u.cy + o[4] * u.R, u.rad((small ? 0.45 + 0.25 * w : 0.95 + 0.6 * w) * o[5]), lit(nx, ny, nz) * (0.75 + 0.25 * w) + 0.55 * w * smooth(0.3, -0.3, nz));
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
function rowX(r, t) {
|
|
1644
|
+
return r.x0 + SPEED * (t % ((r.x1 - r.x0) / SPEED + REST));
|
|
1645
|
+
}
|
|
1646
|
+
function size(I) {
|
|
1647
|
+
var cw = I.cv.clientWidth, ch = I.cv.clientHeight;
|
|
1648
|
+
if (!cw || !ch) return;
|
|
1649
|
+
var small = Math.min(cw, ch) < SMALL, dpr = Math.min(devicePixelRatio || 1, small ? 3 : 2);
|
|
1650
|
+
var w = Math.round(cw * dpr), h = Math.round(ch * dpr);
|
|
1651
|
+
if (I.cv.width !== w || I.cv.height !== h) {
|
|
1652
|
+
I.cv.width = w;
|
|
1653
|
+
I.cv.height = h;
|
|
1654
|
+
}
|
|
1655
|
+
var R = Math.min(w, h) * 0.44, k = small ? dpr : R / 80, minR = (small ? 0.4 : 0.5) * dpr;
|
|
1656
|
+
I.w = w;
|
|
1657
|
+
I.h = h;
|
|
1658
|
+
I.u = { cx: w / 2, cy: h / 2, R, rad: function(d) {
|
|
1659
|
+
return Math.max(minR, d * k);
|
|
1660
|
+
} };
|
|
1661
|
+
I.ink = getComputedStyle(I.el).getPropertyValue("--thinking-ink").trim() || "#EDEDED";
|
|
1662
|
+
if (I.small !== small) {
|
|
1663
|
+
I.small = small;
|
|
1664
|
+
I.sim = I.kind === "sweep" ? sweep(small, I.row && function(t) {
|
|
1665
|
+
return (rowX(I.row, t) - I.row.cx) / I.row.R;
|
|
1666
|
+
}) : sphere(small);
|
|
1667
|
+
if (reduce.matches) for (var i = 0; i < 150; i++) I.sim.step(1 / 60);
|
|
1668
|
+
}
|
|
1669
|
+
if (I.row) {
|
|
1670
|
+
var L = I.label;
|
|
1671
|
+
I.row.cx = I.cv.offsetLeft + I.cv.offsetWidth / 2 - L.offsetLeft;
|
|
1672
|
+
I.row.R = R / dpr;
|
|
1673
|
+
I.row.x0 = I.row.cx - 1.3 * I.row.R;
|
|
1674
|
+
I.row.x1 = L.offsetWidth + HALO + 4;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function frame(now) {
|
|
1678
|
+
if (!all.length) {
|
|
1679
|
+
running = false;
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
requestAnimationFrame(frame);
|
|
1683
|
+
var dt = Math.min(1 / 30, Math.max(0, (now - last) / 1e3));
|
|
1684
|
+
last = now;
|
|
1685
|
+
if (document.hidden) return;
|
|
1686
|
+
for (var i = 0; i < all.length; i++) {
|
|
1687
|
+
var I = all[i];
|
|
1688
|
+
if (!I.seen || !I.u) continue;
|
|
1689
|
+
if (reduce.matches) I.calm += dt;
|
|
1690
|
+
else I.sim.step(dt);
|
|
1691
|
+
I.ctx.clearRect(0, 0, I.w, I.h);
|
|
1692
|
+
I.p.gain = (I.small ? 1.35 : 1) * (reduce.matches ? 0.72 + 0.28 * Math.sin(I.calm * TAU / 3.2) : 1);
|
|
1693
|
+
I.sim.draw(I.p, I.u);
|
|
1694
|
+
I.p.flush(I.ctx, I.ink);
|
|
1695
|
+
if (I.row && !reduce.matches) I.label.style.setProperty("--thinking-x", rowX(I.row, I.sim.time()).toFixed(1) + "px");
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function thinking(el) {
|
|
1699
|
+
if (el.__thinking) return el.__thinking;
|
|
1700
|
+
var cv = el.querySelector(".thinking-orb");
|
|
1701
|
+
if (!cv) {
|
|
1702
|
+
cv = document.createElement("canvas");
|
|
1703
|
+
cv.className = "thinking-orb";
|
|
1704
|
+
cv.setAttribute("aria-hidden", "true");
|
|
1705
|
+
el.insertBefore(cv, el.firstChild);
|
|
1706
|
+
}
|
|
1707
|
+
var label = el.querySelector(".thinking-label"), kind = el.getAttribute("data-kind") === "sweep" ? "sweep" : "sphere";
|
|
1708
|
+
var I = {
|
|
1709
|
+
el,
|
|
1710
|
+
cv,
|
|
1711
|
+
ctx: cv.getContext("2d"),
|
|
1712
|
+
label,
|
|
1713
|
+
kind,
|
|
1714
|
+
row: kind === "sweep" && label ? { cx: 0, R: 1, x0: 0, x1: 1 } : null,
|
|
1715
|
+
sim: null,
|
|
1716
|
+
small: null,
|
|
1717
|
+
u: null,
|
|
1718
|
+
w: 0,
|
|
1719
|
+
h: 0,
|
|
1720
|
+
ink: "",
|
|
1721
|
+
seen: true,
|
|
1722
|
+
calm: 0,
|
|
1723
|
+
p: painter()
|
|
1724
|
+
};
|
|
1725
|
+
I.ro = new ResizeObserver(function() {
|
|
1726
|
+
size(I);
|
|
1727
|
+
});
|
|
1728
|
+
I.ro.observe(cv);
|
|
1729
|
+
if (label) I.ro.observe(label);
|
|
1730
|
+
I.io = new IntersectionObserver(function(es) {
|
|
1731
|
+
I.seen = es[es.length - 1].isIntersecting;
|
|
1732
|
+
});
|
|
1733
|
+
I.io.observe(el);
|
|
1734
|
+
size(I);
|
|
1735
|
+
all.push(I);
|
|
1736
|
+
if (!running) {
|
|
1737
|
+
running = true;
|
|
1738
|
+
last = performance.now();
|
|
1739
|
+
requestAnimationFrame(frame);
|
|
1740
|
+
}
|
|
1741
|
+
return el.__thinking = {
|
|
1742
|
+
destroy: function() {
|
|
1743
|
+
I.ro.disconnect();
|
|
1744
|
+
I.io.disconnect();
|
|
1745
|
+
all.splice(all.indexOf(I), 1);
|
|
1746
|
+
I.ctx.clearRect(0, 0, I.w, I.h);
|
|
1747
|
+
if (label) label.style.removeProperty("--thinking-x");
|
|
1748
|
+
el.__thinking = null;
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
function scan() {
|
|
1753
|
+
Array.prototype.forEach.call(document.querySelectorAll(".thinking"), thinking);
|
|
1754
|
+
}
|
|
1755
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", scan);
|
|
1756
|
+
else scan();
|
|
1757
|
+
window2.thinking = thinking;
|
|
1758
|
+
})();
|
|
1759
|
+
return api4 = window2;
|
|
1760
|
+
}
|
|
1761
|
+
function mountThinking(host, o = {}) {
|
|
1762
|
+
css("craft-thinking", CSS4);
|
|
1763
|
+
const T = engine4();
|
|
1764
|
+
const kind = o.kind === "sweep" ? "sweep" : "sphere";
|
|
1765
|
+
const label = o.label == null || o.label === true ? "Thinking" : o.label === false ? "" : String(o.label);
|
|
1766
|
+
host.innerHTML = '<span class="thinking" data-kind="' + kind + '" role="status"><canvas class="thinking-orb" aria-hidden="true"></canvas>' + (label ? '<span class="thinking-label"></span>' : "") + "</span>";
|
|
1767
|
+
const el = host.firstChild;
|
|
1768
|
+
if (label) el.lastChild.textContent = label;
|
|
1769
|
+
else el.setAttribute("aria-label", "Thinking");
|
|
1770
|
+
if (o.size) el.style.setProperty("--thinking-size", typeof o.size === "number" ? o.size + "px" : String(o.size));
|
|
1771
|
+
const m = T.thinking(el);
|
|
1772
|
+
return { element: el, destroy() {
|
|
1773
|
+
m.destroy();
|
|
1774
|
+
host.innerHTML = "";
|
|
1775
|
+
} };
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1358
1778
|
// dist/elements.js
|
|
1359
1779
|
function define(name, mount, options) {
|
|
1360
1780
|
if (typeof customElements === "undefined" || customElements.get(name)) return;
|
|
@@ -1401,7 +1821,27 @@
|
|
|
1401
1821
|
entrance: el.getAttribute("entrance") === "false" ? false : void 0,
|
|
1402
1822
|
label: el.getAttribute("label") || void 0
|
|
1403
1823
|
}));
|
|
1824
|
+
define("piggy-bank", (el, o) => {
|
|
1825
|
+
css("craft-piggy-host", ":where(piggy-bank){display:block;position:relative;width:100%;min-width:280px;height:560px}");
|
|
1826
|
+
const m = mountPiggyBank(el, o);
|
|
1827
|
+
el.play = m.play;
|
|
1828
|
+
return m;
|
|
1829
|
+
}, (el) => ({
|
|
1830
|
+
autoplay: el.getAttribute("autoplay") === "false" ? false : void 0,
|
|
1831
|
+
interactive: el.getAttribute("interactive") === "false" ? false : void 0,
|
|
1832
|
+
label: el.getAttribute("label") || void 0
|
|
1833
|
+
}));
|
|
1834
|
+
var thinkingTag = (kind) => (el, o) => {
|
|
1835
|
+
css("craft-thinking-host", "magic-sphere,sweep-sphere{display:inline-block;vertical-align:middle}");
|
|
1836
|
+
return mountThinking(el, { ...o, kind });
|
|
1837
|
+
};
|
|
1838
|
+
var thinkingOptions = (el) => ({
|
|
1839
|
+
size: el.getAttribute("size") ? isNaN(el.getAttribute("size")) ? el.getAttribute("size") : +el.getAttribute("size") : void 0,
|
|
1840
|
+
label: el.hasAttribute("label") ? el.getAttribute("label") : void 0
|
|
1841
|
+
});
|
|
1842
|
+
define("magic-sphere", thinkingTag("sphere"), thinkingOptions);
|
|
1843
|
+
define("sweep-sphere", thinkingTag("sweep"), thinkingOptions);
|
|
1404
1844
|
|
|
1405
1845
|
// dist/cdn.js
|
|
1406
|
-
window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard };
|
|
1846
|
+
window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
|
|
1407
1847
|
})();
|
package/dist/elements.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
// <glass-nav>, <like-button>, <magnetic-icon
|
|
2
|
-
// They render into the page itself (no shadow root), so the glass can
|
|
1
|
+
// <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere> and <sweep-sphere>: the
|
|
2
|
+
// pieces as HTML tags, for pages without React. They render into the page itself (no shadow root), so the glass can
|
|
3
|
+
// blend with what is behind it.
|
|
3
4
|
import { mountLike } from './like.js';
|
|
4
5
|
import { mountMagnet } from './magnet.js';
|
|
5
6
|
import { mountGlassNav } from './glassnav.js';
|
|
6
7
|
import { mountIridescentCard } from './card-mount.js';
|
|
8
|
+
import { mountPiggyBank } from './piggy-mount.js';
|
|
9
|
+
import { mountThinking } from './thinking.js';
|
|
7
10
|
import { css } from './style.js';
|
|
8
11
|
|
|
9
12
|
function define(name, mount, options) {
|
|
@@ -51,4 +54,30 @@ define('iridescent-card', (el, o) => {
|
|
|
51
54
|
label: el.getAttribute('label') || undefined
|
|
52
55
|
}));
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
// the piggy bank fills the tag: a block, 100% wide (at least 280px) and 560px tall unless the page sizes it. It plays
|
|
58
|
+
// the first time it comes into view; each play ends with a "piggydone" event. el.play() plays it again.
|
|
59
|
+
define('piggy-bank', (el, o) => {
|
|
60
|
+
css('craft-piggy-host', ':where(piggy-bank){display:block;position:relative;width:100%;min-width:280px;height:560px}');
|
|
61
|
+
const m = mountPiggyBank(el, o);
|
|
62
|
+
el.play = m.play;
|
|
63
|
+
return m;
|
|
64
|
+
}, (el) => ({
|
|
65
|
+
autoplay: el.getAttribute('autoplay') === 'false' ? false : undefined,
|
|
66
|
+
interactive: el.getAttribute('interactive') === 'false' ? false : undefined,
|
|
67
|
+
label: el.getAttribute('label') || undefined
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// the thinking indicators: inline, beside text. size="32" (px) or any CSS length; label="Thinking" by default,
|
|
71
|
+
// label="" for the ball alone
|
|
72
|
+
const thinkingTag = (kind) => (el, o) => {
|
|
73
|
+
css('craft-thinking-host', 'magic-sphere,sweep-sphere{display:inline-block;vertical-align:middle}');
|
|
74
|
+
return mountThinking(el, { ...o, kind });
|
|
75
|
+
};
|
|
76
|
+
const thinkingOptions = (el) => ({
|
|
77
|
+
size: el.getAttribute('size') ? (isNaN(el.getAttribute('size')) ? el.getAttribute('size') : +el.getAttribute('size')) : undefined,
|
|
78
|
+
label: el.hasAttribute('label') ? el.getAttribute('label') : undefined
|
|
79
|
+
});
|
|
80
|
+
define('magic-sphere', thinkingTag('sphere'), thinkingOptions);
|
|
81
|
+
define('sweep-sphere', thinkingTag('sweep'), thinkingOptions);
|
|
82
|
+
|
|
83
|
+
export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { mountLike } from './like.js';
|
|
|
5
5
|
import { mountMagnet } from './magnet.js';
|
|
6
6
|
import { mountGlassNav } from './glassnav.js';
|
|
7
7
|
import { mountIridescentCard } from './card-mount.js';
|
|
8
|
+
import { mountPiggyBank } from './piggy-mount.js';
|
|
9
|
+
import { mountThinking } from './thinking.js';
|
|
8
10
|
|
|
9
11
|
const HOST = { display: 'inline-block', lineHeight: 0, verticalAlign: 'middle' };
|
|
10
12
|
|
|
@@ -54,4 +56,35 @@ export function IridescentCard({ height = 480, size, name, number, expiry, cvv,
|
|
|
54
56
|
return createElement('div', { ref: host, className, style: { position: 'relative', width: '100%', minWidth: 300, height, ...style } });
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
// The piggy bank fills the box it renders (100% wide and `height` tall, 560px by default; it is made for a phone screen)
|
|
60
|
+
// and plays its moment the first time it comes into view; a tap plays it again. replay: change it (a counter) to play
|
|
61
|
+
// it again from code. It loads its 3D scene (three.js) the first time it mounts. onDone reports the end of each play.
|
|
62
|
+
export function PiggyBank({ height = 560, autoplay, interactive, replay, label, onDone, className, style }) {
|
|
63
|
+
const host = useRef(null), pig = useRef(null), last = useRef(replay), done = useLatest(onDone);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const m = pig.current = mountPiggyBank(host.current, { autoplay, interactive, label, onDone: () => done.current && done.current() });
|
|
66
|
+
return () => { m.destroy(); pig.current = null; };
|
|
67
|
+
}, [autoplay, interactive, label]);
|
|
68
|
+
useEffect(() => { if (last.current === replay) return; last.current = replay; if (pig.current) pig.current.play(); }, [replay]);
|
|
69
|
+
return createElement('div', { ref: host, className, style: { position: 'relative', width: '100%', minWidth: 280, height, ...style } });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The Magic sphere and the Sweep: a "thinking" indicator, a ball of dots with the word beside it. size: the ball, px or
|
|
73
|
+
// any CSS length (32 by default; under 64px it draws finer, denser dots made to sit beside text). label: the word
|
|
74
|
+
// ("Thinking" by default; '' for the ball alone). The host keeps its line height, so the word is never clipped.
|
|
75
|
+
function thinkingComponent(kind, name) {
|
|
76
|
+
function Thinking({ size, label, className, style }) {
|
|
77
|
+
const host = useRef(null);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const m = mountThinking(host.current, { kind, size, label });
|
|
80
|
+
return () => m.destroy();
|
|
81
|
+
}, [size, label]);
|
|
82
|
+
return createElement('span', { ref: host, className, style: { display: 'inline-block', verticalAlign: 'middle', ...style } });
|
|
83
|
+
}
|
|
84
|
+
Thinking.displayName = name;
|
|
85
|
+
return Thinking;
|
|
86
|
+
}
|
|
87
|
+
export const MagicSphere = thinkingComponent('sphere', 'MagicSphere');
|
|
88
|
+
export const SweepSphere = thinkingComponent('sweep', 'SweepSphere');
|
|
89
|
+
|
|
90
|
+
export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Glass piggy bank: the mount around the library's piggy.js. piggy.js, and three.js with it, loads only when a piggy
|
|
2
|
+
// bank is mounted, so the other pieces stay light.
|
|
3
|
+
import { loadPiggy } from './piggy-load.js';
|
|
4
|
+
|
|
5
|
+
const LABEL = 'A glass piggy bank hops in, coins drop in through its slot and it closes its eyes, happy.';
|
|
6
|
+
|
|
7
|
+
// host: the element the moment fills (give it a size). o: { autoplay (false: wait for play()), interactive (false: no
|
|
8
|
+
// tap to replay, no drag), label, onDone() after each play }
|
|
9
|
+
export function mountPiggyBank(host, o = {}) {
|
|
10
|
+
let pig = null, gone = false, wantPlay = false;
|
|
11
|
+
const opts = { label: o.label || LABEL };
|
|
12
|
+
if (o.autoplay === false) opts.autoplay = false;
|
|
13
|
+
if (o.interactive === false) opts.interactive = false;
|
|
14
|
+
const done = () => { if (o.onDone) o.onDone(); };
|
|
15
|
+
host.addEventListener('piggydone', done);
|
|
16
|
+
const ready = loadPiggy().then((m) => {
|
|
17
|
+
if (gone) return null;
|
|
18
|
+
pig = m.mountPiggy(host, opts);
|
|
19
|
+
if (wantPlay) pig.play();
|
|
20
|
+
return pig.ready.then(() => pig);
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
ready,
|
|
24
|
+
play: () => { if (pig) pig.play(); else wantPlay = true; },
|
|
25
|
+
destroy() { gone = true; host.removeEventListener('piggydone', done); if (pig) pig.destroy(); pig = null; }
|
|
26
|
+
};
|
|
27
|
+
}
|