@leftium/logo 0.0.2 → 0.2.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.
Files changed (34) hide show
  1. package/dist/AppLogo.svelte +146 -0
  2. package/dist/AppLogo.svelte.d.ts +7 -0
  3. package/dist/LeftiumLogo.svelte +267 -186
  4. package/dist/LeftiumLogo.svelte.d.ts +1 -0
  5. package/dist/app-logo/color-transform.d.ts +40 -0
  6. package/dist/app-logo/color-transform.js +142 -0
  7. package/dist/app-logo/config-serialization.d.ts +80 -0
  8. package/dist/app-logo/config-serialization.js +489 -0
  9. package/dist/app-logo/defaults.d.ts +60 -0
  10. package/dist/app-logo/defaults.js +55 -0
  11. package/dist/app-logo/generate-favicon-set.d.ts +44 -0
  12. package/dist/app-logo/generate-favicon-set.js +97 -0
  13. package/dist/app-logo/generate-ico.d.ts +18 -0
  14. package/dist/app-logo/generate-ico.js +63 -0
  15. package/dist/app-logo/generate-png.d.ts +16 -0
  16. package/dist/app-logo/generate-png.js +60 -0
  17. package/dist/app-logo/generate-svg.d.ts +9 -0
  18. package/dist/app-logo/generate-svg.js +160 -0
  19. package/dist/app-logo/iconify.d.ts +35 -0
  20. package/dist/app-logo/iconify.js +223 -0
  21. package/dist/app-logo/squircle.d.ts +43 -0
  22. package/dist/app-logo/squircle.js +213 -0
  23. package/dist/app-logo/types.d.ts +39 -0
  24. package/dist/app-logo/types.js +1 -0
  25. package/dist/assets/logo-parts/glow-squircle.svg +44 -0
  26. package/dist/index.d.ts +8 -3
  27. package/dist/index.js +9 -3
  28. package/dist/leftium-logo/generate-svg.d.ts +29 -0
  29. package/dist/leftium-logo/generate-svg.js +470 -0
  30. package/dist/tooltip.d.ts +18 -0
  31. package/dist/tooltip.js +38 -0
  32. package/dist/webgl-ripples/webgl-ripples.d.ts +0 -4
  33. package/dist/webgl-ripples/webgl-ripples.js +1 -1
  34. package/package.json +35 -20
@@ -35,6 +35,7 @@
35
35
  import { Ripples, type RipplesOptions } from './webgl-ripples/webgl-ripples.js';
36
36
 
37
37
  import logoGlow from './assets/logo-parts/glow.svg';
38
+ import logoGlowSquircle from './assets/logo-parts/glow-squircle.svg';
38
39
  import logoLigature from './assets/logo-parts/ligature.svg';
39
40
  import logoShadow from './assets/logo-parts/shadow.svg';
40
41
  import logoSquare from './assets/logo-parts/square.svg?inline';
@@ -44,21 +45,56 @@
44
45
  toggleAnimationWithShift?: boolean;
45
46
  ripplesOptions?: RipplesOptions;
46
47
  boundingBox?: 'square' | 'default' | 'cropped' | 'encircled';
48
+ squircle?: boolean;
47
49
  class?: string;
48
50
  onClick?: (event: MouseEvent | KeyboardEvent) => void;
49
51
  [key: string]: unknown; // Allow any additional props
50
52
  }
51
53
 
