Html5 canvas graph fill on hover - javascript

I'm drawing a graph on a html 5 canvas tag from a array with numbers like
arr = [6,3,16,6,53,1,3,54,67,6,3,21,6,49,7,8,31,66,51,32,56,49,4,78,9,65,43,1,3,54,67,6,3];
These numbers will be the height of the rectangle that is drawn on the canvas and it will be filled white with a transparent background;
for (var i = 0; i < arr.length; i += 1) {
ctx.fillStyle = "#ffffff";
// Fill rectangle with gradient
ctx.fillRect(
arr[i] * 10,
c_height - arr[i],
8,
arr[i]
);
}
Users can hover these rectangles and then see some more data.
I can make them change color but if there are to many rectangles the site laggs a little bit, so my question is if it is possible to make some kind of big horizontal rectangle that will mask(white rectangles) without filling the transparent background?

1) You can define the array as a typed array instead:
var arr = new Uint8Array([6,3,16,6,53,1,3,...,3]);
Just make sure the type (here unsigned 8-bit) fits the values. If you have higher values than 255 then use a 16-bit, or 32-bit, if floating point use Float32Array and so on.
2) If the color is the same don't set the fill style inside the loop. fillStyle is rather expensive as it has to parse the string and convert it to the color it defines.
3) use path to add the rectangle to, defining and filling each time is slower than to define all rects, then fill all at the same time outside the loop.
4) use a smarter for-loop by using the array entry as a conditional statement as well. Not only is this faster in itself but by storing the array entry to a value will be faster too as JS does not have to look up an array entry every time you use arr[i]:
ctx.fillStyle = "#ffffff"; // set fill style outside loop
ctx.beginPath(); // make sure we use a clean path
for (var i = 0, a; a = arr[i]; i++) { // fetch item and use as cond. for loop
ctx.rect(a * 10, c_height - a, 8, a); // add rect to path, but not fill yet
}
ctx.fill(); // fill all rects with fillstyle
Hope this helps!

Related

Subtract opacity instead of multiplying

