Task #9: Here Comes/Goes the Sun

It’s signs of spring here in the Northern Hemisphere… And likely signs of the fall in the Southern…

In honor of the change in daylight hours, let’s do a sunset /sunrise…

There are plenty of rainbow patterns… but what about a sunset? Lush colors, fading away…

Or a sunrise:. From dark to light, peeking over the horizon…

Your mission, here in Task #9 is to do something that rises or sets, and the resulting lightshow.

Maybe you just make a rising sun? A ball of light that glides upwards from the bottom.

That’s the newbie version: Make a circle that rises into the matrix from the bottom upwards

As always, golfing is optional and always fun to see.

1 Like

The sun is coming soon! Here’s a bit of my process along the way…

So, when asked to make a simulated sun, my first thought was, “particle system”. And then, “hmmm, this may require an NVIDIA RTX 4090Ti. I wonder how much particle system I can actually build on a Pixelblaze.”

The pattern below is an attempt to find out. It’s also fun to play with – it’s a 2D n-body gravity simulator. (If you try this and crank the number of particles up, turn the gravity down a bit so all the particles won’t collapse into a personal Pixelblaze black hole.)

Click to Expand Code
/* 2D n-body gravity simulator

 Note: as you'd expect, large numbers of particles at high gravity tend to
 collapse and merge.  Lower the gravity a bit, and they'll fly free again.
 
 Requires a 2D LED array and appropriate pixel mapper.
 
 MIT License
 
 Version  Author        Date      
 1.0.1    JEM(ZRanger1) 04/03/2021
*/ 

// display size - enter the dimensions of your matrix here
var width = 16;
var height = 16;

// Global variables for rendering
var frameBuffer = array(height);  
var frameTimer = 9999;           // accumulator for simulation timer
var clearFn = clearFrameBuffer;  // pointer to function to clear screen between frames

// Global variables for particle system
var _x = 0;   
var _y = 1;
var _dx = 2;
var _dy = 3;
var _hue = 4;

var MAX_PARTICLES = 32;     
export var numParticles = 3;
export var gravity = -8;    // gravitational acceleration constant
export var speed = 25;      // milliseconds between simulation frames
var C = 3.25;               // local speed of light. 
 
// UI
export function sliderGravity(v) {
  gravity = -14.3 * (0.02+v);
}

// overall simulation speed in ms per frame
export function sliderSpeed(v) {
  speed = 150 * (1-v);
}

var last_n = numParticles;
export function sliderParticles(v) {
  numParticles = 1+floor(v * (MAX_PARTICLES-1))
  
// we zero velocity vectors to get a restart when the
// number of particles changes.
  if (last_n != numParticles) {
    initParticles();
    clearFn = clearFrameBuffer;
    frameTime = 9999;
    last_n = numParticles;
  }
}

// we need two particle buffers so we can calculate every particle's
// instantaneous effect on every other particle. Instead of copying,
// we swap pointers on each frame.
var pb1 = array(MAX_PARTICLES);  
var pb2 = array(MAX_PARTICLES);
var particles,work_particles;


function allocateFrameBuffer() {
  for (var i = 0; i < height; i ++) {
    frameBuffer[i] = array(width);
  }
}

// zero entire frame buffer
function clearFrameBuffer() {
  for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {
      frameBuffer[x][y] = 0;
    }
  } 
  clearFn = eraseParticles;
}

// not used by this version, but later...
function coolFrameBuffer() {
  for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {
      frameBuffer[x][y] = max(0,frameBuffer[x][y] - 0.05);
    }
  } 
}

// erase only the dots we drew last frame.  Much faster than
// full clear.
function eraseParticles() {
  for (var i = 0;i < numParticles; i++) {
    frameBuffer[particles[i][_x]][particles[i][_y]] = 0;
  }
}

// allocate memory for particle tables
function allocateParticleLists() {
  for (var i = 0; i < MAX_PARTICLES; i ++) {
    pb1[i] = array(5);
    pb2[i] = array(5); 
  }
}

// set particles to random positions, 0 initial velocity
// all movement is generated by gravity!
function initParticles() {
  particles = pb1;
  work_particles = pb2;
  var hue = 0.001;
  
  for (var i = 0; i < MAX_PARTICLES; i ++) {
    particles[i][_x] = random(width);
    particles[i][_y] = random(height);
    particles[i][_dx] = 0;
    particles[i][_dy] = 0;
    particles[i][_hue] = hue;
    hue = (hue + 0.27) % 1
  }  
}

