Yet another approach to text rendering (WIP)

Ok! Now with a tripod video showing the motion blur and italicization sliders, and … well who doesn’t like rainbows?

// PixelBlaze pattern by Tom Rathborne <tom.rathborne@gmail.com>
// Made for fun. Share and enjoy!

// This pattern creates a rectangular framebuffer
// and does its own integer coordinate mapping
// and draws a text scroller with fonts made at https://brutalita.com/
// using Xiaolin Wu's antialiased line algorithm.

// See the bottom of this pattern for a Ruby program which generates these two lines:
chars = [[[[0,2.5],[2,2.5],[2,1],[1,0],[0,0],[0,4]]],[[[1,0]],[[0.5,1],[1,1],[1,4],[0.5,4],[1.5,4]]],[[[0,1],[2,4]],[[2,1],[0,4]]],[[[0,2.5],[2,2.5],[2,1.5],[1.5,1],[0.5,1],[0,1.5],[0,3.5],[0.5,4],[1.5,4],[2,3.5]]],[[[0.5,0],[1,0],[1,4],[0.5,4],[1.5,4]]],[[[0,0],[1,0],[2,1],[2,2],[1,2],[2,3],[2,4],[0,4],[0,0]]],[[[0.5,1],[1.5,1],[2,1.5],[2,3],[1,4],[0.5,4],[0,3.5],[0,1.5],[0.5,1]],[[2,1],[2,4]]],[[[0,1],[2,1],[0,4],[2,4]]],[[[1,3],[0,2],[0,1.5],[0.5,1],[1,1.5],[1.5,1],[2,1.5],[2,2],[1,3]]],[[[0,1],[0,4]],[[0,2],[1,1],[2,1]]],[[[0,1],[0,3.5],[0.5,4],[1.5,4],[2,3.5],[2,1]]],[[[0,1],[2,1]],[[1,0],[1,3.5],[1.5,4],[2,4]]]]
texts = [[0,1,2,3,4,5,4,6,7,3,-1,8,-1,5,9,10,11,6,4,1,11,6,-1,8,-1]]
// FIXME: So far this pattern only uses texts[0]

// value by which we mulitply each pixel on every frame for motion blur
// more than 50% blur is excessive so /2
var blur = 0
export function sliderMotionBlur(v) { blur = v/2 }

// Duplicate PixelBlaze 'Mapper' functionality without normalizing

var width = 32
var height = 8
// whoops = 1/(pixelCount == (width * height)) // crash on user error :P

// Some constants to avoid repeated arithmetic (esp.division) on constants
var widthm1 = width - 1
var widthm1_inv = 1/widthm1
var heightm1_inv = 1/(height-1)
var heightm1_invR = 0.33333333/(height-1) // How much of a rainbow we want

// Italicization
var italic = 0
export function sliderItalicization(v) { italic = 0.5-v }

// RGB framebuffer
// ... Storage
var FB = array(pixelCount)
FB.mutate(()=>{return array(3)})

// ... direct references to pixel by index for a quick render
var IndexPixel = array(pixelCount)
IndexPixel.mutate((foo,index)=>{
  var x = floor(index / height)
  var y = index % height
  y = (x % 2 == 1) ? (height - 1 - y) : y // I have a zigzag LED matrix
  return FB[x + y * width]
})

// HSV to RGB using global variables
var r, g, b // filled by HSVtoRGB

function HSVtoRGB(h, s, v) {
  var i, f, p, q, t;
  i = floor(h * 6);
  f = h * 6 - i;
  p = v * (1 - s);
  q = v * (1 - f * s);
  t = v * (1 - (1 - f) * s);
  im6 = i % 6
  if (im6 == 0) {
    r = v; g = t; b = p;
  } else { if (im6 == 1) {
    r = q; g = v; b = p;
  } else { if (im6 == 2) {
    r = p; g = v; b = t;
  } else { if (im6 == 3) {
    r = p; g = q; b = v;
  } else { if (im6 == 4) {
    r = t; g = p; b = v;
  } else {if (im6 == 5) {
    r = v; g = p; b = q;
  }}}}}}
}

// ... Render a pixel
//  - Only used by WuLine, so we do 'alpha' here to keep WuLine readable
//  - Uses max() so overlapping line pixels don't make bright spots
//  - XadjustSV makes the left/right areas fade to black via S and V.

var XadjustSV = array(width)
XadjustSV.mutate((foo,x) => { return clamp(1.1 - pow(2*((x*widthm1_inv)-0.5),2),0,1) })

function PixelMax(x,y,a,h,s,v) {
  if (x >= 0 && y >= 0 && x < width && y < height) {
    adjSV = XadjustSV[x]
    HSVtoRGB(
      (h + y*heightm1_invR) % 1, // rainbow gradient in Y 
      s*adjSV,
      v*adjSV*adjSV*a*a*a // ^2 and ^3 here are some excuse for gamma
    )
    var Pixel = FB[x + y * width]
    Pixel[0] = max(Pixel[0], r)
    Pixel[1] = max(Pixel[1], g)
    Pixel[2] = max(Pixel[2], b)
  }
}

// Xiaolin Wu's antialiased line algorithm
// Adapted from https://gist.github.com/polyamide/3f33cb4dc69e22fbf8b66cee39b78d60
// (replaced utility functions with PixelBlaze built-ins)

