Animating Rick and Morty One Pixel at a Time

February 4, 2025

After 8 months of working on this post I don’t know how to start it except to say it culminates in this animation of Rick made in 240 lines of code. No libraries, no images. It was written in a live coding editor that I embedded in this post so that you can program animations. Let me explain how this started…

vec2 rotateAt(vec2 p, float angle, vec2 origin) { 
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float value, float inMin, float inMax, float outMin, float outMax) { 
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  { 
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { 
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) { 
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { 
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) { 
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) { 
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) { 
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}
#define H(i,j) fract(sin(dot(ceil(P+vec2(i,j)), resolution.xy )) * 4e3)
float N( vec2 P) { 
    float s,i,w = .5;
    for (; i < 3. ; i++, w *= .4, P *= 1.9 ) {
        vec2 F = fract( P *= mat2(.866,-.5,.5,.866) ); 
        F *= F*(3.-F-F);
        s += w* mix( mix(H(0,0) , H(1,0), F.x),
                     mix(H(0,1) , H(1,1), F.x),
                     F.y );
    }
    return s;
}
vec3 portal(vec2 pixel, float time) { 
    float l = length( pixel ), 
          a = atan(pixel.y, pixel.x) / 6.28 + .5,
          k = 10.;
     
    a = fract(a + l*.3 - time*.01 );
    vec2 U = vec2( l+time*.3, a );
     
    return vec3[]( vec3(.18, .53, .09),
                    vec3(.56, .89, .16),
                    vec3(.35, .84, .11),
                    vec3(.92, .98, .85)
                  ) [ int( 4.* pow( mix( N(U*k), N(U*k-vec2(0,k)), U.y) * 1.5, 2.5))];
}

vec3 color_for_pixel(vec2 pixel, float time) { 
    { 
        // rotate the whole drawing
        pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
        pixel.y += .1;
    
    
    // Blink eyes
    if (mod(time+1., 3.) < .09) {
        // closed eyes
        float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
        if (d < .008) return vec3(0);      
    } 
    else { 
        // move pupils randomly
        vec2 pupil_warp = pixel + vec2(.095,-.18);
        pupil_warp.x -= noise(vec2(round(time)*7.+.5, 0.5))*.1;
        pupil_warp.y -= noise(vec2(round(time)*9.+.5, 0.5))*.1;
        pupil_warp.x = abs(pupil_warp.x) - .16;
        float d = star(pupil_warp, 0.019, 6., .9);
        if (d < 0.007) {
            return vec3(.1);
        }
            
        // Eyeballs
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) return vec3(step(.013, -d));
        
        // under eye lines
        bool should_show = pixel.y < 0.25 && 
            (abs(pixel.x+.29) < .05 || 
            abs(pixel.x-.12) < .085);
        if (abs(d - .04) < .0055 && should_show) return vec3(0);
    }
  
    
        // Mouth
        float d = bezier(pixel,  
                     vec2(-.26, -.28), 
                     vec2(-.05,-.42), 
                     vec2(.115, -.25));
        if (d < .11) {
            // Teeth
            float width = .065;
            vec2 teeth = pixel;
            teeth.x = mod(teeth.x, width)-width*.5;
            teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34;
            teeth.y = abs(teeth.y)-.06;
            d = parabola(teeth, 38.);
            if (d < 0. && abs(pixel.x+.06) < .194) 
                return vec3(0.902, 0.890, 0.729)*step(d, -.01);
          
            // Tongue
            // `map()` is used to change the thickness of 
            // the tongue along the x axis
            vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
            float tongue_thickness = map(tongue.x, -.16, .01, .02, .045);
            d = bezier(tongue,  
                vec2(-.16, -.35), 
                vec2(.001,-.33), 
                vec2(.01, -.5)) - tongue_thickness;
            if (d < 0.0) 
                return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
              
            // mouth fill color
            return vec3(.42, .147, .152); 
        } 
        
        // lip outlines
        if (d < .12 || (abs(d-.16) < .005 
                        && (pixel.x*-6.4 > -pixel.y+1.6 
                          || pixel.x*1.7 > -pixel.y+.1 
                          || pixel.y < -0.49))) 
            return vec3(0); 
        
        // lips
        if (d < .16) return vec3(.838, .799, 0.76);
       
        
    
        // Nose  
        d = min(
                bezier(pixel, 
                    vec2(-.15, -.13), 
                    vec2(-.21,-.14), 
                    vec2(-.14, .08)),
                bezier(pixel, 
                    vec2(-.085, -.01), 
                    vec2(-.12, -.13),
                    vec2(-.15,-.13)));
        if (d < 0.0055) return vec3(0);
    

    // Eyebrow
    d = bezier(pixel,  
            vec2(-.34, .38), 
            // NEW animate the middle up and down
            vec2(-.05, 0.5 + cos(time)*.1),
            vec2(.205, .36)) - 0.035;
    if (d < 0.0) 
        return vec3(.71, .839, .922)*step(d, -.013);
    
        d = min(
            // Head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01);
    
    
        // Hair     
        float twist = sin(time*2.-length(pixel)*2.1)*.12;
        vec2 hair = rotateAt(pixel, twist, vec2(0.,.1));
        hair -= vec2(.08,.15);
        hair.x *= 1.3;
        hair = warp(hair, 4.0, 0.07);
        d = star(hair, 0.95, 11., .28);
        if (d < 0.) {
            return vec3(0.682, 0.839, 0.929)*step(d, -0.012);
        }
    }
    
    return portal(pixel, time);
}

Eight months ago I published a video titled “I Made a 3D Modeler, in C, in a Week”. The video has several animations, like this one that illustrates the the marching cubes algorithm:

#define INTERP 1.0

#define GRID_RESOLUTION 0

#define TRIANGULATE_4
#define TRIANGULATE_3
#define TRIANGULATE_2
#define TRIANGULATE_1

#define SHOW_INNER_DOTS 1
#define SHOW_OUTER_DOTS 1

#define ANTIALIAS_LEVEL 3

vec3 fill_color = vec3(0.4);

vec3 wireframe_color = vec3(.1);
float wireframe_thickness = .003;

vec2 repeated( vec2 p, float s ) {
    vec2 r = p - s*floor(p/s);
    return r;
}

float Circle(vec2 p, vec2 origin, float radius) {
    return length(p - origin) - radius;
}

float sdTriangle( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2 )
{
    vec2 e0 = p1 - p0;
    vec2 e1 = p2 - p1;
    vec2 e2 = p0 - p2;

    vec2 v0 = p - p0;
    vec2 v1 = p - p1;
    vec2 v2 = p - p2;

    vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
    vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
    vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );

    float s = e0.x*e2.y - e0.y*e2.x;
    vec2 d = min( min( vec2( dot( pq0, pq0 ), s*(v0.x*e0.y-v0.y*e0.x) ),
                       vec2( dot( pq1, pq1 ), s*(v1.x*e1.y-v1.y*e1.x) )),
                       vec2( dot( pq2, pq2 ), s*(v2.x*e2.y-v2.y*e2.x) ));

    return -sqrt(d.x)*sign(d.y);
}

float sdf(vec2 p) {
    float d = Circle(p, vec2(0,-.2), 0.7);

    d = min(d, Circle(vec2(abs(p.x),p.y), vec2(.73,.45), 0.4));
    return d;
}

float triangle_wave(float x, float amplitude, float period) {
    return (amplitude/period) * (period - abs(mod(x, (2.*period)) - period) );
}

    vec2 interpVert(vec2 p1, vec2 p2, float w1, float w2) {
        return p1 + (-w1 / (w2-w1)) * (p2-p1);
    }

vec3 color_for_pixel(vec2 st, float time) {

    vec3 color;
    if (sdf(st) > 0.) {
        color = vec3(.2);
    } else {
        color = vec3(1.);
    }

    float grid_resolution =  sin(time/1.2)/4. + 0.4;

    // find 4 nearest points
    vec2 p1 = floor((st) / grid_resolution) * grid_resolution;
    vec2 p2 = p1 + vec2(0, grid_resolution);
    vec2 p3 = p1 + vec2(grid_resolution, 0);
    vec2 p4 = p1 + vec2(grid_resolution, grid_resolution);

    vec2 nearest = vec2(round(st.x/grid_resolution)*grid_resolution,
                        round(st.y/grid_resolution)*grid_resolution);

    vec2 j = abs(nearest - st);


    // sample sdf
    float v1 = sdf(p1);
    float v2 = sdf(p2);
    float v3 = sdf(p3);
    float v4 = sdf(p4);

    // count number inside
    float count = step(0.,-v1) + step(0.,-v2) +step(0.,-v3) +step(0.,-v4);

    vec2 p12 = (p1 + p2) / 2.;
    vec2 p13 = (p1 + p3) / 2.;
    vec2 p24 = (p4 + p2) / 2.;
    vec2 p34 = (p4 + p3) / 2.;


    p12 = mix(p12, interpVert(p1, p2, v1, v2), min(1.,time*INTERP));
    p13 = mix(p13, interpVert(p1, p3, v1, v3), min(1.,time*INTERP));
    p24 = mix(p24, interpVert(p2, p4, v2, v4), min(1.,time*INTERP));
    p34 = mix(p34, interpVert(p4, p3, v4, v3), min(1.,time*INTERP));

    float d = 10.;

    vec3 special_fill = fill_color;

    if (count == 4.) {
        #ifdef TRIANGULATE_4
        special_fill = vec3(0.178, 0.321, 0.400);
        d = min(sdTriangle(st, p1,p2,p3), sdTriangle(st, p4,p2,p3));
        #endif
    } else if (count == 3.) {
        #ifdef TRIANGULATE_3
        special_fill = vec3(0.431, 0.532, 0.595);
        if (v1  > 0.) {
            d = min(sdTriangle(st, p4,p2,p12), min(sdTriangle(st, p4,p3,p13), sdTriangle(st, p4,p13,p12)));
        } else if (v2  > 0.) {
            d = min(sdTriangle(st, p1,p3,p12), min(sdTriangle(st, p12,p3,p24), sdTriangle(st, p4,p3,p24)));
        } else if (v3  > 0.) {
            d = min(sdTriangle(st, p1,p2,p13), min(sdTriangle(st, p2,p13,p34), sdTriangle(st, p2,p4,p34)));
        } else {
            d = min(sdTriangle(st, p1,p2,p24), min(sdTriangle(st, p1,p24,p34), sdTriangle(st, p1,p3,p34)));
        }
        #endif
    } else if (count == 2.) {
        #ifdef TRIANGULATE_2
        special_fill = vec3(0.622, 0.692, 0.738);
        if ((v1 < 0.) == (v2 < 0.) || (v1 < 0.) == (v3 < 0.)) {
            if (v1 < 0. && v2 < 0.) {
                d = min(sdTriangle(st, p1,p2,p24), sdTriangle(st, p1,p24,p13));
            } else if (v3 < 0. && v4 < 0.) {
                d = min(sdTriangle(st, p3,p4,p24), sdTriangle(st, p3,p24,p13));
            } else if (v3 < 0. && v1 < 0.) {
                d = min(sdTriangle(st, p3,p1,p12), sdTriangle(st, p3,p12,p34));
            } else if (v2 < 0. && v4 < 0.){
                d = min(sdTriangle(st, p4,p2,p12), sdTriangle(st, p4,p12,p34));
            }
        } else {
            // unhandled
        }
        #endif
    } else if (count == 1.) {
        #ifdef TRIANGULATE_1
        special_fill = vec3(0.825, 0.815, 0.794);
        if (v1  < 0.) {
            d = sdTriangle(st, p1,p12,p13);
        } else if (v2  < 0.) {
            d = sdTriangle(st, p2,p12,p24);
        } else if (v3  < 0.) {
            d = sdTriangle(st, p3,p13,p34);
        } else {
            d = sdTriangle(st, p4,p24,p34);
        }
        #endif
    }

    if (abs(d) < wireframe_thickness) {
        color = wireframe_color;
    } else if (d < 0.) {
        color = special_fill;
    }


    if (Circle(repeated(st+.05, grid_resolution), vec2(.05,.05), 0.007) < 0.) {

        if (sdf(nearest) < 0.) {
            #if SHOW_INNER_DOTS
            color =  vec3(0.404, 0.827, 0.478);
            #endif
        } else {
            #if SHOW_OUTER_DOTS
            color =  vec3(0.875, 0.376, 0.243);
            #endif
        }
    }

    return color;
}

I needed this animation for the video to make sense, but couldn’t get past how painful and time consuming it’d be to make in a typical animation program. It seemed like the only way to accurately and quickly make it was with code. So I started coding, and the animation above is what I ended up with. I’m pretty happy with it.

I’m going to show you how to make your own animations, but instead of talking about that marching cubes animation, we’ll animate Rick from Rick and Morty, because that’s more fun. All the techniques I use for Rick can be used for other animations or graphical effects — useful ones for your own presentations, videos, video games, or just for fun. Ok? Let’s go!

Getting Started

This is the editor I used to create Rick. Try changing green = 0.9 to green = 0.1 in the code below and the preview will update immediately.

vec3 color_for_pixel(vec2 pixel, float time) {
    // fract returns fractional part. fract(1.3) == 0.3
    float red   = fract(pixel.y); 
    float green = 0.9;
    float blue  = fract(pixel.x);
    return vec3(red, green, blue);
}

This is OpenGL Shading Language (GLSL). The color_for_pixel function runs on your GPU for every pixel in the preview. Amazingly this is all you need to make animations — a function that answers “What color should this pixel be at this time?”.

I’ve placed optional challenges throughout the article for people that want to go a little deeper. Like this:

What happens if you set green = time? What could you do to make it keep going? (time counts seconds since last edit)

Let’s use GLSL’s built in1 length() function to visualize how far each pixel is from the center of the screen (aka the origin, aka position (0,0)). By returning that distance as the pixel’s color, we get 0 (black) near the center, and fade to 1 (white) further away:

vec3 color_for_pixel(vec2 pixel, float time) {
    return vec3(length(pixel));
}

GLSL Tip: vec3(x) is the same as vec3(x, x, x). We’ll use this trick a lot.

To draw a circle, we compare the distance to a radius:

vec3 color_for_pixel(vec2 pixel, float time) {
    float radius = 0.6;
    return vec3(length(pixel) > radius);
}

GLSL Tip: vec3 turns the boolean result of > into 1 or 0.

What would that circle look like if you replaced length() with your own function that calculates Manhattan distance?

We can extract that into a reusable circle() function:

float circle(vec2 pixel, float radius) {
    return length(pixel) - radius;
}

vec3 color_for_pixel(vec2 pixel, float time) {

    if (circle(pixel - vec2(.3, -.3), .4) < 0.0) {
        return vec3(0.2,.7,.5);
    }
  
    if (circle(pixel - vec2(-.4,0), .8) < 0.0) {
        return vec3(.7,.5, .3);
    }

    return vec3(.2);
}

The circles are positioned by shifting the pixel passed to circle(). The line order of that code is important - it determines which circle appears in front of the other.

Notice that circle() returns the distance to the perimeter instead of just a bool to indicate inside/outside. This is known as a “signed distance field” (SDF) function. The word “signed” here means that the distances for locations inside the shape are negative, and positive outside. We’ll use the distance to achieve some cool effects in a bit.

There are many SDF functions besides circle(). Here are a few we’ll be using:

// Click {...} to see the code
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    // from https://www.shadertoy.com/view/MlKcDD
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // from https://www.shadertoy.com/view/3tSGDy
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en));

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}
float round_rect(vec2 p, vec2 size, vec4 radii) { // fold
    // from https://www.shadertoy.com/view/4llXD7
    radii.xy = (p.x>0.0)?radii.xy : radii.zw;
    radii.x  = (p.y>0.0)?radii.x  : radii.y;
    vec2 q = abs(p)-size+radii.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x;
}

