@shulya/liquid-glass 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@shulya/liquid-glass`
2
2
 
3
- [![npm version](https://img.shields.io/badge/npm-v1.0.0-007aff?style=flat-square)](https://www.npmjs.com/package/@shulya/liquid-glass)
3
+ [![npm version](https://img.shields.io/badge/npm-v1.0.1-007aff?style=flat-square)](https://www.npmjs.com/package/@shulya/liquid-glass)
4
4
  [![Live Demo](https://img.shields.io/badge/Live%20Demo-liquid.shulya.tech-ff2d55?style=flat-square)](https://liquid.shulya.tech)
5
5
  [![GitHub Repo](https://img.shields.io/badge/GitHub-shulya--tech%2Fliquid--glass-181717?style=flat-square&logo=github)](https://github.com/shulya-tech/liquid-glass)
6
6
  [![Zero WebGL](<https://img.shields.io/badge/WebGL-None%20(100%25%20DOM%20%2F%202D%20LUT)-30d158?style=flat-square>)](#why-shulyaliquid-glass)
@@ -10,9 +10,10 @@
10
10
  **`@shulya/liquid-glass`** is an ultra-lightweight, zero-WebGL optical glass rendering engine for modern web applications. Turn any HTML container, card, navigation bar, modal, or interactive widget into a realistic refractive **Liquid Glass** block with real-time control over **`rim`**, **`ior`**, **`height`**, **`frosted`**, and **`fusion`** parameters.
11
11
 
12
12
  - **Live Interactive Demo:** [https://liquid.shulya.tech](https://liquid.shulya.tech)
13
+ - **NPM Package:** [https://www.npmjs.com/package/@shulya/liquid-glass](https://www.npmjs.com/package/@shulya/liquid-glass)
13
14
  - **GitHub Repository:** [https://github.com/shulya-tech/liquid-glass](https://github.com/shulya-tech/liquid-glass)
14
15
 
15
- ![Liquid Glass Interactive Playground](./screen.png)
16
+ ![Liquid Glass Interactive Playground](https://raw.githubusercontent.com/shulya-tech/liquid-glass/main/screen.png)
16
17
 
17
18
  ---
18
19
 
@@ -43,7 +44,7 @@ yarn add @shulya/liquid-glass
43
44
  > **Try it online right now:** [**https://liquid.shulya.tech**](https://liquid.shulya.tech)
44
45
  > **Source code on GitHub:** [**github.com/shulya-tech/liquid-glass/tree/main/example**](https://github.com/shulya-tech/liquid-glass/tree/main/example)
45
46
 
46
- ![Interactive React Playground Preview](./screen.png)
47
+ ![Interactive React Playground Preview](https://raw.githubusercontent.com/shulya-tech/liquid-glass/main/screen.png)
47
48
 
48
49
  The repository includes a complete **React + Vite** interactive playground in [`example/`](./example/) ([`example/src/App.jsx`](./example/src/App.jsx)) where you can drag three `<LiquidGlass>` components (a `136×74` capsule slider, a `196×116` rounded card, and a `112×112` circular lens) across a high-contrast optical test stage with a green pill track, diagonal rainbow ribbon, sunset sphere, black/white stripe grating, and centered `"Shulya"` typography.
49
50
 
@@ -327,6 +328,7 @@ To guarantee zero runtime trigonometric or shader overhead across mobile devices
327
328
  ## Links
328
329
 
329
330
  - **Live Demo:** [https://liquid.shulya.tech](https://liquid.shulya.tech)
331
+ - **NPM Package:** [https://www.npmjs.com/package/@shulya/liquid-glass](https://www.npmjs.com/package/@shulya/liquid-glass)
330
332
  - **GitHub Repository:** [https://github.com/shulya-tech/liquid-glass](https://github.com/shulya-tech/liquid-glass)
331
333
  - **Issue Tracker:** [https://github.com/shulya-tech/liquid-glass/issues](https://github.com/shulya-tech/liquid-glass/issues)
332
334
 
package/dist/core.cjs CHANGED
@@ -399,6 +399,7 @@ let blurCtx = null;
399
399
  let clusterOverlayCanvas = null;
400
400
  let clusterOverlayCtx = null;
401
401
  let rafScheduled = false;
402
+ let rafId = 0;
402
403
  let listenersAttached = false;
403
404
  let lastFrostedRadius = -1;
404
405
 
@@ -449,8 +450,9 @@ function getReadableStrokeColor(borderCol, fallback = 'rgba(148, 163, 184, 0.55)
449
450
 
450
451
  function ensureStageBuffers() {
451
452
  if (typeof document === 'undefined') return false;
452
- const vw = Math.max(1, window.innerWidth);
453
- const vh = Math.max(1, window.innerHeight);
453
+ const docEl = document.documentElement;
454
+ const vw = Math.max(1, window.innerWidth, docEl ? docEl.clientWidth : 0);
455
+ const vh = Math.max(1, window.innerHeight, docEl ? docEl.clientHeight : 0);
454
456
 
455
457
  if (!stageCanvas) {
456
458
  stageCanvas = document.createElement('canvas');
@@ -497,13 +499,16 @@ function captureViewportStage(neededBlur) {
497
499
  stageCtx.fillStyle = '#040814';
498
500
  stageCtx.fillRect(0, 0, vw, vh);
499
501
 
500
- // 2. Copy any background 2D canvas
502
+ // 2. Copy any background 2D canvas using its exact getBoundingClientRect() viewport box
501
503
  const bgCanvases = document.querySelectorAll('canvas:not(.lg-lens-canvas):not(.lg-cluster-canvas)');
502
504
  for (let i = 0; i < bgCanvases.length; i++) {
503
505
  const c = bgCanvases[i];
504
506
  if (c.width > 0 && c.height > 0) {
505
507
  try {
506
- stageCtx.drawImage(c, 0, 0, vw, vh);
508
+ const cr = c.getBoundingClientRect();
509
+ if (cr.width > 0 && cr.height > 0) {
510
+ stageCtx.drawImage(c, cr.left, cr.top, cr.width, cr.height);
511
+ }
507
512
  } catch {
508
513
  // Ignore cross-origin canvas if any
509
514
  }
@@ -696,10 +701,9 @@ function renderIsolatedController(ctrl, rect) {
696
701
  ctrl._canvas.height = h;
697
702
  }
698
703
 
699
- const vw = stageCanvas.width;
700
- const vh = stageCanvas.height;
701
- const dx = Math.max(0, Math.min(vw - w, Math.round(rect.left)));
702
- const dy = Math.max(0, Math.min(vh - h, Math.round(rect.top)));
704
+ // Use exact un-clamped element position so dragging near/past mobile viewport edges never shifts the image
705
+ const dx = Math.round(rect.left);
706
+ const dy = Math.round(rect.top);
703
707
 
704
708
  const sourceCtx = ctrl.options.frosted > 0 ? blurCtx : stageCtx;
705
709
  const srcImg = sourceCtx.getImageData(dx, dy, w, h);
@@ -914,9 +918,12 @@ function renderAllActiveLenses() {
914
918
  }
915
919
 
916
920
  exports.scheduleStageRedraw = function scheduleStageRedraw() {
917
- if (rafScheduled || typeof window === 'undefined') return;
921
+ if (typeof window === 'undefined') return;
922
+ if (rafScheduled && rafId) {
923
+ window.cancelAnimationFrame(rafId);
924
+ }
918
925
  rafScheduled = true;
919
- window.requestAnimationFrame(renderAllActiveLenses);
926
+ rafId = window.requestAnimationFrame(renderAllActiveLenses);
920
927
  }
921
928
 
922
929
  function attachGlobalStageListeners() {
package/dist/core.mjs CHANGED
@@ -399,6 +399,7 @@ let blurCtx = null;
399
399
  let clusterOverlayCanvas = null;
400
400
  let clusterOverlayCtx = null;
401
401
  let rafScheduled = false;
402
+ let rafId = 0;
402
403
  let listenersAttached = false;
403
404
  let lastFrostedRadius = -1;
404
405
 
@@ -449,8 +450,9 @@ function getReadableStrokeColor(borderCol, fallback = 'rgba(148, 163, 184, 0.55)
449
450
 
450
451
  function ensureStageBuffers() {
451
452
  if (typeof document === 'undefined') return false;
452
- const vw = Math.max(1, window.innerWidth);
453
- const vh = Math.max(1, window.innerHeight);
453
+ const docEl = document.documentElement;
454
+ const vw = Math.max(1, window.innerWidth, docEl ? docEl.clientWidth : 0);
455
+ const vh = Math.max(1, window.innerHeight, docEl ? docEl.clientHeight : 0);
454
456
 
455
457
  if (!stageCanvas) {
456
458
  stageCanvas = document.createElement('canvas');
@@ -497,13 +499,16 @@ function captureViewportStage(neededBlur) {
497
499
  stageCtx.fillStyle = '#040814';
498
500
  stageCtx.fillRect(0, 0, vw, vh);
499
501
 
500
- // 2. Copy any background 2D canvas
502
+ // 2. Copy any background 2D canvas using its exact getBoundingClientRect() viewport box
501
503
  const bgCanvases = document.querySelectorAll('canvas:not(.lg-lens-canvas):not(.lg-cluster-canvas)');
502
504
  for (let i = 0; i < bgCanvases.length; i++) {
503
505
  const c = bgCanvases[i];
504
506
  if (c.width > 0 && c.height > 0) {
505
507
  try {
506
- stageCtx.drawImage(c, 0, 0, vw, vh);
508
+ const cr = c.getBoundingClientRect();
509
+ if (cr.width > 0 && cr.height > 0) {
510
+ stageCtx.drawImage(c, cr.left, cr.top, cr.width, cr.height);
511
+ }
507
512
  } catch {
508
513
  // Ignore cross-origin canvas if any
509
514
  }
@@ -696,10 +701,9 @@ function renderIsolatedController(ctrl, rect) {
696
701
  ctrl._canvas.height = h;
697
702
  }
698
703
 
699
- const vw = stageCanvas.width;
700
- const vh = stageCanvas.height;
701
- const dx = Math.max(0, Math.min(vw - w, Math.round(rect.left)));
702
- const dy = Math.max(0, Math.min(vh - h, Math.round(rect.top)));
704
+ // Use exact un-clamped element position so dragging near/past mobile viewport edges never shifts the image
705
+ const dx = Math.round(rect.left);
706
+ const dy = Math.round(rect.top);
703
707
 
704
708
  const sourceCtx = ctrl.options.frosted > 0 ? blurCtx : stageCtx;
705
709
  const srcImg = sourceCtx.getImageData(dx, dy, w, h);
@@ -914,9 +918,12 @@ function renderAllActiveLenses() {
914
918
  }
915
919
 
916
920
  export function scheduleStageRedraw() {
917
- if (rafScheduled || typeof window === 'undefined') return;
921
+ if (typeof window === 'undefined') return;
922
+ if (rafScheduled && rafId) {
923
+ window.cancelAnimationFrame(rafId);
924
+ }
918
925
  rafScheduled = true;
919
- window.requestAnimationFrame(renderAllActiveLenses);
926
+ rafId = window.requestAnimationFrame(renderAllActiveLenses);
920
927
  }
921
928
 
922
929
  function attachGlobalStageListeners() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shulya/liquid-glass",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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",
@@ -66,16 +66,7 @@
66
66
  },
67
67
  "files": [
68
68
  "dist",
69
- "src",
70
69
  "types",
71
- "example/index.html",
72
- "example/package.json",
73
- "example/vite.config.js",
74
- "example/README.md",
75
- "example/src",
76
- "tables/optical_calibration_matrix.xlsx",
77
- "tables/optical_calibration_matrix.csv",
78
- "screen.png",
79
70
  "README.md",
80
71
  "LICENSE"
81
72
  ],
@@ -84,6 +75,7 @@
84
75
  },
85
76
  "scripts": {
86
77
  "build": "node build.mjs",
78
+ "build:example": "node build.mjs && cd example && npm install && npm run build",
87
79
  "example": "cd example && npm install && npm run dev",
88
80
  "prepublishOnly": "node build.mjs"
89
81
  },
@@ -114,4 +106,3 @@
114
106
  "sideEffects": false,
115
107
  "license": "MIT"
116
108
  }
117
-
package/example/README.md DELETED
@@ -1,37 +0,0 @@
1
- # `@shulya/liquid-glass` — React Interactive Playground (`example/`)
2
-
3
- - **Live Demo:** [https://liquid.shulya.tech](https://liquid.shulya.tech)
4
- - **GitHub Repository:** [https://github.com/shulya-tech/liquid-glass](https://github.com/shulya-tech/liquid-glass)
5
-
6
- ![Liquid Glass React Playground](../screen.png)
7
-
8
- This directory contains a complete **React + Vite** application demonstrating the `<LiquidGlass>` component from `@shulya/liquid-glass/react`.
9
-
10
- ## Overview
11
-
12
- - **Three Draggable `<LiquidGlass>` Components:**
13
- - **Capsule Slider (`136 × 74 px`, radius `37 px`)** positioned over a green switch track.
14
- - **Rounded Rectangle (`196 × 116 px`, radius `22 px`)** positioned over an electric blue gradient card.
15
- - **Circular Lens (`112 × 112 px`, radius `56 px`)** positioned over a warm sunset sphere.
16
- - **High-Contrast Optical Stage:** Features a fine `56 px` architectural hairline grid, a thin diagonal rainbow ribbon, high-contrast black/white stripes, and centered `"Shulya"` typography to inspect edge refraction, chromatic dispersion, and specular bevel highlights.
17
- - **Liquid Surface Coalescence (`fusion`):** Drag any two or all three `<LiquidGlass>` elements close together to observe real-time liquid bridge coalescence powered by the precomputed empirical lookup tables (`_T0`–`_T4`), or toggle **Liquid Fusion** off in the bottom control shelf to inspect isolated lenses.
18
- - **Live React State Controls:**
19
- - **Bevel Width (`rim`):** `6–32 px` (default `16 px`)
20
- - **Refraction (`ior`):** `1.10–1.70` (default `1.46`)
21
- - **Thickness (`height`):** `6–34 px` (default `18 px`)
22
- - **Frosted Blur (`frosted`):** `0–30 px` (default `0 px`)
23
- - **Liquid Fusion (`fusion`):** `true / false` toggle
24
-
25
- ## Getting Started
26
-
27
- ```bash
28
- # 1. From the root of the repository, build the library dist bundles:
29
- npm run build
30
-
31
- # 2. Install example dependencies and start the Vite dev server:
32
- cd example
33
- npm install
34
- npm run dev
35
- ```
36
-
37
- Then open **`http://localhost:5173`** in your browser.
@@ -1,15 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
7
- <title>Liquid Glass UI — React Interactive Playground</title>
8
- </head>
9
-
10
- <body>
11
- <div id="root"></div>
12
- <script type="module" src="/src/main.jsx"></script>
13
- </body>
14
-
15
- </html>
@@ -1,27 +0,0 @@
1
- {
2
- "name": "liquid-glass-ui-react-example",
3
- "version": "1.0.0",
4
- "private": true,
5
- "description": "Interactive React playground for @shulya/liquid-glass (https://liquid.shulya.tech)",
6
- "homepage": "https://liquid.shulya.tech",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/shulya-tech/liquid-glass.git",
10
- "directory": "example"
11
- },
12
- "type": "module",
13
- "scripts": {
14
- "dev": "vite",
15
- "build": "vite build",
16
- "preview": "vite preview"
17
- },
18
- "dependencies": {
19
- "liquid-glass-ui": "file:..",
20
- "react": "^19.0.0",
21
- "react-dom": "^19.0.0"
22
- },
23
- "devDependencies": {
24
- "@vitejs/plugin-react": "^4.3.4",
25
- "vite": "^6.2.0"
26
- }
27
- }
@@ -1,353 +0,0 @@
1
- import React, { useEffect, useRef, useState, useCallback } from 'react';
2
- import {
3
- LiquidGlass,
4
- OPTICAL_DEFAULTS,
5
- scheduleStageRedraw
6
- } from '@shulya/liquid-glass/react';
7
-
8
- /**
9
- * Draws a crisp, high-DPI optical test stage onto the background canvas:
10
- * - Light architectural backdrop (#ebedf2) with fine 56px hairline grid
11
- * - Sleek green switch pill track (top-left)
12
- * - Thin diagonal rainbow gradient ribbon
13
- * - Compact electric blue card (right-center)
14
- * - Refined sunset radial sphere (bottom-left)
15
- * - Fine 8px optical stripe grating (bottom-right)
16
- * - Clean centered "Shulya" typography
17
- */
18
- function drawStageBackground(canvas) {
19
- if (!canvas) return;
20
- const w = window.innerWidth;
21
- const h = window.innerHeight;
22
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
23
-
24
- canvas.width = Math.floor(w * dpr);
25
- canvas.height = Math.floor(h * dpr);
26
-
27
- const ctx = canvas.getContext('2d');
28
- if (!ctx) return;
29
-
30
- ctx.save();
31
- ctx.scale(dpr, dpr);
32
-
33
- // 1. Clean Light Gray Backdrop (#ebedf2)
34
- ctx.fillStyle = '#ebedf2';
35
- ctx.fillRect(0, 0, w, h);
36
-
37
- // Fine architectural hairline grid (56px step, 0.5px crisp stroke)
38
- ctx.strokeStyle = 'rgba(0, 0, 0, 0.035)';
39
- ctx.lineWidth = 0.5;
40
- const step = 56;
41
- ctx.beginPath();
42
- for (let x = 0; x < w; x += step) {
43
- ctx.moveTo(x + 0.5, 0);
44
- ctx.lineTo(x + 0.5, h);
45
- }
46
- for (let y = 0; y < h; y += step) {
47
- ctx.moveTo(0, y + 0.5);
48
- ctx.lineTo(w, y + 0.5);
49
- }
50
- ctx.stroke();
51
-
52
- // 2. Sleek Green Switch Track (Top-Left)
53
- const pillX = w * 0.16;
54
- const pillY = h * 0.22;
55
- const pillW = 160;
56
- const pillH = 26;
57
- ctx.save();
58
- ctx.beginPath();
59
- ctx.roundRect(pillX - pillW / 2, pillY - pillH / 2, pillW, pillH, pillH / 2);
60
- ctx.fillStyle = '#32d74b';
61
- ctx.fill();
62
- ctx.restore();
63
-
64
- // 3. Thin Diagonal Rainbow Gradient Ribbon
65
- ctx.save();
66
- ctx.translate(w * 0.5, h * 0.51);
67
- ctx.rotate(-0.2);
68
- const ribHalfLen = Math.min(w * 0.28, 310);
69
- const ribThickness = 26;
70
- const ribGrad = ctx.createLinearGradient(-ribHalfLen, 0, ribHalfLen, 0);
71
- ribGrad.addColorStop(0.0, '#ff2d55');
72
- ribGrad.addColorStop(0.25, '#ff9500');
73
- ribGrad.addColorStop(0.5, '#ffcc00');
74
- ribGrad.addColorStop(0.75, '#5ac8fa');
75
- ribGrad.addColorStop(1.0, '#5856d6');
76
- ctx.fillStyle = ribGrad;
77
- ctx.beginPath();
78
- ctx.roundRect(
79
- -ribHalfLen,
80
- -ribThickness / 2,
81
- ribHalfLen * 2,
82
- ribThickness,
83
- ribThickness / 2
84
- );
85
- ctx.fill();
86
- ctx.restore();
87
-
88
- // 4. Refined Electric Blue Card (Right Center)
89
- ctx.save();
90
- const cardX = w * 0.67;
91
- const cardY = h * 0.24;
92
- const cardW = 152;
93
- const cardH = 88;
94
- const blueGrad = ctx.createLinearGradient(cardX, cardY, cardX + cardW, cardY + cardH);
95
- blueGrad.addColorStop(0, '#007aff');
96
- blueGrad.addColorStop(1, '#5856d6');
97
- ctx.fillStyle = blueGrad;
98
- ctx.beginPath();
99
- ctx.roundRect(cardX, cardY, cardW, cardH, 22);
100
- ctx.fill();
101
- ctx.restore();
102
-
103
- // 5. Compact Sunset Orange Sphere (Bottom Left)
104
- ctx.save();
105
- const orbX = w * 0.26;
106
- const orbY = h * 0.67;
107
- const orbR = 54;
108
- const orbGrad = ctx.createRadialGradient(orbX - 14, orbY - 14, 6, orbX, orbY, orbR);
109
- orbGrad.addColorStop(0, '#ffcc00');
110
- orbGrad.addColorStop(0.6, '#ff3b30');
111
- orbGrad.addColorStop(1, '#c91f66');
112
- ctx.fillStyle = orbGrad;
113
- ctx.beginPath();
114
- ctx.arc(orbX, orbY, orbR, 0, Math.PI * 2);
115
- ctx.fill();
116
- ctx.restore();
117
-
118
- // 6. Fine Optical Stripe Grating (Bottom Right — 10px stripes for refraction testing)
119
- ctx.save();
120
- const stX = w * 0.63;
121
- const stY = h * 0.62;
122
- const stW = 160;
123
- const stH = 92;
124
- const stripeWidth = 10;
125
- ctx.beginPath();
126
- ctx.roundRect(stX, stY, stW, stH, 18);
127
- ctx.clip();
128
- for (let i = 0; i < stW / stripeWidth; i++) {
129
- ctx.fillStyle = i % 2 === 0 ? '#1c1c1e' : '#ffffff';
130
- ctx.fillRect(stX + i * stripeWidth, stY, stripeWidth, stH);
131
- }
132
- ctx.restore();
133
-
134
- // 7. Sleek Center Typography ("Shulya")
135
- ctx.save();
136
- const fontSize = Math.min(w * 0.082, 92);
137
- ctx.font = `600 ${fontSize}px -apple-system, BlinkMacSystemFont, "SF Pro Display", "Inter", sans-serif`;
138
- ctx.textAlign = 'center';
139
- ctx.textBaseline = 'middle';
140
- ctx.fillStyle = '#16181d';
141
- ctx.fillText('Shulya', w * 0.5, h * 0.43);
142
- ctx.restore();
143
-
144
- ctx.restore();
145
- }
146
-
147
- export default function App() {
148
- const stageRef = useRef(null);
149
-
150
- // Live optical configuration parameters bound to <LiquidGlass> props
151
- const [rim, setRim] = useState(OPTICAL_DEFAULTS.rim);
152
- const [ior, setIor] = useState(OPTICAL_DEFAULTS.ior);
153
- const [height, setHeight] = useState(OPTICAL_DEFAULTS.height);
154
- const [frosted, setFrosted] = useState(OPTICAL_DEFAULTS.frosted);
155
- const [fusion, setFusion] = useState(OPTICAL_DEFAULTS.fusion);
156
- const [fps, setFps] = useState(60);
157
-
158
- // Three sleek draggable LiquidGlass bodies
159
- const [shapes, setShapes] = useState(() => {
160
- const w = typeof window !== 'undefined' ? window.innerWidth : 1280;
161
- const h = typeof window !== 'undefined' ? window.innerHeight : 800;
162
- return [
163
- { id: 'slider', x: w * 0.16 + 42, y: h * 0.22, w: 136, h: 74, r: 37 },
164
- { id: 'rect', x: w * 0.71, y: h * 0.21, w: 196, h: 116, r: 22 },
165
- { id: 'circle', x: w * 0.22, y: h * 0.65, w: 112, h: 112, r: 56 }
166
- ];
167
- });
168
-
169
- const dragState = useRef(null);
170
-
171
- // Draw stage canvas and handle window resize
172
- useEffect(() => {
173
- drawStageBackground(stageRef.current);
174
- scheduleStageRedraw();
175
-
176
- const handleResize = () => {
177
- drawStageBackground(stageRef.current);
178
- scheduleStageRedraw();
179
- };
180
-
181
- window.addEventListener('resize', handleResize);
182
- return () => window.removeEventListener('resize', handleResize);
183
- }, []);
184
-
185
- // FPS monitor
186
- useEffect(() => {
187
- let frameCount = 0;
188
- let lastTime = performance.now();
189
- let rafId;
190
-
191
- const tick = (now) => {
192
- frameCount++;
193
- if (now - lastTime >= 500) {
194
- setFps(Math.min(120, Math.round((frameCount * 1000) / (now - lastTime))));
195
- frameCount = 0;
196
- lastTime = now;
197
- }
198
- rafId = requestAnimationFrame(tick);
199
- };
200
-
201
- rafId = requestAnimationFrame(tick);
202
- return () => cancelAnimationFrame(rafId);
203
- }, []);
204
-
205
- // Pointer dragging handlers
206
- const handlePointerDown = useCallback((id, e) => {
207
- e.currentTarget.setPointerCapture(e.pointerId);
208
- setShapes((prev) => {
209
- const idx = prev.findIndex((s) => s.id === id);
210
- if (idx === -1) return prev;
211
- const target = prev[idx];
212
- dragState.current = {
213
- id,
214
- offX: e.clientX - target.x,
215
- offY: e.clientY - target.y
216
- };
217
- // Move active dragged shape to top of render order
218
- const next = [...prev];
219
- const [picked] = next.splice(idx, 1);
220
- next.push(picked);
221
- return next;
222
- });
223
- }, []);
224
-
225
- useEffect(() => {
226
- const handlePointerMove = (e) => {
227
- if (!dragState.current) return;
228
- const { id, offX, offY } = dragState.current;
229
- const nextX = e.clientX - offX;
230
- const nextY = e.clientY - offY;
231
-
232
- setShapes((prev) =>
233
- prev.map((s) => (s.id === id ? { ...s, x: nextX, y: nextY } : s))
234
- );
235
- scheduleStageRedraw();
236
- };
237
-
238
- const handlePointerUp = () => {
239
- dragState.current = null;
240
- };
241
-
242
- window.addEventListener('pointermove', handlePointerMove);
243
- window.addEventListener('pointerup', handlePointerUp);
244
- return () => {
245
- window.removeEventListener('pointermove', handlePointerMove);
246
- window.removeEventListener('pointerup', handlePointerUp);
247
- };
248
- }, []);
249
-
250
- return (
251
- <>
252
- {/* Background Optical Test Stage */}
253
- <canvas ref={stageRef} className="stage-canvas" />
254
-
255
- {/* Three Draggable <LiquidGlass> React Components */}
256
- {shapes.map((s, index) => (
257
- <LiquidGlass
258
- key={s.id}
259
- rim={rim}
260
- ior={ior}
261
- height={height}
262
- frosted={frosted}
263
- fusion={fusion}
264
- className="glass-body"
265
- data-lg-tint="rgba(255, 255, 255, 0.03)"
266
- onPointerDown={(e) => handlePointerDown(s.id, e)}
267
- style={{
268
- width: `${s.w}px`,
269
- height: `${s.h}px`,
270
- borderRadius: `${s.r}px`,
271
- left: `${Math.round(s.x - s.w * 0.5)}px`,
272
- top: `${Math.round(s.y - s.h * 0.5)}px`,
273
- zIndex: 20 + index
274
- }}
275
- />
276
- ))}
277
-
278
- {/* Bottom Interactive Control Shelf */}
279
- <div className="shelf-bar">
280
- <div className="ctrl">
281
- <div className="ctrl-head">
282
- <span>Bevel Width (Rim)</span>
283
- <span className="ctrl-val">{rim} px</span>
284
- </div>
285
- <input
286
- type="range"
287
- min="6"
288
- max="32"
289
- step="1"
290
- value={rim}
291
- onChange={(e) => setRim(Number(e.target.value))}
292
- />
293
- </div>
294
-
295
- <div className="ctrl">
296
- <div className="ctrl-head">
297
- <span>Refraction (IOR)</span>
298
- <span className="ctrl-val">{ior.toFixed(2)}</span>
299
- </div>
300
- <input
301
- type="range"
302
- min="1.10"
303
- max="1.70"
304
- step="0.02"
305
- value={ior}
306
- onChange={(e) => setIor(Number(e.target.value))}
307
- />
308
- </div>
309
-
310
- <div className="ctrl">
311
- <div className="ctrl-head">
312
- <span>Thickness (Height)</span>
313
- <span className="ctrl-val">{height} px</span>
314
- </div>
315
- <input
316
- type="range"
317
- min="6"
318
- max="34"
319
- step="1"
320
- value={height}
321
- onChange={(e) => setHeight(Number(e.target.value))}
322
- />
323
- </div>
324
-
325
- <div className="ctrl">
326
- <div className="ctrl-head">
327
- <span>Frosted Blur</span>
328
- <span className="ctrl-val">{frosted} px</span>
329
- </div>
330
- <input
331
- type="range"
332
- min="0"
333
- max="30"
334
- step="1"
335
- value={frosted}
336
- onChange={(e) => setFrosted(Number(e.target.value))}
337
- />
338
- </div>
339
-
340
- <label className="ctrl-toggle">
341
- <input
342
- type="checkbox"
343
- checked={fusion}
344
- onChange={(e) => setFusion(e.target.checked)}
345
- />
346
- <span>Liquid Fusion</span>
347
- </label>
348
-
349
- <div className="fps-tag">{fps} FPS</div>
350
- </div>
351
- </>
352
- );
353
- }
@@ -1,10 +0,0 @@
1
- import React from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import App from './App.jsx';
4
- import './styles.css';
5
-
6
- createRoot(document.getElementById('root')).render(
7
- <React.StrictMode>
8
- <App />
9
- </React.StrictMode>
10
- );