Flip 16 minimalist creatures to white with a dark edge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 13:32:58 +00:00
co-authored by Claude Opus 5.5
parent 95c6ae7fb4
commit 461a39caa2
49 changed files with 24 additions and 20 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
// 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 ink shape, 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 export const frames=24,delay=85,rim=3,invert=true
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='#111';c.lineWidth=6
+1 -1
View File
@@ -1,6 +1,6 @@
// sheep whose body is a little rain cloud: it floats and bobs, the puffs breathe, tiny legs dangle, raindrops fall from underneath // sheep whose body is a little rain cloud: it floats and bobs, the puffs breathe, tiny legs dangle, raindrops fall from underneath
const{sin,PI,min}=Math,T=2*PI const{sin,PI,min}=Math,T=2*PI
export const frames=24,delay=80,rim=3 export const frames=24,delay=80,rim=3,invert=true
const P=[[78,112,28],[106,90,32],[142,88,32],[170,108,26],[96,130,28],[132,132,30],[160,128,24],[62,130,20]] const P=[[78,112,28],[106,90,32],[142,88,32],[170,108,26],[96,130,28],[132,132,30],[160,128,24],[62,130,20]]
export default(c,t)=>{ export default(c,t)=>{
const y=5*sin(T*t),bl=t>.5&&t<.56 const y=5*sin(T*t),bl=t>.5&&t<.56
+1 -1
View File
@@ -1,6 +1,6 @@
// 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 export const frames=24,delay=85,rim=3,invert=true
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)=>{
+1 -1
View File
@@ -1,6 +1,6 @@
// heron standing on one leg: S-curved neck and long beak, it tilts its head, the plume feathers sway, it blinks // heron standing on one leg: S-curved neck and long beak, it tilts its head, the plume feathers sway, it blinks
const{sin,PI}=Math,T=2*PI const{sin,PI}=Math,T=2*PI
export const frames=24,delay=85,rim=3 export const frames=24,delay=85,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const hd=t>.25&&t<.65?.18*sin(PI*(t-.25)/.4):0,br=sin(T*t),pl=.08*sin(T*t-1),bl=t>.8&&t<.86 const hd=t>.25&&t<.65?.18*sin(PI*(t-.25)/.4):0,br=sin(T*t),pl=.08*sin(T*t-1),bl=t>.8&&t<.86
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#111';c.lineWidth=6
+1 -1
View File
@@ -1,7 +1,7 @@
// fox spirit sitting side-on, three white-tipped tails fanned out behind it swaying one after another, // fox spirit sitting side-on, three white-tipped tails fanned out behind it swaying one after another,
// a fox-fire flame flickering beside its nose, an ear twitches and it blinks // a fox-fire flame flickering beside its nose, an ear twitches and it blinks
const{sin,cos,PI}=Math,T=2*PI const{sin,cos,PI}=Math,T=2*PI
export const frames=24,delay=80,rim=3 export const frames=24,delay=80,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const bl=t>.7&&t<.77,tw=t>.36&&t<.44?.3:0,hr=.04*sin(T*t),f=k=>3*sin(T*t*2+k) const bl=t>.7&&t<.77,tw=t>.36&&t<.44?.3:0,hr=.04*sin(T*t),f=k=>3*sin(T*t*2+k)
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6;c.translate(122,218);c.scale(.88,.88);c.translate(-138,-218) c.fillStyle=c.strokeStyle='#111';c.lineWidth=6;c.translate(122,218);c.scale(.88,.88);c.translate(-138,-218)
+1 -1
View File
@@ -1,6 +1,6 @@
// owl nestled in a crescent moon: the moon's curve is its back and left wing, the lower tip is the perch it grips; big ringed eyes blink, the head tilts, a star twinkles // owl nestled in a crescent moon: the moon's curve is its back and left wing, the lower tip is the perch it grips; big ringed eyes blink, the head tilts, a star twinkles
const{sin,cos,PI,atan2,sqrt,hypot}=Math,T=2*PI const{sin,cos,PI,atan2,sqrt,hypot}=Math,T=2*PI
export const frames=24,delay=85,rim=3 export const frames=24,delay=85,rim=3,invert=true
// crescent edges are circles through tips B and A, bulging s px toward the lower left // crescent edges are circles through tips B and A, bulging s px toward the lower left
const B=[84,60],A=[204,214],M=[(B[0]+A[0])/2,(B[1]+A[1])/2],L=hypot(A[0]-B[0],A[1]-B[1]),n=[(B[1]-A[1])/L,(A[0]-B[0])/L] const B=[84,60],A=[204,214],M=[(B[0]+A[0])/2,(B[1]+A[1])/2],L=hypot(A[0]-B[0],A[1]-B[1]),n=[(B[1]-A[1])/L,(A[0]-B[0])/L]
const circ=s=>{const r=(L*L/4+s*s)/(2*s);return[M[0]-(r-s)*n[0],M[1]-(r-s)*n[1],r]} const circ=s=>{const r=(L*L/4+s*s)/(2*s);return[M[0]-(r-s)*n[0],M[1]-(r-s)*n[1],r]}
+1 -1
View File
@@ -1,6 +1,6 @@
// narwhal swimming along: bobs, flicks its tail, and the white spiral on its long tusk keeps twirling toward the tip // narwhal swimming along: bobs, flicks its tail, and the white spiral on its long tusk keeps twirling toward the tip
const{sin,cos,PI}=Math,T=2*PI const{sin,cos,PI}=Math,T=2*PI
export const frames=24,delay=80,rim=3 export const frames=24,delay=80,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const s=sin(T*t),k=sin(T*t-1.4),bl=t>.8&&t<.86 const s=sin(T*t),k=sin(T*t-1.4),bl=t>.8&&t<.86
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#111';c.lineWidth=6
+1 -1
View File
@@ -1,6 +1,6 @@
// phoenix rising: big wingbeats lift it, the flame crest and tail feathers flicker // phoenix rising: big wingbeats lift it, the flame crest and tail feathers flicker
const{sin,PI}=Math,T=2*PI const{sin,PI}=Math,T=2*PI
export const frames=24,delay=70,rim=3 export const frames=24,delay=70,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const f=sin(T*t) const f=sin(T*t)
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#111';c.lineWidth=6
+1 -1
View File
@@ -1,6 +1,6 @@
// pufferfish: rests, puffs up into a round spiky ball with a surprised look, wobbles, then lets the air out // pufferfish: rests, puffs up into a round spiky ball with a surprised look, wobbles, then lets the air out
const{sin,cos,PI,abs,hypot}=Math,T=2*PI const{sin,cos,PI,abs,hypot}=Math,T=2*PI
export const frames=24,delay=80,rim=3 export const frames=24,delay=80,rim=3,invert=true
const sm=x=>x<0?0:x>1?1:x*x*(3-2*x) const sm=x=>x<0?0:x>1?1:x*x*(3-2*x)
export default(c,t)=>{ export default(c,t)=>{
const g=sm((t-.1)/.25)-sm((t-.6)/.25),cx=134,cy=128+4*sin(T*t),rx=56+24*g+2*g*sin(T*t*4),ry=42+38*g,bl=t>.9&&t<.96 const g=sm((t-.1)/.25)-sm((t-.6)/.25),cx=134,cy=128+4*sin(T*t),rx=56+24*g+2*g*sin(T*t*4),ry=42+38*g,bl=t>.9&&t<.96
+1 -1
View File
@@ -1,6 +1,6 @@
// seahorse standing upright in the current: sways side to side, its dorsal fin flutters, tail curled under, blinks // seahorse standing upright in the current: sways side to side, its dorsal fin flutters, tail curled under, blinks
const{sin,cos,PI}=Math,T=2*PI const{sin,cos,PI}=Math,T=2*PI
export const frames=24,delay=85,rim=3 export const frames=24,delay=85,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const s=sin(T*t),bl=t>.78&&t<.84,f=1+.18*sin(T*t*4) const s=sin(T*t),bl=t>.78&&t<.84,f=1+.18*sin(T*t*4)
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#111';c.lineWidth=6
+1 -1
View File
@@ -1,6 +1,6 @@
// starfish sitting on its two bottom arms, waving hello with its upper right arm, smiling and blinking // starfish sitting on its two bottom arms, waving hello with its upper right arm, smiling and blinking
const{sin,cos,PI}=Math,T=2*PI const{sin,cos,PI}=Math,T=2*PI
export const frames=24,delay=80,rim=3 export const frames=24,delay=80,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const cx=128,cy=134+2*sin(T*t*2),bl=t>.7&&t<.76,wv=.5+.5*sin(T*t*2) const cx=128,cy=134+2*sin(T*t*2),bl=t>.7&&t<.76,wv=.5+.5*sin(T*t*2)
c.fillStyle='#111' c.fillStyle='#111'
+1 -1
View File
@@ -1,6 +1,6 @@
// swan gliding on still water: the neck sways and the head dips, ripples slide back under it, a blink // swan gliding on still water: the neck sways and the head dips, ripples slide back under it, a blink
const{sin,cos,PI}=Math,T=2*PI const{sin,cos,PI}=Math,T=2*PI
export const frames=24,delay=90,rim=3 export const frames=24,delay=90,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const s=sin(T*t),hx=172+5*s,hy=70+4*cos(T*t),bl=t>.55&&t<.62 const s=sin(T*t),hx=172+5*s,hy=70+4*cos(T*t),bl=t>.55&&t<.62
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#111';c.lineWidth=6
+1 -1
View File
@@ -1,6 +1,6 @@
// teapot dragon: the spout is its neck and head, the handle is its arrow-tipped tail; steam puffs from its snout and the lid rattles up // teapot dragon: the spout is its neck and head, the handle is its arrow-tipped tail; steam puffs from its snout and the lid rattles up
const{sin,PI,min}=Math,T=2*PI const{sin,PI,min}=Math,T=2*PI
export const frames=24,delay=85,rim=3 export const frames=24,delay=85,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const lh=t>.4&&t<.6?sin(PI*(t-.4)/.2):0,bl=t>.82&&t<.88 const lh=t>.4&&t<.6?sin(PI*(t-.4)/.2):0,bl=t>.82&&t<.88
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#111';c.lineWidth=6
+1 -1
View File
@@ -1,6 +1,6 @@
// turtle that carries a little cottage as its shell: it plods in place, the chimney puffs smoke, window and door cut out in white // turtle that carries a little cottage as its shell: it plods in place, the chimney puffs smoke, window and door cut out in white
const{sin,PI,max,min}=Math,T=2*PI const{sin,PI,max,min}=Math,T=2*PI
export const frames=24,delay=90,rim=3 export const frames=24,delay=90,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const b=1.5*sin(2*T*t),bl=t>.8&&t<.86 const b=1.5*sin(2*T*t),bl=t>.8&&t<.86
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#111';c.lineWidth=6
+1 -1
View File
@@ -1,6 +1,6 @@
// 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 export const frames=24,delay=80,rim=3,invert=true
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='#111';c.lineWidth=6
+1 -1
View File
@@ -1,6 +1,6 @@
// yeti waving hello: shaggy spiky fur all round, one arm waving, a big open smile and a blink // yeti waving hello: shaggy spiky fur all round, one arm waving, a big open smile and a blink
const{sin,cos,PI}=Math,T=2*PI const{sin,cos,PI}=Math,T=2*PI
export const frames=24,delay=80,rim=3 export const frames=24,delay=80,rim=3,invert=true
export default(c,t)=>{ export default(c,t)=>{
const bl=t>.6&&t<.66,br=sin(T*t),a=-2.65+.28*sin(T*t*2) const bl=t>.6&&t<.66,br=sin(T*t),a=-2.65+.28*sin(T*t*2)
c.fillStyle=c.strokeStyle='#111';c.lineWidth=6 c.fillStyle=c.strokeStyle='#111';c.lineWidth=6
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 68 KiB

+8 -4
View File
@@ -3,6 +3,7 @@
// 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 white sticker border, so black ink still shows on a black page
// and invert=true to swap 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'
@@ -16,9 +17,9 @@ 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}=await import(url(resolve(f))) const{default:draw,frames=16,delay=80,rim=0,invert=false}=await import(url(resolve(f)))
const px=[...Array(frames)].map((_,i)=>{ const px=[...Array(frames)].map((_,i)=>{
const 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){
@@ -26,9 +27,12 @@ for(const[k,f]of files.entries()){
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='#fff';r.fillRect(0,0,S,S)
r.globalCompositeOperation='source-over';r.drawImage(c.canvas,0,0);return Buffer.from(r.getImageData(0,0,S,S).data.buffer) r.globalCompositeOperation='source-over';r.drawImage(c.canvas,0,0);c=r
} }
return Buffer.from(c.getImageData(0,0,S,S).data.buffer) const b=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}})