mirror of
https://github.com/lynchmarkHQ/uwu.lynchmark.com.git
synced 2026-10-08 22:05:19 +00:00
.
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="description" content="Paperwind. An original, living portrait written entirely in letters and punctuation.">
|
||||
<title>paperwind</title>
|
||||
<style>
|
||||
*{box-sizing:border-box}
|
||||
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#fff}
|
||||
canvas{display:block;width:100%;height:100%;height:100dvh}
|
||||
</style>
|
||||
<canvas role="img" aria-label="Paperwind: an anime girl made entirely of flowing letters. She breathes and blinks as the wind moves through her long hair and carries little pieces of language away.">An anime girl, written in the wind.</canvas>
|
||||
<script>
|
||||
(() => {
|
||||
'use strict';
|
||||
const canvas=document.querySelector('canvas');
|
||||
const gl=canvas.getContext('webgl2',{alpha:true,antialias:false}),ctx=gl?null:canvas.getContext('2d');
|
||||
const PI=Math.PI,TAU=PI*2,sin=Math.sin,cos=Math.cos;
|
||||
const ink=['#241d33','#41334f','#6b577c','#a08aad','#d1c1da','#b96f91','#965373'];
|
||||
const poem='the wind turns my thoughts into little birds ';
|
||||
const whisper='a small wind a softer world ',dots='.,:;\'`';
|
||||
const points=[],clamp=(v,a=0,b=1)=>Math.max(a,Math.min(b,v));
|
||||
const rnd=n=>{const v=sin(n*127.1+311.7)*43758.5453;return v-Math.floor(v)};
|
||||
let width=0,height=0,scale=1,dpr=1,last=-1,dirty=true;
|
||||
|
||||
// Geometry only places type; the renderer caches these text glyphs.
|
||||
function path(a,...curves){
|
||||
const out=[a];
|
||||
for(const c of curves){
|
||||
const [x,y]=a,[u,v,w,z,b,d]=c;
|
||||
const n=Math.ceil((Math.hypot(u-x,v-y)+Math.hypot(w-u,z-v)+Math.hypot(b-w,d-z))/3);
|
||||
for(let i=1;i<=n;i++){
|
||||
const t=i/n,s=1-t;
|
||||
out.push([s*s*s*x+3*s*s*t*u+3*s*t*t*w+t*t*t*b,s*s*s*y+3*s*s*t*v+3*s*t*t*z+t*t*t*d]);
|
||||
}
|
||||
a=[b,d];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function within(x,y,p){
|
||||
let b=false;
|
||||
for(let i=0,j=p.length-1;i<p.length;j=i++){
|
||||
const [u,v]=p[i],[w,z]=p[j];
|
||||
if((v>y)!=(z>y)&&x<(w-u)*(y-v)/(z-v)+u)b=!b;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
function put(x,y,ch,size=8,tone=1,part='head',z=0,angle=0,extra={}){
|
||||
points.push({x,y,ch,size,tone,part,z,angle,id:points.length,...extra});
|
||||
}
|
||||
function trace(p,{text=poem,size=7.5,tone=1,part='head',z=0,follow=false,space=size*.66,mask=()=>false,extra={}}={}){
|
||||
let distance=0,k=0;
|
||||
for(let i=1;i<p.length;i++){
|
||||
const [x,y]=p[i],[a,b]=p[i-1];
|
||||
distance+=Math.hypot(x-a,y-b);
|
||||
if(distance<space)continue;
|
||||
distance%=space;
|
||||
const ch=text[k++%text.length];
|
||||
if(ch!==' '&&!mask(x,y))put(x,y,ch,size,tone,part,z,follow?Math.atan2(y-b,x-a):0,extra);
|
||||
}
|
||||
}
|
||||
function fill(p,{text=poem,dx=5.6,dy=7.3,size=8,tone=1,part='head',z=0,mask=()=>false,style=()=>({})}={}){
|
||||
const low=Math.min(...p.map(v=>v[1])),high=Math.max(...p.map(v=>v[1]));
|
||||
for(let y=low+dy*.5,row=0;y<high;y+=dy,row++){
|
||||
const cuts=[];
|
||||
for(let i=0,j=p.length-1;i<p.length;j=i++){
|
||||
const [a,b]=p[i],[c,d]=p[j];
|
||||
if((b>y)!=(d>y))cuts.push(a+(y-b)*(c-a)/(d-b));
|
||||
}
|
||||
cuts.sort((a,b)=>a-b);
|
||||
for(let k=0;k<cuts.length;k+=2){
|
||||
for(let x=Math.ceil(cuts[k]/dx)*dx+(row%2)*dx*.45;x<cuts[k+1];x+=dx){
|
||||
const id=row*107+Math.round(x/dx),r=rnd(id),s=style(x,y,r,id);
|
||||
if(s===false||mask(x,y))continue;
|
||||
const ch=text[(Math.round(x/dx)+row*3)%text.length];
|
||||
if(ch!==' ')put(x+(r-.5)*.8,y+(rnd(id+31)-.5)*.7,ch,size,tone,part,z,0,s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const face=path([399,278],[441,250,551,250,593,279],[631,313,640,408,605,474],[580,518,539,548,511,558],[480,553,436,529,410,488],[382,449,372,405,381,354],[384,318,390,292,399,278]);
|
||||
const neck=path([475,525],[483,554,484,576,455,595],[474,628,540,638,568,598],[544,579,546,551,550,528],[529,550,500,553,475,525]);
|
||||
const shirt=path([455,587],[418,593,378,606,351,628],[321,665,311,754,303,865],[372,886,650,887,717,865],[703,759,688,667,654,632],[631,611,590,601,564,591],[535,620,481,621,455,587]);
|
||||
const covered=(x,y)=>within(x,y,face)||within(x,y,neck)||within(x,y,shirt);
|
||||
const hair=path([350,288],[327,204,405,122,508,124],[609,111,681,171,694,275],[715,398,687,555,731,675],[758,739,801,778,829,752],[827,797,773,825,731,803],[706,791,680,769,664,744],[686,813,725,840,755,832],[712,877,648,830,628,756],[611,816,608,871,554,881],[583,836,561,723,565,655],[509,696,448,695,414,632],[403,741,350,849,275,859],[317,814,322,769,329,717],[277,788,225,800,177,767],[227,783,248,725,268,654],[298,544,315,431,325,361],[329,330,342,302,350,288]);
|
||||
fill(hair,{part:'hair',size:8.6,z:-28,mask:covered,style:(x,y,r)=>{
|
||||
const s=sin(x*.081+sin(y*.008)*2.6),light=Math.exp(-(((y-217-(x-505)**2*.0018)/23)**2));
|
||||
return r<light*.55?false:{tone:light>.45?3:s>.62?3:s>-.1?2:1,alpha:.91};
|
||||
}});
|
||||
trace(hair,{text:'whisper . ',size:7.2,tone:2,part:'hair',mask:covered,z:-28});
|
||||
|
||||
// Long, separately moving currents of language form the hair.
|
||||
const currents=[
|
||||
[[462,144],[365,189,378,365,329,578],[298,723,275,803,218,788]],
|
||||
[[479,140],[375,197,393,350,352,536],[320,692,349,795,290,850]],
|
||||
[[419,171],[340,262,365,381,321,577],[297,688,267,765,207,775]],
|
||||
[[401,192],[341,306,345,406,305,581],[278,681,271,740,237,756]],
|
||||
[[523,134],[636,118,700,251,670,424],[650,588,737,791,801,778]],
|
||||
[[557,146],[668,173,670,300,655,446],[646,590,686,748,752,790]],
|
||||
[[589,165],[671,233,647,372,641,488],[637,635,679,803,726,844]],
|
||||
[[616,198],[665,323,633,444,632,570],[631,700,660,823,701,839]],
|
||||
[[395,325],[381,473,398,562,375,689],[356,775,330,829,311,836]],
|
||||
[[645,316],[671,424,610,581,618,702],[625,783,594,855,575,866]]
|
||||
];
|
||||
currents.forEach((c,i)=>{
|
||||
for(let j=-1;j<=1;j++){
|
||||
const p=path(...c).map(([x,y])=>[x+j*5.4,y]);
|
||||
trace(p,{text:j%2?whisper:poem,size:6.4+(i%3)*.3,tone:j===0?1:3,part:'hair',z:-18,follow:true,mask:covered});
|
||||
}
|
||||
});
|
||||
|
||||
// The blouse fades into paper; the collar remains precise.
|
||||
fill(shirt,{text:'. : , ',dx:7,dy:8,size:7,tone:4,part:'body',z:4,style:(x,y,r)=>{
|
||||
const edge=Math.abs(x-510)/215,fade=clamp((879-y)/135);
|
||||
return r>(.13+edge*.3)*fade?false:{alpha:fade};
|
||||
}});
|
||||
const shoulderL=path([455,590],[414,596,375,607,350,631],[326,664,314,758,306,836]);
|
||||
const shoulderR=path([564,594],[603,603,636,614,656,635],[682,670,700,766,711,842]);
|
||||
[shoulderL,shoulderR].forEach(p=>trace(p,{text:'. : softly : . ',size:7,tone:3,part:'body',z:8}));
|
||||
[
|
||||
path([349,674],[353,733,335,779,338,844]),
|
||||
path([389,716],[395,766,389,813,397,855]),
|
||||
path([658,681],[649,748,670,795,665,848]),
|
||||
path([615,728],[604,770,622,815,619,858])
|
||||
].forEach(p=>trace(p,{text:' . , . ',size:7.2,tone:3,part:'body',z:10}));
|
||||
trace(path([478,542],[486,569,480,587,465,600]),{text:'.:;.,',size:7,tone:2,z:18});
|
||||
trace(path([547,541],[542,565,546,587,561,599]),{text:'.,:',size:7,tone:2,z:18});
|
||||
trace(path([480,593],[492,600,499,604,508,607],[519,602,530,599,542,596]),{text:' . , . ',size:6.5,tone:3,z:20});
|
||||
fill(neck,{text:'. , ',dx:5,dy:5.5,size:6,tone:3,z:14,style:(x,y,r)=>y<577&&r<.42?{}:false});
|
||||
const collars=[
|
||||
path([373,617],[397,603,426,595,450,591],[464,618,484,640,509,657],[487,670,473,685,462,704],[425,675,393,645,373,617]),
|
||||
path([553,593],[586,599,621,612,645,627],[621,654,592,680,562,704],[552,684,534,669,510,657],[531,634,546,613,553,593])
|
||||
];
|
||||
collars.forEach((p,i)=>{
|
||||
fill(p,{text:'ever so softly ',dx:5,dy:6.7,size:7.6,tone:2,part:'body',z:20,style:(x,y,r)=>({tone:r>.8?3:2})});
|
||||
trace(p,{text:'::',size:6.5,tone:1,part:'body',z:21});
|
||||
trace(i?path([563,605],[584,611,611,620,627,632],[607,652,584,674,564,690]):path([390,620],[414,640,438,661,459,682],[471,670,484,658,492,655]),{text:' . ',size:7,tone:4,part:'body',z:22});
|
||||
});
|
||||
const bow=[
|
||||
path([501,685],[487,668,464,659,449,665],[445,680,449,704,459,718],[476,715,491,705,504,698],[505,694,504,689,501,685]),
|
||||
path([519,685],[534,667,550,660,570,663],[578,680,570,706,561,719],[544,713,530,705,517,698],[515,694,517,689,519,685]),
|
||||
path([502,697],[501,727,485,761,465,784],[480,782,490,778,499,781],[513,752,518,724,514,699],[510,697,506,696,502,697]),
|
||||
path([518,699],[522,731,540,759,551,781],[550,767,559,762,567,758],[545,737,533,716,527,699],[524,697,521,697,518,699])
|
||||
];
|
||||
bow.forEach(p=>{
|
||||
fill(p,{text:'flutter ',dx:5,dy:6,size:7.6,tone:5,part:'bow',z:30,style:(x,y,r)=>({tone:r>.65?6:5})});
|
||||
trace(p,{text:'.,:',size:6.5,tone:6,part:'bow',z:30});
|
||||
});
|
||||
fill(path([503,681],[510,678,519,681,524,686],[527,692,524,700,519,704],[513,707,504,703,500,699],[497,693,498,685,503,681]),{text:'xox',dx:4,dy:5,size:7.6,tone:6,part:'body',z:32});
|
||||
|
||||
// A face is mostly the untouched white of the page.
|
||||
trace(path([388,390],[390,441,402,470,422,493],[449,526,491,551,511,555],[534,551,578,520,599,487],[622,452,625,421,626,391]),{text:'.,:;:',size:7.7,tone:1,z:34});
|
||||
trace(path([391,407],[369,389,367,427,389,443]),{text:'.:,',size:7,tone:2,z:22});
|
||||
trace(path([625,404],[647,388,647,423,627,437]),{text:'.:,',size:7,tone:2,z:22});
|
||||
trace(path([384,412],[378,407,379,428,389,431]),{text:',.',size:6,tone:3,z:26});
|
||||
trace(path([632,410],[640,404,638,424,630,427]),{text:',.',size:6,tone:3,z:26});
|
||||
for(let i=0;i<190;i++){
|
||||
const x=394+rnd(i+420)*229,y=444+rnd(i+790)*101;
|
||||
if(within(x,y,face)&&(!within(x-8,y-4,face)||!within(x+8,y-4,face)))put(x,y,'.',5.5,3,'head',30);
|
||||
}
|
||||
trace(path([411,363],[427,353,449,354,465,363]),{text:' .,:;:,. ',size:7.2,tone:2,z:42});
|
||||
trace(path([547,361],[566,351,586,350,602,359]),{text:' .,:;:,. ',size:7.2,tone:2,z:42});
|
||||
|
||||
function eye(cx,cy,side){
|
||||
const p=path([cx-32,cy-3],[cx-19,cy-22,cx+15,cy-23,cx+32,cy-4],[cx+17,cy+20,cx-16,cy+22,cx-30,cy+4],[cx-32,cy+1,cx-32,cy-1,cx-32,cy-3]);
|
||||
const meta={eye:side,cx,cy};
|
||||
for(let y=cy-21;y<cy+23;y+=4.1)for(let x=cx-15;x<cx+18;x+=3.4){
|
||||
const d=((x-cx)/16)**2+((y-cy)/21)**2;
|
||||
if(d>1||!within(x,y,p)||((x-cx+5)/5.6)**2+((y-cy+9)/5.4)**2<1||((x-cx-6)/3)**2+((y-cy-9)/2.7)**2<1)continue;
|
||||
const pupil=Math.abs(x-cx)<5&&Math.abs(y-cy)<13;
|
||||
put(x,y,pupil?'m':d>.72?'o':'iris'[(Math.round(x+y)+1000)%4],6.8,pupil||y<cy-6?0:d>.72?1:y>cy+6?5:2,'head',51,0,{...meta,iris:true});
|
||||
}
|
||||
for(let j=0;j<3;j++)trace(path([cx-33,cy-3-j],[cx-16,cy-23-j,cx+13,cy-24-j,cx+32,cy-4-j]),{text:'vxmwmxv',size:7.8,tone:0,z:52+j,space:3.5,extra:meta});
|
||||
trace(path([cx-30,cy+6],[cx-15,cy+20,cx+15,cy+21,cx+29,cy+2]),{text:' .,_.. ',size:6.7,tone:2,z:52,extra:meta});
|
||||
trace(path([cx-27,cy-14],[cx-11,cy-27,cx+13,cy-27,cx+27,cy-13]),{text:' . , . ',size:6.1,tone:3,z:49,extra:meta});
|
||||
const sideX=side===1?-1:1;
|
||||
for(let k=0;k<4;k++){
|
||||
const a=cx+sideX*(25-k*4),b=cy-9-k*2;
|
||||
trace(path([a,b],[a+sideX*5,b-2,a+sideX*(10-k),b-7,a+sideX*(11-k),b-11]),{text:'vx',size:7.1-k*.3,tone:0,z:55,space:3,extra:meta});
|
||||
}
|
||||
}
|
||||
eye(444,404,1);eye(573,400,2);
|
||||
trace(path([511,441],[510,449,505,455,511,459],[514,460,517,459,519,458]),{text:' .,:. ',size:6.3,tone:2,z:58});
|
||||
trace(path([497,490],[504,495,516,496,525,488]),{text:'.,_..',size:6.8,tone:1,z:59,space:3.5});
|
||||
trace(path([504,501],[509,503,516,502,519,500]),{text:' . ',size:6,tone:5,z:58});
|
||||
for(const [x,y] of [[423,449],[591,444]]){
|
||||
for(let i=0;i<34;i++){
|
||||
const a=rnd(i+93)*TAU,r=Math.sqrt(rnd(i+376));
|
||||
put(x+cos(a)*24*r,y+sin(a)*8*r,i%5?',':'.',6.3,5,'head',47,0,{alpha:.45+rnd(i)*.3});
|
||||
}
|
||||
for(let i=0;i<3;i++)put(x-7+i*7,y,'/',8,5,'head',49,-.14,{alpha:.63});
|
||||
}
|
||||
|
||||
// Pointed locks are written separately so they can lift in the breeze.
|
||||
const locks=[
|
||||
path([499,141],[413,144,357,210,365,294],[367,336,385,370,400,391],[386,327,414,284,453,250],[475,222,489,186,499,141]),
|
||||
path([507,139],[451,161,415,220,419,282],[419,325,432,351,446,365],[438,324,455,294,482,269],[506,236,517,191,507,139]),
|
||||
path([518,138],[488,177,481,219,489,259],[495,290,513,318,532,332],[514,292,527,269,542,236],[553,201,543,160,518,138]),
|
||||
path([530,140],[558,179,549,214,548,248],[552,282,578,313,606,329],[589,296,590,267,586,240],[585,196,562,159,530,140]),
|
||||
path([540,141],[606,154,653,207,658,267],[663,315,639,354,617,378],[632,332,612,291,595,267],[577,224,585,184,540,141]),
|
||||
path([377,289],[364,377,367,457,349,548],[329,635,328,716,289,772],[345,752,377,666,388,590],[406,501,399,428,396,367],[394,335,386,312,377,289]),
|
||||
path([641,284],[653,367,628,425,632,499],[636,600,666,691,713,747],[667,736,618,663,614,589],[600,505,619,424,616,355],[621,323,631,298,641,284])
|
||||
];
|
||||
locks.forEach((p,i)=>{
|
||||
const part=i>4?'lock':'fringe';
|
||||
fill(p,{text:i%2?whisper:poem,dx:5.2,dy:6.8,size:8.3,part,z:65+i,style:(x,y,r)=>{
|
||||
const flow=sin(x*.135+y*.025+i*2),shine=Math.exp(-(((y-226-(x-510)**2*.0015)/15)**2));
|
||||
return r<shine*.57?false:{tone:shine>.5?3:flow>.45?2:1,lock:i};
|
||||
}});
|
||||
trace(p,{text:';:.,',size:6.6,tone:1,part,z:72+i,extra:{lock:i}});
|
||||
});
|
||||
[
|
||||
path([493,153],[445,204,423,269,439,342]),
|
||||
path([507,151],[488,207,495,279,521,310]),
|
||||
path([543,157],[574,218,554,271,592,315]),
|
||||
path([562,155],[623,190,646,281,629,341]),
|
||||
path([383,329],[383,430,385,506,367,597],[354,671,337,728,311,754]),
|
||||
path([641,319],[637,413,620,487,633,569],[644,651,669,709,694,730])
|
||||
].forEach((p,i)=>trace(p,{text:'a softer world ',size:6.5,tone:3,part:i>3?'lock':'fringe',z:80,follow:true,extra:{lock:i}}));
|
||||
|
||||
// Flyaway strands and the little, impossible curl at the crown.
|
||||
[
|
||||
path([499,140],[481,110,479,79,510,65],[531,58,554,74,540,89],[529,100,510,106,513,138]),
|
||||
path([365,253],[342,194,373,141,405,142]),
|
||||
path([674,306],[711,310,729,357,709,397]),
|
||||
path([353,447],[305,497,292,610,232,656],[201,681,180,678,169,669]),
|
||||
path([634,498],[684,535,701,628,768,650],[800,661,823,641,815,626]),
|
||||
path([325,665],[284,718,240,734,207,714]),
|
||||
path([663,677],[708,769,764,844,819,812])
|
||||
].forEach((p,i)=>trace(p,{text:i?' . the wind remembers . ':'. , little bird , .',size:i?7.6:7.2,tone:i?3:2,part:'hair',z:i?82:60,follow:true}));
|
||||
|
||||
// A typographic star clip, and its fine hanging chain.
|
||||
const star=[];
|
||||
for(let i=0;i<=10;i++){
|
||||
const a=i*PI/5-PI/2,r=i%2?9:20;
|
||||
star.push([634+cos(a)*r,301+sin(a)*r]);
|
||||
}
|
||||
fill(star,{text:'*+',dx:4.6,dy:5.4,size:6.7,tone:5,z:90});
|
||||
trace(star,{text:'*.',size:6.1,tone:6,z:91,space:3.3});
|
||||
trace(path([639,320],[648,344,652,360,644,380]),{text:'. : . ',size:7,tone:5,part:'chain',z:88});
|
||||
put(644,385,'+',13,6,'chain',89,.2);
|
||||
put(644,385,'x',8,5,'chain',90,.2);
|
||||
|
||||
// A type atlas avoids shaping thousands of individual letters every frame.
|
||||
// It contains only fillText output: no illustrations or image assets.
|
||||
const font='"Courier New",monospace',atlas=document.createElement('canvas');
|
||||
const chars=[...new Set(points.map(p=>p.ch).join('')+poem+whisper+".,'+*")];
|
||||
const columns=16,glyphs=new Map(),type=atlas.getContext('2d');
|
||||
const batch=new Float32Array((points.length+512)*7);
|
||||
let cell=0,typeSize=0,count=0,program;
|
||||
if(gl){
|
||||
const shader=(kind,source)=>{
|
||||
const s=gl.createShader(kind);gl.shaderSource(s,source);gl.compileShader(s);return s;
|
||||
};
|
||||
program=gl.createProgram();
|
||||
gl.attachShader(program,shader(gl.VERTEX_SHADER,`#version 300 es
|
||||
precision highp float;
|
||||
layout(location=0) in vec4 place;
|
||||
layout(location=1) in vec3 glyph;
|
||||
uniform vec2 view,atlasSize;
|
||||
uniform float zoom,cell;
|
||||
out vec2 uv;
|
||||
out float opacity;
|
||||
const vec2 quad[6]=vec2[6](vec2(-.5,-.5),vec2(.5,-.5),vec2(-.5,.5),vec2(-.5,.5),vec2(.5,-.5),vec2(.5,.5));
|
||||
void main(){
|
||||
vec2 q=quad[gl_VertexID];
|
||||
float c=cos(place.w),s=sin(place.w);
|
||||
vec2 p=place.xy+mat2(c,s,-s,c)*q*place.z;
|
||||
p=((p-vec2(500.,479.))*zoom+view*.5)/view*2.-1.;
|
||||
gl_Position=vec4(p.x,-p.y,0.,1.);
|
||||
uv=(glyph.xy+(q+.5)*cell)/atlasSize;
|
||||
opacity=glyph.z;
|
||||
}`));
|
||||
gl.attachShader(program,shader(gl.FRAGMENT_SHADER,`#version 300 es
|
||||
precision mediump float;
|
||||
uniform sampler2D letters;
|
||||
in vec2 uv;
|
||||
in float opacity;
|
||||
out vec4 color;
|
||||
void main(){color=texture(letters,uv)*opacity;}`));
|
||||
gl.linkProgram(program);gl.useProgram(program);
|
||||
if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(program));
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER,gl.createBuffer());
|
||||
gl.bufferData(gl.ARRAY_BUFFER,batch.byteLength,gl.DYNAMIC_DRAW);
|
||||
gl.enableVertexAttribArray(0);gl.vertexAttribPointer(0,4,gl.FLOAT,false,28,0);gl.vertexAttribDivisor(0,1);
|
||||
gl.enableVertexAttribArray(1);gl.vertexAttribPointer(1,3,gl.FLOAT,false,28,16);gl.vertexAttribDivisor(1,1);
|
||||
gl.bindTexture(gl.TEXTURE_2D,gl.createTexture());
|
||||
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);
|
||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,true);
|
||||
gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.clearColor(0,0,0,0);
|
||||
}
|
||||
function resize(){
|
||||
width=innerWidth;height=innerHeight;dpr=Math.min(devicePixelRatio||1,2);
|
||||
canvas.width=Math.round(width*dpr);canvas.height=Math.round(height*dpr);
|
||||
scale=Math.min(width/890,height/950)*.96;dirty=true;
|
||||
if(ctx){ctx.textAlign='center';ctx.textBaseline='middle';ctx.imageSmoothingQuality='high'}
|
||||
typeSize=8*dpr*scale;cell=Math.ceil(typeSize*2);
|
||||
atlas.width=cell*columns;atlas.height=cell*Math.ceil(chars.length*ink.length/columns);
|
||||
type.font=`${typeSize}px ${font}`;type.textAlign='center';type.textBaseline='middle';
|
||||
ink.forEach((color,tone)=>chars.forEach((ch,i)=>{
|
||||
const n=tone*chars.length+i,x=n%columns*cell,y=Math.floor(n/columns)*cell;
|
||||
type.fillStyle=color;type.fillText(ch,x+cell/2,y+cell/2);
|
||||
glyphs.set(tone+ch,[x,y]);
|
||||
}));
|
||||
points.forEach(p=>p.glyph=glyphs.get(p.tone+p.ch));
|
||||
if(gl){
|
||||
gl.viewport(0,0,canvas.width,canvas.height);
|
||||
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,atlas);
|
||||
gl.uniform2f(gl.getUniformLocation(program,'view'),width,height);
|
||||
gl.uniform2f(gl.getUniformLocation(program,'atlasSize'),atlas.width,atlas.height);
|
||||
gl.uniform1f(gl.getUniformLocation(program,'zoom'),scale);
|
||||
gl.uniform1f(gl.getUniformLocation(program,'cell'),cell);
|
||||
}
|
||||
}
|
||||
addEventListener('resize',resize);resize();
|
||||
const reduce=matchMedia('(prefers-reduced-motion: reduce)');
|
||||
reduce.addEventListener('change',()=>dirty=true);
|
||||
function blink(t){
|
||||
const p=t%8.7;
|
||||
return 1-.975*Math.max(Math.exp(-(((p-3.7)/.105)**2)),.94*Math.exp(-(((p-4.08)/.09)**2)),Math.exp(-(((p-7.35)/.12)**2)));
|
||||
}
|
||||
function render(ms){
|
||||
requestAnimationFrame(render);
|
||||
if(document.hidden||!gl&&ms-last<30||reduce.matches&&!dirty)return;
|
||||
last=ms;dirty=false;
|
||||
const t=reduce.matches?1.6:ms/1000,closed=blink(t);
|
||||
const turn=sin(t*.32)*.105,tilt=-.022+sin(t*.43)*.018,breathe=sin(t*1.25)*1.6;
|
||||
const ct=cos(tilt),st=sin(tilt),cy=cos(turn),sy=sin(turn);
|
||||
count=0;
|
||||
if(gl)gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
else{
|
||||
ctx.setTransform(dpr*scale,0,0,dpr*scale,width*dpr/2-500*dpr*scale,height*dpr/2-479*dpr*scale);
|
||||
ctx.clearRect(-width/scale,-height/scale,width*3/scale,height*3/scale);
|
||||
}
|
||||
function letter(x,y,ch,size,tone,angle=0,alpha=1,glyph){
|
||||
const [u,v]=glyph||glyphs.get(tone+ch),s=cell*size/typeSize;
|
||||
if(gl){
|
||||
let i=count++*7;
|
||||
batch[i++]=x;batch[i++]=y;batch[i++]=s;batch[i++]=angle;
|
||||
batch[i++]=u;batch[i++]=v;batch[i]=alpha;return;
|
||||
}
|
||||
ctx.globalAlpha=alpha;
|
||||
if(Math.abs(angle)>.04){
|
||||
ctx.translate(x,y);ctx.rotate(angle);ctx.drawImage(atlas,u,v,cell,cell,-s/2,-s/2,s,s);ctx.rotate(-angle);ctx.translate(-x,-y);
|
||||
}else ctx.drawImage(atlas,u,v,cell,cell,x-s/2,y-s/2,s,s);
|
||||
}
|
||||
function wind(front){
|
||||
const text=poem+' '+whisper+' ';
|
||||
for(let j=0;j<2;j++)for(let i=0;i<text.length;i++){
|
||||
if(text[i]===' ')continue;
|
||||
const a=i*.019+t*.115+j*PI,depth=sin(a);
|
||||
if((depth>0)!==front)continue;
|
||||
const u=cos(a)*413,v=sin(a)*224;
|
||||
const x=500+u*.974+v*.226,y=492-u*.226+v*.974+sin(a*3+t*.45)*7;
|
||||
if(!front&&(within(x,y,hair)||covered(x,y)))continue;
|
||||
const angle=Math.atan2(-413*sin(a)*-.226+224*cos(a)*.974,-413*sin(a)*.974+224*cos(a)*.226);
|
||||
const fade=Math.min(1,i/9,(text.length-i)/12);
|
||||
letter(x,y,text[i],8.6+depth*.8,j?5:3,angle,.47*fade);
|
||||
}
|
||||
for(let i=0;i<46;i++){
|
||||
const a=i*2.39996+t*(.10+rnd(i)*.045),depth=sin(a);
|
||||
if((depth>0)!==front)continue;
|
||||
const rx=343+rnd(i+40)*70,ry=246+rnd(i+12)*155;
|
||||
const x=500+cos(a)*rx+sin(t*.54+i)*9,y=466+sin(a)*ry;
|
||||
const size=7+rnd(i+8)*4,alpha=.25+rnd(i+51)*.3;
|
||||
letter(x,y,".,'+*"[i%5],size,i%4===0?5:3,sin(t*.5+i)*.5,alpha);
|
||||
if(i%8===0){
|
||||
letter(x+6,y-5,',',size*.8,5,a,.4);
|
||||
letter(x-5,y+3,"'",size*.8,5,-a,.35);
|
||||
}
|
||||
}
|
||||
}
|
||||
wind(false);
|
||||
for(const p of points){
|
||||
let {x,y,z,angle}=p,alpha=p.alpha??1;
|
||||
const body=p.part==='body'||p.part==='bow',hairy=p.part==='hair'||p.part==='lock';
|
||||
if(p.eye){
|
||||
y=p.cy+(y-p.cy)*closed;
|
||||
if(p.iris){x+=sin(t*.57)*2.1;y+=sin(t*.41)*.8*closed;alpha*=clamp(closed*3)}
|
||||
}
|
||||
if(hairy){
|
||||
const k=clamp((y-270)/540),wave=sin(t*1.12-y*.009+p.x*.003);
|
||||
x+=k*k*(wave*26+sin(t*.57)*21);
|
||||
y+=k*sin(t*.94+p.x*.014)*5;
|
||||
angle+=k*cos(t*1.12-p.y*.009)*.035;
|
||||
}
|
||||
if(p.part==='fringe')x+=sin(t*.94+p.lock)*clamp((y-170)/180)*1.2;
|
||||
if(p.part==='chain')x+=sin(t*1.5)*clamp((y-316)/70)*4;
|
||||
if(p.part==='bow'){
|
||||
x+=sin(t*1.3-y*.022)*clamp((y-687)/100)*4;
|
||||
y+=sin(t*1.1+x*.03)*2;
|
||||
}
|
||||
if(body){
|
||||
x=500+(x-500)*cos(turn*.25);y+=breathe*.6;
|
||||
alpha*=clamp((892-y)/85);
|
||||
}else{
|
||||
const dx=x-510,dy=y-536;
|
||||
x=510+dx*ct-dy*st;y=536+dx*st+dy*ct+breathe;
|
||||
x=510+(x-510)*cy+z*sy;
|
||||
angle+=tilt;
|
||||
}
|
||||
letter(x,y,p.ch,p.size,p.tone,angle,alpha,p.glyph);
|
||||
}
|
||||
wind(true);
|
||||
if(gl){
|
||||
gl.bufferSubData(gl.ARRAY_BUFFER,0,batch.subarray(0,count*7));
|
||||
gl.drawArraysInstanced(gl.TRIANGLES,0,6,count);
|
||||
}else ctx.globalAlpha=1;
|
||||
}
|
||||
requestAnimationFrame(render);
|
||||
})();
|
||||
</script>
|
||||
</html>
|
||||
Reference in New Issue
Block a user