1

I am writing a program to generate some orbital images. I decided to use js because I have a fair amount of experience with it and its fast to write for prototypes. The issue is that when I go to use ctx.putImageData it is rotated by a -45° angle and the image is stretched. This is the actually important code:

  genImg(){
    let index = 0;
    for(let y = 0; y <= 400; y++){
      for(let x = 0; x <= 400; x++){
        let lx = (x - 200)*this.step;
        let ly = (200 - y)*this.step;
        this.points.push(this.value(Math.sqrt(Math.pow(lx, 2) + Math.pow(ly, 2)), this.t, Math.atan2(lx,ly))[0]);
      }
    }
    let sclFct = 1000/Math.max(...this.points);
    for(let i = 0; i<= 160000; i++){
      let val = Math.round(this.points[i]*sclFct);
      this.imgDataArr[index] = cmap[val*4]; // R value        
      this.imgDataArr[index + 1] = cmap[val*4 + 1]; // G value
      this.imgDataArr[index + 2] = cmap[val*4 + 2]; // B value
      this.imgDataArr[index + 3] = 255; // A value
      index += 4; 
    }
    let imgData = new ImageData(this.imgDataArr, 400, 400);
    ctx.putImageData(imgData, 0, 0);
  }

The Full code is here. Just off the bat I should mention this is all written in a class. The html is just a canvas element in the <main></main> area that is 400px by 400px. The CSS is just to center everything and give the canvas a border. The function this.value(r,t,p) takes the values r(radius), t(theta) and p(phi). Theta is a constant and radius and phi are calculated from (x,y) cords (see line 7 of the prior code).
enter image description here
In the image you can see where there is the diagonal. The black should be in the center with the other colors radiating out. So far I have tried a 45° rotation to the atan2 function, messing with the css and trying to add a rotation, and rotating the ctx element in code (i.e. using ctx.rotate(Math.PI/4)). This all is very strange to me because I have other projects where I have used the same method without issue. Any ideas would be wonderfull!

2
  • 1
    It looks like there is a discontinuity between the number of pixels in the buffer and the number of pixels reported to the ImageData. For instance, if the buffer is sized for 400x400 pixels, it should have 160000 color values. However, your loop condition is i <= 160000 inclusive, meaning it loops over 160001 color values. This could mean your buffer is "one pixel too wide", causing each row to be shifted left by 1 pixel when "squeezed" into the smaller ImageData, and at scale this looks like a 45 degree skew. Commented Sep 5 at 21:38
  • Simple answer... use < 400 (0 - 399 is 400px) rather than <= 400 (0-400 is 401px) in the two for loops creating the points array Commented Sep 6 at 17:17

1 Answer 1

3

There is a discontinuity between the dimensions of the buffer and the coordinates you are iterating over. For example, here's the same bug reproduced with a smaller 8x8 buffer:

const buf = new Uint8ClampedArray(res * res * 4);
let index = 0;
for (let y = 0; y <= res; y++) {
  for (let x = 0; x <= res; x++) {
    const r = Math.round(x * (255 / res));
    const g = Math.round(y * (255 / res));
    buf[index++] = r;
    buf[index++] = g;
    buf[index++] = 0;
    buf[index++] = 255;
  }
}

skewed color map

As you can see, the same exact skewing is happening here. Just as in your code, I have introduced an off-by-one error that causes the code to attempt to draw more pixels than expected. Due to the inclusive loop condition <= res, it visits res + 1 rather than res pixels on each axis. The last pixel of the first row becomes the first pixel of the second row, et cetera. The fix is as simple as changing the conditions to x < res and y < res respectively:

const buf = new Uint8ClampedArray(res * res * 4);
let index = 0;
for (let y = 0; y < res; y++) {
  for (let x = 0; x < res; x++) {
    const r = Math.round(x * (255 / res));
    const g = Math.round(y * (255 / res));
    buf[index++] = r;
    buf[index++] = g;
    buf[index++] = 0;
    buf[index++] = 255;
  }
}

fixed color map

In other programming languages, writing to the buffer out of bounds like this will throw an error. However in JavaScript assigning a value with this syntax typically means that you are declaring a new property on the object, and typed arrays are a special case. JS chooses to simply ignore the attempt to create new numerical properties on typed arrays. If we change this code to instead use TypedArray#set, it would perform a strict bounds check and using the buggy loop condition would throw as expected.

const buf = new Uint8ClampedArray(res * res * 4);
for (let y = 0; y < res; y++) {
  for (let x = 0; x < res; x++) {
    const r = Math.round(x * (255 / res));
    const g = Math.round(y * (255 / res));
    buf.set([ r, g, 0, 255 ], ((y * res) + x) << 2);
  }
}

This all is very strange to me because I have other projects where I have used the same method without issue. Any ideas would be wonderfull!

While the loop condition in this project is also buggy, it does not manifest as you correctly calculate the color of each pixel based on the position that pixel ends up in the image data. As a result, you paint too many pixels, but the color of those pixels is unaffected by the bug.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.