← GPT-MOTION-GRAPHICS
CODE · 15.7 KB

experience.js

Workspace snapshot · 09/04 13:41

const canvas = document.querySelector("#world");
const gl = canvas.getContext("webgl2", { antialias: false, alpha: false, preserveDrawingBuffer: true });
if (!gl) throw new Error("WebGL 2 is required");

const VERTEX = `#version 300 es
in vec2 position;
out vec2 vUv;
void main(){vUv=position*.5+.5;gl_Position=vec4(position,0.,1.);}`;

const FRAGMENT = `#version 300 es
precision highp float;
in vec2 vUv;
out vec4 outColor;
uniform sampler2D uBefore;
uniform sampler2D uDusk;
uniform sampler2D uAfter;
uniform vec2 uResolution;
uniform vec2 uPointer;
uniform float uTime;
uniform float uStory;
uniform float uAlive;
uniform float uHoldMemory;
uniform float uInheritance;
float hash(vec2 p){p=fract(p*vec2(123.34,345.45));p+=dot(p,p+34.345);return fract(p.x*p.y);}
float noise(vec2 p){vec2 i=floor(p),f=fract(p);f=f*f*(3.-2.*f);return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),mix(hash(i+vec2(0,1)),hash(i+1.),f.x),f.y);}
float fbm(vec2 p){float v=0.,a=.5;mat2 r=mat2(.8,-.6,.6,.8);for(int i=0;i<6;i++){v+=a*noise(p);p=r*p*2.03+11.7;a*=.5;}return v;}
float ease(float a,float b,float x){x=clamp((x-a)/(b-a),0.,1.);return x*x*(3.-2.*x);}
float bell(float a,float b,float c,float x){return ease(a,b,x)*(1.-ease(b,c,x));}
float sdSegment(vec2 p,vec2 a,vec2 b){vec2 pa=p-a,ba=b-a;float h=clamp(dot(pa,ba)/dot(ba,ba),0.,1.);return length(pa-ba*h);}
vec2 coverUv(vec2 uv){float screen=uResolution.x/uResolution.y;float image=1672./943.;if(screen>image){float h=image/screen;uv.y=(uv.y-.5)*h+.5;}else{float w=screen/image;uv.x=(uv.x-.5)*w+.5;}return uv;}
vec3 filmSample(sampler2D tex,vec2 uv,float aberration){vec2 p=coverUv(uv);vec2 d=(p-.5)*aberration;return vec3(texture(tex,p+d).r,texture(tex,p).g,texture(tex,p-d).b);}
void main(){
  vec2 uv=vUv;float s=uStory,t=uTime;
  float n=fbm(uv*3.2+vec2(t*.027,-t*.019));float detail=fbm(uv*11.-vec2(t*.041,t*.023));
  vec2 drift=vec2(sin(t*.17+n*2.),cos(t*.13-detail))*.004*uAlive;
  float awareness=bell(.25,.36,.47,s),duskIn=ease(.405,.455,s),duskOut=ease(.585,.67,s),birth=bell(.54,.66,.76,s),rupture=ease(.72,.805,s),after=ease(.80,.91,s);
  vec2 beforeUv=uv+drift;beforeUv=(beforeUv-.5)*(1.-s*.025)+.5+vec2(sin(t*.08),cos(t*.065))*.005;
  vec3 before=filmSample(uBefore,beforeUv,.0015*awareness);
  vec2 duskUv=uv+drift*1.7;float inhale=sin(t*1.35)*.0035*duskIn*(1.-duskOut);duskUv=(duskUv-.5)*(1.+inhale)+.5;
  vec3 dusk=filmSample(uDusk,duskUv,.0025*birth);
  vec2 afterUv=uv+vec2(n-.5,detail-.5)*.006*after;afterUv=(afterUv-.5)*(1.-after*.035)+.5;
  vec3 afterCol=filmSample(uAfter,afterUv,.0018*after);
  float pointerHold=1.-clamp(abs(uPointer.x-.5)*2.,0.,1.);float boundary=mix(.5,uPointer.x,.72*duskIn*(1.-duskOut));
  float folded=abs(uv.x-boundary)+abs(uv.y-.53)*.16;float fibers=sin((uv.y+n*.15)*115.+t*2.)*.012+sin((uv.y-detail*.08)*41.-t*.7)*.018;
  float front=1.-smoothstep(.02+.52*duskIn,.045+.52*duskIn,folded+fibers);front*=1.-duskOut;
  float fullDusk=ease(.48,.54,s)*(1.-duskOut);float meeting=max(front,fullDusk);vec3 col=mix(before,dusk,meeting);
  float frontEdge=exp(-85.*abs((folded+fibers)-(.035+.52*duskIn)))*duskIn*(1.-duskOut);col+=vec3(.88,.76,.52)*frontEdge*(.16+.36*pointerHold+.48*uHoldMemory);
  float transfer=bell(.575,.655,.735,s);vec2 contact=vec2(.52,.49);float cord=sdSegment(uv,vec2(.25,.63),contact)+sin(uv.x*47.+t*2.1)*.004;float cord2=sdSegment(uv,vec2(.77,.34),contact)+sin(uv.x*39.-t*1.7)*.004;
  float conduit=exp(-190.*min(cord,cord2))*transfer;float pulse=.35+.65*pow(max(0.,sin(t*7.-length(uv-contact)*95.)),10.);col+=vec3(.96,.72,.27)*conduit*pulse*1.45;
  float shellR=length((uv-contact)*vec2(1.,1.32));float shell=exp(-210.*abs(shellR-(.012+.072*ease(.59,.72,s))));float yolk=1.-smoothstep(.012,.048,abs(shellR-.025-fbm((uv-contact)*54.+t*.12)*.018));
  col=mix(col,vec3(.055,.036,.018),yolk*transfer*.58);col+=vec3(1.,.69,.2)*shell*transfer*(.55+pulse);
  float tearNoise=(n-.5)*.07+(detail-.5)*.018;float tear=abs(uv.x-.5+tearNoise);float tearEdge=exp(-180.*tear)*bell(.70,.77,.84,s);float pull=rupture*.045;
  vec2 splitUv=uv+vec2(uv.x<.5?-pull:pull,0.);vec3 split=filmSample(uDusk,splitUv,.004*rupture);col=mix(col,split,rupture*(1.-after));col+=vec3(1.,.94,.78)*tearEdge*1.8;
  float shatter=bell(.735,.775,.83,s);float shardLines=pow(max(0.,sin(atan(uv.y-.5,uv.x-.5)*11.+tearNoise*50.)),42.)*shatter*(1.-smoothstep(.02,.44,length(uv-.5)));col+=vec3(1.,.88,.62)*shardLines*1.8;
  vec2 seed=mix(contact,vec2(.555,.46),ease(.78,.88,s));vec2 anatomy=(uv-seed)*vec2(1.,uResolution.y/uResolution.x*1.78);float angle=atan(anatomy.y,anatomy.x);float radius=length(anatomy);
  float inheritance=uInheritance*.7;float unfold=ease(.82,.985,s);float breathing=1.+sin(t*1.15)*.018*after;
  float fan=pow(abs(sin(angle*5.+inheritance*2.4)),1.7);float edgeMemory=(n-.5)*.026+(detail-.5)*.009;float inheritedScale=mix(.72,1.06,uHoldMemory);float bladeRadius=unfold*(.045+.175*fan+edgeMemory)*breathing*inheritedScale;float blade=1.-smoothstep(bladeRadius-.018,bladeRadius+.014,radius);
  float darkGene=.5+.5*sin(angle*5.+inheritance*2.4);darkGene=darkGene*darkGene*(3.-2.*darkGene);vec3 inherited=mix(filmSample(uBefore,afterUv,.002),afterCol,darkGene);col=mix(col,inherited,blade*after*.58);
  float ribAngle=abs(sin(angle*5.+inheritance*2.4));float ribs=pow(max(0.,1.-ribAngle*24.),4.)*blade;float ring=pow(max(0.,sin(radius*124.-t*1.1)),28.)*blade;
  float spineX=seed.x+sin((uv.y-seed.y)*18.+t*.22)*.009+inheritance*(uv.y-seed.y)*.18;float spine=exp(-260.*abs(uv.x-spineX))*smoothstep(seed.y-.19*unfold,seed.y-.12*unfold,uv.y)*(1.-smoothstep(seed.y+.14*unfold,seed.y+.22*unfold,uv.y));
  float pupil=1.-smoothstep(.012,.034,radius);float inheritedPulse=(.3+.7*uHoldMemory)*(.45+.55*pow(max(0.,sin(t*2.3-radius*42.)),8.));col+=vec3(.86,.62,.23)*(ribs*.18+ring*.07+spine*.48+pupil*.52)*after*inheritedPulse;
  float transparentGene=(1.-darkGene)*blade*(.12+.22*n);col=mix(col,filmSample(uBefore,afterUv+vec2(.006*n,0.),.003),transparentGene*after*.26);
  float residual=after*exp(-11.*radius)*inheritedPulse;col+=vec3(.42,.31,.12)*residual;
  float caustic=pow(max(0.,sin((n+detail*.35)*22.+t*.32)),12.);col+=vec3(.18,.16,.13)*caustic*(.15+.5*birth);
  float vignette=smoothstep(.83,.25,length((uv-.5)*vec2(.84,1.)));col*=mix(.60,1.,vignette);col=1.-exp(-col*1.16);col=pow(col,vec3(.93));
  float grain=(hash(gl_FragCoord.xy+fract(t)*311.)-.5)*.032;float gate=smoothstep(0.,.035,uAlive);outColor=vec4((col+grain)*mix(.58,1.,gate),1.);
}`;

