@leftium/logo 0.0.1 → 0.1.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.
@@ -1,5 +1,6 @@
1
1
  <script module>
2
2
  import { dev } from '$app/environment';
3
+ import { SvelteSet } from 'svelte/reactivity';
3
4
 
4
5
  // Simple shared variable - no store needed!
5
6
  let globalAnimated = !dev;
@@ -16,7 +17,7 @@
16
17
  }
17
18
 
18
19
  // Keep track of all instances for updates
19
- let instances = new Set<() => void>();
20
+ let instances = new SvelteSet<() => void>();
20
21
 
21
22
  function updateAllInstances() {
22
23
  instances.forEach((update) => update());
@@ -34,6 +35,7 @@
34
35
  import { Ripples, type RipplesOptions } from './webgl-ripples/webgl-ripples.js';
35
36
 
36
37
  import logoGlow from './assets/logo-parts/glow.svg';
38
+ import logoGlowSquircle from './assets/logo-parts/glow-squircle.svg';
37
39
  import logoLigature from './assets/logo-parts/ligature.svg';
38
40
  import logoShadow from './assets/logo-parts/shadow.svg';
39
41
  import logoSquare from './assets/logo-parts/square.svg?inline';
@@ -43,21 +45,56 @@
43
45
  toggleAnimationWithShift?: boolean;
44
46
  ripplesOptions?: RipplesOptions;
45
47
  boundingBox?: 'square' | 'default' | 'cropped' | 'encircled';
48
+ squircle?: boolean;
46
49
  class?: string;
47
50
  onClick?: (event: MouseEvent | KeyboardEvent) => void;
48
51
  [key: string]: unknown; // Allow any additional props
49
52
  }
50
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
+
51
58
  let {
52
59
  size = '100%',
53
60
  toggleAnimationWithShift = false,
54
61
  ripplesOptions: ripplesOptionsProp = {},
55
62
  boundingBox = 'default',
63
+ squircle = false,
56
64
  class: className = '',
57
65
  onClick = undefined,
58
66
  ...restProps
59
67
  }: Props = $props();
60
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
+
61
98
  // Use global animation state shared across ALL instances
62
99
  let animated = $state(globalAnimated);
63
100
 
@@ -73,6 +110,9 @@
73
110
  let animatedElements: Element[];
74
111
  let animate: (time: number) => void;
75
112
 
113
+ // State for dimension bindings
114
+ let ripplesWidth = $state(0);
115
+
76
116
  // Reactive effect to handle animation state changes from global store
