@shulya/liquid-glass 1.0.3 → 1.0.4
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/dist/core.cjs +127 -68
- package/dist/core.mjs +127 -68
- package/package.json +1 -1
package/dist/core.cjs
CHANGED
|
@@ -221,7 +221,7 @@ exports.buildOpticalMapURI = function buildOpticalMapURI(width, height, borderRa
|
|
|
221
221
|
*/
|
|
222
222
|
exports.renderFusedCluster = function renderFusedCluster(ctx, sourceCtx, elements, rawOptions = {}, dpr = 1) {
|
|
223
223
|
const options = normalizeOptions(rawOptions);
|
|
224
|
-
const fusionSpan = options.fusion ? (rawOptions.fusionDistance ??
|
|
224
|
+
const fusionSpan = options.fusion ? (rawOptions.fusionDistance ?? 24) * dpr : 0;
|
|
225
225
|
const rimPx = Math.max(1, options.rim * dpr);
|
|
226
226
|
const ior = options.ior;
|
|
227
227
|
const heightPx = options.height * dpr;
|
|
@@ -259,19 +259,26 @@ exports.renderFusedCluster = function renderFusedCluster(ctx, sourceCtx, element
|
|
|
259
259
|
const dstData = dstImg.data;
|
|
260
260
|
|
|
261
261
|
const distGrid = new Float32Array(boxW * boxH);
|
|
262
|
-
const prep = elements.map((b) =>
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
262
|
+
const prep = elements.map((b) => {
|
|
263
|
+
const halfW = b.w * 0.5 * dpr;
|
|
264
|
+
const halfH = b.h * 0.5 * dpr;
|
|
265
|
+
const rad = (b.borderRadius ?? 0) * dpr;
|
|
266
|
+
const r = Math.min(rad, halfW, halfH);
|
|
267
|
+
const isCircle = b.shape === 'circle' || (Math.abs(halfW - halfH) < 1.0 && Math.abs(r - halfW) < 1.0);
|
|
268
|
+
return {
|
|
269
|
+
cx: (b.x + b.w * 0.5) * dpr,
|
|
270
|
+
cy: (b.y + b.h * 0.5) * dpr,
|
|
271
|
+
halfW,
|
|
272
|
+
halfH,
|
|
273
|
+
r,
|
|
274
|
+
isCircle,
|
|
275
|
+
fusion: b.fusion !== undefined ? Boolean(b.fusion) : options.fusion,
|
|
276
|
+
tintR: b.tint ? b.tint[0] : 15,
|
|
277
|
+
tintG: b.tint ? b.tint[1] : 23,
|
|
278
|
+
tintB: b.tint ? b.tint[2] : 42,
|
|
279
|
+
tintA: b.tint ? b.tint[3] : 0.35
|
|
280
|
+
};
|
|
281
|
+
});
|
|
275
282
|
|
|
276
283
|
const primary = prep[0];
|
|
277
284
|
const tR = primary.tintR;
|
|
@@ -279,8 +286,10 @@ exports.renderFusedCluster = function renderFusedCluster(ctx, sourceCtx, element
|
|
|
279
286
|
const tB = primary.tintB;
|
|
280
287
|
const tA = primary.tintA;
|
|
281
288
|
const invTA = 1.0 - tA;
|
|
289
|
+
const span = fusionSpan;
|
|
290
|
+
const invSpan = span > 0.001 ? 1.0 / span : 0;
|
|
282
291
|
|
|
283
|
-
// Pass 1: Boundary evaluation across cluster bounding box
|
|
292
|
+
// Pass 1: Boundary evaluation across cluster bounding box (Inlined fast LUT math)
|
|
284
293
|
for (let y = 0; y < boxH; y++) {
|
|
285
294
|
const py = cMinY + y;
|
|
286
295
|
const row = y * boxW;
|
|
@@ -289,22 +298,36 @@ exports.renderFusedCluster = function renderFusedCluster(ctx, sourceCtx, element
|
|
|
289
298
|
let dCluster = Infinity;
|
|
290
299
|
for (let i = 0; i < prep.length; i++) {
|
|
291
300
|
const b = prep[i];
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
301
|
+
let dShape;
|
|
302
|
+
if (b.isCircle) {
|
|
303
|
+
dShape = Math.hypot(px - b.cx, py - b.cy) - b.halfW;
|
|
304
|
+
} else {
|
|
305
|
+
const qx = Math.abs(px - b.cx) - b.halfW + b.r;
|
|
306
|
+
const qy = Math.abs(py - b.cy) - b.halfH + b.r;
|
|
307
|
+
dShape = Math.min(Math.max(qx, qy), 0.0) + Math.hypot(Math.max(qx, 0.0), Math.max(qy, 0.0)) - b.r;
|
|
308
|
+
}
|
|
309
|
+
|
|
295
310
|
if (i === 0) {
|
|
296
311
|
dCluster = dShape;
|
|
297
|
-
} else if (b.fusion &&
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
312
|
+
} else if (b.fusion && span > 0.001) {
|
|
313
|
+
const diff = dCluster > dShape ? dCluster - dShape : dShape - dCluster;
|
|
314
|
+
if (diff < span) {
|
|
315
|
+
const pos = diff * invSpan * 255.0;
|
|
316
|
+
const pIdx = Math.min(254, pos | 0);
|
|
317
|
+
const wWeight = _T3[pIdx] + (_T3[pIdx + 1] - _T3[pIdx]) * (pos - pIdx);
|
|
318
|
+
dCluster = (dCluster < dShape ? dCluster : dShape) - wWeight * span;
|
|
319
|
+
} else if (dShape < dCluster) {
|
|
320
|
+
dCluster = dShape;
|
|
321
|
+
}
|
|
322
|
+
} else if (dShape < dCluster) {
|
|
323
|
+
dCluster = dShape;
|
|
301
324
|
}
|
|
302
325
|
}
|
|
303
326
|
distGrid[row + x] = dCluster;
|
|
304
327
|
}
|
|
305
328
|
}
|
|
306
329
|
|
|
307
|
-
// Pass 2:
|
|
330
|
+
// Pass 2: Fast vectorized lookup from distGrid + tabular chromatic shift (_T4)
|
|
308
331
|
for (let y = 1; y < boxH - 1; y++) {
|
|
309
332
|
const row = y * boxW;
|
|
310
333
|
for (let x = 1; x < boxW - 1; x++) {
|
|
@@ -325,8 +348,11 @@ exports.renderFusedCluster = function renderFusedCluster(ctx, sourceCtx, element
|
|
|
325
348
|
const innerDist = -d;
|
|
326
349
|
if (innerDist < rimPx) {
|
|
327
350
|
const u = innerDist / rimPx;
|
|
328
|
-
const
|
|
329
|
-
|
|
351
|
+
const posU = u * 255.0;
|
|
352
|
+
const uIdx = Math.min(254, posU | 0);
|
|
353
|
+
const uFrac = posU - uIdx;
|
|
354
|
+
const gain = _T0[uIdx] + (_T0[uIdx + 1] - _T0[uIdx]) * uFrac;
|
|
355
|
+
disp = _T4[uIdx] + (_T4[uIdx + 1] - _T4[uIdx]) * uFrac;
|
|
330
356
|
|
|
331
357
|
const dx = distGrid[row + x + 1] - distGrid[row + x - 1];
|
|
332
358
|
const dy = distGrid[(y + 1) * boxW + x] - distGrid[(y - 1) * boxW + x];
|
|
@@ -339,7 +365,8 @@ exports.renderFusedCluster = function renderFusedCluster(ctx, sourceCtx, element
|
|
|
339
365
|
offY = Math.round(-gy * shift);
|
|
340
366
|
|
|
341
367
|
if (gy < -0.2) {
|
|
342
|
-
|
|
368
|
+
const sVal = _T2[uIdx] + (_T2[uIdx + 1] - _T2[uIdx]) * uFrac;
|
|
369
|
+
sheen = Math.round(sVal * (-gy) * 0.78);
|
|
343
370
|
}
|
|
344
371
|
} else {
|
|
345
372
|
// Fast interior path: zero displacement inside flat region (-d >= rimPx)
|
|
@@ -354,7 +381,9 @@ exports.renderFusedCluster = function renderFusedCluster(ctx, sourceCtx, element
|
|
|
354
381
|
|
|
355
382
|
const absD = Math.abs(d);
|
|
356
383
|
if (absD < 3.0) {
|
|
357
|
-
|
|
384
|
+
const posG = Math.min(254, absD * 0.33 * 255.0);
|
|
385
|
+
const gIdx = posG | 0;
|
|
386
|
+
glint = Math.round((_T1[gIdx] + (_T1[gIdx + 1] - _T1[gIdx]) * (posG - gIdx)) * 0.65);
|
|
358
387
|
}
|
|
359
388
|
|
|
360
389
|
const sxG = Math.max(0, Math.min(boxW - 1, x + offX));
|
|
@@ -448,11 +477,18 @@ function getReadableStrokeColor(borderCol, fallback = 'rgba(148, 163, 184, 0.55)
|
|
|
448
477
|
return fallback;
|
|
449
478
|
}
|
|
450
479
|
|
|
480
|
+
function getDevicePixelRatio() {
|
|
481
|
+
return typeof window !== 'undefined' ? Math.min(window.devicePixelRatio || 1, 1.5) : 1;
|
|
482
|
+
}
|
|
483
|
+
|
|
451
484
|
function ensureStageBuffers() {
|
|
452
485
|
if (typeof document === 'undefined') return false;
|
|
486
|
+
const dpr = getDevicePixelRatio();
|
|
453
487
|
const docEl = document.documentElement;
|
|
454
488
|
const vw = Math.max(1, window.innerWidth, docEl ? docEl.clientWidth : 0);
|
|
455
489
|
const vh = Math.max(1, window.innerHeight, docEl ? docEl.clientHeight : 0);
|
|
490
|
+
const bufW = Math.round(vw * dpr);
|
|
491
|
+
const bufH = Math.round(vh * dpr);
|
|
456
492
|
|
|
457
493
|
if (!stageCanvas) {
|
|
458
494
|
stageCanvas = document.createElement('canvas');
|
|
@@ -461,11 +497,11 @@ function ensureStageBuffers() {
|
|
|
461
497
|
blurCtx = blurCanvas.getContext('2d', { willReadFrequently: true });
|
|
462
498
|
}
|
|
463
499
|
|
|
464
|
-
if (stageCanvas.width !==
|
|
465
|
-
stageCanvas.width =
|
|
466
|
-
stageCanvas.height =
|
|
467
|
-
blurCanvas.width =
|
|
468
|
-
blurCanvas.height =
|
|
500
|
+
if (stageCanvas.width !== bufW || stageCanvas.height !== bufH) {
|
|
501
|
+
stageCanvas.width = bufW;
|
|
502
|
+
stageCanvas.height = bufH;
|
|
503
|
+
blurCanvas.width = bufW;
|
|
504
|
+
blurCanvas.height = bufH;
|
|
469
505
|
}
|
|
470
506
|
|
|
471
507
|
if (!clusterOverlayCanvas) {
|
|
@@ -492,14 +528,20 @@ function ensureStageBuffers() {
|
|
|
492
528
|
*/
|
|
493
529
|
function captureViewportStage(neededBlur) {
|
|
494
530
|
if (!ensureStageBuffers()) return;
|
|
495
|
-
const
|
|
496
|
-
const
|
|
531
|
+
const dpr = getDevicePixelRatio();
|
|
532
|
+
const bw = stageCanvas.width;
|
|
533
|
+
const bh = stageCanvas.height;
|
|
534
|
+
const vw = Math.round(bw / dpr);
|
|
535
|
+
const vh = Math.round(bh / dpr);
|
|
497
536
|
|
|
498
|
-
|
|
499
|
-
stageCtx.fillStyle = '#040814';
|
|
500
|
-
stageCtx.fillRect(0, 0, vw, vh);
|
|
537
|
+
stageCtx.save();
|
|
501
538
|
|
|
502
|
-
//
|
|
539
|
+
// 1. Base page background (detect light/dark body background)
|
|
540
|
+
const bodyBg = typeof document !== 'undefined' ? window.getComputedStyle(document.body).backgroundColor : '';
|
|
541
|
+
stageCtx.fillStyle = bodyBg && bodyBg !== 'transparent' && bodyBg !== 'rgba(0, 0, 0, 0)' ? bodyBg : '#040814';
|
|
542
|
+
stageCtx.fillRect(0, 0, bw, bh);
|
|
543
|
+
|
|
544
|
+
// 2. Copy any background 2D canvas using physical buffer pixels
|
|
503
545
|
const bgCanvases = document.querySelectorAll('canvas:not(.lg-lens-canvas):not(.lg-cluster-canvas)');
|
|
504
546
|
for (let i = 0; i < bgCanvases.length; i++) {
|
|
505
547
|
const c = bgCanvases[i];
|
|
@@ -507,7 +549,13 @@ function captureViewportStage(neededBlur) {
|
|
|
507
549
|
try {
|
|
508
550
|
const cr = c.getBoundingClientRect();
|
|
509
551
|
if (cr.width > 0 && cr.height > 0) {
|
|
510
|
-
stageCtx.drawImage(
|
|
552
|
+
stageCtx.drawImage(
|
|
553
|
+
c,
|
|
554
|
+
Math.round(cr.left * dpr),
|
|
555
|
+
Math.round(cr.top * dpr),
|
|
556
|
+
Math.round(cr.width * dpr),
|
|
557
|
+
Math.round(cr.height * dpr)
|
|
558
|
+
);
|
|
511
559
|
}
|
|
512
560
|
} catch {
|
|
513
561
|
// Ignore cross-origin canvas if any
|
|
@@ -515,6 +563,9 @@ function captureViewportStage(neededBlur) {
|
|
|
515
563
|
}
|
|
516
564
|
}
|
|
517
565
|
|
|
566
|
+
// Scale context for DOM elements rendered in CSS pixels
|
|
567
|
+
stageCtx.scale(dpr, dpr);
|
|
568
|
+
|
|
518
569
|
// 3. Draw visible block surfaces (cards, buttons, pills, inputs) inside <main> and <footer>
|
|
519
570
|
const surfaceEls = document.querySelectorAll(
|
|
520
571
|
'main div.rounded-2xl, main div.rounded-xl, main a.rounded-2xl, main a.rounded-xl, main button, main a.inline-flex, main input, main textarea, main span.rounded-lg, main span.rounded-full'
|
|
@@ -668,13 +719,28 @@ function captureViewportStage(neededBlur) {
|
|
|
668
719
|
stageCtx.restore();
|
|
669
720
|
}
|
|
670
721
|
|
|
671
|
-
|
|
722
|
+
stageCtx.restore();
|
|
723
|
+
|
|
724
|
+
// 5. Blur buffer for frosted glass (Robust across all mobile WebKit & Blink engines)
|
|
672
725
|
if (neededBlur > 0) {
|
|
673
726
|
lastFrostedRadius = neededBlur;
|
|
674
|
-
blurCtx.clearRect(0, 0,
|
|
675
|
-
|
|
727
|
+
blurCtx.clearRect(0, 0, bw, bh);
|
|
728
|
+
const filterStr = `blur(${Math.round(neededBlur * dpr)}px)`;
|
|
729
|
+
blurCtx.filter = filterStr;
|
|
676
730
|
blurCtx.drawImage(stageCanvas, 0, 0);
|
|
677
|
-
|
|
731
|
+
|
|
732
|
+
// Safari iOS fallback if CanvasRenderingContext2D.filter is unsupported or ignored
|
|
733
|
+
if (!blurCtx.filter || blurCtx.filter === 'none') {
|
|
734
|
+
const scale = Math.max(0.08, 1.0 / (1.0 + neededBlur * 0.45));
|
|
735
|
+
const sw = Math.max(4, Math.round(bw * scale));
|
|
736
|
+
const sh = Math.max(4, Math.round(bh * scale));
|
|
737
|
+
blurCtx.imageSmoothingEnabled = true;
|
|
738
|
+
blurCtx.imageSmoothingQuality = 'high';
|
|
739
|
+
blurCtx.drawImage(stageCanvas, 0, 0, sw, sh);
|
|
740
|
+
blurCtx.drawImage(blurCanvas, 0, 0, sw, sh, 0, 0, bw, bh);
|
|
741
|
+
} else {
|
|
742
|
+
blurCtx.filter = 'none';
|
|
743
|
+
}
|
|
678
744
|
}
|
|
679
745
|
}
|
|
680
746
|
|
|
@@ -682,17 +748,19 @@ function captureViewportStage(neededBlur) {
|
|
|
682
748
|
* Fast Isolated Lens Renderer (Takes ~0.12 ms using precomputed TypedArray LUT!)
|
|
683
749
|
*/
|
|
684
750
|
function renderIsolatedController(ctrl, rect) {
|
|
685
|
-
const
|
|
686
|
-
const
|
|
751
|
+
const dpr = getDevicePixelRatio();
|
|
752
|
+
const w = Math.max(16, Math.round(rect.width * dpr));
|
|
753
|
+
const h = Math.max(16, Math.round(rect.height * dpr));
|
|
687
754
|
const rad = ctrl._getRadius();
|
|
755
|
+
const radPx = Math.min(rad * dpr, w * 0.5, h * 0.5);
|
|
688
756
|
|
|
689
757
|
if (
|
|
690
758
|
!ctrl._lut ||
|
|
691
759
|
ctrl._lut.w !== w ||
|
|
692
760
|
ctrl._lut.h !== h ||
|
|
693
|
-
Math.abs(ctrl._lut.rad -
|
|
761
|
+
Math.abs(ctrl._lut.rad - radPx) > 1.0
|
|
694
762
|
) {
|
|
695
|
-
ctrl._lut = buildElementLUT(w, h, rad, ctrl.options,
|
|
763
|
+
ctrl._lut = buildElementLUT(w, h, rad, ctrl.options, dpr);
|
|
696
764
|
}
|
|
697
765
|
|
|
698
766
|
const lut = ctrl._lut;
|
|
@@ -701,9 +769,9 @@ function renderIsolatedController(ctrl, rect) {
|
|
|
701
769
|
ctrl._canvas.height = h;
|
|
702
770
|
}
|
|
703
771
|
|
|
704
|
-
// Use exact un-clamped element position
|
|
705
|
-
const dx = Math.round(rect.left);
|
|
706
|
-
const dy = Math.round(rect.top);
|
|
772
|
+
// Use exact un-clamped element position in physical canvas pixels
|
|
773
|
+
const dx = Math.round(rect.left * dpr);
|
|
774
|
+
const dy = Math.round(rect.top * dpr);
|
|
707
775
|
|
|
708
776
|
const sourceCtx = ctrl.options.frosted > 0 ? blurCtx : stageCtx;
|
|
709
777
|
const srcImg = sourceCtx.getImageData(dx, dy, w, h);
|
|
@@ -773,7 +841,7 @@ function renderIsolatedController(ctrl, rect) {
|
|
|
773
841
|
ctrl._canvas.style.opacity = '1';
|
|
774
842
|
}
|
|
775
843
|
|
|
776
|
-
function areRectsTouching(r1, r2, threshold =
|
|
844
|
+
function areRectsTouching(r1, r2, threshold = 24) {
|
|
777
845
|
const dx = Math.max(
|
|
778
846
|
0,
|
|
779
847
|
Math.abs(r1.left + r1.width * 0.5 - (r2.left + r2.width * 0.5)) -
|
|
@@ -843,7 +911,7 @@ function renderAllActiveLenses() {
|
|
|
843
911
|
if (!visible[i].ctrl.options.fusion) continue;
|
|
844
912
|
for (let j = i + 1; j < n; j++) {
|
|
845
913
|
if (!visible[j].ctrl.options.fusion) continue;
|
|
846
|
-
if (areRectsTouching(visible[i].rect, visible[j].rect,
|
|
914
|
+
if (areRectsTouching(visible[i].rect, visible[j].rect, 24)) {
|
|
847
915
|
union(i, j);
|
|
848
916
|
}
|
|
849
917
|
}
|
|
@@ -861,21 +929,13 @@ function renderAllActiveLenses() {
|
|
|
861
929
|
for (const group of clusters.values()) {
|
|
862
930
|
if (group.length === 1) {
|
|
863
931
|
const { ctrl, rect } = group[0];
|
|
864
|
-
if (ctrl._savedShadow && ctrl.element.style.boxShadow === 'none') {
|
|
865
|
-
ctrl.element.style.boxShadow = ctrl._savedInlineShadow || '';
|
|
866
|
-
}
|
|
867
932
|
if (rect.bottom > 1 && rect.top < vh) {
|
|
868
933
|
renderIsolatedController(ctrl, rect);
|
|
869
934
|
}
|
|
870
935
|
} else {
|
|
871
936
|
hasFusedCluster = true;
|
|
872
|
-
let anyHasShadow = false;
|
|
873
937
|
for (const item of group) {
|
|
874
938
|
item.ctrl._canvas.style.opacity = '0';
|
|
875
|
-
if (item.ctrl._savedShadow) {
|
|
876
|
-
anyHasShadow = true;
|
|
877
|
-
item.ctrl.element.style.boxShadow = 'none';
|
|
878
|
-
}
|
|
879
939
|
}
|
|
880
940
|
const primaryCtrl = group[group.length - 1].ctrl;
|
|
881
941
|
const sourceCtx = primaryCtrl.options.frosted > 0 ? blurCtx : stageCtx;
|
|
@@ -889,25 +949,24 @@ function renderAllActiveLenses() {
|
|
|
889
949
|
tint: ctrl._tint
|
|
890
950
|
}));
|
|
891
951
|
|
|
952
|
+
const dpr = getDevicePixelRatio();
|
|
892
953
|
const box = renderFusedCluster(
|
|
893
954
|
clusterOverlayCtx,
|
|
894
955
|
sourceCtx,
|
|
895
956
|
elements,
|
|
896
957
|
{
|
|
897
958
|
...primaryCtrl.options,
|
|
898
|
-
fusionDistance:
|
|
959
|
+
fusionDistance: 24
|
|
899
960
|
},
|
|
900
|
-
|
|
961
|
+
dpr
|
|
901
962
|
);
|
|
902
963
|
|
|
903
964
|
if (box && clusterOverlayCanvas) {
|
|
904
965
|
clusterOverlayCanvas.style.display = 'block';
|
|
905
|
-
clusterOverlayCanvas.style.filter =
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
clusterOverlayCanvas.style.
|
|
909
|
-
clusterOverlayCanvas.style.width = `${box.boxW}px`;
|
|
910
|
-
clusterOverlayCanvas.style.height = `${box.boxH}px`;
|
|
966
|
+
clusterOverlayCanvas.style.filter = 'none';
|
|
967
|
+
clusterOverlayCanvas.style.transform = `translate3d(${box.cMinX / dpr}px, ${box.cMinY / dpr}px, 0)`;
|
|
968
|
+
clusterOverlayCanvas.style.width = `${box.boxW / dpr}px`;
|
|
969
|
+
clusterOverlayCanvas.style.height = `${box.boxH / dpr}px`;
|
|
911
970
|
}
|
|
912
971
|
}
|
|
913
972
|
}
|
package/dist/core.mjs
CHANGED
|
@@ -221,7 +221,7 @@ export function buildOpticalMapURI(width, height, borderRadius, rawOptions = {})
|
|
|
221
221
|
*/
|
|
222
222
|
export function renderFusedCluster(ctx, sourceCtx, elements, rawOptions = {}, dpr = 1) {
|
|
223
223
|
const options = normalizeOptions(rawOptions);
|
|
224
|
-
const fusionSpan = options.fusion ? (rawOptions.fusionDistance ??
|
|
224
|
+
const fusionSpan = options.fusion ? (rawOptions.fusionDistance ?? 24) * dpr : 0;
|
|
225
225
|
const rimPx = Math.max(1, options.rim * dpr);
|
|
226
226
|
const ior = options.ior;
|
|
227
227
|
const heightPx = options.height * dpr;
|
|
@@ -259,19 +259,26 @@ export function renderFusedCluster(ctx, sourceCtx, elements, rawOptions = {}, dp
|
|
|
259
259
|
const dstData = dstImg.data;
|
|
260
260
|
|
|
261
261
|
const distGrid = new Float32Array(boxW * boxH);
|
|
262
|
-
const prep = elements.map((b) =>
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
262
|
+
const prep = elements.map((b) => {
|
|
263
|
+
const halfW = b.w * 0.5 * dpr;
|
|
264
|
+
const halfH = b.h * 0.5 * dpr;
|
|
265
|
+
const rad = (b.borderRadius ?? 0) * dpr;
|
|
266
|
+
const r = Math.min(rad, halfW, halfH);
|
|
267
|
+
const isCircle = b.shape === 'circle' || (Math.abs(halfW - halfH) < 1.0 && Math.abs(r - halfW) < 1.0);
|
|
268
|
+
return {
|
|
269
|
+
cx: (b.x + b.w * 0.5) * dpr,
|
|
270
|
+
cy: (b.y + b.h * 0.5) * dpr,
|
|
271
|
+
halfW,
|
|
272
|
+
halfH,
|
|
273
|
+
r,
|
|
274
|
+
isCircle,
|
|
275
|
+
fusion: b.fusion !== undefined ? Boolean(b.fusion) : options.fusion,
|
|
276
|
+
tintR: b.tint ? b.tint[0] : 15,
|
|
277
|
+
tintG: b.tint ? b.tint[1] : 23,
|
|
278
|
+
tintB: b.tint ? b.tint[2] : 42,
|
|
279
|
+
tintA: b.tint ? b.tint[3] : 0.35
|
|
280
|
+
};
|
|
281
|
+
});
|
|
275
282
|
|
|
276
283
|
const primary = prep[0];
|
|
277
284
|
const tR = primary.tintR;
|
|
@@ -279,8 +286,10 @@ export function renderFusedCluster(ctx, sourceCtx, elements, rawOptions = {}, dp
|
|
|
279
286
|
const tB = primary.tintB;
|
|
280
287
|
const tA = primary.tintA;
|
|
281
288
|
const invTA = 1.0 - tA;
|
|
289
|
+
const span = fusionSpan;
|
|
290
|
+
const invSpan = span > 0.001 ? 1.0 / span : 0;
|
|
282
291
|
|
|
283
|
-
// Pass 1: Boundary evaluation across cluster bounding box
|
|
292
|
+
// Pass 1: Boundary evaluation across cluster bounding box (Inlined fast LUT math)
|
|
284
293
|
for (let y = 0; y < boxH; y++) {
|
|
285
294
|
const py = cMinY + y;
|
|
286
295
|
const row = y * boxW;
|
|
@@ -289,22 +298,36 @@ export function renderFusedCluster(ctx, sourceCtx, elements, rawOptions = {}, dp
|
|
|
289
298
|
let dCluster = Infinity;
|
|
290
299
|
for (let i = 0; i < prep.length; i++) {
|
|
291
300
|
const b = prep[i];
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
301
|
+
let dShape;
|
|
302
|
+
if (b.isCircle) {
|
|
303
|
+
dShape = Math.hypot(px - b.cx, py - b.cy) - b.halfW;
|
|
304
|
+
} else {
|
|
305
|
+
const qx = Math.abs(px - b.cx) - b.halfW + b.r;
|
|
306
|
+
const qy = Math.abs(py - b.cy) - b.halfH + b.r;
|
|
307
|
+
dShape = Math.min(Math.max(qx, qy), 0.0) + Math.hypot(Math.max(qx, 0.0), Math.max(qy, 0.0)) - b.r;
|
|
308
|
+
}
|
|
309
|
+
|
|
295
310
|
if (i === 0) {
|
|
296
311
|
dCluster = dShape;
|
|
297
|
-
} else if (b.fusion &&
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
312
|
+
} else if (b.fusion && span > 0.001) {
|
|
313
|
+
const diff = dCluster > dShape ? dCluster - dShape : dShape - dCluster;
|
|
314
|
+
if (diff < span) {
|
|
315
|
+
const pos = diff * invSpan * 255.0;
|
|
316
|
+
const pIdx = Math.min(254, pos | 0);
|
|
317
|
+
const wWeight = _T3[pIdx] + (_T3[pIdx + 1] - _T3[pIdx]) * (pos - pIdx);
|
|
318
|
+
dCluster = (dCluster < dShape ? dCluster : dShape) - wWeight * span;
|
|
319
|
+
} else if (dShape < dCluster) {
|
|
320
|
+
dCluster = dShape;
|
|
321
|
+
}
|
|
322
|
+
} else if (dShape < dCluster) {
|
|
323
|
+
dCluster = dShape;
|
|
301
324
|
}
|
|
302
325
|
}
|
|
303
326
|
distGrid[row + x] = dCluster;
|
|
304
327
|
}
|
|
305
328
|
}
|
|
306
329
|
|
|
307
|
-
// Pass 2:
|
|
330
|
+
// Pass 2: Fast vectorized lookup from distGrid + tabular chromatic shift (_T4)
|
|
308
331
|
for (let y = 1; y < boxH - 1; y++) {
|
|
309
332
|
const row = y * boxW;
|
|
310
333
|
for (let x = 1; x < boxW - 1; x++) {
|
|
@@ -325,8 +348,11 @@ export function renderFusedCluster(ctx, sourceCtx, elements, rawOptions = {}, dp
|
|
|
325
348
|
const innerDist = -d;
|
|
326
349
|
if (innerDist < rimPx) {
|
|
327
350
|
const u = innerDist / rimPx;
|
|
328
|
-
const
|
|
329
|
-
|
|
351
|
+
const posU = u * 255.0;
|
|
352
|
+
const uIdx = Math.min(254, posU | 0);
|
|
353
|
+
const uFrac = posU - uIdx;
|
|
354
|
+
const gain = _T0[uIdx] + (_T0[uIdx + 1] - _T0[uIdx]) * uFrac;
|
|
355
|
+
disp = _T4[uIdx] + (_T4[uIdx + 1] - _T4[uIdx]) * uFrac;
|
|
330
356
|
|
|
331
357
|
const dx = distGrid[row + x + 1] - distGrid[row + x - 1];
|
|
332
358
|
const dy = distGrid[(y + 1) * boxW + x] - distGrid[(y - 1) * boxW + x];
|
|
@@ -339,7 +365,8 @@ export function renderFusedCluster(ctx, sourceCtx, elements, rawOptions = {}, dp
|
|
|
339
365
|
offY = Math.round(-gy * shift);
|
|
340
366
|
|
|
341
367
|
if (gy < -0.2) {
|
|
342
|
-
|
|
368
|
+
const sVal = _T2[uIdx] + (_T2[uIdx + 1] - _T2[uIdx]) * uFrac;
|
|
369
|
+
sheen = Math.round(sVal * (-gy) * 0.78);
|
|
343
370
|
}
|
|
344
371
|
} else {
|
|
345
372
|
// Fast interior path: zero displacement inside flat region (-d >= rimPx)
|
|
@@ -354,7 +381,9 @@ export function renderFusedCluster(ctx, sourceCtx, elements, rawOptions = {}, dp
|
|
|
354
381
|
|
|
355
382
|
const absD = Math.abs(d);
|
|
356
383
|
if (absD < 3.0) {
|
|
357
|
-
|
|
384
|
+
const posG = Math.min(254, absD * 0.33 * 255.0);
|
|
385
|
+
const gIdx = posG | 0;
|
|
386
|
+
glint = Math.round((_T1[gIdx] + (_T1[gIdx + 1] - _T1[gIdx]) * (posG - gIdx)) * 0.65);
|
|
358
387
|
}
|
|
359
388
|
|
|
360
389
|
const sxG = Math.max(0, Math.min(boxW - 1, x + offX));
|
|
@@ -448,11 +477,18 @@ function getReadableStrokeColor(borderCol, fallback = 'rgba(148, 163, 184, 0.55)
|
|
|
448
477
|
return fallback;
|
|
449
478
|
}
|
|
450
479
|
|
|
480
|
+
function getDevicePixelRatio() {
|
|
481
|
+
return typeof window !== 'undefined' ? Math.min(window.devicePixelRatio || 1, 1.5) : 1;
|
|
482
|
+
}
|
|
483
|
+
|
|
451
484
|
function ensureStageBuffers() {
|
|
452
485
|
if (typeof document === 'undefined') return false;
|
|
486
|
+
const dpr = getDevicePixelRatio();
|
|
453
487
|
const docEl = document.documentElement;
|
|
454
488
|
const vw = Math.max(1, window.innerWidth, docEl ? docEl.clientWidth : 0);
|
|
455
489
|
const vh = Math.max(1, window.innerHeight, docEl ? docEl.clientHeight : 0);
|
|
490
|
+
const bufW = Math.round(vw * dpr);
|
|
491
|
+
const bufH = Math.round(vh * dpr);
|
|
456
492
|
|
|
457
493
|
if (!stageCanvas) {
|
|
458
494
|
stageCanvas = document.createElement('canvas');
|
|
@@ -461,11 +497,11 @@ function ensureStageBuffers() {
|
|
|
461
497
|
blurCtx = blurCanvas.getContext('2d', { willReadFrequently: true });
|
|
462
498
|
}
|
|
463
499
|
|
|
464
|
-
if (stageCanvas.width !==
|
|
465
|
-
stageCanvas.width =
|
|
466
|
-
stageCanvas.height =
|
|
467
|
-
blurCanvas.width =
|
|
468
|
-
blurCanvas.height =
|
|
500
|
+
if (stageCanvas.width !== bufW || stageCanvas.height !== bufH) {
|
|
501
|
+
stageCanvas.width = bufW;
|
|
502
|
+
stageCanvas.height = bufH;
|
|
503
|
+
blurCanvas.width = bufW;
|
|
504
|
+
blurCanvas.height = bufH;
|
|
469
505
|
}
|
|
470
506
|
|
|
471
507
|
if (!clusterOverlayCanvas) {
|
|
@@ -492,14 +528,20 @@ function ensureStageBuffers() {
|
|
|
492
528
|
*/
|
|
493
529
|
function captureViewportStage(neededBlur) {
|
|
494
530
|
if (!ensureStageBuffers()) return;
|
|
495
|
-
const
|
|
496
|
-
const
|
|
531
|
+
const dpr = getDevicePixelRatio();
|
|
532
|
+
const bw = stageCanvas.width;
|
|
533
|
+
const bh = stageCanvas.height;
|
|
534
|
+
const vw = Math.round(bw / dpr);
|
|
535
|
+
const vh = Math.round(bh / dpr);
|
|
497
536
|
|
|
498
|
-
|
|
499
|
-
stageCtx.fillStyle = '#040814';
|
|
500
|
-
stageCtx.fillRect(0, 0, vw, vh);
|
|
537
|
+
stageCtx.save();
|
|
501
538
|
|
|
502
|
-
//
|
|
539
|
+
// 1. Base page background (detect light/dark body background)
|
|
540
|
+
const bodyBg = typeof document !== 'undefined' ? window.getComputedStyle(document.body).backgroundColor : '';
|
|
541
|
+
stageCtx.fillStyle = bodyBg && bodyBg !== 'transparent' && bodyBg !== 'rgba(0, 0, 0, 0)' ? bodyBg : '#040814';
|
|
542
|
+
stageCtx.fillRect(0, 0, bw, bh);
|
|
543
|
+
|
|
544
|
+
// 2. Copy any background 2D canvas using physical buffer pixels
|
|
503
545
|
const bgCanvases = document.querySelectorAll('canvas:not(.lg-lens-canvas):not(.lg-cluster-canvas)');
|
|
504
546
|
for (let i = 0; i < bgCanvases.length; i++) {
|
|
505
547
|
const c = bgCanvases[i];
|
|
@@ -507,7 +549,13 @@ function captureViewportStage(neededBlur) {
|
|
|
507
549
|
try {
|
|
508
550
|
const cr = c.getBoundingClientRect();
|
|
509
551
|
if (cr.width > 0 && cr.height > 0) {
|
|
510
|
-
stageCtx.drawImage(
|
|
552
|
+
stageCtx.drawImage(
|
|
553
|
+
c,
|
|
554
|
+
Math.round(cr.left * dpr),
|
|
555
|
+
Math.round(cr.top * dpr),
|
|
556
|
+
Math.round(cr.width * dpr),
|
|
557
|
+
Math.round(cr.height * dpr)
|
|
558
|
+
);
|
|
511
559
|
}
|
|
512
560
|
} catch {
|
|
513
561
|
// Ignore cross-origin canvas if any
|
|
@@ -515,6 +563,9 @@ function captureViewportStage(neededBlur) {
|
|
|
515
563
|
}
|
|
516
564
|
}
|
|
517
565
|
|
|
566
|
+
// Scale context for DOM elements rendered in CSS pixels
|
|
567
|
+
stageCtx.scale(dpr, dpr);
|
|
568
|
+
|
|
518
569
|
// 3. Draw visible block surfaces (cards, buttons, pills, inputs) inside <main> and <footer>
|
|
519
570
|
const surfaceEls = document.querySelectorAll(
|
|
520
571
|
'main div.rounded-2xl, main div.rounded-xl, main a.rounded-2xl, main a.rounded-xl, main button, main a.inline-flex, main input, main textarea, main span.rounded-lg, main span.rounded-full'
|
|
@@ -668,13 +719,28 @@ function captureViewportStage(neededBlur) {
|
|
|
668
719
|
stageCtx.restore();
|
|
669
720
|
}
|
|
670
721
|
|
|
671
|
-
|
|
722
|
+
stageCtx.restore();
|
|
723
|
+
|
|
724
|
+
// 5. Blur buffer for frosted glass (Robust across all mobile WebKit & Blink engines)
|
|
672
725
|
if (neededBlur > 0) {
|
|
673
726
|
lastFrostedRadius = neededBlur;
|
|
674
|
-
blurCtx.clearRect(0, 0,
|
|
675
|
-
|
|
727
|
+
blurCtx.clearRect(0, 0, bw, bh);
|
|
728
|
+
const filterStr = `blur(${Math.round(neededBlur * dpr)}px)`;
|
|
729
|
+
blurCtx.filter = filterStr;
|
|
676
730
|
blurCtx.drawImage(stageCanvas, 0, 0);
|
|
677
|
-
|
|
731
|
+
|
|
732
|
+
// Safari iOS fallback if CanvasRenderingContext2D.filter is unsupported or ignored
|
|
733
|
+
if (!blurCtx.filter || blurCtx.filter === 'none') {
|
|
734
|
+
const scale = Math.max(0.08, 1.0 / (1.0 + neededBlur * 0.45));
|
|
735
|
+
const sw = Math.max(4, Math.round(bw * scale));
|
|
736
|
+
const sh = Math.max(4, Math.round(bh * scale));
|
|
737
|
+
blurCtx.imageSmoothingEnabled = true;
|
|
738
|
+
blurCtx.imageSmoothingQuality = 'high';
|
|
739
|
+
blurCtx.drawImage(stageCanvas, 0, 0, sw, sh);
|
|
740
|
+
blurCtx.drawImage(blurCanvas, 0, 0, sw, sh, 0, 0, bw, bh);
|
|
741
|
+
} else {
|
|
742
|
+
blurCtx.filter = 'none';
|
|
743
|
+
}
|
|
678
744
|
}
|
|
679
745
|
}
|
|
680
746
|
|
|
@@ -682,17 +748,19 @@ function captureViewportStage(neededBlur) {
|
|
|
682
748
|
* Fast Isolated Lens Renderer (Takes ~0.12 ms using precomputed TypedArray LUT!)
|
|
683
749
|
*/
|
|
684
750
|
function renderIsolatedController(ctrl, rect) {
|
|
685
|
-
const
|
|
686
|
-
const
|
|
751
|
+
const dpr = getDevicePixelRatio();
|
|
752
|
+
const w = Math.max(16, Math.round(rect.width * dpr));
|
|
753
|
+
const h = Math.max(16, Math.round(rect.height * dpr));
|
|
687
754
|
const rad = ctrl._getRadius();
|
|
755
|
+
const radPx = Math.min(rad * dpr, w * 0.5, h * 0.5);
|
|
688
756
|
|
|
689
757
|
if (
|
|
690
758
|
!ctrl._lut ||
|
|
691
759
|
ctrl._lut.w !== w ||
|
|
692
760
|
ctrl._lut.h !== h ||
|
|
693
|
-
Math.abs(ctrl._lut.rad -
|
|
761
|
+
Math.abs(ctrl._lut.rad - radPx) > 1.0
|
|
694
762
|
) {
|
|
695
|
-
ctrl._lut = buildElementLUT(w, h, rad, ctrl.options,
|
|
763
|
+
ctrl._lut = buildElementLUT(w, h, rad, ctrl.options, dpr);
|
|
696
764
|
}
|
|
697
765
|
|
|
698
766
|
const lut = ctrl._lut;
|
|
@@ -701,9 +769,9 @@ function renderIsolatedController(ctrl, rect) {
|
|
|
701
769
|
ctrl._canvas.height = h;
|
|
702
770
|
}
|
|
703
771
|
|
|
704
|
-
// Use exact un-clamped element position
|
|
705
|
-
const dx = Math.round(rect.left);
|
|
706
|
-
const dy = Math.round(rect.top);
|
|
772
|
+
// Use exact un-clamped element position in physical canvas pixels
|
|
773
|
+
const dx = Math.round(rect.left * dpr);
|
|
774
|
+
const dy = Math.round(rect.top * dpr);
|
|
707
775
|
|
|
708
776
|
const sourceCtx = ctrl.options.frosted > 0 ? blurCtx : stageCtx;
|
|
709
777
|
const srcImg = sourceCtx.getImageData(dx, dy, w, h);
|
|
@@ -773,7 +841,7 @@ function renderIsolatedController(ctrl, rect) {
|
|
|
773
841
|
ctrl._canvas.style.opacity = '1';
|
|
774
842
|
}
|
|
775
843
|
|
|
776
|
-
function areRectsTouching(r1, r2, threshold =
|
|
844
|
+
function areRectsTouching(r1, r2, threshold = 24) {
|
|
777
845
|
const dx = Math.max(
|
|
778
846
|
0,
|
|
779
847
|
Math.abs(r1.left + r1.width * 0.5 - (r2.left + r2.width * 0.5)) -
|
|
@@ -843,7 +911,7 @@ function renderAllActiveLenses() {
|
|
|
843
911
|
if (!visible[i].ctrl.options.fusion) continue;
|
|
844
912
|
for (let j = i + 1; j < n; j++) {
|
|
845
913
|
if (!visible[j].ctrl.options.fusion) continue;
|
|
846
|
-
if (areRectsTouching(visible[i].rect, visible[j].rect,
|
|
914
|
+
if (areRectsTouching(visible[i].rect, visible[j].rect, 24)) {
|
|
847
915
|
union(i, j);
|
|
848
916
|
}
|
|
849
917
|
}
|
|
@@ -861,21 +929,13 @@ function renderAllActiveLenses() {
|
|
|
861
929
|
for (const group of clusters.values()) {
|
|
862
930
|
if (group.length === 1) {
|
|
863
931
|
const { ctrl, rect } = group[0];
|
|
864
|
-
if (ctrl._savedShadow && ctrl.element.style.boxShadow === 'none') {
|
|
865
|
-
ctrl.element.style.boxShadow = ctrl._savedInlineShadow || '';
|
|
866
|
-
}
|
|
867
932
|
if (rect.bottom > 1 && rect.top < vh) {
|
|
868
933
|
renderIsolatedController(ctrl, rect);
|
|
869
934
|
}
|
|
870
935
|
} else {
|
|
871
936
|
hasFusedCluster = true;
|
|
872
|
-
let anyHasShadow = false;
|
|
873
937
|
for (const item of group) {
|
|
874
938
|
item.ctrl._canvas.style.opacity = '0';
|
|
875
|
-
if (item.ctrl._savedShadow) {
|
|
876
|
-
anyHasShadow = true;
|
|
877
|
-
item.ctrl.element.style.boxShadow = 'none';
|
|
878
|
-
}
|
|
879
939
|
}
|
|
880
940
|
const primaryCtrl = group[group.length - 1].ctrl;
|
|
881
941
|
const sourceCtx = primaryCtrl.options.frosted > 0 ? blurCtx : stageCtx;
|
|
@@ -889,25 +949,24 @@ function renderAllActiveLenses() {
|
|
|
889
949
|
tint: ctrl._tint
|
|
890
950
|
}));
|
|
891
951
|
|
|
952
|
+
const dpr = getDevicePixelRatio();
|
|
892
953
|
const box = renderFusedCluster(
|
|
893
954
|
clusterOverlayCtx,
|
|
894
955
|
sourceCtx,
|
|
895
956
|
elements,
|
|
896
957
|
{
|
|
897
958
|
...primaryCtrl.options,
|
|
898
|
-
fusionDistance:
|
|
959
|
+
fusionDistance: 24
|
|
899
960
|
},
|
|
900
|
-
|
|
961
|
+
dpr
|
|
901
962
|
);
|
|
902
963
|
|
|
903
964
|
if (box && clusterOverlayCanvas) {
|
|
904
965
|
clusterOverlayCanvas.style.display = 'block';
|
|
905
|
-
clusterOverlayCanvas.style.filter =
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
clusterOverlayCanvas.style.
|
|
909
|
-
clusterOverlayCanvas.style.width = `${box.boxW}px`;
|
|
910
|
-
clusterOverlayCanvas.style.height = `${box.boxH}px`;
|
|
966
|
+
clusterOverlayCanvas.style.filter = 'none';
|
|
967
|
+
clusterOverlayCanvas.style.transform = `translate3d(${box.cMinX / dpr}px, ${box.cMinY / dpr}px, 0)`;
|
|
968
|
+
clusterOverlayCanvas.style.width = `${box.boxW / dpr}px`;
|
|
969
|
+
clusterOverlayCanvas.style.height = `${box.boxH / dpr}px`;
|
|
911
970
|
}
|
|
912
971
|
}
|
|
913
972
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shulya/liquid-glass",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "High-performance, zero-WebGL Liquid Glass optical UI engine with pre-computed empirical lookup tables for React, Vue, Svelte, Angular, SolidJS, and Vanilla JS.",
|
|
5
5
|
"author": "Shulya (https://shulya.tech)",
|
|
6
6
|
"homepage": "https://liquid.shulya.tech",
|