Canvas - flip half the image - javascript

I have some image data in canvas, and now I need to take the left half of the image, flip it and apply it to the right, like a mirror effect.
Example, from this:
To this:
I got this far (I have the image data ready):
ctx.drawImage(this, 0, 0, 960, 540);
var imgData = ctx.getImageData(0,0,960,540);
// loop through the data and apply mirror ??
Width & height is known. Any ideas?

Loop through the image data
If the current pixel is in the left half of the image, copy it to a position on the right:
for(var y = 0; y < height; y++) {
for(var x = 0; x < width / 2; x++) { // divide by 2 to only loop through the left half of the image.
var offset = ((width* y) + x) * 4; // Pixel origin
// Get pixel
var r = data[offset];
var g = data[offset + 1];
var b = data[offset + 2];
var a = data[offset + 3];
// Calculate how far to the right the mirrored pixel is
var mirrorOffset = (width - (x * 2)) * 4;
// Get set mirrored pixel's colours
data[offset + mirrorOffset] = r;
data[offset + 1 + mirrorOffset] = g;
data[offset + 2 + mirrorOffset] = b;
data[offset + 3 + mirrorOffset] = a;
}
}
I haven't tested this, but it should (More-or less) work, or at least give you an idea of how to do it.

Related

How to change size of pixel square using createImageData in html canvas? [duplicate]

I have a canvas in my webpage; I create a new Image data in this canvas then I modify some pixel through myImgData.data[] array. Now I would like to scale this image and make it bigger. I tried by scaling the context but the image remains small. Is it possible to do this?
Thanks
You could draw the imageData to a new canvas, scale the original canvas and then draw the new canvas to the original canvas.
Something like this should work:
var imageData = context.getImageData(0, 0, 100, 100);
var newCanvas = $("<canvas>")
.attr("width", imageData.width)
.attr("height", imageData.height)[0];
newCanvas.getContext("2d").putImageData(imageData, 0, 0);
context.scale(1.5, 1.5);
context.drawImage(newCanvas, 0, 0);
Here's a functioning demo http://jsfiddle.net/Hm2xq/2/.
I needed to do it without the interpolation that putImageData() causes, so I did it by scaling the image data into a new, resized ImageData object. I can't think of any other time I've thought that using 5 nested for loops was a good idea:
function scaleImageData(imageData, scale) {
var scaled = c.createImageData(imageData.width * scale, imageData.height * scale);
for(var row = 0; row < imageData.height; row++) {
for(var col = 0; col < imageData.width; col++) {
var sourcePixel = [
imageData.data[(row * imageData.width + col) * 4 + 0],
imageData.data[(row * imageData.width + col) * 4 + 1],
imageData.data[(row * imageData.width + col) * 4 + 2],
imageData.data[(row * imageData.width + col) * 4 + 3]
];
for(var y = 0; y < scale; y++) {
var destRow = row * scale + y;
for(var x = 0; x < scale; x++) {
var destCol = col * scale + x;
for(var i = 0; i < 4; i++) {
scaled.data[(destRow * scaled.width + destCol) * 4 + i] =
sourcePixel[i];
}
}
}
}
}
return scaled;
}
I hope that at least one other programmer can copy and paste this into their editor while muttering, "There but for the grace of god go I."
I know it's an old subject, but since people like may find it useful, I add my optimization to the code of rodarmor :
function scaleImageData(imageData, scale) {
var scaled = ctx.createImageData(imageData.width * scale, imageData.height * scale);
var subLine = ctx.createImageData(scale, 1).data
for (var row = 0; row < imageData.height; row++) {
for (var col = 0; col < imageData.width; col++) {
var sourcePixel = imageData.data.subarray(
(row * imageData.width + col) * 4,
(row * imageData.width + col) * 4 + 4
);
for (var x = 0; x < scale; x++) subLine.set(sourcePixel, x*4)
for (var y = 0; y < scale; y++) {
var destRow = row * scale + y;
var destCol = col * scale;
scaled.data.set(subLine, (destRow * scaled.width + destCol) * 4)
}
}
}
return scaled;
}
This code uses less loops and runs roughly 30 times faster. For instance, on a 100x zoom of a 100*100 area this codes takes 250 ms while the other takes more than 8 seconds.
You can scale the canvas using the drawImage method.
context = canvas.getContext('2d');
context.drawImage( canvas, 0, 0, 2*canvas.width, 2*canvas.height );
This would scale the image to double the size and render the north-west part of it to the canvas. Scaling is achieved with the third and fourth parameters to the drawImage method, which specify the resulting width and height of the image.
See docs at MDN https://developer.mozilla.org/en-US/docs/DOM/CanvasRenderingContext2D#drawImage%28%29
#Castrohenge's answer works, but as Muhammad Umer points out, it messes up the mouse coordinates on the original canvas after that. If you want to maintain the ability to perform additional scales (for cropping, etc.) then you need to utilize a second canvas (for scaling) and then fetch the image data from the second canvas and put that into the original canvas. Like so:
function scaleImageData(imageData, scale){
var newCanvas = $("<canvas>")
.attr("width", imageData.width)
.attr("height", imageData.height)[0];
newCanvas.getContext("2d").putImageData(imageData, 0, 0);
// Second canvas, for scaling
var scaleCanvas = $("<canvas>")
.attr("width", canvas.width)
.attr("height", canvas.height)[0];
var scaleCtx = scaleCanvas.getContext("2d");
scaleCtx.scale(scale, scale);
scaleCtx.drawImage(newCanvas, 0, 0);
var scaledImageData = scaleCtx.getImageData(0, 0, scaleCanvas.width, scaleCanvas.height);
return scaledImageData;
}
Nowadays, the best way to render a scaled ImageData object is generally to create an ImageBitmap from it.
All modern browsers finally do support it.
This will use a faster path to render the ImageData's content to a bitmap readily available to be painted by drawImage(), theoretically a lot faster than the second best option of putting the ImageData on a secondary <canvas> and redraw that <canvas>.
The main catch-up is that createImageBitmap() is asynchronous*, and thus it may not fit in an animation frame very well.
(async () => {
// here I'll just make some noise in my ImageData
const imagedata = new ImageData(100, 100);
const arr = new Uint32Array(imagedata.data.buffer);
for( let i = 0; i < arr.length; i++ ) {
arr[i] = Math.random() * 0xFFFFFF + 0xFF000000;
}
// now to render it bigger
const bitmap = await createImageBitmap(imagedata);
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false; // keep pixel perfect
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
})();
<canvas width="1000" height="1000"></canvas>
* Technically, only Firefox does implement createImageBitmap(<ImageData>) asynchronously, Chrome and Safari will resolve the returned Promise synchronously, and there, it's safe to use it in an animation frame: https://jsfiddle.net/vraz3xcg/