54
+ // Squircle clip-path (50% radius, K=2 superellipse, CSS polygon with percentages)
55
+ const SQUIRCLE_CLIP =
56
+ 'polygon(50% 0%, 53.05% 0%, 55.96% 0%, 58.74% 0%, 61.38% 0%, 63.89% 0%, 66.27% 0%, 68.54% 0.01%, 70.69% 0.01%, 72.73% 0.02%, 74.66% 0.03%, 76.48% 0.04%, 78.21% 0.06%, 79.84% 0.09%, 81.37% 0.11%, 82.82% 0.15%, 84.18% 0.2%, 85.46% 0.25%, 86.66% 0.31%, 87.78% 0.39%, 88.83% 0.48%, 89.81% 0.58%, 90.73% 0.7%, 91.58% 0.83%, 92.37% 0.99%, 93.11% 1.16%, 93.79% 1.36%, 94.41% 1.58%, 94.99% 1.83%, 95.53% 2.11%, 96.02% 2.41%, 96.47% 2.75%, 96.88% 3.13%, 97.25% 3.53%, 97.59% 3.98%, 97.89% 4.47%, 98.17% 5.01%, 98.42% 5.59%, 98.64% 6.21%, 98.84% 6.89%, 99.01% 7.63%, 99.17% 8.42%, 99.3% 9.27%, 99.42% 10.19%, 99.52% 11.17%, 99.61% 12.22%, 99.69% 13.34%, 99.75% 14.54%, 99.8% 15.82%, 99.85% 17.18%, 99.89% 18.63%, 99.91% 20.16%, 99.94% 21.79%, 99.96% 23.52%, 99.97% 25.34%, 99.98% 27.27%, 99.99% 29.31%, 99.99% 31.46%, 100% 33.73%, 100% 36.11%, 100% 38.62%, 100% 41.26%, 100% 44.04%, 100% 46.95%, 100% 50%, 100% 50%, 100% 53.05%, 100% 55.96%, 100% 58.74%, 100% 61.38%, 100% 63.89%, 100% 66.27%, 99.99% 68.54%, 99.99% 70.69%, 99.98% 72.73%, 99.97% 74.66%, 99.96% 76.48%, 99.94% 78.21%, 99.91% 79.84%, 99.89% 81.37%, 99.85% 82.82%, 99.8% 84.18%, 99.75% 85.46%, 99.69% 86.66%, 99.61% 87.78%, 99.52% 88.83%, 99.42% 89.81%, 99.3% 90.73%, 99.17% 91.58%, 99.01% 92.37%, 98.84% 93.11%, 98.64% 93.79%, 98.42% 94.41%, 98.17% 94.99%, 97.89% 95.53%, 97.59% 96.02%, 97.25% 96.47%, 96.88% 96.88%, 96.47% 97.25%, 96.02% 97.59%, 95.53% 97.89%, 94.99% 98.17%, 94.41% 98.42%, 93.79% 98.64%, 93.11% 98.84%, 92.37% 99.01%, 91.58% 99.17%, 90.73% 99.3%, 89.81% 99.42%, 88.83% 99.52%, 87.78% 99.61%, 86.66% 99.69%, 85.46% 99.75%, 84.18% 99.8%, 82.82% 99.85%, 81.37% 99.89%, 79.84% 99.91%, 78.21% 99.94%, 76.48% 99.96%, 74.66% 99.97%, 72.73% 99.98%, 70.69% 99.99%, 68.54% 99.99%, 66.27% 100%, 63.89% 100%, 61.38% 100%, 58.74% 100%, 55.96% 100%, 53.05% 100%, 50% 100%, 50% 100%, 46.95% 100%, 44.04% 100%, 41.26% 100%, 38.62% 100%, 36.11% 100%, 33.73% 100%, 31.46% 99.99%, 29.31% 99.99%, 27.27% 99.98%, 25.34% 99.97%, 23.52% 99.96%, 21.79% 99.94%, 20.16% 99.91%, 18.63% 99.89%, 17.18% 99.85%, 15.82% 99.8%, 14.54% 99.75%, 13.34% 99.69%, 12.22% 99.61%, 11.17% 99.52%, 10.19% 99.42%, 9.27% 99.3%, 8.42% 99.17%, 7.63% 99.01%, 6.89% 98.84%, 6.21% 98.64%, 5.59% 98.42%, 5.01% 98.17%, 4.47% 97.89%, 3.98% 97.59%, 3.53% 97.25%, 3.13% 96.88%, 2.75% 96.47%, 2.41% 96.02%, 2.11% 95.53%, 1.83% 94.99%, 1.58% 94.41%, 1.36% 93.79%, 1.16% 93.11%, 0.99% 92.37%, 0.83% 91.58%, 0.7% 90.73%, 0.58% 89.81%, 0.48% 88.83%, 0.39% 87.78%, 0.31% 86.66%, 0.25% 85.46%, 0.2% 84.18%, 0.15% 82.82%, 0.11% 81.37%, 0.09% 79.84%, 0.06% 78.21%, 0.04% 76.48%, 0.03% 74.66%, 0.02% 72.73%, 0.01% 70.69%, 0.01% 68.54%, 0% 66.27%, 0% 63.89%, 0% 61.38%, 0% 58.74%, 0% 55.96%, 0% 53.05%, 0% 50%, 0% 50%, 0% 46.95%, 0% 44.04%, 0% 41.26%, 0% 38.62%, 0% 36.11%, 0% 33.73%, 0.01% 31.46%, 0.01% 29.31%, 0.02% 27.27%, 0.03% 25.34%, 0.04% 23.52%, 0.06% 21.79%, 0.09% 20.16%, 0.11% 18.63%, 0.15% 17.18%, 0.2% 15.82%, 0.25% 14.54%, 0.31% 13.34%, 0.39% 12.22%, 0.48% 11.17%, 0.58% 10.19%, 0.7% 9.27%, 0.83% 8.42%, 0.99% 7.63%, 1.16% 6.89%, 1.36% 6.21%, 1.58% 5.59%, 1.83% 5.01%, 2.11% 4.47%, 2.41% 3.98%, 2.75% 3.53%, 3.13% 3.13%, 3.53% 2.75%, 3.98% 2.41%, 4.47% 2.11%, 5.01% 1.83%, 5.59% 1.58%, 6.21% 1.36%, 6.89% 1.16%, 7.63% 0.99%, 8.42% 0.83%, 9.27% 0.7%, 10.19% 0.58%, 11.17% 0.48%, 12.22% 0.39%, 13.34% 0.31%, 14.54% 0.25%, 15.82% 0.2%, 17.18% 0.15%, 18.63% 0.11%, 20.16% 0.09%, 21.79% 0.06%, 23.52% 0.04%, 25.34% 0.03%, 27.27% 0.02%, 29.31% 0.01%, 31.46% 0.01%, 33.73% 0%, 36.11% 0%, 38.62% 0%, 41.26% 0%, 44.04% 0%, 46.95% 0%, 50% 0%)';
57
+
52
58
  let {
53
59
  size = '100%',
54
60
  toggleAnimationWithShift = false,
55
61
  ripplesOptions: ripplesOptionsProp = {},
56
62
  boundingBox = 'default',
63
+ squircle = false,
57
64
  class: className = '',
58
65
  onClick = undefined,
59
66
  ...restProps
60
67
  }: Props = $props();