vec3 color_for_pixel(vec2 pixel, float time) {
    if (bezier(pixel, 
        vec2(-.7,-.35), 
        vec2(-1.5,-.4), 
        vec2(-1.2,.35)) < 0.1) 
        return vec3(.9,.3,.3); 
    
    if (round_rect(pixel, vec2(.3, .4), vec4(.1)) < 0.0) 
        return vec3(.3, .9, .3); 
    
    if (star(pixel - vec2(1.,0.), .45, 5., .3) < 0.0) 
        return vec3(.2, .4, .9);
    
    return vec3(1.0);
}

And that’s the basics. Let’s get started with Rick.

Drawing Rick

I wish I could tell you I had the ability to look at a cartoon and then effortlessly replicate it in code. Unfortunately, I don’t. I spent a lot of time painstakingly trying things to recreate Rick’s face from the season 1 poster.

I did find one trick that sped up the trial and error process: I flashed my reference image of Rick on top of the preview while programming. The editor below has that enabled so you can experience what my week has been like.

Change the size and corner radii parameters to make the rectangle match Rick’s head shape.

float round_rect(vec2 p, vec2 size, vec4 radii) { // fold
    radii.xy = (p.x>0.0)?radii.xy : radii.zw;
    radii.x  = (p.y>0.0)?radii.x  : radii.y;
    vec2 q = abs(p)-size+radii.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x;
}
   
