Circles, circles, circles, and did I say Circles?

@sorceror inspired me, with a video that showed an old Dr. Who video effect, basically warping circles… but I realized I didn’t have circle code (yet), so… first off:

var canvasHue = array(pixelCount)
var canvasSat = array(pixelCount)
var canvasVal = array(pixelCount)

var height = sqrt(pixelCount)  // y axis
var width = sqrt(pixelCount)  // x axis

function DrawPixel(x, y, hue, sat, val) {
  if (x < 0 || x > width-1 || y < 0 || y > height-1) { return; }
  canvasHue[x + y*height] = hue
  canvasSat[x + y*height] = sat
  canvasVal[x + y*height] = val
}

function DrawCircle(x0, y0,radius,fill,hue, sat, val) {
  var x = round(radius);
  line = x
  originalx = x
  if (fill == 1) {line = 0}
  if (fill == 2) {
    DrawCircle(x0, y0,radius,1,0,0,0)
    DrawCircle(x0, y0,radius,0,hue, sat, val)
  }
  for (xi = line; xi <=originalx; xi++){
    x = xi
    //hue += .3
    var y = 0;
    var radiusError = 1 - x;
    x0 = round(x0)
    y0 = round(y0)
    while (x >= y) {
      y1 = round(y)
      x1 = round(x)
      DrawPixel(x1 + x0, y1 + y0,hue, sat, val);
      DrawPixel(y1 + x0, x1 + y0,hue, sat, val);
      DrawPixel(-x1 + x0, y1 + y0,hue, sat, val);
      DrawPixel(-y1 + x0, x1 + y0,hue, sat, val);
      DrawPixel(-x1 + x0, -y1 + y0,hue, sat, val);
      DrawPixel(-y1 + x0, -x1 + y0,hue, sat, val);
      DrawPixel(x1 + x0, -y1 + y0,hue, sat, val);
      DrawPixel(y1 + x0, -x1 + y0,hue, sat, val);
      y += .25;
    
      if (radiusError < 0) {
        radiusError += 2 * y + 1;
      }
      else {
        x -= .25;
        radiusError+= 2 * (y - x + 1);
      }
    }
  }
};


export function beforeRender(delta) {
  resetTransform()
  scale(width,height)
  if (random(1000) > 999) {
    canvasclear()
  }
  if (random(100) > 97) {
    DrawCircle(random(height),random(width),random(height/2),floor(random(3)),random(1),1,random(1)+.5)
  }
}

function canvasclear() {
  canvasHue.mutate(wipe)
  canvasSat.mutate(wipe)
  canvasVal.mutate(wipe)
}

function wipe(){
  return 0
}

export function render2D(index,x,y) {
  x = floor(x)
  y = floor(y)
  pixel = x+y*height
  hsv(canvasHue[pixel],canvasSat[pixel],canvasVal[pixel])
}

This is the start of a canvas drawing API library… 3 pixelCount arrays to cover HSV (could be used for RGB/RYB/etc too), height/width assumption to be square, but easily changeable if not, scale the map to be roughly width/height integer (which while I could perhaps avoid that, makes the array math trivial) [but not yet handling negative numbers, like putting origin in center]

DrawCircle(x0, y0, radius, fill, hue, sat, val) is pretty self explanatory, except for fill, which is 0 to draw the circle but no fill, 1 to fill the center same color, 2 to wipe the center then draw the outline. Might tweak to handle more options, like second color (of which 2 is a special case)

More demos to come…

1 Like

I don’t have it wired up anymore, but here’s a distance field circle implementation, with an inner radius for solid fill and outer radius that fades out to black. Looks like it could use some updating for new features like hypot().

Some things might make sense if you know that it’s for a square matrix wrapped around a cylinder, with a light barrier between the top and bottom parts. I’ll make a video next time I have it wired up!

var br1 = 1.5/16.0
export function sliderBallRadius1(v) {
    br1 = v * (2.0/16.0)
}

var br2 = 2.0/16.0
export function sliderBallRadius2(v) {
    br2 = v * (3.0/16.0)
}

export function beforeRender(delta) {
  th = time(0.071)
  bx = 1.0 - time(0.013)
  by = triangle(time(0.017)) * (1.0-br2) // ((sin(time(0.037)*PI2) * (1.0-br2)) + 1.0) / 2.0
}