61
68
 
69
+ // Ligature positioning: original (square) vs squircle-adjusted base values
70
+ // Square: original positioning where ligature inner corners touch the square corners
71
+ const LIG_ORIG_W = 440;
72
+ const LIG_ORIG_H = 666;
73
+ const LIG_ORIG_L = 133.5;
74
+ const LIG_ORIG_T = -65.75;
75
+ const BLUR_PAD_ORIG = 50; // shadow extends 50px beyond ligature on each side
76
+
77
+ // Squircle: scaled to align inner corners with squircle boundary
78
+ // (base 94.46% * 1.023 scale, offset x=-6.5, y=7)
79
+ const LIG_SQRC_W = 425.2;
80
+ const LIG_SQRC_H = 643.6;
81
+ const LIG_SQRC_L = 129.5;
82
+ const LIG_SQRC_T = -47.6;
83
+ const BLUR_PAD_SQRC = 48.3;
84
+
85
+ // Select positioning values depending on squircle mode
86
+ let ligW = $derived(squircle ? LIG_SQRC_W : LIG_ORIG_W);
87
+ let ligH = $derived(squircle ? LIG_SQRC_H : LIG_ORIG_H);
88
+ let ligL = $derived(squircle ? LIG_SQRC_L : LIG_ORIG_L);
89
+ let ligT = $derived(squircle ? LIG_SQRC_T : LIG_ORIG_T);
90
+
91
+ // Shadow tracks ligature center + blur padding
92
+ let blurPad = $derived(squircle ? BLUR_PAD_SQRC : BLUR_PAD_ORIG);
93
+ let shadW = $derived(ligW + blurPad * 2);
94
+ let shadH = $derived(ligH + blurPad * 2);
95
+ let shadL = $derived(ligL - blurPad);
96
+ let shadT = $derived(ligT - blurPad);
97
+
62
98
  // Use global animation state shared across ALL instances
63
99
  let animated = $state(globalAnimated);
64
100
 
@@ -74,6 +110,9 @@
74
110
  let animatedElements: Element[];
75
111
  let animate: (time: number) => void;
76
112
 
113
+ // State for dimension bindings
114
+ let ripplesWidth = $state(0);
115
+
77
116
  // Reactive effect to handle animation state changes from global store
