fancy-ui-react 0.2.0 → 0.3.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.
@@ -52,7 +52,11 @@ fn fs_splat(in: VSOut) -> @location(0) vec4f {
52
52
  p.x = p.x * u.aspect;
53
53
  let splat = exp(-dot(p, p) / u.radius) * u.color.rgb;
54
54
  let base = textureSample(tex0, smpLinear, in.uv).rgb;
55
- return vec4f(base + splat, 1.0);
55
+ // color.a is the click flag: a click splat lifts each texel to its own
56
+ // profile instead of adding to it, so a burst of clicks paints the same
57
+ // disc once instead of summing towards white and widening every click.
58
+ let sum = select(base + splat, max(base, splat), u.color.a > 0.5);
59
+ return vec4f(sum, 1.0);
56
60
  }
57
61
 
58
62
  @fragment
@@ -233,7 +237,7 @@ async function createWebGpuFluid(canvas, opts) {
233
237
  scratch[OFF_COLOR] = v.color[0];
234
238
  scratch[OFF_COLOR + 1] = v.color[1];
235
239
  scratch[OFF_COLOR + 2] = v.color[2];
236
- scratch[OFF_COLOR + 3] = 1;
240
+ scratch[OFF_COLOR + 3] = v.color[3] ?? 0;
237
241
  }
238
242
  scratch[OFF_DT] = v.dt ?? 0;
239
243
  scratch[OFF_DISSIPATION] = v.dissipation ?? 0;
@@ -368,7 +372,7 @@ async function createWebGpuFluid(canvas, opts) {
368
372
  canvas.height = height;
369
373
  initTextures();
370
374
  }
371
- }, splat = function(x, y, dx, dy, color) {
375
+ }, splat = function(x, y, dx, dy, color, saturate = false) {
372
376
  if (destroyed || lost || !velocity || !dye) return;
373
377
  let radius = correctRadius(opts.splatRadius / 100, canvas.width, canvas.height);
374
378
  if (opts.contained) {
@@ -383,7 +387,7 @@ async function createWebGpuFluid(canvas, opts) {
383
387
  });
384
388
  writeUniforms(uSplatDye, {
385
389
  point: [x, y],
386
- color: [color.r, color.g, color.b],
390
+ color: [color.r, color.g, color.b, saturate ? 1 : 0],
387
391
  aspect,
388
392
  radius
389
393
  });
@@ -716,13 +720,15 @@ async function startWebGpuFluid(canvas, opts) {
716
720
  engine.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
717
721
  }
718
722
  function clickSplat(pointer) {
719
- const color = opts.generateColor();
720
- color.r *= 10;
721
- color.g *= 10;
722
- color.b *= 10;
723
+ const base = opts.generateColor();
724
+ const color = {
725
+ r: base.r * opts.clickBoost,
726
+ g: base.g * opts.clickBoost,
727
+ b: base.b * opts.clickBoost
728
+ };
723
729
  const dx = 10 * (Math.random() - 0.5);
724
730
  const dy = 30 * (Math.random() - 0.5);
725
- engine.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
731
+ engine.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color, true);
726
732
  }
