Business Card Code

June 7, 2023

This is an entry for @sableRaph's weekly creative coding challenge. The theme for this week was "cards." I've always wanted to make a business card like the business card raytracer where the code to generate the card fits on the card itself.

This is the code:

W=175;H=100;F=255;n=[11492,0,-1150,3410,-1062,602,-318,83,134,-74,260,176,-111,
-115,-244,-84,94,111,-115,-7,-18,44,49,41,-78,-15,43,-10,-46,0,-57,22,30,-10,-67,
25,1,2,-4,16,-22,-1,-13,-6,-17,4,-27,2,-9,1,-30,6,-12,11,-6,-3,-23,0,-13,1,-11,
-2,-27,-3,7896,0,-1119,-10,-312,-969,-392,-691,-324,371,-87,287,38,-110,55,-67,
-88,101,76,-29,-45,-23,-33,11,8,16,6,9,9,16,-29,-37,65,70,-42,-68,39,34,-25,-5,
23,8,5,-3,5,8,11,1,10,6,7,-15,2,21,16,-14,0,9,10,-5,8,7,6,-4];m=[9013,0,-391,340,
-75,-53,-83,-169,98,-96,-81,62,22,-27,23,-10,-13,0,24,11,3,-20,2,7,15,-7,1,0,14,
0,10,-1,1094,0,-115,-636,172,-317,-66,-151,-28,60,19,57,57,-27,12,5,44,18,-10,
-37,27,4,8,5,16,-1,17,0,17,-3,21,-4];d=document;e=d.createElement('canvas');d
.body.appendChild(e);e.width=W;e.height=H;c=e.getContext('2d');with(Math){a=(a,c
)=>{l=a.length/2;x = a.slice(0,l);y = a.slice(l);f=(t,v)=>{b=v[0]/l;for(s=2;s<l;s
+=2){b+=2/l*hypot(v[s], v[s+1])*cos(PI/l*s*t-atan2(v[s+1],v[s]))};return b};g=[];
for(i=0;i<c;i++){t=(i/c*0.67+0.3)*l;g.push([f(t, x),f(t, y)])};return g};h=a(n,70
);j=a(m,35);k=([a,b],[c,d])=>[a-c,b-d];q=([a,b],k)=>[a*k,b*k];r=([a,b],[c,d])=>a*
c+b*d;u=(v,p)=>{d=r(k(p,v[0]),k(p,v[0]));for(s=1;s<v.length;s++){e=k(v[s-1],v[s])
;w=k(p,v[s]);b=k(w,q(e,min(max(r(w,e)/r(e,e),0),1)));d=min(d,r(b,b))};return sqrt
(d)};m=new ImageData(W,H);z=m.data;i=0;a=[[F,172,129],[F,146,139],[254,195,166],[
239,233,174],[205,234,192]];for(y=0;y<H;y++)for(x=0;x<W;x++){P=[x*2,y*2];d=min(u(h,
P),u(j,P));[R,G,B]=d<3?[F,F,F]:a[~~(d/10)%5];z[i++]=R;z[i++]=G;z[i++]=B;z[i++]=F};c
.putImageData(m,0,0)}
// davepagurek.com

The result looks like this:

You can see it live on OpenProcessing.

Here's an expanded, commented version of the code to see what it's actually doing. It uses a Fourier decomposition of the text to encode it as numbers, samples the Fourier curves into line segments, and then (very slowly on the CPU) renders a signed distance field of those segments:

// Some constants for width/height. Originally I was going to do
// 350x200, but doing an sdf without a shader is slow so I'm using
// half the side lengths and embracing the pixely look.
W=175;
H=100;

// The number 255 is used enough that it saves characters to assign it
// to a single-letter variable and reuse that.
F=255;

// The next two arrays contain the Fourier coefficients of curves that draw,
// respectively, the cursive "Dave" and the cursive "p5." They're represented
// as imaginary numbers, so coefficients are read in twos: real and imaginary
// parts. Each array is also actually two arrays concatenated: the
// coefficients for the x axis and then the coefficients for the y axis.
n=[11492,0,-1150,3410,-1062,602,-318,83,134,-74,260,176,-111,
-115,-244,-84,94,111,-115,-7,-18,44,49,41,-78,-15,43,-10,-46,0,-57,22,30,-10,-67,
25,1,2,-4,16,-22,-1,-13,-6,-17,4,-27,2,-9,1,-30,6,-12,11,-6,-3,-23,0,-13,1,-11,
-2,-27,-3,7896,0,-1119,-10,-312,-969,-392,-691,-324,371,-87,287,38,-110,55,-67,
-88,101,76,-29,-45,-23,-33,11,8,16,6,9,9,16,-29,-37,65,70,-42,-68,39,34,-25,-5,
23,8,5,-3,5,8,11,1,10,6,7,-15,2,21,16,-14,0,9,10,-5,8,7,6,-4];
m=[9013,0,-391,340,
-75,-53,-83,-169,98,-96,-81,62,22,-27,23,-10,-13,0,24,11,3,-20,2,7,15,-7,1,0,14,
0,10,-1,1094,0,-115,-636,172,-317,-66,-151,-28,60,19,57,57,-27,12,5,44,18,-10,
-37,27,4,8,5,16,-1,17,0,17,-3,21,-4];

