fancy-ui-svelte 0.4.0 → 0.4.1

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.
@@ -12,27 +12,6 @@ Creates animated SVG beams that connect two elements with smooth gradients and c
12
12
  - Reversible animation direction
13
13
  - Position offsets for fine-tuning
14
14
 
15
- ## Implementation Notes
16
-
17
- ### Vue → Svelte Conversions
18
- - `ref()` → `$state()`
19
- - `computed()` → `$derived()`
20
- - `watchEffect()` → `$effect()` (via `onMount`)
21
- - Template refs → `bind:this`
22
- - Props handling with destructuring defaults
23
-
24
- ### Key Differences
25
- 1. **Element References**: Svelte uses `bind:this` instead of Vue's `ref` template attribute
26
- 2. **Lifecycle**: Setup logic moved to `onMount` with cleanup return
27
- 3. **Derived Values**: Functions returning derived values (x1, x2, y1, y2) instead of computed refs
28
- 4. **Conditional Rendering**: Used `{#if}` blocks to ensure refs are defined before rendering beams
29
-
30
- ### Implementation Details
31
- - ResizeObserver for responsive path updates
32
- - SVG path calculation using quadratic Bezier curves (Q command)
33
- - Direction detection for vertical/horizontal beams
34
- - Unique gradient IDs to prevent conflicts with multiple instances
35
-
36
15
  ## Demo
37
16
  See `/demo/animated-beam` for usage examples.
38
17
 
@@ -11,7 +11,7 @@
11
11
  interface Props {
12
12
  options?: ConfettiOptions;
13
13
  globalOptions?: ConfettiGlobalOptions;
14
- manualstart?: boolean;
14
+ manualStart?: boolean;
15
15
  class?: string;
16
16
  children?: Snippet;
17
17
  }
@@ -19,7 +19,7 @@
19
19
  let {
20
20
  options = {},
21
21
  globalOptions = {},
22
- manualstart = false,
22
+ manualStart = false,
23
23
  class: className = "",
24
24
  children,
25
25
  }: Props = $props();
@@ -39,7 +39,7 @@
39
39
  resize: true,
40
40
  });
41
41
 
42
- if (!manualstart) {
42
+ if (!manualStart) {
43
43
  fire();
44
44
  }
45
45
 
@@ -3,7 +3,7 @@ import type { GlobalOptions as ConfettiGlobalOptions, Options as ConfettiOptions
3
3
  interface Props {
4
4
  options?: ConfettiOptions;
5
5
  globalOptions?: ConfettiGlobalOptions;
6
- manualstart?: boolean;
6
+ manualStart?: boolean;
7
7
  class?: string;
8
8
  children?: Snippet;
9
9
  }
@@ -11,7 +11,7 @@ Canvas wrapper that creates a confetti instance.
11
11
  |------|------|---------|-------------|
12
12
  | `options` | `ConfettiOptions` | `{}` | Default confetti options |
13
13
  | `globalOptions` | `ConfettiGlobalOptions` | `{}` | Canvas creation options |
14
- | `manualstart` | `boolean` | `false` | Skip auto-fire on mount |
14
+ | `manualStart` | `boolean` | `false` | Skip auto-fire on mount |
15
15
  | `class` | `string` | `''` | Canvas CSS classes |
16
16
 
17
17
  ### ConfettiButton
@@ -21,8 +21,11 @@
21
21
  splatForce?: number;
22
22
  shading?: boolean;
23
23
  colorUpdateSpeed?: number;
24
- backColor?: ColorRGB;
24
+ backColor?: ColorRGB | string;
25
25
  transparent?: boolean;
26
+ fluidColor?: string;
27
+ fluidColors?: string[];
28
+ colorIntensity?: number;
26
29
  class?: string;
27
30
  }
28
31
 
@@ -41,9 +44,32 @@
41
44
  colorUpdateSpeed = 10,
42
45
  backColor = { r: 0.5, g: 0, b: 0 },
43
46
  transparent = true,
47
+ fluidColor,
48
+ fluidColors,
49
+ colorIntensity = 0.15,
44
50
  class: className = "",
45
51
  }: Props = $props();
46
52
 
53
+ function hexToRgb(hex: string): ColorRGB {
54
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
55
+ if (!result) {
56
+ console.warn(
57
+ `[FluidCursor] Invalid hex color: "${hex}". Expected format: "#rrggbb". Falling back to white.`
58
+ );
59
+ return { r: 1, g: 1, b: 1 };
60
+ }
61
+ return {
62
+ r: parseInt(result[1], 16) / 255,
63
+ g: parseInt(result[2], 16) / 255,
64
+ b: parseInt(result[3], 16) / 255,
65
+ };
66
+ }
67
+
68
+ const clampedColorIntensity = Math.max(0, Math.min(1, colorIntensity));
69
+
70
+ const resolvedBackColor: ColorRGB =
71
+ typeof backColor === "string" ? hexToRgb(backColor) : backColor;
72
+
47
73
  let canvasRef: HTMLCanvasElement;
48
74
 
