mirror of
https://github.com/direct-img/vibegif.direct-img.link.git
synced 2026-10-29 03:34:51 +00:00
Black ink, screentone and white only: reaction tropes (gaan shock, gloom corner, menacing aura, crow silence, petrify), comedy and action (harisen, iai draw, kendo, kyudo, miko), school life (locker letters, zero test, rooftop wind, shared umbrella), town and home (omikuji, shrine prayer, tanabata, floating lantern, alarm smash) and fantasy (yurei, oni, dragon, android, summoning circle, knight, pirate, detective, princess). gen files can now export rim=<px> for a white sticker border, so solid black ink still shows on the black page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
7.1 KiB
JavaScript
75 lines
7.1 KiB
JavaScript
// manga pirate captain with an eye patch and wavy ink hair: lifts her spyglass to her eye, extends it, scans, spots something, collapses and lowers it; coat and hair whipping in the wind
|
|
import{createCanvas}from'@napi-rs/canvas'
|
|
const{sin,cos,PI,max,min}=Math,T=2*PI
|
|
export const frames=20,delay=80,rim=4
|
|
const TL=createCanvas(6,6),tx=TL.getContext('2d');tx.fillStyle='#111';for(const[a,b]of[[1.5,1.5],[4.5,4.5]])tx.beginPath(),tx.arc(a,b,1.05,0,T),tx.fill()
|
|
const ease=x=>x<0?0:x>1?1:x*x*(3-2*x)
|
|
const eye=(c,x,y,d,tn)=>{
|
|
c.fillStyle='#fff';c.beginPath();c.ellipse(x,y,14,17,0,0,T);c.fill()
|
|
c.save();c.beginPath();c.ellipse(x+2,y+2,11.5,15,0,0,T);c.clip();c.fillStyle=tn;c.fillRect(x-12,y-14,28,32);c.fillStyle='#111';c.fillRect(x-12,y-14,28,13);c.restore()
|
|
c.lineWidth=2.5;c.beginPath();c.ellipse(x+2,y+2,11.5,15,0,0,T);c.stroke()
|
|
c.fillStyle='#111';c.beginPath();c.ellipse(x+2,y+3,4.5,7,0,0,T);c.fill()
|
|
c.fillStyle='#fff';c.beginPath();c.ellipse(x-2,y-5,4.5,5.5,-.4,0,T);c.fill();c.beginPath();c.arc(x+6,y+8,2.2,0,T);c.fill()
|
|
c.lineWidth=7;c.beginPath();c.moveTo(x-17,y-6);c.quadraticCurveTo(x,y-21,x+17,y-6);c.moveTo(x+d*16,y-7);c.lineTo(x+d*23,y-12);c.moveTo(x+d*17,y-2);c.lineTo(x+d*23,y-3);c.stroke()
|
|
c.lineWidth=2.5;c.beginPath();c.moveTo(x-8,y+19);c.lineTo(x+8,y+19);c.stroke()
|
|
}
|
|
export default(c,t)=>{
|
|
const tn=c.createPattern(TL,'repeat'),w=sin(t*T*2),spot=t>.5&&t<.66
|
|
// lift: spyglass from her chest up to her eye and back; ex: segments slide out only while it's up
|
|
const lf=ease((t-.08)/.12)-ease((t-.8)/.12),ex=ease((t-.22)/.12)-ease((t-.68)/.1),sa0=-.28+.1*sin((t-.34)/.34*T)*(t>.34&&t<.68)
|
|
const H=f=>{c.save();c.translate(108,98);c.scale(.62,.62);c.translate(-128,-120);f();c.restore()}
|
|
// wavy ink hair streaming left behind
|
|
H(()=>{c.lineWidth=7;c.fillStyle='#111';c.beginPath();c.moveTo(128,50);c.bezierCurveTo(60,40,40,100,36,150)
|
|
for(const[x,y,k]of[[-6+8*w,170,0],[18,190,1],[-20+10*w,214,2],[30,226,3],[80,230,4]])c.quadraticCurveTo(x+20,y-20+4*sin(t*T*2+k),x,y)
|
|
c.lineTo(180,200);c.bezierCurveTo(196,140,190,50,128,50);c.fill();c.stroke()
|
|
c.strokeStyle='#fff';c.lineWidth=3;c.beginPath();c.moveTo(60,110);c.quadraticCurveTo(30,150,10+6*w,170);c.moveTo(70,150);c.quadraticCurveTo(46,190,0+8*w,212);c.stroke();c.strokeStyle='#111'})
|
|
// coat with flapping tails, screentone, big white cuffs and a jabot
|
|
const ct=()=>{c.beginPath();c.moveTo(92,156);c.quadraticCurveTo(64,170,60,206);c.quadraticCurveTo(40,226+6*w,22+8*w,246+4*w);c.quadraticCurveTo(50,242,64,252);c.lineTo(168,252);c.quadraticCurveTo(164,176,128,156);c.closePath()}
|
|
c.lineWidth=5;c.fillStyle='#fff';ct();c.fill();c.save();c.clip();c.fillStyle=tn;c.fillRect(0,150,180,110);c.restore();ct();c.stroke()
|
|
c.lineWidth=3;c.beginPath();c.moveTo(60,206);c.quadraticCurveTo(44,228,34+7*w,242);c.stroke()
|
|
c.fillStyle='#fff';c.lineWidth=4;c.beginPath();c.moveTo(96,158);c.lineTo(110,252);c.lineTo(132,252);c.lineTo(126,158);c.closePath();c.fill();c.stroke()
|
|
c.beginPath();c.moveTo(112,160);for(let y=160,k=0;y<230;y+=10,k++)c.quadraticCurveTo(k%2?98:126,y+5,112,y+10);c.lineTo(112,160);c.fill();c.lineWidth=3;c.stroke()
|
|
c.beginPath();c.moveTo(112,166);c.lineTo(112,226);c.stroke()
|
|
c.fillStyle='#111';for(const y of[214,232])c.beginPath(),c.arc(140,y,4,0,T),c.fill()
|
|
// head
|
|
H(()=>{
|
|
c.lineWidth=7;c.fillStyle='#fff';c.beginPath();c.moveTo(74,96);c.bezierCurveTo(70,152,100,184,128,186);c.bezierCurveTo(156,184,186,152,182,96);c.bezierCurveTo(182,44,74,44,74,96);c.fill();c.stroke()
|
|
// eye patch over her left eye, strap across the forehead
|
|
c.lineWidth=3;c.beginPath();c.moveTo(74,112);c.lineTo(96,134);c.moveTo(112,130);c.lineTo(182,104);c.stroke()
|
|
c.fillStyle='#111';c.beginPath();c.ellipse(104,141,12,10,-.2,0,T);c.fill()
|
|
// her uncovered eye, big and bright; the eyepiece covers it while she looks
|
|
eye(c,153,140,1,tn)
|
|
// grin and blush
|
|
c.lineWidth=4;c.fillStyle='#fff';c.beginPath();c.moveTo(112,168);c.quadraticCurveTo(128,184,146,166);c.quadraticCurveTo(128,172,112,168);c.closePath();c.fill();c.stroke()
|
|
c.lineWidth=3;for(const x of[85,171])for(const k of[-5,1,7])c.beginPath(),c.moveTo(x+k-3,170),c.lineTo(x+k+3,162),c.stroke()
|
|
// wavy ink bangs
|
|
c.lineWidth=7;c.fillStyle='#111';c.beginPath();c.moveTo(70,150);c.bezierCurveTo(52,74,86,44,128,44);c.bezierCurveTo(170,44,204,74,186,150);c.quadraticCurveTo(184,118,176,104)
|
|
c.quadraticCurveTo(168,110,160,108);c.quadraticCurveTo(158,94,146,88);c.quadraticCurveTo(138,104,128,108);c.quadraticCurveTo(118,96,108,90);c.quadraticCurveTo(102,106,92,110);c.quadraticCurveTo(84,104,80,100);c.quadraticCurveTo(72,120,70,150);c.closePath();c.fill();c.stroke()
|
|
c.strokeStyle='#fff';c.lineWidth=4;for(const[a,e]of[[1.34,1.46],[1.54,1.64]])c.beginPath(),c.arc(128,110,52,PI*a,PI*e),c.stroke();c.strokeStyle='#111'
|
|
// tricorn hat with skull and crossbones
|
|
const hat=()=>{c.beginPath();c.moveTo(40,82);c.quadraticCurveTo(70,66,84,40);c.quadraticCurveTo(128,4,172,40);c.quadraticCurveTo(186,66,216,82);c.quadraticCurveTo(170,74,128,86);c.quadraticCurveTo(86,74,40,82);c.closePath()}
|
|
c.lineWidth=7;c.fillStyle='#111';hat();c.fill();c.stroke()
|
|
c.strokeStyle='#fff';c.lineWidth=3;c.beginPath();c.moveTo(48,80);c.quadraticCurveTo(86,70,128,80);c.quadraticCurveTo(170,70,208,80);c.stroke()
|
|
c.lineWidth=4;c.beginPath();c.moveTo(114,44);c.lineTo(142,66);c.moveTo(142,44);c.lineTo(114,66);c.stroke()
|
|
c.fillStyle='#fff';c.beginPath();c.arc(128,46,9,0,T);c.fill();c.fillRect(123,50,10,7);c.fillStyle='#111';c.beginPath();c.arc(124.5,46,2.5,0,T);c.arc(131.5,46,2.5,0,T);c.fill()
|
|
c.strokeStyle='#111'
|
|
})
|
|
// spyglass from her right eye, segments sliding out
|
|
const ey=[156-38*lf,178-68*lf],L=[34,30*ex,26*ex],sa=-.95+(.95+sa0)*lf
|
|
c.save();c.translate(...ey);c.rotate(sa)
|
|
let x=4;const seg=[[34,13,true],[30,10.5,false],[26,8,false]]
|
|
for(let k=2;k>=0;k--){const o=4+L.slice(0,k).reduce((a,b)=>a+b,0)-(k?6:0),[l,r,tone]=seg[k];if(k&&!ex)continue
|
|
c.lineWidth=4;c.fillStyle='#fff';c.beginPath();c.rect(o,-r,l,2*r);c.fill();if(tone){c.save();c.clip();c.fillStyle=tn;c.fillRect(o,0,l,r);c.restore()}c.stroke()
|
|
c.lineWidth=3;c.beginPath();c.moveTo(o+l-3,-r-2);c.lineTo(o+l-3,r+2);c.stroke()}
|
|
const tip=4+L[0]+L[1]+L[2]-(ex?12:0)
|
|
c.lineWidth=4;c.fillStyle='#fff';c.beginPath();c.ellipse(tip+(ex?0:0),0,4,ex?9:13,0,0,T);c.fill();c.stroke()
|
|
c.restore()
|
|
// hand on the scope
|
|
const hp=[ey[0]+30*cos(sa),ey[1]+30*sin(sa)+12]
|
|
for(const[l,s]of[[20,'#111'],[11,'#fff']])c.lineWidth=l,c.strokeStyle=s,c.beginPath(),c.moveTo(132,172),c.quadraticCurveTo(170,186-14*lf,hp[0],hp[1]),c.stroke()
|
|
c.strokeStyle='#111';c.lineWidth=4;c.fillStyle='#fff';c.beginPath();c.ellipse(hp[0],hp[1]-2,10,9,0,0,T);c.fill();c.stroke()
|
|
// she spotted something
|
|
if(spot){const g=min(1,(t-.55)/.06);c.lineWidth=4;c.fillStyle='#fff';c.save();c.translate(178,40);c.scale(1.35*g,1.35*g);c.beginPath();c.moveTo(-5,-20);c.lineTo(5,-20);c.lineTo(2,4);c.lineTo(-2,4);c.closePath();c.fill();c.stroke();c.beginPath();c.arc(0,12,4.5,0,T);c.fill();c.stroke()
|
|
c.lineWidth=3;for(const a of[-2.6,-.5])c.beginPath(),c.moveTo(cos(a)*16,sin(a)*16-4),c.lineTo(cos(a)*24,sin(a)*24-4),c.stroke();c.restore()}
|
|
}
|