@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 +5 -3
- package/dist/core.cjs +17 -10
- package/dist/core.mjs +17 -10
- package/package.json +2 -11
- package/example/README.md +0 -37
- package/example/index.html +0 -15
- package/example/package.json +0 -27
- package/example/src/App.jsx +0 -353
- package/example/src/main.jsx +0 -10
- package/example/src/styles.css +0 -155
- package/example/vite.config.js +0 -22
- package/screen.png +0 -0
- package/src/angular.js +0 -76
- package/src/core.js +0 -1119
- package/src/index.js +0 -15
- package/src/react.js +0 -106
- package/src/solid.js +0 -80
- package/src/svelte.js +0 -27
- package/src/tables.js +0 -14
- package/src/vue.js +0 -92
- package/tables/optical_calibration_matrix.csv +0 -257
- package/tables/optical_calibration_matrix.xlsx +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@shulya/liquid-glass`
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@shulya/liquid-glass)
|
|
4
4
|
[](https://liquid.shulya.tech)
|
|
5
5
|
[](https://github.com/shulya-tech/liquid-glass)
|
|
6
6
|
[-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
|
-

|
|
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
|
-

|
|
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
|
|
453
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
700
|
-
const
|
|
701
|
-
const
|
|
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 (
|
|
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
|
|
453
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
700
|
-
const
|
|
701
|
-
const
|
|
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 (
|
|
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.
|
|
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
|
-

|
|
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.
|
package/example/index.html
DELETED
|
@@ -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>
|
package/example/package.json
DELETED
|
@@ -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
|
-
}
|
package/example/src/App.jsx
DELETED
|
@@ -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
|
-
}
|
package/example/src/main.jsx
DELETED