CreateJS - scaling the canvas does not scale the mouse coordinates - javascript

I am working on a big project where exercises in Canvas are created through JSON-data and CreateJS. The purpose of having it in HTML 5 is to not have to use a separate app for your phone, you can always use the website.
Everything works fine, however in mobile the Canvas is rescaled to full screen. This is done through checking the screen size, and if it's small enough to be mobile the canvas is scaled through this code:
// browser viewport size
var w = window.innerWidth;
var h = window.innerHeight;
// stage dimensions
var ow = canvasWidth;
var oh = canvasHeight;
// keep aspect ratio
var scale = Math.min(w / ow, h / oh);
stage.scaleX = scale;
stage.scaleY = scale;
// adjust canvas size
stage.canvas.width = ow * scale;
stage.canvas.height = oh * scale;
This works great for most of the exercises, like quizzes and such, where all you have to do is click on a button. However we also have some drag and drop-exercises, and an exercise where you can color a drawing. These of course rely on the mouse coordinates to work properly. The problem is, when the canvas is scaled the mouse coordinates are not. So when you drag an item or try to draw, there is an offset happening. So your drawing appears way left of your click, and when picking up a draggable object it doesn't quite follow your click correctly.
Had I made the code from the beginning I'm fairly sure how I would have recalculated the coordinates, but since they are calculated by CreateJS I don't really know how I should go about this.
This was reported as a problem by someone about a year ago here, where this solution was suggested:
I was able to work around this by adding a top-level container and attaching my Bitmaps to that and scaling it.
The whole exercise is inside a container which I have tried to scale but to no avail. I have also tried sending the scale as a parameter to the parts of the exercise created (for example the menu, background images and such) and not scale it all together, and it seems to work okay since then I can exclude the drawing layer. But since it is a large project and many different exercises and parts to be scaled it would take quite some time to implement, and I'm not sure it's a viable solution.
Is there a good and easy way to rescale the mouse coordinates along with the canvas size in CreateJS? I have found pure Javascript examples here on SO, but nothing for CreateJS in particular.

Continued searching and finally stumbled upon this, which I hadn't seen before:
EaselJS - dragging children of scaled parent. It was exactly what I was looking for. I needed to change the coordinates I drew with this:
var coords = e.target.globalToLocal(e.stageX, e.stageY);
Then I could use the coords.x and coords.y instead of directly using e.stageX and e.stageY like before.

Related

HTML Canvas - Prevent blurriness

Context:
I'm working on a project to render a map into a HTML Canvas, this map is based on a jittered points and voronoi diagrams. Because of this technique I need to use ctx.scale (both values, width and height are set to 8). Once the map is generated in the canvas, I read each pixel looking for a specific color set, which will then became a 3D map.
Problem:
Everything 'works' fine, except that the canvas shapes gets somehow anti-aliased, and that obviously creates a huge problem since some of the colors I read from the pixels won't match my colorset.
So far this is how my canvas setup looks like:
const dpr = window.devicePixelRatio | 1
const ctx = mapCanvas.getContext('2d')
ctx.save()
ctx.scale((mapCanvas.width / CONFIG.RESOLUTION) *dpr, (mapCanvas.Height / CONFIG.RESOLUTION) *dpr)
ctx.lineWidth = 0.5
ctx.globalCompositeOperation = 'source-over'
ctx.imageSmoothingEnabled = false
As for globalCompositeOperation I tried almost all options that made some sense, but the results still the same.
I also added on the CSS side the following:
image-rendering: pixelated;
But also tried crisp-edges.
Long story short, I read more articles / tutorial than I wished, spent 3 hours trying to get around it, but no matter what I do I can't see how am I going to fix it.
Can anybody give a little help?
Thanks in advance.
TF

Stabilize touch events coordinates on touchmove