How to make my drawing of an exponential function to grow within the canvas?

I have an exponential curve made using p5.js that draws itself over time as follow :
However I am trying to have it responsive in such a way that as the curve grows, it would always be fully visible inside the canvas.
Here are screenshots of what I mean to achieve :
Working examples found on a website
As you can see on this example, once it reaches the edges of the canvas, it kind of "zooms out" in order for the canvas to fit the whole curve, as a result since it never leaves the canvas the curve bends the more it grows.
To try and achieve this, I explored using scale scale(x / 100, y / 100) to be triggered once the limits of the canvas are reached. That way the canvas starts scaling proportionally to the curve's expansion.
However, using this method does not solve my problem because it seems that reducing the scaling while adding points to the curve does not make the curve grow anymore.
Here is my current (updated) code :
var y = 49;
var x = 0;
var inc = 0.02;
var translateY;
let createTopY = false;
let createTopX = false;
var topY;
var percentageIncY = 100;
var perecntageIncX = 100;
// Scaling
var scaleX = 1
var scaleY = 1
function setup() {
createCanvas(400, 200);
background(20, 25, 29)
}
function draw() {
frameRate(20)
// Set curve history for continuous lines
let prev_x = x
let prev_y = y
// Recreate canvas position (origin) based on Scale Y (zoom)
translateY = height * scaleY
translate(0, (height - translateY) + 49 ) // 49 is the initial y value
scale(scaleX, scaleY)
// Exponential curve values
x = x + 5 // Approximate
y = y + y * inc
// Draw curve
strokeWeight(3)
stroke(229, 34, 71);
line(prev_x, height - prev_y, x, height - y);
// Create topY when top at scale(1) is reached
if (createTopY !== true) checkInt(y)
if (createTopX !== true) checkInt(x)
//-------------- SCALING ---------------//
// From TopX, decrease scale exponentialy
if (x > width - 20) { // Temporarily set to 20 (50-30) to better visualize
// The increased value of x in % starting at topX
percentageIncX = (x * 100) / (width - 20)
// Decrease scaleX exponentialy
scaleX = 100 / percentageIncX
print(
"% increase X: " +
percentageIncX
)
}
// From topY, decrease scale exponentialy
if (y > height + 20) { // Temporarily set to 20 (50-30) to visualize
// The increased value of y in % starting at topY
percentageIncY = (y * 100) / (height + 20) // Temporarily set to 20 (50-30) to better visualize
// Decrease scaleY exponentialy
scaleY = 100 / percentageIncY
print(
"% increase Y: " +
percentageIncY
)
}
//-------------------------------------//
}
const checkInt = (prop) => {
const parsed = int(prop)
if (parsed > height + 20) { // Temporarily set to 20 (50-30) to better visualize
createTopY = true
createTopX = true
topY = y
print('createTopY is: ' + createTopY)
print('createTopX is: ' + createTopX)
print("---START SCALING---")
print('starting at ' + y)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>
Use frameRate() to control to the number of frames to be displayed every second. Stitch the curve with line segments (line()) instead of drawing with single dots (ellipse()) to draw a curve without interruptions.
var y = 1;
var x = 0;
var inc = 0.01;
function setup() {
createCanvas(400, 400);
background(100)
frameRate(100)
}
function draw() {
let prev_x = x;
let prev_y = y;
x = x + 0.5
y = y + y * inc;
noFill();
stroke(255, 0, 0, 255);
strokeWeight(3);
line(prev_x, height-prev_y, x, height-y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js"></script>

Nested For Loop for music visualiser not iterating (p5.js)

I am trying to create some scales measuring sound frequency for a music visualiser project. They are meant to display 4 different frequencies ( bass, lowMid, highMid and treble in a 2x2 grid pattern. I'm nearly there I have my rectangles but the needle which measures and shows the frequency itself is only iterating for the top row x and not the bottom row. I'm pretty new to JavaScript so I'm sure it could be something very simple that I'm missing.
// draw the plots to the screen
this.draw = function() {
//create an array amplitude values from the fft.
var spectrum = fourier.analyze();
//iterator for selecting frequency bin.
var currentBin = 0;
push();
fill('#f0f2d2');
//nested for loop to place plots in 2*2 grid.
for(var i = 0; i < this.plotsDown; i++) {
for(var j = 0; j < this.plotsAcross; j++) {
//calculate the size of the plots
var x = this.pad * j * 10;
var y = height/20 * i * 10;
var w = (width - this.pad) / this.plotsAcross;
var h = (height - this.pad) / this.plotsDown;
//draw a rectangle at that location and size
rect(x, y, w, h);
//add on the ticks
this.ticks((x + w/2), h, this.frequencyBins[i])
var energy = fourier.getEnergy(this.frequencyBins[currentBin]);
//add the needle
this.needle(energy, (x + w/2), h)
currentBin++;
}
}
pop();
};
Please try changing
this.ticks((x + w/2), h, this.frequencyBins[i])
to
this.ticks((x + w/2), y + h, this.frequencyBins[i]),
and changing
this.needle(energy, (x + w/2), h)
to
this.needle(energy, (x + w/2), y + h).
This should work.

Linear interpolation on canvas

I'm trying to understand how image resampling methods work. I've read/watched several pages/videos and I think I got the idea. However, I couldn't find any working example on how to implement it. So I thought I should start with the basics: nearest neighbor resampling on 1D.
This was very straightforward and I think I got it. JSFiddle Demo.
function resample() {
var widthScaled = Math.round(originalPixels.width * scaleX);
var sampledPixels = context.createImageData(widthScaled, originalPixels.height);
for (var i = 0; i < sampledPixels.data.length; i+=4) {
var position = index2pos(sampledPixels, i);
var origPosX = Math.floor(position.x / scaleX);
var origColor = getPixel(originalPixels, origPosX, position.y);
setPixel(sampledPixels, position.x, position.y, origColor);
}
loadImage(context, sampledPixels);
}
Next, I moved on to linear interpolation. Thought it'd be simple too, but I'm having problems. First, how do I deal with the last pixel (marked red)? It has only one neighboring pixel. Second, my result is too sharp when compared to Photoshop's. Is my method flawed, or is PS doing some extra work? JSFiddle Demo.
function resample() {
var sampledPixels = context.createImageData(originalPixels.width * scaleX, originalPixels.height);
for (var i = 0; i < sampledPixels.data.length; i+=4) {
var position = index2pos(sampledPixels, i);
var origPosX = position.x / scaleX;
var leftPixelPosX = Math.floor(origPosX);
var rightPixelPosX = Math.ceil(origPosX);
var leftPixelColor = getPixel(originalPixels, leftPixelPosX, position.y);
var rightPixelColor = getPixel(originalPixels, rightPixelPosX, position.y);
var weight = origPosX % 1;
var color = mix(leftPixelColor[0], rightPixelColor[0], weight);
color = [color, color, color, 255];
setPixel(sampledPixels, position.x, position.y, color);
}
loadImage(context, sampledPixels);
}
function mix(x, y, a) {
return x * (1 - a) + y * a;
}
Linear interpolation of pixels
There is no real right and wrong way to do filtering, as the result is subjective and the quality of the result is up to you, Is it good enough, or do you feel there is room for improvement.
There are also a wide variety of filtering methods, nearest neighbor, linear, bilinear, polynomial, spline, Lanczos... and each can have many variations. There are also factors like what is the filtering output format; screen, print, video. Is quality prefered over speed, or memory efficiency. And why upscale when hardware will do it for you in real-time anyways.
It looks like you have the basics of linear filtering correct
Update Correction. Linear and bilinear refer to the same type of interpolation, bilinear is 2D and linear is 1D
Handling the last Pixel
In the case of the missing pixel there are several options,
Assume the colour continues so just copy the last pixel.
Assume the next pixel is the background, border colour, or some predefined edge colour.
Wrap around to the pixel at the other side (best option for tile maps)
If you know there is a background image use its pixels
Just drop the last pixel (image size will be 1 pixel smaller)
The PS result
To me the PhotoShop result looks like a form of bilinear filtering, though it should be keeping the original pixel colours, so something a little more sophisticated is being used. Without knowing what the method is you will have a hard time matching it.
A spectrum for best results
Good filtering will find the spectrum of frequencies at a particular point and reconstruct the missing pixel based on that information.
If you think of a line of pixels not as values but as volume then a line of pixels makes a waveform. Any complex waveform can be broken down into a set of simpler basic pure tones (frequencies). You can then get a good approximation by adding all the frequencies at a particular point.
Filters that use this method are usually denoted with Fourier, or FFT (Fast Fourier Transform) and require a significant amount of process over standard linear interpolation.
What RGB values represent.
Each channel red, green, and blue represent the square root of that channel's intensity/brightness. (this is a close general purpose approximation) Thus when you interpolate you need to convert to the correct values then interpolate then convert back to the logarithmic values.
Correct interpolation
function interpolateLinear(pos,c1,c2){ // pos 0-1, c1,c2 are objects {r,g,b}
return {
r : Math.sqrt((c2.r * c2.r + c1.r * c1.r) * pos + c1.r * c1.r),
g : Math.sqrt((c2.g * c2.g + c1.g * c1.g) * pos + c1.g * c1.g),
b : Math.sqrt((c2.b * c2.b + c1.b * c1.b) * pos + c1.b * c1.b),
};
}
It is important to note that the vast majority of digital processing software does not correctly interpolate. This is in part due to developers ignorance of the output format (why I harp on about it when I can), and partly due to compliance with ye olde computers that struggled just to display an image let alone process it (though I don't buy that excuse).
HTML5 is no exception and incorrectly interpolates pixel values in almost all interpolations. This producing dark bands where there is strong hue contrast and darker total brightness for up and down scaled image. Once you notice the error it will forever annoy you as today's hardware is easily up to the job.
To illustrate just how bad incorrect interpolation can be the following image shows the correct (top) and the canvas 2D API using a SVG filter (bottom) interpolation.
2D linear interpolation (Bilinear)
Interpolating along both axis is done by doing each axis in turn. First interpolate along the x axis and then along the y axis. You can do this as a 2 pass process or a single pass.
The following function will interpolate at any sub pixel coordinate. This function is not built for speed and there is plenty of room for optimisation.
// Get pixel RGBA value using bilinear interpolation.
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgDat.width) * 4;
var i2 = (ix2 + iy1 * imgDat.width) * 4;
var i3 = (ix1 + iy2 * imgDat.width) * 4;
var i4 = (ix2 + iy2 * imgDat.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
for(i = 0; i < 3; i ++){
// interpolate x for top and bottom pixels
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const upScale = 4;
// usage
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const imgData2 = ctx.createImageData(ctx.canvas.width * upScale, ctx.canvas.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgdata2.width) * 4);
}
}
Example upscale canvas 8 times
The example uses the above function to upscale a test pattern by 8. Three images are displayed. The original 64 by 8 then, the computed upscale using logarithmic bilinear interpolation, and then using the canvas standard API drawImage to upScale (Using the default interpolation, bilinear) .
// helper functions create canvas and get context
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// iterators
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const upScale = 8;
var canvas1 = CImageCtx(64,8);
var canvas2 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
var canvas3 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y) | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgDat.width) * 4;
var i2 = (ix2 + iy1 * imgDat.width) * 4;
var i3 = (ix1 + iy2 * imgDat.width) * 4;
var i4 = (ix2 + iy2 * imgDat.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
// interpolate x for top and bottom pixels
for(i = 0; i < 3; i ++){
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const ctx = canvas1.ctx;
var cols = ["black","red","green","Blue","Yellow","Cyan","Magenta","White"];
doFor(8,j => eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(j*8+i,0,1,8)}));
eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(i * 8,4,8,4)});
const imgData = ctx.getImageData(0, 0, canvas1.width, canvas1.height);
const imgData2 = ctx.createImageData(canvas1.width * upScale, canvas1.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgData2.width) * 4);
}
}
canvas2.ctx.putImageData(imgData2,0,0);
function $(el,text){const e = document.createElement(el); e.textContent = text; document.body.appendChild(e)};
document.body.appendChild(canvas1);
$("div","Next Logarithmic upscale using linear interpolation * 8");
document.body.appendChild(canvas2);
canvas3.ctx.drawImage(canvas1,0,0,canvas3.width,canvas3.height);
document.body.appendChild(canvas3);
$("div","Previous Canvas 2D API upscale via default linear interpolation * 8");
$("div","Note the overall darker result and dark lines at hue boundaries");
canvas { border : 2px solid black; }

