I'm attempting to implement the ability to draw text to the screen on a basic 2D plane, using only HTML Canvas and JavaScript.
I start by setting up the canvas.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
I have a keydown event listener that appends to an array whatever characters I enter.
let characters = []
document.onkeydown = function(event) {
if (event.key.length === 1) {
characters.append(event.key)
}
}
I want to draw each character onto the canvas.
let x,y = 0;
characters.forEach(char => {
ctx.beginPath()
ctx.font = "20px Arial"
ctx.fillText(char, x, y)
x += someValue // this needs to be calculated based on character pixel width
})
To avoid drawing all the characters at the same spot and overlapping, I have to increment the x coordinate by some value each iteration. This can't be a constant value, because characters differ in their pixel width ('O' is wider than 'I').
I need a way to know how much pixel space a character will take up, to determine what x coodinate to draw the subsequent character at.
I'm not looking for an HTML/JS specific answer. I want to know what the general computer science approaches are to rendering characters with correct spacing to a graphical context. i.e, how does HTML itself do it under the hood?
Related
I'm new to HTML5 Canvas and Javascript. I'm currently working on a HTML5 Canvas project (I wrote a separate question about it here and here).
The logic of the game is pretty simple: track the mouse position and if the mouse position exits the blue region, the game resets. The user starts on level 1 (function firstLevel). If their mouse position enters the red box region, they advance onto the next level (return secondLevel()). I did this previously by a hefty list of if statements that compared the mouse's x and y coordinates.
I've corrected the code to create a global constructor function I can reference in each of the level functions:
function Path(array, color) {
let path = new Path2D();
path.moveTo(array[0][0], array[0][1]);
for (i = 1; i < array.length; i++) {
path.lineTo(array[i][0], array[i][1]);
}
path.lineTo(array[0][0], array[0][1]);
return path;
}
And its referenced in the level functions as such:
// In the individual levels, put code that describes the shapes locally
var big = [[350,200], [900,200], [900,250], [700,250], [600,250], [600,650], [350,650]];
var blue = Path(big);
var small = [[900,200], [900,250], [850,250], [850, 200], [900, 200]];
var red = Path(small);
// 2. create cursor
c.beginPath();
c.rect(mouseX, mouseY, mouseWidth, mouseHeight);
c.fillStyle = "#928C6F";
c.fill();
// Local code that draws shapes:
c.beginPath()
c.fillStyle = '#C1EEFF';
c.fill(blue);
c.beginPath()
c.fillStyle = "#FF4000";
c.fill(red);
I want to know how can I check for the mouse position so it can run those conditional statements using the isPointInPath method? I now have a reference (refs. blue and red) for the Canvas shape, so I'm hoping there's a way I can check if the mouse's x and y position is a point that is inside the shape/path.
Link to my project: https://github.uconn.edu/pages/ssw19002/dmd-3475/final-project/maze-page-1.html
Source code: https://github.uconn.edu/ssw19002/dmd-3475/tree/master/final-project
You could use the MouseEvent.offsetX and MouseEvent.offsetY properties of a mouse event such as mousemove. (You would need to add an event listener for mouse events to the canvas element of course.)
Then use the x and y offset values obtained to call isPointInPath like
ctx.isPointInPath(path, x, y)
where path is a return value from function Path.
While MDN lists the offset properties as "experimental" they seem to have been around since IE9 at least.
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>
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});
}
I'm trying to make a little game just for the fun of it where i have some collision problems.
I have a player drawn on a canvas and some blocks (16 x 16px) drawn on another canvas.
But i have a problem with detecting horizontal collisions.
...
My problem comes down to this:
My player uses x y coordinates that are stored as:
var p_x; var p_y;
these values is the players bottom left coordinates in pixels.
But my blocks is in a 2d array called:
var g_levelarray;
And each block is 16 x 16 px so for instance if i do:
g_levelarray[3][2] = 1;
means that a block will be drawn at canvas left: 48px and canvas bottom 32px
...
But then i have my code to check if block exists (according to player) where the x and y is playercoordinates
function blockexists(x, y) {
var xpos = parseInt(x / g_blocksize);
var ypos = parseInt(y / g_blocksize);
$("#checkedblock").html("checked block: " + xpos + " " + ypos);
if (g_levelarray[xpos][ypos] != undefined) {
return true;
}
else {
return false;
}
}
but that check has some errors due to the fact that it rounds down the number so when i hit a block from half down the top (as shown on image below) it allows player to go inside block.
(source: userhome.org)
i have also tried Math.round instead of parseInt but that just makes a problem at players middle.
So how can i write this code in a right way so that my player doesnt go into the block?
thx in advance
Instead of just using the parseInt and round try using a range.
Take the lower bound(Math.floor) and the upper bound Math.ceil and check if the block exists in this range by checking these values in the array.
I'm working on my first canvas project, and it requires a partial map of the US, with a zoom and center on a state when clicked.
I was able to find X Y arrays of points to draw the country, with each state being its own array. I needed the states to be drawn out larger then these dimensions, so I introduced a scale varaible to multiply each point by.
My next challenge was that the client only wanted 13 states drawn out, but not placed to scale against each other. (Example, put Ohio and Illinois next to each other on the canvas and ignore Indiana). My solution to that was to introduce a fixed X, Y "constant" for each state, that after the scaling happens, add the X Y value for that state and make that the spot to draw on.
for ( var j = 0; j < state.myPolygons.length; ++j) {
context.beginPath();
context.lineWidth = lineWidth;
context.strokeStyle = stateStroke;
context.fillStyle = stateFill;
for ( var k = 0; k < state.myPolygons[j].myXVals.length; ++k ) {
var x = parseFloat(state.myPolygons[j].myXVals[k]*state.scale)+state.posX;
var y = parseFloat(state.myPolygons[j].myYVals[k]*state.scale)+state.posY;
y = canvas.height - y;
if ( k == 0 )
context.moveTo(x,y);
else
context.lineTo(x,y);
}
context.closePath();
context.fill();
context.stroke();
}
The effect of clicking on a state, and growing it and centering on the canvas was accomplished by defining a target scale and number of steps. I get the difference between the target scale and current scale, and divide that by number of steps to figure out how much to add to the scale of the state at each "frame".
Example: Ohio's initial scale is 1.97 of the found coords. My target for Ohio scale is 3.75%. I get the difference (1.78), and divide that by 45 (the defined set of steps) to draw. This gives me 0.039 as an incrementer to my scale at each frame. I then loop through while my states current scale is less than the target scale. Again however, since I need to manipulate the X Y of the rendering, I have then a zoomx and zoomy constant for each state that gets added to the calculated X Y so it can "slide" to the center of the canvas.
All of this works perfectly and I have California zoom/sliding from left to right, Ohio sliding right to left, etc. --- Here is my problem.
I have a series of dots to indicate client loctions in the state. These are simple X Ys that I draw a circle on. The initial rendering of the map includes a loop to run through each states set of locations. I'm applying the same scale factor, and posX,posY variables to adjust final placement of the dot in relation to final rendering of the state
for (var loc in state.Locations) {
var locx = parseFloat(state.Locations[loc].x*state.scale)+state.posX
var locy =parseFloat(state.Locations[loc].y*state.scale)+state.posY;
var txt=state.Locations[loc].text;
var lnk=state.Locations[loc].link;
context.beginPath();
context.arc(locx,locy,locationSize,0,Math.PI*2,true);
context.fillStyle = locationFill;
context.closePath();
context.fill();
context.stroke();
}
When the state is zooming however, the scaling logic for the dots fails. The state scale for a given frame applies
x = parseFloat(activeState.myPolygons[j].myXVals[k]*activeState.scale)+activeState.posX;
y = parseFloat(activeState.myPolygons[j].myYVals[k]*activeState.scale)+activeState.posY;
When I apply this to a given location in the state with
locx = parseFloat(activeState.Locations[loc].x*activeState.scale)+activeState.posX;
locy = parseFloat(activeState.Locations[loc].y*activeState.scale)+activeState.posY;
I end up with X following pretty closely, but in Ohio's example, the Y is somewhere near Florida. Other states like California are even worse with their dots starting more "stacked" on top of each other and end up more "spread out" beside each other.
I'm trying to figure out the trig functions needed to grow and shrink the position of the X Y on a location in relation to the current scale of the state, and keep it on the same path the state is traveling on through the animation (both zooming in and zooming out).
My final attempt before coming here was to get the inital X Y of the location, and compare its distance to the LAST X Y of the state array. I was trying to then find the angle of the line connecting those 2 points, and then use all this to scale. I still feel that I may be onto something with this approach, I just can't make it happen.
Thank you everyone for taking the time to read this, I appriciate any help you can offer
You could just look at the paper I put on your desk, the one with the equation on it. However, SVGs would be more optimal for the project, as you could easily group things together using the g tag and then could just scale the entire group.
However, since you're forced to use canvas at this point: You would have to scale up and down director, using trig given the angle of the start point to location dot and the DIFFERENCE of left or right travelled from the original distance. I will explain in more detail, with actual equations, when you allow me to give me that paper back. However, the only line you really need to modify at this point is:
locy = parseFloat(activeState.Locations[loc].y*activeState.scale)+activeState.posY;