77
117
  $effect(() => {
78
118
  if (animated) {
@@ -102,27 +142,71 @@
102
142
 
103
143
  const logoAnimation: Attachment = (element) => {
104
144
  animatedElements = [...element.children].filter((child) => child.classList.contains('animate'));
105
- 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
+ }
106
189
 
107
- // Higher resolution for smaller sizes to avoid blurriness
108
- // 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
109
204
  const elementSize = ripplesElement?.offsetWidth || 100;
110
- const resolution = !ripplesElement ? 512 : Math.min(512, Math.max(128, elementSize * 0.8));
111
-
112
- // Scale drop radius based on element size: 15px at 500px down to 2px at 62px
113
- // Linear interpolation from 62px:2px to 800px:23.36px, capped at 20px
114
- // Formula: 2 + ((elementSize - 62) / (800 - 62)) * (23.36 - 2)
115
- const scaledDropRadius = Math.min(
116
- 20,
117
- Math.max(2, 2 + ((elementSize - 62) / (800 - 62)) * 21.36)
118
- );
119
-
120
- // Scale wave propagation based on element size: 2.0 at 500px down to 0.2 at 125px
121
- // Linear interpolation for sizes 500px down to 125px, then clamp at 0.2 for smaller
122
- const scaledWavePropagation = Math.max(
123
- 0.2,
124
- Math.min(2.0, 2.0 - ((500 - elementSize) / (500 - 125)) * 1.5)
125
- );
205
+ const resolution = !ripplesElement
206
+ ? SCALING_CONSTANTS.MAX_RESOLUTION
207
+ : calculateResolution(elementSize);
208
+ const scaledDropRadius = calculateDropRadius(elementSize);
209
+ const scaledWavePropagation = calculateWavePropagation(elementSize);
126
210
 
127
211
  const DEFAULT_RIPPLES_OPTIONS = {
128
212
  resolution,
@@ -134,77 +218,77 @@
134
218
  };
135
219
  const rippleOptions = { ...DEFAULT_RIPPLES_OPTIONS, ...ripplesOptionsProp };
136
220
 
137
- // Set up ResizeObserver to handle component resizing
138
- let resizeObserver: ResizeObserver | null = null;
221
+ // Use Svelte dimension bindings for resize handling
139
222
  let lastWidth = ripplesElement?.offsetWidth;
140
- let lastHeight = ripplesElement?.offsetHeight;
141
223
  let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
224
+ let hasExecutedLeading = $state(false);
142
225
 
143
- if (ripplesElement && typeof ResizeObserver !== 'undefined') {
144
- resizeObserver = new ResizeObserver(() => {
145
- const currentWidth = ripplesElement.offsetWidth;
146
- 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
+ }
147
238
 
148
- // 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)
149
269
  const widthChanged = Math.abs(currentWidth - (lastWidth || 0)) > 5;
150
- const heightChanged = Math.abs(currentHeight - (lastHeight || 0)) > 5;
151
270
 
152
- if (widthChanged || heightChanged) {
271
+ if (widthChanged) {
153
272
  lastWidth = currentWidth;
154
- lastHeight = currentHeight;
155
273
 
156
- // 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
157
281
  if (resizeTimeout) {
158
282
  clearTimeout(resizeTimeout);
159
283
  }
160
284
 
161
285
  resizeTimeout = setTimeout(() => {
162
- if (animated && ripplesElement) {
163
- // Destroy old instance first
164
- if (ripples) {
165
- try {
166
- ripples.destroy();
167
- } catch (e) {
168
- console.error('Error destroying ripples:', e);
169
- }
170
- ripples = null;
171
- }
172
-
173
- // Wait a frame before creating new instance to ensure cleanup
174
- requestAnimationFrame(() => {
175
- if (!ripples && animated && ripplesElement) {
176
- // Higher resolution for smaller sizes to avoid blurriness
177
- const newResolution = Math.min(512, Math.max(128, currentWidth * 0.8));
178
- // Scale drop radius and wave propagation for new size
179
- // Linear interpolation from 62px:2px to 800px:23.36px, capped at 20px
180
- const newScaledDropRadius = Math.min(
181
- 20,
182
- Math.max(2, 2 + ((currentWidth - 62) / (800 - 62)) * 21.36)
183
- );
184
- const newScaledWavePropagation = Math.max(
185
- 0.2,
186
- Math.min(2.0, 2.0 - ((500 - currentWidth) / (500 - 125)) * 1.5)
187
- );
188
- const newRippleOptions = {
189
- ...rippleOptions,
190
- resolution: newResolution,
191
- dropRadius: newScaledDropRadius,
192
- wavePropagation: newScaledWavePropagation
193
- };
194
-
195
- try {
196
- ripples = new Ripples(ripplesElement, newRippleOptions);
197
- } catch (e) {
198
- console.error('Error creating ripples:', e);
199
- }
200
- }
201
- });
202
- }
286
+ recreateRipples(currentWidth);
287
+ hasExecutedLeading = false; // Reset for next change sequence
203
288
  }, 100); // 100ms debounce
204
289
  }
205
- });
206
- resizeObserver.observe(ripplesElement);
207
- }
290
+ }
291
+ });
208
292
 
209
293
  let angle = $state(0);
210
294
  let lastDropTime = $state(0);
@@ -235,10 +319,7 @@
235
319
  const y = Math.random() * ripplesElement.offsetHeight;
236
320
  // Use scaled drop radius for automatic drops
237
321
  const currentSize = ripplesElement.offsetWidth;
238
- const autoDropRadius = Math.min(
239
- 20,
240
- Math.max(2, 2 + ((currentSize - 62) / (800 - 62)) * 21.36)
241
- );
322
+ const autoDropRadius = calculateDropRadius(currentSize);
242
323
  // Scale strength based on size - ensure minimum visibility for small sizes
243
324
  const sizeFactor = Math.max(0.7, Math.min(1, currentSize / 200)); // Never below 70%
244
325
  const baseStrength = 0.1 + Math.random() * 0.04;
@@ -262,16 +343,14 @@
262
343
  // Now we need to scale them for each element's actual size