function swapParticleBuffers()  {
  var tmp = work_particles;
  work_particles = particles;
  particles = tmp;
}

function moveParticles() {
  for (var i = 0; i < numParticles; i++) {
    accel_x = 0;
    accel_y = 0;
    for (var j = 0; j < numParticles; j++) {   
      if (i == j) {
        continue;  // you aren't moved by your own gravity
      }
      // calculate effect of gravity between two particles
      // force of gravity decreases with distance, except that
      // "escape velocity" isn't possible, so particles can't just walk
      // out of our simulation. All particles are considered to have
      // a mass of 1.
      var dx = (particles[i][_x] - particles[j][_x]);
      var dy = (particles[i][_y] - particles[j][_y]);
       
      var r = sqrt(dx*dx + dy*dy);
      var f = (r > 1) ? gravity / r * r : gravity;
      accel_x += f * dx / r;   
      accel_y += f * dy / r;
    }
    // calculate new velocity and limit it to C - the local speed of light
    work_particles[i][_dx] = clamp(particles[i][_dx] + (accel_x / 100),-C,C);
    work_particles[i][_dy] = clamp(particles[i][_dy] + (accel_y / 100),-C,C); 

    work_particles[i][_hue] = particles[i][_hue];

    // calculate new location based on velocity, and wrap around
    // the edges of our display.  Note that a wrapping universe
    // makes things a little weird!
    work_particles[i][_x] = particles[i][_x] + work_particles[i][_dx];
    if (work_particles[i][_x] < 0) { work_particles[i][_x] = width - 1; }
    else if ((work_particles[i][_x] >= width)) { work_particles[i][_x] = 0; }
    
    work_particles[i][_y] = particles[i][_y] + work_particles[i][_dy];
    if (work_particles[i][_y] < 0) { work_particles[i][_y] = height - 1; }
    else if ((work_particles[i][_y] >= height)) { work_particles[i][_y] = 0; }

    // draw the particle into our frame buffer    
    frameBuffer[work_particles[i][_x]][work_particles[i][_y]] = particles[i][_hue];
  }
  swapParticleBuffers();
}

// Initialization
allocateFrameBuffer();
allocateParticleLists();
initParticles();

export function beforeRender(delta) {
  frameTimer += delta;
  
  if (frameTimer > speed) {
    clearFn();
    moveParticles();
    frameTimer = 0;
  }
}

export function render2D(index, x, y) {
  var v = frameBuffer[(x * 16)][(y * 16)];
  hsv(v, 1, (v > 0) * 0.6);
}
1 Like

That’s really funny cause I was thinking about doing a particle system for PB today, and got busy with other things.

Great minds think alike, you know… I’m trying to knock a few things off my to-do list before I get my second COVID shot tomorrow so I can relax and enjoy the side effects.

It’s pretty surprising how much computation you can do if you structure it carefully. That pattern will calculate motion for 32 particles, each influencing all the others each other, on a PB2 at 16fps with the simulation running full speed. Slow it down a little so you can watch, and the frame rate goes back up really quickly. A PB3 could easily drive more particles. Things are looking pretty good for the corona and solar flare portion of my sun!

Here’s the code for my sunrise. I’ve just uploaded it to the library as well. The sun rises when you load the pattern, and when you move the “Make the Sun Rise” slider. Once up, the sun is well… having a pretty active year. Another good challenge – this was fun to think about and fun to make!

/* 2D Sunrise/solar activity simulator

 Move the slider to see the sunrise again!

 Requires a 2D LED array and appropriate pixel mapper.
 
 MIT License
 
 Version  Author        Date      
 1.0.0    JEM(ZRanger1) 04/04/2021
*/ 

// display size - enter the dimensions of your matrix here
var width = 16;
var height = 16;
var centerX = (width-1) / 2;
var centerY = (width-1) / 2;

// Global variables for rendering
var sunDiameter = 5.79
var sunMask = array(pixelCount);  // holds precalculated brightness data for sun
var frameBuffer = array(height);  // main drawing surface
var frameTimer = 9999;            // accumulator for simulation timer