78
117
  $effect(() => {
79
118
  if (animated) {
@@ -103,27 +142,71 @@
103
142
 
104
143
  const logoAnimation: Attachment = (element) => {
105
144
  animatedElements = [...element.children].filter((child) => child.classList.contains('animate'));
106
- const ripplesElement = element.getElementsByClassName('ripples')[0] as HTMLElement | undefined;
145
+ const ripplesElement = element.getElementsByTagName('d-ripple')[0] as HTMLElement | undefined;
146
+
147
+ // Scaling constants for ripple calculations
148
+ const SCALING_CONSTANTS = {
149
+ // Resolution scaling
150
+ MIN_RESOLUTION: 128,
151
+ MAX_RESOLUTION: 512,
152
+ RESOLUTION_FACTOR: 0.8,
153
+
154
+ // Drop radius scaling (linear interpolation from 62px:2px to 800px:23.36px, capped at 20px)
155
+ MIN_DROP_RADIUS: 2,
156
+ MAX_DROP_RADIUS: 20,
157
+ DROP_RADIUS_MIN_SIZE: 62,
158
+ DROP_RADIUS_MAX_SIZE: 800,
159
+ DROP_RADIUS_RANGE: 21.36,
160
+
161
+ // Wave propagation scaling (2.0 at 500px down to 0.2 at 125px)
162
+ MIN_WAVE_PROPAGATION: 0.2,
163
+ MAX_WAVE_PROPAGATION: 2.0,
164
+ WAVE_PROP_REFERENCE_SIZE: 500,
165
+ WAVE_PROP_MIN_SIZE: 125,
166
+ WAVE_PROP_FACTOR: 1.5
167
+ };
168
+
169
+ // Scaling utility functions
170
+ function calculateResolution(width: number): number {
171
+ return Math.min(
172
+ SCALING_CONSTANTS.MAX_RESOLUTION,
173
+ Math.max(SCALING_CONSTANTS.MIN_RESOLUTION, width * SCALING_CONSTANTS.RESOLUTION_FACTOR)
174
+ );
175
+ }
176
+
177
+ function calculateDropRadius(width: number): number {
178
+ return Math.min(
179
+ SCALING_CONSTANTS.MAX_DROP_RADIUS,
180
+ Math.max(
181
+ SCALING_CONSTANTS.MIN_DROP_RADIUS,
182
+ SCALING_CONSTANTS.MIN_DROP_RADIUS +
183
+ ((width - SCALING_CONSTANTS.DROP_RADIUS_MIN_SIZE) /
184
+ (SCALING_CONSTANTS.DROP_RADIUS_MAX_SIZE - SCALING_CONSTANTS.DROP_RADIUS_MIN_SIZE)) *
185
+ SCALING_CONSTANTS.DROP_RADIUS_RANGE
186
+ )
187
+ );
188
+ }
107
189
 
108
- // Higher resolution for smaller sizes to avoid blurriness
109
- // For small logos, use closer to 1:1 ratio, for large logos cap at 512
190
+ function calculateWavePropagation(width: number): number {
191
+ return Math.max(
192
+ SCALING_CONSTANTS.MIN_WAVE_PROPAGATION,
193
+ Math.min(
194
+ SCALING_CONSTANTS.MAX_WAVE_PROPAGATION,
195
+ SCALING_CONSTANTS.MAX_WAVE_PROPAGATION -
196
+ ((SCALING_CONSTANTS.WAVE_PROP_REFERENCE_SIZE - width) /
197
+ (SCALING_CONSTANTS.WAVE_PROP_REFERENCE_SIZE - SCALING_CONSTANTS.WAVE_PROP_MIN_SIZE)) *
198
+ SCALING_CONSTANTS.WAVE_PROP_FACTOR
199
+ )
200
+ );
201
+ }
202
+
203
+ // Calculate initial scaling values
110
204
  const elementSize = ripplesElement?.offsetWidth || 100;
111
- const resolution = !ripplesElement ? 512 : Math.min(512, Math.max(128, elementSize * 0.8));
112
-
113
- // Scale drop radius based on element size: 15px at 500px down to 2px at 62px
114
- // Linear interpolation from 62px:2px to 800px:23.36px, capped at 20px
115
- // Formula: 2 + ((elementSize - 62) / (800 - 62)) * (23.36 - 2)
116
- const scaledDropRadius = Math.min(
117
- 20,
118
- Math.max(2, 2 + ((elementSize - 62) / (800 - 62)) * 21.36)
119
- );
120
-
121
- // Scale wave propagation based on element size: 2.0 at 500px down to 0.2 at 125px
122
- // Linear interpolation for sizes 500px down to 125px, then clamp at 0.2 for smaller
123
- const scaledWavePropagation = Math.max(
124
- 0.2,
125
- Math.min(2.0, 2.0 - ((500 - elementSize) / (500 - 125)) * 1.5)
126
- );
205
+ const resolution = !ripplesElement
206
+ ? SCALING_CONSTANTS.MAX_RESOLUTION
207
+ : calculateResolution(elementSize);
208
+ const scaledDropRadius = calculateDropRadius(elementSize);
209
+ const scaledWavePropagation = calculateWavePropagation(elementSize);
127
210
 
128
211
  const DEFAULT_RIPPLES_OPTIONS = {
129
212
  resolution,
@@ -135,77 +218,77 @@
135
218
  };
136
219
  const rippleOptions = { ...DEFAULT_RIPPLES_OPTIONS, ...ripplesOptionsProp };
137
220
 
138
- // Set up ResizeObserver to handle component resizing
139
- let resizeObserver: ResizeObserver | null = null;
221
+ // Use Svelte dimension bindings for resize handling
140
222
  let lastWidth = ripplesElement?.offsetWidth;
141
- let lastHeight = ripplesElement?.offsetHeight;
142
223
  let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
224
+ let hasExecutedLeading = $state(false);
143
225
 
144
- if (ripplesElement && typeof ResizeObserver !== 'undefined') {
145
- resizeObserver = new ResizeObserver(() => {
146
- const currentWidth = ripplesElement.offsetWidth;
147
- const currentHeight = ripplesElement.offsetHeight;
226
+ // Extract ripples recreation logic for reuse
227
+ function recreateRipples(currentWidth: number) {
228
+ if (animated && ripplesElement) {
229
+ // Destroy old instance first
230
+ if (ripples) {
231
+ try {
232
+ ripples.destroy();
233
+ } catch (e) {
234
+ console.error('Error destroying ripples:', e);
235
+ }
236
+ ripples = null;
237
+ }
148
238
 
149
- // Only recreate if size actually changed significantly (more than 5px)
239
+ // Wait a frame before creating new instance to ensure cleanup
240
+ requestAnimationFrame(() => {
241
+ if (!ripples && animated && ripplesElement) {
242
+ // Calculate scaled values using utility functions
243
+ const newResolution = calculateResolution(currentWidth);
244
+ const newScaledDropRadius = calculateDropRadius(currentWidth);
245
+ const newScaledWavePropagation = calculateWavePropagation(currentWidth);
246
+ const newRippleOptions = {
247
+ ...rippleOptions,
248
+ resolution: newResolution,
249
+ dropRadius: newScaledDropRadius,
250
+ wavePropagation: newScaledWavePropagation
251
+ };
252
+
253
+ try {
254
+ ripples = new Ripples(ripplesElement, newRippleOptions);
255
+ } catch (e) {
256
+ console.error('Error creating ripples:', e);
257
+ }
258
+ }
259
+ });
260
+ }
261
+ }
262
+
263
+ // Reactive effect to handle dimension changes with leading + trailing edge debouncing
264
+ $effect(() => {
265
+ if (ripplesWidth && animated && ripplesElement) {
266
+ const currentWidth = ripplesWidth;
267
+
268
+ // Only recreate if width actually changed significantly (more than 5px)
150
269
  const widthChanged = Math.abs(currentWidth - (lastWidth || 0)) > 5;
151
- const heightChanged = Math.abs(currentHeight - (lastHeight || 0)) > 5;
152
270
 
153
- if (widthChanged || heightChanged) {
271
+ if (widthChanged) {
154
272
  lastWidth = currentWidth;
155
- lastHeight = currentHeight;
156
273
 
157
- // Debounce the recreation to avoid too many WebGL contexts
274
+ // LEADING EDGE: Execute immediately on first change in sequence
275
+ if (!hasExecutedLeading) {
276
+ hasExecutedLeading = true;
277
+ recreateRipples(currentWidth);
278
+ }
279
+
280
+ // TRAILING EDGE: Debounced execution to handle rapid changes
158
281
  if (resizeTimeout) {
159
282
  clearTimeout(resizeTimeout);
160
283
  }
161
284
 
162
285
  resizeTimeout = setTimeout(() => {
163
- if (animated && ripplesElement) {
164
- // Destroy old instance first
165
- if (ripples) {
166
- try {
167
- ripples.destroy();
168
- } catch (e) {
169
- console.error('Error destroying ripples:', e);
170
- }
171
- ripples = null;
172
- }
173
-
174
- // Wait a frame before creating new instance to ensure cleanup
175
- requestAnimationFrame(() => {
176
- if (!ripples && animated && ripplesElement) {
177
- // Higher resolution for smaller sizes to avoid blurriness
178
- const newResolution = Math.min(512, Math.max(128, currentWidth * 0.8));
179
- // Scale drop radius and wave propagation for new size
180
- // Linear interpolation from 62px:2px to 800px:23.36px, capped at 20px
181
- const newScaledDropRadius = Math.min(
182
- 20,
183
- Math.max(2, 2 + ((currentWidth - 62) / (800 - 62)) * 21.36)
184
- );
185
- const newScaledWavePropagation = Math.max(
186
- 0.2,
187
- Math.min(2.0, 2.0 - ((500 - currentWidth) / (500 - 125)) * 1.5)
188
- );
189
- const newRippleOptions = {
190
- ...rippleOptions,
191
- resolution: newResolution,
192
- dropRadius: newScaledDropRadius,
193
- wavePropagation: newScaledWavePropagation
194
- };
195
-
196
- try {
197
- ripples = new Ripples(ripplesElement, newRippleOptions);
198
- } catch (e) {
199
- console.error('Error creating ripples:', e);
200
- }
201
- }
202
- });
203
- }
286
+ recreateRipples(currentWidth);
287
+ hasExecutedLeading = false; // Reset for next change sequence
204
288
  }, 100); // 100ms debounce
205
289
  }
206
- });
207
- resizeObserver.observe(ripplesElement);
208
- }
290
+ }
291
+ });
209
292
 
210
293
  let angle = $state(0);
211
294
  let lastDropTime = $state(0);
@@ -236,10 +319,7 @@
236
319
  const y = Math.random() * ripplesElement.offsetHeight;
237
320
  // Use scaled drop radius for automatic drops
238
321
  const currentSize = ripplesElement.offsetWidth;
239
- const autoDropRadius = Math.min(
240
- 20,
241
- Math.max(2, 2 + ((currentSize - 62) / (800 - 62)) * 21.36)
242
- );
322
+ const autoDropRadius = calculateDropRadius(currentSize);
243
323
  // Scale strength based on size - ensure minimum visibility for small sizes
244
324
  const sizeFactor = Math.max(0.7, Math.min(1, currentSize / 200)); // Never below 70%
245
325
  const baseStrength = 0.1 + Math.random() * 0.04;
@@ -263,16 +343,14 @@
263
343
  // Now we need to scale them for each element's actual size
264
344
  for (const el of animatedElements) {
265
345
  if (el.classList.contains('shadow')) {
266
- // Shadow is 540x766, need to scale movement to match original animation
267
- // Original: 4% of 800px = 32px. For shadow: 32px / 540px = 5.93%
268
- const scaleX = 800 / 540;
269
- const scaleY = 800 / 766;
346
+ // Shadow is 510.1x723.6 (scaled 94.46% for squircle)
347
+ const scaleX = 800 / 510.1;
348
+ const scaleY = 800 / 723.6;
270
349
  (el as HTMLElement).style.transform = `translate(${dx * scaleX}%, ${dy * scaleY}%)`;
271
350
  } else if (el.classList.contains('ligature')) {
272
- // Ligature is 440x666, need to scale movement to match original animation
273
- // Original: 4% of 800px = 32px. For ligature: 32px / 440px = 7.27%
274
- const scaleX = 800 / 440;
275
- const scaleY = 800 / 666;
351
+ // Ligature is 415.6x629.1 (scaled 94.46% for squircle)
352
+ const scaleX = 800 / 415.6;
353
+ const scaleY = 800 / 629.1;
276
354
  (el as HTMLElement).style.transform = `translate(${dx * scaleX}%, ${dy * scaleY}%)`;
277
355
  } else {
278
356
  // Default for any other animated elements
@@ -302,9 +380,7 @@
302
380
  console.error('Error destroying ripples on cleanup:', e);
303
381
  }
304
382
  }
305
- if (resizeObserver) {
306
- resizeObserver.disconnect();
307
- }
383
+ hasExecutedLeading = false; // Reset leading edge state
308
384
  };
309
385
  };
310
386
 
@@ -330,20 +406,38 @@
330
406
  }
331
407
  </script>
332
408
 
333
- <logo-container style:--size={size} class="{boundingBox} {className}" role="none" {...restProps}>
409
+ <logo-container
410
+ style:--size={size}
411
+ class="{boundingBox} {className}"
412
+ class:squircle
413
+ role="none"
414
+ {...restProps}
415
+ >
334
416
  <grid-logo {@attach logoAnimation}>
335
- <img class="animate shadow" alt="" src={logoShadow} />
336
- <img class="glow" alt="" src={logoGlow} />
337
- <div
338
- class="ripples square"
417
+ <img
418
+ class="animate shadow"
419
+ alt=""
420
+ src={logoShadow}
421
+ style="width:calc(100% * {shadW} / 532);height:calc(100% * {shadH} / 532);left:calc(100% * {shadL} / 532);top:calc(100% * {shadT} / 532)"
422
+ />
423
+ <img class="glow" alt="" src={squircle ? logoGlowSquircle : logoGlow} />
424
+ <d-ripple
425
+ class="square"
339
426
  style:background-image={`url("${logoSquare}")`}
427
+ style:clip-path={squircle ? SQUIRCLE_CLIP : 'none'}
428
+ bind:offsetWidth={ripplesWidth}
340
429
  onclick={handleClick}
341
430
  onkeydown={handleClick}
342
431
  role="button"
343
432
  tabindex="0"
344
433
  aria-label="Toggle logo animation"
345
- ></div>
346
- <img class="animate ligature" alt="" src={logoLigature} />
434
+ ></d-ripple>
435
+ <img
436
+ class="animate ligature"
437
+ alt=""
438
+ src={logoLigature}
439
+ style="width:calc(100% * {ligW} / 532);height:calc(100% * {ligH} / 532);left:calc(100% * {ligL} / 532);top:calc(100% * {ligT} / 532)"
440
+ />
347
441
  </grid-logo>
348
442
  </logo-container>
349
443
 
@@ -359,26 +453,55 @@
359
453
  -moz-user-select: none;
360
454
  -ms-user-select: none;
361
455
  overflow: visible;
362
- }
363
456
 
364
- /* Square mode - square container */
365
- logo-container.square {
366
- aspect-ratio: 1;
367
- }
457
+ /* Cropped mode - match ellipse aspect ratio: 723.08875/812.58868 ≈ 0.8906 */
458
+ &.cropped {
459
+ aspect-ratio: 0.8906;
460
+ }
368
461
 
369
- /* Default mode - square container */
370
- logo-container.default {
371
- aspect-ratio: 1;
372
- }
462
+ /* Square bounding box mode - grid fills the container */
463
+ &.square grid-logo {
464
+ width: 100%;
465
+ left: 0;
466
+ top: 0;
467
+ }
373
468
 
374
- /* Encircled mode - square container */
375
- logo-container.encircled {
376
- aspect-ratio: 1;
377
- }
469
+ /* Default bounding box mode - grid is scaled down to leave some padding */
470
+ &.default grid-logo {
471
+ /* Grid is 1/1.2519 = 79.88% of container to account for padding */
472
+ width: calc(100% / 1.2519);
473
+ /* Center within container */
474
+ left: calc((100% - 100% / 1.2519) / 2);
475
+ top: calc((100% - 100% / 1.2519) / 2);
476
+ }
378
477
 
379
- /* Cropped mode - match ellipse aspect ratio: 723.08875/812.58868 0.8906 */
380
- logo-container.cropped {
381
- aspect-ratio: 0.8906;
478
+ /* Encircled bounding box mode - grid is scaled down more to leave full padding */
479
+ &.encircled grid-logo {
480
+ /* Grid is 1/1.5037 = 66.5% of container (532/800) */
481
+ width: calc(100% / 1.5037);
482
+ /* Center within container */
483
+ left: calc((100% - 100% / 1.5037) / 2);
484
+ top: calc((100% - 100% / 1.5037) / 2);
485
+ }
486
+
487
+ /* Squircle + encircled: scale up 1.04x to compensate for smaller ligature */
488
+ &.encircled.squircle grid-logo {
489
+ width: calc(100% / 1.5037 * 1.04);
490
+ left: calc((100% - 100% / 1.5037 * 1.04) / 2);
491
+ top: calc((100% - 100% / 1.5037 * 1.04) / 2);
492
+ }
493
+
494
+ /* Cropped bounding box mode - grid scaled and positioned to match reference SVG */
495
+ &.cropped grid-logo {
496
+ /* Square is 1131.371px in a 1447px wide container = 78.2% */
497
+ width: 78.2%;
498
+ /* Position calculations from SVG transforms */
499
+ /* Final square position after transforms: x=122.18, y=247.73 */
500
+ /* Left offset: 122.18/1447 = 8.44% */
501
+ left: 8.44%;
502
+ /* Top offset: 247.73/1626.9 = 15.23% */
503
+ top: 15.23%;
504
+ }
382
505
  }
383
506
 
384
507
  /* Grid that holds all the logo elements */
@@ -390,58 +513,23 @@
390
513
  aspect-ratio: 1;
391
514
 
392
515
  img {
393
- /* Prevent PicoCSS styles. */
394
- max-width: unset !important;
516
+ max-width: unset;
395
517
  }
396
- }
397
-
398
- /* Square bounding box mode - grid fills the container */
399
- logo-container.square grid-logo {
400
- width: 100%;
401
- left: 0;
402
- top: 0;
403
- }
404
-
405
- /* Default bounding box mode - grid is scaled down to leave some padding */
406
- logo-container.default grid-logo {
407
- /* Grid is 1/1.2519 = 79.88% of container to account for padding */
408
- width: calc(100% / 1.2519);
409
- /* Center within container */
410
- left: calc((100% - 100% / 1.2519) / 2);
411
- top: calc((100% - 100% / 1.2519) / 2);
412
- }
413
-
414
- /* Encircled bounding box mode - grid is scaled down more to leave full padding */
415
- logo-container.encircled grid-logo {
416
- /* Grid is 1/1.5037 = 66.5% of container (532/800) */
417
- width: calc(100% / 1.5037);
418
- /* Center within container */
419
- left: calc((100% - 100% / 1.5037) / 2);
420
- top: calc((100% - 100% / 1.5037) / 2);
421
- }
422
-
423
- /* Cropped bounding box mode - grid scaled and positioned to match reference SVG */
424
- logo-container.cropped grid-logo {
425
- /* Square is 1131.371px in a 1447px wide container = 78.2% */
426
- width: 78.2%;
427
- /* Position calculations from SVG transforms */
428
- /* Final square position after transforms: x=122.18, y=247.73 */
429
- /* Left offset: 122.18/1447 = 8.44% */
430
- left: 8.44%;
431
- /* Top offset: 247.73/1626.9 = 15.23% */
432
- top: 15.23%;
433
- }
434
518
 
