Draw the minimalist creatures white directly and drop invert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 14:21:01 +00:00
co-authored by Claude Opus 5.5
parent b9200e390f
commit f25e39b727
13 changed files with 18 additions and 22 deletions
+4 -4
View File
@@ -1,9 +1,9 @@
// cat-whale drifting along: one solid ink shape, it bobs, flicks its tail, twitches an ear, puffs a spout and blinks // cat-whale drifting along: one solid white shape with a dark edge, it bobs, flicks its tail, twitches an ear, puffs a spout and blinks
const{sin,cos,PI,min,max}=Math,T=2*PI const{sin,cos,PI,min,max}=Math,T=2*PI
export const frames=24,delay=85,rim=3,invert=true export const frames=24,delay=85,rim=3,rimColor='#111'
export default(c,t)=>{ export default(c,t)=>{
const s=sin(T*t),k=sin(T*t-1.4),p=t/.45,tw=t>.6&&t<.68?.3:0,bl=t>.82&&t<.9,tx=46+4*k,ty=92-5*k const s=sin(T*t),k=sin(T*t-1.4),p=t/.45,tw=t>.6&&t<.68?.3:0,bl=t>.82&&t<.9,tx=46+4*k,ty=92-5*k
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#fff';c.lineWidth=6
c.translate(128,132+7*s);c.rotate(.04*cos(T*t));c.scale(.9,.9);c.translate(-126,-128) c.translate(128,132+7*s);c.rotate(.04*cos(T*t));c.scale(.9,.9);c.translate(-126,-128)
const shape=(x,y,r,f)=>{c.save();c.translate(x,y);c.rotate(r);c.beginPath();f();c.fill();c.stroke();c.restore()} const shape=(x,y,r,f)=>{c.save();c.translate(x,y);c.rotate(r);c.beginPath();f();c.fill();c.stroke();c.restore()}
shape(0,0,0,()=>{ shape(0,0,0,()=>{
@@ -24,7 +24,7 @@ export default(c,t)=>{
for(const d of[-1,1])c.beginPath(),c.moveTo(x,y-h),c.quadraticCurveTo(x+d*w*.8,y-h-w*.5,x+d*w,y-h+w*.5),c.stroke(), for(const d of[-1,1])c.beginPath(),c.moveTo(x,y-h),c.quadraticCurveTo(x+d*w*.8,y-h-w*.5,x+d*w,y-h+w*.5),c.stroke(),
c.beginPath(),c.arc(x+d*w,y-h+w*.5+5,5*g,0,T),c.fill() c.beginPath(),c.arc(x+d*w,y-h+w*.5+5,5*g,0,T),c.fill()
} }
c.fillStyle=c.strokeStyle='#fff' c.fillStyle=c.strokeStyle='#111'
if(bl)c.lineWidth=3.5,c.beginPath(),c.arc(190,128,6,.15*PI,.85*PI),c.stroke() if(bl)c.lineWidth=3.5,c.beginPath(),c.arc(190,128,6,.15*PI,.85*PI),c.stroke()
else c.beginPath(),c.arc(190,130,6.5,0,T),c.fill() else c.beginPath(),c.arc(190,130,6.5,0,T),c.fill()
c.lineWidth=3;c.beginPath();c.moveTo(214,160);c.quadraticCurveTo(206,167,196,165);c.stroke() c.lineWidth=3;c.beginPath();c.moveTo(214,160);c.quadraticCurveTo(206,167,196,165);c.stroke()
+3 -3
View File
@@ -1,11 +1,11 @@
// lion whose mane is dandelion fluff: three seeds blow off and drift away, new ones grow back in their place // lion whose mane is dandelion fluff: three seeds blow off and drift away, new ones grow back in their place
const{sin,cos,PI,min,max}=Math,T=2*PI const{sin,cos,PI,min,max}=Math,T=2*PI
export const frames=24,delay=85,rim=3,invert=true export const frames=24,delay=85,rim=3,rimColor='#111'
const N=18,F=[1,2,3] const N=18,F=[1,2,3]
const seed=c=>{c.beginPath();c.moveTo(0,0);c.lineTo(24,0);for(let j=-2;j<=2;j++)c.moveTo(24,0),c.lineTo(24+10*cos(j*.42),10*sin(j*.42));c.stroke()} const seed=c=>{c.beginPath();c.moveTo(0,0);c.lineTo(24,0);for(let j=-2;j<=2;j++)c.moveTo(24,0),c.lineTo(24+10*cos(j*.42),10*sin(j*.42));c.stroke()}
export default(c,t)=>{ export default(c,t)=>{
const bl=t>.86&&t<.92 const bl=t>.86&&t<.92
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#fff';c.lineWidth=6
c.translate(128,128);c.scale(1.1,1.1);c.translate(-128,-128) c.translate(128,128);c.scale(1.1,1.1);c.translate(-128,-128)
const shape=g=>{c.beginPath();g();c.fill();c.stroke()} const shape=g=>{c.beginPath();g();c.fill();c.stroke()}
c.lineWidth=7;c.beginPath();c.moveTo(150,204);c.quadraticCurveTo(186,210,188+6*sin(T*t),178);c.stroke();c.lineWidth=6 c.lineWidth=7;c.beginPath();c.moveTo(150,204);c.quadraticCurveTo(186,210,188+6*sin(T*t),178);c.stroke();c.lineWidth=6
@@ -23,7 +23,7 @@ export default(c,t)=>{
c.lineWidth=6 c.lineWidth=6
for(const d of[-1,1])shape(()=>c.arc(128+d*30,70,11,0,T)) for(const d of[-1,1])shape(()=>c.arc(128+d*30,70,11,0,T))
shape(()=>c.arc(128,100,40,0,T)) shape(()=>c.arc(128,100,40,0,T))
c.fillStyle=c.strokeStyle='#fff' c.fillStyle=c.strokeStyle='#111'
for(const d of[-1,1])if(bl)c.lineWidth=3.5,c.beginPath(),c.arc(128+d*15,94,5,.15*PI,.85*PI),c.stroke() for(const d of[-1,1])if(bl)c.lineWidth=3.5,c.beginPath(),c.arc(128+d*15,94,5,.15*PI,.85*PI),c.stroke()
else c.beginPath(),c.arc(128+d*15,96,5,0,T),c.fill() else c.beginPath(),c.arc(128+d*15,96,5,0,T),c.fill()
c.beginPath();c.moveTo(121,108);c.lineTo(135,108);c.lineTo(128,116);c.closePath();c.fill() c.beginPath();c.moveTo(121,108);c.lineTo(135,108);c.lineTo(128,116);c.closePath();c.fill()
+3 -3
View File
@@ -1,9 +1,9 @@
// praying mantis: it sways, tilts its triangle head side to side, and gives its folded forearms a little pump // praying mantis: it sways, tilts its triangle head side to side, and gives its folded forearms a little pump
const{sin,cos,PI,max}=Math,T=2*PI const{sin,cos,PI,max}=Math,T=2*PI
export const frames=24,delay=85,rim=3,invert=true export const frames=24,delay=85,rim=3,rimColor='#111'
export default(c,t)=>{ export default(c,t)=>{
const s=sin(T*t),pm=t>.3&&t<.6?sin(PI*(t-.3)/.3):0,ht=.3*sin(T*t+1) const s=sin(T*t),pm=t>.3&&t<.6?sin(PI*(t-.3)/.3):0,ht=.3*sin(T*t+1)
c.fillStyle=c.strokeStyle='#111';c.lineWidth=4 c.fillStyle=c.strokeStyle='#fff';c.lineWidth=4
const shape=f=>{c.beginPath();f();c.fill();c.stroke()} const shape=f=>{c.beginPath();f();c.fill();c.stroke()}
for(const[hx,kx,ky,fx]of[[130,152,186,166],[118,98,190,82],[112,62,184,46]])c.beginPath(),c.moveTo(hx,160),c.lineTo(kx,ky),c.lineTo(fx,212),c.stroke() for(const[hx,kx,ky,fx]of[[130,152,186,166],[118,98,190,82],[112,62,184,46]])c.beginPath(),c.moveTo(hx,160),c.lineTo(kx,ky),c.lineTo(fx,212),c.stroke()
c.translate(122,212);c.rotate(.03*s);c.translate(-122,-212) c.translate(122,212);c.rotate(.03*s);c.translate(-122,-212)
@@ -17,6 +17,6 @@ export default(c,t)=>{
c.save();c.translate(152,80);c.rotate(ht) c.save();c.translate(152,80);c.rotate(ht)
c.lineWidth=3;for(const d of[-1,1])c.beginPath(),c.moveTo(d*6,-16),c.quadraticCurveTo(d*12,-48,d*30,-64),c.stroke() c.lineWidth=3;for(const d of[-1,1])c.beginPath(),c.moveTo(d*6,-16),c.quadraticCurveTo(d*12,-48,d*30,-64),c.stroke()
c.lineWidth=6;shape(()=>{c.moveTo(-20,-16);c.lineTo(20,-16);c.lineTo(0,10);c.closePath()}) c.lineWidth=6;shape(()=>{c.moveTo(-20,-16);c.lineTo(20,-16);c.lineTo(0,10);c.closePath()})
c.fillStyle='#fff';for(const d of[-1,1])c.beginPath(),c.arc(d*15,-15,3.5,0,T),c.fill() c.fillStyle='#111';for(const d of[-1,1])c.beginPath(),c.arc(d*15,-15,3.5,0,T),c.fill()
c.restore() c.restore()
} }
+3 -3
View File
@@ -1,16 +1,16 @@
// will-o-wisp floating: a round flame spirit bobbing and tilting, its flame tips flicker, embers drift up beside it, it blinks // will-o-wisp floating: a round flame spirit bobbing and tilting, its flame tips flicker, embers drift up beside it, it blinks
const{sin,PI}=Math,T=2*PI const{sin,PI}=Math,T=2*PI
export const frames=24,delay=80,rim=3,invert=true export const frames=24,delay=80,rim=3,rimColor='#111'
export default(c,t)=>{ export default(c,t)=>{
const bl=t>.4&&t<.46,Mx=128+6*sin(T*t*2),My=40+4*sin(T*t*3),R=4*sin(T*t*2+1),L=4*sin(T*t*2+2.5) const bl=t>.4&&t<.46,Mx=128+6*sin(T*t*2),My=40+4*sin(T*t*3),R=4*sin(T*t*2+1),L=4*sin(T*t*2+2.5)
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#fff';c.lineWidth=6
for(const[i,x]of[-46,40,-30].entries()){const q=(t+i/3)%1;c.beginPath();c.arc(128+x+5*sin(T*q),110-74*q,5*sin(PI*q),0,T);c.fill()} for(const[i,x]of[-46,40,-30].entries()){const q=(t+i/3)%1;c.beginPath();c.arc(128+x+5*sin(T*q),110-74*q,5*sin(PI*q),0,T);c.fill()}
c.translate(128,132+8*sin(T*t));c.rotate(.05*sin(T*t+1));c.scale(1.15,1.15);c.translate(-128,-128) c.translate(128,132+8*sin(T*t));c.rotate(.05*sin(T*t+1));c.scale(1.15,1.15);c.translate(-128,-128)
c.beginPath();c.moveTo(82,148);c.arc(128,148,46,PI,0,true) c.beginPath();c.moveTo(82,148);c.arc(128,148,46,PI,0,true)
c.bezierCurveTo(176,122,176,96,170+R,74);c.quadraticCurveTo(158,84,150,94) c.bezierCurveTo(176,122,176,96,170+R,74);c.quadraticCurveTo(158,84,150,94)
c.bezierCurveTo(152,72,140,54,Mx,My);c.bezierCurveTo(116,56,104,74,106,92) c.bezierCurveTo(152,72,140,54,Mx,My);c.bezierCurveTo(116,56,104,74,106,92)
c.quadraticCurveTo(98,86,88+L,80);c.bezierCurveTo(80,100,80,124,82,148);c.fill();c.stroke() c.quadraticCurveTo(98,86,88+L,80);c.bezierCurveTo(80,100,80,124,82,148);c.fill();c.stroke()
c.fillStyle=c.strokeStyle='#fff' c.fillStyle=c.strokeStyle='#111'
for(const d of[-1,1])if(bl)c.lineWidth=3.5,c.beginPath(),c.arc(128+d*16,136,7,.15*PI,.85*PI),c.stroke() for(const d of[-1,1])if(bl)c.lineWidth=3.5,c.beginPath(),c.arc(128+d*16,136,7,.15*PI,.85*PI),c.stroke()
else c.beginPath(),c.ellipse(128+d*16,138,7,10,0,0,T),c.fill() else c.beginPath(),c.ellipse(128+d*16,138,7,10,0,0,T),c.fill()
c.lineWidth=3.5;c.beginPath();c.arc(128,156,7,.2*PI,.8*PI);c.stroke() c.lineWidth=3.5;c.beginPath();c.arc(128,156,7,.2*PI,.8*PI);c.stroke()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 81 KiB

+5 -9
View File
@@ -2,9 +2,8 @@
// the section folder (bw, color, anime, manga, mini, minicolor) only picks where it shows in the gallery, it isn't part of the url // the section folder (bw, color, anime, manga, mini, minicolor) only picks where it shows in the gallery, it isn't part of the url
// npm run gen renders only what's missing (published files never change by accident) // npm run gen renders only what's missing (published files never change by accident)
// npm run gen -- cat/blink renders only the named ones, even if they already exist // npm run gen -- cat/blink renders only the named ones, even if they already exist
// a file can export rim=<px> to get a white sticker border, so black ink still shows on a black page // a file can export rim=<px> to get a sticker border, white unless it also exports rimColor, so a shape that matches the page still shows
// delay can be one number or a list with one entry per frame (ms) // delay can be one number or a list with one entry per frame (ms)
// invert=true swaps ink and paper (#111 <-> #fff), turning a black silhouette with a white rim into a white one with a dark rim
import{createCanvas}from'@napi-rs/canvas' import{createCanvas}from'@napi-rs/canvas'
import sharp from'sharp' import sharp from'sharp'
import{readdirSync as rd,existsSync as ex,mkdirSync as mk,statSync as st}from'fs' import{readdirSync as rd,existsSync as ex,mkdirSync as mk,statSync as st}from'fs'
@@ -18,22 +17,19 @@ if(dup)throw Error(`${dup} is in more than one section`)
for(const[k,f]of files.entries()){ for(const[k,f]of files.entries()){
const id=ids[k],o=`public/${id}` const id=ids[k],o=`public/${id}`
if(redo.length?!redo.includes(id):ex(o+'.webp')&&ex(o+'.gif'))continue if(redo.length?!redo.includes(id):ex(o+'.webp')&&ex(o+'.gif'))continue
const{default:draw,frames=16,delay=80,rim=0,invert=false}=await import(url(resolve(f))) const{default:draw,frames=16,delay=80,rim=0,rimColor='#fff'}=await import(url(resolve(f)))
const px=[...Array(frames)].map((_,i)=>{ const px=[...Array(frames)].map((_,i)=>{
let c=createCanvas(S,S).getContext('2d') let c=createCanvas(S,S).getContext('2d')
Object.assign(c,{lineWidth:7,lineCap:'round',lineJoin:'round',strokeStyle:'#111',fillStyle:'#fff'}) Object.assign(c,{lineWidth:7,lineCap:'round',lineJoin:'round',strokeStyle:'#111',fillStyle:'#fff'})
draw(c,i/frames,i) draw(c,i/frames,i)
if(rim){ if(rim){
// white silhouette grown by rim px (copies shifted around rings 1..rim px out), then the frame on top // silhouette grown by rim px (copies shifted around rings 1..rim px out) filled with rimColor, then the frame on top
const r=createCanvas(S,S).getContext('2d') const r=createCanvas(S,S).getContext('2d')
for(let d=1;d<=rim;d++)for(let a=0;a<24;a++)r.drawImage(c.canvas,d*Math.cos(a*Math.PI/12),d*Math.sin(a*Math.PI/12)) for(let d=1;d<=rim;d++)for(let a=0;a<24;a++)r.drawImage(c.canvas,d*Math.cos(a*Math.PI/12),d*Math.sin(a*Math.PI/12))
r.globalCompositeOperation='source-in';r.fillStyle='#fff';r.fillRect(0,0,S,S) r.globalCompositeOperation='source-in';r.fillStyle=rimColor;r.fillRect(0,0,S,S)
r.globalCompositeOperation='source-over';r.drawImage(c.canvas,0,0);c=r r.globalCompositeOperation='source-over';r.drawImage(c.canvas,0,0);c=r
} }
const b=Buffer.from(c.getImageData(0,0,S,S).data.buffer) return Buffer.from(c.getImageData(0,0,S,S).data.buffer)
// 17 and 255 trade places, the antialiased grays in between flip along, alpha is untouched
if(invert)for(let j=0;j<b.length;j++)if(j%4<3)b[j]=Math.min(255,272-b[j])
return b
}) })
mk(dirname(o),{recursive:true}) mk(dirname(o),{recursive:true})
const img=()=>sharp(Buffer.concat(px),{raw:{width:S,height:S*frames,channels:4,pageHeight:S}}) const img=()=>sharp(Buffer.concat(px),{raw:{width:S,height:S*frames,channels:4,pageHeight:S}})