I'm trying to make it appear as though movement on my <canvas> creates motion trails. In order to do this, instead of clearing the canvas between frames I reduce the opacity of the existing content by replacing a clearRect call with something like this:
// Redraw the canvas's contents at lower opacity. The 'copy' blend
// mode keeps only the new content, discarding what was previously
// there. That way we don't have to use a second canvas when copying
// data
ctx.globalCompositeOperation = 'copy';
ctx.globalAlpha = 0.98;
ctx.drawImage(canvas, 0, 0);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
However, since setting globalAlpha multiplies alpha values, the alpha values of the trail can approach zero but will never actually reach it. This means that graphics never quite fade, leaving traces like these on the canvas that do not fade even after thousands of frames have passed over several minutes:
To combat this, I've been subtracting alpha values pixel-by-pixel instead of using globalAlpha. Subtraction guarantees that the pixel opacity will reach zero.
// Reduce opacity of each pixel in canvas
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Iterates, hitting only the alpha values of each pixel.
for (let i = 3; i < data.length; i += 4) {
// Use 0 if the result of subtraction would be less than zero.
data[i] = Math.max(data[i] - (0.02 * 255), 0);
}
ctx.putImageData(imageData, 0, 0);
This fixes the problem, but it's extremely slow since I'm manually changing each pixel value and then using the expensive putImageData() method.
Is there a more performant way to subtract, rather than multiplying, the opacity of pixels being drawn on the canvas?
Unfortunately there is nothing we can do about it except from manually iterating over the pixels to clear low-value alpha pixels like you do already.
The problem is related to integer math and rounding (more details at this link, from the answer).
There are blending modes such as "luminosity" (and to a certain degree "multiply") which can be used to subtract luma, the problem is it works on the entire surface contrary to composite modes which only works on alpha - there is no equivalent in composite operations. So this won't help here.
There is also a new luma mask via CSS but the problem is that the image source (which in theory could've been manipulated using for example contrast) has to be updated every frame and basically, the performance would be very bad.
Workaround
One workaround is to use "particles". That is, instead of using a feedback-loop instead log and store the path points, then redraw all logged points every frame. Using a max value and reusing that to set alpha can work fine in many cases.
This simple example is just a proof-of-concept and can be implemented in various ways in regards to perhaps pre-populated arrays, order of drawing, alpha value calculations and so forth. But I think you'll get the idea.
var ctx = c.getContext("2d");
var cx = c.width>>1, cy = c.height>>1, r = c.width>>2, o=c.width>>3;
var particles = [], max = 50;
ctx.fillStyle = "#fff";
(function anim(t) {
var d = t * 0.002, x = cx + r * Math.cos(d), y = cy + r * Math.sin(d);
// store point and trim array when reached max
particles.push({x: x, y: y});
if (particles.length > max) particles.shift();
// clear frame as usual
ctx.clearRect(0,0,c.width,c.height);
// redraw all particles at a log. alpha, except last which is drawn full
for(var i = 0, p, a; p = particles[i++];) {
a = i / max * 0.6;
ctx.globalAlpha = i === max ? 1 : a*a*a;
ctx.fillRect(p.x-o, p.y-o, r, r); // or image etc.
}
requestAnimationFrame(anim);
})();
body {background:#037}
<canvas id=c width=400 height=400></canvas>

Canvas - Fill a shape created with multiple paths

What I want to do
I would like to draw a custom shape (for example a simple rectangle) which has different colors for each edge. I can do it with four paths, it works like a charm. BUT, in this way, it seems I can not fill the shape.
Trying the other way, I can draw the shape with one path and fill it, BUT in this case, I can not use different colors for the edges, because the last fillStyle will override the previous ones, even if I stroke the subpaths individually.
Is it possible to mix the two, by coloring subpaths individually, or by filling a shape consisting multiple paths?
Use different "layers" on the canvas, one for the filled with color shape, and a new one for each color path you have, z-index doesn't work on canvas, just make sure you draw what goes underneath first, and just wrap everything on a group <g> tag to make it easier to manipulate
After some experiment, I managed to solve my problem. It is not an ideal solution, because it has some overhead, but it works fine.
In the beginning of the drawing operation, I store the target coordinates in an array, and draw the whole stuff again and again. Each run is a new path. With .globalCompositeOperation = "destination-over" I can draw the lines under the existing ones, so each line can have a different color.
At the end of the drawing operation, the array contains all the coordinates of the shape, so the .fill() method can fill the path.
I hope it can help others:
// get the canvas context
var ctx = document.getElementById("myCanvas").getContext("2d");
// init shape array
var shape = [];
shape.push({
x: 0,
y: 0
}); // or any other starting point
// let's try
draw(20, 20);
draw(40, 40);
draw(60, 60);
// this is how we draw
function draw(x, y) {
// this is important
// see: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
ctx.globalCompositeOperation = "destination-over";
// this is just to be more visible
ctx.lineWidth = 10;
// get a random color
ctx.strokeStyle = myRandomColor();
// save target coordinates
shape.push({
x: x,
y: y
});
// reset the path
ctx.beginPath();
// jump to the start point
ctx.moveTo(shape[0].x, shape[0].y);
// draw the whole stuff
for (var i = 0; i < shape.length; i++) {
ctx.lineTo(shape[i].x, shape[i].y);
}
ctx.stroke();
}
function myRandomColor() {
var colors = ["red", "green", "blue", "yellow", "pink"];
var rand = Math.round(Math.random() * 5);
return colors[rand];
}
<canvas id="myCanvas"></canvas>

Canvas won't draw rectangle after for loop in function

I'm trying to create a news ticker that renders text in little square "pixels". I say "pixels" because they look like pixels but the actual squares being displayed are bigger than just 1px.
So far I can get all the letters rendered from an object that I built which contains the pixel coordinates for each rectangle. Code can be seen here:
https://jsfiddle.net/9u3ez6gu/2/
The letters render correctly, but after my for loop (see code sample below) I'm trying to create a lime colored space between each letter. This lime space never gets rendered no matter what I do. Here is the for loop and the code I'm using to render the space. Does anyone know why canvas will not let me draw the lime colored rectangle?
for (i = 0; i <= inv[letter].length; i++) {
var x = inv[letter][i][1] * full;
var y = inv[letter][i][0] * full;
context.beginPath();
context.rect(x, y, unit, unit);
context.fillStyle = 'black';
context.closePath();
context.fill();
}// End for loop
//Add a gap between letters
var gapx = full * 5;
context.beginPath();
context.rect(gapx, 0, full, full);
context.fillStyle = 'lime';
context.closePath();
context.fill();
}// End function
Your for loop goes one iteration too far:
for (i = 0; i <= inv[letter].length; i++) {
Should be <, not <=. Keep your developer console open!
Also, i should be declared with var, either at the top of the function or in the for loop header itself. In this case it (probably) doesn't matter, but it's good to get in the habit. If you don't declare the variable, it'll be global. If another of your functions also fails to declare another i, they'll be the same thing, and weird bugs can result.
If you put
"use strict";
at the top of your <script> blocks or the very top of each function, the parser (in modern browsers) will flag assignments to implicit global variables as errors.

How to scale alpha values in a canvas?

What's the best way to scale alpha values in a canvas?
The first problem I'm trying to solve is drawing a sprite that has intrinsic low alpha values. I want to draw it 3-4 times brighter than it really is. Currently I'm just drawing it 4 times in the same spot. (I cannot edit the image file and globalAlpha doesn't go above 1)
The second problem I'm trying to solve is drawing the boundary of multiple overlapping sprites. The sprites are circular but with squiggles. I figured I'd use this method combined with globalCompositeOperation = 'destination-out', but for that I need to maximize the alpha values for the second drawing.
As an option to markE's answer - you can simply scale the alpha channel directly.
I would only recommend this approach as a part of a pre-processing stage and not for use every time you need to use a sprite as iterating the buffer this way is a relatively slow process.
LIVE DEMO HERE
Assuming you already have the sprite in a canvas and know its position:
/// get the image data and cache its pixel buffer and length
var imageData = context.getImageData(x, y, width, height);
var data = imageData.data;
var length = data.length;
var i = 0;
var scale = 4; /// scale values 4 times. This may be a fractional value
/// scale only alpha channel
for(; i < length; i += 4) {
data[i + 3] *= scale;
}
context.putImageData(imageData, x, y);
The good thing with the Uint8ClampedArray which the canvas is using clamps and rounds the values for you so you do not need to check lower or upper bounds, nor convert the value to integer - the internal code do all this for you.
You can "brighten" an rgba color by flattening it to rgb and then increasing the rgb component values.
Convert the rgba value to rgb, also taking the background color into effect.
Increase the resulting red,green,blue values by a percentage to "brighten" the color.
Here's a function to do that (disclaimer: untested code here!):
function brighten(RGBA,bg,pct){
// convert rgba to rgb
alpha = 1 - RGBA.alpha/255;
red = Math.round((RGBA.alpha*(RGBA.red/255)+(alpha*(bg.red/255)))*255);
green = Math.round((RGBA.alpha*(RGBA.green/255)+(alpha*(bg.green/255)))*255);
blue = Math.round((RGBA.alpha*(RGBA.blue/255)+(alpha*(bg.blue/255)))*255);
// brighten the flattened rgb by a percentage (100 will leave the rgb unaltered)
redBright=parseInt( Math.min(255,red*pct/100) );
greenBright=parseInt( Math.min(255,green*pct/100) );
blueBright=parseInt( Math.min(255,blue*pct/100) );
return({red:redBright,green:greenBright,blue:blueBright});
}

Manual anti-aliasing algorithm in javascript for HTML5 Canvas

I want to do some manual anti-aliasing on some text on a canvas. I know how to iterate over the image / colour data, but not exactly how to anti-alias.
From googling around a bit it seems like to do anti-aliasing I need to have an original image which I use as my sample, then pass over the colour data, then for each pixel take an average of the surrounding pixels, then copy this new value into the data for my anti-aliased image.
The bit I'm not sure about is exactly how to 'take an average' of the surrounding pixels.
I have done a jsFiddle to demostrate what I have done so far. As you will see I am copying the image data from the original canvas, making it negative, then putting it into the second canvas.
If I am being more specific in what I am struggling with, it is how exactly do you figure out what the surrounding pixels are in a loop which only has one iterator? And also is the average just a case of adding the nearest pixels colour vals to the current pixels vals, then dividing by the number of pixels?
This is the loop in which I wish to manipulate the data:
var imgData = originalContext.getImageData(0, 0, width, height);
var aliasedData = originalContext.createImageData(width, height);
aliasedData.data.set(imgData.data);
for (var i = 0; i < imgData.data.length; i += 4) {
// just make the data negative to show something is happening
aliasedData.data[i] = 255 - imgData.data[i];
aliasedData.data[i + 1] = 255 - imgData.data[i + 1];
aliasedData.data[i + 2] = 255 - imgData.data[i + 2];
// need to get an average of surrounding pixels here
}
aliasedContext.putImageData(aliasedData, 0, 0);

Categories

Resources