export function render2D(index,x,y) {
  h = (y < 0.5) ? th : by
  s = 1
  v = 0
  
  dy = abs(y-by) 
  if (dy < br2) {
    mx = x
    bx2 = bx
    if ((bx < br2) || (bx > (1.0-br2))) {
      bx2 = ((bx2 + 0.5 ) % 1.0)
      mx = ((mx + 0.5 ) % 1.0)
    }
    
    dx = abs(mx-bx2)
    
    if (dx < br2) {
      distance = sqrt(dx*dx + dy*dy)
      if (distance <= br1) {
        v = 1
        s = distance / br1
      } else if (distance < br2) {
        v = 1.0 - ((distance-br1) / (br2-br1))
      }
    }
  }
  
  hsv(h, s, v*v)
}
1 Like

Interesting, has a bit of a 3d look on the ball, thanks to the fade and glow.

Oh right, it’s not a solid fill inside, it fades to white there via s = distance / br1.

Thanks for giving it a spin!

Inspired again by @sorceror, I present Soap Bubbles aka Fizzy Lifting

v0.5 - static bubbles
// SoapBubbles v0.5 by Scruffynerf
var canvasHue = array(pixelCount)
var canvasSat = array(pixelCount)
var canvasVal = array(pixelCount)
var canvasAge = array(pixelCount)
var canvasType = array(pixelCount)

var height = sqrt(pixelCount)  // y axis
var width = sqrt(pixelCount)  // x axis

function DrawPixel(x, y, hue, sat, val, special) {
  if (x < 0 || x > width-1 || y < 0 || y > height-1) { return }
  index = x + y*height
  canvasHue[index] = hue
  canvasSat[index] = sat
  canvasVal[index] = val
  canvasAge[index] = 99
  canvasType[index] = special
}

function DrawCircle(x0, y0, radius, fill, hue, sat, val, hue2, sat2, val2) {
  var x = round(radius);
  line = x
  originalx = x
  originalsat = sat
  special = floor(random(3))
  if (fill == 1 || fill == 3) {line = 0}
  if (fill == 2) {
    DrawCircle(x0, y0,radius,1,hue2,sat2,val2)
    DrawCircle(x0, y0,radius,0,hue, sat, val)
  }
  for (xi = line; xi <=originalx; xi++){
    if (fill == 3) {
      sat = lerp(0,originalsat*.99,(xi)/(originalx))
    }
    x = xi
    var y = 0;
    var radiusError = 1 - x;
    x0 = round(x0)
    y0 = round(y0)
    while (x >= y) {
      x1 = round(x)
      y1 = round(y)
      DrawPixel(x1 + x0, y1 + y0,hue, sat, val, special);
      DrawPixel(y1 + x0, x1 + y0,hue, sat, val, special);
      DrawPixel(-x1 + x0, y1 + y0,hue, sat, val, special);
      DrawPixel(-y1 + x0, x1 + y0,hue, sat, val, special);
      DrawPixel(-x1 + x0, -y1 + y0,hue, sat, val, special);
      DrawPixel(-y1 + x0, -x1 + y0,hue, sat, val, special);
      DrawPixel(x1 + x0, -y1 + y0,hue, sat, val, special);
      DrawPixel(y1 + x0, -x1 + y0,hue, sat, val, special);
      y += .25;
    
      if (radiusError < 0) {
        radiusError += 2 * y + 1;
      }
      else {
        x -= .25;
        radiusError+= 2 * (y - x + 1);
      }
    }
  }
}

export function beforeRender(delta) {
  resetTransform()
  scale(width,height)
  if (random(1) > .99) {
    radius = random(height/4)+2
    fill = 3
    DrawCircle(random(height-radius*1.5)+radius*.75,random(width-radius*1.5)+radius*.75,radius,fill,random(1),1,1)
  }
  canvasfade()
}

function canvasfade() {
  canvasVal.mutate(fade)
}

/*
function canvasclear() {
  canvasHue.mutate(wipe)
  canvasSat.mutate(wipe)
  canvasVal.mutate(wipe)
}
function wipe(){
  return 0
}
*/

function fade(value, index, array){
  canvasAge[index] -= random(1)
  if (canvasType[index] == 0){
    pop = canvasSat[index]
  } else if (canvasType[index] == 1) {
    pop = 1-canvasSat[index]
  } else {
    pop = canvasVal[index]
  }
  if (canvasAge[index] < 30*(pop) ){
    canvasSat[index] = canvasSat[index] * .95
    return value*canvasSat[index]
  } else {
    return value 
  }
}

function lerp(a, b, t) {
  return a * (1-t) + b * t
}

export function render2D(index,x,y) {
  x = floor(x)
  y = floor(y)
  pixel = x+y*height
  hsv(canvasHue[pixel],canvasSat[pixel],canvasVal[pixel])
}

