craft-library 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,7 @@ import { LikeButton } from 'craft-library';
44
44
 
45
45
  ## Magnetic icon
46
46
 
47
- A round search button with a brushed-metal rim, for dark backgrounds. Near the cursor the rim acts like metal near a strong magnet: the disc is drawn toward the pointer, the rim creases and trembles, its tip cracks into shards and small voxels fly off to the pointer.
47
+ A round search button with a brushed-metal rim, for dark backgrounds. Near the cursor the rim acts like metal near a strong magnet: the disc is drawn toward the pointer, the rim creases, the whole ring frays out of round and loses small voxels of metal, its tip cracks into shards, and the voxels fly off to the pointer.
48
48
 
49
49
  ```jsx
50
50
  import { MagneticIcon } from 'craft-library';
@@ -63,7 +63,7 @@ import { MagneticIcon } from 'craft-library';
63
63
  One script registers three tags. Same pieces, same props as attributes.
64
64
 
65
65
  ```html
66
- <script src="https://cdn.jsdelivr.net/npm/craft-library@0.1/dist/craft.js"></script>
66
+ <script src="https://cdn.jsdelivr.net/npm/craft-library@0/dist/craft.js"></script>
67
67
 
68
68
  <glass-nav active="0"></glass-nav> <!-- "select" event: e.detail.index -->
69
69
  <like-button></like-button> <!-- "change" event: e.detail.liked -->
package/dist/craft.js CHANGED
@@ -19,7 +19,8 @@
19
19
  meet, both are at rest there, so nothing hitches. Unliking only takes the colour away. Pairs with like.js. */
20
20
 
21
21
  :root {
22
- --like-color: #F91880; /* the liked heart and the disc */
22
+ --like-heart: #FF6767; /* the liked heart, filled (Figma: General / Heart) */
23
+ --like-color: #F91880; /* the disc */
23
24
  --like-ring: #CC8EF5; /* the ring as it opens */
24
25
  --like-spring: cubic-bezier(.34, 1.4, .64, 1); /* fallback for browsers without linear() */
25
26
  }
@@ -83,12 +84,12 @@
83
84
  transition: stroke .24s cubic-bezier(.4, 0, .2, 1);
84
85
  }
85
86
  .like-fill {
86
- fill: var(--like-color);
87
+ fill: var(--like-heart);
87
88
  opacity: 0;
88
89
  transition: opacity .24s cubic-bezier(.4, 0, .2, 1);
89
90
  }
90
91
  .like[aria-pressed="true"] .like-line {
91
- stroke: var(--like-color);
92
+ stroke: var(--like-heart);
92
93
  }
93
94
  .like[aria-pressed="true"] .like-fill {
94
95
  opacity: 1;
@@ -435,6 +436,7 @@
435
436
  var PULL = 15, FALL = 1.1;
436
437
  var BODY = 0.3, SHIFT = 0.14;
437
438
  var JAG = 0.18;
439
+ var FRAY = 2.6, CHIPS = 64;
438
440
  var DENT = 3.5;
439
441
  var RATE = 90, MAXDUST = 90;
440
442
  var ROOM = 46;
@@ -475,16 +477,24 @@
475
477
  var dt = wrap(t - m.a), d = m.d;
476
478
  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);
477
479
  }
480
+ function strainOf(m) {
481
+ return clamp((Math.abs(m.d) + 3 * m.heat) / PULL, 0, 1);
482
+ }
483
+ function damage(m, t) {
484
+ return clamp((Math.abs(m.d) + 3 * m.heat) / 9, 0, 1) * (0.5 + 0.5 * bell(wrap(t - m.a), 1.3));
485
+ }
478
486
  function rimPath(m, now) {
479
- 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;
487
+ var pts = [], strain = strainOf(m), i, t, r, env, sharp, fray, n, p, a, b, c, e;
480
488
  for (i = 0; i < N; i++) {
481
489
  t = i / N * 2 * Math.PI;
482
490
  r = radius(m, t);
483
491
  env = bell(wrap(t - m.a), 0.6) * strain;
484
492
  sharp = env > 0.12;
485
- if (sharp && i % 3) continue;
493
+ fray = sharp ? 0 : clamp((damage(m, t) - 0.15) / 0.85, 0, 1);
494
+ if (sharp && i % 3 || fray > 0 && i % 2) continue;
486
495
  if (sharp) r += 2 * JAG * PULL * env * tremble(i, now);
487
- pts.push([m.cx + r * Math.cos(t), m.cy + r * Math.sin(t), sharp]);
496
+ else if (fray > 0) r += FRAY * fray * (hash(i, 21) - 0.62 + 0.3 * tremble(i + 500, now));
497
+ pts.push([m.cx + r * Math.cos(t), m.cy + r * Math.sin(t), sharp || fray > 0]);
488
498
  }
489
499
  n = pts.length;
490
500
  p = "M" + n2(pts[0][0]) + " " + n2(pts[0][1]);
@@ -671,9 +681,50 @@
671
681
  }