49
75
  onMount(() => {
@@ -94,7 +120,7 @@
94
120
  SHADING: shading,
95
121
  COLOR_UPDATE_SPEED: colorUpdateSpeed,
96
122
  PAUSED: false,
97
- BACK_COLOR: backColor,
123
+ BACK_COLOR: resolvedBackColor,
98
124
  TRANSPARENT: transparent,
99
125
  };
100
126
 
@@ -936,8 +962,40 @@
936
962
 
937
963
  let lastUpdateTime = Date.now();
938
964
  let colorUpdateTimer = 0.0;
965
+ let colorIndex = 0;
939
966
  let animationFrameId: number;
940
967
 
968
+ // Cached pre-scaled colors to avoid re-parsing hex on every splat
969
+ let cachedFluidColorHex: string | undefined;
970
+ let cachedFluidColor: ColorRGB | null = null;
971
+ let cachedFluidColorsRef: string[] | undefined;
972
+ let cachedFluidColorsScaled: ColorRGB[] = [];
973
+
974
+ function getScaledColor(hex: string): ColorRGB {
975
+ const { r, g, b } = hexToRgb(hex);
976
+ return {
977
+ r: r * clampedColorIntensity,
978
+ g: g * clampedColorIntensity,
979
+ b: b * clampedColorIntensity,
980
+ };
981
+ }
982
+
983
+ function getCachedFluidColor(hex: string): ColorRGB {
984
+ if (cachedFluidColor === null || cachedFluidColorHex !== hex) {
985
+ cachedFluidColorHex = hex;
986
+ cachedFluidColor = getScaledColor(hex);
987
+ }
988
+ return cachedFluidColor;
989
+ }
990
+
991
+ function getCachedFluidColors(colors: string[]): ColorRGB[] {
992
+ if (cachedFluidColorsRef !== colors) {
993
+ cachedFluidColorsRef = colors;
994
+ cachedFluidColorsScaled = colors.map(getScaledColor);
995
+ }
996
+ return cachedFluidColorsScaled;
997
+ }
998
+
941
999
  function updateFrame() {
942
1000
  const dt = calcDeltaTime();
943
1001
  if (resizeCanvas()) initFramebuffers();
@@ -1232,10 +1290,19 @@
1232
1290
  }
1233
1291
 
1234
1292
  function generateColor(): ColorRGB {
1293
+ if (fluidColor) {
1294
+ return getCachedFluidColor(fluidColor);
1295
+ }
1296
+ if (fluidColors && fluidColors.length > 0) {
1297
+ const colors = getCachedFluidColors(fluidColors);
1298
+ const idx = colorIndex % colors.length;
1299
+ colorIndex++;
1300
+ return colors[idx];
1301
+ }
1235
1302
  const c = HSVtoRGB(Math.random(), 1.0, 1.0);
1236
- c.r *= 0.15;
1237
- c.g *= 0.15;
1238
- c.b *= 0.15;
1303
+ c.r *= clampedColorIntensity;
1304
+ c.g *= clampedColorIntensity;
1305
+ c.b *= clampedColorIntensity;
1239
1306
  return c;
1240
1307
  }
1241
1308
 
@@ -16,8 +16,11 @@ interface Props {
16
16
  splatForce?: number;
17
17
  shading?: boolean;
18
18
  colorUpdateSpeed?: number;
19
- backColor?: ColorRGB;
19
+ backColor?: ColorRGB | string;
20
20
  transparent?: boolean;
21
+ fluidColor?: string;
22
+ fluidColors?: string[];
23
+ colorIntensity?: number;
21
24
  class?: string;
22
25
  }
23
26
  declare const FluidCursor: import("svelte").Component<Props, {}, "">;
@@ -1,5 +1,5 @@
1
1
  import { render, cleanup } from "@testing-library/svelte";
2
- import { afterEach, describe, it, expect } from "vitest";
2
+ import { afterEach, describe, it, expect, vi } from "vitest";
3
3
  import FluidCursor from "./FluidCursor.svelte";
4
4
  describe("FluidCursor", () => {
5
5
  afterEach(cleanup);
@@ -38,4 +38,37 @@ describe("FluidCursor", () => {
38
38
  const div = container.firstElementChild;
39
39
  expect(div?.className).toContain("my-fluid");
40
40
  });
41
+ describe("color props", () => {
42
+ it("renders without error when fluidColor is provided", () => {
43
+ const { container } = render(FluidCursor, { props: { fluidColor: "#00ffcc" } });
44
+ expect(container.querySelector("canvas")).toBeInTheDocument();
45
+ });
46
+ it("renders without error when fluidColors is provided", () => {
47
+ const { container } = render(FluidCursor, {
48
+ props: { fluidColors: ["#ff0080", "#00ffcc", "#7700ff"] },
49
+ });
50
+ expect(container.querySelector("canvas")).toBeInTheDocument();
51
+ });
52
+ it("renders without error when fluidColor and fluidColors are both provided (fluidColor takes priority)", () => {
53
+ // Both props accepted without throw — fluidColor priority is exercised at runtime
54
+ const { container } = render(FluidCursor, {
55
+ props: { fluidColor: "#ff0000", fluidColors: ["#00ffcc", "#7700ff"] },
56
+ });
57
+ expect(container.querySelector("canvas")).toBeInTheDocument();
58
+ });
59
+ it("renders without error when backColor is a hex string", () => {
60
+ const { container } = render(FluidCursor, { props: { backColor: "#1a1a2e" } });
61
+ expect(container.querySelector("canvas")).toBeInTheDocument();
62
+ });
63
+ it("warns and falls back when an invalid hex string is passed to backColor", () => {
64
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
65
+ render(FluidCursor, { props: { backColor: "not-a-color" } });
66
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("[FluidCursor] Invalid hex color"));
67
+ warn.mockRestore();
68
+ });
69
+ it("renders without error when colorIntensity is out of [0,1] range (clamped internally)", () => {
70
+ const { container } = render(FluidCursor, { props: { colorIntensity: 5 } });
71
+ expect(container.querySelector("canvas")).toBeInTheDocument();
72
+ });
73
+ });
41
74
  });