vec3 color_for_pixel(vec2 pixel, float time) {
    float dist = round_rect(
        pixel, 
        // Change these:
        vec2(.3, .5),  // size
        vec4(.1, .01, .05, .1) // corner radii
    );
    
    if (dist < 0.) 
        return vec3(.838, 0.8, 0.76);
    
    return vec3(1);
}

In case it isn’t obvious by now, the techniques in this post won’t be replacing your favorite vector drawing tool. This is the only time we’ll do the flashing exercise; just know that all the seemingly random numbers in the rest of this post were discovered via this process. I found the color values using an image editor’s eyedropper tool.

Ok, so here are the values I came up with for Rick’s head. I also added a second round_rect() for his ear:

float round_rect(vec2 p, vec2 size, vec4 radii) { // fold
    radii.xy = (p.x>0.0)?radii.xy : radii.zw;
    radii.x  = (p.y>0.0)?radii.x  : radii.y;
    vec2 q = abs(p)-size+radii.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x;
}
   
vec3 color_for_pixel(vec2 pixel, float time) {
    
    vec3 skin_color = vec3(0.838, 0.799, 0.760);
    
    // head
    float dist = round_rect(
        pixel, 
        vec2(.36, 0.6385), 
        vec4(.34, .415, .363, .315)
    );
    if (dist < 0.) return skin_color;
    
    // ear
    dist = round_rect(
        pixel + vec2(-.32, .15), 
        vec2(.15, 0.12), 
        vec4(.13,.1,.13,.13));
    if (dist < 0.) return skin_color;
    
    return vec3(1);
}

Let’s add the outline. This is where drawing with signed distance functions comes in handy. We can return black for pixels with a distance between -0.01 and 0.0.

float round_rect(vec2 p, vec2 size, vec4 radii) { // fold
    radii.xy = (p.x>0.0)?radii.xy : radii.zw;
    radii.x  = (p.y>0.0)?radii.x  : radii.y;
    vec2 q = abs(p)-size+radii.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x;
}
   
vec3 color_for_pixel(vec2 pixel, float time) {

    vec3 skin_color = vec3(0.838, 0.799, 0.760);
    
    // head
    float dist = round_rect(
        pixel, 
        vec2(.36, 0.6385), 
        vec4(.34, .415, .363, .315)
    );
    
    if (dist < -0.01) return skin_color; 
    if (dist < 0.0) return vec3(0); // outline
    
    // ear
    dist = round_rect(
        pixel + vec2(-.32, .15), 
        vec2(.15, 0.12), 
        vec4(.13,.1,.13,.13));
        
    if (dist < -0.01) return skin_color;
    if (dist < 0.0) return vec3(0); // outline
    
    
    return vec3(1); // background
}

That line between the ear and the head shouldn’t be there (according to my reference image of Rick). I don’t want to outline each shape individually, I want to outline the union of the shapes. Union is easy with SDFs - use min() to combine two distances:

float round_rect(vec2 p, vec2 size, vec4 radii) { // fold
    radii.xy = (p.x>0.0)?radii.xy : radii.zw;
    radii.x  = (p.y>0.0)?radii.x  : radii.y;
    vec2 q = abs(p)-size+radii.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x;
}
   
vec3 color_for_pixel(vec2 pixel, float time) {
    float dist = min( // <- combine the shapes
        // head
        round_rect(
        pixel, 
        vec2(.36, 0.6385), 
        vec4(.34, .415, .363, .315)),
        
        // ear
        round_rect(
        pixel + vec2(-.32, .15), 
        vec2(.15, 0.12), 
        vec4(.13,.1,.13,.13))
    );
        
    if (dist < -0.01) return vec3(0.838, 0.799, 0.760);
    if (dist < 0.0) return vec3(0);
    
    return vec3(1);
}

There are other ways to combine two signed distance fields. Try swapping out min() for the smooth union function to smoothly blend the ear with the head.

Let’s draw an eye:

float circle(vec2 pixel, float radius) { // fold
    return length(pixel) - radius;
}
float round_rect(vec2 p, vec2 size, vec4 radii) { // fold
    radii.xy = (p.x>0.0)?radii.xy : radii.zw;
    radii.x  = (p.y>0.0)?radii.x  : radii.y;
    vec2 q = abs(p)-size+radii.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); 

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}
   
vec3 color_for_pixel(vec2 pixel, float time) {
     // pupil
    vec2 pupil_pos = pixel - vec2(.16-.13,.24); 

    // subtract 0.007 to outset & round the corners of star
    if (star(pupil_pos, 0.019, 6., .9) - 0.007 < 0.0) {
        return vec3(.1);
    }
    
    // eyeball
    vec2 eyeball_pos = pixel;
    eyeball_pos.y *= .93; // stretch vertically
    eyeball_pos -= vec2(0.07, .16);
    float dist = circle(eyeball_pos, .16);
    if (dist < 0.0) return vec3(dist < -0.013);
    
    // head
    { // fold
    dist = min(
        // head
        round_rect(
        pixel, 
        vec2(.36, 0.6385), 
        vec4(.34, .415, .363, .315)),
        
        // ear
        round_rect(
        pixel + vec2(-.32, .15), 
        vec2(.15, 0.12), 
        vec4(.13,.1,.13,.13))
    );
        
    if (dist < -0.01) return vec3(0.838, 0.799, 0.760);
    if (dist < 0.0) return vec3(0); // outline
    }
    
    return vec3(1.);
}

Two interesting things here:

  1. eyeball_pos.y *= .93 stretches the eyeball a tiny bit — just like we move shapes by adding to positions, we scale by multiplying positions.
  2. I used a 6-point star for the eye, and I subtracted a little from the star’s distance to round its corners. Any SDF shape can be rounded this way. It helps to visualize the distance field so you see how it gets more round the further from the shape you get:
float star(vec2 p, float r, float points, float ratio) { // fold
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); 

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}
   
vec3 color_for_pixel(vec2 pixel, float time) {
    float d = star(pixel, 0.4, 6., .5);
    
    // show blue inside shape, orange outside
    vec3 color = (d < 0.0) ? vec3(0.5, .8, 1.) : vec3(0.98,.6,.13);
    color *= sin(d*150.)*.1+.8; // show distance field lines
    color *= 1.0 - exp(-20.0*abs(d)); // darken near perimeter
    
    float offset = (sin(time)+1.)*.25; // animate outline offset
    if (abs(d-offset) < 0.01) return vec3(1.0); // draw white outline
  
    return color;
}

For the second eye, we could duplicate the first eye’s code, but instead let’s mirror it horizontally with pixel.x = abs(pixel.x). To rationalize this, consider that if the point (1, 0) is inside the circle, then it’s mirror (-1, 0) will also be inside the circle after pixel.x = abs(pixel.x), so both points will get colored.

float circle(vec2 pixel, float radius) { // fold
    return length(pixel) - radius;
}
   
vec3 color_for_pixel(vec2 pixel, float time) {
    pixel.x -= .3; // controls position
    pixel.x = abs(pixel.x); // mirror
    pixel.x -= .7; // controls spacing
    return vec3(circle(pixel, .5) > 0.0);
}

The way that order of operations works still hurts my head, but it helps to play with the code to get a feel for what’s going on.

Mirror the circles on both the x and the y axis

Here is the mirroring technique applied to Rick’s eyes:

float circle(vec2 pixel, float radius) { // fold
    return length(pixel) - radius;
}
float round_rect(vec2 p, vec2 size, vec4 radii) { // fold
    radii.xy = (p.x>0.0)?radii.xy : radii.zw;
    radii.x  = (p.y>0.0)?radii.x  : radii.y;
    vec2 q = abs(p)-size+radii.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radii.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); 

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
     // pupils
    vec2 pupil_pos = pixel;
    pupil_pos += vec2(.13, -.24); // position pupils on eyeballs
    pupil_pos.x = abs(pupil_pos.x); // mirror pupils
    pupil_pos.x -= .16; // pupil spacing
    if (star(pupil_pos, 0.019, 6., .9) < 0.007) {
        return vec3(.1);
    }
    
    // eyeballs
    // position/mirror/scale one liner
    vec2 eye_pos = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
    float dist = circle(eye_pos, .16);
    if (dist < 0.0) return vec3(dist < -0.013);
    
    // head
    { // fold
        dist = min(
            // head
            round_rect(
            pixel, 
            vec2(.36, 0.6385), 
            vec4(.34, .415, .363, .315)),
            
            // ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, .12), 
            vec4(.13, .1, .13, .13))
        );
            
        if (dist < -0.01) return vec3(0.838, 0.799, 0.760);
        if (dist < 0.0) return vec3(0); // outline
    }   
    
    return vec3(1);
}