function shader(type,source){const result=gl.createShader(type);gl.shaderSource(result,source);gl.compileShader(result);if(!gl.getShaderParameter(result,gl.COMPILE_STATUS))throw new Error(gl.getShaderInfoLog(result));return result;}
const program=gl.createProgram();gl.attachShader(program,shader(gl.VERTEX_SHADER,VERTEX));gl.attachShader(program,shader(gl.FRAGMENT_SHADER,FRAGMENT));gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw new Error(gl.getProgramInfoLog(program));gl.useProgram(program);
const quad=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,quad);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),gl.STATIC_DRAW);const position=gl.getAttribLocation(program,"position");gl.enableVertexAttribArray(position);gl.vertexAttribPointer(position,2,gl.FLOAT,false,0,0);
const uniformNames=["uBefore","uDusk","uAfter","uResolution","uPointer","uTime","uStory","uAlive","uHoldMemory","uInheritance"];const uniforms=Object.fromEntries(uniformNames.map(name=>[name,gl.getUniformLocation(program,name)]));
function loadTexture(path,unit){const texture=gl.createTexture();gl.activeTexture(gl.TEXTURE0+unit);gl.bindTexture(gl.TEXTURE_2D,texture);gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,1,1,0,gl.RGBA,gl.UNSIGNED_BYTE,new Uint8Array([8,8,8,255]));gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);const image=new Image();image.onload=()=>{gl.activeTexture(gl.TEXTURE0+unit);gl.bindTexture(gl.TEXTURE_2D,texture);gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,true);gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,image);};image.src=path;gl.uniform1i([uniforms.uBefore,uniforms.uDusk,uniforms.uAfter][unit],unit);}
loadTexture("./assets/dusk-01-before.png",0);loadTexture("./assets/dusk-02-eight-seconds.png",1);loadTexture("./assets/dusk-03-what-remains.png",2);