435
- /* Individual logo elements positioned relative to the square */
436
- grid-logo > * {
437
- position: absolute;
438
- grid-column: 1 / 2;
439
- grid-row: 1 / 2;
440
- will-change: auto;
519
+ /* Individual logo elements positioned relative to the square */
520
+ > * {
521
+ position: absolute;
522
+ grid-column: 1 / 2;
523
+ grid-row: 1 / 2;
524
+ will-change: auto;
525
+ }
441
526
  }
442
527
 
443
528
  /* Square image - base element at 532x532 */
444
- .ripples.square {
529
+ /* Uses <d-ripple> custom element to avoid classless CSS library interference */
530
+ /* Still needs resets for [role="button"] targeting by classless CSS libraries */
531
+ d-ripple.square {
532
+ display: block;
445
533
  width: 100%;
446
534
  aspect-ratio: 1;
447
535
  background-size: contain;
@@ -451,16 +539,23 @@
451
539
  cursor: pointer;
452
540
  outline: none;
453
541
 
454
- /* Prevent PicoCSS [role=button] styles. */
455
- padding: 0 !important;
456
- border: none !important;
457
- border-radius: 0 !important;
458
- outline: none !important;
459
- box-shadow: none !important;
460
- }
461
-
462
- .ripples.square:focus {
463
- outline: none;
542
+ /* Reset [role="button"] styles from classless CSS libraries */
543
+ margin: 0;
544
+ padding: 0;
545
+ border: none;
546
+ border-radius: 0;
547
+ box-shadow: none;
548
+ background-color: transparent;
549
+ color: inherit;
550
+ font: inherit;
551
+ text-align: inherit;
552
+ text-decoration: none;
553
+ line-height: inherit;
554
+
555
+ &:focus {
556
+ outline: none;
557
+ box-shadow: none;
558
+ }
464
559
  }
465
560
 
466
561
  /* Glow - 647x647, centered on square */
@@ -475,28 +570,14 @@
475
570
  pointer-events: none;
476
571
  }