Let’s skip ahead. The mouth, nose, and eyebrow are all created with bezier(). The hair is an 11-point star() that I stretched vertically.

float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float circle(vec2 p, float r) { // fold
    return length(p) - r;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float d;
  
   // eyes
   { // fold
        // pupils
        vec2 pupil_warp = pixel;
        pupil_warp.x = abs(pupil_warp.x +.13);
        pupil_warp -= vec2(.16,.24);

        d = star(pupil_warp, 0.019, 6., .9);
        if (d < 0.007) {
            return vec3(.1);
        }
        
        // eyeballs
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) {
            return vec3(step(0.013, -d));
        }
    }
    
    // nose  
    d = min( // combine the curves
            bezier(pixel, 
                vec2(-.15, -.13), 
                vec2(-.21,-.14), 
                vec2(-.14, .08)),
            bezier(pixel, 
                vec2(-.085, -.01), 
                vec2(-.12, -.13),
                vec2(-.15,-.13)));
    if (d < 0.0055) return vec3(0);
  
        
    // mouth
    d = bezier(pixel,  
                 vec2(-.26, -.28), 
                 vec2(-.05,-.42), 
                 vec2(.115, -.25));
    if (d < .12) {
        // The `*step(d, .11)` creates the outline.
        // it's the same as `*vec3(d < .11)` 
        // aka, it multiplies the color by zero for 
        // pixels near the perimeter  
        return vec3(.42, .147, .152)*step(d, .11);
    }
  
    // eyebrow
    d = bezier(pixel,  
                 vec2(-.34, .38), 
                 vec2(-.05, .68), 
                 vec2(.205, .36)) - 0.035;
    if (d < 0.0) 
        return vec3(.71, .839, .922)*step(d, -.013);

    // head
    {  // fold
        float dist = min(
            // head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (dist < -.01) return vec3(.838, .799, .76);
        if (dist < 0.) return vec3(0); // outline
    }
    
    // hair
    d = star((pixel-vec2(.08,.15))*vec2(1.3,1.), 0.95, 11., .28);
    if (d < 0.) {
        return vec3(0.682, 0.839, 0.929)*step(0.012, -d);
    }
    
    return vec3(1.);
}

That’s as far as basic shape positioning, scaling, and outlining can get us.

Making the Hair Wavy

The remaining steps will elevate our crude sketch of Rick into a drawing that looks exactly like him. We’ll learn a few more techniques to make this possible. First up: let’s fix his rigid looking hair. There isn’t a “wavy hair” signed distance function, but we can make the star shape more wavy using a technique called domain warping.

Domain warping randomly offsets pixel locations. That random offset is “seeded” by the pixel’s location, so the offset is consistent over time for any given location. You can use that warped location for whatever shapes you want warped. Here’s an 11-point star with and without warping:

float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

// these functions are used by the `warp()` function 
// to generate pseudo random numbers. The details aren't 
// super important. I looked these functions up:
// https://www.shadertoy.com/view/XdXGW8
vec2 grad(ivec2 z)  { // fold
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { // fold
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}

vec2 warp(vec2 p, float scale, float strength) {
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}

vec3 color_for_pixel(vec2 pixel, float time) {
    vec2 warped_pixel = warp(pixel, 4., .07);
    float d = min(
        star(warped_pixel + vec2(.8,0), 0.7, 11., .28),
        star(pixel - vec2(.8,0), 0.7, 11., .28)
        );
    if (d < 0.) {
        return vec3(0.682, 0.839, 0.929);
    }
    
    return vec3(1);
}

Visualize the warp offsets by drawing the x offset to the red channel and the y offset to the green channel

Fun fact: the Lord of the Rings movies used domain warping to create the visual effect seen when Frodo is wearing the Ring. Their warp offsets came from tracking fire movement.

Animate the warp effect above to achieve the Lord of the Rings effect.

Drawing Infinite Teeth

Rick needs teeth, a lot them. But we’ll start by drawing one. A parabola is the best tooth shape I could find:

float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float d = parabola(pixel, 38.);
    if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01);

    return vec3(1);
}

Yes that is a tooth. Stick with me.

Is there a way to draw multiple teeth without duplicating a bunch of code, or using a for loop? Yes! Similar to how we used abs() to mirror shapes, we can use mod() to repeat shapes. mod(a,b) calculates the reminder of a/b. Look below at what mod(pixel.x, 0.5) does. Every time pixel.x increases above a multiple of .5 , mod() starts back at zero (black) again.

vec3 color_for_pixel(vec2 pixel, float time) {
    return vec3(mod(pixel.x, 0.5));
}

Here is mod() applied to the single tooth

float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float width = .065;    
    pixel.x = mod(pixel.x, width)-width*.5; // NEW: repeat horizontally
    float d = parabola(pixel, 38.);
    if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01);

    return vec3(1);
}

Repeat the teeth in a circle instead of in a line to create a sandworm mouth

and we can mirror that to get the bottom teeth

float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float width = .065;
    pixel.y = abs(pixel.y)-.06; // NEW: mirror vertically 
    pixel.x = mod(pixel.x, width)-width*.5; // repeat horizontally
    float d = parabola(pixel, 38.);
    if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01);

    return vec3(1);
}

Then to make it a smile, we offset the y position of the tooth based on pixel.x.

float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float width = .065;
    pixel.y -= pow(pixel.x, 2.); // NEW: curve into a smile
    pixel.y = abs(pixel.y)-.06; // mirror vertically
    pixel.x = mod(pixel.x, width)-width*.5; // repeat horizontally
    float d = parabola(pixel, 38.);
    if (d < 0.) return vec3(0.902, 0.890, 0.729)*step(d, -.01);

    return vec3(1);
}

Kind of creepy. Reducing the infinite teeth down to 12 will make it a little less creepy — done by only drawing teeth when pixel.x is within the desired range

float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float width = .065;
    vec2 teeth = pixel;
    teeth.y -= pow(pixel.x, 2.);
    teeth.y = abs(teeth.y)-.06; 
    teeth.x = mod(teeth.x, width)-width*.5;
    float d = parabola(teeth, 38.);
    if (d < 0. 
        // Limit where the teeth are drawn
        && pixel.x < width*3.
        && pixel.x > -width*3.
    ) {
        return vec3(0.902, 0.890, 0.729)*step(d, -.01);
    }

    return vec3(1);
}

Here’s Rick with wavy hair and new set of teeth. I also added the tongue. Notice that the tongue and teeth only draw inside the mouth thanks to placing their code inside the if that checks the mouth distance.

float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  { // fold
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { // fold
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) { // fold
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float d;
  
    // Mouth
    d = bezier(pixel,  
                 vec2(-.26, -.28), 
                 vec2(-.05,-.42), 
                 vec2(.115, -.25));
    if (d < .11) {
        // only draw the teeth and tongue inside hte mouth shape
        
        // Teeth
        float width = .065;
        vec2 teeth = pixel;
        teeth.x = mod(teeth.x, width)-width*.5;
        teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34;
        teeth.y = abs(teeth.y)-.06;
        d = parabola(teeth, 38.);
        if (d < 0. && abs(pixel.x+.06) < .194) 
            return vec3(0.902, 0.890, 0.729)*step(d, -.01);
      
        // Tongue
        // Make the right side of the tongue thicker
        float tongue_thickness = map(pixel.x, -.16, .01, .02, .045);
        d = bezier(pixel,  
            vec2(-.16, -.35), 
            vec2(.001,-.33), 
            vec2(.01, -.5)) - tongue_thickness;
        if (d < 0.0) 
            return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
          
        // mouth fill color
        return vec3(.42, .147, .152); 
    } 
    if (d < .12) // mouth outline
        return vec3(0); 
  
    // Eyebrow, Eyes, Nose & Head
   { // fold
        // Pupils
        vec2 pupil_warp = pixel;
        pupil_warp.x = abs(pupil_warp.x +.13);
        pupil_warp -= vec2(.16,.24);

        d = star(pupil_warp, 0.019, 6., .9);
        if (d < 0.007) {
            return vec3(.1);
        }
        
        // Eyeballs
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) {
            return vec3(step(0.013, -d));
        }
    
        // Nose  
        d = min(
                bezier(pixel, 
                    vec2(-.15, -.13), 
                    vec2(-.21,-.14), 
                    vec2(-.14, .08)),
                bezier(pixel, 
                    vec2(-.085, -.01), 
                    vec2(-.12, -.13),
                    vec2(-.15,-.13)));
        if (d < 0.0055) return vec3(0);

        // Eyebrow
        d = bezier(pixel,  
                vec2(-.34, .38), 
                vec2(-.05, .68), 
                vec2(.205, .36)) - 0.035;
        if (d < 0.0) 
            return vec3(.71, .839, .922)*step(d, -.013);
  
        d = min(
            // Head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01);
    }
    
    // Hair
    vec2 hair = pixel;
    hair -= vec2(.08,.15);
    hair.x *= 1.3;
    hair = warp(hair, 4.0, 0.07);
    d = star(hair, 0.95, 11., .28);
    if (d < 0.) {
        return vec3(0.682, 0.839, 0.929)*step(0.012, -d);
    }
    
    return vec3(1.);
}

Artistic Lines

The final bits needed are the curves below the eyes and around the mouth. Those lines are just like our normal shape outlines, except they’re offset away from the perimeter of the shape. This can be done by subtracting a little from distance when drawing the outline. In other words this:
if (abs(distance_to_shape) < thickness) return vec3(0);
becomes this:
if (abs(distance_to_shape - outset) < thickness) return vec3(0);
The blue line below illustrates that technique.

Since Rick’s under-eye lines should only be visible…under the eye, we’ll need to limit where they are drawn. That can be done using whatever logic you can think of, as shown by the green line:

float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}