// Make a canvas element and get its context
d = document;
e = d.createElement("canvas");
d.body.appendChild(e);
e.width = W;
e.height = H;
c = e.getContext("2d");

// MDN says never to use "with" because it puts every property of the object
// into the global scope and that can cause all sorts of unexpected errors,
// but it saves me from typing `Math.` over and over so you can't stop me
with (Math) {
  // A function to sample `c` points from the curve defined by coefficients `a`
  a = (a, c) => {
    l = a.length / 2;
    // The arrays have the two 1D curves for the x and the y axes concatenated
    // together, so we split them apart here
    x = a.slice(0, l);
    y = a.slice(l);
    // This function evaluates all the stacked cosines from the Fourier data
    // `v` at time `t` along the curve
    f = (t, v) => {
      b = v[0] / l;
      for (s = 2; s < l; s += 2) {
        b +=
          (2 / l) *
          hypot(v[s], v[s + 1]) * // Magnitude of the imaginary number
          cos((PI / l) * s * t - atan2(v[s + 1], v[s])); // Angle
      }
      return b;
    };
    // Loop `c` times to generate points
    g = [];
    for (i = 0; i < c; i++) {
      // `l` is the period of the whole shape. These Fourier curves are
      // periodic, so they represent closed curves. I don't want my words
      // to loop back on themselves though, so instead of picking a distance
      // `t` that goes between 0 and 1, I go from 0.3 to 0.97 to cut off
      // the bit of the curve that loops back on itself.
      t = ((i / c) * 0.67 + 0.3) * l;
      g.push([f(t, x), f(t, y)]);
    }
    return g;
  };
  
  // Get sample points for the "Dave" curve
  h = a(n, 70);
  // Get sample points for the "p5" curve
  j = a(m, 35);
  
  // Function to subtract a vector from another
  k = ([a, b], [c, d]) => [a - c, b - d];
  // Function to scale a vector by a constant `k`
  q = ([a, b], k) => [a * k, b * k];
  // Function to get the dot product of two vectors
  r = ([a, b], [c, d]) => a * c + b * d;
  // Function to get the shortest distance from a
  // point `p` to a polyline `v`
  u = (v, p) => {
    d = r(k(p, v[0]), k(p, v[0]));
    for (s = 1; s < v.length; s++) {
      // This is the vector between a pair of polyline points
      e = k(v[s - 1], v[s]);
      // This is the vector between one side of the line segment
      // and the view point `p`
      w = k(p, v[s]);
      // If you draw a line from `p` to the line segment that goes
      // perpendicular to the line, dot(w,e)/dot(e,e) is the (signed)
      // fraction of the line segment at which there's an intersection.
      // Clamping it to 0,1 means it stops at the endpoints of the
      // segment, and then doing the dot product with that * `e` and `w`
      // gives the squared distance to that closest point on the segment
      b = k(w, q(e, min(max(r(w, e) / r(e, e), 0), 1)));
      // Take the minimum distance for all segments
      d = min(d, r(b, b));
    }
    return sqrt(d);
  };
  // We're going to write pixels of an ImageData directly instead of
  // relying on canvas commands
  m = new ImageData(W, H);
  // This is the array we have to put pixel data in
  z = m.data;
  // Index in the array we're writing to
  i = 0;
  // A color palette
  a = [
    [F, 172, 129],
    [F, 146, 139],
    [254, 195, 166],
    [239, 233, 174],
    [205, 234, 192],
  ];
  // Loop over each pixel
  for (y = 0; y < H; y++)
    for (x = 0; x < W; x++) {
      // Get the distance to the text at this pixel. The *2 is because
      // I was initially doing this at a higher resolution.
      P = [x * 2, y * 2];
      d = min(u(h, P), u(j, P));
      // If the distance is <3px, color white. Otherwise, cycle through
      // the palette colors every 10px away we go. The ~~ is a shorter
      // form of floor().
      [R, G, B] = d < 3 ? [F, F, F] : a[~~(d / 10) % 5];
      // Write the values into the array
      z[i++] = R;
      z[i++] = G;
      z[i++] = B;
      z[i++] = F;
    }
  c.putImageData(m, 0, 0);
}
// davepagurek.com