I'm working on a small application that allows users to draw freehand in 3d with three.js (the result is somewhat similar to Blender's grease pencil). The application works in any browser but I'm struggling with my main target platform: the iPad with the Apple pencil.
Compared to a mouse, the Apple pencil fires a very high amount of touchmove events and that causes my lines to be "jagged" in some scenarios, especially when the line is drawn slowly.
Here's a jsfiddle that shows the functionality. The only thing I'm doing here is this:
//convert coordinate to three.js space
x = (x / window.innerWidth) * 2 - 1;
y = -(y / window.innerHeight) * 2 + 1;
var vNow = new THREE.Vector3(x, y, 0);
//unproject the coordinates based on the camera
vNow.unproject(camera);
//push a new line into the line geometry
line.geometry.vertices.push(vNow);
//re-render the line at every mousemove
setGeometry()
If you try it with a mouse, the result are pretty okay. But once you switch to Apple pencil… this is what I tend to get 👇🏻 the lines are mostly "jagged"
I tried many things, but nothing satisfied me yet.
I tried generating curves from some or all of the coordinates and re-rendering the line on touchend but I do not like the effect and controlling the quality of the output is very difficult.
I tried simplifying the line with simplify.js and while close to the desired result it produces inconsistent results.
So, I thought that instead of manipulating the output (the line) I could manipulate the input (the points) and stabilise them before drawing. What would be the best approach to "stabilize" mouse events, given an array of points? I googled left and right but did not find a good answer. Most solutions for drawing smooth curves are for the canvas, which is very different from 3d geometry.
Thanks!

How to properly resize canvas according to screen size? Javascript (p5.js)

So, as the question says I need to resize the canvas according to screen size. However the thing is that that's not it. I also need to have the mouse coordinates updated proportionally. Seems like I made a fatal mistake of not considering screen sizes from the start since this is my first proper game. I made the game on basis of my 1080p screen. Things like positioning stuff and checking mouse coordinates are all based on a 1920x1080 canvas. Please help!
Github links:
Main game, will work but size will depend on your screen: https://proqbr.github.io/powerdown/
all the files(only 15MB in case you'd like to see by downloading): https://github.com/proqbr/powerdown
sketch.js (pretty much main file regarding this): https://github.com/proqbr/powerdown/blob/master/sketch.js
Main thing is in sketch.js, On lines 260-263 are some createCanvas lines in function setup(), the uncommented one is createCanvas(1920,1080); as it's what i was working with. And on lines 302-314 are a bunch of camera.zoom lines, uncommented one is camera.zoom = 1; since on my screen there was no required zoom for the main menu.
Line 352 onwards is some code on knowing where the player clicked, the problem is this. I do know how to make the canvas's contents look properly resized on all screens by using windowWidth & windowHeight however it's the incorrect mouse coordinates which causes the problem. It would be great if someone could help.
In case there's some problem with the game on your side, here's a quick second video how the game menu works, although it's pretty straightforward anyways: https://youtu.be/eZZw5CmOXEE
I figured it out myself. For some values like when testing if mouseX >= 720 I had to edit it to mouseX >= 785*windowWidth/1920.
For one translate that I had used, translate(95,525), I had to modify it a little more and this was the one that properly worked translate(displayWidth/2-865*(windowWidth)/1920,displayHeight/2-15*(windowHeight)/1080);
As for screen resizing this worked fine:
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
if(windowHeight > windowWidth){
factor = windowHeight;
factdiv = 1080;
}else{
factor = windowWidth;
factdiv = 1920;
}
}
I had to mess around with these to figure out a decent solution
canvas dimensions i.e. 1920, 1080
windowWidth, windowHeight
and also displayWidth, displayHeight
I have also updated the same in the repository so you can see the final outcome from here: https://proqbr.github.io/powerdown/

Parallax effect with zoom and rotating