477
572
 
478
- /* Shadow - 540x766, positioned to outline the ligature */
573
+ /* Shadow - dimensions/position set via inline style for dynamic ligature adjustment */
479
574
  .shadow {
480
- width: calc(100% * 540 / 532);
481
- height: calc(100% * 766 / 532);
482
- /* Shadow should be centered on ligature */
483
- /* Ligature is 440x666, shadow is 540x766 */
484
- /* Shadow is (540-440)/2 = 50px wider on each side, (766-666)/2 = 50px taller on each side */
485
- left: calc(100% * (133 - 50) / 532); /* Center shadow on ligature */
486
- top: calc(100% * (-66 - 50) / 532);
487
575
  z-index: 0;
488
- /* Shadow should not capture clicks */
489
576
  pointer-events: none;
490
577
  }
491
578
 
492
- /* Ligature - 440x666, positioned based on SVG */
579
+ /* Ligature - dimensions/position set via inline style */
493
580
  .ligature {
494
- width: calc(100% * 440 / 532);
495
- height: calc(100% * 666 / 532);
496
- /* Ligature extends to the right and below the square */
497
- /* Looking at the SVG path, the ligature starts at top-left and extends right and down */
498
- left: calc(100% * 133.5 / 532);
499
- top: calc(100% * -65.75 / 532);
500
581
  z-index: 3;
501
582
  pointer-events: none;
502
583
  }
