Pixels array p5.js vs java improving speed

Hi. I translated a java to a p5.js sketch.
The p5.js pixels array takes 4 times more values and I believe that is why it the sketch runs far more slowly. The java sketch runs faster as the gif image below. The p5.js runs terribly slow as you can see here in the web editor. Is there a way (maybe through bit shifting) to improve the speed?

java:

int k[] = new int[128];
int[] a;
float u, v, r = 42, g = 84, b = 128;
int w = 300, p = 0;

void setup() {
  size(300, 300);
  a = new int[w * w];
  for (int x = 0; x < w; x++) {
    for (int y = 0; y < w; y++) {
      a[x + y * w] = int((b + (b * sin(x / 32.0)) +
        (b + (b * cos(y / 32.0))) +
        (b + (b * sin(sqrt((x * x + y * y)) / 32.0)))) / 4);
    }
  }
}

void draw() {
  p++;
  for (int i = 0; i < b; i++) {
    u = sin(i * 0.12);
    v = sin(i * PI / 50 + 0.79);
    k[i] = color(r + u * b, g + v * b, b + u * b);
  }
  loadPixels();
  for (int i = 0; i < w * w; i++) {
    pixels[i] =  k[(a[i] + p) & 127];
  }
  updatePixels();
}

p5.js:

let a = [];
let k = [];
let u, v, r = 42, g = 84, b = 128, p = 0, s = 32;

function setup() {
  createCanvas((w = 300), w);
  pixelDensity(1);
  for (let x = 0; x < w; x ++) {
    for (let y = 0; y < 4 * w; y ++) {
      a[x * 4 + y  * 4* w] = int((b + b * sin(x / 32.0) +
      (b + b * cos(y / s)) +
      (b + b * sin(sqrt(x * x + y * y) / s))) / 4);
    }
  }
}

function draw() {
  p++;
  for (i = 0; i < 128; i++) {
    u = sin(i * 0.12);
    v = sin(i * PI / 50 + 0.79);
    k[i] = color(r + u * b, g + v * b, b + u * b);
  }
  loadPixels();
  for (let i = 0; i < 4 * w * w; i += 4) {
    c = k[(a[i] + p) & 127];
    pixels[i] = red(c);
    pixels[i + 1] = green(c);
    pixels[i + 2] = blue(c);
    pixels[i + 3] = alpha(c);
  }
  updatePixels();
}

plasma

If your aim was just to make it run online just know your sketch is totally convertible to Processing.js:

index.html:

<script defer src=https://Unpkg.com/processing-js></script>
<canvas data-src=Trig_Pixel_Colors.pde></canvas>

As you can see from the embedded <iframe> above I’ve made many changes to your Java sketch.

Most important 1 was moving the K[] array initialization to setup().

After all, nothing in the K[] array changes after it’s filled w/ colors the 1st time, just like A[].

While Java Mode uses a 32bit integer value to represent an aRGB color for its pixels[] array, p5js version goes w/ an Uint8ClampedArray container which requires 4 elements to represent an RGBa value:

However there’s a workaround to access p5js’ pixels[] as a 32bit value!

We simply create a new 32bit typed array, either Int32Array or Uint32Array, and pass the pixels[]'s Uint8ClampedArray::buffer to its constructor:
pix32 = new Uint32Array(pixels.buffer);

This way we get a view over the original Uint8ClampedArray container, where any changes made to 1 reflect on the other.

The only trap about it is that typed arrays store their values according to a computer’s endianness, which for almost all personal devices is little-endian:

So we need to invert the byte-order when we’re converting a sequence of RGBa 8bit values to a 32bit unified value, resulting in an aBGR order!

Another obstacle is that p5js’ color() returns a p5.Color object rather than a 32bit value like Processing’s.

Among the many p5.Color undocumented properties is levels[], which stores an RGBa representation as a 4-length array:
{ levels: rgbaArray } = color(R + u, G + v, B + u),