I am currently experimenting with parallax effect that i am planning to implement to my HTML5-canvas game engine.
The effect itself is fairly easy to achieve, but when you add zooming and rotating, things get a little more complicated, at least for me. My goal is to achieve something like this:Youtube video.
As you can see, you can zoom in and out "to the center", and also rotate around it and get the parallax effect.
In my engine i want to have multiple canvases that are going to be my parallax layers, and i am going to translate them.
I came up with something like this:
var parallax = {
target: {
x: Mouse.x,
y: Mouse.y
},
offset: {
x: -ctx.width / 2,
y: -ctx.height / 2
},
factor: {
x: 1,
y: 1
}
}
var angle = 0;
var zoomX = 1;
var zoomY = 1;
var loop = function(){
ctx.canvas.width = ctx.canvas.width; //Clear the canvas.
ctx.translate(parallax.target.x * parallax.factor.x, parallax.target.y * parallax.factor.y);
ctx.rotate(angle);
ctx.scale(zoomX, zoomY);
ctx.translate((-parallax.target.x - parallax.offset.x) * parallax.factor.x, (-parallax.target.y - parallax.offset.y) * parallax.factor.y);
Draw(); //Function that draws all the objects on the screen.
}
This is a very small and simplified part of my script, but i hope that's enough to get what i am doing. The object "parallax" contains the target position, the offset(the distance from the target), and the factor that is determining how fast the canvas is moving away relatively to the target. ctx is the canvas that is moving in the opposite direction of the target.(In this example i am using only one layer.) I am using the mouse as the "target", but i could also use the player, or some other object with x and y property. The target is also the point around which i rotate and scale the canvas.
This method works completely fine as long as the factor is equal to 1. If it is something else, the whole thing suddenly stops working correctly, and when i try to zoom, it zooms to the top-left corner, not the target. I also noticed that if i zoom out too much, the canvas is not moving in the opposite way of the target, but in the same direction.
So my question is: What is the correct way of implementing parallax with zooming and rotating?
P.S. It is important to me that i am using canvases as the layers.
To prepare for the next animation frame, you must undo any previous transforms in the reverse order they were executed:
context.translate(x,y);
context.scale(sx,sy);
context.rotate(r);
// draw stuff
context.rotate(-r);
context.scale(-sx,-sy);
context.translate(-x,-y);
Alternatively, you can use context.save / context.restore to undo the previous transforms.
Adjust your parallax values for the current frame,
Save the un-transformed context state using context.save(),
Do your transforms (translate, scale, rotate, etc),
Draw you objects as if they were in non-transformed space with [0,0] at your translate point,
Restore your context to it's untransformed state using context.restore()/
Either way will correctly give you a default-oriented canvas to use for your next animation frame.
The exact parallax effects you apply are up to your own design, but using these methods will make the canvas return to a normal default state for you to design with.

Pixel perfect 2D mouse picking with Canvas