// Indices for particle data array
var _x = 0;   
var _y = 1;
var _dx = 2;
var _dy = 3;
var _hue = 4;

// particle system parameters
var MAX_PARTICLES = 32;     
var numParticles = 24;
var gravity = -155;       // gravitational acceleration constant
var drift;                // allows the center of gravity to "wobble" slightly
var sunRise = (height - 1);
var speed = 80;           // milliseconds between simulation frames
var C = 3.25;             // local speed of light. 
var r1,r2,t1;             // timers and waves for animation
 
// we need two particle buffers so we can calculate every particle's
// instantaneous effect on every other particle. Instead of copying,
// we swap pointers on each frame.
var pb1 = array(MAX_PARTICLES);  
var pb2 = array(MAX_PARTICLES);
var particles,work_particles;

// we "stage manage" the sunrise by switching between several
// different beforeRender functions during the run.
var preRender = doSunrise;

// Moving this slider makes the sun rise!
export function sliderMakeTheSunRise(v) {
  preRender = doFadeout;
}

// precompute brightness mask for the sun.  This
// saves us from having to compute distance for all
// the sun's pixels on every frame
function initSunMask() {
  for (var i = 0; i < pixelCount; i++) {
    var x = i % width;
    var y = floor(i / width);
    x = centerX - x; y = centerY - y;
    var dx = sqrt(x*x + y*y);
    dx = (dx < sunDiameter) * (1-(dx / sunDiameter)); 
    sunMask[i] = dx;
  }
}

// draws the sun on the frame buffer, modeling surface activity
// with an additive sine wave plasma.  This function also
// manages "sunrise" by allowing you to offset the sun's
// position in the Y direction.
function renderSunMask() {
  for (var y1 = 0;y1 < height; y1++) {
    if ((y1+sunRise) >= height) break;    
    for (var x1 = 0; x1 < width; x1++) {
      var v2 = sunMask[x1 + (y1 * width)];
      if (v2) {
        var x = x1 / width; var y = y1 / height;
        var v1 = wave(x*r1 + y) + triangle(x - y*r2) + wave(v2);
        v1 = v1/3; 
        v1 = (v1*v1*v1);   
        frameBuffer[x1][y1+sunRise] = floor((0.2*v1) * 1000) + (v2*v1*4);
      }
    }
  }
}

function allocateFrameBuffer() {
  for (var i = 0; i < height; i ++) {
    frameBuffer[i] = array(width);
  }
}

// gradually reduce brightness of all colored pixels
function coolFrameBuffer() {
  for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {
      var n = frameBuffer[x][y] % 1;
      if (!n) continue;      
      frameBuffer[x][y] = floor(frameBuffer[x][y])+max(0,n - 0.081);
    }
  } 
}

// allocate memory for particle tables
function allocateParticleLists() {
  for (var i = 0; i < MAX_PARTICLES; i ++) {
    pb1[i] = array(5);
    pb2[i] = array(5); 
  }
}

// set particles to random positions, 0 initial velocity
// all movement is generated by gravity!
function initParticles() {
  particles = pb1;
  work_particles = pb2;
  var sunRadius = -sunDiameter / 2

  for (var i = 0; i < MAX_PARTICLES; i ++) {
    particles[i][_x] = centerX + sunRadius + random(sunDiameter);
    particles[i][_y] = centerY + sunRadius + random(sunDiameter);
    particles[i][_dx] = 0;
    particles[i][_dy] = 0;
    particles[i][_hue] = random(0.06);
  }  
}

function swapParticleBuffers()  {
  var tmp = work_particles;
  work_particles = particles;
  particles = tmp;
}