function WuLine(DrawPixel, x0, y0, x1, y1, h, s, v) {
  if (x0 == x1 && y0 == y1) return

  steep = abs(y1 - y0) > abs(x1 - x0);

  if (steep) {
    tmp = y0; y0 = x0; x0 = tmp;
    tmp = y1; y1 = x1; x1 = tmp;
  }

  if (x0 > x1) {
    tmp = x0; x0 = x1; x1 = tmp;
    tmp = y0; y0 = y1; y1 = tmp;
  }

  dx = x1 - x0;
  dy = y1 - y0;
  gradient = dy / dx;

  xEnd = round(x0);
  yEnd = y0 + gradient * (xEnd - x0);
  xGap = 1 - frac(x0 + 0.5);
  xPx1 = xEnd;
  yPx1 = trunc(yEnd);

  if (steep) {
    DrawPixel(yPx1, xPx1, 1 - frac(yEnd) * xGap, h, s, v )
    DrawPixel(yPx1 + 1, xPx1, frac(yEnd) * xGap, h, s, v )
  } else {
    DrawPixel(xPx1, yPx1, 1 - frac(yEnd) * xGap, h, s, v )
    DrawPixel(xPx1, yPx1 + 1, frac(yEnd) * xGap, h, s, v )
  }

  intery = yEnd + gradient;

  xEnd = round(x1);
  yEnd = y1 + gradient * (xEnd - x1);
  xGap = frac(x1 + 0.5);

  xPx2 = xEnd;
  yPx2 = trunc(yEnd);

  if (steep) {
    DrawPixel(yPx2, xPx2, 1 - frac(yEnd) * xGap, h, s, v )
    DrawPixel(yPx2 + 1, xPx2, frac(yEnd) * xGap, h, s, v )
  } else {
    DrawPixel(xPx2, yPx2, 1 - frac(yEnd) * xGap, h, s, v )
    DrawPixel(xPx2, yPx2 + 1, frac(yEnd) * xGap, h, s, v )
  }

  if (steep) {
    for (x = xPx1 + 1; x <= xPx2 - 1; x++) {
      DrawPixel(trunc(intery), x, 1 - frac(intery), h, s, v )
      DrawPixel(trunc(intery) + 1, x, frac(intery), h, s, v )
      intery = intery + gradient;
    }
  } else {
    for (x = xPx1 + 1; x <= xPx2 - 1; x++) {
      DrawPixel(x, trunc(intery), 1 - frac(intery), h, s, v )
      DrawPixel(x, trunc(intery) + 1, frac(intery), h, s, v )
      intery = intery + gradient
    }
  }
}

function WarpPoint(x,y,out) {
  xt = x * widthm1_inv
  out[0] = widthm1 * 0.5 * (cos(PI + PI * xt) + 1) + y * italic
  out[1] = y * clamp((1 - pow(2*(xt-0.5),2)),0,1)
}

// Draw a Character
// - starting at x,y
// - draw WuLine with r,g,b
var pstart = array(2)
var pend = array(2)

function DrawCharacter(chr,x,y,sx,sy,h,s,v) {
  var segments = chr.length

  for (N=0; N<segments; N++) {
    var segment = chr[N]
    points = segment.length

    if (points == 1) {
      WarpPoint(x + segment[0][0] * sx, y + segment[0][1] * sy, pstart)
      PixelMax(floor(pstart[0]),floor(pstart[1]),1,h,s,v)
    } else {
      WarpPoint(x + segment[0][0] * sx, y + segment[0][1] * sy, pstart)
      for (L=1; L<points; L++) {
        WarpPoint(x + segment[L][0] * sx, y + segment[L][1] * sy, pend)
        WuLine(
          PixelMax,
          pstart[0], pstart[1],
          pend[0], pend[1],
          h, s, v
        )
        pstart[0] = pend[0]
        pstart[1] = pend[1]
      }
    }
  }
}

// Font data is on a 3x6 grid,
// so we divide our target size by 2 and 5 to get a scaling factor:
cws = 2.3 / 2
chs = 8.3 / 5
// FIXME: just walk over the coordinate array and do this once
// Character spacing
csp = 3.6

// Clear (or fade if blur enabled) framebuffer
// and render the string

export function beforeRender(delta) {
  FB.forEach((Pixel) => { Pixel.mutate((v) => { return blur * v } )})

  text = texts[0] // FIXME: handle multiple strings?
  t1 = time(0.09)

  // FIXME: speed depends on string length
  // FIXME: we start waaaaay offscreen to the left but could just start -1 character
  xoff = -text.length * csp * t1

  while (xoff < width) {
    text.forEach((nchr) => {
      if (xoff > -csp && nchr != -1 && xoff < width) {
        DrawCharacter(chars[nchr],xoff,0,cws,chs,t1,1,1)
      }
      xoff = xoff + csp
    })
  }
}

// Draw from the framebuffer
export function render(index) {
  Pixel = IndexPixel[index]
  rgb(Pixel[0], Pixel[1], Pixel[2])
}

// #!/usr/bin/env ruby
//
// # Usage: $0 < font.json "string" ...
//
// require 'multi_json'
//
// # Output variables
// chars = []
// texts = []
//
// # Character -> chars[] index tracker
// cnums = { ' ' => -1 }
// cnum = 0
//
// font = MultiJson.load(STDIN.read)
//
// ARGV.each do |text|
//   tcnums = []
//   text.chars.each do |char|
//     if cnums.key?(char)
//       tcnums.append(cnums[char])
//     elsif font.key?(char)
//       chars[cnum] = font[char]
//       cnums[char] = cnum
//       tcnums.append(cnum)
//       cnum += 1
//     else
//       warn('Character not in font: ' + char)
//     end
//   end
//   texts.append(tcnums)
// end
//
// puts 'chars = ' + MultiJson.dump(chars)
// puts 'texts = ' + MultiJson.dump(texts)
6 Likes