nexu-io--open-design
156 行
9.7 KiB
HTML
156 行
9.7 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Distortion & Grain — Open Design</title>
|
|
<!--
|
|
Faithful single-file port of jankohlbach's "On-Scroll Image Distortion & Grain"
|
|
(Codrops, MIT). Original: https://github.com/jankohlbach/codrops-shader-on-scroll
|
|
Vertical gallery: WebGL planes synced to DOM images bend with scroll velocity
|
|
(vertex deform), distort under the cursor via simplex noise (fragment), and
|
|
carry a film grain. Native scroll + self-computed velocity (no Lenis). Three.js
|
|
via CDN; images original (generated, not the author's Midjourney set).
|
|
Simplex noise © Ashima Arts (MIT). Runs in powered preview.
|
|
-->
|
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Manrope:wght@700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
*{box-sizing:border-box} :root{ --bg:#0a0a0c; --ink:#ededf0; --mut:#82828a; }
|
|
html,body{ margin:0; background:var(--bg); color:var(--ink); font-family:'Manrope',system-ui,sans-serif; -webkit-font-smoothing:antialiased; }
|
|
canvas#gl{ position:fixed; inset:0; width:100%; height:100%; z-index:2; pointer-events:none; }
|
|
.frame{ position:fixed; inset:0; z-index:5; pointer-events:none; padding:1.4rem 1.6rem; display:flex; justify-content:space-between; align-items:flex-start;
|
|
font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.22em; text-transform:uppercase; color:var(--mut); }
|
|
.frame .dot{ width:7px;height:7px;border-radius:50%;background:#b8ff3c;box-shadow:0 0 12px #b8ff3c;display:inline-block;margin-right:8px;vertical-align:middle;}
|
|
.frame a{ color:var(--mut); pointer-events:auto; }
|
|
.wrap{ max-width:760px; margin:0 auto; padding:0 1.6rem; }
|
|
header.intro{ padding:24vh 0 14vh; }
|
|
header.intro h1{ margin:0; font-size:clamp(40px,7vw,96px); font-weight:800; line-height:0.92; letter-spacing:-0.03em; }
|
|
header.intro p{ margin:1.2rem 0 0; max-width:40ch; font-family:'IBM Plex Mono',monospace; font-size:13px; color:var(--mut); line-height:1.6; }
|
|
.fig{ margin:0 0 20vh; }
|
|
.fig img{ display:block; width:100%; height:auto; opacity:0; border-radius:6px; } /* WebGL draws it */
|
|
.fig .cap{ margin-top:0.8rem; font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.06em; text-transform:uppercase; color:var(--mut);
|
|
display:flex; justify-content:space-between; }
|
|
footer{ padding:0 0 22vh; font-family:'IBM Plex Mono',monospace; font-size:11px; color:var(--mut); letter-spacing:0.2em; text-transform:uppercase; text-align:center; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="gl"></canvas>
|
|
<div class="frame"><span><span class="dot"></span>Open Design · WebGL Preview</span>
|
|
<span>Effect: <a href="https://github.com/jankohlbach/codrops-shader-on-scroll" target="_blank">Jan Kohlbach / Codrops</a></span></div>
|
|
<div class="wrap">
|
|
<header class="intro"><h1>Scroll to<br>Distort</h1>
|
|
<p>Each frame bends with scroll velocity and ripples under your cursor — a simplex-noise distortion with a film grain, running live.</p></header>
|
|
<div id="figs"></div>
|
|
<footer>Scroll ↓ · hover the frames</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const DATA=[
|
|
{ src:'dgs-img/1.jpg', name:'Fold I', idx:'01' },
|
|
{ src:'dgs-img/2.jpg', name:'Drape', idx:'02' },
|
|
{ src:'dgs-img/3.jpg', name:'Shadow', idx:'03' },
|
|
{ src:'dgs-img/4.jpg', name:'Crease', idx:'04' },
|
|
{ src:'dgs-img/5.jpg', name:'Veil', idx:'05' },
|
|
];
|
|
const figs=document.getElementById('figs');
|
|
DATA.forEach(d=>{ const f=document.createElement('figure'); f.className='fig';
|
|
f.innerHTML=`<img data-gl src="${d.src}" crossorigin="anonymous" alt="${d.name}">
|
|
<figcaption class="cap"><span>${d.idx} · ${d.name}</span><span>◍</span></figcaption>`;
|
|
figs.appendChild(f); });
|
|
</script>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
|
|
const CAMERA_POS=500;
|
|
const NOISE=`
|
|
vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
|
|
vec2 mod289(vec2 x){return x-floor(x*(1.0/289.0))*289.0;}
|
|
vec3 permute(vec3 x){return mod289(((x*34.0)+10.0)*x);}
|
|
float snoise(vec2 v){ const vec4 C=vec4(0.211324865405187,0.366025403784439,-0.577350269189626,0.024390243902439);
|
|
vec2 i=floor(v+dot(v,C.yy)); vec2 x0=v-i+dot(i,C.xx);
|
|
vec2 i1=(x0.x>x0.y)?vec2(1.0,0.0):vec2(0.0,1.0); vec4 x12=x0.xyxy+C.xxzz; x12.xy-=i1;
|
|
i=mod289(i); vec3 p=permute(permute(i.y+vec3(0.0,i1.y,1.0))+i.x+vec3(0.0,i1.x,1.0));
|
|
vec3 m=max(0.5-vec3(dot(x0,x0),dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)),0.0); m=m*m; m=m*m;
|
|
vec3 x=2.0*fract(p*C.www)-1.0; vec3 h=abs(x)-0.5; vec3 ox=floor(x+0.5); vec3 a0=x-ox;
|
|
m*=1.79284291400159-0.85373472095314*(a0*a0+h*h);
|
|
vec3 g; g.x=a0.x*x0.x+h.x*x0.y; g.yz=a0.yz*x12.xz+h.yz*x12.yw; return 130.0*dot(m,g); }
|
|
vec2 getCoverUvVert(vec2 uv, vec2 t, vec2 q){ vec2 r=vec2(min((q.x/q.y)/(t.x/t.y),1.0), min((q.y/q.x)/(t.y/t.x),1.0));
|
|
return vec2(uv.x*r.x+(1.0-r.x)*0.5, uv.y*r.y+(1.0-r.y)*0.5); }`;
|
|
const VERT=`${NOISE}
|
|
uniform float uScrollVelocity; uniform vec2 uTextureSize; uniform vec2 uQuadSize;
|
|
out vec2 vUv; out vec2 vUvCover;
|
|
vec3 deform(vec3 pos, vec2 uv){ pos.y -= sin(uv.x*3.14159265)*min(abs(uScrollVelocity),5.0)*sign(uScrollVelocity)*-0.01; return pos; }
|
|
void main(){ vUv=uv; vUvCover=getCoverUvVert(uv,uTextureSize,uQuadSize);
|
|
gl_Position=projectionMatrix*modelViewMatrix*vec4(deform(position,vUvCover),1.0); }`;
|
|
const FRAG=`${NOISE}
|
|
precision highp float;
|
|
uniform sampler2D uTexture; uniform vec2 uQuadSize; uniform float uScrollVelocity; uniform float uMouseEnter; uniform vec2 uMouseOverPos; uniform float uTime;
|
|
in vec2 vUv; in vec2 vUvCover; out vec4 outColor;
|
|
void main(){ vec2 tc=vUvCover;
|
|
float aspect=uQuadSize.y/uQuadSize.x;
|
|
float circle=1.0-distance(vec2(uMouseOverPos.x,(1.0-uMouseOverPos.y)*aspect), vec2(vUv.x,vUv.y*aspect))*15.0;
|
|
float n=snoise(gl_FragCoord.xy);
|
|
tc.x+=mix(0.0, circle*n*0.01, uMouseEnter+abs(uScrollVelocity)*0.1);
|
|
tc.y+=mix(0.0, circle*n*0.01, uMouseEnter+abs(uScrollVelocity)*0.1);
|
|
vec3 col=texture(uTexture,tc).rgb;
|
|
// film grain — cheap hash (per-pixel snoise every frame is far too heavy)
|
|
float g=fract(sin(dot(gl_FragCoord.xy, vec2(12.9898,78.233)) + uTime*57.0)*43758.5453);
|
|
col+=(g-0.5)*0.08;
|
|
outColor=vec4(col,1.0);
|
|
}`;
|
|
const canvas=document.getElementById('gl');
|
|
const renderer=new THREE.WebGLRenderer({canvas,alpha:true,antialias:true});
|
|
renderer.setSize(innerWidth,innerHeight); renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
|
|
const scene=new THREE.Scene();
|
|
const calcFov=z=>2*Math.atan((innerHeight/2)/z)*(180/Math.PI);
|
|
const camera=new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 10, 1000);
|
|
camera.position.z=CAMERA_POS; camera.fov=calcFov(CAMERA_POS); camera.updateProjectionMatrix();
|
|
const geometry=new THREE.PlaneGeometry(1,1,24,24);
|
|
const loader=new THREE.TextureLoader();
|
|
const lerp=(a,b,t)=>a+(b-a)*t;
|
|
const store=[];
|
|
class Media{
|
|
constructor(el){ this.el=el; this.enter=0; this.over={cx:0.5,cy:0.5,tx:0.5,ty:0.5};
|
|
this.mat=new THREE.ShaderMaterial({ glslVersion:THREE.GLSL3, vertexShader:VERT, fragmentShader:FRAG, uniforms:{
|
|
uTexture:{value:null}, uTextureSize:{value:new THREE.Vector2(1,1)}, uQuadSize:{value:new THREE.Vector2(1,1)},
|
|
uScrollVelocity:{value:0}, uMouseEnter:{value:0}, uMouseOverPos:{value:new THREE.Vector2(0.5,0.5)}, uTime:{value:0} }});
|
|
this.mesh=new THREE.Mesh(geometry,this.mat); scene.add(this.mesh);
|
|
this.mat.uniforms.uTexture.value=loader.load(el.src,t=>{ t.colorSpace=THREE.SRGBColorSpace;
|
|
this.mat.uniforms.uTextureSize.value.set(t.image.width,t.image.height); });
|
|
el.addEventListener('pointerenter',()=>this.tEnter=1);
|
|
el.addEventListener('pointerleave',()=>{ this.tEnter=0; this.over.tx=0.5; this.over.ty=0.5; });
|
|
el.addEventListener('pointermove',e=>{ const b=el.getBoundingClientRect(); this.over.tx=(e.clientX-b.left)/b.width; this.over.ty=(e.clientY-b.top)/b.height; });
|
|
this.tEnter=0; this.measure();
|
|
}
|
|
measure(){ this.b=this.el.getBoundingClientRect(); this.top=this.b.top+window.scrollY; this.left=this.b.left;
|
|
this.w=this.b.width; this.h=this.b.height; this.mesh.scale.set(this.w,this.h,1);
|
|
this.mat.uniforms.uQuadSize.value.set(this.w,this.h); }
|
|
update(scrollY,vel,time){
|
|
const vy=this.top - scrollY; // viewport-space top
|
|
this.mesh.visible = vy < innerHeight+300 && vy+this.h > -300; // cull off-screen
|
|
if(!this.mesh.visible) return;
|
|
this.mesh.position.x=this.left - innerWidth/2 + this.w/2;
|
|
this.mesh.position.y=-this.top + innerHeight/2 - this.h/2 + scrollY;
|
|
this.enter=lerp(this.enter,this.tEnter,0.1);
|
|
this.over.cx=lerp(this.over.cx,this.over.tx,0.06); this.over.cy=lerp(this.over.cy,this.over.ty,0.06);
|
|
const u=this.mat.uniforms; u.uScrollVelocity.value=vel; u.uMouseEnter.value=this.enter;
|
|
u.uMouseOverPos.value.set(this.over.cx,this.over.cy); u.uTime.value=time;
|
|
}
|
|
}
|
|
let prevY=window.scrollY, vel=0;
|
|
function resize(){ const w=innerWidth,h=innerHeight; if(w===resize._w&&h===resize._h)return; resize._w=w; resize._h=h;
|
|
renderer.setSize(w,h); camera.aspect=w/h; camera.fov=calcFov(CAMERA_POS); camera.updateProjectionMatrix(); store.forEach(m=>m.measure()); }
|
|
addEventListener('resize',resize);
|
|
function boot(){
|
|
document.querySelectorAll('img[data-gl]').forEach(im=>store.push(new Media(im)));
|
|
(function frame(t){ requestAnimationFrame(frame); resize(); const time=(t||0)/1000;
|
|
const y=window.scrollY; const raw=y-prevY; prevY=y; vel=lerp(vel,raw*0.06,0.1); // smoothed scroll velocity
|
|
store.forEach(m=>m.update(y,vel,time)); renderer.render(scene,camera);
|
|
})();
|
|
}
|
|
(function whenReady(cb){ const imgs=[...document.querySelectorAll('img[data-gl]')]; let n=0,t=imgs.length;
|
|
const d=()=>{ if(++n>=t) cb(); }; imgs.forEach(im=>{ if(im.complete&&im.naturalWidth) d(); else{ im.onload=d; im.onerror=d; } }); if(!t)cb(); })(boot);
|
|
</script>
|
|
</body>
|
|
</html>
|