// calculate new position and acceleration for each "solar flare" particle
// and draw them in the frame buffer.  In this particle system, only
// the sun's gravity matters.  We move the center of gravity around
// a little over time to keep things stirred up.
function moveParticles() {
  drift = random(2) - 1;    
  
  for (var i = 0; i < numParticles; i++) {
    var dx = (particles[i][_x] - (centerX+drift));
    var dy = (particles[i][_y] - (centerY+drift));

    // compute force of gravity based on distance from the center of the sun       
    r = sqrt(dx*dx + dy*dy);
    var f = (r > 1) ? gravity / r * r : gravity;
    accel_x = f * dx / r;   
    accel_y = f * dy / r;

    // calculate new velocity and limit it to C - the local speed of light
    work_particles[i][_dx] = clamp(particles[i][_dx] + (accel_x / 100),-C,C);
    work_particles[i][_dy] = clamp(particles[i][_dy] + (accel_y / 100),-C,C); 

    work_particles[i][_x] = particles[i][_x] + work_particles[i][_dx];
    work_particles[i][_y] = particles[i][_y] + work_particles[i][_dy];

    // clip to our drawing area  
    if ((work_particles[i][_x] < 0) || (work_particles[i][_x] >= width)) continue;
    if ((work_particles[i][_y] < 0) || (work_particles[i][_y] >= height)) continue;    
    
    // draw in the frame buffer, limiting the drawing area and matching brightness
    // so it looks like flares are erupting fro the surface.
    if (r >= (sunDiameter * 0.9)) {
      work_particles[i][_hue] = particles[i][_hue]; 
      var bri = frameBuffer[work_particles[i][_x]][work_particles[i][_y]] % 1;
      bri = (bri) ? bri : 0.5
      frameBuffer[work_particles[i][_x]][work_particles[i][_y]] = (floor((work_particles[i][_hue]+t1) * 1000)) + bri;
    }
  }
  swapParticleBuffers();
}

// Initialization
allocateFrameBuffer();
allocateParticleLists();
initSunMask();
initParticles();

// prerender function that fades the screen to black
// over a period of time, then triggers a sunrise
// and switches back to the normal beforeRender function.
var fadeTime = 0;
function doFadeout(delta) {
  frameTimer += delta;
  fadeTime += delta

  if (frameTimer > speed) {
    coolFrameBuffer();
    frameTimer = 0;
  }
  if (fadeTime > 1500) {
    fadeTime = 0;
    sunRise = (height - 1);
    preRender = doSunrise;
  }
}

// prerender function that implements the rising sun
// and pauses for a moment before starting full
// activity.
var sunrisePause = 0;
function doSunrise(delta) {
  frameTimer += delta;
  
  sunRise = max(0,sunRise - (delta * 0.007)); 
  if (sunRise == 0) sunrisePause += delta;
  
  r1 = wave(time(0.064));
  r2 = wave(time(0.035));
  t1 = 0.05 * time(0.08);
  
  if (frameTimer > speed) {
    coolFrameBuffer();
    renderSunMask();
    frameTimer = 0;
  }  
  if (sunrisePause > 2500) {
    sunrisePause = 0;
    preRender = doActiveSun;
  }
}

// prerender function for the sun in "normal" mode with
// sunspots, surface activity, solar flares and whatnot
function doActiveSun(delta) {
  frameTimer += delta;
  
  r1 = wave(time(0.064));
  r2 = wave(time(0.035));
  t1 = 0.05 * time(0.08);
  
  if (frameTimer > speed) {
    coolFrameBuffer();
    renderSunMask();
    moveParticles();
    frameTimer = 0;
  }  
}

export function beforeRender(delta) {
  preRender(delta);
}

export function render2D(index, x, y) {
  var x1,x2,v,h;
  x1 = floor(x * width) ; y1 = floor(y * height);
  v = frameBuffer[x1][y1];
  h = floor(v) / 1000;
  v = v % 1;
  hsv(h, 1, v);
}
2 Likes

I’m thinking of removing the gravity/etc and streamlining your particle system code, making it more neutral, so adding other things besides gravity could be done with it. Need to find some time to play with it.

Go for it! This code exists to be played with and adapted for other uses. A more complete “normal” particle system would be really fun.

I was thinking last night, if we can do n-body gravity, we’re like 90% of the way to boids! And lots of other interesting things as well.

1 Like

Yeah, boids and many other fun toys

Here’s my first completely solo pattern, for the sunrise challenge.
While I initially intended it to be a 2D pattern as I am using it on lights around a bed, I liked the look of a 1D pattern, across the 0-1 world of the render2D pattern. I’m sure there is a more logical way of organizing the render2D/render3D sections.

