TYPOGRAPHY / 348
Handwriting stroke sequence
書く順序に沿って、文字を描く
オリジナルの線で組んだNAOYAMANを、線の長さに沿って一筆ずつ描きます。
見本の中を操作して変化を試してください。
実装コードを見る
<!doctype html><html lang="ja"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{margin:0;min-height:330px}.sample{width:100%;height:100%;min-height:330px;padding:12px!important;display:flex!important;flex-direction:column;justify-content:center;align-items:center;gap:8px!important;background:#f0efeb;color:#232323;font-family:Arial,Meiryo,sans-serif;overflow:hidden;box-sizing:border-box}.sample *{box-sizing:border-box}.lab-scene{width:100%;max-width:430px;height:140px;position:relative;overflow:hidden;border-bottom:1px solid #ccc}.lab-scene:has(canvas){max-width:320px;height:auto;aspect-ratio:16/9;flex-shrink:0}.lab-canvas{display:block;width:100%;height:100%!important;outline-offset:-3px}.lab-canvas:focus-visible{outline:2px solid #222}.lab-controls{width:100%;max-width:390px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 14px}.lab-controls label{display:grid;grid-template-columns:1fr auto;gap:2px;font-size:11px;min-width:0}.lab-controls input{grid-column:1/-1;width:100%;min-width:0;margin:0;accent-color:#333;height:30px}.lab-controls output{font-variant-numeric:tabular-nums}.lab-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center}.sample .lab-actions button{color:inherit;font:11px Arial,Meiryo,sans-serif;border:1px solid #aaa;border-radius:99px;background:transparent;min-height:44px;padding:8px 12px;cursor:pointer}.lab-status{font-size:10px;line-height:1.4;min-height:14px;margin:0;text-align:center}.lab-note{font-size:9px;line-height:1.3;color:#666;margin:0;text-align:center}.lab-actions button:focus-visible{outline:2px solid #222;outline-offset:2px}@media(prefers-reduced-motion:reduce){.sample *{animation-play-state:paused!important;transition:none!important}}</style><div class="sample"><div class="lab-scene"><canvas class="lab-canvas" tabindex="0" role="img" aria-label="書く順序に沿って、文字を描く。矢印キーでも位置を変えられます。" style="touch-action:pan-y"></canvas></div><div class="lab-controls"><label>線の太さ<output>3</output><input aria-label="線の太さ" type="range" min="1" max="8" step="1" value="3" data-key="width"></label><label>書く速さ<output>1</output><input aria-label="書く速さ" type="range" min="0.5" max="2" step="0.1" value="1" data-key="speed"></label></div><div class="lab-actions"><button type="button" data-play>停止</button><button type="button" data-reset>最初から</button></div><p class="lab-status"></p></div><script>const lab=(function runCanvasLab(root,cfg,draw){
const canvas=root.querySelector('canvas'),ctx=canvas.getContext('2d'),status=root.querySelector('.lab-status'),play=root.querySelector('[data-play]'),reset=root.querySelector('[data-reset]'),abort=new AbortController(),mq=matchMedia('(prefers-reduced-motion:reduce)');
const values=Object.fromEntries(cfg.controls.map(c=>[c.key,c.value]));let state={},time=0,frame=0,last=0,allowed=!mq.matches,userPaused=false,destroyed=false,visible=true;
const pointer={x:160,y:90,down:false,active:false,dx:0,dy:0};let event='init';
const on=(el,type,fn,opts={})=>el.addEventListener(type,fn,{...opts,signal:abort.signal});
const circle=(x,y,r,fill='#292929')=>{ctx.beginPath();ctx.arc(x,y,Math.max(.1,r),0,Math.PI*2);ctx.fillStyle=fill;ctx.fill()};
const line=(x,y,a,b,color='#aaa',width=1)=>{ctx.beginPath();ctx.strokeStyle=color;ctx.lineWidth=width;ctx.moveTo(x,y);ctx.lineTo(a,b);ctx.stroke()};
const text=(str,x,y,size=12,color='#333',align='center')=>{ctx.fillStyle=color;ctx.font=`${size>25?'700':'400'} ${size}px Arial, sans-serif`;ctx.textAlign=align;ctx.fillText(str,x,y)};
function render(dt=0){if(destroyed||!ctx)return;ctx.setTransform(canvas.width/320,0,0,canvas.height/180,0,0);ctx.clearRect(0,0,320,180);ctx.save();draw({ctx,w:320,h:180,time,dt,values,state,pointer,event,kind:cfg.kind,circle,line,text,status:s=>{status.textContent=s}});ctx.restore();event='';}
function tick(now){frame=0;if(!running())return;const dt=Math.min((now-(last||now))/1000,.033);last=now;time+=dt;render(dt);frame=requestAnimationFrame(tick)}
function running(){return !destroyed&&allowed&&!userPaused&&visible&&!document.hidden&&cfg.animated!==false}
function sync(){cancelAnimationFrame(frame);frame=0;last=0;if(running())frame=requestAnimationFrame(tick);play.textContent=running()?'停止':'再生';play.setAttribute('aria-pressed',String(userPaused));}
function resize(){const dpr=Math.min(devicePixelRatio||1,2);canvas.width=Math.round(640*dpr);canvas.height=Math.round(360*dpr);render()}
on(play,'click',()=>{userPaused=running();if(!userPaused)allowed=true;sync();render()});
on(reset,'click',()=>{state={};time=0;event='reset';pointer.down=false;render();sync()});
for(const input of root.querySelectorAll('[data-key]')){on(input,'input',()=>{values[input.dataset.key]=Number(input.value);input.closest('label').querySelector('output').textContent=input.value;event='input';render()})}
function position(e){const b=canvas.getBoundingClientRect(),x=(e.clientX-b.left)/b.width*320,y=(e.clientY-b.top)/b.height*180;pointer.dx=x-pointer.x;pointer.dy=y-pointer.y;pointer.x=Math.max(0,Math.min(320,x));pointer.y=Math.max(0,Math.min(180,y));pointer.active=true;}
on(canvas,'pointerdown',e=>{position(e);pointer.down=true;event='down';canvas.setPointerCapture(e.pointerId);render()});
on(canvas,'pointermove',e=>{position(e);event='move';render()});
on(canvas,'pointerup',e=>{position(e);pointer.down=false;event='up';render()});
on(canvas,'pointercancel',()=>{pointer.down=false;event='cancel';render()});
on(canvas,'pointerleave',()=>{pointer.active=false;if(!pointer.down){event='leave';render()}});
on(canvas,'keydown',e=>{const keys={ArrowLeft:[-12,0],ArrowRight:[12,0],ArrowUp:[0,-12],ArrowDown:[0,12]};if(keys[e.key]){e.preventDefault();pointer.active=true;pointer.x=Math.max(0,Math.min(320,pointer.x+keys[e.key][0]));pointer.y=Math.max(0,Math.min(180,pointer.y+keys[e.key][1]));event='keyboard';render()}if(e.key===' '||e.key==='Enter'){e.preventDefault();pointer.down=!pointer.down;pointer.active=true;event=pointer.down?'down':'up';render()}});
on(document,'visibilitychange',sync);on(mq,'change',()=>{allowed=!mq.matches;sync();render()});
const observer=new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;sync()});observer.observe(canvas);resize();sync();
if(cfg.animated===false)play.hidden=true;
return {setAuto(on){allowed=on&&!mq.matches;sync()},destroy(){destroyed=true;cancelAnimationFrame(frame);abort.abort();observer.disconnect();state={}}};
})(document,{"kind":"type-handwriting","controls":[{"key":"width","label":"線の太さ","min":1,"max":8,"value":3,"step":1},{"key":"speed","label":"書く速さ","min":0.5,"max":2,"value":1,"step":0.1}],"animated":true},function draw({ctx,time,values:v,state:s,pointer,kind,text,status}){
const clamp=(x,a,b)=>Math.max(a,Math.min(b,x)),noise=n=>{const x=Math.sin(n*127.1+31.7)*43758.5453;return x-Math.floor(x)},reduced=matchMedia('(prefers-reduced-motion:reduce)').matches,t=time*(v.speed||1),progress=reduced?1:clamp((t%5)/3,0,1);
const canvas=(w,h)=>{const a=document.createElement('canvas');a.width=w;a.height=h;return a};
const base=(g,w=320,h=180)=>{g.fillStyle='#cce4e7';g.fillRect(0,0,w,h);g.fillStyle='#edaf67';g.beginPath();g.arc(w*.76,h*.26,h*.16,0,Math.PI*2);g.fill();g.fillStyle='#60949d';g.beginPath();g.moveTo(0,h);g.lineTo(w*.31,h*.18);g.lineTo(w*.64,h);g.fill();g.fillStyle='#234956';g.beginPath();g.moveTo(w*.23,h);g.lineTo(w*.72,h*.41);g.lineTo(w,h*.81);g.lineTo(w,h);g.fill();g.strokeStyle='#f1f1df';for(let i=0;i<6;i++){g.lineWidth=1;g.beginPath();g.arc(w*.31,h*.52,8+i*6,0,Math.PI*2);g.stroke()}g.fillStyle='#fff';g.font=`bold ${Math.round(w/16)}px Arial`;g.fillText('FLYING BRAIN',w*.05,h*.9)};
const paper=()=>{ctx.fillStyle='#e7e2d7';ctx.fillRect(0,0,320,180);ctx.fillStyle='#5f554d0c';for(let i=0;i<600;i++)ctx.fillRect(noise(i)*320,noise(i+900)*180,1,1)};
const word=(g,label,size=36)=>{g.font=`bold ${size}px Arial`;g.textAlign='center';g.textBaseline='middle';g.fillText(label,160,90)};
if(kind==='type-scramble'){
const target='NAOYAMAN',chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';let value='';for(let i=0;i<target.length;i++)value+=progress>=((i+1)/target.length)?target[i]:chars[Math.floor(noise(i+Math.floor(t*14)*11)*chars.length)];ctx.fillStyle='#153c47';word(ctx,value,39);status(progress===1?'NAOYAMAN · 文字が確定':'ランダムな字形から、左から順に確定');return;
}
if(kind==='type-odometer'){
const value=Math.round(progress*v.target),digits=String(v.target).padStart(4,'0');ctx.fillStyle='#213c47';ctx.fillRect(28,48,264,84);
for(let i=0;i<4;i++){const unit=10**(3-i),n=(reduced||progress===1)?Number(digits[i]):(progress*v.target/unit)%10,integer=Math.floor(n),fraction=n-integer;ctx.save();ctx.beginPath();ctx.rect(30+i*65,54,60,72);ctx.clip();ctx.fillStyle='#eff4e9';ctx.font='bold 55px monospace';ctx.textAlign='center';ctx.textBaseline='middle';for(let j=-1;j<=1;j++)ctx.fillText(String((integer+j+10)%10),60+i*65,90+(j-fraction)*72);ctx.restore()}
status(`回転する数字 · 到達値 ${v.target} / 表示過程 ${value}`);return;
}
if(kind==='type-splitflap'){
const before='MIXWELL ',after='NAOYAMAN';ctx.fillStyle='#ddd8cd';ctx.fillRect(0,0,320,180);
for(let i=0;i<8;i++){const p=clamp(progress*1.6-i*.085,0,1),x=6+i*39;ctx.fillStyle='#213b46';ctx.fillRect(x,58,35,64);ctx.font='bold 27px monospace';ctx.textAlign='center';ctx.textBaseline='middle';const glyph=(ch,y,h)=>{ctx.save();ctx.beginPath();ctx.rect(x,y,35,h);ctx.clip();ctx.fillStyle='#f1ead7';ctx.fillText(ch,x+17.5,90);ctx.restore()};glyph(after[i],58,32);glyph(before[i],90,32);if(p<.5){ctx.save();ctx.translate(0,90);ctx.scale(1,Math.cos(p*Math.PI));ctx.translate(0,-90);ctx.fillStyle='#29434e';ctx.fillRect(x,58,35,32);glyph(before[i],58,32);ctx.restore()}else{glyph(after[i],90,32);ctx.save();ctx.translate(0,90);ctx.scale(1,Math.abs(Math.cos(p*Math.PI)));ctx.translate(0,-90);glyph(after[i],90,32);ctx.restore()}ctx.fillStyle='#0c222f';ctx.fillRect(x,89,35,2)}status('一枚ずつめくれる、反転式の文字盤');return;
}
if(kind==='type-handwriting'){
const glyphs={N:[[[0,1],[0,0],[1,1],[1,0]]],A:[[[0,1],[.5,0],[1,1]],[[.22,.6],[.78,.6]]],O:[[[.5,0],[.12,.1],[0,.5],[.12,.9],[.5,1],[.88,.9],[1,.5],[.88,.1],[.5,0]]],Y:[[[0,0],[.5,.5],[1,0]],[[.5,.5],[.5,1]]],M:[[[0,1],[0,0],[.5,.55],[1,0],[1,1]]]};
const segments=[];[...'NAOYAMAN'].forEach((letter,i)=>glyphs[letter].forEach(path=>path.slice(1).forEach((q,j)=>{const a=path[j];segments.push([12+i*38+a[0]*26,60+a[1]*56,12+i*38+q[0]*26,60+q[1]*56])})));const total=segments.reduce((sum,[x,y,a,b])=>sum+Math.hypot(a-x,b-y),0);let budget=progress*total;ctx.strokeStyle='#203f4b';ctx.lineWidth=v.width;ctx.lineCap='round';ctx.lineJoin='round';for(const [x,y,a,b]of segments){const length=Math.hypot(a-x,b-y),p=clamp(budget/length,0,1);if(p){ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(x+(a-x)*p,y+(b-y)*p);ctx.stroke()}budget-=length}status('線の長さに沿って描く · 図鑑オリジナルの一筆ずつの字形');return;
}
if(['texture-foil','texture-emboss','texture-letterpress','texture-ink','texture-distress'].includes(kind)){
paper();const label=kind==='texture-foil'?'MINERALion':'MIXWELL';
if(kind==='texture-foil'){
const angle=(v.angle+(reduced?0:time*15))*Math.PI/180,dx=Math.cos(angle)*150,dy=Math.sin(angle)*90,g=ctx.createLinearGradient(160-dx,90-dy,160+dx,90+dy);[[0,'#6b4923'],[.22,'#c7a65c'],[.4,'#fff1b0'],[.46,'#95662e'],[.65,'#f2d28c'],[1,'#795121']].forEach(([p,c])=>g.addColorStop(p,c));ctx.fillStyle=g;word(ctx,label,38);ctx.save();ctx.globalCompositeOperation='source-atop';ctx.globalAlpha=.08;ctx.strokeStyle='#fff';for(let i=0;i<35;i++){ctx.beginPath();ctx.moveTo(i*12,60);ctx.lineTo(i*12+20,120);ctx.stroke()}ctx.restore();status('光の角度で濃淡が変わる、箔押しの見本');return;
}
if(kind==='texture-emboss'||kind==='texture-letterpress'){
const depth=v.depth,sign=kind==='texture-emboss'?1:-1;ctx.save();ctx.translate(-depth*sign,-depth*sign);ctx.fillStyle='#fffdf3';word(ctx,label,43);ctx.restore();ctx.save();ctx.translate(depth*sign,depth*sign);ctx.fillStyle='#948d7e';word(ctx,label,43);ctx.restore();ctx.fillStyle=kind==='texture-emboss'?'#dfd9cc':'#9b9282';word(ctx,label,43);status(kind==='texture-emboss'?'明暗をずらして、文字が浮き上がる':'陰影を反転し、紙へ文字が沈む');return;
}
if(!s.layer)s.layer=canvas(320,180);const g=s.layer.getContext('2d');g.clearRect(0,0,320,180);g.fillStyle='#244a57';
if(kind==='texture-ink'){
g.save();g.filter=`blur(${v.bleed}px)`;word(g,label,43);g.restore();g.globalAlpha=.55;word(g,label,43);g.globalAlpha=1;ctx.drawImage(s.layer,0,0);status('輪郭へにじみを重ねる · インク浸透の視覚的な近似');
}else{word(g,label,43);g.globalCompositeOperation='destination-out';for(let i=0;i<v.amount*15;i++){g.fillStyle='#000';g.fillRect(noise(i)*320,55+noise(i+900)*70,.6+noise(i+150)*2,.6+noise(i+350)*4)}g.globalCompositeOperation='source-over';ctx.drawImage(s.layer,0,0);status(`文字のインクを削る · かすれ ${v.amount}%`)}return;
}
if(['optical-hologram','optical-pearl'].includes(kind)){
const angle=v.angle+(pointer.active?(pointer.x-160)/2:0)+(reduced?0:time*8);ctx.fillStyle='#172f3a';ctx.fillRect(0,0,320,180);const g=ctx.createLinearGradient(0,0,320,180);for(let i=0;i<=12;i++)g.addColorStop(i/12,`hsl(${(angle+i*31)%360} ${kind==='optical-pearl'?35:60}% ${kind==='optical-pearl'?82:60}%)`);ctx.fillStyle=g;
if(kind==='optical-hologram'){ctx.fillRect(35,26,250,128);ctx.save();ctx.globalCompositeOperation='screen';ctx.globalAlpha=.45;ctx.strokeStyle='#fff';for(let i=-20;i<80;i++){ctx.beginPath();ctx.moveTo(i*7+angle%7,26);ctx.lineTo(i*7+70+angle%7,154);ctx.stroke()}ctx.restore();ctx.fillStyle='#11353a';word(ctx,'MINERALion',34)}else{ctx.beginPath();ctx.ellipse(160,90,91,68,0,0,Math.PI*2);ctx.fill();const light=ctx.createRadialGradient(133,60,2,164,90,95);light.addColorStop(0,'#ffffffdd');light.addColorStop(.5,'#ffffff00');light.addColorStop(1,'#243545bb');ctx.fillStyle=light;ctx.fill();}status('観察角で色が動く · 薄膜干渉を模した配色の近似');return;
}
if(kind==='optical-refraction'){
if(!s.image){s.image=canvas(160,90);base(s.image.getContext('2d'),160,90);s.pixels=s.image.getContext('2d').getImageData(0,0,160,90);s.out=canvas(160,90)}const g=s.out.getContext('2d'),out=g.createImageData(160,90),a=s.pixels.data;const cx=pointer.active?pointer.x/2:80,cy=pointer.active?pointer.y/2:45;
for(let y=0;y<90;y++)for(let x=0;x<160;x++){const dx=x-cx,dy=y-cy,r=Math.hypot(dx,dy),scale=r<36?1-(1-(r/36)**2)*v.strength:.0;const sx=clamp(Math.round(x-dx*scale),0,159),sy=clamp(Math.round(y-dy*scale),0,89),i=(y*160+x)*4,j=(sy*160+sx)*4;out.data.set(a.subarray(j,j+4),i)}g.putImageData(out,0,0);ctx.drawImage(s.out,0,0,320,180);ctx.strokeStyle='#ffffffbb';ctx.lineWidth=2;ctx.beginPath();ctx.arc(cx*2,cy*2,72,0,Math.PI*2);ctx.stroke();status('背景を読み取る位置を曲げる · 球形レンズの近似');return;
}
if(kind==='optical-caustics'){
ctx.fillStyle='#236478';ctx.fillRect(0,0,320,180);ctx.save();ctx.globalCompositeOperation='screen';for(let j=0;j<17;j++){ctx.beginPath();for(let x=0;x<=320;x+=3){const y=j*12+Math.sin(x*.04+j*.8+time*.7)*v.strength+Math.sin(x*.077-time*.4)*5;x?ctx.lineTo(x,y):ctx.moveTo(x,y)}ctx.strokeStyle='#76c6cb';ctx.lineWidth=1.6;ctx.stroke()}for(let j=0;j<20;j++){ctx.beginPath();for(let y=0;y<=180;y+=3){const x=j*17+Math.sin(y*.06+j+time*.5)*v.strength;y?ctx.lineTo(x,y):ctx.moveTo(x,y)}ctx.strokeStyle='#63a8b4';ctx.lineWidth=1.3;ctx.stroke()}ctx.restore();status('水底の集光模様を、交差する波で近似');return;
}
if(['film-glitch','film-crt','film-vhs','film-pixelsort'].includes(kind)){
if(!s.image){s.image=canvas(320,180);base(s.image.getContext('2d'));}ctx.drawImage(s.image,0,0);
if(kind==='film-glitch'){
const frame=reduced?4:Math.floor(time*3);for(let i=0;i<8;i++){const y=Math.floor(noise(i+frame)*170),h=3+noise(i+frame+50)*13,shift=(noise(i+frame+100)-.5)*v.amount;ctx.drawImage(s.image,0,y,320,h,shift,y,320,h)}ctx.globalAlpha=.2;ctx.globalCompositeOperation='screen';ctx.fillStyle='#4facba';ctx.fillRect(0,noise(frame)*170,320,4);status('画像の帯を、横にずらすグリッチ');return;
}
if(kind==='film-crt'){
ctx.fillStyle=`rgba(0,0,0,${v.amount/100})`;for(let y=0;y<180;y+=3)ctx.fillRect(0,y,320,1);const g=ctx.createRadialGradient(160,90,40,160,90,175);g.addColorStop(0,'#0000');g.addColorStop(1,'#000b');ctx.fillStyle=g;ctx.fillRect(0,0,320,180);status('走査線と周辺減光で、ブラウン管の画面を模す');return;
}
if(kind==='film-vhs'){
const frame=reduced?3:Math.floor(time*8);for(let y=0;y<180;y+=3){const shift=Math.sin(y*.12+frame)*v.amount*.12+(y>150?Math.sin(y+frame)*v.amount*.6:0);ctx.drawImage(s.image,0,y,320,3,shift,y,320,3)}ctx.fillStyle='#eee7';for(let i=0;i<45;i++)ctx.fillRect(noise(i+frame)*320,noise(i+frame+400)*180,noise(i+frame+80)*8,1);ctx.fillStyle='#fff';ctx.font='12px monospace';ctx.textAlign='left';ctx.fillText('PLAY ▷ 00:00:'+String(Math.floor(time)%60).padStart(2,'0'),12,20);status('水平の揺れと追跡ノイズ · VHSの視覚的な近似');return;
}
if(s.threshold!==v.threshold){const g=s.image.getContext('2d'),data=g.getImageData(0,0,320,180),a=data.data,lum=i=>a[i]*.2126+a[i+1]*.7152+a[i+2]*.0722;for(let y=0;y<180;y++){let x=0;while(x<320){while(x<320&&lum((y*320+x)*4)<v.threshold)x++;let start=x;while(x<320&&lum((y*320+x)*4)>=v.threshold)x++;const pixels=[];for(let j=start;j<x;j++){const i=(y*320+j)*4;pixels.push([a[i],a[i+1],a[i+2],255])}pixels.sort((a,b)=>(a[0]+a[1]+a[2])-(b[0]+b[1]+b[2]));pixels.forEach((p,j)=>a.set(p,(y*320+start+j)*4))}}s.sorted=canvas(320,180);s.sorted.getContext('2d').putImageData(data,0,0);s.threshold=v.threshold}ctx.drawImage(s.sorted,0,0);status('明度の条件に合う連続区間だけ、画素を並べ替える');return;
}
if(kind==='frame-sprite'){
if(!s.sheet){s.sheet=canvas(960,80);const g=s.sheet.getContext('2d');for(let f=0;f<12;f++){const x=f*80,bob=Math.sin(f/12*Math.PI*2)*8;g.save();g.translate(x+40,35+bob);g.fillStyle='#22515f';g.beginPath();g.arc(0,0,22,0,Math.PI*2);g.fill();g.strokeStyle='#eee9d9';g.lineWidth=2;g.beginPath();g.arc(0,0,13,0,Math.PI*2);g.stroke();g.fillStyle='#eee';g.fillRect(-8,-6,4,4);g.fillRect(5,-6,4,4);g.strokeStyle='#22515f';g.lineWidth=4;g.beginPath();g.moveTo(-12,18);g.lineTo(-18,32-bob);g.moveTo(12,18);g.lineTo(18,32+bob);g.stroke();g.restore()}}
const f=reduced?0:Math.floor(time*v.fps)%12;ctx.drawImage(s.sheet,f*80,0,80,80,100,5,120,120);for(let i=0;i<12;i++){ctx.drawImage(s.sheet,i*80,0,80,80,10+i*25,140,24,24);if(i===f){ctx.strokeStyle='#236478';ctx.strokeRect(10+i*25,138,24,28)}}status(`スプライトシートの ${f+1} / 12 コマ目を切り出す`);return;
}
if(kind==='frame-stopmotion'){
const step=reduced?0:Math.floor(time*v.fps)/v.fps,x=30+(step*55)%260,y=96-Math.abs(Math.sin(step*2))*45;ctx.fillStyle='#ddd8ce';ctx.fillRect(0,0,320,180);ctx.fillStyle='#b9b0a2';ctx.fillRect(0,142,320,2);ctx.save();ctx.translate(x,y);ctx.rotate(Math.floor(step*4)*.12);ctx.fillStyle='#23515f';ctx.fillRect(-22,-22,44,44);ctx.fillStyle='#fff';ctx.font='bold 10px Arial';ctx.textAlign='center';ctx.fillText('MIX',0,4);ctx.restore();status(`${v.fps}コマ/秒 · 時間を区切って、コマ撮りの動きにする`);return;
}
if(kind==='frame-turntable'){
if(!s.frames){s.frames=[];for(let f=0;f<24;f++){const a=canvas(160,160),g=a.getContext('2d'),angle=f/24*Math.PI*2,w=60*Math.abs(Math.cos(angle))+20*Math.abs(Math.sin(angle));g.fillStyle='#a3d2df';g.fillRect(80-w/2,35,w,100);g.fillStyle=Math.sin(angle)>0?'#35616e':'#6c98a5';g.fillRect(80+w/2-10,35,10,100);g.fillStyle='#173e4a';g.font='bold 9px Arial';g.textAlign='center';if(Math.cos(angle)>.3)g.fillText('MINERALion',75,85);else if(Math.cos(angle)<-.3)g.fillText('BACK',80,85);s.frames.push(a)}}const frame=Math.round(v.angle/15)%24;ctx.drawImage(s.frames[frame],80,5,160,160);status(`${frame+1} / 24 コマ · 生成済み連番画像を切り替える`);return;
}
if(kind==='frame-lineboil'){
const frame=reduced?0:Math.floor(time*v.fps)%3;ctx.strokeStyle='#254751';ctx.lineWidth=2.3;ctx.lineJoin='round';for(let pass=0;pass<2;pass++){ctx.beginPath();for(let i=0;i<=64;i++){const a=i/64*Math.PI*2,r=48+(noise(i+frame*100+pass*33)-.5)*v.amount;const x=160+Math.cos(a)*r*1.7,y=86+Math.sin(a)*r;i?ctx.lineTo(x,y):ctx.moveTo(x,y)}ctx.stroke()}ctx.fillStyle='#254751';word(ctx,'FLYING BRAIN',22);status('三枚の線画を交互に表示 · 線だけが小さく揺らぐ');return;
}
});addEventListener('pagehide',()=>lab.destroy(),{once:true});</script></html>