263
344
  for (const el of animatedElements) {
264
345
  if (el.classList.contains('shadow')) {
265
- // Shadow is 540x766, need to scale movement to match original animation
266
- // Original: 4% of 800px = 32px. For shadow: 32px / 540px = 5.93%
267
- const scaleX = 800 / 540;
268
- 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;
269
349
  (el as HTMLElement).style.transform = `translate(${dx * scaleX}%, ${dy * scaleY}%)`;
270
350
  } else if (el.classList.contains('ligature')) {
271
- // Ligature is 440x666, need to scale movement to match original animation
272
- // Original: 4% of 800px = 32px. For ligature: 32px / 440px = 7.27%
273
- const scaleX = 800 / 440;
274
- 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;
275
354
  (el as HTMLElement).style.transform = `translate(${dx * scaleX}%, ${dy * scaleY}%)`;
276
355
  } else {
277
356
  // Default for any other animated elements
@@ -301,9 +380,7 @@
301
380
  console.error('Error destroying ripples on cleanup:', e);
302
381
  }
303
382
  }
304
- if (resizeObserver) {
305
- resizeObserver.disconnect();
306
- }
383
+ hasExecutedLeading = false; // Reset leading edge state
307
384
  };
308
385
  };
309
386
 
@@ -329,20 +406,38 @@
329
406
  }
330
407
  </script>
331
408
 
332
- <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
+ >
333
416
  <grid-logo {@attach logoAnimation}>
334
- <img class="animate shadow" alt="" src={logoShadow} />
335
- <img class="glow" alt="" src={logoGlow} />
336
- <div
337
- 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"
338
426
  style:background-image={`url("${logoSquare}")`}
427
+ style:clip-path={squircle ? SQUIRCLE_CLIP : 'none'}
428
+ bind:offsetWidth={ripplesWidth}
339
429
  onclick={handleClick}
340
430
  onkeydown={handleClick}
341
431
  role="button"
342
432
  tabindex="0"
343
433
  aria-label="Toggle logo animation"
344
- ></div>
345
- <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
+ />
346
441
  </grid-logo>
347
442
  </logo-container>
348
443
 
@@ -358,26 +453,55 @@
358
453
  -moz-user-select: none;
359
454
  -ms-user-select: none;
360
455
  overflow: visible;
361
- }
362
456
 
363
- /* Square mode - square container */
364
- logo-container.square {
365
- aspect-ratio: 1;
366
- }
457
+ /* Cropped mode - match ellipse aspect ratio: 723.08875/812.58868 ≈ 0.8906 */
458
+ &.cropped {
459
+ aspect-ratio: 0.8906;
460
+ }
367
461
 
368
- /* Default mode - square container */
369
- logo-container.default {
370
- aspect-ratio: 1;
371
- }
462
+ /* Square bounding box mode - grid fills the container */
463
+ &.square grid-logo {
464
+ width: 100%;
465
+ left: 0;
466
+ top: 0;
467
+ }
372
468
 
373
- /* Encircled mode - square container */
374
- logo-container.encircled {
375
- aspect-ratio: 1;
376
- }
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
+ }
377
477
 
378
- /* Cropped mode - match ellipse aspect ratio: 723.08875/812.58868 0.8906 */
379
- logo-container.cropped {
380
- 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
+ }
381
505
  }
382
506
 
383
507
  /* Grid that holds all the logo elements */
@@ -389,58 +513,23 @@
389
513
  aspect-ratio: 1;
390
514
 
391
515
  img {
392
- /* Prevent PicoCSS styles. */
393
- max-width: unset !important;
516
+ max-width: unset;
394
517
  }
395
- }
396
-
397
- /* Square bounding box mode - grid fills the container */
398
- logo-container.square grid-logo {
399
- width: 100%;
400
- left: 0;
401
- top: 0;
402
- }
403
-
404
- /* Default bounding box mode - grid is scaled down to leave some padding */
405
- logo-container.default grid-logo {
406
- /* Grid is 1/1.2519 = 79.88% of container to account for padding */
407
- width: calc(100% / 1.2519);
408
- /* Center within container */
409
- left: calc((100% - 100% / 1.2519) / 2);
410
- top: calc((100% - 100% / 1.2519) / 2);
411
- }
412
-
413
- /* Encircled bounding box mode - grid is scaled down more to leave full padding */
414
- logo-container.encircled grid-logo {
415
- /* Grid is 1/1.5037 = 66.5% of container (532/800) */
416
- width: calc(100% / 1.5037);
417
- /* Center within container */
418
- left: calc((100% - 100% / 1.5037) / 2);
419
- top: calc((100% - 100% / 1.5037) / 2);
420
- }
421
-
422
- /* Cropped bounding box mode - grid scaled and positioned to match reference SVG */
423
- logo-container.cropped grid-logo {
424
- /* Square is 1131.371px in a 1447px wide container = 78.2% */
425
- width: 78.2%;
426
- /* Position calculations from SVG transforms */
427
- /* Final square position after transforms: x=122.18, y=247.73 */
428
- /* Left offset: 122.18/1447 = 8.44% */
429
- left: 8.44%;
430
- /* Top offset: 247.73/1626.9 = 15.23% */
431
- top: 15.23%;
432
- }
433
518
 