// my first fully independent pattern, for the sunrise challenge.
// there are 3 different main variables, 
// 1)  the curve for the increasing brightness, which uses the Spread (essentially the final total width of bright pixels)
// and the Sharpness, which is the focal nature of initial bright pixels.  This is a function of time.  Currently time loops, but modifying the t1 variable could allow 
// for a run-once type scenario
// 2)  the hue, set to hueSun for within the sun, and hueEdges for the edges, and between the two uses a coarse gradient between the two.  This works for yellow-red, as
// they are neighbours on the HSV color spectrum, however a more sophistocated gradient formula would be needed for non-neighbour colors.
// 3)  the sun radius, increases over time, and then the center of this is desaturated to make it white

// for all of this example, the sun rises centred on the y-axis.  This could be easily changed in the render2D function.
// While my physical implementation is a 3D model, I found the 2D rendering works quite nicely as is, and wouldn't likely benefit from the 3rd dimension

export var hueSun = .10, hueEdges = 0.01   //0.05, 0.001
export var sharpness = 2, spread = 10
var location = 0.5 // the centre of the sunrise
export var t1  // main time cycling variable, from 0 to 1
export var sunRadius = 0
var rimToWhite = 0.1

//make a sharpness UI slider to spread the color across the whole strip or sharpen to a spot
export function sliderSpread(v) {
  spread = v * 10 // give a range of 0-10
}

// make a function for how sharp the initial pinpoint of sunrise will be, range 0-3
export function sliderSharpness(v) {
	sharpness = v * 3
}


// over-simplified, given that the HSV colorspace almost allows for a reasonable sunset color gradient.  Ideally, this would be a proper gradient calculation
// this will be fed a value from 0 to 0.5.  at 0, it should return hueSun ,and at 0.5 (or more, error correction), it should return hueEdges
function sunsetGradient(distFromSun) {
  var returnHue = hueSun
  if (distFromSun <=0) returnHue = hueSun
  else if (distFromSun >=0.4) returnHue = hueEdges
  else {
    // safely have a value of cDist between 0 and 0.4.  calculate slope of higher-lower/horizontal, plus lower, to give line between higher and lower, over the given horizontal distance.
	  returnHue = ((hueEdges-hueSun)/0.4)*distFromSun + hueSun
  }
  return returnHue
}

// time loops every 65.54*interval seconds, and interval 0.015 = 1 second apprx.
// the clamp formula with time causes the sun size growth to lag 0.1 time unit behind the increase in brightness
export function beforeRender(delta) {
  t1 = time(0.5)
  sunRadius = (clamp(t1-0.1,0,1))*0.35
}

export function render2D(index, x, y) {
   var cDist = abs(location-y)
   var distFromSun = clamp((cDist-sunRadius),0,1)
   var sat
   var hue
 // y=-2*(0.5*x)^2 + x
   var va = - spread * pow(((1/spread)*cDist), sharpness) + (t1)
   va = clamp(va,0,1) //clamp the value between zero and one
 
    // calculate white (desaturated) centre of sun
    if (cDist<(sunRadius-rimToWhite)) {
      // from cDist=0 to cDist=sunRadius-rimToWhite, gradient between 0.5 and 1  sat=(1/(radius-rimToWhite))*cDist
      sat=((1-0.5)/(sunRadius-rimToWhite))*cDist + 0.5
    }
    else sat = 1
    
    // calculate gradient,as a functin of the distance from the sun centre.
    hue = sunsetGradient(distFromSun)
    
    hsv(hue, sat , va)
}

export function render3D(index, x, y, z) {
   render2D(index,x,y)
}

edit: fixed my math on the value (brightness) equation.
I would still sort of like the first color to be the edge (red) color, then fade into the yellow, then progress as already coded, but that’s a problem for another day.

4 Likes

@bdm! Ben!! This is a monster of a first pattern from scratch!

Great job! I like it so much that I’ll submit a tweak of yours as my opinionated submission; hopefully there’s some good tricks to pick up along the way.

var hueSun = .10, hueEdges = 0.01
var sharpness = 3, spread = .8
var location = 0.5 // the centre of the sunrise
export var t1  // main time cycling variable, from 0 to 1
var sunRadius = 0.5
var rimToWhite = 0.01

