I have an html5 canvas game for desktop browsers that uses the keyboard, and I'm trying to create compatibility for smartphones by adding touch-screen controls.
The current issue is that the events for touch input are returning X and Y coordinates that don't match the location where I'm touching.
The purple dots in the image above are a diagnostic test I added. The game places a circle in each location where it detects a touch event. In this case, I traced the border of my phone screen. The placement of dots illustrates the difference between where my screen actually is and where the game seems to think it is.
<body onload="html_init()">
<div class="canvas_div" id="canvas_div">
<canvas width="1050" height="600" id="canvas"></canvas>
</div>
</body>
function html_init()
{
canvas_html = document.getElementById ("canvas");
window.addEventListener ("resize", resize_canvas, false);
canvas_html.addEventListener ("touchstart", function(event) {touch_start (event)}, false);
}
function resize_canvas ()
{
// Scale to fit window, decide which scaling reaches edge first.
canvas_2d.restore();
canvas_2d.save();
var game_ratio = canvas_2d.canvas.width / canvas_2d.canvas.height;
var window_ratio = window.innerWidth / window.innerHeight;
var xratio = window.innerWidth / canvas_2d.canvas.width;
var yratio = window.innerHeight / canvas_2d.canvas.height;
var ratio;
if (game_ratio > window_ratio) ratio = xratio;
else ratio = yratio;
if (ratio > 1) ratio = 1;
canvas_2d.scale (ratio, ratio);
}
function touch_start (event)
{
var touch = event.changedTouches;
var x = touch[0].pageX;
var y = touch[0].pageY;
// this goes on to add the purple test sprite to the screen at that location.
}
I've tried referencing pageX, clientX and screenX, but none of them have the correct values. Even though my canvas is set to 1050 x 600, which is what my game runs at, the touch screen seems to think it's somewhere around 650 x 350. If it matters, the test phone is an iPhone 6.
I'm not sure what to check at this point. Any ideas are appreciated.
UPDATE: I've narrowed the problem down to two facts - that the canvas size is larger than the display area of the web page being shown, and that I'm using resize_canvas() to set the scale. It looks like I'm going to have to either reduce the overall size of the canvas to fit the phone or add a formulaic adjustment to the coordinates based on the scale ratio.
canvas_html.addEventListener ("touchstart", touch_start, false);
function resize_canvas ()
{
// Scale to fit window, decide which scaling reaches edge first.
canvas_2d.restore();
canvas_2d.save();
var current_ratio = canvas_2d.canvas.width / canvas_2d.canvas.height;
var new_ratio = window.innerWidth / window.innerHeight;
var xratio = window.innerWidth / canvas_2d.canvas.width;
var yratio = window.innerHeight / canvas_2d.canvas.height;
if (current_ratio > new_ratio) screen_size_ratio = xratio;
else screen_size_ratio = yratio;
if (screen_size_ratio > 1) screen_size_ratio = 1;
canvas_2d.scale (screen_size_ratio, screen_size_ratio);
}
function touch_start (event)
{
var touch = event.changedTouches;
var x = Math.floor (touch[0].clientX / screen_size_ratio);
var y = Math.floor (touch[0].clientY / screen_size_ratio);
}
Because of the scale() I was doing on the canvas, the actual coordinates had to be un-scaled using the ratio I calculated in canvas_resize().
I also had to change my "touchstart" event handler to not use an anonymous function. For whatever reason, this was causing the phone browser to default back to using "mousedown" for the click.
Related
I am trying to apply a blur to a canvas and I am trying to apply it with the same strength on every device.
// this.#blur is usually 7
const ctx = this.#canvas.getContext("2d");
ctx.filter = "blur(" + this.#blur + "px)";
const draw_2DCanvas = () => {
ctx.clearRect(0, 0, width, height);
ctx.drawImage(this.#video, width_diff, height_diff, width, height);
requestAnimationFrame(draw_2DCanvas);
};
draw_2DCanvas();
But with just this the blur strength changes depending on the device, possibly because the canvas size differs.
The blur applies as intended on pc devices, but is too strong on mobile devices.
So I tried to spice things a little and change the blur in accordance to the current canvas style width (the canvas and video size is 360x270 but the html style size is responsive).
// this.#blur is usually 7
const ctx = this.#canvas.getContext("2d");
const draw_2DCanvas = () => {
const blur = Math.ceil(this.#blur * this.#canvas.clientWidth / 360);
ctx.filter = "blur(" + blur + "px)";
ctx.clearRect(0, 0, width, height);
ctx.drawImage(this.#video, width_diff, height_diff, width, height);
requestAnimationFrame(draw_2DCanvas);
};
draw_2DCanvas();
But this time the blur is too strong on pc devices and a little too weak on mobile devices.
What is the correct way to make the canvas blur consistent on every device?
Here is the final code to fix the blur before I explain the how or why:
import "context-filter-polyfill/dist/index.js";
const resize_dvr = this.#device.os == 'iOS' ? 1 : window.devicePixelRatio;
const width = calc_width / resize_dvr;
const height = calc_height / resize_dvr;
const width_diff = this.#ar ? calc_wdiff / resize_dvr / 2 : 0;
const height_diff = this.#ar ? calc_hdiff / resize_dvr / 2 : 0;
const ctx = this.#canvas.getContext("2d");
//do not use window.devicePixelRatio to calculate width here
this.#canvas.width = calc_width;
this.#canvas.height = calc_height;
ctx.scale(resize_dvr, resize_dvr);
ctx.filter = "blur(" + this.#blur + "px)";
const draw_2DCanvas = () => {
ctx.clearRect(0, 0, width, height);
ctx.drawImage(this.#video, width_diff, height_diff, width, height);
requestAnimationFrame(draw_2DCanvas);
};
draw_2DCanvas();
I'll start by saying that something like blur = blur * window.devicePixelRatio will not blur properly!
Also this is a simplified version of my code to make it more readable so is hasn't been tested as is.
After testing directly changing the blur I tried changing the value of the scale like this:
(as shown in protob's link)
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
This managed to fix the blur for Android devices, but made the blur waaay too strong in iOS devices and also made Android devices zoom like crazy.
To fix the Android device version I simply divided all the canvas coordinates by window.devicePixelRatio like this:
const resize_dvr = window.devicePixelRatio;
const width = calc_width / resize_dvr;
const height = calc_height / resize_dvr;
const width_diff = this.#ar ? calc_wdiff / resize_dvr / 2 : 0;
const height_diff = this.#ar ? calc_hdiff / resize_dvr / 2 : 0;
However iOS devices do not support ctx.filter at all! (more info on MDN)
So I was fully relying on the context-filter-polyfill for IOS devices, and this library seems to fully monkey patch the code with a window.devicePixelRatio = 1 setting.
So I had to change the devicePixelRatio i got to 1 for devices that had to rely on this library.
const resize_dvr = this.#device.os == 'iOS' ? 1 : window.devicePixelRatio;
Here is how you can catch an IOS device with your code:
const isIOS = /iP(hone|([oa])d)|Macintosh/.test(navigator.userAgent) && 'ontouchend' in document;
I do not recommend using libraries when catching IOS devices cuz they just read the userAgent as is, and iPads always fake being a Mac OS! (while having none of the web API you can use on a Mac available....)
And if you do use a library remember to double check that it catches all IOS devices!
I am able to get the intersects from my click event when I use the window object to acquire height and width, but getting the intersects position on a canvas that's dynamically sized is proving much harder. I'm not certain of the formula I would need to use to calculate the vector.x and vector.y values with a div that isn't always the same size.
The canvas is the size of a div that always has a width: height ratio of 4:3 and resizes to fit in the window and is always positioned in the center of the window.
If I resize the window to be 4:3 then the following code works perfectly:
mouse.x = (ecx/div_width) *2 -1;
mouse.y= -(ecy/div_height) *2 + 1;
when I resize the window, whichever dimension is larger than the size of the canvas has the incorrect value. I've linked an image to roughly describe how the problem presents itself
Image of horizontal dimension issue
I initially thought that the matches would be as simple as dividing the difference between the the sizes of the window and the canvas by
My question is, how would I acquire the correct values to pass to the vector object for it's x and y attributes? (using Vector3 and Raycaster)
here is the function I'm using to try and get the object(s) being clicked:
function getClicked(event){
event.preventDefault();
var ecx = event.clientX;
var ecy = event.clientY;
//elem is the div containing the canvas
//the canvas is not the same size as the window
var elem_w = elem.innerWidth();
var elem_h = elem.innerHeight();
//most examples suggest using the window height and width
//to get the position of the mouse in the scene.
//since the scene isn't the same size as the window, that doesn't work
var ww = window.innerWidth;
var wh = window.innerHeight;
mouse.x = (ecx/ww) *2 -1;
mouse.y= -(ecy/wh) *2 + 1;
var objlist = []
rc.setFromCamera(mouse, camera);
var intersects = rc.intersectObjects(scene.children, true);
for (var i=0;i<names_to_spin.length;i++){
var obj = intersects[i];
objlist.push(obj);
}
//ideally, this should return a list of the objects under the cursor
return objlist;
}
Basically, I have an animation of a sqaure travelling from right to left. Since I dont know what screen size I will be using to present my work, I cannot hard code a starting x-position value (e.g sqrx = 1400) for the starting position.If the animation is played on a larger screen to what I am developing on, the browser window will be much larger therefore the animation will start in the middle of the screen.
TLDR: Set the starting X position of right to left moving animation to snap to the right edge of a browser window no matter how big/small the browser window is.
var canvasWidth = c.width();
var canvasHeight= c.height();
//Vars to allow buttons to work for reset functionality
var playAnimation= true;
var startButton = $("#startAnimation");
var stopButton = $("#stopAnimation");
//Start position of Square
var sqrx = 1310;
//Animation timer
function animate(){
sqrx--;
ctx.clearRect(0,0,canvasWidth, canvasHeight);
ctx.fillRect(sqrx,700,40,50);
setTimeout(animate,33);
//save state when the cavnas is first drawn.
ctx.save();
};
animate();
it'll be a lot easier if you can give canvas, the width and height of window so that no matter which device you use it'll get the value of window screen size :
var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;
Give the rectangle value for its size :
var rect_size = 50;
This will make it easier for you to position the rectangle in the canvas.
function draw(){
ctx.beginPath();
ctx.fillStyle = "red";
ctx.fillRect(width - rect_size , 0 , rect_size , rect_size);
ctx.fill();
}
draw();
What I'm trying to create is a small canvas widget that would allow a user to dynamically create a shape onto an image and then place it above an area that caught their interest, effectively it is a highlighter.
The problem is with adding a zoom function, as when I zoom onto the image I would like to ensure that;
There is no possible way for the dynamically created shape to be
dragged anywhere outside the image area. (completed - ish, relies on 2nd step)
You cannot drag the image out of the page view, the canvas area cannot show white space. Part of the image must always be shown, and fill the entire canvas area. (problem)
Here are two examples that I've drawn up, neither of which work correctly;
First example - getBoundingRect does not update and is bound to the image
Second example - getBoundingRect does update and is bound to the grouped object
From the link description you can see that I think I've narrowed the problem down, or at least noticed a key difference between the scripts with how the getBoundingRect behaves.
The first plunk seems to work fine, until you try to zoom in multiple times and at a greater zoom level, then it seems to start bugging out (may take a few clicks and a bit of messing around, it is very inconsistent). The second plunk is very jittery and doesn't work very well.
I've been stuck on this for a week or so now, and I'm at breaking point! So really hoping someone can point out what I'm doing wrong?
Code snippet below for first plunk;
// creates group
var objs = canvas.getObjects();
var group = new fabric.Group(objs, {
status: 'moving'
});
// sets grouped object position
var originalX = active.left,
originalY = active.top,
mouseX = evt.e.pageX,
mouseY = evt.e.pageY;
active.on('moving', function(evt) {
group.left += evt.e.pageX - mouseX;
group.top += evt.e.pageY - mouseY;
active.left = originalX;
active.top = originalY;
originalX = active.left;
originalY = active.top;
mouseX = evt.e.pageX;
mouseY = evt.e.pageY;
// sets boundary area for image when zoomed
// THIS IS THE PART THAT DOESN'T WORK
active.setCoords();
// SET BOUNDING RECT TO 'active'
var boundingRect = active.getBoundingRect();
var zoom = canvas.getZoom();
var viewportMatrix = canvas.viewportTransform;
// scales bounding rect when zoomed
boundingRect.top = (boundingRect.top - viewportMatrix[5]) / zoom;
boundingRect.left = (boundingRect.left - viewportMatrix[4]) / zoom;
boundingRect.width /= zoom;
boundingRect.height /= zoom;
var canvasHeight = canvas.height / zoom,
canvasWidth = canvas.width / zoom,
rTop = boundingRect.top + boundingRect.height,
rLeft = boundingRect.left + boundingRect.width;
// checks top left
if (rTop < canvasHeight || rLeft < canvasWidth) {
group.top = Math.max(group.top, canvasHeight - boundingRect.height);
group.left = Math.max(group.left, canvasWidth - boundingRect.width);
}
// checks bottom right
if (rTop > 0 || rLeft > 0) {
group.top = Math.min(group.top, canvas.height - boundingRect.height + active.top - boundingRect.top);
group.left = Math.min(group.left, canvas.width - boundingRect.width + active.left - boundingRect.left);
}
});
// deactivates all objects on mouseup
active.on('mouseup', function() {
active.off('moving');
canvas.deactivateAll().renderAll();
})
// sets group
canvas.setActiveGroup(group.setCoords()).renderAll();
}
EDIT:
I've added comments and tried to simplify the code in the plunks.
The relevant code starts within the if (active.id == "img") { code block.
I've put irrelevant code as functions at the bottom, they can largely be ignored. ( createNewRect() + preventRectFromLeaving() )
I've removed one of the plunks to avoid confusion.
Let me know if it helps, or If I should try to simplify further.
Thanks!
I think that the grouping was messing with the position of the background image. So, I tried removing the group when the image is moving and manually updating the position of the rect instead.
It sets the last position of the image before moving
var lastLeft = active.left,
lastTop = active.top;
And then it updates those and the position of the rect every time the image moves
rect.left += active.left - lastLeft;
rect.top += active.top - lastTop;
// I think this is needed so the rectangle can be re-selected
rect.setCoords();
lastLeft = active.left;
lastTop = active.top;
Since the image has to stay within the canvas, the rect stays inside the canvas, too, whenever the image moves. The rest of the code you wrote seemed to work fine.
http://plnkr.co/edit/6GGcUxGC7CjcyQzExMoK?p=preview
I am trying to create a simple canvas grid which will fit itself to the player's current zoom level, but also to a certain canvas height/width proportional screen limit. Here is what I got so far:
JS:
var bw = window.innerWidth / 2; //canvas size before padding
var bh = window.innerHeight / 1.3; //canvas size before padding
//padding around grid, h and w
var pW = 30;
var pH = 2;
var lLimit = 0; //9 line limit for both height and width to create 8x8
//size of canvas - it will consist the padding around the grid from all sides + the grid itself. it's a total sum
var cw = bw + pW;
var ch = bh + pH;
var canvas = $('<canvas/>').attr({width: cw, height: ch}).appendTo('body');
var context = canvas.get(0).getContext("2d");
function drawBoard(){
for (var x = 0; lLimit <= 8; x += bw / 8) { //handling the height grid
context.moveTo(x, 0);
context.lineTo(x, bh);
lLimit++;
}
for (var x = 0; lLimit <= 17; x += bh / 8) { //handling the width grid
context.moveTo(0, x); //begin the line at this cord
context.lineTo(bw, x); //end the line at this cord
lLimit++;
}
//context.lineWidth = 0.5; what should I put here?
context.strokeStyle = "black";
context.stroke();
}
drawBoard();
Now, I succeeded at making the canvas to be at the same proportional level for each screen resolution zoom level. this is part of what I am trying to achieve. I also try to achieve thin lines, which will look the same at all different zooming levels, and of course to remove the blurriness. right now the thickness
of the lines change according to the zooming levels and are sometimes blurry.
Here is jsFiddle (although the jsFiddle window itself is small so you will barely notice the difference):
https://jsfiddle.net/wL60jo5n/
Help will be greatly appreciated.
To prevent blur, you should account for window.devicePixelRatio when setting dimensions of your canvas element (and account for that dimensions during subsequent drawing, of course).
width and height properties of your canvas element should contain values that are proportionally higher than values in CSS properties of the same names. This can be expressed e.g. as the following function:
function setCanvasSize(canvas, width, height) {
var ratio = window.devicePixelRatio,
style = canvas.style;
style.width = '' + (width / ratio) + 'px';
style.height = '' + (height / ratio) + 'px';
canvas.width = width;
canvas.height = height;
}
To remove blurry effect on canvas zoom/scale i used image-rendering: pixelated in css
The problem is that you are using decimal values to draw. Both the canvas width and the position increments in your drawBoard() loop use fractions. The canvas is a bitmap surface, not a vectorial drawing. When you set the width and height of the canvas, you set the actual number of pixels stored in memory. That value cannot be decimal (browsers will probably just trim the decimal part). When you try to draw at decimal positions, the canvas will use pixel interpolation to avoid aliasing, hence the occasional blur.
See a version where I round x before drawing:
https://jsfiddle.net/hts7yybm/
Try rounding the values just before you draw them, but not in your actual logic. That way, the imprecision won't stack as the algorithm keeps adding to the value.
function drawBoard(){
for (var x = 0; lLimit <= 8; x += bw / 8) {
var roundedX = Math.round(x);
context.moveTo(roundedX, 0);
context.lineTo(roundedX, bh);
lLimit++;
}
for (var x = 0; lLimit <= 17; x += bh / 8) {
var roundedX = Math.round(x);
context.moveTo(0, roundedX);
context.lineTo(bw, roundedX);
lLimit++;
}
context.lineWidth = 1; // never use decimals
context.strokeStyle = "black";
context.stroke();
}
EDIT: I'm pretty sure all browsers behave as if the canvas was an img element, so there's no way to prevent aliasing when the user zooms with their browser's zoom function, other than with prefixed css. And even then, I'm not sure the browsers's zoom feature takes that into account.
canvas {
image-rendering: -moz-crisp-edges;
image-rendering: -o-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
-ms-interpolation-mode: nearest-neighbor;
}
Also, make sure the canvas doesn't have any CSS-set dimensions. That only stretches the image after it's been drawn instead of increasing the drawing surface. If you want to fill a block with the canvas by giving it 100% width and height, then you need some JS to compute the CSS-given height and width and set the value of the canvas's width and height property based on that. Then you can make your own implementation of a zoom function within your canvas drawing code, but depending on what you're doing it might be overkill.