vec3 color_for_pixel(vec2 pixel, float time) {
    float dist = round_rect(pixel, vec2(.5), vec4(.1));
    float thickness = .02;
    
    // outline
    if (abs(dist) < thickness) 
        return vec3(0); 
    
    // outset outline
    if (abs(dist-.2) < thickness) 
        return vec3(.1,.1,1); 
    
    // limited outline
    if (abs(dist-.4) < thickness && pixel.y < -.4) 
        return vec3(.1,.9,.1); 
    
    // fill
    if (dist < 0.) return vec3(1); 
    
    return vec3(.92);    
}

Here are those techniques applied to Rick:

float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  { // fold
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { // fold
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) { // fold
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

vec3 color_for_pixel(vec2 pixel, float time) {  
    // Mouth
    float d = bezier(pixel,  
                 vec2(-.26, -.28), 
                 vec2(-.05,-.42), 
                 vec2(.115, -.25));
    if (d < .11) { // fold
        // Teeth
        float width = .065;
        vec2 teeth = pixel;
        teeth.x = mod(teeth.x, width)-width*.5;
        teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34;
        teeth.y = abs(teeth.y)-.06;
        d = parabola(teeth, 38.);
        if (d < 0. && abs(pixel.x+.06) < .194) 
            return vec3(0.902, 0.890, 0.729)*step(d, -.01);
      
        // Tongue
        // Make the right side of the tongue thicker
        float tongue_thickness = map(pixel.x, -.16, .01, .02, .045);
        d = bezier(pixel,  
            vec2(-.16, -.35), 
            vec2(.001,-.33), 
            vec2(.01, -.5)) - tongue_thickness;
        if (d < 0.0) 
            return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
          
        // mouth fill color
        return vec3(.42, .147, .152); 
    } 
    
    // lip outlines
    if (d < .12 || (abs(d-.16) < .005 
                    && (pixel.x*-6.4 > -pixel.y+1.6 
                      || pixel.x*1.7 > -pixel.y+.1 
                      || pixel.y < -0.49))) 
        return vec3(0); 
    
    // lips
    if (d < .16) return vec3(.838, .799, 0.76);
   
    // Pupils
    { // fold
        vec2 pupil_warp = pixel;
        pupil_warp.x = abs(pupil_warp.x +.13);
        pupil_warp -= vec2(.16,.24);

        d = star(pupil_warp, 0.019, 6., .9);
        if (d < 0.007) {
            return vec3(.1);
        }
    }
        
    // Eyeballs
    vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
    d = length(eye) - .16;
    if (d < 0.) return vec3(step(.013, -d));
    
    // under eye lines
    bool should_show = pixel.y < 0.25 && 
        (abs(pixel.x+.29) < .05 || 
        abs(pixel.x-.12) < .085);
    if (abs(d - .04) < .0055 && should_show) return vec3(0);
    
    // Nose, Eyebrow, Head, Hair
    { // fold
        // Nose  
        d = min(
                bezier(pixel, 
                    vec2(-.15, -.13), 
                    vec2(-.21,-.14), 
                    vec2(-.14, .08)),
                bezier(pixel, 
                    vec2(-.085, -.01), 
                    vec2(-.12, -.13),
                    vec2(-.15,-.13)));
        if (d < 0.0055) return vec3(0);

        // Eyebrow
        d = bezier(pixel,  
                vec2(-.34, .38), 
                vec2(-.05, .68), 
                vec2(.205, .36)) - 0.035;
        if (d < 0.0) 
            return vec3(.71, .839, .922)*step(d, -.013);
  
        d = min(
            // Head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01);
    
        // Hair
        vec2 hair = pixel;
        hair -= vec2(.08,.15);
        hair.x *= 1.3;
        hair = warp(hair, 4.0, 0.07);
        d = star(hair, 0.95, 11., .28);
        if (d < 0.) {
            return vec3(0.682, 0.839, 0.929)*step(0.012, -d);
        }
    }
    
    return vec3(1);
}

Draw another character from Rick and Morty, or whatever your favorite cartoon is.

Use raymarching with 3D signed distance fields to draw a 3D version of Rick. Let me know if you do this, I want to see.

Animation

With our drawing complete, there are several animation techniques we can use to introduce movement. First up:

1. Looping Values

The easiest way to add animation is to slap a sin(time) into the code somewhere. The sin is important because it wraps the ever-increasing time value into the range of -1 to 1, which makes nice looping animations. You will often change that range with a scale and offset like so: sin(time)*.5 + .5. The head angle, tongue angle, and eyebrow height are animated in this way. I added a rotateAt function to do the rotation math.

vec2 rotateAt(vec2 p, float angle, vec2 origin) {
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}

float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  { // fold
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { // fold
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) { // fold
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

vec3 color_for_pixel(vec2 pixel, float time) { 
    
    // NEW: rotate the whole drawing
    pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
    pixel.y += .1;
    
    // Mouth, eyes, nose
    { // fold
        // Mouth
        float d = bezier(pixel,  
                     vec2(-.26, -.28), 
                     vec2(-.05,-.42), 
                     vec2(.115, -.25));
        if (d < .11) {
            // Teeth
            float width = .065;
            vec2 teeth = pixel;
            teeth.x = mod(teeth.x, width)-width*.5;
            teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34;
            teeth.y = abs(teeth.y)-.06;
            d = parabola(teeth, 38.);
            if (d < 0. && abs(pixel.x+.06) < .194) 
                return vec3(0.902, 0.890, 0.729)*step(d, -.01);
          
            // Tongue
            vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
            float tongue_thickness = map(tongue.x, -.16, .01, .02, .045);
            d = bezier(tongue,  
                vec2(-.16, -.35), 
                vec2(.001,-.33), 
                vec2(.01, -.5)) - tongue_thickness;
            if (d < 0.0) 
                return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
              
            // mouth fill color
            return vec3(.42, .147, .152); 
        } 
        
        // lip outlines
        if (d < .12 || (abs(d-.16) < .005 
                        && (pixel.x*-6.4 > -pixel.y+1.6 
                          || pixel.x*1.7 > -pixel.y+.1 
                          || pixel.y < -0.49))) 
            return vec3(0); 
        
        // lips
        if (d < .16) return vec3(.838, .799, 0.76);
       
        // Pupils
        vec2 pupil_warp = pixel;
        pupil_warp.x = abs(pupil_warp.x +.13);
        pupil_warp -= vec2(.16,.24);

        d = star(pupil_warp, 0.019, 6., .9);
        if (d < 0.007) {
            return vec3(.1);
        }
            
        // Eyeballs
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) return vec3(step(.013, -d));
        
        // under eye lines
        bool should_show = pixel.y < 0.25 && 
            (abs(pixel.x+.29) < .05 || 
            abs(pixel.x-.12) < .085);
        if (abs(d - .04) < .0055 && should_show) return vec3(0);
    
        // Nose  
        d = min(
                bezier(pixel, 
                    vec2(-.15, -.13), 
                    vec2(-.21,-.14), 
                    vec2(-.14, .08)),
                bezier(pixel, 
                    vec2(-.085, -.01), 
                    vec2(-.12, -.13),
                    vec2(-.15,-.13)));
        if (d < 0.0055) return vec3(0);
    }

    // Eyebrow
    float d = bezier(pixel,  
            vec2(-.34, .38), 
            // NEW: animate the middle up and down
            vec2(-.05, 0.5 + cos(time)*.1),
            vec2(.205, .36)) - 0.035;
    if (d < 0.0) 
        return vec3(.71, .839, .922)*step(d, -.013);
    
    // Head and hair
    { // fold
        d = min(
            // Head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01);
    
        // Hair
        vec2 hair = pixel;
        hair -= vec2(.08,.15);
        hair.x *= 1.3;
        hair = warp(hair, 4.0, 0.07);
        d = star(hair, 0.95, 11., .28);
        if (d < 0.) {
            return vec3(0.682, 0.839, 0.929)*step(0.012, -d);
        }
    }
    
    return vec3(1.);
}

Animate Rick’s head as if he is walking left and right. Flip the face direction when he is moving to the right (this is easier than it sounds!).

2. Switching What’s Drawn

Animating a property with sin() just moves stuff around, but you can also draw something totally different based on time. We’ll do that to make Rick blink.

vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  { // fold
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { // fold
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) { // fold
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

vec3 color_for_pixel(vec2 pixel, float time) {     
    { // fold
        // rotate the whole drawing
        pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
        pixel.y += .1;
    }

    // blink for .09 seconds, every 2 seconds
    if (mod(time, 2.) < .09) { // closed eyes
        float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
        if (d < .008) return vec3(0);      
    } 
    else // open eyes
    { // fold
        // Pupils
        vec2 pupil_warp = pixel;
        pupil_warp.x = abs(pupil_warp.x +.13);
        pupil_warp -= vec2(.16,.24);

        float d = star(pupil_warp, 0.019, 6., .9);
        if (d < 0.007) {
            return vec3(.1);
        }
            
        // Eyeballs
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) return vec3(step(.013, -d));
        
        // under eye lines
        bool should_show = pixel.y < 0.25 && 
            (abs(pixel.x+.29) < .05 || 
            abs(pixel.x-.12) < .085);
        if (abs(d - .04) < .0055 && should_show) return vec3(0);
    }
  
    // Rest of face
    { // fold
        // Mouth
        float d = bezier(pixel,  
                     vec2(-.26, -.28), 
                     vec2(-.05,-.42), 
                     vec2(.115, -.25));
        if (d < .11) {
            // Teeth
            float width = .065;
            vec2 teeth = pixel;
            teeth.x = mod(teeth.x, width)-width*.5;
            teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34;
            teeth.y = abs(teeth.y)-.06;
            d = parabola(teeth, 38.);
            if (d < 0. && abs(pixel.x+.06) < .194) 
                return vec3(0.902, 0.890, 0.729)*step(d, -.01);
          
            // Tongue
            // `map()` is used to change the thickness of 
            // the tongue along the x axis
            vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
            float tongue_thickness = map(tongue.x, -.16, .01, .02, .045);
            d = bezier(tongue,  
                vec2(-.16, -.35), 
                vec2(.001,-.33), 
                vec2(.01, -.5)) - tongue_thickness;
            if (d < 0.0) 
                return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
              
            // mouth fill color
            return vec3(.42, .147, .152); 
        } 
        
        // lip outlines
        if (d < .12 || (abs(d-.16) < .005 
                        && (pixel.x*-6.4 > -pixel.y+1.6 
                          || pixel.x*1.7 > -pixel.y+.1 
                          || pixel.y < -0.49))) 
            return vec3(0); 
        
        // lips
        if (d < .16) return vec3(.838, .799, 0.76);
       
        
    
        // Nose  
        d = min(
                bezier(pixel, 
                    vec2(-.15, -.13), 
                    vec2(-.21,-.14), 
                    vec2(-.14, .08)),
                bezier(pixel, 
                    vec2(-.085, -.01), 
                    vec2(-.12, -.13),
                    vec2(-.15,-.13)));
        if (d < 0.0055) return vec3(0);
    

    // Eyebrow
    d = bezier(pixel,  
            vec2(-.34, .38), 
            // NEW animate the middle up and down
            vec2(-.05, 0.5 + cos(time)*.1),
            vec2(.205, .36)) - 0.035;
    if (d < 0.0) 
        return vec3(.71, .839, .922)*step(d, -.013);
    
        d = min(
            // Head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01);
    
        // Hair
        vec2 hair = pixel;
        hair -= vec2(.08,.15);
        hair.x *= 1.3;
        hair = warp(hair, 4.0, 0.07);
        d = star(hair, 0.95, 11., .28);
        if (d < 0.) {
            return vec3(0.682, 0.839, 0.929)*step(0.012, -d);
        }
    }
    
    return vec3(1);
}

Use this technique to animate Rick’s mouth so it looks like he is talking.

3. Noisy Movement

If sin is too smooth for you, try using noise! I used noise() to make the eyes randomly look around. Since I don’t want the eyes to be continuously moving, I rounded the time value before passing it to noise().

vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  { // fold
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { // fold
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) { // fold
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

vec3 color_for_pixel(vec2 pixel, float time) { 
    { // fold
        // rotate the whole drawing
        pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
        pixel.y += .1;
    }
    
    // Blink eyes
    if (mod(time, 2.) < .09) { // fold
        // closed eyes
        float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
        if (d < .008) return vec3(0);      
    } 
    else { 
        // move pupils randomly
        vec2 pupil_warp = pixel + vec2(.095,-.18);
        pupil_warp.x -= noise(vec2(round(time)*7.+.5, 0.5))*.1;
        pupil_warp.y -= noise(vec2(round(time)*9.+.5, 0.5))*.1;
        pupil_warp.x = abs(pupil_warp.x) - .16;
        float d = star(pupil_warp, 0.019, 6., .9);
        
        {// fold
            if (d < 0.007) {
                return vec3(.1);
            }
                
            // Eyeballs
            vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
            d = length(eye) - .16;
            if (d < 0.) return vec3(step(.013, -d));
            
            // under eye lines
            bool should_show = pixel.y < 0.25 && 
                (abs(pixel.x+.29) < .05 || 
                abs(pixel.x-.12) < .085);
            if (abs(d - .04) < .0055 && should_show) return vec3(0);
        }
    }
  
    // Rest of face
    { // fold
        // Mouth
        float d = bezier(pixel,  
                     vec2(-.26, -.28), 
                     vec2(-.05,-.42), 
                     vec2(.115, -.25));
        if (d < .11) {
            // Teeth
            float width = .065;
            vec2 teeth = pixel;
            teeth.x = mod(teeth.x, width)-width*.5;
            teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34;
            teeth.y = abs(teeth.y)-.06;
            d = parabola(teeth, 38.);
            if (d < 0. && abs(pixel.x+.06) < .194) 
                return vec3(0.902, 0.890, 0.729)*step(d, -.01);
          
            // Tongue
            // `map()` is used to change the thickness of 
            // the tongue along the x axis
            vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
            float tongue_thickness = map(tongue.x, -.16, .01, .02, .045);
            d = bezier(tongue,  
                vec2(-.16, -.35), 
                vec2(.001,-.33), 
                vec2(.01, -.5)) - tongue_thickness;
            if (d < 0.0) 
                return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
              
            // mouth fill color
            return vec3(.42, .147, .152); 
        } 
        
        // lip outlines
        if (d < .12 || (abs(d-.16) < .005 
                        && (pixel.x*-6.4 > -pixel.y+1.6 
                          || pixel.x*1.7 > -pixel.y+.1 
                          || pixel.y < -0.49))) 
            return vec3(0); 
        
        // lips
        if (d < .16) return vec3(.838, .799, 0.76);
       
        
    
        // Nose  
        d = min(
                bezier(pixel, 
                    vec2(-.15, -.13), 
                    vec2(-.21,-.14), 
                    vec2(-.14, .08)),
                bezier(pixel, 
                    vec2(-.085, -.01), 
                    vec2(-.12, -.13),
                    vec2(-.15,-.13)));
        if (d < 0.0055) return vec3(0);
    

    // Eyebrow
    d = bezier(pixel,  
            vec2(-.34, .38), 
            // NEW animate the middle up and down
            vec2(-.05, 0.5 + cos(time)*.1),
            vec2(.205, .36)) - 0.035;
    if (d < 0.0) 
        return vec3(.71, .839, .922)*step(d, -.013);
    
        d = min(
            // Head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01);
    
        // Hair
        vec2 hair = pixel;
        hair -= vec2(.08,.15);
        hair.x *= 1.3;
        hair = warp(hair, 4.0, 0.07);
        d = star(hair, 0.95, 11., .28);
        if (d < 0.) {
            return vec3(0.682, 0.839, 0.929)*step(0.012, -d);
        }
    }
    
    return vec3(1);
}

Make the pupil movement more realistic instead of jumping between positions

Bonus: Warping Time

Our final animation technique is “time domain warping” to make the hair bend as the head tilts. It’s like domain warping, except instead of offsetting space we offset time. Basically we delay time more the closer to the hair tip a pixel is. Because that delay isn’t constant along the length of the hair, the hair will bend instead of rotate rigidly.


vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  { // fold
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { // fold
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) { // fold
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}

vec3 color_for_pixel(vec2 pixel, float time) { 
    { // fold
        // rotate the whole drawing
        pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
        pixel.y += .1;
    
    
    // Blink eyes
    if (mod(time, 2.) < .09) {
        // closed eyes
        float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
        if (d < .008) return vec3(0);      
    } 
    else { 
        // move pupils randomly
        vec2 pupil_warp = pixel + vec2(.095,-.18);
        pupil_warp.x -= noise(vec2(round(time)*7.+.5, 0.5))*.1;
        pupil_warp.y -= noise(vec2(round(time)*9.+.5, 0.5))*.1;
        pupil_warp.x = abs(pupil_warp.x) - .16;
        float d = star(pupil_warp, 0.019, 6., .9);
        if (d < 0.007) {
            return vec3(.1);
        }
            
        // Eyeballs
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) return vec3(step(.013, -d));
        
        // under eye lines
        bool should_show = pixel.y < 0.25 && 
            (abs(pixel.x+.29) < .05 || 
            abs(pixel.x-.12) < .085);
        if (abs(d - .04) < .0055 && should_show) return vec3(0);
    }
  
    
        // Mouth
        float d = bezier(pixel,  
                     vec2(-.26, -.28), 
                     vec2(-.05,-.42), 
                     vec2(.115, -.25));
        if (d < .11) {
            // Teeth
            float width = .065;
            vec2 teeth = pixel;
            teeth.x = mod(teeth.x, width)-width*.5;
            teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34;
            teeth.y = abs(teeth.y)-.06;
            d = parabola(teeth, 38.);
            if (d < 0. && abs(pixel.x+.06) < .194) 
                return vec3(0.902, 0.890, 0.729)*step(d, -.01);
          
            // Tongue
            // `map()` is used to change the thickness of 
            // the tongue along the x axis
            vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
            float tongue_thickness = map(tongue.x, -.16, .01, .02, .045);
            d = bezier(tongue,  
                vec2(-.16, -.35), 
                vec2(.001,-.33), 
                vec2(.01, -.5)) - tongue_thickness;
            if (d < 0.0) 
                return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
              
            // mouth fill color
            return vec3(.42, .147, .152); 
        } 
        
        // lip outlines
        if (d < .12 || (abs(d-.16) < .005 
                        && (pixel.x*-6.4 > -pixel.y+1.6 
                          || pixel.x*1.7 > -pixel.y+.1 
                          || pixel.y < -0.49))) 
            return vec3(0); 
        
        // lips
        if (d < .16) return vec3(.838, .799, 0.76);
       
        
    
        // Nose  
        d = min(
                bezier(pixel, 
                    vec2(-.15, -.13), 
                    vec2(-.21,-.14), 
                    vec2(-.14, .08)),
                bezier(pixel, 
                    vec2(-.085, -.01), 
                    vec2(-.12, -.13),
                    vec2(-.15,-.13)));
        if (d < 0.0055) return vec3(0);
    

    // Eyebrow
    d = bezier(pixel,  
            vec2(-.34, .38), 
            // NEW animate the middle up and down
            vec2(-.05, 0.5 + cos(time)*.1),
            vec2(.205, .36)) - 0.035;
    if (d < 0.0) 
        return vec3(.71, .839, .922)*step(d, -.013);
    
        d = min(
            // Head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01);
    }
    
    // Hair     
    float twist = sin(time*2.-length(pixel)*2.1)*.12;
    vec2 hair = rotateAt(pixel, twist, vec2(0.,.1));
    hair -= vec2(.08,.15);
    hair.x *= 1.3;
    hair = warp(hair, 4.0, 0.07);
    float d = star(hair, 0.95, 11., .28);
    if (d < 0.) {
        return vec3(0.682, 0.839, 0.929)*step(d, -0.012);
    }
    
    return vec3(1);
}

Apply this trick to to other parts of Rick’s face for a rubbery and ricklaxed look.

Wrapping up

After we add a portal effect2 our animation is complete.


vec2 rotateAt(vec2 p, float angle, vec2 origin) { // fold
    float s = sin(angle), c = cos(angle);
    return (p-origin)*mat2( c, -s, s, c ) + origin;
}
float map(float value, float inMin, float inMax, float outMin, float outMax) { // fold
  value = clamp(value, inMin, inMax);
  return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
vec2 grad(ivec2 z)  { // fold
    int n = z.x+z.y*11111;
    n = (n<<13)^n;
    n = (n*(n*n*15731+789221)+1376312589)>>16;
    n &= 7;
    vec2 gr = vec2(n&1,n>>1)*2.0-1.0;
    return ( n>=6 ) ? vec2(0.0,gr.x) : 
           ( n>=4 ) ? vec2(gr.x,0.0) :
                              gr;                            
}
float noise(vec2 p) { // fold
    ivec2 i = ivec2(floor(p));
    vec2  f =       fract(p);
    vec2 u = f*f*(3.0-2.0*f); 
    return mix( mix( dot( grad( i+ivec2(0,0) ), f-vec2(0.0,0.0) ), 
                     dot( grad( i+ivec2(1,0) ), f-vec2(1.0,0.0) ), u.x),
                mix( dot( grad( i+ivec2(0,1) ), f-vec2(0.0,1.0) ), 
                     dot( grad( i+ivec2(1,1) ), f-vec2(1.0,1.0) ), u.x), u.y);
}
vec2 warp(vec2 p, float scale, float strength) { // fold
    float offsetX = noise(p * scale + vec2(0.0, 100.0));
    float offsetY = noise(p * scale + vec2(100.0, 0.0));
    return p + vec2(offsetX, offsetY) * strength;
}
float bezier(vec2 p, vec2 v0, vec2 v1, vec2 v2) { // fold
    vec2 i = v0 - v2;
    vec2 j = v2 - v1;
    vec2 k = v1 - v0;
    vec2 w = j-k;

    v0-= p; v1-= p; v2-= p;
    
    float x = v0.x*v2.y-v0.y*v2.x;
    float y = v1.x*v0.y-v1.y*v0.x;
    float z = v2.x*v1.y-v2.y*v1.x;

    vec2 s = 2.0*(y*j+z*k)-x*i;

    float r =  (y*z-x*x*0.25)/dot(s,s);
    float t = clamp( (0.5*x+y+r*dot(s,w))/(x+y+z),0.0,1.0);
    
    vec2 d = v0+t*(k+k+t*w);
    vec2 outQ = d + p;
    return length(d);
}
float parabola(vec2 pos, float k) { // fold
    // from https://www.shadertoy.com/view/ws3GD7
    pos.x = abs(pos.x);
    float ik = 1.0/k;
    float p = ik*(pos.y - 0.5*ik)/3.0;
    float q = 0.25*ik*ik*pos.x;
    float h = q*q - p*p*p;
    float r = sqrt(abs(h));
    float x = (h>0.0) ? 
        pow(q+r,1.0/3.0) - pow(abs(q-r),1.0/3.0)*sign(r-q) :
        2.0*cos(atan(r,q)/3.0)*sqrt(p);
    return length(pos-vec2(x,k*x*x)) * sign(pos.x-x);
}
float round_rect(vec2 p, vec2 b, vec4 r) { // fold
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;
    vec2 q = abs(p)-b+r.x;
    return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x;
}
float star(vec2 p, float r, float points, float ratio) { // fold
    // next 4 lines can be precomputed for a given shape
    float an = 3.141593/points;
    float en = 3.141593/(ratio*(points-2.) + 2.); 
    vec2  acs = vec2(cos(an),sin(an));
    vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon

    float bn = mod(atan(p.x,p.y),2.0*an) - an;
    p = length(p)*vec2(cos(bn),abs(sin(bn)));
    p -= r*acs;
    p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
    return length(p)*sign(p.x);
}
#define H(i,j) fract(sin(dot(ceil(P+vec2(i,j)), resolution.xy )) * 4e3)
float N( vec2 P) { // fold
    float s,i,w = .5;
    for (; i < 3. ; i++, w *= .4, P *= 1.9 ) {
        vec2 F = fract( P *= mat2(.866,-.5,.5,.866) ); 
        F *= F*(3.-F-F);
        s += w* mix( mix(H(0,0) , H(1,0), F.x),
                     mix(H(0,1) , H(1,1), F.x),
                     F.y );
    }
    return s;
}
vec3 portal(vec2 pixel, float time) { // fold
    // from https://www.shadertoy.com/view/l3f3zM
    float l = length( pixel ), 
          a = atan(pixel.y, pixel.x) / 6.28 + .5,
          k = 10.;
     
    a = fract(a + l*.3 - time*.01 );
    vec2 U = vec2( l+time*.3, a );
     
    return vec3[]( vec3(.18, .53, .09),
                    vec3(.56, .89, .16),
                    vec3(.35, .84, .11),
                    vec3(.92, .98, .85)
                  ) [ int( 4.* pow( mix( N(U*k), N(U*k-vec2(0,k)), U.y) * 1.5, 2.5))];
}

vec3 color_for_pixel(vec2 pixel, float time) { 
    { // fold
        // rotate the whole drawing
        pixel = rotateAt(pixel, sin(time*2.)*.1, vec2(0,-.6));
        pixel.y += .1;
    
    
    // Blink eyes
    if (mod(time, 2.) < .09) {
        // closed eyes
        float d = round_rect(pixel+vec2(.07,-.16), vec2(.24,0), vec4(0));
        if (d < .008) return vec3(0);      
    } 
    else { 
        // move pupils randomly
        vec2 pupil_warp = pixel + vec2(.095,-.18);
        pupil_warp.x -= noise(vec2(round(time)*7.+.5, 0.5))*.1;
        pupil_warp.y -= noise(vec2(round(time)*9.+.5, 0.5))*.1;
        pupil_warp.x = abs(pupil_warp.x) - .16;
        float d = star(pupil_warp, 0.019, 6., .9);
        if (d < 0.007) {
            return vec3(.1);
        }
            
        // Eyeballs
        vec2 eye = vec2(abs(pixel.x+.1)-.17, pixel.y*.93 - .16);
        d = length(eye) - .16;
        if (d < 0.) return vec3(step(.013, -d));
        
        // under eye lines
        bool should_show = pixel.y < 0.25 && 
            (abs(pixel.x+.29) < .05 || 
            abs(pixel.x-.12) < .085);
        if (abs(d - .04) < .0055 && should_show) return vec3(0);
    }
  
    
        // Mouth
        float d = bezier(pixel,  
                     vec2(-.26, -.28), 
                     vec2(-.05,-.42), 
                     vec2(.115, -.25));
        if (d < .11) {
            // Teeth
            float width = .065;
            vec2 teeth = pixel;
            teeth.x = mod(teeth.x, width)-width*.5;
            teeth.y -= pow(pixel.x+.09, 2.) * 1.5 - .34;
            teeth.y = abs(teeth.y)-.06;
            d = parabola(teeth, 38.);
            if (d < 0. && abs(pixel.x+.06) < .194) 
                return vec3(0.902, 0.890, 0.729)*step(d, -.01);
          
            // Tongue
            // `map()` is used to change the thickness of 
            // the tongue along the x axis
            vec2 tongue = rotateAt(pixel, sin(time*2.-1.5)*.15+.1, vec2(0,-.5));
            float tongue_thickness = map(tongue.x, -.16, .01, .02, .045);
            d = bezier(tongue,  
                vec2(-.16, -.35), 
                vec2(.001,-.33), 
                vec2(.01, -.5)) - tongue_thickness;
            if (d < 0.0) 
                return vec3(0.816, 0.302, 0.275)*step(d, -0.01);
              
            // mouth fill color
            return vec3(.42, .147, .152); 
        } 
        
        // lip outlines
        if (d < .12 || (abs(d-.16) < .005 
                        && (pixel.x*-6.4 > -pixel.y+1.6 
                          || pixel.x*1.7 > -pixel.y+.1 
                          || pixel.y < -0.49))) 
            return vec3(0); 
        
        // lips
        if (d < .16) return vec3(.838, .799, 0.76);
       
        
    
        // Nose  
        d = min(
                bezier(pixel, 
                    vec2(-.15, -.13), 
                    vec2(-.21,-.14), 
                    vec2(-.14, .08)),
                bezier(pixel, 
                    vec2(-.085, -.01), 
                    vec2(-.12, -.13),
                    vec2(-.15,-.13)));
        if (d < 0.0055) return vec3(0);
    

    // Eyebrow
    d = bezier(pixel,  
            vec2(-.34, .38), 
            // NEW animate the middle up and down
            vec2(-.05, 0.5 + cos(time)*.1),
            vec2(.205, .36)) - 0.035;
    if (d < 0.0) 
        return vec3(.71, .839, .922)*step(d, -.013);
    
        d = min(
            // Head
            round_rect(
            pixel, 
            vec2(.36, .6385), 
            vec4(.34, .415, .363, .315)),
            
            // Ear
            round_rect(
            pixel + vec2(-.32, .15), 
            vec2(.15, 0.12), 
            vec4(.13,.1,.13,.13))
        );
            
        if (d < 0.) return vec3(.838, .799, .76)*step(d, -.01);
    
    
        // Hair     
        float twist = sin(time*2.-length(pixel)*2.1)*.12;
        vec2 hair = rotateAt(pixel, twist, vec2(0.,.1));
        hair -= vec2(.08,.15);
        hair.x *= 1.3;
        hair = warp(hair, 4.0, 0.07);
        d = star(hair, 0.95, 11., .28);
        if (d < 0.) {
            return vec3(0.682, 0.839, 0.929)*step(d, -0.012);
        }
    }
    
    return portal(pixel, time);
}

I prioritized reability over performance for this code - see how much faster you can make it run.

That’s everything I know about making 2D animations using shaders. I hope it’s useful. Maybe next time we’ll talk about 3D, or some totally different topic! If you’d like to be notified about my next post, please join my newsletter.

Join my newsletter lol

While I love teaching and making posts like this, they are very time consuming to make — this one took about two weeks of work spread over 8 months. So if you’d like to see more work like this, please consider supporting me.

Appendix 1: Creating a Video

When you’re done with an animation you’ll probably want to turn it into a video. The editor we’ve been using on this page can not yet do that, but I’m working on it. Join my newsletter to be notified when I add video export!

In the meantime, you can use a script with glslviewer and ffmpeg. Below is my macOS workflow, on Windows and Linux you’ll have to figure out what your platform’s equivalent is.

  1. Install the dependencies.
brew install glslviewer ffmpeg # brew is macos only
  1. Write your shader.frag file

  2. And then put this in a bash file and run to export your video

#!/bin/bash

set -e
set -o pipefail

if [ -z "$1" ]; then
  echo "Usage: $0 <shader_file>"
  exit 1
fi

ORIGINAL_DIR=$(pwd)

TMP_DIR=$(mktemp -d)
if [ ! -d "$TMP_DIR" ]; then
  echo "Failed to create temporary directory."
  exit 1
fi
cd "$TMP_DIR"

glslViewer "$ORIGINAL_DIR/$1" -w 1920 -h 1080 --headless -e sequence,0,7,60 -e q
ffmpeg -framerate 60 -y -i %05d.png -c:v libx264 -pix_fmt yuv420p animation.mp4
mv animation.mp4 "$ORIGINAL_DIR/"

cd "$ORIGINAL_DIR"
rm -rf "$TMP_DIR"

And if you want to live code locally, use this:

glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0

Appendix 2: Super Sampling

You may have noticed that the edges of shapes in the examples on this page are smooth. I did a bit of work behind the scenes make that happen. I use a technique called super sampling where I call color_for_pixel() for 9 locations within each screen pixel and then display the average. The left side of this example shows what it looks like with super sampling disabled. You may need to zoom in on the page to see the difference.

#version 300 es 
// The above line switches the editor to "pro" mode 
// and removes automatic super sampling

precision highp float;
uniform float time;
uniform vec2 resolution;
out vec4 outColor;

vec3 color_for_pixel(vec2 p, float time) {   
    return vec3(length(mod(p+time*.05, .5) - .25) > 0.2);
}

void main() {
    float zone = gl_FragCoord.x - resolution.x*.5;
    if (abs(zone) < 1.5) {
      // vertical line
      outColor = vec4(1, 0, 0, 1);
    } else if (zone < 0.) {
      // left side: no super sampling
      vec2 st = (2.0*(gl_FragCoord.xy)-resolution)/resolution.y;
      outColor = vec4(color_for_pixel(st, time), 1);
    } else {
      // right side: super sampling
      int sample_count = 3;     
      vec3 sum = vec3(0);
      for( int m=0; m<sample_count; m++ ) {
          for( int n=0; n<sample_count; n++ ) {
              vec2 o = (vec2(m,n) + 0.5) / float(sample_count);
              vec2 st = (2.0*(gl_FragCoord.xy+o)-resolution)/resolution.y;
              sum += color_for_pixel(st, time);
          }
      }

      outColor = vec4(sum / float(sample_count*sample_count), 1); 
    }
}

Thanks to Carrie, Stan, Amin, and Martin for providing feedback on early drafts.


  1. This document has a list of GLSL’s built in functions on the last page. ↩︎

  2. The portal effect was made by ShaderToy user valena and shortened by FabriceNeyret2↩︎

Get notified about my next post.