@@ -7,6 +7,7 @@ interface Props {
7
7
  toggleAnimationWithShift?: boolean;
8
8
  ripplesOptions?: RipplesOptions;
9
9
  boundingBox?: 'square' | 'default' | 'cropped' | 'encircled';
10
+ squircle?: boolean;
10
11
  class?: string;
11
12
  onClick?: (event: MouseEvent | KeyboardEvent) => void;
12
13
  [key: string]: unknown;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * OKLCH-based color transforms for icon color modes.
3
+ *
4
+ * Uses culori for perceptually uniform color manipulation.
5
+ * All transforms operate on individual CSS color strings.
6
+ */
7
+ import type { IconColorMode } from './types.js';
8
+ /**
9
+ * Transform a single CSS color to grayscale using OKLCH.
10
+ * Sets chroma to 0, preserving perceptual lightness.
11
+ * @param lightness Optional lightness multiplier (0-200), default 100 = no change
12
+ */
13
+ export declare function toGrayscale(color: string, lightness?: number): string;
14
+ /**
15
+ * Transform a single CSS color to grayscale, then tint toward a target color.
16
+ *
17
+ * The tint color's hue and chroma influence the result, while the original
18
+ * color's lightness is preserved. This unifies a palette while keeping contrast.
19
+ * @param lightness Optional lightness multiplier (0-200), default 100 = no change
20
+ */
21
+ export declare function toGrayscaleTint(color: string, tintColor: string, lightness?: number): string;
22
+ /**
23
+ * Remap a single CSS color to a target hue, optionally overriding saturation.
24
+ *
25
+ * Preserves the original lightness. If saturation is provided, it's used as
26
+ * a percentage of the maximum chroma; otherwise, the original chroma is preserved.
27
+ */
28
+ export declare function remapHue(color: string, targetHue: number, targetSaturation?: number): string;
29
+ /**
30
+ * Apply a full IconColorMode transformation to SVG content.
31
+ *
32
+ * Handles all modes: 'auto', 'original', 'monochrome', 'grayscale',
33
+ * 'grayscale-tint', and { hue, saturation? }.
34
+ *
35
+ * Color transforms operate on fill/stroke attribute values and style properties,
36
+ * replacing each color with its transformed equivalent.
37
+ *
38
+ * @param grayscaleLightness Lightness multiplier for grayscale modes (0-200), default 100
39
+ */
40
+ export declare function applyColorMode(svgContent: string, isMonochrome: boolean, colorMode: IconColorMode, iconColor: string, grayscaleLightness?: number): string;