// over-simplified, given that the HSV colorspace almost allows for a reasonable sunset color gradient.  Ideally, this would be a proper gradient calculation
// this will be fed a value from 0 to 0.5.  at 0, it should return hueSun ,and at 0.5 (or more, error correction), it should return hueEdges
function sunsetGradient(distFromSun) {
  var returnHue = hueSun
  if (distFromSun <=0) returnHue = hueSun
  else if (distFromSun >=0.4) returnHue = hueEdges
  else {
    // safely have a value of cDist between 0 and 0.4.  calculate slope of higher-lower/horizontal, plus lower, to give line between higher and lower, over the given horizontal distance.
	  returnHue = ((hueEdges-hueSun)/0.4)*distFromSun + hueSun
  }
  return returnHue
}

// Only here for the v2
function hypot(a, b) { return sqrt(a * a + b * b) }

//https://www.gizma.com/easing/
function easeOutQuad(t, b, c, d) {
	t /= d
	return -c * t * (t-2) + b
}

// time loops every 65.54*interval seconds, and interval 0.015 = 1 second apprx.
export function beforeRender(delta) {
  t0 = clamp(1.5 * time(0.2) - .25, 0, 1)
  t1 = easeOutQuad(t0, 0, 1, 1)
  sunRadius = .3 + t0 / 3
}

export function render2D(index, x, y) {
  x -= .5; y = 4 - y - t1
  var cDist = hypot(x, y - 2 * t1)
  var distFromSun = clamp((cDist-sunRadius)/2,0, 1)
  var sat, hue
 
  var va = spread * pow((1/spread)-cDist, sharpness) + t1
  va = clamp(va, 0, .5) //clamp the value
 
  // calculate white (desaturated) centre of sun
  sat = (cDist<(sunRadius-rimToWhite)) ?.5/(sunRadius-rimToWhite)*cDist + .5 : 1
    
  // calculate gradient,as a functin of the distance from the sun centre.
  hue = sunsetGradient(distFromSun)
    
  hsv(hue, sqrt(sat), va * va * va)
}

export function render3D(index, x, y, z) {
   render2D(index,x,y)
}
export function render(index) {
   render2D(index, .5, index / pixelCount)
}
1 Like

@bdm, I love the range of colors – it looks very much like the spring/summer sun out here in the desert!

I built onto the work by @bdm and @jeff to adapt this for a light up alarm clock and a nightlight. Needs some work yet to better leverage the pixel mapper

var width = 16
var height = 32
var numPixels = width * height
var canvasValues = array(numPixels) //make a "canvas" of brightness values
var canvasHues = array(numPixels) //likewise for hues
var canvasSats = array(numPixels) //likewise for sats
var syi = -1.2 //Starting location of sun core in y direction "rises" to positive values
var syf = .1 //Final location of sun core in y direction
var sx = .5 //Starting location of sun core in x direction, centered on panel
var sy = syi //Current sun location in y direction
var r = .01  //Radius of sun core from 0-1
var hueSun = .10, hueEdges = 0.01
var t1 = 0


function cDist(x, y){
  return hypot(x - sx,y - sy)
}

export function inputNumberWakeTimer(v){ //how long the animation will run in minutes
  minutes = clamp(v, 1, 30) //minutes from 1 - 30
  sec = (minutes * 60) //determine seconds
  return sec; minutes
}

export function inputNumberOffTimer(v){ //how long the lights will remain lit in minutes
  offMinutes = clamp(v, minutes + 1, 120) //minutes from 1 - 120
  offAfter = (offMinutes * 60) //determine seconds
  return offAfter
}

export function toggleEnableSunRise(isEnabled){
  if (isEnabled){
    sunrise = true
    syi = -1.2 //Starting location of sun core in y direction "rises" to positive values
    syf = .1 //Final location of sun core in y direction
  }
  else{
    sunrise = false
    syi = .1 //Starting location of sun core in y direction "sets" to negative values
    syf = -1.3 //Final location of sun core in y direction
  }
}

function sunsetGradient(distFromSun) {
  var returnHue = hueSun
  if (distFromSun <=0) returnHue = hueSun
  else if (distFromSun >=0.9) returnHue = hueEdges
  else {
    // safely have a value of cDist between 0 and 0.9.  calculate slope of higher-lower/horizontal, plus lower, to give line between higher and lower, over the given horizontal distance.
	  returnHue = ((hueEdges-hueSun)/0.9)*distFromSun + hueSun
  }
  return returnHue
}

//find the pixel index within a canvas array
//pixels are packed in rows, then columns of rows
function getIndex(x, y) {
  return floor(x*width) + floor(y*height)*width
}