Canvas: draw lots of elements with a changing gradient (emulate angular gradient)

for this project http://biduleohm.free.fr/ledohm/ (sorry, the user interface is in french but the code is in english) I need an angular gradient but it doesn't exists in native so I've implemented it using a linear gradient on a line and I draw the lines more and more longer to form a triangle. The result is graphically OK but the speed isn't really good (1850 ms for 125 triangles). It's in the tab [RĂ©partition], it redraws if there is a keyup event on one of the inputs, don't be afraid of the apparent slowness, I've limited to maximum one redraw every 2000 ms.
Before I used a simple linear gradient on the whole triangle (but this doesn't match the reality) and the speed was OK, it draws thousands of triangles in less than a second. This function was used :
drawFrontLightForColor : function(x, y, w, h, color) {
var x2 = x - w;
var x3 = x + w;
var gradient = Distri.frontCanvas.createLinearGradient(x2, y, x3, y);
gradient.addColorStop(0, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
gradient.addColorStop(0.5, 'rgba(' + color + ', ' + (color == Distri.lightColors.cw ? Distri.lightCenterAlphaCw : Distri.lightCenterAlphaOther) + ')');
gradient.addColorStop(1, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
Distri.frontCanvas.fillStyle = gradient;
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x, y);
Distri.frontCanvas.lineTo(x2, (y + h));
Distri.frontCanvas.lineTo(x3, (y + h));
Distri.frontCanvas.lineTo(x, y);
Distri.frontCanvas.fill();
Distri.frontCanvas.closePath();
},
Then I switched to this function :
drawFrontLightForColor : function(x, y, w, h, centerColor, edgeColor) {
var ratio = w / h;
var tmpY;
var tmpW;
var x2;
var x3;
var gradient;
Distri.frontCanvas.lineWidth = 1;
for (var tmpH = 0; tmpH < h; tmpH++) {
tmpY = y + tmpH;
tmpW = Math.round(tmpH * ratio);
x2 = x - tmpW;
x3 = x + tmpW;
gradient = Distri.frontCanvas.createLinearGradient(x2, tmpY, x3, tmpY);
gradient.addColorStop(0, edgeColor);
gradient.addColorStop(0.5, centerColor);
gradient.addColorStop(1, edgeColor);
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x2, tmpY);
Distri.frontCanvas.lineTo(x, tmpY);
Distri.frontCanvas.lineTo(x3, tmpY);
Distri.frontCanvas.strokeStyle = gradient;
Distri.frontCanvas.stroke();
Distri.frontCanvas.closePath();
}
},
You can find the whole source here
I can't put the beginPath, stroke, closePath out of the loop because of the gradient which is changing every iteration (I've tried but it used the last gradient for every line (which, ironically, is identical to the first function...) which is understandable but not what I want).
I accept any advice (including redo the whole function and modify his caller to outsource some code) to improve the speed let's say 5x (ideally more).
I think you took the wrong way from the start : when doing so much changes of color, you have better operate at the pixel level.
So yes that could be with a webgl pixel shader, but you'll have to fight just to get the boilerplate running ok on all platform (or get a lib to do that for you).
And anyway there's a solution perfect for your need, and fast enough (a few ms) : use raw pixel data, update them one by one with the relevant function, then draw the result.
The steps to do that are :
- create a buffer same size as the canvas.
- iterate through it's pixel, keeping track of the x,y of the point.
- normalize the coordinates so they match your 'space'.
- compute the value for the normalized (x,y) out of all the data that you have.
- write a color (in my example i choose greyscale) out of that value.
- draw the whole buffer to canvas.
I did a jsfiddle, and here's the result with 4 data points :
fiddle is here :
http://jsfiddle.net/gamealchemist/KsM9c/3/
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
var width = canvas.width,
height = canvas.height;
// builds an image for the target canvas
function buildImage(targetCanvas, valueForXY, someData) {
var width = targetCanvas.width;
var height = targetCanvas.height;
var tempImg = ctx.createImageData(width, height);
var buffer = tempImg.data;
var offset = 0;
var xy = [0, 0];
function normalizeXY(xy) {
xy[0] = xy[0] / width ;
xy[1] = xy[1] / height;
}
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++, offset += 4) {
xy[0] = x; xy[1]=y;
normalizeXY(xy);
var val = Math.floor(valueForXY(xy, someData) * 255);
buffer[offset] = val;
buffer[offset + 1] = val;
buffer[offset + 2] = val;
buffer[offset + 3] = 255;
}
ctx.putImageData(tempImg, 0, 0);
}
// return normalized (0->1) value for x,y and
// provided data.
// xy is a 2 elements array
function someValueForXY(xy, someData) {
var res = 0;
for (var i = 0; i < someData.length; i++) {
var thisData = someData[i];
var dist = Math.pow(sq(thisData[0] - xy[0]) + sq(thisData[1] - xy[1]), -0.55);
localRes = 0.04 * dist;
res += localRes;
}
if (res > 1) res = 1;
return res;
}
var someData = [
[0.6, 0.2],
[0.35, 0.8],
[0.2, 0.5],
[0.6, 0.75]
];
buildImage(canvas, someValueForXY, someData);
// ------------------------
function sq(x) {
return x * x
}
In fact the GameAlchemist's solution isn't fast or I do something really wrong. I've implemented this algo only for the top view because the front view is much more complex.
For 120 lights the top view take 100-105 ms with the old code and it take 1650-1700 ms with this code (and moreover it still lacks a few things in the new code like the color for example):
drawTopLightForColor_ : function(canvasW, canvasD, rampX, rampY, rampZ, ledsArrays, color) {
function sq(x) {
return x * x;
}
var tmpImg = Distri.topCanvasCtx.createImageData(canvasW, canvasD);
var rawData = tmpImg.data;
var ledsArray = ledsArrays[color];
var len = ledsArray.length;
var i = 0;
for (var y = 0; y < canvasD; y++) {
for (var x = 0; x < canvasW; x++, i += 4) {
var intensity = 0;
for (var j = 0; j < len; j++) {
intensity += 2 * Math.pow(
sq((rampX + ledsArray[j].x) - x) +
sq((rampZ + ledsArray[j].y) - y),
-0.5
);
}
if (intensity > 1) {
intensity = 1;
}
intensity = Math.round(intensity * 255);
rawData[i] = intensity;
rawData[i + 1] = intensity;
rawData[i + 2] = intensity;
rawData[i + 3] = 255;
}
}
Distri.topCanvasCtx.putImageData(tmpImg, 0, 0);
},
Am I doing something wrong?

Categories