434
- /* Individual logo elements positioned relative to the square */
435
- grid-logo > * {
436
- position: absolute;
437
- grid-column: 1 / 2;
438
- grid-row: 1 / 2;
439
- 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
+ }
440
526
  }
441
527
 
442
528
  /* Square image - base element at 532x532 */
443
- .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;
444
533
  width: 100%;
445
534
  aspect-ratio: 1;
446
535
  background-size: contain;
@@ -450,16 +539,23 @@
450
539
  cursor: pointer;
451
540
  outline: none;
452
541
 
453
- /* Prevent PicoCSS [role=button] styles. */
454
- padding: 0 !important;
455
- border: none !important;
456
- border-radius: 0 !important;
457
- outline: none !important;
458
- box-shadow: none !important;
459
- }
460
-
461
- .ripples.square:focus {
462
- 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
+ }
463
559
  }
464
560
 
465
561
  /* Glow - 647x647, centered on square */
@@ -474,28 +570,14 @@
474
570
  pointer-events: none;
475
571
  }
476
572
 
477
- /* Shadow - 540x766, positioned to outline the ligature */
573
+ /* Shadow - dimensions/position set via inline style for dynamic ligature adjustment */
478
574
  .shadow {
479
- width: calc(100% * 540 / 532);
480
- height: calc(100% * 766 / 532);
481
- /* Shadow should be centered on ligature */
482
- /* Ligature is 440x666, shadow is 540x766 */
483
- /* Shadow is (540-440)/2 = 50px wider on each side, (766-666)/2 = 50px taller on each side */
484
- left: calc(100% * (133 - 50) / 532); /* Center shadow on ligature */
485
- top: calc(100% * (-66 - 50) / 532);
486
575
  z-index: 0;
487
- /* Shadow should not capture clicks */
488
576
  pointer-events: none;
489
577
  }
490
578
 
491
- /* Ligature - 440x666, positioned based on SVG */
579
+ /* Ligature - dimensions/position set via inline style */
492
580
  .ligature {
493
- width: calc(100% * 440 / 532);
494
- height: calc(100% * 666 / 532);
495
- /* Ligature extends to the right and below the square */
496
- /* Looking at the SVG path, the ligature starts at top-left and extends right and down */
497
- left: calc(100% * 133.5 / 532);
498
- top: calc(100% * -65.75 / 532);
499
581
  z-index: 3;
500
582
  pointer-events: none;
501
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,36 @@
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
+ */
12
+ export declare function toGrayscale(color: string): string;
13
+ /**
14
+ * Transform a single CSS color to grayscale, then tint toward a target color.
15
+ *
16
+ * The tint color's hue and chroma influence the result, while the original
17
+ * color's lightness is preserved. This unifies a palette while keeping contrast.
18
+ */
19
+ export declare function toGrayscaleTint(color: string, tintColor: string): string;
20
+ /**
21
+ * Remap a single CSS color to a target hue, optionally overriding saturation.
22
+ *
23
+ * Preserves the original lightness. If saturation is provided, it's used as
24
+ * a percentage of the maximum chroma; otherwise, the original chroma is preserved.
25
+ */
26
+ export declare function remapHue(color: string, targetHue: number, targetSaturation?: number): string;
27
+ /**
28
+ * Apply a full IconColorMode transformation to SVG content.
29
+ *
30
+ * Handles all modes: 'auto', 'original', 'monochrome', 'grayscale',
31
+ * 'grayscale-tint', and { hue, saturation? }.
32
+ *
33
+ * Color transforms operate on fill/stroke attribute values and style properties,
34
+ * replacing each color with its transformed equivalent.
35
+ */
36
+ export declare function applyColorMode(svgContent: string, isMonochrome: boolean, colorMode: IconColorMode, iconColor: string): string;