类型:3D · 分类:Backgrounds · 收录:2026-09-16 20:35:01 · 源码:会员可见
# Portal Field
Five ambient field backgrounds collected across Three.js, raw WebGL, and Canvas 2D renderers.
> 来源:ThreeUI Community · https://threeui.com · MIT License(Meng To / Design+Code)
> 组件页:https://threeui.com/component/portal-field · 预览动画:https://threeui.com/previews/portal-field.webm
> 分类:Backgrounds
> 包含变体(4):cloud-field、flow-field、bell-field、stream-convergence
/* ===== src/shaders/portal-field/PortalFieldCollection.tsx (tsx) ===== */
import { lazy, Suspense } from "react";
import type { BellFieldBackgroundProps } from "../bell-field/BellFieldBackground";
import type { NeuformBatchEffectProps } from "../neuform-isolated/NeuformBatchEffects";
import type { NeuformIsolatedEffectProps } from "../neuform-isolated/NeuformIsolatedEffects";
import type { StreamConvergenceBackgroundProps } from "../stream-convergence/StreamConvergenceBackground";
export type PortalFieldVariant =
| "portal-field"
| "flow-field"
| "cloud-field"
| "bell-field"
| "stream-convergence";
type PortalVariantProps = NeuformBatchEffectProps & {
variant?: "portal-field";
};
type FlowVariantProps = NeuformBatchEffectProps & {
variant: "flow-field";
};
type CloudVariantProps = NeuformIsolatedEffectProps & {
variant: "cloud-field";
};
type BellVariantProps = BellFieldBackgroundProps & {
variant: "bell-field";
};
type StreamVariantProps = StreamConvergenceBackgroundProps & {
variant: "stream-convergence";
};
export type PortalFieldCollectionProps =
| PortalVariantProps
| FlowVariantProps
| CloudVariantProps
| BellVariantProps
| StreamVariantProps;
const PortalVariant = lazy(() =>
import("../neuform-isolated/NeuformBatchEffects").then((module) => ({ default: module.PortalField })),
);
const FlowVariant = lazy(() =>
import("../neuform-isolated/NeuformBatchEffects").then((module) => ({ default: module.FlowField })),
);
const CloudVariant = lazy(() =>
import("../neuform-isolated/NeuformIsolatedEffects").then((module) => ({ default: module.CloudField })),
);
const BellVariant = lazy(() =>
import("../bell-field/BellFieldBackground").then((module) => ({ default: module.BellFieldBackground })),
);
const StreamVariant = lazy(() =>
import("../stream-convergence/StreamConvergenceBackground").then((module) => ({ default: module.StreamConvergenceBackground })),
);
const FALLBACK = <div className="threeui-background portal-field" />;
export function PortalFieldCollection(props: PortalFieldCollectionProps) {
if (props.variant === "flow-field") {
const { variant: _variant, ...variantProps } = props;
return <Suspense fallback={FALLBACK}><FlowVariant {...variantProps} /></Suspense>;
}
if (props.variant === "cloud-field") {
const { variant: _variant, ...variantProps } = props;
return <Suspense fallback={FALLBACK}><CloudVariant {...variantProps} /></Suspense>;
}
if (props.variant === "bell-field") {
const { variant: _variant, ...variantProps } = props;
return <Suspense fallback={FALLBACK}><BellVariant {...variantProps} /></Suspense>;
}
if (props.variant === "stream-convergence") {
const { variant: _variant, ...variantProps } = props;
return <Suspense fallback={FALLBACK}><StreamVariant {...variantProps} /></Suspense>;
}
const { variant: _variant, ...variantProps } = props;
return <Suspense fallback={FALLBACK}><PortalVariant {...variantProps} /></Suspense>;
}
/* ===== src/shaders/neuform-isolated/sources/portal-field.html (html) ===== */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AEON // Portal</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400&family=JetBrains+Mono:wght@300;400&display=swap" rel="stylesheet">
</head>
<body style="
--bg-base: #020202;
--bg-surface-1: rgba(255, 255, 255, 0.015);
--bg-surface-2: rgba(255, 255, 255, 0.04);
--bg-surface-3: rgba(255, 255, 255, 0.08);
--text-primary: #F4F4F5;
--text-secondary: #A3A3A3;
--text-accent: #020202;
--text-tech: #666666;
--bg-accent: #F4F4F5;
--bg-accent-active: #22C55E;
--border-glow: rgba(255, 255, 255, 0.08);
--glow-color: 255, 255, 255;
background-color: var(--bg-base);
color: var(--text-primary);
transition: background-color 0.8s cubic-bezier(0.16, 1, 0.3, 1), color 0.8s cubic-bezier(0.16, 1, 0.3, 1);
" class="font-['Inter'] min-h-screen overflow-hidden flex items-center justify-center antialiased selection:bg-[var(--bg-surface-3)] selection:text-[var(--text-primary)]">
<!-- Ambient Background -->
<div id="webgl-container" class="absolute inset-0 z-0 pointer-events-none opacity-80 mix-blend-screen"></div>
<!-- Structural Framing -->
<div class="fixed inset-4 lg:inset-8 pointer-events-none z-0 border border-[var(--border-glow)] hidden lg:block transition-colors duration-700">
<!-- Corner Registration Marks -->
<div class="absolute -top-[1px] -left-[1px] w-3 h-3 border-t border-l border-[var(--text-secondary)] transition-colors duration-500"></div>
<div class="absolute -top-[1px] -right-[1px] w-3 h-3 border-t border-r border-[var(--text-secondary)] transition-colors duration-500"></div>
<div class="absolute -bottom-[1px] -left-[1px] w-3 h-3 border-b border-l border-[var(--text-secondary)] transition-colors duration-500"></div>
<div class="absolute -bottom-[1px] -right-[1px] w-3 h-3 border-b border-r border-[var(--text-secondary)] transition-colors duration-500"></div>
<!-- Precision Grid Lines -->
<div class="absolute top-0 bottom-0 left-1/3 w-[1px] bg-gradient-to-b from-transparent via-[var(--border-glow)] to-transparent opacity-20"></div>
<div class="absolute top-1/2 left-0 right-0 h-[1px] bg-gradient-to-r from-transparent via-[var(--border-glow)] to-transparent opacity-10"></div>
</div>
<!-- Interface Controls -->
<div class="absolute top-6 lg:top-12 right-6 lg:right-12 z-20 flex items-center gap-6">
<div class="font-['JetBrains_Mono'] text-xs text-[var(--text-tech)] tracking-widest uppercase transition-colors duration-500 hidden sm:block">SYS-2.9</div>
<button id="themeToggleBtn" class="h-8 px-4 border border-[var(--border-glow)] bg-[var(--bg-surface-1)] hover:bg-[var(--bg-surface-2)] text-[var(--text-primary)] backdrop-blur-md flex items-center gap-2 transition-all duration-300 text-xs font-light tracking-wide rounded-sm">
<iconify-icon id="themeIcon" icon="solar:moon-linear" stroke-width="1.5" class="text-sm"></iconify-icon>
<span id="themeText">DARK</span>
</button>
</div>
<!-- Metadata Labels -->
<div class="absolute top-12 left-8 lg:top-16 lg:left-16 z-10 font-['JetBrains_Mono'] text-xs tracking-widest text-[var(--text-tech)] uppercase transition-colors duration-500 hidden sm:block">
NEURAL.NET // ACTIVE
</div>
<div style="transform: translateY(-50%) rotate(90deg); transform-origin: center right;" class="absolute top-1/2 right-8 lg:right-12 z-10 font-['JetBrains_Mono'] text-xs tracking-widest text-[var(--text-tech)] uppercase flex items-center gap-4 transition-colors duration-500 hidden xl:flex">
<span class="opacity-40">[</span>
<span>PRT</span>
<span class="opacity-40">=</span>
<span>ECHO-9</span>
<span class="opacity-40">]</span>
</div>
<!-- Remixed Layout: Typographic Hero + Interactive Section -->
<div class="relative z-10 w-full max-w-[1440px] min-h-screen flex flex-col lg:flex-row items-center justify-center lg:justify-between px-6 lg:px-24 py-24 gap-16 lg:gap-8">
<!-- Hero Section -->
<div class="w-full lg:w-1/2 flex flex-col gap-6 lg:pr-12">
<div class="font-['JetBrains_Mono'] text-xs text-[var(--text-secondary)] tracking-widest uppercase flex items-center gap-3">
<div class="w-1.5 h-1.5 rounded-full bg-[var(--bg-accent-active)] animate-pulse"></div>
<span class="block overflow-hidden"><span class="block masked-word translate-y-full opacity-0">Authorization Pending</span></span>
</div>
<h1 class="text-5xl sm:text-6xl lg:text-7xl xl:text-8xl tracking-tighter font-extralight leading-[0.95] text-[var(--text-primary)] uppercase">
<span class="block overflow-hidden pb-2"><span class="block masked-word translate-y-full opacity-0">Establish</span></span>
<span class="block overflow-hidden pb-2"><span class="block masked-word translate-y-full opacity-0">Connection</span></span>
</h1>
<p class="text-sm lg:text-base font-extralight text-[var(--text-secondary)] leading-relaxed max-w-md mt-4">
<span class="block overflow-hidden"><span class="block masked-word translate-y-full opacity-0">Restricted network sector. Verify your identity via biometric uplink to access the centralized mainframe and quantum datasets.</span></span>
</p>
</div>
<!-- Second Section (Interactive Card) -->
<div class="w-full max-w-[420px] lg:w-1/2 flex justify-center lg:justify-end">
<!-- Structural Shell with Gradient Border -->
<div class="relative w-full rounded-sm p-[1px] bg-gradient-to-br from-[var(--border-glow)] via-transparent to-[var(--border-glow)] group">
<div class="w-full rounded-sm bg-[var(--bg-surface-1)] backdrop-blur-2xl p-8 lg:p-10 flex flex-col gap-8 transition-colors duration-500 shadow-2xl shadow-black/40">
<!-- Card Header -->
<div class="flex flex-col gap-2">
<div class="font-['JetBrains_Mono'] text-xs text-[var(--text-primary)] border border-[var(--border-glow)] bg-[var(--bg-surface-2)] px-3 py-1 self-start mb-2 tracking-widest uppercase rounded-sm transition-colors duration-500">IDENTITY.CHECK</div>
<h2 class="text-2xl tracking-tight font-light text-[var(--text-primary)]">
<span class="block overflow-hidden"><span class="block masked-word translate-y-full opacity-0">Authentication</span></span>
</h2>
</div>
<!-- Alternate Methods Grid -->
<div class="grid grid-cols-3 gap-3">
<button class="h-12 rounded-sm bg-[var(--bg-surface-2)] hover:bg-[var(--text-primary)] text-[var(--text-primary)] hover:text-[var(--text-accent)] flex items-center justify-center transition-all duration-300 border border-[var(--border-glow)] group/btn" aria-label="Biometric Scan">
<iconify-icon icon="solar:fingerprint-linear" stroke-width="1.5" class="text-xl transition-transform duration-300 group-hover/btn:scale-110"></iconify-icon>
</button>
<button class="h-12 rounded-sm bg-[var(--bg-surface-2)] hover:bg-[var(--text-primary)] text-[var(--text-primary)] hover:text-[var(--text-accent)] flex items-center justify-center transition-all duration-300 border border-[var(--border-glow)] group/btn" aria-label="Hardware Token">
<iconify-icon icon="solar:smartphone-linear" stroke-width="1.5" class="text-xl transition-transform duration-300 group-hover/btn:scale-110"></iconify-icon>
</button>
<button class="h-12 rounded-sm bg-[var(--bg-surface-2)] hover:bg-[var(--text-primary)] text-[var(--text-primary)] hover:text-[var(--text-accent)] flex items-center justify-center transition-all duration-300 border border-[var(--border-glow)] group/btn" aria-label="Neural Passkey">
<iconify-icon icon="solar:shield-keyhole-linear" stroke-width="1.5" class="text-xl transition-transform duration-300 group-hover/btn:scale-110"></iconify-icon>
</button>
</div>
<!-- Divider -->
<div class="flex items-center gap-4 text-[var(--text-tech)] font-['JetBrains_Mono'] text-xs tracking-widest uppercase">
<div class="flex-1 h-[1px] bg-[var(--border-glow)] transition-colors duration-500"></div>
<span>FALLBACK PROTOCOL</span>
<div class="flex-1 h-[1px] bg-[var(--border-glow)] transition-colors duration-500"></div>
</div>
<!-- Input Fields -->
<div class="flex flex-col gap-5">
<div class="relative flex flex-col gap-2">
<label class="font-['JetBrains_Mono'] text-xs font-light text-[var(--text-secondary)] uppercase tracking-widest transition-colors duration-500">AGENT_ALIAS</label>
<input type="text" placeholder="agent.name@aeon.net" autocomplete="username" class="w-full px-4 h-12 border border-[var(--border-glow)] outline-none rounded-sm bg-[var(--bg-surface-1)] text-[var(--text-primary)] font-['Inter'] text-sm transition-all duration-300 focus:bg-[var(--bg-surface-2)] focus:border-[var(--text-secondary)] placeholder-[var(--text-tech)] font-light">
</div>
<div class="relative flex flex-col gap-2">
<label class="font-['JetBrains_Mono'] text-xs font-light text-[var(--text-secondary)] uppercase tracking-widest transition-colors duration-500">ACCESS_CODE</label>
<input type="password" placeholder="••••••••••••" autocomplete="current-password" class="w-full px-4 h-12 border border-[var(--border-glow)] outline-none rounded-sm bg-[var(--bg-surface-1)] text-[var(--text-primary)] font-['Inter'] text-sm transition-all duration-300 focus:bg-[var(--bg-surface-2)] focus:border-[var(--text-secondary)] placeholder-[var(--text-tech)] font-light">
</div>
</div>
<!-- Primary Action -->
<button class="h-14 mt-4 rounded-sm bg-[var(--bg-accent)] text-[var(--text-accent)] font-medium text-xs tracking-widest uppercase flex items-center justify-center gap-3 transition-all duration-500 hover:bg-[var(--bg-accent-active)] hover:text-white group/submit relative overflow-hidden">
<span class="relative z-10 flex items-center gap-2">
INITIATE LINK
<iconify-icon icon="solar:arrow-right-linear" stroke-width="1.5" class="text-base transition-transform duration-300 group-hover/submit:translate-x-1"></iconify-icon>
</span>
</button>
</div>
</div>
</div>
</div>
<script>
// GSAP Masked Reveal Directive
document.addEventListener("DOMContentLoaded", () => {
gsap.registerPlugin(ScrollTrigger);
gsap.to(".masked-word", {
y: "0%",
opacity: 1,
duration: 1.2,
ease: "power4.out",
stagger: 0.1,
delay: 0.1
});
});
// Theme Toggling Logic
const body = document.body;
const themeBtn = document.getElementById('themeToggleBtn');
const themeIcon = document.getElementById('themeIcon');
const themeText = document.getElementById('themeText');
let isDark = true;
const themes = {
dark: {
'--bg-base': '#020202',
'--bg-surface-1': 'rgba(255, 255, 255, 0.015)',
'--bg-surface-2': 'rgba(255, 255, 255, 0.04)',
'--bg-surface-3': 'rgba(255, 255, 255, 0.08)',
'--text-primary': '#F4F4F5',
'--text-secondary': '#A3A3A3',
'--text-accent': '#020202',
'--text-tech': '#666666',
'--bg-accent': '#F4F4F5',
'--bg-accent-active': '#22C55E',
'--border-glow': 'rgba(255, 255, 255, 0.08)',
'--glow-color': '255, 255, 255',
shaderCore: '#FFFFFF',
shaderFringe: '#1e3a8a'
},
light: {
'--bg-base': '#F4F4F5',
'--bg-surface-1': 'rgba(0, 0, 0, 0.02)',
'--bg-surface-2': 'rgba(0, 0, 0, 0.05)',
'--bg-surface-3': 'rgba(0, 0, 0, 0.1)',
'--text-primary': '#020202',
'--text-secondary': '#666666',
'--text-accent': '#FFFFFF',
'--text-tech': '#A3A3A3',
'--bg-accent': '#020202',
'--bg-accent-active': '#16A34A',
'--border-glow': 'rgba(0, 0, 0, 0.08)',
'--glow-color': '0, 0, 0',
shaderCore: '#020202',
shaderFringe: '#94a3b8'
}
};
themeBtn.addEventListener('click', () => {
isDark = !isDark;
const currentTheme = isDark ? themes.dark : themes.light;
for (const [key, value] of Object.entries(currentTheme)) {
if (key.startsWith('--')) {
body.style.setProperty(key, value);
}
}
if (isDark) {
themeIcon.setAttribute('icon', 'solar:moon-linear');
themeText.textContent = 'DARK';
} else {
themeIcon.setAttribute('icon', 'solar:sun-linear');
themeText.textContent = 'LIGHT';
}
if (typeof material !== 'undefined') {
material.uniforms.u_colorCore.value = new THREE.Color(currentTheme.shaderCore);
material.uniforms.u_colorFringe.value = new THREE.Color(currentTheme.shaderFringe);
material.uniforms.u_isLightMode.value = isDark ? 0.0 : 1.0;
}
});
// Ambient WebGL Background
const container = document.getElementById('webgl-container');
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
camera.position.z = 1;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
const geometry = new THREE.PlaneGeometry(2, 2);
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform vec3 u_colorCore;
uniform vec3 u_colorFringe;
uniform float u_isLightMode;
varying vec2 vUv;
vec2 hash( vec2 p ) {
p = vec2( dot(p,vec2(127.1,311.7)), dot(p,vec2(269.5,183.3)) );
return -1.0 + 2.0*fract(sin(p)*43758.5453123);
}
float noise( in vec2 p ) {
const float K1 = 0.366025404;
const float K2 = 0.211324865;
vec2 i = floor( p + (p.x+p.y)*K1 );
vec2 a = p - i + (i.x+i.y)*K2;
vec2 o = (a.x>a.y) ? vec2(1.0,0.0) : vec2(0.0,1.0);
vec2 b = a - o + K2;
vec2 c = a - 1.0 + 2.0*K2;
vec3 h = max( 0.5-vec3(dot(a,a), dot(b,b), dot(c,c) ), 0.0 );
vec3 n = h*h*h*h*vec3( dot(a,hash(i+0.0)), dot(b,hash(i+o)), dot(c,hash(i+1.0)));
return dot( n, vec3(70.0) );
}
float sdArc(vec2 p, vec2 center, float radius, float width, float warp) {
p.y += sin(p.x * 2.0 + u_time * 0.3) * warp;
p.x += noise(p * 1.5 + u_time * 0.1) * (warp * 0.8);
float d = length(p - center) - radius;
return abs(d) - width;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
vec2 st = uv;
st.x *= u_resolution.x / u_resolution.y;
vec2 mouseOffset = (u_mouse - 0.5) * 0.05;
st += mouseOffset;
// Center organic form behind the content area
vec2 center = vec2(0.5, 0.5);
float d1 = sdArc(st, center, 0.6, 0.02, 0.15);
float d2 = sdArc(st, center, 0.65, 0.06, 0.2);
float coreGlow = exp(-d1 * 30.0);
float fringeGlow = exp(-d2 * 10.0);
float wash = smoothstep(1.5, -0.5, length(st - center)) * 0.15;
vec3 finalColor = vec3(0.0);
finalColor += u_colorCore * coreGlow;
finalColor += u_colorFringe * fringeGlow;
finalColor += u_colorFringe * wash * (sin(u_time * 0.5) * 0.2 + 0.8);
float alpha = clamp(coreGlow + fringeGlow + wash, 0.0, 1.0);
finalColor = vec3(1.0) - exp(-finalColor * 1.5);
if(u_isLightMode > 0.5) {
alpha = clamp((coreGlow * 1.2 + fringeGlow + wash * 0.4), 0.0, 0.4);
}
gl_FragColor = vec4(finalColor, alpha * 0.6);
}
`;
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
u_time: { value: 0.0 },
u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
u_mouse: { value: new THREE.Vector2(0.5, 0.5) },
u_colorCore: { value: new THREE.Color(themes.dark.shaderCore) },
u_colorFringe: { value: new THREE.Color(themes.dark.shaderFringe) },
u_isLightMode: { value: 0.0 }
},
transparent: true,
blending: THREE.AdditiveBlending
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
let targetMouse = new THREE.Vector2(0.5, 0.5);
document.addEventListener('mousemove', (e) => {
targetMouse.x = e.clientX / window.innerWidth;
targetMouse.y = 1.0 - (e.clientY / window.innerHeight);
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
material.uniforms.u_time.value = clock.getElapsedTime();
material.uniforms.u_mouse.value.lerp(targetMouse, 0.03);
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width, height);
material.uniforms.u_resolution.value.set(width, height);
});
</script>
</body>
</html>
/* ===== src/shaders/neuform-isolated/sources/flow-field.html (html) ===== */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flow Field with Particle Trails</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a0a; }
canvas { display: block; width: 100vw; height: 100vh; }
.label {
position: fixed;
top: 20px;
left: 24px;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 11px;
letter-spacing: 0.15em;
text-transform: uppercase;
color: rgba(200, 149, 108, 0.5);
z-index: 10;
pointer-events: none;
user-select: none;
}
</style>
</head>
<body>
<div class="label">Flow Field</div>
<canvas id="canvas"></canvas>
<script>
// ─────────────────────────────────────────────
// Simplex Noise (compact implementation)
// Based on Stefan Gustavson's simplex noise
// ─────────────────────────────────────────────
const SimplexNoise = (function() {
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
const F3 = 1 / 3;
const G3 = 1 / 6;
const grad3 = [
[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]
];
function SimplexNoise(seed) {
this.perm = new Uint8Array(512);
this.permMod12 = new Uint8Array(512);
const p = new Uint8Array(256);
// Seed-based permutation
seed = seed || Math.random() * 65536;
for (let i = 0; i < 256; i++) p[i] = i;
for (let i = 255; i > 0; i--) {
seed = (seed * 16807 + 0) % 2147483647;
const j = seed % (i + 1);
const tmp = p[i];
p[i] = p[j];
p[j] = tmp;
}
for (let i = 0; i < 512; i++) {
this.perm[i] = p[i & 255];
this.permMod12[i] = this.perm[i] % 12;
}
}
SimplexNoise.prototype.noise2D = function(xin, yin) {
const perm = this.perm, permMod12 = this.permMod12;
let n0, n1, n2;
const s = (xin + yin) * F2;
const i = Math.floor(xin + s);
const j = Math.floor(yin + s);
const t = (i + j) * G2;
const X0 = i - t, Y0 = j - t;
const x0 = xin - X0, y0 = yin - Y0;
let i1, j1;
if (x0 > y0) { i1 = 1; j1 = 0; }
else { i1 = 0; j1 = 1; }
const x1 = x0 - i1 + G2, y1 = y0 - j1 + G2;
const x2 = x0 - 1 + 2 * G2, y2 = y0 - 1 + 2 * G2;
const ii = i & 255, jj = j & 255;
let t0 = 0.5 - x0*x0 - y0*y0;
if (t0 < 0) n0 = 0;
else { t0 *= t0; const gi = permMod12[ii + perm[jj]]; n0 = t0 * t0 * (grad3[gi][0]*x0 + grad3[gi][1]*y0); }
let t1 = 0.5 - x1*x1 - y1*y1;
if (t1 < 0) n1 = 0;
else { t1 *= t1; const gi = permMod12[ii + i1 + perm[jj + j1]]; n1 = t1 * t1 * (grad3[gi][0]*x1 + grad3[gi][1]*y1); }
let t2 = 0.5 - x2*x2 - y2*y2;
if (t2 < 0) n2 = 0;
else { t2 *= t2; const gi = permMod12[ii + 1 + perm[jj + 1]]; n2 = t2 * t2 * (grad3[gi][0]*x2 + grad3[gi][1]*y2); }
return 70 * (n0 + n1 + n2);
};
SimplexNoise.prototype.noise3D = function(xin, yin, zin) {
const perm = this.perm, permMod12 = this.permMod12;
let n0, n1, n2, n3;
const s = (xin + yin + zin) * F3;
const i = Math.floor(xin + s), j = Math.floor(yin + s), k = Math.floor(zin + s);
const t = (i + j + k) * G3;
const X0 = i - t, Y0 = j - t, Z0 = k - t;
const x0 = xin - X0, y0 = yin - Y0, z0 = zin - Z0;
let i1, j1, k1, i2, j2, k2;
if (x0 >= y0) {
if (y0 >= z0) { i1=1;j1=0;k1=0;i2=1;j2=1;k2=0; }
else if (x0 >= z0) { i1=1;j1=0;k1=0;i2=1;j2=0;k2=1; }
else { i1=0;j1=0;k1=1;i2=1;j2=0;k2=1; }
} else {
if (y0 < z0) { i1=0;j1=0;k1=1;i2=0;j2=1;k2=1; }
else if (x0 < z0) { i1=0;j1=1;k1=0;i2=0;j2=1;k2=1; }
else { i1=0;j1=1;k1=0;i2=1;j2=1;k2=0; }
}
const x1 = x0 - i1 + G3, y1 = y0 - j1 + G3, z1 = z0 - k1 + G3;
const x2 = x0 - i2 + 2*G3, y2 = y0 - j2 + 2*G3, z2 = z0 - k2 + 2*G3;
const x3 = x0 - 1 + 3*G3, y3 = y0 - 1 + 3*G3, z3 = z0 - 1 + 3*G3;
const ii = i & 255, jj = j & 255, kk = k & 255;
let t0 = 0.6 - x0*x0 - y0*y0 - z0*z0;
if (t0 < 0) n0 = 0;
else { t0 *= t0; const gi = permMod12[ii+perm[jj+perm[kk]]]; n0 = t0*t0*(grad3[gi][0]*x0+grad3[gi][1]*y0+grad3[gi][2]*z0); }
let t1 = 0.6 - x1*x1 - y1*y1 - z1*z1;
if (t1 < 0) n1 = 0;
else { t1 *= t1; const gi = permMod12[ii+i1+perm[jj+j1+perm[kk+k1]]]; n1 = t1*t1*(grad3[gi][0]*x1+grad3[gi][1]*y1+grad3[gi][2]*z1); }
let t2 = 0.6 - x2*x2 - y2*y2 - z2*z2;
if (t2 < 0) n2 = 0;
else { t2 *= t2; const gi = permMod12[ii+i2+perm[jj+j2+perm[kk+k2]]]; n2 = t2*t2*(grad3[gi][0]*x2+grad3[gi][1]*y2+grad3[gi][2]*z2); }
let t3 = 0.6 - x3*x3 - y3*y3 - z3*z3;
if (t3 < 0) n3 = 0;
else { t3 *= t3; const gi = permMod12[ii+1+perm[jj+1+perm[kk+1]]]; n3 = t3*t3*(grad3[gi][0]*x3+grad3[gi][1]*y3+grad3[gi][2]*z3); }
return 32 * (n0 + n1 + n2 + n3);
};
return SimplexNoise;
})();
// ─────────────────────────────────────────────
// Flow Field Renderer
// ─────────────────────────────────────────────
(function() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const noise = new SimplexNoise(42);
const dpr = window.devicePixelRatio || 1;
let W, H;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// Fill with background on resize to avoid flash
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, W, H);
}
window.addEventListener('resize', resize);
resize();
// ── Mouse tracking ──
const mouse = { x: -9999, y: -9999, active: false };
window.addEventListener('mousemove', function(e) {
mouse.x = e.clientX;
mouse.y = e.clientY;
mouse.active = true;
});
window.addEventListener('mouseleave', function() {
mouse.active = false;
});
// ── Color palette ──
// Warm tones: amber, gold, coral, with variations
const palette = [
{ r: 200, g: 149, b: 108 }, // amber #c8956c
{ r: 212, g: 165, b: 116 }, // gold #d4a574
{ r: 224, g: 120, b: 80 }, // coral #e07850
{ r: 190, g: 130, b: 90 }, // dark amber
{ r: 230, g: 180, b: 140 }, // light gold
{ r: 210, g: 100, b: 70 }, // deep coral
{ r: 180, g: 160, b: 120 }, // muted gold
];
function lerpColor(a, b, t) {
return {
r: a.r + (b.r - a.r) * t,
g: a.g + (b.g - a.g) * t,
b: a.b + (b.b - a.b) * t
};
}
function getColor(noiseVal) {
// Map noise [-1,1] to palette index
const t = (noiseVal + 1) * 0.5; // 0..1
const idx = t * (palette.length - 1);
const i = Math.floor(idx);
const f = idx - i;
const a = palette[Math.min(i, palette.length - 1)];
const b = palette[Math.min(i + 1, palette.length - 1)];
return lerpColor(a, b, f);
}
// ── Particles ──
const PARTICLE_COUNT = 2500;
let NOISE_SCALE = 0.0025;
let SPEED = 1.2;
const MOUSE_RADIUS = 150;
const MOUSE_FORCE = 2.5;
const particles = [];
function createParticle() {
return {
x: Math.random() * W,
y: Math.random() * H,
speed: 0.4 + Math.random() * 1.0,
alpha: 0.15 + Math.random() * 0.55,
size: 0.5 + Math.random() * 1.5
};
}
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles.push(createParticle());
}
// ── Animation ──
let time = 0;
function draw() {
// Fade previous frame with dark overlay — creates trailing effect
ctx.fillStyle = 'rgba(10, 10, 10, 0.03)';
ctx.fillRect(0, 0, W, H);
time += 0.0008;
for (let i = 0; i < PARTICLE_COUNT; i++) {
const p = particles[i];
// Sample noise field at particle position with time evolution
const nx = p.x * NOISE_SCALE;
const ny = p.y * NOISE_SCALE;
const angle = noise.noise3D(nx, ny, time) * Math.PI * 2;
// Secondary noise for color variation
const colorNoise = noise.noise3D(nx * 1.5 + 100, ny * 1.5 + 100, time * 0.5);
// Velocity from flow field
let vx = Math.cos(angle) * p.speed * SPEED;
let vy = Math.sin(angle) * p.speed * SPEED;
// Mouse interaction — gentle push away
if (mouse.active) {
const dx = p.x - mouse.x;
const dy = p.y - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < MOUSE_RADIUS && dist > 0) {
const force = (1 - dist / MOUSE_RADIUS) * MOUSE_FORCE;
vx += (dx / dist) * force;
vy += (dy / dist) * force;
}
}
// Store previous position for line drawing
const px = p.x;
const py = p.y;
// Update position
p.x += vx;
p.y += vy;
// Draw particle trail segment
const color = getColor(colorNoise);
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(p.x, p.y);
ctx.strokeStyle = 'rgba(' + Math.round(color.r) + ',' + Math.round(color.g) + ',' + Math.round(color.b) + ',' + p.alpha + ')';
ctx.lineWidth = p.size;
ctx.stroke();
// Respawn if offscreen
if (p.x < -20 || p.x > W + 20 || p.y < -20 || p.y > H + 20) {
p.x = Math.random() * W;
p.y = Math.random() * H;
}
}
requestAnimationFrame(draw);
}
// Initial fill
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, W, H);
requestAnimationFrame(draw);
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'param') {
switch (e.data.name) {
case 'SPEED': SPEED = e.data.value; break;
case 'NOISE_SCALE': NOISE_SCALE = e.data.value; break;
}
}
});
})();
</script>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516" integrity="sha512-8DS7rgIrAmghBFwoOTujcf6D9rXvH8xm8JQ1Ja01h9QX8EzXldiszufYa4IFfKdLUKTTrnSFXLDkUEOTrZQ8Qg==" data-cf-beacon='{"version":"2024.11.0","token":"216c03e5eb1b42998a91f716785010f9","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>
/* ===== src/shaders/neuform-isolated/sources/strata-cloud.html (html) ===== */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Strata — Cloud Migration Platform</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
</head>
<body class="bg-[#050510] text-white font-['Inter',system-ui,sans-serif] overflow-x-hidden antialiased selection:bg-violet-500/30 selection:text-violet-200">
<!-- WebGL Background Canvas -->
<canvas id="c" class="fixed inset-0 w-full h-full z-0 pointer-events-none"></canvas>
<!-- Navigation -->
<nav class="fixed top-0 left-0 right-0 z-50 flex items-center justify-center py-4 px-6 md:px-10 bg-[#050510]/50 backdrop-blur-xl border-b border-violet-600/10">
<div class="w-full max-w-[1200px] flex items-center justify-between">
<div class="flex items-center gap-3 font-medium text-sm tracking-wide">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-600 to-violet-400 flex items-center justify-center shadow-[0_0_16px_rgba(124,58,237,0.3)]">
<iconify-icon icon="solar:layers-linear" class="text-white text-lg"></iconify-icon>
</div>
Strata
</div>
<div class="hidden md:flex items-center gap-8">
<a href="#" class="text-white/50 hover:text-white/90 text-sm font-light tracking-wide transition-colors">Platform</a>
<a href="#" class="text-white/50 hover:text-white/90 text-sm font-light tracking-wide transition-colors">Solutions</a>
<a href="#" class="text-white/50 hover:text-white/90 text-sm font-light tracking-wide transition-colors">Pricing</a>
<a href="#" class="text-white/50 hover:text-white/90 text-sm font-light tracking-wide transition-colors">Docs</a>
</div>
<button class="px-5 py-2.5 rounded-full text-xs font-normal bg-gradient-to-br from-violet-600/20 to-violet-500/10 border border-violet-600/30 text-violet-400 hover:from-violet-600/30 hover:to-violet-500/20 transition-all shadow-[0_0_20px_rgba(124,58,237,0.1)] hover:shadow-[0_0_28px_rgba(124,58,237,0.2)] tracking-wide">
Start Migration
</button>
</div>
</nav>
<!-- Hero Section -->
<main class="relative z-10 flex flex-col items-center justify-center text-center min-h-[100svh] px-6 pt-20">
<!-- Radial shadow overlay to help text readability over canvas -->
<div class="absolute inset-0 pointer-events-none z-[-1]" style="background: radial-gradient(ellipse 70% 50% at 50% 55%, rgba(5,5,16,0.6) 0%, transparent 70%);"></div>
<div class="relative z-1 max-w-3xl mx-auto w-full flex flex-col items-center">
<div class="inline-flex items-center gap-2 px-4 py-1.5 mb-8 bg-violet-600/5 border border-violet-600/20 rounded-full text-xs font-medium tracking-[0.1em] uppercase text-violet-400 backdrop-blur-sm">
<span class="w-1.5 h-1.5 rounded-full bg-violet-500 shadow-[0_0_8px_#8b5cf6] animate-pulse"></span>
Now in General Availability
</div>
<h1 class="text-[clamp(2.4rem,6vw,4rem)] font-thin tracking-tight leading-[1.08] text-white mb-6">
Migrate to the cloud<br>with <em class="not-italic bg-gradient-to-br from-violet-500 via-violet-400 to-violet-200 bg-clip-text text-transparent">confidence</em>
</h1>
<p class="text-sm md:text-base font-light leading-relaxed text-white/40 max-w-xl mx-auto mb-10">
Automated discovery, dependency mapping, and zero-downtime migration for enterprise workloads. Move faster with less risk.
</p>
<div class="flex flex-wrap justify-center gap-4 mb-16 w-full">
<button class="relative overflow-hidden px-8 py-3.5 rounded-xl text-sm font-medium border-none text-white tracking-wide bg-gradient-to-br from-violet-700 to-violet-500 shadow-[0_0_30px_rgba(124,58,237,0.3),0_4px_16px_rgba(0,0,0,0.4)] hover:shadow-[0_0_40px_rgba(124,58,237,0.45),0_6px_20px_rgba(0,0,0,0.5)] transition-all hover:-translate-y-[1px] group">
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-500"></div>
Start Free Assessment
</button>
<button class="px-8 py-3.5 rounded-xl text-sm font-normal border border-white/10 bg-white/5 text-white/70 backdrop-blur-md hover:border-white/20 hover:text-white hover:bg-white/10 transition-all tracking-wide">
Watch Demo
</button>
</div>
<div class="flex flex-wrap justify-center gap-8 md:gap-14 w-full">
<div class="text-center">
<div class="text-2xl md:text-3xl font-thin tracking-tight bg-gradient-to-br from-violet-400 to-violet-200 bg-clip-text text-transparent">99.99%</div>
<div class="text-xs font-light text-white/30 tracking-[0.08em] uppercase mt-1.5">Uptime SLA</div>
</div>
<div class="text-center">
<div class="text-2xl md:text-3xl font-thin tracking-tight bg-gradient-to-br from-violet-400 to-violet-200 bg-clip-text text-transparent">8.5M</div>
<div class="text-xs font-light text-white/30 tracking-[0.08em] uppercase mt-1.5">Workloads Migrated</div>
</div>
<div class="text-center">
<div class="text-2xl md:text-3xl font-thin tracking-tight bg-gradient-to-br from-violet-400 to-violet-200 bg-clip-text text-transparent">75%</div>
<div class="text-xs font-light text-white/30 tracking-[0.08em] uppercase mt-1.5">Faster Migrations</div>
</div>
<div class="hidden sm:block text-center">
<div class="text-2xl md:text-3xl font-thin tracking-tight bg-gradient-to-br from-violet-400 to-violet-200 bg-clip-text text-transparent">500+</div>
<div class="text-xs font-light text-white/30 tracking-[0.08em] uppercase mt-1.5">Enterprise Clients</div>
</div>
</div>
</div>
<div class="absolute bottom-10 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 opacity-40 pointer-events-none">
<div class="w-5 h-[30px] rounded-full border-[1.5px] border-white/40 relative">
<div id="scroll-dot" class="w-[2px] h-1.5 bg-white/60 rounded-full absolute top-1.5 left-1/2 -translate-x-1/2"></div>
</div>
<span class="text-[0.6rem] tracking-[0.14em] uppercase text-white/40">Scroll</span>
</div>
</main>
<!-- Scroll Reveal Section (GSAP Masked Stagger) -->
<section class="relative z-10 py-32 px-6 min-h-[70svh] flex items-center justify-center bg-[#050510]">
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-[#050510] to-[#050510] pointer-events-none z-[-1]"></div>
<div class="max-w-4xl mx-auto w-full text-center">
<p class="text-xs font-medium tracking-[0.15em] uppercase text-violet-500 mb-8">Intelligent Infrastructure</p>
<h2 id="reveal-text" class="text-2xl md:text-5xl font-thin tracking-tight leading-[1.3] text-white/90">
Discover untethered scalability. The platform built to dynamically map, migrate, and optimize your entire infrastructure without lifting a finger.
</h2>
</div>
</section>
<!-- Feature Section with Premium Gradient Borders & Aura Images -->
<section class="relative z-10 py-24 px-6 bg-[#050510]">
<div class="max-w-[1200px] mx-auto w-full grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Card 1 -->
<div class="p-[1px] rounded-3xl bg-gradient-to-b from-white/10 via-white/5 to-transparent relative group overflow-hidden">
<div class="absolute inset-0 bg-violet-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-700 blur-3xl rounded-3xl pointer-events-none"></div>
<div class="bg-[#080814] rounded-[23px] h-full p-2 relative z-10 flex flex-col">
<div class="w-full h-56 md:h-72 rounded-[18px] overflow-hidden mb-6 relative">
<img src="https://hoirqrkdgbmvpwutwuwj.supabase.co/storage/v1/object/public/assets/assets/724142aa-44a6-48d3-9cf3-761e00d05b78_1600w.jpg" alt="Architecture" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-1000 ease-out opacity-80" loading="lazy">
<div class="absolute inset-0 bg-gradient-to-t from-[#080814] to-transparent"></div>
</div>
<div class="px-6 pb-8">
<div class="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center mb-5 text-white/70">
<iconify-icon icon="solar:box-minimalistic-linear" class="text-xl"></iconify-icon>
</div>
<h3 class="text-xl font-light tracking-tight text-white mb-3">Architectural Clarity</h3>
<p class="text-sm font-light text-white/40 leading-relaxed">
Automatically visualize dependencies across microservices and legacy monolithic structures before execution.
</p>
</div>
</div>
</div>
<!-- Card 2 -->
<div class="p-[1px] rounded-3xl bg-gradient-to-b from-white/10 via-white/5 to-transparent relative group overflow-hidden">
<div class="absolute inset-0 bg-violet-500/10 opacity-0 group-hover:opacity-100 transition-opacity duration-700 blur-3xl rounded-3xl pointer-events-none"></div>
<div class="bg-[#080814] rounded-[23px] h-full p-2 relative z-10 flex flex-col">
<div class="w-full h-56 md:h-72 rounded-[18px] overflow-hidden mb-6 relative">
<img src="https://hoirqrkdgbmvpwutwuwj.supabase.co/storage/v1/object/public/assets/assets/4734259a-bad7-422f-981e-ce01e79184f2_1600w.jpg" alt="Abstract" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-1000 ease-out opacity-80" loading="lazy">
<div class="absolute inset-0 bg-gradient-to-t from-[#080814] to-transparent"></div>
</div>
<div class="px-6 pb-8">
<div class="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center mb-5 text-white/70">
<iconify-icon icon="solar:graph-linear" class="text-xl"></iconify-icon>
</div>
<h3 class="text-xl font-light tracking-tight text-white mb-3">Dynamic Scaling</h3>
<p class="text-sm font-light text-white/40 leading-relaxed">
Leverage predictive models to right-size compute resources dynamically as your user traffic shifts.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Footer Spacer -->
<footer class="relative z-10 bg-[#050510] border-t border-white/5 py-10 text-center">
<p class="text-xs font-light text-white/30 tracking-wide">© Strata Technologies. All rights reserved.</p>
</footer>
<!-- Logic & Animations -->
<script>
// --- GSAP Animations ---
gsap.registerPlugin(ScrollTrigger);
// Scroll Mouse Dot Animation
gsap.to("#scroll-dot", {
y: 8,
opacity: 0.2,
duration: 1.2,
repeat: -1,
yoyo: true,
ease: "power1.inOut"
});
// Masked Staggered Text Reveal
const revealEl = document.getElementById('reveal-text');
const textContent = revealEl.innerText;
revealEl.innerHTML = '';
// Wrap words in masked spans
const words = textContent.split(' ');
words.forEach(word => {
const wrapper = document.createElement('span');
// Using inline styles for structural mask requirements (avoids `<style>`)
wrapper.style.display = 'inline-block';
wrapper.style.overflow = 'hidden';
wrapper.style.verticalAlign = 'bottom';
wrapper.style.marginRight = '0.25em';
wrapper.style.paddingBottom = '0.1em'; // Prevent clipping of descenders
const inner = document.createElement('span');
inner.className = 'reveal-word';
inner.style.display = 'inline-block';
inner.style.transform = 'translateY(110%)';
inner.style.transformOrigin = 'top left';
inner.innerText = word;
wrapper.appendChild(inner);
revealEl.appendChild(wrapper);
});
// Trigger animation
gsap.to('.reveal-word', {
y: 0,
duration: 1.2,
stagger: 0.03,
ease: "power4.out",
scrollTrigger: {
trigger: revealEl,
start: "top 85%",
toggleActions: "play none none reverse"
}
});
// --- WebGL Background Canvas ---
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl', { antialias: true, alpha: false, preserveDrawingBuffer: true });
let W, H, dpr;
function resize() {
dpr = Math.min(window.devicePixelRatio, 2);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
const VS = `
attribute vec2 a_pos;
void main(){ gl_Position = vec4(a_pos, 0.0, 1.0); }
`;
const FS = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform vec2 u_mouse;
float hash(float n){ return fract(sin(n)*43758.5453123); }
float hash2(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
float noise(float x){
float i = floor(x);
float f = fract(x);
f = f*f*(3.0-2.0*f);
return mix(hash(i), hash(i+1.0), f);
}
float fbm(float x, float octaves){
float val = 0.0;
float amp = 0.5;
float freq = 1.0;
for(int i = 0; i < 6; i++){
if(float(i) >= octaves) break;
val += amp * noise(x * freq);
freq *= 2.17;
amp *= 0.48;
}
return val;
}
float meteor(vec2 uv, float t){
float cycle = mod(t * 0.15, 1.0);
float seed = floor(t * 0.15);
float h = hash(seed * 7.31);
float h2 = hash(seed * 13.17);
if(h > 0.30) return 0.0;
vec2 start = vec2(0.2 + h2 * 0.6, 0.7 + h * 0.25);
vec2 dir = normalize(vec2(1.0, -0.6 - h * 0.3));
float progress = smoothstep(0.0, 0.7, cycle);
vec2 pos = start + dir * progress * 0.5;
vec2 toP = uv - pos;
float along = dot(toP, dir);
float perp = length(toP - dir * along);
float trail = smoothstep(0.0, -0.12, along) * smoothstep(-0.18, -0.04, along);
float core = smoothstep(0.003, 0.0, perp) * trail;
float glow = smoothstep(0.012, 0.0, perp) * trail * 0.3;
float fade = smoothstep(0.0, 0.1, cycle) * smoothstep(0.8, 0.55, cycle);
return (core + glow) * fade;
}
float stars(vec2 uv, float density){
vec2 cell = floor(uv * density);
vec2 sub = fract(uv * density);
float h = hash2(cell);
float brightness = step(0.975, h);
float size = 0.025 + h * 0.045;
float d = length(sub - vec2(hash2(cell + 100.0), hash2(cell + 200.0)));
float star = brightness * smoothstep(size, 0.0, d);
star *= 0.5 + 0.5 * sin(u_time * (1.0 + h * 3.0) + h * 6.28);
return star;
}
void main(){
vec2 uv = gl_FragCoord.xy / u_res;
float aspect = u_res.x / u_res.y;
vec2 mouse = u_mouse * 2.0 - 1.0;
vec3 skyTop = vec3(0.015, 0.012, 0.045);
vec3 skyMid = vec3(0.035, 0.025, 0.085);
vec3 skyBottom = vec3(0.065, 0.045, 0.14);
float skyGrad = uv.y;
vec3 col = mix(skyBottom, skyMid, smoothstep(0.3, 0.6, skyGrad));
col = mix(col, skyTop, smoothstep(0.6, 1.0, skyGrad));
float horizonY = 0.35;
float horizonGlow = exp(-pow((uv.y - horizonY) * 3.8, 2.0));
col += vec3(0.15, 0.07, 0.26) * horizonGlow * 0.8;
float centerGlow = exp(-pow((uv.x - 0.5) * 1.5, 2.0)) * exp(-pow((uv.y - horizonY) * 4.0, 2.0));
col += vec3(0.14, 0.10, 0.24) * centerGlow * 0.6;
float starField = stars(uv * vec2(aspect, 1.0), 60.0)
+ stars(uv * vec2(aspect, 1.0) + 500.0, 100.0) * 0.7
+ stars(uv * vec2(aspect, 1.0) + 900.0, 160.0) * 0.4;
float starMask = 1.0;
float xC, yS, prof, mTop, mtn, rDist, rGlow, rAmb;
vec3 lC;
// Layer 0
lC = vec3(0.14, 0.10, 0.24);
xC = uv.x * aspect * 1.6 + u_time * 0.006 + mouse.x * 0.010;
yS = mouse.y * 0.003;
prof = fbm(xC, 5.0) * 0.10 + fbm(xC * 0.3 + 17.0, 3.0) * 0.07;
mTop = 0.40 + prof + yS;
mtn = smoothstep(mTop + 0.003, mTop - 0.001, uv.y);
rDist = abs(uv.y - mTop);
rGlow = smoothstep(0.012, 0.0, rDist) * 0.18;
rAmb = smoothstep(0.04, 0.0, rDist) * 0.06;
col = mix(col, lC, mtn);
col += vec3(0.20, 0.10, 0.35) * rGlow;
col += vec3(0.12, 0.06, 0.22) * rAmb;
starMask *= (1.0 - mtn);
// Layer 1
lC = vec3(0.11, 0.07, 0.19);
xC = uv.x * aspect * 2.0 + u_time * 0.012 + mouse.x * 0.020;
yS = mouse.y * 0.006;
prof = fbm(xC, 5.0) * 0.13 + fbm(xC * 0.3 + 34.0, 3.0) * 0.091;
mTop = 0.33 + prof + yS;
mtn = smoothstep(mTop + 0.003, mTop - 0.001, uv.y);
rDist = abs(uv.y - mTop);
rGlow = smoothstep(0.012, 0.0, rDist) * 0.15;
rAmb = smoothstep(0.04, 0.0, rDist) * 0.045;
col = mix(col, lC, mtn);
col += vec3(0.20, 0.10, 0.35) * rGlow;
col += vec3(0.12, 0.06, 0.22) * rAmb;
starMask *= (1.0 - mtn);
// Layer 2
lC = vec3(0.08, 0.05, 0.14);
xC = uv.x * aspect * 2.6 + u_time * 0.020 + mouse.x * 0.034;
yS = mouse.y * 0.010;
prof = fbm(xC, 5.0) * 0.16 + fbm(xC * 0.3 + 51.0, 3.0) * 0.112;
mTop = 0.26 + prof + yS;
mtn = smoothstep(mTop + 0.003, mTop - 0.001, uv.y);
rDist = abs(uv.y - mTop);
rGlow = smoothstep(0.012, 0.0, rDist) * 0.12;
rAmb = smoothstep(0.04, 0.0, rDist) * 0.03;
col = mix(col, lC, mtn);
col += vec3(0.20, 0.10, 0.35) * rGlow;
col += vec3(0.12, 0.06, 0.22) * rAmb;
starMask *= (1.0 - mtn);
// Layer 3
lC = vec3(0.05, 0.03, 0.09);
xC = uv.x * aspect * 3.2 + u_time * 0.030 + mouse.x * 0.050;
yS = mouse.y * 0.015;
prof = fbm(xC, 5.0) * 0.14 + fbm(xC * 0.3 + 68.0, 3.0) * 0.098;
mTop = 0.18 + prof + yS;
mtn = smoothstep(mTop + 0.003, mTop - 0.001, uv.y);
rDist = abs(uv.y - mTop);
rGlow = smoothstep(0.012, 0.0, rDist) * 0.09;
col = mix(col, lC, mtn);
col += vec3(0.20, 0.10, 0.35) * rGlow;
starMask *= (1.0 - mtn);
// Layer 4
lC = vec3(0.03, 0.018, 0.055);
xC = uv.x * aspect * 4.0 + u_time * 0.044 + mouse.x * 0.070;
yS = mouse.y * 0.021;
prof = fbm(xC, 5.0) * 0.11 + fbm(xC * 0.3 + 85.0, 3.0) * 0.077;
mTop = 0.09 + prof + yS;
mtn = smoothstep(mTop + 0.003, mTop - 0.001, uv.y);
rDist = abs(uv.y - mTop);
rGlow = smoothstep(0.012, 0.0, rDist) * 0.06;
col = mix(col, lC, mtn);
col += vec3(0.20, 0.10, 0.35) * rGlow;
starMask *= (1.0 - mtn);
col += vec3(0.9, 0.8, 1.0) * starField * starMask;
float met = meteor(uv * vec2(aspect, 1.0), u_time);
col += vec3(0.8, 0.6, 1.0) * met * starMask;
float vig = 1.0 - 0.3 * pow(length((uv - 0.5) * vec2(1.1, 1.6)), 2.0);
col *= vig;
float haze = exp(-pow((uv.y - 0.33) * 5.0, 2.0)) * 0.05;
col += vec3(0.15, 0.10, 0.30) * haze;
col = pow(col, vec3(0.95));
gl_FragColor = vec4(col, 1.0);
}
`;
function createShader(src, type) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) return null;
return s;
}
const vs = createShader(VS, gl.VERTEX_SHADER);
const fs = createShader(FS, gl.FRAGMENT_SHADER);
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
gl.useProgram(prog);
const quad = new Float32Array([-1,-1, 1,-1, -1,1, 1,1]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
const uRes = gl.getUniformLocation(prog, 'u_res');
const uTime = gl.getUniformLocation(prog, 'u_time');
const uMouse = gl.getUniformLocation(prog, 'u_mouse');
let mx = 0.5, my = 0.5;
let smx = 0.5, smy = 0.5;
document.addEventListener('mousemove', e => {
mx = e.clientX / window.innerWidth;
my = 1.0 - e.clientY / window.innerHeight;
});
function frame(t) {
t *= 0.001;
smx += (mx - smx) * 0.04;
smy += (my - smy) * 0.04;
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, t);
gl.uniform2f(uMouse, smx, smy);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
/* ===== src/shaders/bell-field/BellFieldBackground.tsx (tsx) ===== */
import { useEffect, useRef } from "react";
import { BELL_FIELD_FRAGMENT_SHADER, BELL_FIELD_VERTEX_SHADER } from "./bellFieldShaders";
export type BellFieldBackgroundProps = { speed?: number; pointerAmount?: number; strikeDuration?: number; emberAmount?: number; brightness?: number; opacity?: number; hue?: number; saturation?: number; className?: string };
export const BELL_FIELD_DEFAULTS = { speed: 1, pointerAmount: 1, strikeDuration: 2400, emberAmount: 1, brightness: 1, opacity: 1, hue: 0, saturation: 1 } as const;
type Ember = { x: number; y: number; r: number; vy: number; vx: number; ph: number; sp: number; hot: boolean };
function compile(gl: WebGLRenderingContext, type: number, source: string) { const shader = gl.createShader(type); if (!shader) throw new Error("Unable to create Bell Field shader"); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader) ?? "Bell Field shader compilation failed"); return shader; }
export function BellFieldBackground({ className = "", ...props }: BellFieldBackgroundProps) {
const hostRef = useRef<HTMLDivElement>(null), modeRef = useRef<HTMLCanvasElement>(null), emberRef = useRef<HTMLCanvasElement>(null); const optionsRef = useRef({ ...BELL_FIELD_DEFAULTS, ...props }); optionsRef.current = { ...BELL_FIELD_DEFAULTS, ...props };
useEffect(() => {
const host = hostRef.current, canvas = modeRef.current, emberCanvas = emberRef.current; if (!host || !canvas || !emberCanvas) return undefined; const gl = canvas.getContext("webgl"), emberContext = emberCanvas.getContext("2d"); if (!gl || !emberContext) return undefined;
const vertex = compile(gl, gl.VERTEX_SHADER, BELL_FIELD_VERTEX_SHADER), fragment = compile(gl, gl.FRAGMENT_SHADER, BELL_FIELD_FRAGMENT_SHADER), program = gl.createProgram(); if (!program) return undefined; gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) ?? "Bell Field program link failed"); gl.useProgram(program);
const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, -1, 1, 1, -1, 1]), gl.STATIC_DRAW); const position = gl.getAttribLocation(program, "position"); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
const resolution = gl.getUniformLocation(program, "u_resolution"), time = gl.getUniformLocation(program, "u_time"), mouseUniform = gl.getUniformLocation(program, "u_mouse"), strikeUniform = gl.getUniformLocation(program, "u_strike"); let width = 1, height = 1, dpr = 1, mouseX = 0.5, mouseY = 0.5, targetX = 0.5, targetY = 0.5, frame = 0, visible = true, initialized = false, lastStrikeMs = -1e9; const startedAt = performance.now();
const embers: Ember[] = Array.from({ length: 58 }, () => ({ x: Math.random(), y: Math.random(), r: 0.4 + Math.random() * 1.4, vy: -(0.10 + Math.random() * 0.26), vx: (Math.random() - 0.5) * 0.08, ph: Math.random() * Math.PI * 2, sp: 0.5 + Math.random() * 1.4, hot: Math.random() < 0.36 }));
const resize = () => { const bounds = host.getBoundingClientRect(); width = Math.max(1, bounds.width); height = Math.max(1, bounds.height); dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = Math.max(1, Math.round(width * dpr)); canvas.height = Math.max(1, Math.round(height * dpr)); emberCanvas.width = canvas.width; emberCanvas.height = canvas.height; emberContext.setTransform(dpr, 0, 0, dpr, 0, 0); gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform2f(resolution, canvas.width, canvas.height); if (!initialized) { mouseX = targetX = width * 0.5; mouseY = targetY = height * 0.5; initialized = true; } embers.forEach((ember) => { if (ember.x <= 1) ember.x *= width; if (ember.y <= 1) ember.y *= height; }); };
const pointer = (event: PointerEvent) => { const bounds = host.getBoundingClientRect(), amount = optionsRef.current.pointerAmount; targetX = width * 0.5 + (event.clientX - bounds.left - width * 0.5) * amount; targetY = height * 0.5 + (event.clientY - bounds.top - height * 0.5) * amount; };
const strike = () => { lastStrikeMs = performance.now(); };
const firstStrike = window.setTimeout(strike, 1700), strikeTimer = window.setInterval(strike, 8200);
const render = (now: number) => { const options = optionsRef.current, t = now * 0.001 * options.speed; mouseX += (targetX - mouseX) * 0.04; mouseY += (targetY - mouseY) * 0.04; gl.uniform1f(time, (now - startedAt) * 0.001 * options.speed); gl.uniform1f(strikeUniform, Math.min(1, Math.max(0, (now - lastStrikeMs) / options.strikeDuration))); gl.uniform2f(mouseUniform, mouseX * dpr, mouseY * dpr); gl.drawArrays(gl.TRIANGLES, 0, 6);
emberContext.clearRect(0, 0, width, height); const count = Math.max(0, Math.min(58, Math.round(58 * options.emberAmount))); for (let index = 0; index < count; index += 1) { const ember = embers[index]; ember.y += ember.vy * options.speed; ember.x += (ember.vx + Math.sin(t * ember.sp * 0.5 + ember.ph) * 0.13) * options.speed; if (ember.y < -4) { ember.y = height + 4; ember.x = Math.random() * width; } if (ember.x < -4) ember.x = width + 4; if (ember.x > width + 4) ember.x = -4; const twinkle = 0.5 + 0.5 * Math.sin(t * ember.sp + ember.ph); emberContext.beginPath(); emberContext.arc(ember.x, ember.y, ember.r, 0, Math.PI * 2); emberContext.fillStyle = ember.hot ? `rgba(231, 193, 101, ${0.06 + twinkle * 0.34})` : `rgba(143, 203, 185, ${0.04 + twinkle * 0.24})`; emberContext.fill(); }
frame = visible && !document.hidden ? requestAnimationFrame(render) : 0; };
const resizeObserver = new ResizeObserver(resize), intersection = new IntersectionObserver(([entry]) => { visible = entry?.isIntersecting ?? true; if (visible && !frame) frame = requestAnimationFrame(render); if (!visible && frame) cancelAnimationFrame(frame), frame = 0; }); resizeObserver.observe(host); intersection.observe(host); host.addEventListener("pointermove", pointer, { passive: true }); host.addEventListener("pointerdown", strike); resize(); frame = requestAnimationFrame(render);
return () => { if (frame) cancelAnimationFrame(frame); window.clearTimeout(firstStrike); window.clearInterval(strikeTimer); resizeObserver.disconnect(); intersection.disconnect(); host.removeEventListener("pointermove", pointer); host.removeEventListener("pointerdown", strike); gl.deleteBuffer(buffer); gl.deleteShader(vertex); gl.deleteShader(fragment); gl.deleteProgram(program); };
}, []);
const options = optionsRef.current; return <div ref={hostRef} className={`threeui-background bell-field${className ? ` ${className}` : ""}`} style={{ background: "#08100f", opacity: options.opacity, filter: `hue-rotate(${options.hue}deg) saturate(${options.saturation}) brightness(${options.brightness})` }}><canvas ref={modeRef} style={{ zIndex: 0 }} /><canvas ref={emberRef} style={{ zIndex: 1, pointerEvents: "none" }} /></div>;
}
/* ===== src/shaders/bell-field/bellFieldShaders.ts (typescript) ===== */
export const BELL_FIELD_VERTEX_SHADER = `
attribute vec2 position;
void main() { gl_Position = vec4(position, 0.0, 1.0); }
`;
export const BELL_FIELD_FRAGMENT_SHADER = `
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
uniform float u_strike;
#define PI 3.14159265359
float hash(vec2 p) { return fract(sin(dot(p, vec2(23.71, 91.37))) * 41537.1234); }
// damped-cosine stand-in for the Bessel envelope of a circular mode
float bess(float x) { return cos(x - 0.785398) / sqrt(1.0 + abs(x)); }
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
vec2 p = uv * 2.0 - 1.0;
p.x *= u_resolution.x / u_resolution.y;
p.y += 0.08;
vec2 m = u_mouse / u_resolution.xy * 2.0 - 1.0;
m.y = -m.y;
m.x *= u_resolution.x / u_resolution.y;
p -= m * 0.11;
float t = u_time * 0.09;
float r = length(p);
float a = atan(p.y, p.x);
// the bell drifts between partials the way a struck bell does
float ang = 3.0 + 1.6 * sin(t * 0.37) + sin(t * 0.19 + 1.7);
float k = 3.1 + 1.0 * sin(t * 0.23 + 0.6);
float amp = 1.0 + (1.0 - u_strike) * 0.55;
float f1 = bess(r * k * PI - t * 2.2) * cos(ang * a + t * 0.5);
float f2 = bess(r * k * 1.6 * PI + t * 1.4) * cos((ang * 2.0 + 1.0) * a - t * 0.31);
float f = (f1 + f2 * 0.30) * amp;
// nodal lines — where the metal stands still
float node = 1.0 - smoothstep(0.0, 0.075 + 0.075 * r, abs(f));
// antinodes — where it moves, and glows hot
float anti = smoothstep(0.40, 0.95, abs(f));
// the crown stays quiet — clears a reading zone under the type
float open = smoothstep(0.14, 0.92, r);
node *= open;
anti *= open;
vec3 deep = vec3(0.031, 0.055, 0.051);
vec3 patina = vec3(0.306, 0.608, 0.541);
vec3 bronze = vec3(0.847, 0.608, 0.247);
vec3 ash = vec3(0.937, 0.914, 0.863);
vec3 col = deep;
col = mix(col, patina, node * 0.50);
col = mix(col, bronze, anti * 0.22);
col += ash * pow(node, 3.0) * 0.13;
// shock ring travelling out from the strike
float ring = smoothstep(0.06, 0.0, abs(r - u_strike * 2.3)) * (1.0 - u_strike);
col += mix(bronze, ash, 0.4) * ring * 0.7;
col *= mix(0.10, 1.0, smoothstep(2.0, 0.28, r));
col += (hash(gl_FragCoord.xy) - 0.5) * 0.022;
gl_FragColor = vec4(col, 1.0);
}
`;
/* ===== src/shaders/stream-convergence/StreamConvergenceBackground.tsx (tsx) ===== */
import { useEffect, useRef } from "react";
import { STREAM_CONVERGENCE_FRAGMENT_SHADER, STREAM_CONVERGENCE_VERTEX_SHADER } from "./streamConvergenceShaders";
export type StreamConvergenceBackgroundProps = { speed?: number; fidelity?: number; scale?: number; brightness?: number; opacity?: number; hue?: number; saturation?: number; className?: string };
export const STREAM_CONVERGENCE_DEFAULTS = { speed: 1, fidelity: 0.5, scale: 1, brightness: 1, opacity: 1, hue: 0, saturation: 1 } as const;
function compile(gl: WebGLRenderingContext, type: number, source: string) { const shader = gl.createShader(type); if (!shader) throw new Error("Unable to create Stream Convergence shader"); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader) ?? "Stream Convergence shader compilation failed"); return shader; }
export function StreamConvergenceBackground({ className = "", ...props }: StreamConvergenceBackgroundProps) {
const hostRef = useRef<HTMLDivElement>(null), canvasRef = useRef<HTMLCanvasElement>(null); const optionsRef = useRef({ ...STREAM_CONVERGENCE_DEFAULTS, ...props }); optionsRef.current = { ...STREAM_CONVERGENCE_DEFAULTS, ...props };
useEffect(() => {
const host = hostRef.current, canvas = canvasRef.current; if (!host || !canvas) return undefined; const gl = canvas.getContext("webgl", { alpha: true, antialias: false }); if (!gl) return undefined;
const vertex = compile(gl, gl.VERTEX_SHADER, STREAM_CONVERGENCE_VERTEX_SHADER), fragment = compile(gl, gl.FRAGMENT_SHADER, `precision highp float;n${STREAM_CONVERGENCE_FRAGMENT_SHADER}`), program = gl.createProgram(); if (!program) return undefined;
gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) ?? "Stream Convergence program link failed"); gl.useProgram(program);
const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW); const position = gl.getAttribLocation(program, "position"); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
const uTime = gl.getUniformLocation(program, "u_time"), uResolution = gl.getUniformLocation(program, "u_resolution"), uFidelity = gl.getUniformLocation(program, "u_interactive_fidelity"); let frame = 0, visible = true;
const resize = () => { const bounds = host.getBoundingClientRect(), dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = Math.max(1, Math.round(bounds.width * dpr)); canvas.height = Math.max(1, Math.round(bounds.height * dpr)); gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform2f(uResolution, canvas.width, canvas.height); };
const render = (now: number) => { const options = optionsRef.current; gl.uniform1f(uTime, now * 0.0003 * options.speed); gl.uniform1f(uFidelity, options.fidelity); gl.drawArrays(gl.TRIANGLES, 0, 6); frame = visible && !document.hidden ? requestAnimationFrame(render) : 0; };
const resizeObserver = new ResizeObserver(resize), intersection = new IntersectionObserver(([entry]) => { visible = entry?.isIntersecting ?? true; if (visible && !frame) frame = requestAnimationFrame(render); if (!visible && frame) cancelAnimationFrame(frame), frame = 0; }); resizeObserver.observe(host); intersection.observe(host); resize(); frame = requestAnimationFrame(render);
return () => { if (frame) cancelAnimationFrame(frame); resizeObserver.disconnect(); intersection.disconnect(); gl.deleteBuffer(buffer); gl.deleteShader(vertex); gl.deleteShader(fragment); gl.deleteProgram(program); };
}, []);
const options = optionsRef.current; return <div ref={hostRef} className={`threeui-background stream-convergence${className ? ` ${className}` : ""}`}><canvas ref={canvasRef} style={{ opacity: options.opacity, filter: `hue-rotate(${options.hue}deg) saturate(${options.saturation}) brightness(${options.brightness})`, transform: `scale(${options.scale})` }} /></div>;
}
/* ===== src/shaders/stream-convergence/streamConvergenceShaders.ts (typescript) ===== */
export const STREAM_CONVERGENCE_VERTEX_SHADER = `
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = position * 0.5 + 0.5;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
export const STREAM_CONVERGENCE_CANONICAL_VERTEX_SHADER = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
export const STREAM_CONVERGENCE_FRAGMENT_SHADER = `
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_interactive_fidelity;
varying vec2 vUv;
mat2 rotate2d(float _angle){
return mat2(cos(_angle),-sin(_angle),
sin(_angle),cos(_angle));
}
void main() {
vec2 p = vUv * 2.0 - 1.0;
p.x *= u_resolution.x / u_resolution.y;
p = rotate2d(0.55) * p;
vec3 color = vec3(0.0);
float spread = 0.06 * (0.3 + u_interactive_fidelity * 0.7);
for(int i = 0; i < 3; i++) {
float offset = float(1 - i) * spread;
float y = p.y + offset + (sin(p.x * 2.5 - u_time * 1.5) * 0.12);
float wave = smoothstep(0.85, 0.99, sin(y * 6.0 + u_time * 2.0) * 0.5 + 0.5);
x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20x20
// Modulating color mixing logic for the violet-indigo theme
if(i == 0) color.r += wave * 1.2;x20
if(i == 1) color.g += wave * 0.5;x20
if(i == 2) color.b += wave * 1.8;x20
}
float vignette = exp(-length(vUv * 2.0 - 1.0) * 0.8);
color *= vignette;
gl_FragColor = vec4(color, 1.0);
}
`;
[/hidecontent]