function distanceToCenter(x, y){
  dindex = getIndex(x, y)
  sDistance = cDist(x, y)
  if (pow(x - sx,2) + pow(y - sy,2) <= (sqrt(r)/8)){ //White center based on distance from x, y to sx, sy
    canvasValues[dindex] = .75
    canvasHues[dindex] = sunsetGradient(sDistance)
    canvasSats[dindex] = 0
  }
  else if ((pow(x - sx,2) + pow(y - sy,2) <= (15 * (sqrt(r)))) & (pow(x - sx,2) + pow(y - sy,2) >= (sqrt(r)/8))){ //use sunsetGradient to determine hue based on distance from x, y to sx, sy
    canvasValues[dindex] = .75
    canvasHues[dindex] = sunsetGradient(sDistance)
    canvasSats[dindex] = 1
  }
  else if ((pow(x - sx,2) + pow(y - sy,2) > (15 * (sqrt(r))))){ //use sunsetGradient to turn off pixels based on distance from x, y to sx, sy
    canvasValues[dindex] = 0
    canvasHues[dindex] = sunsetGradient(sDistance)
    canvasSats[dindex] = 1
  }
}

export function beforeRender(delta) { 
  t1sec = delta / 1000 //Convert delta to seconds to avoid wrapping
  t1 = clamp(t1 + t1sec, 0, offAfter) //keep track of elapsed time, stop at offAfter interval to avoid wrapping
  if (t1 == 0){  //reset y to start position
    sy = syi
  }
  else if(t1 >= sec){  //freeze y at final position
    sy = syf
  }
  else{ //move y from start to final position
    pos = clamp(t1 / sec, 0, 1) //determine how far y will move since last frame, as a percentage
    if (sunrise){
      sy = (pos*abs(syi-syf)) + syi //move y pos % to its final position
    }
    if (!sunrise){
      sy = syi - (pos*abs(syf-syi)) //move y pos % to its final position
    }
  }
}

export function render2D(index, x, y) {
  if (t1 >= offAfter){ //shut off all lights after elapsed interval
    hsv(0, 0, 0)
  }
  else{
    index = getIndex(x, y) //calc this pixel's index in the canvas based on position
    distanceToCenter(x, y) //calc this pixels distance to the center of the sun and set H, S, V based on this value
    h = canvasHues[index]
    v = canvasValues[index]
    s = canvasSats[index]
    hsv(h, s, v*v)
  }
}

//var watcher for troubleshooting
export var sy;
export var offAfter;
export var t1;
export var sec;
export var minutes;
export var pos;
export var sunrise;
1 Like

This isn’t an entry, per se, but it would make any of the entries more fun, because you could make them animate in sync with the actual sun!

I started with sunrise-sunset/index.js at b7b0cd9712257c1dea1f47b0390471c2ca6d966d · udivankin/sunrise-sunset · GitHub (the version before it was ported to TypeScript) and PBscriptified it, rolled a bunch of degrees/radians calculations directly into the code, and added a cache and fade in/out utility.

It seems to calculate the correct sunrise/sunset for my location (hardcoded in this example, be sure to change it to your lat/long!), more or less the same as Sunrise and Sunset Calculator. I have a small doubt about the +1 I had to add to the hour. I expect that’s a DST artifact and I’m happy to report that DST is soon going away forever where I live. :smiley:

Sunrise/Sunset calculator code
function pmod(a, b) {
  result = a % b

  return result < 0
    ? result + b
    : result
}

function getDayOfYear(Year, Month, Day) {
  // check for leap year
  mdays = [31,
    ( (((Year % 4) == 0) && (Year % 100 != 0)) || (Year % 400 == 0) ) ? 29 : 28,
  31,30,31,30,31,31,30,31,30,31]

  yday = Day

  for(m = 0; m < Month; m++) {
      yday += mdays[m]
  }

  return yday + 1 // ? +1 because it's 0-based? I dunno.
}

DEFAULT_ZENITH = 90.8333
DEGREES_PER_HOUR = 15