The line above relies on an object destructuring assignment in order to unpack the levels[] property from the p5.Color object and then rename it to rgbaArray[].

Next step now is to convert that 4-length array to an aBGR unified value.

For that I’ve chosen method Array::reduce():

abgrValue = rgbaArray.reduce(RGBa_Arr_to_aBGR_32bit_Val, 0);

function RGBa_Arr_to_aBGR_32bit_Val(acc, val, idx) {
  return acc | val << idx * 0o10;
}

Here’s the complete p5js sketch running online w/ same performance as Processing’s:

index.html:

<script async src=https://cdn.JsDelivr.net/npm/p5></script>
<script defer src=sketch.js></script>

Extra links about other JS syntaxes I’ve used on the p5js flavor:

4 Likes

One fix is to calculate the k array just once in setup rather than every frame.

let a = [];
let k = [];
let u, v, r = 42, g = 84, b = 128, p = 0, s = 32;

function setup() {
  createCanvas((w = 300), w);
  pixelDensity(1);
  for (let x = 0; x < w; x ++) {
    for (let y = 0; y < 4 * w; y ++) {
      a[x * 4 + y  * 4* w] = int((b + b * sin(x / 32.0) +
        (b + b * cos(y / s)) +
        (b + b * sin(sqrt(x * x + y * y) / s))) / 4);
    }
  }
  for (i = 0; i < 128; i++) {
    u = sin(i * 0.12);
    v = sin(i * PI / 50 + 0.79);
    k[i] = color(r + u * b, g + v * b, b + u * b);
  }
}

function draw() {
  p++;
  loadPixels();
  for (let i = 0; i < 4 * w * w; i += 4) {
    c = k[(a[i] + p) & 127];
    pixels[i] = red(c);
    pixels[i + 1] = green(c);
    pixels[i + 2] = blue(c);
    pixels[i + 3] = alpha(c);
  }
  updatePixels();
}
3 Likes

I have made the following changes

  1. calculate the k array in setup rather than every frame
  2. using the array.length rather than calculate the number of pixels in every loop iteration.
  3. the k array does not use the alpha channel so set alpha to 255 (opaque) directly
  4. get the rgb channel values directly from the pixel color rather that via the graphics objectt

The last one should avoid p5.js doing a lot of internal color mode calcs, although I am not sure.

let a = [];
let k = [];
let u, v, r = 42, g = 84, b = 128, p = 0, s = 32;

function setup() {
  createCanvas((w = 300), w);
  pixelDensity(1);
  for (let x = 0; x < w; x ++) {
    for (let y = 0; y < 4 * w; y ++) {
      a[x * 4 + y  * 4* w] = int((b + b * sin(x / 32.0) +
        (b + b * cos(y / s)) +
        (b + b * sin(sqrt(x * x + y * y) / s))) / 4);
    }
  }
  for (i = 0; i < 128; i++) {
    u = sin(i * 0.12);
    v = sin(i * PI / 50 + 0.79);
    k[i] = color(r + u * b, g + v * b, b + u * b);
  }
}

function draw() {
  p++;
  loadPixels();
  for (let i = 0; i < pixels.length; i += 4) {
    c = k[(a[i] + p) & 127];
    pixels[i] = c._getRed();
    pixels[i + 1] = c._getGreen();
    pixels[i + 2] = c._getBlue();
    pixels[i + 3] = 255;
  }
  updatePixels();
}

You would need to do some bench marking to see if any improvement is significant :smile:

2 Likes

Thank you so much for the detailed info and relevant links. I’m going to study these carefully. The speed is excellent now!

1 Like

Thank you very much. The changes including the last one, which I tested maintaining the k array in draw, but using the rgb channel values directly from the pixel color improves the speed a lot. When using an 800/800 canvas, the original sketch nearly stops, but when using the values directly, the sketch still runs fluidly. So that’s a big win. My goal was trying to make a processing’s tweet of it, but that is not going to happen. Still far too much characters :unamused:

a=k=,r=42,g=84,b=128,u=v=p=0,s=32,setup=e=>{createCanvas(w=400,w),pixelDensity(1);for(let e=0;e<w;e++)for(let i=0;i<4w;i++)a[4e+4iw]=int((b+bsin(e/32)+(b+bcos(i/s))+(b+bsin(sqrt(ee+ii)/s)))/4);for(i=0;i<128;i++)u=sin(.12i),v=sin(iPI/50+.79),k[i]=color(r+ub,g+vb,b+ub)},draw=e=>{p++,loadPixels();for(let e=0;e<pixels.length;e+=4)c=k[a[e]+p&127],pixels[e]=c._getRed(),pixels[e+1]=c._getGreen(),pixels[e+2]=c._getBlue(),pixels[e+3]=255;updatePixels()};//#つぶやきProcessing

Changing the draw method to

function draw() {
  p++;
  loadPixels();
  let t = pixels;
  for (let i = 0; i < t.length; i += 4) {
    c = k[(a[i] + p) & 127];
    t[i] = c._getRed();
    t[i + 1] = c._getGreen();
    t[i + 2] = c._getBlue();
    t[i + 3] = 255;
  }
  updatePixels();
}

will save a few bytes :smile:

2 Likes

I was able to shrink my sketch version to merely 402 (386) characters within 80 columns. :shrimp:

Dunno if that’s enough for a tweet, but you can take a look at it running online: :bird:

Also leaving a copy of it in this forum: :copyright:

R=42,G=84,B=128,V=600,S=32
I=Int32Array,A=new I(V*V),K=new I(B),f=0
setup=_=>{createCanvas(V,V)
pixelDensity(1).loadPixels(s=sin)
p=new I(pixels.buffer)
for(y=0;y<V;++y)for(x=0;x<V;)A[V*y+x]=B/4*(s(x/S)+cos(y/S)+s(sqrt(x*x+++y*y)/S))
for(i=0;i<B;)u=B*s(.12*i),
K[i]=color(R+u,G+B*s(PI/50*i+++.79),B+u).levels.reduce((a,v,i)=>v<<i*8|a,0)}
draw=_=>{++f
for(i=0;i<V*V;)p[i]=K[f+A[i++]&B-1]
updatePixels()}
3 Likes

That’s really nice. I still have to dive in to this to understand your code. BTW You might want to include “pixelDensity(1)”, otherwise, at least my computer, the canvas is not correctly filled. Twitter allows only 280 characters so it’s too big. I am working on a plasma code without the use of arrays to shorten the code and it works fine in java, but in p5.js it’s terribly slow. I don’t know if it’s possible.

float x,a,t,y,b,d,s,c,p=0;void setup(){size(500, 500);colorMode(HSB,180);loadPixels();}void draw(){p++;for(int u=0;u<500;u++){x=u*0.02;a=sin(x);for(int t=0;t<500;t++){y=t*0.03;b=sin(y);d=sin((x+y+p*.1)/2);s=(a+b+d)/3;c=s*128+128;pixels[u+t*500]=color(c,255-c,255);}}updatePixels();}

Ru allowed to split the same code in multiple posts? :scissors:

P.S.: Added pixelDensity(1) to the code. Code increased from 386 to 402 characters. =(

1 Like

I don’t know what you exactly mean by that. I am referring to the https://twitter.com/tweetprocessing page that states as requirment:

Please post the Processing code that fits in one tweet with the hashtag #tweet Processing and the execution result!

Diff. from Processing Java, where color is just a 32bit value, p5js creates a heavy p5.Color object every time we invoke color(), fill(), stroke(), etc.

As a workaround we can pre-create a palette[] array filled w/ p5.Color objects.

For pixels[] in particular, we can even pre-create an array of 32bit values, as I did on my sketch version.

1 Like

I understand, but now I am trying to avoid arrays to shorten the code length. The last java code I posted fits in a tweet.

Python Mode bonus. However run it offline. It barely runs online:

2 Likes

I don’t know Python, but honestly this speed is kind of disappointing!