299 lines
11 KiB
HTML
299 lines
11 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Water-like Cursor Ripples — Strong Chromatic Dispersion</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
width: 100%;
|
||
height: 100%;
|
||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial;
|
||
background: #000000; /* black background */
|
||
color: #ffefcc; /* keep nav/footer readable */
|
||
overflow: hidden;
|
||
}
|
||
nav {
|
||
position: fixed; top: 0; left: 0; right: 0;
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 16px 24px; z-index: 2; pointer-events: none;
|
||
}
|
||
.nav-left, .nav-right { display: flex; gap: 16px; align-items: center; pointer-events: auto; }
|
||
.logo { font-weight: 700; letter-spacing: 0.5px; }
|
||
.nav-right button {
|
||
background: transparent; color: #ffefcc; border: 1px solid #ffefcc66;
|
||
border-radius: 999px; padding: 8px 14px; cursor: pointer;
|
||
}
|
||
footer {
|
||
position: fixed; bottom: 0; left: 0; right: 0;
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 16px 24px; z-index: 2; pointer-events: none;
|
||
}
|
||
footer .title { width: 40%; font-size: clamp(24px, 6vw, 64px); line-height: 1.1; font-weight: 800; }
|
||
footer .links { display: flex; gap: 20px; pointer-events: auto; }
|
||
canvas.webgl { position: fixed; inset: 0; width: 100vw; height: 100vh; display: block; z-index: 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<nav>
|
||
<div class="nav-left">
|
||
<div class="logo">YP</div>
|
||
<p>Work</p><p>About</p><p>Contact</p>
|
||
</div>
|
||
<div class="nav-right"><button>Get Started</button></div>
|
||
</nav>
|
||
|
||
<footer>
|
||
<div class="title">YP</div>
|
||
<div class="links">
|
||
<a href="#" style="color:#ffefcc;">Twitter</a>
|
||
<a href="#" style="color:#ffefcc;">Instagram</a>
|
||
<a href="#" style="color:#ffefcc;">Discord</a>
|
||
</div>
|
||
</footer>
|
||
|
||
<canvas class="webgl"></canvas>
|
||
|
||
<script type="module">
|
||
import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
|
||
|
||
// Config
|
||
const DPR_MAX = 2;
|
||
const BASE_TEXT = 'YOUNG PANDAS';
|
||
const BG_COLOR = '#000000'; // black background on canvas texture
|
||
const TEXT_COLOR = '#ffffff'; // white center text
|
||
|
||
// Renderer
|
||
const canvas = document.querySelector('canvas.webgl');
|
||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, DPR_MAX));
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
||
// Scenes & Camera
|
||
const simScene = new THREE.Scene();
|
||
const mainScene = new THREE.Scene();
|
||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||
|
||
// Render targets
|
||
let width = Math.floor(window.innerWidth * renderer.getPixelRatio());
|
||
let height = Math.floor(window.innerHeight * renderer.getPixelRatio());
|
||
const rtOptions = {
|
||
minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter,
|
||
wrapS: THREE.ClampToEdgeWrapping, wrapT: THREE.ClampToEdgeWrapping,
|
||
type: THREE.HalfFloatType ?? THREE.FloatType, format: THREE.RGBAFormat,
|
||
depthBuffer: false, stencilBuffer: false
|
||
};
|
||
let rta = new THREE.WebGLRenderTarget(width, height, rtOptions);
|
||
let rtb = new THREE.WebGLRenderTarget(width, height, rtOptions);
|
||
|
||
// Text Canvas -> Texture
|
||
let textCanvas, textCtx, textTexture;
|
||
function makeTextTexture() {
|
||
const dpr = renderer.getPixelRatio();
|
||
width = Math.floor(window.innerWidth * dpr);
|
||
height = Math.floor(window.innerHeight * dpr);
|
||
|
||
textCanvas = document.createElement('canvas');
|
||
textCanvas.width = width; textCanvas.height = height;
|
||
textCtx = textCanvas.getContext('2d', { alpha: true });
|
||
|
||
// Black background
|
||
textCtx.fillStyle = BG_COLOR;
|
||
textCtx.fillRect(0, 0, width, height);
|
||
|
||
// Orange center text
|
||
const fontPx = Math.floor(Math.min(width, height) * 0.18);
|
||
textCtx.fillStyle = TEXT_COLOR;
|
||
textCtx.textAlign = 'center'; textCtx.textBaseline = 'middle';
|
||
textCtx.font = `800 ${fontPx}px Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial`;
|
||
textCtx.fillText(BASE_TEXT, width * 0.5, height * 0.5);
|
||
|
||
if (textTexture) textTexture.dispose();
|
||
textTexture = new THREE.CanvasTexture(textCanvas);
|
||
textTexture.needsUpdate = true;
|
||
textTexture.minFilter = THREE.LinearFilter;
|
||
textTexture.magFilter = THREE.LinearFilter;
|
||
textTexture.format = THREE.RGBAFormat;
|
||
}
|
||
makeTextTexture();
|
||
|
||
// Geometry
|
||
const quad = new THREE.PlaneGeometry(2, 2);
|
||
|
||
// Shaders
|
||
const passThroughVert = `
|
||
varying vec2 vUv;
|
||
void main(){ vUv = uv; gl_Position = vec4(position, 1.0); }
|
||
`;
|
||
|
||
// Ripple simulation (tight brush, fast decay)
|
||
const simFrag = `
|
||
precision highp float;
|
||
varying vec2 vUv;
|
||
uniform sampler2D uTexture;
|
||
uniform vec2 uResolution;
|
||
uniform vec2 uMouse;
|
||
uniform float uTime;
|
||
void main(){
|
||
vec2 texel = 1.0 / uResolution;
|
||
vec2 data = texture2D(uTexture, vUv).xy;
|
||
float h = data.x;
|
||
float hPrev = data.y;
|
||
|
||
float hL = texture2D(uTexture, vUv - vec2(texel.x, 0.0)).x;
|
||
float hR = texture2D(uTexture, vUv + vec2(texel.x, 0.0)).x;
|
||
float hT = texture2D(uTexture, vUv + vec2(0.0, texel.y)).x;
|
||
float hB = texture2D(uTexture, vUv - vec2(0.0, texel.y)).x;
|
||
float sum = hL + hR + hT + hB;
|
||
|
||
float hNew = (sum * 0.5 - hPrev);
|
||
hNew *= 0.985;
|
||
|
||
vec2 frag = vUv * uResolution;
|
||
float radius = 8.0;
|
||
float dist = length(frag - uMouse);
|
||
float impulse = exp(-dist*dist/(2.0*radius*radius));
|
||
hNew += 0.25 * impulse;
|
||
|
||
hNew = mix(hNew, 0.0, 0.01);
|
||
gl_FragColor = vec4(hNew, h, 0.0, 1.0);
|
||
}
|
||
`;
|
||
|
||
// Render with strong, masked chromatic dispersion (offsets in pixels)
|
||
const renderFrag = `
|
||
precision highp float;
|
||
varying vec2 vUv;
|
||
uniform sampler2D uSim;
|
||
uniform sampler2D uText;
|
||
uniform vec2 uResolution;
|
||
uniform float uRefract;
|
||
uniform float uCAPixels; // RGB shift in pixels (large for strong separation)
|
||
uniform vec2 uCAMask; // gradient thresholds for where CA applies
|
||
|
||
void main(){
|
||
vec2 texel = 1.0 / uResolution;
|
||
|
||
// Height gradients
|
||
float hX = texture2D(uSim, vUv + vec2(texel.x, 0.0)).x
|
||
- texture2D(uSim, vUv - vec2(texel.x, 0.0)).x;
|
||
float hY = texture2D(uSim, vUv + vec2(0.0, texel.y)).x
|
||
- texture2D(uSim, vUv - vec2(0.0, texel.y)).x;
|
||
|
||
// Normal and base refraction
|
||
vec3 normal = normalize(vec3(-hX, -hY, 1.0));
|
||
vec2 baseUV = vUv + normal.xy * uRefract;
|
||
|
||
// Ripple strength mask (lower thresholds -> more CA on ripples)
|
||
float g = length(vec2(hX, hY));
|
||
float mask = smoothstep(uCAMask.x, uCAMask.y, g);
|
||
|
||
// Direction along gradient and large pixel-based offsets
|
||
vec2 dir = normalize(vec2(hX, hY) + 1e-6);
|
||
vec2 px = texel * uCAPixels;
|
||
|
||
// Exaggerated dispersion: push red/blue more than green
|
||
// Red goes +dir, Blue goes -dir, Green slightly centered/offset
|
||
vec2 rUV = baseUV + dir * (px * 1.30);
|
||
vec2 gUV = baseUV + dir * (px * 0.20);
|
||
vec2 bUV = baseUV - dir * (px * 1.35);
|
||
|
||
float r = texture2D(uText, rUV).r;
|
||
float gC = texture2D(uText, gUV).g;
|
||
float b = texture2D(uText, bUV).b;
|
||
|
||
vec4 caColor = vec4(r, gC, b, 1.0);
|
||
vec4 base = texture2D(uText, baseUV);
|
||
|
||
// Mix CA in only on ripples
|
||
vec4 color = mix(base, caColor, mask);
|
||
|
||
// Mild lighting accent
|
||
float light = dot(normal, normalize(vec3(0.0, 0.0, 1.0)));
|
||
color.rgb += 0.08 * light;
|
||
|
||
gl_FragColor = color;
|
||
}
|
||
`;
|
||
|
||
// Materials & meshes
|
||
const simUniforms = {
|
||
uTexture: { value: rta.texture },
|
||
uResolution: { value: new THREE.Vector2(width, height) },
|
||
uMouse: { value: new THREE.Vector2(-1.0, -1.0) },
|
||
uTime: { value: 0 }
|
||
};
|
||
const simMat = new THREE.ShaderMaterial({ vertexShader: passThroughVert, fragmentShader: simFrag, uniforms: simUniforms });
|
||
simScene.add(new THREE.Mesh(quad, simMat));
|
||
|
||
const renderUniforms = {
|
||
uSim: { value: rta.texture },
|
||
uText: { value: textTexture },
|
||
uResolution: { value: new THREE.Vector2(width, height) },
|
||
uRefract: { value: 0.8 },
|
||
uCAPixels: { value: 8.0 }, // big shift: increase to 10–14 for even more
|
||
uCAMask: { value: new THREE.Vector2(0.00008, 0.0005) } // lower => CA engages more
|
||
};
|
||
const renderMat = new THREE.ShaderMaterial({
|
||
vertexShader: passThroughVert, fragmentShader: renderFrag, uniforms: renderUniforms, transparent: true
|
||
});
|
||
mainScene.add(new THREE.Mesh(quad, renderMat));
|
||
|
||
// Mouse input
|
||
const mouse = new THREE.Vector2(-1, -1);
|
||
function setMouseFromEvent(e){
|
||
const dpr = renderer.getPixelRatio();
|
||
const rect = renderer.domElement.getBoundingClientRect();
|
||
const x = (e.clientX - rect.left) * dpr;
|
||
const y = (e.clientY - rect.top) * dpr;
|
||
mouse.set(x, (rect.height * dpr) - y);
|
||
}
|
||
renderer.domElement.addEventListener('mousemove', (e)=>{ setMouseFromEvent(e); simUniforms.uMouse.value.copy(mouse); });
|
||
renderer.domElement.addEventListener('mouseleave', ()=>{ simUniforms.uMouse.value.set(-1.0, -1.0); });
|
||
|
||
// Resize
|
||
function onResize(){
|
||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, DPR_MAX));
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
const dpr = renderer.getPixelRatio();
|
||
width = Math.floor(window.innerWidth * dpr);
|
||
height = Math.floor(window.innerHeight * dpr);
|
||
|
||
rta.dispose(); rtb.dispose();
|
||
rta = new THREE.WebGLRenderTarget(width, height, rtOptions);
|
||
rtb = new THREE.WebGLRenderTarget(width, height, rtOptions);
|
||
|
||
simUniforms.uResolution.value.set(width, height);
|
||
renderUniforms.uResolution.value.set(width, height);
|
||
|
||
makeTextTexture();
|
||
renderUniforms.uText.value = textTexture;
|
||
}
|
||
window.addEventListener('resize', onResize);
|
||
|
||
// Animate
|
||
const clock = new THREE.Clock();
|
||
function animate(){
|
||
simUniforms.uTime.value = clock.getElapsedTime();
|
||
|
||
// Sim: rta -> rtb
|
||
simUniforms.uTexture.value = rta.texture;
|
||
renderer.setRenderTarget(rtb);
|
||
renderer.render(simScene, camera);
|
||
|
||
// Render with latest sim
|
||
renderUniforms.uSim.value = rtb.texture;
|
||
renderer.setRenderTarget(null);
|
||
renderer.render(mainScene, camera);
|
||
|
||
// Swap
|
||
const tmp = rta; rta = rtb; rtb = tmp;
|
||
|
||
requestAnimationFrame(animate);
|
||
}
|
||
animate();
|
||
</script>
|
||
</body>
|
||
</html>
|