const title=document.querySelector("#title"),begin=document.querySelector("#begin"),line=document.querySelector("#line"),chapterNo=document.querySelector("#chapterNo"),chapterName=document.querySelector("#chapterName"),timeline=document.querySelector("#timelineFill"),timecode=document.querySelector("#timecode"),gesture=document.querySelector("#gesture"),soundButton=document.querySelector("#sound"),flash=document.querySelector("#flash"),save=document.querySelector("#save");
const duration=48;let started=false,finished=false,startedAt=0,lastScene=-1,soundOn=false,audio=null,master=null,audioScore=null,nextBeat=0;let pointer={x:.5,y:.5,tx:.5,ty:.5,motion:0,active:false};let holdMemory=.48,inheritance=0,lastFrame=0;let textShowTimer=0,textHideTimer=0;window.__duskState={holdMemory,inheritance,soundOn};
const scenes=[
  {at:0,no:"I",name:"DAY WITHOUT NIGHT",text:"光は、自分に影があることを知らなかった。"},{at:7,no:"II",name:"NIGHT WITHOUT DAY",text:"闇は、自分に輪郭があることを知らなかった。"},{at:14,no:"III",name:"THE FIRST SHADOW",text:"先に届いたのは、姿ではなく変化だった。"},{at:20,no:"IV",name:"EIGHT SECONDS",text:"ここにいて。あと、八秒だけ。"},{at:28,no:"V",name:"THE THIRD LAW",text:"ふたりは、自分の一部を失って、ひとつを残した。"},{at:35,no:"VI",name:"DAWN IS VIOLENCE",text:"朝は、すべてを同じ場所には置いてくれない。"},{at:40,no:"VII",name:"WHAT REMAINS",text:"離れたあとにしか、ひらけないものがある。"}
];
function resize(){const dpr=Math.min(devicePixelRatio||1,innerWidth<700?1.25:1.65);const w=Math.floor(innerWidth*dpr),h=Math.floor(innerHeight*dpr);if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;gl.viewport(0,0,w,h);}}
function initAudio(){if(audio)return;audio=new AudioContext();master=audio.createGain();master.gain.value=0;master.connect(audio.destination);const filter=audio.createBiquadFilter();filter.type="lowpass";filter.frequency.value=260;filter.Q.value=1.6;filter.connect(master);const voices=[[55,"sine",.075],[73.42,"triangle",.022],[110,"sine",.012]].map(([hz,type,gain],i)=>{const osc=audio.createOscillator(),amp=audio.createGain();osc.type=type;osc.frequency.value=hz;amp.gain.value=gain;osc.detune.value=i===2?-7:0;osc.connect(amp).connect(filter);osc.start();return{osc,amp};});audioScore={filter,voices};}
function strike(when,frequency,duration=.35,volume=.08){if(!audio||!soundOn)return;const osc=audio.createOscillator(),gain=audio.createGain();osc.type="sine";osc.frequency.setValueAtTime(frequency,when);osc.frequency.exponentialRampToValueAtTime(frequency*.52,when+duration);gain.gain.setValueAtTime(.0001,when);gain.gain.exponentialRampToValueAtTime(volume,when+.012);gain.gain.exponentialRampToValueAtTime(.0001,when+duration);osc.connect(gain).connect(master);osc.start(when);osc.stop(when+duration+.03);}
function shapeSound(elapsed){if(!audioScore||!soundOn)return;let scene=0;for(let i=0;i<scenes.length;i++)if(elapsed>=scenes[i].at)scene=i;const filters=[240,170,430,1050,680,120,520],levels=[[.075,.006,.004],[.018,.035,.003],[.05,.028,.008],[.065,.055,.026],[.038,.025,.055],[.012,.008,.003],[.035,.018,.032]];audioScore.filter.frequency.setTargetAtTime(filters[scene],audio.currentTime,.28);audioScore.voices.forEach((voice,index)=>voice.amp.gain.setTargetAtTime(levels[scene][index],audio.currentTime,.32));if(elapsed>=nextBeat){const interval=scene===3?.64:scene===4?.82:scene===6?1.45:2.2;const pitch=scene===3?110+holdMemory*110:scene===6?165:72;strike(audio.currentTime+.01,pitch,scene===3?.26:.42,scene===3?.055:.026);nextBeat=elapsed+interval;}}
function setSound(next){initAudio();soundOn=next;audio.resume();master.gain.cancelScheduledValues(audio.currentTime);master.gain.linearRampToValueAtTime(next?.58:0,audio.currentTime+.7);soundButton.textContent=next?"SOUND ON":"SOUND OFF";}
function showScene(index){if(index===lastScene)return;lastScene=index;clearTimeout(textShowTimer);clearTimeout(textHideTimer);line.classList.remove("is-visible");const scene=scenes[index];chapterNo.textContent=scene.no;chapterName.textContent=scene.name;textShowTimer=setTimeout(()=>{line.textContent=scene.text;line.classList.add("is-visible");},380);textHideTimer=setTimeout(()=>line.classList.remove("is-visible"),index===scenes.length-1?6100:3600);gesture.classList.toggle("is-visible",scene.name==="EIGHT SECONDS");if(audio&&soundOn&&scene.name==="THE THIRD LAW"){strike(audio.currentTime+.08,220,.65,.12);strike(audio.currentTime+.42,330,.8,.06);}if(scene.name==="DAWN IS VIOLENCE"){flash.classList.remove("is-active");void flash.offsetWidth;flash.classList.add("is-active");if(audio&&soundOn){strike(audio.currentTime+.02,92,1.1,.18);master.gain.setValueAtTime(.58,audio.currentTime);master.gain.exponentialRampToValueAtTime(.001,audio.currentTime+.1);master.gain.linearRampToValueAtTime(.42,audio.currentTime+2.4);}}}
function startFilm(){started=true;finished=false;startedAt=performance.now();lastScene=-1;lastFrame=startedAt;holdMemory=.48;inheritance=0;nextBeat=0;title.classList.add("is-hidden");save.classList.remove("is-visible");setSound(true);}
function render(now){resize();const dt=Math.min(.05,Math.max(0,(now-lastFrame)/1000));lastFrame=now;pointer.x+=(pointer.tx-pointer.x)*.06;pointer.y+=(pointer.ty-pointer.y)*.06;pointer.motion*=.96;let elapsed=started?(now-startedAt)/1000:finished?duration:0;if(elapsed>=20&&elapsed<28&&pointer.active){const quality=1.-Math.min(1,Math.abs(pointer.x-.5)*2.2);holdMemory+=(quality-holdMemory)*dt*.9;inheritance+=(pointer.x-.5-inheritance)*dt*.75;}if(elapsed>=duration){elapsed=duration;finished=true;started=false;save.classList.add("is-visible");}const story=elapsed/duration;if(started||finished){let index=0;for(let i=0;i<scenes.length;i++)if(elapsed>=scenes[i].at)index=i;showScene(index);shapeSound(elapsed);timeline.style.width=`${story*100}%`;timecode.textContent=`00:${String(Math.floor(elapsed)).padStart(2,"0")}`;}window.__duskState={holdMemory,inheritance,soundOn};gl.uniform2f(uniforms.uResolution,canvas.width,canvas.height);gl.uniform2f(uniforms.uPointer,pointer.x,1.-pointer.y);gl.uniform1f(uniforms.uTime,now/1000);gl.uniform1f(uniforms.uStory,story);gl.uniform1f(uniforms.uAlive,started||finished?1:0);gl.uniform1f(uniforms.uHoldMemory,holdMemory);gl.uniform1f(uniforms.uInheritance,inheritance);gl.drawArrays(gl.TRIANGLES,0,3);requestAnimationFrame(render);}
addEventListener("pointermove",e=>{pointer.tx=e.clientX/innerWidth;pointer.ty=e.clientY/innerHeight;pointer.motion=1;pointer.active=true;});addEventListener("resize",resize);begin.addEventListener("click",startFilm);soundButton.addEventListener("click",()=>setSound(!soundOn));
document.querySelector("#restart").addEventListener("click",e=>{e.preventDefault();started=false;finished=false;lastScene=-1;clearTimeout(textShowTimer);clearTimeout(textHideTimer);line.classList.remove("is-visible");title.classList.remove("is-hidden");gesture.classList.remove("is-visible");save.classList.remove("is-visible");timeline.style.width="0";timecode.textContent="00:00";});
save.addEventListener("click",()=>{const a=document.createElement("a");a.download="eight-seconds-of-dusk.png";a.href=canvas.toDataURL("image/png");a.click();});resize();requestAnimationFrame(render);const testParams=new URLSearchParams(location.search);if(navigator.webdriver||testParams.get("autoplay")==="1"){startFilm();const testOffset=Number(testParams.get("at"));if(Number.isFinite(testOffset)&&testOffset>0)startedAt-=Math.min(testOffset,duration)*1000;}