672
682
  g.restore();
673
683
  }
684
+ function chips(m, now) {
685
+ var t = m.t;
686
+ m.chips.forEach(function(c, k) {
687
+ var o = clamp((damage(m, c.a) + 0.08 * tremble(300 + k, now) - c.at) / 0.12, 0, 1), z, r, x, y, dx, dy, l;
688
+ if (o < 0.02) o = 0;
689
+ if (!o && !c.open) return;
690
+ c.open = o;
691
+ z = c.s * o;
692
+ r = radius(m, c.a) + c.o;
693
+ x = m.cx + r * Math.cos(c.a);
694
+ y = m.cy + r * Math.sin(c.a);
695
+ c.el.setAttribute("x", n2(x - z / 2));
696
+ c.el.setAttribute("y", n2(y - z / 2));
697
+ c.el.setAttribute("width", n2(z));
698
+ c.el.setAttribute("height", n2(z));
699
+ if (o < 0.2) c.gone = false;
700
+ else if (o > 0.6 && !c.gone && t && now - c.at2 > 500 && m.dust.length < MAXDUST) {
701
+ c.gone = true;
702
+ c.at2 = now;
703
+ dx = C + t.x - x;
704
+ dy = C + t.y - y;
705
+ l = Math.hypot(dx, dy) || 1;
706
+ m.dust.push({
707
+ x,
708
+ y,
709
+ vx: dx / l * 10,
710
+ vy: dy / l * 10,
711
+ hx: x,
712
+ hy: y,
713
+ s: c.s,
714
+ k: 1,
715
+ age: 0,
716
+ life: 0.7 + Math.random() * 0.6,
717
+ back: false,
718
+ spin: Math.random() < 0.5 ? -1 : 1,
719
+ c: Math.random() < 0.35 ? 190 : 255
720
+ });
721
+ }
722
+ });
723
+ }
674
724
  function draw(m, now) {
675
725
  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;
676
726
  m.rim.setAttribute("d", rimPath(m, now));
727
+ chips(m, now);
677
728
  m.grad.setAttribute("cx", n2(m.cx));
678
729
  m.grad.setAttribute("cy", n2(m.cy));
679
730
  m.stops[0].setAttribute("offset", (f[0] / 40).toFixed(4));
@@ -740,6 +791,24 @@
740
791
  cv.className = "mag-dust";
741
792
  cv.setAttribute("aria-hidden", "true");
742
793
  el.insertBefore(cv, icon);
794
+ var mask = svg.querySelector("mask"), holes = [], k, q;
795
+ for (k = 0; mask && k < CHIPS; k++) {
796
+ q = document.createElementNS("http://www.w3.org/2000/svg", "rect");
797
+ q.setAttribute("fill", "#000");
798
+ q.setAttribute("width", 0);
799
+ q.setAttribute("height", 0);
800
+ mask.appendChild(q);
801
+ holes.push({
802
+ el: q,
803
+ a: (k + 0.8 * hash(k, 11)) / CHIPS * 2 * Math.PI,
804
+ s: 0.9 + 1.5 * hash(k, 12),
805
+ o: 0.2 + 1.1 * (hash(k, 13) - 0.5),
806
+ at: 0.08 + 0.72 * hash(k, 14),
807
+ open: 0,
808
+ gone: false,
809
+ at2: -1e9
810
+ });
811
+ }
743
812
  el.__mag = {
744
813
  el,
745
814
  rim: svg.querySelector(".mag-rim"),
@@ -749,6 +818,7 @@
749
818
  cv,
750
819
  g: cv.getContext("2d"),
751
820
  dust: [],
821
+ chips: holes,
752
822
  acc: 0,
753
823
  pace: 0,
754
824
  lp: null,
package/dist/like.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Bursting heart · Craft Library, by Sara Lobo. like.css and like.js from the library, unchanged, around a mount.
2
2
  import { css } from './style.js';
3
- const CSS = "/* Bursting heart · Craft Library, by Sara Lobo\n The heart eases out of sight, a disc grows and hollows into a ring, 14 dots and 2 or 3 small hearts fly out,\n and the heart springs back full. Every motion runs on its own property with its own curve; where two curves\n meet, both are at rest there, so nothing hitches. Unliking only takes the colour away. Pairs with like.js. */\n\n:root {\n --like-color: #F91880; /* the liked heart and the disc */\n --like-ring: #CC8EF5; /* the ring as it opens */\n --like-spring: cubic-bezier(.34, 1.4, .64, 1); /* fallback for browsers without linear() */\n}\n\n/* a damped spring (damping .55, 700 ms): about 12% over, a short settle, at rest at the end */\n@supports (animation-timing-function: linear(0, 1)) {\n :root {\n --like-spring: linear(0, 0.034, 0.12, 0.24, 0.378, 0.52, 0.655, 0.778, 0.883, 0.968, 1.033, 1.079, 1.108, 1.123, 1.126, 1.12, 1.108, 1.092, 1.075, 1.057, 1.04, 1.025, 1.012, 1.002, 0.994, 0.989, 0.986, 0.984, 0.984, 0.985, 0.987, 0.989, 0.991, 0.993, 0.995, 0.997, 0.999, 1, 1.001, 1.001, 1);\n }\n}\n\n/* lets the ring's hole open smoothly (without it, the ring leaves at once) */\n@property --like-hole {\n syntax: '<percentage>';\n inherits: false;\n initial-value: 0%;\n}\n\n/* the button: a 40px circle in black at 60%, made to sit over images */\n.like {\n all: unset;\n box-sizing: border-box;\n position: relative;\n display: inline-block;\n flex: none;\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: rgba(0, 0, 0, .6);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n transition: background .2s ease, transform .22s cubic-bezier(.2, .8, .2, 1);\n}\n.like:hover {\n background: rgba(0, 0, 0, .72);\n}\n.like:active {\n transform: scale(.94);\n}\n.like:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n.like-icon {\n position: absolute;\n left: 8px;\n top: 9px;\n width: 24px;\n height: 24px;\n}\n.like-icon svg {\n display: block;\n overflow: visible;\n}\n.like-line {\n fill: none;\n stroke: #fff;\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n transition: stroke .24s cubic-bezier(.4, 0, .2, 1);\n}\n.like-fill {\n fill: var(--like-color);\n opacity: 0;\n transition: opacity .24s cubic-bezier(.4, 0, .2, 1);\n}\n.like[aria-pressed=\"true\"] .like-line {\n stroke: var(--like-color);\n}\n.like[aria-pressed=\"true\"] .like-fill {\n opacity: 1;\n}\n/* the colour swaps while the heart is out of sight */\n.like.is-liking .like-line,\n.like.is-liking .like-fill {\n transition-duration: 0s;\n transition-delay: .3s;\n}\n/* everything that flies out starts from the heart's centre */\n.like-fx {\n position: absolute;\n left: 20px;\n top: 21px;\n width: 0;\n height: 0;\n pointer-events: none;\n}\n\n/* the heart: out of sight in 120 ms, waits inside the disc, then the spring brings it back full */\n.like.is-liking .like-icon {\n animation: like-vanish .12s cubic-bezier(.5, 0, .75, 0) both, like-return .7s var(--like-spring) .32s forwards;\n}\n@keyframes like-vanish {\n from {\n scale: 1;\n opacity: 1;\n }\n to {\n scale: .15;\n opacity: 0;\n }\n}\n@keyframes like-return {\n from {\n scale: .15;\n opacity: 0;\n }\n to {\n scale: 1;\n opacity: 1;\n }\n}\n\n/* unliking: a soft dip and back while the colour leaves, at rest at both ends */\n.like.is-unliking .like-icon {\n animation: like-dip .34s cubic-bezier(.37, 0, .63, 1);\n}\n@keyframes like-dip {\n 0% {\n scale: 1;\n }\n 40% {\n scale: .86;\n }\n 100% {\n scale: 1;\n }\n}\n\n/* the ring: one long ease-out growth (to 110%), the tint, and a hole that opens before the growth ends */\n.like-ring {\n position: absolute;\n left: -23px;\n top: -23px;\n width: 46px;\n height: 46px;\n border-radius: 50%;\n background: var(--like-color);\n scale: 0;\n -webkit-mask: radial-gradient(circle closest-side, transparent var(--like-hole), #000 calc(var(--like-hole) + 2%));\n mask: radial-gradient(circle closest-side, transparent var(--like-hole), #000 calc(var(--like-hole) + 2%));\n}\n.like.is-liking .like-ring {\n animation: like-ring-grow .56s cubic-bezier(.22, 1, .36, 1) .06s both, like-ring-tint .3s ease-out .06s both, like-ring-open .36s cubic-bezier(.33, 1, .68, 1) .28s both;\n}\n@keyframes like-ring-grow {\n from {\n scale: 0;\n }\n to {\n scale: 1.1;\n }\n}\n@keyframes like-ring-tint {\n from {\n background-color: var(--like-color);\n }\n to {\n background-color: var(--like-ring);\n }\n}\n@keyframes like-ring-open {\n from {\n --like-hole: 0%;\n }\n to {\n --like-hole: 100%;\n }\n}\n\n/* the dots: 7 pairs around the ring. They fly out on a long ease-out, swell and shrink on their own clock,\n and the whole spray turns 12° as it goes */\n.like-dots i {\n position: absolute;\n left: 0;\n top: 0;\n width: 0;\n height: 0;\n transform: rotate(var(--a));\n}\n.like-dots b {\n position: absolute;\n left: -2.5px;\n top: -2.5px;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: var(--d1);\n scale: 0;\n}\n.like-dots b + b {\n left: -1.75px;\n top: -1.75px;\n width: 3.5px;\n height: 3.5px;\n background: var(--d2);\n}\n.like-dots i:nth-child(1) { --a: 0deg; --d1: #F91880; --d2: #FF7AB6; }\n.like-dots i:nth-child(2) { --a: 51.43deg; --d1: #FFAD1F; --d2: #FFD166; }\n.like-dots i:nth-child(3) { --a: 102.86deg; --d1: #1D9BF0; --d2: #7CC6FE; }\n.like-dots i:nth-child(4) { --a: 154.29deg; --d1: #A78BFA; --d2: #C4B5FD; }\n.like-dots i:nth-child(5) { --a: 205.71deg; --d1: #00BA7C; --d2: #6EE7B7; }\n.like-dots i:nth-child(6) { --a: 257.14deg; --d1: #F97316; --d2: #FDBA74; }\n.like-dots i:nth-child(7) { --a: 308.57deg; --d1: #F91880; --d2: #CC8EF5; }\n.like.is-liking .like-dots i {\n animation: like-swirl .9s cubic-bezier(.22, 1, .36, 1) .34s both;\n}\n.like.is-liking .like-dots b {\n animation: like-fly .62s cubic-bezier(.16, 1, .3, 1) .34s both, like-dot .62s .34s both;\n}\n.like.is-liking .like-dots b + b {\n animation-name: like-fly-2, like-dot;\n animation-delay: .38s;\n}\n@keyframes like-swirl {\n from {\n rotate: 0deg;\n }\n to {\n rotate: 12deg;\n }\n}\n@keyframes like-fly {\n from {\n translate: 0 -20px;\n }\n to {\n translate: 0 -33px;\n }\n}\n@keyframes like-fly-2 {\n from {\n translate: 4.5px -18px;\n }\n to {\n translate: 8px -29px;\n }\n}\n@keyframes like-dot {\n 0% {\n scale: 0;\n animation-timing-function: cubic-bezier(.33, 1, .68, 1);\n }\n 24% {\n scale: 1;\n animation-timing-function: cubic-bezier(.32, 0, .67, 0);\n }\n 100% {\n scale: 0;\n }\n}\n\n/* the small hearts (like.js adds them): the rise is one unbroken ease-out, the sway a sine that rests at its turns,\n and size and fade keep their own clocks */\n.like-float {\n position: absolute;\n left: 0;\n top: -6px;\n opacity: 0;\n animation: like-rise var(--dur) cubic-bezier(.33, 1, .68, 1) var(--dl) both, like-fade var(--dur) var(--dl) both;\n}\n.like-float span {\n display: block;\n animation: like-sway var(--dur) cubic-bezier(.37, 0, .63, 1) var(--dl) both, like-grow var(--dur) var(--dl) both;\n}\n.like-float svg {\n display: block;\n width: var(--s);\n height: var(--s);\n margin: calc(var(--s) / -2) 0 0 calc(var(--s) / -2);\n fill: var(--fc);\n}\n@keyframes like-rise {\n from {\n translate: 0 0;\n }\n to {\n translate: 0 var(--dy);\n }\n}\n@keyframes like-fade {\n 0% {\n opacity: 0;\n animation-timing-function: cubic-bezier(.33, 1, .68, 1);\n }\n 14% {\n opacity: 1;\n animation-timing-function: linear;\n }\n 50% {\n opacity: 1;\n animation-timing-function: cubic-bezier(.37, 0, .63, 1);\n }\n 100% {\n opacity: 0;\n }\n}\n@keyframes like-sway {\n 0% {\n translate: 0 0;\n rotate: 0deg;\n }\n 33% {\n translate: var(--dx) 0;\n rotate: var(--r);\n }\n 70% {\n translate: calc(var(--dx) * -.6) 0;\n rotate: calc(var(--r) * -.6);\n }\n 100% {\n translate: calc(var(--dx) * .25) 0;\n rotate: 0deg;\n }\n}\n@keyframes like-grow {\n 0% {\n scale: .3;\n animation-timing-function: cubic-bezier(.34, 1.56, .64, 1);\n }\n 24% {\n scale: 1;\n animation-timing-function: cubic-bezier(.32, 0, .67, 0);\n }\n 100% {\n scale: .8;\n }\n}\n\n/* less motion: only the colour changes */\n@media (prefers-reduced-motion: reduce) {\n .like,\n .like * {\n animation: none !important;\n }\n}\n";
3
+ const CSS = "/* Bursting heart · Craft Library, by Sara Lobo\n The heart eases out of sight, a disc grows and hollows into a ring, 14 dots and 2 or 3 small hearts fly out,\n and the heart springs back full. Every motion runs on its own property with its own curve; where two curves\n meet, both are at rest there, so nothing hitches. Unliking only takes the colour away. Pairs with like.js. */\n\n:root {\n --like-heart: #FF6767; /* the liked heart, filled (Figma: General / Heart) */\n --like-color: #F91880; /* the disc */\n --like-ring: #CC8EF5; /* the ring as it opens */\n --like-spring: cubic-bezier(.34, 1.4, .64, 1); /* fallback for browsers without linear() */\n}\n\n/* a damped spring (damping .55, 700 ms): about 12% over, a short settle, at rest at the end */\n@supports (animation-timing-function: linear(0, 1)) {\n :root {\n --like-spring: linear(0, 0.034, 0.12, 0.24, 0.378, 0.52, 0.655, 0.778, 0.883, 0.968, 1.033, 1.079, 1.108, 1.123, 1.126, 1.12, 1.108, 1.092, 1.075, 1.057, 1.04, 1.025, 1.012, 1.002, 0.994, 0.989, 0.986, 0.984, 0.984, 0.985, 0.987, 0.989, 0.991, 0.993, 0.995, 0.997, 0.999, 1, 1.001, 1.001, 1);\n }\n}\n\n/* lets the ring's hole open smoothly (without it, the ring leaves at once) */\n@property --like-hole {\n syntax: '<percentage>';\n inherits: false;\n initial-value: 0%;\n}\n\n/* the button: a 40px circle in black at 60%, made to sit over images */\n.like {\n all: unset;\n box-sizing: border-box;\n position: relative;\n display: inline-block;\n flex: none;\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: rgba(0, 0, 0, .6);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n transition: background .2s ease, transform .22s cubic-bezier(.2, .8, .2, 1);\n}\n.like:hover {\n background: rgba(0, 0, 0, .72);\n}\n.like:active {\n transform: scale(.94);\n}\n.like:focus-visible {\n outline: 2px solid #fff;\n outline-offset: 2px;\n}\n.like-icon {\n position: absolute;\n left: 8px;\n top: 9px;\n width: 24px;\n height: 24px;\n}\n.like-icon svg {\n display: block;\n overflow: visible;\n}\n.like-line {\n fill: none;\n stroke: #fff;\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n transition: stroke .24s cubic-bezier(.4, 0, .2, 1);\n}\n.like-fill {\n fill: var(--like-heart);\n opacity: 0;\n transition: opacity .24s cubic-bezier(.4, 0, .2, 1);\n}\n.like[aria-pressed=\"true\"] .like-line {\n stroke: var(--like-heart);\n}\n.like[aria-pressed=\"true\"] .like-fill {\n opacity: 1;\n}\n/* the colour swaps while the heart is out of sight */\n.like.is-liking .like-line,\n.like.is-liking .like-fill {\n transition-duration: 0s;\n transition-delay: .3s;\n}\n/* everything that flies out starts from the heart's centre */\n.like-fx {\n position: absolute;\n left: 20px;\n top: 21px;\n width: 0;\n height: 0;\n pointer-events: none;\n}\n\n/* the heart: out of sight in 120 ms, waits inside the disc, then the spring brings it back full */\n.like.is-liking .like-icon {\n animation: like-vanish .12s cubic-bezier(.5, 0, .75, 0) both, like-return .7s var(--like-spring) .32s forwards;\n}\n@keyframes like-vanish {\n from {\n scale: 1;\n opacity: 1;\n }\n to {\n scale: .15;\n opacity: 0;\n }\n}\n@keyframes like-return {\n from {\n scale: .15;\n opacity: 0;\n }\n to {\n scale: 1;\n opacity: 1;\n }\n}\n\n/* unliking: a soft dip and back while the colour leaves, at rest at both ends */\n.like.is-unliking .like-icon {\n animation: like-dip .34s cubic-bezier(.37, 0, .63, 1);\n}\n@keyframes like-dip {\n 0% {\n scale: 1;\n }\n 40% {\n scale: .86;\n }\n 100% {\n scale: 1;\n }\n}\n\n/* the ring: one long ease-out growth (to 110%), the tint, and a hole that opens before the growth ends */\n.like-ring {\n position: absolute;\n left: -23px;\n top: -23px;\n width: 46px;\n height: 46px;\n border-radius: 50%;\n background: var(--like-color);\n scale: 0;\n -webkit-mask: radial-gradient(circle closest-side, transparent var(--like-hole), #000 calc(var(--like-hole) + 2%));\n mask: radial-gradient(circle closest-side, transparent var(--like-hole), #000 calc(var(--like-hole) + 2%));\n}\n.like.is-liking .like-ring {\n animation: like-ring-grow .56s cubic-bezier(.22, 1, .36, 1) .06s both, like-ring-tint .3s ease-out .06s both, like-ring-open .36s cubic-bezier(.33, 1, .68, 1) .28s both;\n}\n@keyframes like-ring-grow {\n from {\n scale: 0;\n }\n to {\n scale: 1.1;\n }\n}\n@keyframes like-ring-tint {\n from {\n background-color: var(--like-color);\n }\n to {\n background-color: var(--like-ring);\n }\n}\n@keyframes like-ring-open {\n from {\n --like-hole: 0%;\n }\n to {\n --like-hole: 100%;\n }\n}\n\n/* the dots: 7 pairs around the ring. They fly out on a long ease-out, swell and shrink on their own clock,\n and the whole spray turns 12° as it goes */\n.like-dots i {\n position: absolute;\n left: 0;\n top: 0;\n width: 0;\n height: 0;\n transform: rotate(var(--a));\n}\n.like-dots b {\n position: absolute;\n left: -2.5px;\n top: -2.5px;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: var(--d1);\n scale: 0;\n}\n.like-dots b + b {\n left: -1.75px;\n top: -1.75px;\n width: 3.5px;\n height: 3.5px;\n background: var(--d2);\n}\n.like-dots i:nth-child(1) { --a: 0deg; --d1: #F91880; --d2: #FF7AB6; }\n.like-dots i:nth-child(2) { --a: 51.43deg; --d1: #FFAD1F; --d2: #FFD166; }\n.like-dots i:nth-child(3) { --a: 102.86deg; --d1: #1D9BF0; --d2: #7CC6FE; }\n.like-dots i:nth-child(4) { --a: 154.29deg; --d1: #A78BFA; --d2: #C4B5FD; }\n.like-dots i:nth-child(5) { --a: 205.71deg; --d1: #00BA7C; --d2: #6EE7B7; }\n.like-dots i:nth-child(6) { --a: 257.14deg; --d1: #F97316; --d2: #FDBA74; }\n.like-dots i:nth-child(7) { --a: 308.57deg; --d1: #F91880; --d2: #CC8EF5; }\n.like.is-liking .like-dots i {\n animation: like-swirl .9s cubic-bezier(.22, 1, .36, 1) .34s both;\n}\n.like.is-liking .like-dots b {\n animation: like-fly .62s cubic-bezier(.16, 1, .3, 1) .34s both, like-dot .62s .34s both;\n}\n.like.is-liking .like-dots b + b {\n animation-name: like-fly-2, like-dot;\n animation-delay: .38s;\n}\n@keyframes like-swirl {\n from {\n rotate: 0deg;\n }\n to {\n rotate: 12deg;\n }\n}\n@keyframes like-fly {\n from {\n translate: 0 -20px;\n }\n to {\n translate: 0 -33px;\n }\n}\n@keyframes like-fly-2 {\n from {\n translate: 4.5px -18px;\n }\n to {\n translate: 8px -29px;\n }\n}\n@keyframes like-dot {\n 0% {\n scale: 0;\n animation-timing-function: cubic-bezier(.33, 1, .68, 1);\n }\n 24% {\n scale: 1;\n animation-timing-function: cubic-bezier(.32, 0, .67, 0);\n }\n 100% {\n scale: 0;\n }\n}\n\n/* the small hearts (like.js adds them): the rise is one unbroken ease-out, the sway a sine that rests at its turns,\n and size and fade keep their own clocks */\n.like-float {\n position: absolute;\n left: 0;\n top: -6px;\n opacity: 0;\n animation: like-rise var(--dur) cubic-bezier(.33, 1, .68, 1) var(--dl) both, like-fade var(--dur) var(--dl) both;\n}\n.like-float span {\n display: block;\n animation: like-sway var(--dur) cubic-bezier(.37, 0, .63, 1) var(--dl) both, like-grow var(--dur) var(--dl) both;\n}\n.like-float svg {\n display: block;\n width: var(--s);\n height: var(--s);\n margin: calc(var(--s) / -2) 0 0 calc(var(--s) / -2);\n fill: var(--fc);\n}\n@keyframes like-rise {\n from {\n translate: 0 0;\n }\n to {\n translate: 0 var(--dy);\n }\n}\n@keyframes like-fade {\n 0% {\n opacity: 0;\n animation-timing-function: cubic-bezier(.33, 1, .68, 1);\n }\n 14% {\n opacity: 1;\n animation-timing-function: linear;\n }\n 50% {\n opacity: 1;\n animation-timing-function: cubic-bezier(.37, 0, .63, 1);\n }\n 100% {\n opacity: 0;\n }\n}\n@keyframes like-sway {\n 0% {\n translate: 0 0;\n rotate: 0deg;\n }\n 33% {\n translate: var(--dx) 0;\n rotate: var(--r);\n }\n 70% {\n translate: calc(var(--dx) * -.6) 0;\n rotate: calc(var(--r) * -.6);\n }\n 100% {\n translate: calc(var(--dx) * .25) 0;\n rotate: 0deg;\n }\n}\n@keyframes like-grow {\n 0% {\n scale: .3;\n animation-timing-function: cubic-bezier(.34, 1.56, .64, 1);\n }\n 24% {\n scale: 1;\n animation-timing-function: cubic-bezier(.32, 0, .67, 0);\n }\n 100% {\n scale: .8;\n }\n}\n\n/* less motion: only the colour changes */\n@media (prefers-reduced-motion: reduce) {\n .like,\n .like * {\n animation: none !important;\n }\n}\n";
4
4
  const INNER = "<span class=\"like-fx\" aria-hidden=\"true\"><span class=\"like-ring\"></span><span class=\"like-dots\"><i><b></b><b></b></i><i><b></b><b></b></i><i><b></b><b></b></i><i><b></b><b></b></i><i><b></b><b></b></i><i><b></b><b></b></i><i><b></b><b></b></i></span></span><span class=\"like-icon\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\"><path class=\"like-fill\" d=\"M19.0711 13.1421L13.4142 18.799C12.6332 19.58 11.3668 19.58 10.5858 18.799L4.92893 13.1421C2.97631 11.1895 2.97631 8.02369 4.92893 6.07107C6.88155 4.11845 10.0474 4.11845 12 6.07107C13.9526 4.11845 17.1184 4.11845 19.0711 6.07107C21.0237 8.02369 21.0237 11.1895 19.0711 13.1421Z\"></path><path class=\"like-line\" d=\"M19.0711 13.1421L13.4142 18.799C12.6332 19.58 11.3668 19.58 10.5858 18.799L4.92893 13.1421C2.97631 11.1895 2.97631 8.02369 4.92893 6.07107C6.88155 4.11845 10.0474 4.11845 12 6.07107C13.9526 4.11845 17.1184 4.11845 19.0711 6.07107C21.0237 8.02369 21.0237 11.1895 19.0711 13.1421Z\"></path></svg></span>";
5
5
  let api;
6
6
  function engine() {
package/dist/magnet.js CHANGED
@@ -8,8 +8,9 @@ function engine() {
8
8
  const window = {};
9
9
  // Magnetic icon · Craft Library, by Sara Lobo
10
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.
11
+ // strained rim creases and trembles, the rest of the ring frays out of round and loses voxels, the tip cracks into
12
+ // shards, and the voxels fly to the pointer. Let go, and the rim springs back with a wobble, closing up as the loose
13
+ // voxels return to it. Pairs with magnet.css.
13
14
  // Buttons added later: magnetize(root). To steer one without a mouse (a demo, a tour): magnetPoint(button, x, y) with
14
15
  // a point on screen, and magnetPoint(button, null) to hand it back to the pointer.
15
16
  (function () {
@@ -18,6 +19,7 @@ function engine() {
18
19
  var PULL = 15, FALL = 1.1; // the furthest the rim reaches out, and how fast the pull drops with distance
19
20
  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
21
  var JAG = 0.18; // how deep the strained rim creases
22
+ var FRAY = 2.6, CHIPS = 64; // how far the rest of the ring frays out of round, and how many voxels can fall out of it
21
23
  var DENT = 3.5; // the furthest the rim is drawn in while the pointer is over the button
22
24
  var RATE = 90, MAXDUST = 90; // voxels shed per second at full pull, and at most at once
23
25
  var ROOM = 46; // room for the shards and voxels on every side of the button (matches .mag-dust)
@@ -45,18 +47,26 @@ function engine() {
45
47
  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
48
  }
47
49
 
50
+ // how hard the field strains the metal (0 at rest, 1 at a full pull), and how worn the ring is at angle t: wear
51
+ // builds faster than the strain, so a middling pull already marks the whole ring; it is worst toward the pull, but
52
+ // the far side gives too, so the ring reads as coming apart, not only its tip
53
+ function strainOf(m) { return clamp((Math.abs(m.d) + 3 * m.heat) / PULL, 0, 1); }
54
+ function damage(m, t) { return clamp((Math.abs(m.d) + 3 * m.heat) / 9, 0, 1) * (0.5 + 0.5 * bell(wrap(t - m.a), 1.3)); }
55
+
48
56
  // 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
57
+ // trembling facets where the magnet strains it, and frayed out of round, in short crooked facets, around the rest
50
58
  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;
59
+ var pts = [], strain = strainOf(m), i, t, r, env, sharp, fray, n, p, a, b, c, e;
52
60
  for (i = 0; i < N; i++) {
53
61
  t = i / N * 2 * Math.PI;
54
62
  r = radius(m, t);
55
63
  env = bell(wrap(t - m.a), 0.6) * strain;
56
64
  sharp = env > 0.12;
57
- if (sharp && i % 3) continue;
65
+ fray = sharp ? 0 : clamp((damage(m, t) - 0.15) / 0.85, 0, 1);
66
+ if ((sharp && i % 3) || (fray > 0 && i % 2)) continue;
58
67
  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]);
68
+ else if (fray > 0) r += FRAY * fray * (hash(i, 21) - 0.62 + 0.3 * tremble(i + 500, now)); // each facet keeps its own dent (mostly worn in), and shivers a little
69
+ pts.push([m.cx + r * Math.cos(t), m.cy + r * Math.sin(t), sharp || fray > 0]);
60
70
  }
61
71
  n = pts.length;
62
72
  p = 'M' + n2(pts[0][0]) + ' ' + n2(pts[0][1]);
@@ -201,9 +211,33 @@ function engine() {
201
211
  g.restore();
202
212
  }
203
213
 
214
+ // the voxels falling out of the ring: holes in the rim's mask, each opening once the metal is broken enough where it
215
+ // sits (and flickering at the edge of breaking). The piece that falls out flies to the magnet; as the ring heals,
216
+ // the holes close again
217
+ function chips(m, now) {
218
+ var t = m.t;
219
+ m.chips.forEach(function (c, k) {
220
+ var o = clamp((damage(m, c.a) + 0.08 * tremble(300 + k, now) - c.at) / 0.12, 0, 1), z, r, x, y, dx, dy, l;
221
+ if (o < 0.02) o = 0; // closed means fully closed: no sliver left in the mask
222
+ if (!o && !c.open) return;
223
+ c.open = o;
224
+ z = c.s * o; r = radius(m, c.a) + c.o; x = m.cx + r * Math.cos(c.a); y = m.cy + r * Math.sin(c.a);
225
+ c.el.setAttribute('x', n2(x - z / 2)); c.el.setAttribute('y', n2(y - z / 2));
226
+ c.el.setAttribute('width', n2(z)); c.el.setAttribute('height', n2(z));
227
+ if (o < 0.2) c.gone = false;
228
+ else if (o > 0.6 && !c.gone && t && now - c.at2 > 500 && m.dust.length < MAXDUST) {
229
+ c.gone = true; c.at2 = now;
230
+ dx = C + t.x - x; dy = C + t.y - y; l = Math.hypot(dx, dy) || 1;
231
+ m.dust.push({ x: x, y: y, vx: dx / l * 10, vy: dy / l * 10, hx: x, hy: y, s: c.s, k: 1, age: 0, life: 0.7 + Math.random() * 0.6,
232
+ back: false, spin: Math.random() < 0.5 ? -1 : 1, c: Math.random() < 0.35 ? 190 : 255 });
233
+ }
234
+ });
235
+ }
236
+
204
237
  function draw(m, now) {
205
238
  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
239
  m.rim.setAttribute('d', rimPath(m, now));
240
+ chips(m, now);
207
241
  // the drawn rim gives way at the crack line, where the shards take over
208
242
  m.grad.setAttribute('cx', n2(m.cx)); m.grad.setAttribute('cy', n2(m.cy));
209
243
  m.stops[0].setAttribute('offset', (f[0] / 40).toFixed(4));
@@ -261,8 +295,18 @@ function engine() {
261
295
  cv.className = 'mag-dust';
262
296
  cv.setAttribute('aria-hidden', 'true');
263
297
  el.insertBefore(cv, icon);
298
+ // the voxels that can fall out of the ring: squares in the rim's mask, closed until the metal strains (see chips()).
299
+ // Each sits at its own spot round the ring, mostly biting the rim from outside, and gives way at its own strain
300
+ var mask = svg.querySelector('mask'), holes = [], k, q;
301
+ for (k = 0; mask && k < CHIPS; k++) {
302
+ q = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
303
+ q.setAttribute('fill', '#000'); q.setAttribute('width', 0); q.setAttribute('height', 0);
304
+ mask.appendChild(q);
305
+ holes.push({ el: q, a: (k + 0.8 * hash(k, 11)) / CHIPS * 2 * Math.PI, s: 0.9 + 1.5 * hash(k, 12), o: 0.2 + 1.1 * (hash(k, 13) - 0.5),
306
+ at: 0.08 + 0.72 * hash(k, 14), open: 0, gone: false, at2: -1e9 });
307
+ }
264
308
  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,
309
+ icon: icon, cv: cv, g: cv.getContext('2d'), dust: [], chips: holes, acc: 0, pace: 0, lp: null, ghost: null,
266
310
  d: 0, v: 0, a: -Math.PI / 2, heat: 0, cx: C, cy: C, ix: 0, iy: 0, t: null, still: true };
267
311
  mags.push(el.__mag);
268
312
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-library",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
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
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -27,7 +27,8 @@
27
27
  }
28
28
  },
29
29
  "devDependencies": {
30
- "esbuild": "0.24.0"
30
+ "esbuild": "0.24.0",
31
+ "playwright-core": "^1.61.1"
31
32
  },
32
33
  "scripts": {
33
34
  "build": "node build.mjs",