// Return fractional hour of sunrise or sunset
function calculate(latitude, longitude, isSunrise, zenith, Year, Month, Day) {
  var dayOfYear = getDayOfYear(Year, Month, Day)
  var hoursFromMeridian = longitude / DEGREES_PER_HOUR
  var approxTimeOfEventInDays = isSunrise
    ? dayOfYear + ((6 - hoursFromMeridian) / 24)
    : dayOfYear + ((18 - hoursFromMeridian) / 24)

  var sunMeanAnomaly = (0.9856 * approxTimeOfEventInDays) - 3.289
  var sunTrueLongitude = pmod(sunMeanAnomaly + (1.916 * sin(sunMeanAnomaly * PI / 180)) + (0.020 * sin(sunMeanAnomaly * PI / 90)) + 282.634, 360)
  var ascension = 0.91764 * tan(sunTrueLongitude * PI / 180)

  var rightAscension = atan(ascension) * 180 / PI
  rightAscension = pmod(rightAscension, 360)

  var lQuadrant = floor(sunTrueLongitude / 90) * 90
  var raQuadrant = floor(rightAscension / 90) * 90
  rightAscension = rightAscension + (lQuadrant - raQuadrant)
  rightAscension /= DEGREES_PER_HOUR

  var sinDec = 0.39782 * sin(sunTrueLongitude * PI / 180)
  var cosDec = cos(asin(sinDec))
  var cosLocalHourAngle = ((cos(zenith * PI / 180)) - (sinDec * (sin(latitude * PI / 180)))) / (cosDec * cos(latitude * PI / 180))

  var localHourAngle = isSunrise
    ? 360 - (acos(cosLocalHourAngle) * (180 / PI))
    : acos(cosLocalHourAngle) * (180 / PI)

  var localHour = localHourAngle / DEGREES_PER_HOUR
  var localMeanTime = localHour + rightAscension - (0.06571 * approxTimeOfEventInDays) - 6.622
  var hour = pmod(localMeanTime - (longitude / DEGREES_PER_HOUR), 24) + 1 // ? +1 not sure why, maybe DST?

  return hour
}

// sunrise calc-and-cache
sr_cache = [-1,-1,-1,-1]

function getSunrise(latitude, longitude) {
  if (sr_cache[0] != clockYear() || sr_cache[1] != clockMonth() || sr_cache[2] != clockDay()) {
    sr_cache[0] = clockYear()
    sr_cache[1] = clockMonth()
    sr_cache[2] = clockDay()
    sr_cache[3] = calculate(latitude, longitude, true, DEFAULT_ZENITH, sr_cache[0], sr_cache[1], sr_cache[2])
  }
  return sr_cache[3]
}

// sunset calc-and-cache
ss_cache = [-1,-1,-1,-1]

function getSunset(latitude, longitude) {
  if (ss_cache[0] != clockYear() || ss_cache[1] != clockMonth() || ss_cache[2] != clockDay()) {
    ss_cache[0] = clockYear()
    ss_cache[1] = clockMonth()
    ss_cache[2] = clockDay()
    ss_cache[3] = calculate(latitude, longitude, false, DEFAULT_ZENITH, ss_cache[0], ss_cache[1], ss_cache[2])
  }
  return ss_cache[3]
}

// given a location, return a value which is 1 between sunrise and sunset
// but fades to 0 over trans (fractional) hours just outside of sr/ss
function getPower(latitude, longitude, trans) {
  var now = clockHour() + (clockMinute() / 60) + (clockSecond() / 3600)
  var sr = getSunrise(latitude, longitude)
  var ss = getSunset(latitude, longitude)

  if ( (now > sr) && (now < ss) ) {
    return 1
  }

  sr = sr - now
  if ( (sr > 0) && (sr < trans) ) {
    return (1 - sr/trans)
  }

  ss = now - ss
  if ( (ss > 0) && (ss < trans) ) {
    return ss/trans
  }

  return 0
}

// simple demonstration - use vars watch to see the results.
export var srh, srm, ssh, ssm, power

export function beforeRender(delta)  {
    var lat = 52.339
    var long = 4.9592
    sr = getSunrise(lat, long)
    srh = floor(sr)
    srm = 60 * (sr % 1)
    ss = getSunset(lat, long)
    ssh = floor(ss)
    ssm = 60 * (ss % 1)

    power = getPower(lat, long, 0.25) // 0.25h = 15m
}

export function render(index) {
    rgb(power,power,power)
}
2 Likes

That’s awesome @sorceror ! Very nice to have the mathy sunrise/set calculators for patterns.