727
733
  function multipleSplats(steps) {
728
734
  const angle = Math.random() * Math.PI * 2;
@@ -780,6 +786,9 @@ async function startWebGpuFluid(canvas, opts) {
780
786
  updatePointerDownData(pointer, -1, posX, posY);
781
787
  clickSplat(pointer);
782
788
  }
789
+ function handlePointerUp() {
790
+ pointers[0].down = false;
791
+ }
783
792
  function handleFirstMouseMove(e) {
784
793
  const pointer = pointers[0];
785
794
  const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
@@ -826,11 +835,14 @@ async function startWebGpuFluid(canvas, opts) {
826
835
  }
827
836
  if (opts.interactive) {
828
837
  window.addEventListener("mousedown", handleMouseDown);
838
+ window.addEventListener("mouseup", handlePointerUp);
829
839
  document.body.addEventListener("mousemove", handleFirstMouseMove);
830
840
  window.addEventListener("mousemove", handleMouseMove);
831
841
  document.body.addEventListener("touchstart", handleFirstTouchStart);
832
842
  window.addEventListener("touchstart", handleTouchStart, false);
833
843
  window.addEventListener("touchmove", handleTouchMove, false);
844
+ window.addEventListener("touchend", handlePointerUp);
845
+ window.addEventListener("touchcancel", handlePointerUp);
834
846
  }
835
847
  let observer = null;
836
848
  if (opts.pauseWhenHidden) {
@@ -903,11 +915,14 @@ async function startWebGpuFluid(canvas, opts) {
903
915
  if (autoSplatTimer) clearInterval(autoSplatTimer);
904
916
  if (opts.interactive) {
905
917
  window.removeEventListener("mousedown", handleMouseDown);
918
+ window.removeEventListener("mouseup", handlePointerUp);
906
919
  document.body.removeEventListener("mousemove", handleFirstMouseMove);
907
920
  window.removeEventListener("mousemove", handleMouseMove);
908
921
  document.body.removeEventListener("touchstart", handleFirstTouchStart);
909
922
  window.removeEventListener("touchstart", handleTouchStart);
910
923
  window.removeEventListener("touchmove", handleTouchMove);
924
+ window.removeEventListener("touchend", handlePointerUp);
925
+ window.removeEventListener("touchcancel", handlePointerUp);
911
926
  }
912
927
  if (opts.contained) {
913
928
  window.removeEventListener("resize", updateCanvasRectCache);
package/dist/index.d.ts CHANGED
@@ -85,6 +85,7 @@ export * from "./components/marquee/index.js";
85
85
  export * from "./components/matrix-rain/index.js";
86
86
  export * from "./components/meteors/index.js";
87
87
  export * from "./components/mosaic-glow/index.js";
88
+ export * from "./components/datamosh-transition/index.js";
88
89
  export * from "./components/navbar/index.js";
89
90
  export * from "./components/navigation-menu/index.js";
90
91
  export * from "./components/neon-border/index.js";
package/dist/index.js CHANGED
@@ -134,6 +134,8 @@ import { ReviewCard } from "./components/marquee/ReviewCard.js";
134
134
  import { MatrixRain } from "./components/matrix-rain/MatrixRain.js";
135
135
  import { Meteors } from "./components/meteors/Meteors.js";
136
136
  import { MosaicGlow } from "./components/mosaic-glow/MosaicGlow.js";
137
+ import { DatamoshTransition } from "./components/datamosh-transition/DatamoshTransition.js";
138
+ import { DATAMOSH_PALETTES } from "./components/datamosh-transition/datamosh-transition-core.js";
137
139
  import { Navbar } from "./components/navbar/Navbar.js";
138
140
  import { NavbarLink } from "./components/navbar/NavbarLink.js";
139
141
  import { NavigationMenu } from "./components/navigation-menu/NavigationMenu.js";
@@ -302,9 +304,11 @@ export {
302
304
  ContextMenuTrigger,
303
305
  ContextRing,
304
306
  CopyButton,
307
+ DATAMOSH_PALETTES,
305
308
  DEFAULT_SOUND_FEEDBACK_ON,
306
309
  DEFAULT_SOUND_PREFERENCES,
307
310
  DROPDOWN_MENU_KEY,
311
+ DatamoshTransition,
308
312
  DatePicker,
309
313
  Dialog,
310
314
  DimSiblings,
package/dist/styles.css CHANGED
@@ -313,7 +313,81 @@
313
313
  * scales the panel about its own centre without touching the centring.
314
314
  */
315
315
  .ft-btn {
316
- --ft-btn-accent: var(--ft-accent, light-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75)));
316
+ --ft-btn-accent: var(
317
+ --ft-accent,
318
+ light-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))
319
+ );
320
+ /* 150ms = tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) =
321
+ tokens.EASINGS.inout — the reversible-state pair. It runs the colour
322
+ channel, and the release half of the press: letting go settles, it does
323
+ not snap back. */
324
+ --ft-btn-motion: var(--ft-duration-fast, 150ms) var(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1));
325
+ /* The down-stroke only. 150ms = tokens.DURATIONS.fast,
326
+ cubic-bezier(0.16, 1, 0.3, 1) = tokens.EASINGS.out — the arrival curve,
327
+ which spends most of the scale in the first frames, so the press bites
328
+ instead of easing in. Pressing is an arrival; releasing is what resolves
329
+ either way. */
330
+ --ft-btn-press: var(--ft-duration-fast, 150ms) var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
331
+ /*
332
+ * Replaces the `transition-colors` utility removed from the class string,
333
+ * at exactly the values that utility already resolved to. Colour is a state
334
+ * change, not motion, so it stays OUTSIDE the reduced-motion query — gating
335
+ * it would only make a theme flip look broken for the people who asked for
336
+ * less movement.
337
+ *
338
+ * `box-shadow` is deliberately absent, and must stay absent: this button's
339
+ * focus ring is `focus-visible:ring-*`, which compiles to a `box-shadow`,
340
+ * and a focus ring must never animate.
341
+ */
342
+ transition:
343
+ color var(--ft-btn-motion),
344
+ background-color var(--ft-btn-motion),
345
+ border-color var(--ft-btn-motion);
346
+ /* Kills the ~300ms tap delay without blocking scroll. A press that answers
347
+ a third of a second late is not press feedback. */
348
+ touch-action: manipulation;
349
+ }
350
+
351
+ /*
352
+ * The press. Pressable's contract inlined as a plain rule on the native
353
+ * control rather than wrapping a `<button>` in a `<Pressable>` div — same
354
+ * `0.97`, same clock, one element instead of two.
355
+ *
356
+ * Only the property list is re-declared in here: `transform` joins the colour
357
+ * channel under `no-preference` and nowhere else, so with motion reduced the
358
+ * colours still cross and the button simply does not move. The resting state
359
+ * (no `transform` at all) is the ungated fallback.
360
+ *
361
+ * The two strokes are timed apart. The rule below carries the release curve,
362
+ * because that is the transition the button runs on its way back to rest;
363
+ * `:active` overrides `transform` with the arrival curve for the way down.
364
+ */
365
+ @media (prefers-reduced-motion: no-preference) {
366
+ .ft-btn {
367
+ transition:
368
+ color var(--ft-btn-motion),
369
+ background-color var(--ft-btn-motion),
370
+ border-color var(--ft-btn-motion),
371
+ transform var(--ft-btn-motion);
372
+ }
373
+
374
+ /* Both inert states are excluded so a press that does nothing does not
375
+ pretend to. `[data-disabled]` is the anchor branch, which has no native
376
+ `:disabled` to hang this off; `[aria-busy]` is the loading one, on both
377
+ branches. A native disabled `<button>` never matches `:active` anyway —
378
+ the attribute selectors are what make the anchor behave like it. */
379
+ .ft-btn:not([data-disabled="true"]):not([aria-busy="true"]):active {
380
+ transform: scale(0.97);
381
+ /* The whole list is re-declared rather than just
382
+ `transition-timing-function`, which has no per-property form here: a
383
+ single value would retime the colour channel too. Only `transform`
384
+ differs from the rule above. */
385
+ transition:
386
+ color var(--ft-btn-motion),
387
+ background-color var(--ft-btn-motion),
388
+ border-color var(--ft-btn-motion),
389
+ transform var(--ft-btn-press);
390
+ }
317
391
  }
318
392
 
319
393
  .ft-btn--accent {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fancy-ui-react",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Animated UI components for React — the React counterpart of fancy-ui-svelte. WebGL, GSAP and CSS motion, styled with Tailwind CSS v4.",
5
5
  "license": "MIT",
6
6
  "type": "module",