mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
This reverts commit 3b53bfd2f7.
This commit is contained in:
@@ -3,21 +3,40 @@ title: "Liquid Glass"
|
||||
description: "VFX composition block"
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/vfx-liquid-glass.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass preview" />
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-liquid-glass.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-liquid-glass.png" autoPlay muted loop playsInline />
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add vfx-liquid-glass" />
|
||||
```bash Terminal
|
||||
npx hyperframes add vfx-liquid-glass
|
||||
```
|
||||
|
||||
That writes one file: `compositions/vfx-liquid-glass.html`.
|
||||
|
||||
<Danger>
|
||||
<Warning>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
Without it this item's screen renders black. Rendering from the CLI switches
|
||||
it on for you. [How it works](/guides/html-in-canvas)
|
||||
</Danger>
|
||||
Rendering from the CLI switches it on for you. [How it
|
||||
works](/guides/html-in-canvas)
|
||||
</Warning>
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 20 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="vfx-liquid-glass"
|
||||
data-composition-src="compositions/vfx-liquid-glass.html"
|
||||
data-start="0"
|
||||
data-duration="20"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change the colors
|
||||
|
||||
@@ -26,775 +45,6 @@ Set these CSS variables on the block:
|
||||
- `--bg-color` — Background. Defaults to `#030407`.
|
||||
- `--accent-color` — Accent. Defaults to `#00d4ff`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`vfx-liquid-glass.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Liquid Glass Parallax</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/shaders/CopyShader.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/EffectComposer.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/RenderPass.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/ShaderPass.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #030407;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #030407;
|
||||
}
|
||||
|
||||
.text-source {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(160deg, #030407 0%, #0a1020 40%, #070910 70%, #030407 100%);
|
||||
text-align: center;
|
||||
padding: 0 120px;
|
||||
gap: 32px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.text-source::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 700px;
|
||||
height: 700px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(0, 212, 255, 0.06) 0%, transparent 70%);
|
||||
top: -150px;
|
||||
right: -100px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.text-source::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(124, 58, 237, 0.05) 0%, transparent 70%);
|
||||
bottom: -100px;
|
||||
left: -50px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.badge {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(0, 212, 255, 0.8);
|
||||
}
|
||||
.text-source h1 {
|
||||
font-size: 148px;
|
||||
font-weight: 900;
|
||||
line-height: 0.9;
|
||||
color: rgba(248, 249, 253, 0.96);
|
||||
text-shadow: 0 0 60px rgba(255, 255, 255, 0.15);
|
||||
letter-spacing: -5px;
|
||||
max-width: 1400px;
|
||||
}
|
||||
.text-source h1 span {
|
||||
background: linear-gradient(135deg, #00d4ff, #a855f7);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
max-width: 800px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 48px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
.stat-val {
|
||||
font-size: 42px;
|
||||
font-weight: 800;
|
||||
color: #00d4ff;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="liquid-glass"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="20"
|
||||
data-root="true"
|
||||
>
|
||||
<canvas
|
||||
id="cap-text"
|
||||
layoutsubtree
|
||||
width="1920"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
"
|
||||
>
|
||||
<div class="text-source">
|
||||
<div class="badge">Write HTML → Render Video</div>
|
||||
<h1>Ship videos <span>10x faster</span></h1>
|
||||
<div class="subtitle">
|
||||
HTML is the source of truth for video. No timeline editors, no After Effects — just
|
||||
code.
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-val">47x</div>
|
||||
<div class="stat-label">Faster than AE</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-val">12.4K</div>
|
||||
<div class="stat-label">Creators</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-val">2.4M</div>
|
||||
<div class="stat-label">Videos Rendered</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</canvas>
|
||||
|
||||
<canvas
|
||||
id="theater"
|
||||
width="1920"
|
||||
height="1080"
|
||||
style="position: absolute; top: 0; left: 0; width: 1920px; height: 1080px"
|
||||
></canvas>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="20"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var W = 1920,
|
||||
H = 1080,
|
||||
DURATION = 20,
|
||||
ASPECT = W / H;
|
||||
var TEXT_Z = -3.08;
|
||||
|
||||
// ── Seeded PRNG ────────────────────────────────────────────────────
|
||||
function seededRandom(seed) {
|
||||
var v = seed >>> 0;
|
||||
return function () {
|
||||
v += 0x6d2b79f5;
|
||||
var m = v;
|
||||
m = Math.imul(m ^ (m >>> 15), m | 1);
|
||||
m ^= m + Math.imul(m ^ (m >>> 7), m | 61);
|
||||
return ((m ^ (m >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Voronoi (self-contained, no d3) ────────────────────────────────
|
||||
function polygonArea(pts) {
|
||||
var a = 0;
|
||||
for (var i = 0; i < pts.length; i++) {
|
||||
var j = (i + 1) % pts.length;
|
||||
a += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
|
||||
}
|
||||
return a * 0.5;
|
||||
}
|
||||
|
||||
function polygonCentroid(pts) {
|
||||
var sa = polygonArea(pts) || 1;
|
||||
var cx = 0,
|
||||
cy = 0;
|
||||
for (var i = 0; i < pts.length; i++) {
|
||||
var j = (i + 1) % pts.length;
|
||||
var f = pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
|
||||
cx += (pts[i][0] + pts[j][0]) * f;
|
||||
cy += (pts[i][1] + pts[j][1]) * f;
|
||||
}
|
||||
return { x: cx / (6 * sa), y: cy / (6 * sa) };
|
||||
}
|
||||
|
||||
function clipPoly(poly, px, py, nx, ny) {
|
||||
var out = [];
|
||||
for (var i = 0; i < poly.length; i++) {
|
||||
var c = poly[i],
|
||||
n = poly[(i + 1) % poly.length];
|
||||
var dc = (c[0] - px) * nx + (c[1] - py) * ny;
|
||||
var dn = (n[0] - px) * nx + (n[1] - py) * ny;
|
||||
if (dc <= 0) out.push(c);
|
||||
if (dc <= 0 !== dn <= 0) {
|
||||
var t = dc / (dc - dn);
|
||||
out.push([c[0] + t * (n[0] - c[0]), c[1] + t * (n[1] - c[1])]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function computeVoronoi(points, xmin, ymin, xmax, ymax) {
|
||||
var cells = [];
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
var px = points[i][0],
|
||||
py = points[i][1];
|
||||
var poly = [
|
||||
[xmin, ymin],
|
||||
[xmax, ymin],
|
||||
[xmax, ymax],
|
||||
[xmin, ymax],
|
||||
];
|
||||
for (var j = 0; j < points.length; j++) {
|
||||
if (i === j) continue;
|
||||
var qx = points[j][0],
|
||||
qy = points[j][1];
|
||||
var mx = (px + qx) * 0.5,
|
||||
my = (py + qy) * 0.5;
|
||||
poly = clipPoly(poly, mx, my, qx - px, qy - py);
|
||||
if (poly.length < 3) break;
|
||||
}
|
||||
cells.push(poly.length >= 3 ? poly : null);
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
// ── Feature Detection ──────────────────────────────────────────────
|
||||
function canCapture() {
|
||||
var tc = document.createElement("canvas");
|
||||
if (!("layoutSubtree" in tc)) return false;
|
||||
tc.setAttribute("layoutsubtree", "");
|
||||
var ctx = tc.getContext("2d");
|
||||
return ctx && typeof ctx.drawElementImage === "function";
|
||||
}
|
||||
var apiOk = canCapture();
|
||||
|
||||
// ── Capture backdrop ───────────────────────────────────────────────
|
||||
var capCanvas = document.getElementById("cap-text");
|
||||
var capCtx = capCanvas.getContext("2d");
|
||||
var textEl = capCanvas.querySelector(".text-source");
|
||||
|
||||
function captureBackdrop() {
|
||||
capCtx.clearRect(0, 0, W, H);
|
||||
if (apiOk) {
|
||||
capCtx.drawElementImage(textEl, 0, 0, W, H);
|
||||
} else {
|
||||
var g = capCtx.createLinearGradient(0, 0, W, H);
|
||||
g.addColorStop(0, "#030407");
|
||||
g.addColorStop(0.5, "#070910");
|
||||
g.addColorStop(1, "#030407");
|
||||
capCtx.fillStyle = g;
|
||||
capCtx.fillRect(0, 0, W, H);
|
||||
capCtx.textAlign = "center";
|
||||
capCtx.textBaseline = "middle";
|
||||
capCtx.font = "900 172px Inter, system-ui, sans-serif";
|
||||
capCtx.shadowColor = "rgba(255,255,255,0.22)";
|
||||
capCtx.shadowBlur = 20;
|
||||
capCtx.fillStyle = "rgba(248,249,253,0.94)";
|
||||
capCtx.fillText("Liquid Glass", W / 2, H / 2 - 80);
|
||||
capCtx.fillText("Design", W / 2, H / 2 + 90);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Three.js Renderer ──────────────────────────────────────────────
|
||||
var theaterCanvas = document.getElementById("theater");
|
||||
var renderer = new THREE.WebGLRenderer({
|
||||
canvas: theaterCanvas,
|
||||
antialias: true,
|
||||
alpha: false,
|
||||
powerPreference: "high-performance",
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setSize(W, H, false);
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.outputEncoding = THREE.sRGBEncoding;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.08;
|
||||
renderer.setClearColor(0x030407, 1);
|
||||
|
||||
var scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x030407);
|
||||
|
||||
// ── Environment Map (procedural) ───────────────────────────────────
|
||||
var envCanvas = document.createElement("canvas");
|
||||
envCanvas.width = 1024;
|
||||
envCanvas.height = 512;
|
||||
var ectx = envCanvas.getContext("2d");
|
||||
var eg = ectx.createLinearGradient(0, 0, 1024, 512);
|
||||
eg.addColorStop(0, "#020307");
|
||||
eg.addColorStop(0.18, "#ffffff");
|
||||
eg.addColorStop(0.24, "#7fdcff");
|
||||
eg.addColorStop(0.42, "#07101c");
|
||||
eg.addColorStop(0.58, "#ffffff");
|
||||
eg.addColorStop(0.64, "#ffc4a6");
|
||||
eg.addColorStop(0.82, "#0b0c12");
|
||||
eg.addColorStop(1, "#020307");
|
||||
ectx.fillStyle = eg;
|
||||
ectx.fillRect(0, 0, 1024, 512);
|
||||
ectx.globalCompositeOperation = "screen";
|
||||
var envRng = seededRandom(999);
|
||||
for (var ei = 0; ei < 9; ei++) {
|
||||
var ey = 36 + ei * 52;
|
||||
ectx.fillStyle = "rgba(255,255,255," + (ei % 3 === 0 ? 0.22 : 0.1) + ")";
|
||||
ectx.fillRect((ei % 2) * 120, ey, 1024 * 0.72, 4 + (ei % 3) * 2);
|
||||
}
|
||||
var envTex = new THREE.CanvasTexture(envCanvas);
|
||||
envTex.mapping = THREE.EquirectangularReflectionMapping;
|
||||
var pmrem = new THREE.PMREMGenerator(renderer);
|
||||
var envMap = pmrem.fromEquirectangular(envTex);
|
||||
scene.environment = envMap.texture;
|
||||
envTex.dispose();
|
||||
pmrem.dispose();
|
||||
|
||||
// ── Camera ─────────────────────────────────────────────────────────
|
||||
var CAM_Z = 9.35;
|
||||
var camera = new THREE.PerspectiveCamera(34, ASPECT, 0.1, 80);
|
||||
camera.position.set(0, 0, CAM_Z);
|
||||
|
||||
// ── Scene Groups ───────────────────────────────────────────────────
|
||||
var rig = new THREE.Group();
|
||||
scene.add(rig);
|
||||
var textRig = new THREE.Group();
|
||||
rig.add(textRig);
|
||||
|
||||
// ── Backdrop Plane (text behind glass) ─────────────────────────────
|
||||
var backdropTex = new THREE.CanvasTexture(capCanvas);
|
||||
backdropTex.minFilter = THREE.LinearFilter;
|
||||
backdropTex.magFilter = THREE.LinearFilter;
|
||||
backdropTex.generateMipmaps = false;
|
||||
|
||||
function viewportAtZ(z) {
|
||||
var d = camera.position.z - z;
|
||||
var h = 2 * Math.tan(THREE.MathUtils.degToRad(camera.fov * 0.5)) * d;
|
||||
return { w: h * camera.aspect, h: h };
|
||||
}
|
||||
var vp = viewportAtZ(TEXT_Z);
|
||||
var backdropMesh = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(vp.w * 1.18, vp.h * 1.18),
|
||||
new THREE.MeshBasicMaterial({ map: backdropTex, depthWrite: true, depthTest: true }),
|
||||
);
|
||||
backdropMesh.position.z = TEXT_Z;
|
||||
backdropMesh.renderOrder = -4;
|
||||
textRig.add(backdropMesh);
|
||||
|
||||
// ── Light Bands ────────────────────────────────────────────────────
|
||||
var bandSpecs = [
|
||||
[-2.8, 1.45, -1.78, 3.2, 0.035, 0.13, 0x9edaff, 0.24],
|
||||
[2.7, -1.24, -1.76, 3.0, 0.032, -0.1, 0xffd1b5, 0.18],
|
||||
[0.0, 2.05, -1.82, 5.2, 0.026, 0.02, 0xffffff, 0.13],
|
||||
[-0.55, -1.92, -1.8, 4.2, 0.024, -0.03, 0xc7f6ff, 0.1],
|
||||
];
|
||||
bandSpecs.forEach(function (s) {
|
||||
var m = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(s[3], s[4]),
|
||||
new THREE.MeshBasicMaterial({
|
||||
color: s[6],
|
||||
transparent: true,
|
||||
opacity: s[7],
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
}),
|
||||
);
|
||||
m.position.set(s[0], s[1], s[2]);
|
||||
m.rotation.z = s[5];
|
||||
rig.add(m);
|
||||
});
|
||||
|
||||
// ── Glass Shards (Voronoi fracture) ────────────────────────────────
|
||||
var LAYOUT = { width: 9.7, height: 5.78, cols: 9, rows: 6, seed: 5297 };
|
||||
var rng = seededRandom(LAYOUT.seed);
|
||||
var seedPoints = [];
|
||||
var cw = LAYOUT.width / LAYOUT.cols,
|
||||
ch = LAYOUT.height / LAYOUT.rows;
|
||||
|
||||
for (var row = 0; row < LAYOUT.rows; row++) {
|
||||
for (var col = 0; col < LAYOUT.cols; col++) {
|
||||
var cp =
|
||||
1 -
|
||||
Math.min(
|
||||
0.62,
|
||||
Math.hypot((col + 0.5) / LAYOUT.cols - 0.5, (row + 0.5) / LAYOUT.rows - 0.5),
|
||||
);
|
||||
var jx = (rng() - 0.5) * cw * (0.58 + cp * 0.16);
|
||||
var jy = (rng() - 0.5) * ch * (0.58 + cp * 0.16);
|
||||
seedPoints.push([(col + 0.5) * cw + jx, (row + 0.5) * ch + jy]);
|
||||
}
|
||||
}
|
||||
|
||||
var voronoiCells = computeVoronoi(seedPoints, 0, 0, LAYOUT.width, LAYOUT.height);
|
||||
|
||||
var glassRig = new THREE.Group();
|
||||
glassRig.scale.setScalar(1.025);
|
||||
glassRig.position.set(0, 0, 0.08);
|
||||
rig.add(glassRig);
|
||||
|
||||
var panes = [];
|
||||
var GAP_SCALE = 0.954;
|
||||
|
||||
for (var pi = 0; pi < voronoiCells.length; pi++) {
|
||||
var cell = voronoiCells[pi];
|
||||
if (!cell || cell.length < 3) continue;
|
||||
|
||||
var worldPts = cell.map(function (p) {
|
||||
return [p[0] - LAYOUT.width / 2, LAYOUT.height / 2 - p[1]];
|
||||
});
|
||||
var area = Math.abs(polygonArea(worldPts));
|
||||
if (area < 0.034) continue;
|
||||
|
||||
var cent = polygonCentroid(worldPts);
|
||||
var rad = new THREE.Vector2(cent.x, cent.y);
|
||||
var radLen = Math.max(rad.length(), 0.001);
|
||||
rad.divideScalar(radLen);
|
||||
|
||||
var localPts = worldPts.map(function (p) {
|
||||
return [(p[0] - cent.x) * GAP_SCALE, (p[1] - cent.y) * GAP_SCALE];
|
||||
});
|
||||
var outward = 0.046 + rng() * 0.105;
|
||||
var depthLift = (area / (LAYOUT.width * LAYOUT.height)) * 1.42;
|
||||
var phase = pi * 0.71 + rng() * 4;
|
||||
var depth = 0.18 + rng() * 0.1;
|
||||
var bevel = Math.min(0.108, Math.sqrt(area) * 0.095);
|
||||
var roughness = pi % 7 === 0 ? 0.08 + rng() * 0.035 : 0.012 + rng() * 0.038;
|
||||
var thickness = 1.45 + rng() * 1.25;
|
||||
|
||||
// Geometry
|
||||
var shape = new THREE.Shape();
|
||||
localPts.forEach(function (p, idx) {
|
||||
idx === 0 ? shape.moveTo(p[0], p[1]) : shape.lineTo(p[0], p[1]);
|
||||
});
|
||||
shape.closePath();
|
||||
var geo = new THREE.ExtrudeGeometry(shape, {
|
||||
depth: depth,
|
||||
bevelEnabled: true,
|
||||
bevelThickness: bevel,
|
||||
bevelSize: bevel,
|
||||
bevelSegments: 9,
|
||||
curveSegments: 2,
|
||||
steps: 2,
|
||||
});
|
||||
geo.center();
|
||||
geo.computeVertexNormals();
|
||||
|
||||
// Material
|
||||
var mat = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xf7fbff,
|
||||
metalness: 0,
|
||||
roughness: roughness,
|
||||
transmission: 1,
|
||||
thickness: thickness,
|
||||
ior: 1.55,
|
||||
attenuationColor: new THREE.Color(0xe5f8ff),
|
||||
attenuationDistance: 12,
|
||||
clearcoat: 1,
|
||||
clearcoatRoughness: Math.min(0.12, roughness + 0.035),
|
||||
specularIntensity: 1,
|
||||
envMapIntensity: 2.1,
|
||||
transparent: true,
|
||||
opacity: 0.76,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
|
||||
var pane = new THREE.Group();
|
||||
var mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.renderOrder = 4 + pi;
|
||||
pane.add(mesh);
|
||||
|
||||
// Edge highlights (white + cyan + pink for chromatic aberration look)
|
||||
var edgeGeo = new THREE.EdgesGeometry(geo, 18);
|
||||
var whiteEdge = new THREE.LineSegments(
|
||||
edgeGeo,
|
||||
new THREE.LineBasicMaterial({
|
||||
color: 0xffffff,
|
||||
transparent: true,
|
||||
opacity: roughness > 0.08 ? 0.32 : 0.44,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
}),
|
||||
);
|
||||
pane.add(whiteEdge);
|
||||
var cyanEdge = new THREE.LineSegments(
|
||||
edgeGeo.clone(),
|
||||
new THREE.LineBasicMaterial({
|
||||
color: 0x72e8ff,
|
||||
transparent: true,
|
||||
opacity: 0.18,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
}),
|
||||
);
|
||||
cyanEdge.position.set(0.008, -0.002, -0.008);
|
||||
pane.add(cyanEdge);
|
||||
var pinkEdge = new THREE.LineSegments(
|
||||
edgeGeo.clone(),
|
||||
new THREE.LineBasicMaterial({
|
||||
color: 0xff6f8d,
|
||||
transparent: true,
|
||||
opacity: 0.14,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
}),
|
||||
);
|
||||
pinkEdge.position.set(-0.009, 0.003, 0.006);
|
||||
pane.add(pinkEdge);
|
||||
|
||||
var basePos = new THREE.Vector3(
|
||||
cent.x + rad.x * outward,
|
||||
cent.y + rad.y * outward,
|
||||
0.28 + depthLift + rng() * 0.18,
|
||||
);
|
||||
var baseRot = new THREE.Euler(
|
||||
(rng() - 0.5) * 0.074,
|
||||
(rng() - 0.5) * 0.086,
|
||||
(rng() - 0.5) * 0.052,
|
||||
);
|
||||
pane.position.copy(basePos);
|
||||
pane.rotation.copy(baseRot);
|
||||
|
||||
pane.userData = {
|
||||
basePos: basePos.clone(),
|
||||
baseRot: { x: baseRot.x, y: baseRot.y, z: baseRot.z },
|
||||
phase: phase,
|
||||
speed: 0.28 + rng() * 0.16,
|
||||
drift: new THREE.Vector3(
|
||||
0.02 + rng() * 0.026,
|
||||
0.018 + rng() * 0.024,
|
||||
0.068 + rng() * 0.092,
|
||||
),
|
||||
rotDrift: new THREE.Vector3(
|
||||
0.016 + rng() * 0.026,
|
||||
0.018 + rng() * 0.03,
|
||||
0.008 + rng() * 0.016,
|
||||
),
|
||||
ptrWeight: 0.04 + rng() * 0.052,
|
||||
rad: rad.clone(),
|
||||
baseOpacity: 0.76,
|
||||
baseRoughness: roughness,
|
||||
glassMat: mat,
|
||||
edgeW: whiteEdge.material,
|
||||
edgeC: cyanEdge.material,
|
||||
edgeP: pinkEdge.material,
|
||||
edgeWBase: whiteEdge.material.opacity,
|
||||
edgeCBase: cyanEdge.material.opacity,
|
||||
edgePBase: pinkEdge.material.opacity,
|
||||
};
|
||||
|
||||
panes.push(pane);
|
||||
glassRig.add(pane);
|
||||
}
|
||||
|
||||
// ── Lighting ───────────────────────────────────────────────────────
|
||||
scene.add(new THREE.HemisphereLight(0xe8f0ff, 0x07040b, 1.1));
|
||||
var keyLight = new THREE.DirectionalLight(0xffffff, 3.4);
|
||||
keyLight.position.set(-3.8, 4.4, 5.6);
|
||||
scene.add(keyLight);
|
||||
var edgeLight = new THREE.PointLight(0x8bdcff, 14, 12);
|
||||
edgeLight.position.set(3.1, 1.4, 3.4);
|
||||
scene.add(edgeLight);
|
||||
var warmLight = new THREE.PointLight(0xffc8a8, 7, 12);
|
||||
warmLight.position.set(-4.4, -2.2, 3.2);
|
||||
scene.add(warmLight);
|
||||
|
||||
// ── Post-Processing ────────────────────────────────────────────────
|
||||
var GrainShader = {
|
||||
uniforms: { tDiffuse: { value: null }, time: { value: 0 }, strength: { value: 0.024 } },
|
||||
vertexShader:
|
||||
"varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }",
|
||||
fragmentShader: [
|
||||
"uniform sampler2D tDiffuse; uniform float time; uniform float strength; varying vec2 vUv;",
|
||||
"float n(vec2 v){ return fract(sin(dot(v+time,vec2(12.9898,78.233)))*43758.5453); }",
|
||||
"void main(){",
|
||||
" vec4 c=texture2D(tDiffuse,vUv);",
|
||||
" c.rgb+=(n(vUv*vec2(1520.0,940.0))-0.5)*strength;",
|
||||
" c.rgb=pow(max(c.rgb,vec3(0.0)),vec3(0.985));",
|
||||
" gl_FragColor=c;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
};
|
||||
|
||||
var composer = new THREE.EffectComposer(renderer);
|
||||
composer.addPass(new THREE.RenderPass(scene, camera));
|
||||
var bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(W, H), 0.32, 0.4, 0.86);
|
||||
composer.addPass(bloomPass);
|
||||
var grainPass = new THREE.ShaderPass(GrainShader);
|
||||
composer.addPass(grainPass);
|
||||
|
||||
// ── Pointer State (GSAP-animated, replaces mouse) ──────────────────
|
||||
var ptr = { x: 0.16, y: 0.04 };
|
||||
var REVEAL_R = 2.18;
|
||||
var PTR_WORLD = new THREE.Vector2();
|
||||
|
||||
function updatePtrWorld() {
|
||||
PTR_WORLD.set(ptr.x * 4.86, ptr.y * 2.84);
|
||||
}
|
||||
|
||||
// ── Render Function (deterministic, seekable) ──────────────────────
|
||||
function renderScene(elapsed) {
|
||||
updatePtrWorld();
|
||||
|
||||
camera.position.x = ptr.x * 0.38;
|
||||
camera.position.y = -ptr.y * 0.27;
|
||||
camera.position.z = CAM_Z;
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
textRig.position.x = -ptr.x * 0.56;
|
||||
textRig.position.y = ptr.y * 0.36;
|
||||
textRig.position.z = Math.sin(elapsed * 0.28) * 0.018;
|
||||
textRig.rotation.x = ptr.y * 0.028;
|
||||
textRig.rotation.y = ptr.x * 0.042;
|
||||
|
||||
glassRig.rotation.x = -ptr.y * 0.064;
|
||||
glassRig.rotation.y = ptr.x * 0.078;
|
||||
|
||||
edgeLight.position.x = 3.1 + ptr.x * 0.9;
|
||||
edgeLight.position.y = 1.4 + ptr.y * 0.55;
|
||||
warmLight.position.x = -4.4 - ptr.x * 0.65;
|
||||
|
||||
for (var i = 0; i < panes.length; i++) {
|
||||
var pane = panes[i];
|
||||
var d = pane.userData;
|
||||
var t = elapsed * d.speed + d.phase;
|
||||
var ptrDist = PTR_WORLD.distanceTo(new THREE.Vector2(d.basePos.x, d.basePos.y));
|
||||
var reveal = Math.pow(Math.max(0, 1 - ptrDist / REVEAL_R), 2);
|
||||
var shatter = reveal * 0.36;
|
||||
var awayX = d.basePos.x - PTR_WORLD.x;
|
||||
var awayY = d.basePos.y - PTR_WORLD.y;
|
||||
var awayLen = Math.max(Math.hypot(awayX, awayY), 0.001);
|
||||
var revealSpread = reveal * 0.24;
|
||||
var shatterSpread = shatter * 0.68;
|
||||
var pSlide = d.ptrWeight * 1.9;
|
||||
|
||||
pane.position.x =
|
||||
d.basePos.x +
|
||||
Math.sin(t * 0.42) * d.drift.x +
|
||||
ptr.x * pSlide +
|
||||
(awayX / awayLen) * (revealSpread + shatterSpread);
|
||||
pane.position.y =
|
||||
d.basePos.y +
|
||||
Math.cos(t * 0.38) * d.drift.y -
|
||||
ptr.y * pSlide * 0.72 +
|
||||
(awayY / awayLen) * (revealSpread + shatterSpread * 0.84);
|
||||
pane.position.z =
|
||||
d.basePos.z + Math.sin(t * 0.52 + i) * d.drift.z + reveal * 0.32 + shatter * 0.74;
|
||||
|
||||
pane.rotation.x =
|
||||
d.baseRot.x +
|
||||
Math.sin(t * 0.46) * d.rotDrift.x -
|
||||
ptr.y * d.ptrWeight * 1.15 +
|
||||
reveal * (awayY / awayLen) * 0.16 +
|
||||
shatter * (awayY / awayLen) * 0.58;
|
||||
pane.rotation.y =
|
||||
d.baseRot.y +
|
||||
Math.cos(t * 0.43) * d.rotDrift.y +
|
||||
ptr.x * d.ptrWeight * 1.38 -
|
||||
reveal * (awayX / awayLen) * 0.2 -
|
||||
shatter * (awayX / awayLen) * 0.66;
|
||||
pane.rotation.z =
|
||||
d.baseRot.z +
|
||||
Math.sin(t * 0.34) * d.rotDrift.z +
|
||||
reveal * Math.sin(t + i) * 0.028 +
|
||||
shatter * (d.rad.x * awayY - d.rad.y * awayX) * 0.18;
|
||||
|
||||
d.glassMat.opacity = d.baseOpacity + shatter * 0.14;
|
||||
d.glassMat.roughness = Math.min(0.14, d.baseRoughness + shatter * 0.048);
|
||||
d.edgeW.opacity = d.edgeWBase + shatter * 0.34;
|
||||
d.edgeC.opacity = d.edgeCBase + shatter * 0.22;
|
||||
d.edgeP.opacity = d.edgePBase + shatter * 0.2;
|
||||
}
|
||||
|
||||
grainPass.uniforms.time.value = elapsed;
|
||||
composer.render();
|
||||
}
|
||||
|
||||
// ── GSAP Timeline ──────────────────────────────────────────────────
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to({ _: 0 }, { _: 1, duration: DURATION, ease: "none" }, 0);
|
||||
|
||||
// Pointer choreography — smooth wandering reveal
|
||||
tl.set(ptr, { x: 0.16, y: 0.04 }, 0);
|
||||
tl.to(ptr, { x: 0.45, y: -0.15, duration: 3.5, ease: "sine.inOut" }, 0.5);
|
||||
tl.to(ptr, { x: -0.35, y: 0.25, duration: 3.5, ease: "sine.inOut" }, 4);
|
||||
tl.to(ptr, { x: 0.15, y: -0.35, duration: 3, ease: "sine.inOut" }, 7.5);
|
||||
tl.to(ptr, { x: -0.45, y: 0.05, duration: 3, ease: "sine.inOut" }, 10.5);
|
||||
tl.to(ptr, { x: 0.3, y: 0.2, duration: 3, ease: "sine.inOut" }, 13.5);
|
||||
tl.to(ptr, { x: -0.1, y: -0.1, duration: 2.5, ease: "sine.inOut" }, 16.5);
|
||||
tl.to(ptr, { x: 0.16, y: 0.04, duration: 1, ease: "power2.inOut" }, 19);
|
||||
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
renderScene(tl.time());
|
||||
});
|
||||
|
||||
window.__timelines["liquid-glass"] = tl;
|
||||
|
||||
setTimeout(function () {
|
||||
captureBackdrop();
|
||||
backdropTex.needsUpdate = true;
|
||||
tl.seek(0);
|
||||
}, 0);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="vfx-liquid-glass" data-composition-src="compositions/vfx-liquid-glass.html" data-start="0" data-duration="20" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `html-in-canvas` `webgl`.
|
||||
|
||||
Reference in New Issue
Block a user