They have some depth, thanks to the sat changes, and they pop in 3 ways, either inside to out, outside to in, or just fizzle. This version doesn’t have any sliders (yet), nor does it have them rising into view, then popping. (I’ve figured out how I think I can do that, using additional height in the canvas, so we draw ‘offscreen’, then either copy the array up a row, or perhaps use translate to move the viewing window down (which would look like the bubbles were rising) [ way cleaner/faster, but have to handle how to smoothly move back up, or maybe I shift-copy the array only then when I hit bottom, then move back to zero?]

So I just did a straight copy (mutate the various arrays upwards by one row), no translation. I still like the idea, but this works, so… I’m definitely going to use this technique again… ideas come to mind, like the classic ‘road race’…

v0.7 of Soap Bubbles - now more of a Fizzy Lifting pattern
// v0.7 of Soap Bubbles - now more of a Fizzy Lifting pattern - by Scruffynerf
var canvasHue = array(pixelCount*2)
var canvasSat = array(pixelCount*2)
var canvasVal = array(pixelCount*2)
var canvasAge = array(pixelCount*2)
var canvasType = array(pixelCount*2)

var height = sqrt(pixelCount)  // y axis
var width = sqrt(pixelCount)  // x axis

function DrawPixel(x, y, hue, sat, val, special) {
  if (x < 0 || x > width-1 || y < 0 || y > (2*height)-1) { return }
  cindex = x + y*height
  canvasHue[cindex] = hue
  canvasSat[cindex] = sat
  canvasVal[cindex] = val
  canvasAge[cindex] = 60 + ((2*height)-y)*6
  canvasType[cindex] = special
}

function DrawCircle(x0, y0, radius, fill, hue, sat, val, hue2, sat2, val2) {
  var x = round(radius);
  line = x
  originalx = x
  originalsat = sat
  special = floor(random(3))
  if (fill == 1 || fill == 3) {line = 0}
  if (fill == 2) {
    DrawCircle(x0, y0,radius,1,hue2,sat2,val2)
    DrawCircle(x0, y0,radius,0,hue, sat, val)
  }
  for (xi = line; xi <=originalx; xi++){
    if (fill == 3) {
      sat = lerp(0,originalsat*.99,(xi)/(originalx))
    }
    x = xi
    var y = 0;
    var radiusError = 1 - x;
    x0 = round(x0)
    y0 = round(y0)
    while (x >= y) {
      x1 = round(x)
      y1 = round(y)
      DrawPixel(x1 + x0, y1 + y0,hue, sat, val, special);
      DrawPixel(y1 + x0, x1 + y0,hue, sat, val, special);
      DrawPixel(-x1 + x0, y1 + y0,hue, sat, val, special);
      DrawPixel(-y1 + x0, x1 + y0,hue, sat, val, special);
      DrawPixel(-x1 + x0, -y1 + y0,hue, sat, val, special);
      DrawPixel(-y1 + x0, -x1 + y0,hue, sat, val, special);
      DrawPixel(x1 + x0, -y1 + y0,hue, sat, val, special);
      DrawPixel(y1 + x0, -x1 + y0,hue, sat, val, special);
      y += .25;
    
      if (radiusError < 0) {
        radiusError += 2 * y + 1;
      }
      else {
        x -= .25;
        radiusError+= 2 * (y - x + 1);
      }
    }
  }
}

var timer
export var speed

export function sliderSpeed(v){
  speed = 400 * v
}

export function beforeRender(delta) {
  timer = timer + delta
  resetTransform()
  scale(width,height)
  if (random(1) > .98) {
    radius = random(height/4)+2
    fill = 3
    DrawCircle(random(width-radius*1.5)+radius*.75,random(height)-radius-1+height,radius,fill,random(1),1,1)
  }
  canvasfade()
  if (timer > speed){
    timer = timer - speed
    canvasscrollup()
  }
}

function canvasfade() {
  canvasVal.mutate(fade)
}

function canvasscrollup(){
  canvasHue.mutate(scrollup)
  canvasSat.mutate(scrollup)
  canvasVal.mutate(scrollup)
  canvasAge.mutate(scrollup)
  canvasType.mutate(scrollup)
}

function scrollup(value, index, array){
  if (index < (pixelCount*2)-height){
    return array[index+height]
  }
}

function fade(value, index, array){
  if (index < pixelCount){ 
    canvasAge[index] -= random(10 * (100/speed) * ((pixelCount-index)/pixelCount) )
    if (canvasType[index] == 0){
      pop = canvasSat[index]
    } else if (canvasType[index] == 1) {
      pop = 1-canvasSat[index]
    } else {
      pop = canvasVal[index]
    }
    if (canvasAge[index] < 90*(pop) ){
      canvasSat[index] = canvasSat[index] * .95
      return value*canvasSat[index]
    } else {
      return value 
    }
  }
  return value
}

function lerp(a, b, t) {
  return a * (1-t) + b * t
}

export function render2D(index,x,y) {
  x = floor(x)
  y = floor(y)
  pixel = x+y*(height)
  if (pixel >=0 && pixel < pixelCount) { 
    hsv(canvasHue[pixel],canvasSat[pixel],canvasVal[pixel])
  }
}

Absolutely needs more tweaking, and fine tuning, but not bad for a Saturday morning’s LED creation

you-got-fingerprints-on-your-leds-when-you-ran-my-pattern-good-day-sir

2 Likes

Works great, the bubbles are very bubbly! I like the Age and Type buffers, great ideas! I am pondering how to reconcile this classic framebuffer+blitting approach with shader techniques.

Just to muddy the water even more, here’s something I’m working on that uses signed distance functions to draw various antialiased geometric shapes, filled and unfilled. This is mostly a testbed for the distance functions, so shading and so forth are pretty basic (um… slightly silly?) atm.

Geometry Testbed #1

// Global Variables
var maxObjects = 4;
var numObjects = 4;
export var objectSize = 0.21;
export var speed = 0.18;
var numShapes = 4;
var shapeSdf = array(numShapes)
var shapeCompare = array(2);
var filled = 1;
var lineWidth = 0.04;

var theta;

shapeCompare[0] = (f) => (abs(f) > lineWidth); // unfilled shapes
shapeCompare[1] = (f) => (f > lineWidth);      // filled shapes

shapeSdf[0] = circle;
shapeSdf[1] = square;
shapeSdf[2] = triangle;
shapeSdf[3] = hexagon;

// signed distance functions for various shapes, adapted for 2D. 
// Math from https://iquilezles.org/www/articles/distfunctions/distfunctions.htm
function circle(x,y,r) {
  return hypot(x,y) - r;
}

function square(x,y,size) {
  dx = abs(x) - size;  d1 = max(dx,0);
  dy = abs(y) - size;  d2 = max(dy,0);
	return min(max(dx, dy), 0.0) + hypot(d1,d2);
}

function triangle(x,y,r) {
	return max((abs(x) * 0.866025) - (y * 0.5), y) - r / 2;
}

function hexagon(x,y,r){
     x = abs(x); y = abs(y);
     return  max((x * 0.5 + y * 0.866025),x) - r;
}

// array of object vectors
var objects = array(maxObjects);

// UI
export function sliderSize(v) {
  objectSize = 0.4 * v;
}

export function sliderSpeed(v) {
  speed = v;
}

export function sliderFilled(v) {
  filled = (v >= 0.5);
}

// allocate memory for object vectors
function createObjects() {
  for (var i = 0; i < maxObjects; i++) {  
    objects[i] = array(8);
  }
}

// create object vector with a random position, direction, speed, color
function initObjects() {
  var hue = random(1);
  for (var i = 0; i < numObjects; i++) {
    var b = objects[i];  
    
    b[0] = random(1);     // x pos
    b[1] = random(1);     // y pos

    b[2] = random(0.2);  // x velocity
    b[3] = random(0.2);  // y velocity

    b[4] = hue;           // color
    b[5] = i % numShapes; // shape
    hue += 0.619033
  }
}

// move objects and bounce them off "walls"
function bounce() {
  for (var i = 0; i < numObjects; i++) {
    var b = objects[i];
    
// move object
    b[0] += b[2] * speed;
    b[1] += b[3] * speed;

// bounce off walls by flipping vector element sign when we hit.
    if (b[0] < 0) { b[0] = 0; b[2] = -b[2]; continue; } 
    if (b[1] < 0) { b[1] = 0; b[3] = -b[3]; continue; }

    if (b[0] > 1) { b[0] = 1; b[2] = -b[2]; continue; }
    if (b[1] > 1) { b[1] = 1; b[3] = -b[3]; continue; }
  }
}

createObjects();
initObjects();

export function beforeRender(delta) {
  bounce();
  theta = PI2 * time(0.1);

// uncomment the block below to rotate entire scene around its
// center
/*

  resetTransform();
  translate(-0.5,-0.5);  
  rotate(theta);  
  translate(0.5,0.5);
*/  
}

export function render2D(index,x,y) {
  var d;
  var v = 0;

  for (var i = 0; i < numObjects; i++) {
    d = shapeSdf[objects[i][5]](x-objects[i][0],y-objects[i][1],objectSize);
    if (shapeCompare[filled](d)) continue;
    
    v = 1-(d/0.04);
    s = 1.5-abs(d)/objectSize
    h = objects[i][4]-d;      
    break;
  }
     
  hsv(h, s, v*v*v)
}
1 Like

To be clear, I only implemented a most basic version of the Midpoint circle algorithm, and I’d love to replace it with something that does any polygon, arcs, etc. As I said up top, I really want to do a pattern like the DrWho one, and realized that I needed some form of circle first. (But of course have spent my time on other stuff instead)

@zranger1 these are cool, but I’m not quite sure how to do a conversion from the 2d code to the signed functions you created, can you walk thru one, and maybe do a new one?
These blow my circle code out of the water: they do fills, they do variable widths… so now I want use these (not just for circles), but still not quite ‘getting it’. So if I wanted to rotate the triangle (and only the triangle), I could either figure out how to rotate the points, OR I guess I could rotate the canvas and rotate back (ugly!)

Ha… I didn’t even know he had 2d functions – I went from the 3D.
Will do another shape one this evening to show what I did… I may actually be wrong!

1 Like

And actually, they don’t blow your circle code out of the water at all. Apples and oranges. I timed yours, and DrawCircle() is really fast, especially if you use the simpler fills. Also, the cool bubble popping effect is much harder to do on the non-frame buffer version.

Short answer: We need both. Geometry is an underexplored area on the Pixelblaze, and I look forward to seeing all the cool tools and techniques we come up with!

1 Like

Ok… here’s my first cut at annotating the GLSL->Pixelblaze porting process for the basic square sdf, and a new hexagonal star function. This is just the distance function code, but you can swap it into the demo pattern I posted earlier to see what it looks like.

Tomorrow, I’ll plug this into the demo pattern , and start dealing with rotation.

// iq's GLSL code
// note that this version permits independent x and y side length
float sdBox( in vec2 p, in vec2 b )
{
    vec2 d = abs(p)-b;
    
    // this is GLSL being sneaky about how it handles vectors -- the expression max(d,0)
    // is actually doing something like max(max(d.x,d.y),0)).  
    return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}

// Pixelblaze version
// incoming x and y coords are the difference between the object's center and the
// pixel being evaluated.   
// size is the side length of the square we want to draw.
function square(x,y,size) {

  // The two lines below compute the various parts of d = abs(p)-b, and max(d,0.0) from
  // the original, where p is a vector containing the x and y coords, and b holds the side lengths
  // of the rectangle we're checking. I force the side lengths to be the same for simplicity.
  
  dx = abs(x) - size;  d1 = max(dx,0);  // how far are x and y from the nearest edge?
  dy = abs(y) - size;  d2 = max(dy,0);
  
  // Here we put everything together for the final result. This whole thing works just
  // like the GLSL version. I just broke up the vector operations and reordered things
  // a little. 
  return min(max(dx, dy), 0.0) + hypot(d1,d2);
}

Now for a new one. A hexagonal star seems like it’d be an interesting thing to have!

// iq's GLSL code
float sdHexagram( in vec2 p, in float r )
{
    const vec4 k = vec4(-0.5,0.8660254038,0.5773502692,1.7320508076);
    p = abs(p);
    p -= 2.0*min(dot(k.xy,p),0.0)*k.xy;
    p -= 2.0*min(dot(k.yx,p),0.0)*k.yx;
    p -= vec2(clamp(p.x,r*k.z,r*k.w),r);
    return length(p)*sign(p.y);
}

// Pixelblaze version
function hexStar(x,y,r) {
  // rescale and take absolute value of pixel coords
  // original is apparently scaled to a 2*unit size
  x = abs(x*2); y = abs(y*2); 
  
  // the vector of scary constants is just a grab bag of precalculated values.
  // 1.73205 is the square root of 3.  0.866025 is half that
  // these constants are used in a lot of hexagonal sdf things
  // because on a hex with flat to flat radius of 0.5, the
  // distance between the points is 0.5/sqrt(3)
  dot = 2 * min(-0.5*x + 0.866025 * y,0);
  x -= dot * -0.5; y -= dot * 0.866025;
  
  dot = 2 * min(0.866025*x + -0.5 * y,0);
  x -= dot * 0.866025; y -= dot * -0.5;
  
  x -= clamp(x, r * 0.5773502692, r * 1.7320508076);
  y -= r;
  result = hypot(x,y);
  // TODO - could be more efficient here.  I know I've got a fast bit masking
  // signum function around somewhere.
  return (y > 0) ? result : -result;
}