I'm writing a 2D game in html5 using Canvas which requires mouse click and hover events to be detected. There are 3 problems with this: detections must be pixel-perfect, objects are not rectangular (houses, weird-shaped UI buttons...), and it is required to be fast and responsive. (Obviously brute force is not an option)
So what I want to ask is how do I find out which object the mouse is on, and what are the possible optimizations.
P.S: I did some investigation and found a guy who used QuadTree here.
I have a (dated) tutorial that explains the concept of a ghost canvas which is decent for pixel-perfect hit detection. The tutorial is here. Ignore the warning about a newer tutorial, the newer one does not use the ghost canvas concept.
The idea is to draw the image in question to an in-memory canvas and then use getImageData to get the single pixel of the mouse click. Then you see if that single pixel is fully transparent or not.
If its not fully transparent, well, you've got your target.
If it is fully transparent, draw the next object to the in-memory canvas and repeat.
You only have to clear the in-memory canvas at the end.
getImageData is slow but it is your only option if you want pixel-perfect hit detection and aren't pre-computing anything.
Alternatively you could precompute a path or else an array of pixels with an offset. This would be a lot of work but might be faster. For instance if you have a 40x20 image with some transparency you'd compute an array[40][20] that would have true or false corresponding to transparent or not. Then you'd test that against the mouse position, with some offset, if the image is drawn at (25, 55) you'd want to subtract that from the mouse position and then test if the new position is true when you look at array[posx][posy].
That's my answer to your question. My Suggestion? Forget pixel-perfect detection if this is a game.
Seriously.
Instead make paths (not in canvas, in plain javascript code) that represent the objects but are not pixel perfect, for instance a house might be a square with a triangle on the top that is a very close approximation of the image but is used in its stead when it comes to hit testing. It is comparatively extremely fast to compute if a point is inside a path than it is to do pixel-perfect detection. Look up point in polygon winding number rule detection. That's your best bet, honestly.
The common solution in traditional game development is to build a click mask. You can re-render everything onto a separate off-screen canvas in a solid color (the rendering should be very quick). When you want to figure out what was clicked on, you simply sample the color at the x/y co-ordinate on the off-screen canvas. You end up building a color-->obj hash, akin to:
var map = {
'#000000' : obj1
, '#000001' : obj2
, ...
};
You can also optimize the rendering to the secondary canvas to only happen when the user clicks on something. And using various techniques, you can further optimize it to only draw the part of the canvas that the user has clicked on (for example, you can split you canvas into an NxN grid, e.g. a grid of 20x20 pixel squares, and flag all of the objects in that square -- you'd then only need to re-draw a small number of objects)
HTML5 Canvas is just a drawing plane, where you can set different transforms before calling each drawing API function. Objects cannot be created and there is no display list. So you have to build these features yourself or you can use different libraries available for this.
http://www.kineticjs.com/
http://easeljs.com/
A few months before I got interested in this and even wrote a library for this purpose. You can see it here : http://exsprite.com. Ended up facing a lot of performance issues, but because of lack of time I couldn't optimize it. It was really interesting, so waiting for some time to make it perfect.
I believe the comments should suffice. This is how I determine user intention in my 2d isometric scroller, currently located at http://untitled.servegame.com
var lastUp = 0;
function mouseUp(){
mousedown = false; //one of my program globals.
var timeNow = new Date().getTime();
if(mouseX == xmouse && mouseY == ymouse && timeNow > lastUp + 100){//if it was a centralized click. (mouseX = click down point, xmouse = mouse's most recent x) and is at least 1/10th of a second after the previous click.
lastUp = new Date().getTime();
var elem = document.elementFromPoint(mouseX, mouseY); //get the element under the mouse.
var url = extractUrl($(elem).css('background-image')); // function I found here: http://webdevel.blogspot.com/2009/07/jquery-quick-tip-extract-css-background.html
imgW = $("#hiddenCanvas").width(); //EVERY art file is 88px wide. thus my canvas element is set to 88px wide.
imgH = $(elem).css('height').split('p')[0]; //But they vary in height. (currently up to 200);
hiddenCanvas.clearRect(0, 0, imgW, imgH); //so only clear what is necessary.
var img = new Image();
img.src = url;
img.onload = function(){
//draw this elements image to the canvas at 0,0
hiddenCanvas.drawImage(img,0,0);
///This computes where the mouse is clicking the element.
var left = $(elem).css('left').split('p')[0]; //get this element's css absolute left.
var top = $(elem).css('top').split('p')[0];
offX = left - offsetLeft; //left minus the game rendering element's absolute left. gives us the element's position relative of document 0,0
offY = top - offsetTop;
offX = mouseX - offX; //apply the difference of the click point's x and y
offY = mouseY - offY;
var imgPixel = hiddenCanvas.getImageData(offX, offY, 1, 1); //Grab that pixel. Start at it's relative X and it's relative Y and only grab one pixel.
var opacity = imgPixel.data[3]; //get the opacity value of this pixel.
if(opacity == 0){//if that pixel is fully transparent
$(elem).hide();
var temp = document.elementFromPoint(mouseX, mouseY); //set the element right under this one
$(elem).show();
elem = temp;
}
//draw a circle on our hiddenCanvas so when it's not hidden we can see it working!
hiddenCanvas.beginPath();
hiddenCanvas.arc(offX, offY, 10, 0, Math.PI*2, true);
hiddenCanvas.closePath();
hiddenCanvas.fill();
$(elem).css("top", "+=1"); //apply something to the final element.
}
}
}
In conjunction with this:
<canvas id="hiddenCanvas" width="88" height="200"></canvas>
Set the CSS positioning absolute and x = -(width) to hide;

Categories