How to resize a canvas properly in React using p5js? - javascript

I'm currently working on a project and one part is about resizing the model in the canvas once the canvas is resized (or window is resized). I have checked documentation for resizeCanvas() and applied it.
I'm first finding the ratio by dividing the currentWidth/defaultWidth which are currentWidth of the user's device and the default/selected width by us, respectively.
findRatio(wd, hd, wc, hc) {
const w = wc / wd;
const h = hc / hd;
return w < h ? w : h;
}
and in my windowResized() function, I'm setting width/height once canvas is resized on window resize.
export const windowResized = (p5) => {
if (p5) {
initVariables.browserWidth = p5.windowWidth;
initVariables.browserHeight = p5.windowHeight;
p5.resizeCanvas(initVariables.browserWidth, initVariables.browserHeight);
for (let m of initVariables.mM) {
m.updateonResize();
}
}
};
here initVariables is just an object with some bunch of variables.
I also have updateonResize() function in my parent class that triggers once window is resized.
updateonResize() {
this.r = this.findRatio(
this.initVariables.width,
this.initVariables.height,
this.initVariables.browserWidth,
this.initVariables.browserHeight
);
this.arr.push(this.r);
if (this.arr[this.arr.length - 2] !== undefined) {
if (this.r < this.arr[this.arr.length - 2]) {
console.log("canvas getting larger, not sure if I need to divide or multiply here");
this.min = this.min * this.r;
this.max = this.max * this.r;
this.measure = this.measure * this.r;
} else {
this.min = this.min * this.r;
this.max = this.max * this.r;
this.measure = this.measure * this.r;
}
}
}
Here, min, max, and measure variables are variables to detect the size of object parts which are the length of lines. If the canvas is getting smaller, you need to multiply measure value with ratio (where ratio is usually less than 1).
Problem1:
I'm having a problem when I go to window mode from full-screen mode in my Chrome browser. It does not trigger the windowOnResize() function. Is it possible to automatically trigger this function once you go to window mode?
Problem2:
When you resize the browser, the ratio changes every frame, and thus measure*ratio value becomes too low (to 0). Is there any algorithm I could apply to decrease the measure value slightly, but not drastically?

I have found the solution myself. I have added resize event listener with JavaScript and find the ratio with my own function and resize the canvas with it. I have added timeout not to see changes at the same time, but after some delay.
let timeOutFunctionId;
window.addEventListener("resize", function () {
if (!initVariables.subSet) {
r = findRatio(
initVariables.width,
initVariables.height,
initVariables.browserWidth,
initVariables.browserHeight
);
clearTimeout(timeOutFunctionId);
timeOutFunctionId = setTimeout(() => {
if (p5) {
if (true) {
initVariables.browserWidth = p5.windowWidth - 310;
initVariables.browserHeight = p5.windowHeight - 200;
} else {
}
p5.resizeCanvas(
initVariables.browserWidth,
initVariables.browserHeight
);
//calling custom class
}
}, 500);
}
});

Related

How to fix HTML canvas element and its coordinates across different screen sizes?

I've spent the past two weeks making a game with a few friends using the HTML canvas tag and JavaScript. None of us have any prior experience with a project of this scale, so considerations of browser/screen-size compatibility wasn't on our minds. The game runs and looks fine on our laptops (all have similar screen sizes), but it looked bad when we sent a link to another friend whose screen size differs greatly from what we had in mind.
To discuss the layout of the game in greater detail, it's set up with the canvas element acting as the actual game with a series of divs sitting below the canvas to represent things like a dialogue box or the pause menu (only one of these divs is shown at a time with the others being hidden).
The game is gridbased in that every object, from the wall tiles to enemies, has a size relative to some constant blockWidth (defined below) which itself is relative to the desired amount of squares on-screen, numSquares (also defined below).
Changing the canvas's height and width properties in JavaScript did successfully fix a ratio of the canvas size and ensure that the wall and floor textures loaded in their proper place. The player and other NPCs, however, appear at odd places onscreen, sometimes not showing up onload at all.
I'm not quite sure what to attribute this problem to, but I think it has something to do with canvas' coordinate system not mixing well with the admittedly poorly executed block system we put in place.
//some relevant misc variables
var numSquares = 30;
const blockWidth = 1132 / numSquares * 0.75;
screen.width = 1132;
screen.height = 600;
//some relevant player variables
stats.x = 678;
stats.y = 600;
stats.width = blockWidth * 3 * 0.37;
stats.height = blockWidth * 3;
Again, I suspect that the problem has something to do with the fact that the tiles that render correctly (i.e. wall and floor textures) have their coordinates in terms of blockWidth whereas the tiles that render incorrectly (i.e. the player) have their coordinates as regular numbers.
Is there a way to go about adjusting our game for different monitor sizes other than revamping the entire coordinate system?
try this meta which will solve cross platform screen (laptop, Computer, Tab, Mobile)problem:
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
Your main problem is using hard coded values for your variables. If stats.x = 678;and your screen is 480px wide (for example), the stats will fall out the screen.
Also: screen.width and screen.height is a hardcoded number. Probably what you need is something like: screen.width = window.innerWidth and screen.height = window.innerHeight
What I'm missing from your relevant misc variables: In your code you have this:
var numSquares = 30;
const blockWidth = 1132 / numSquares * 0.75;
Where 1132 is in fact the screen.width. This means that you don't have a grid as you say. This means you have only 1 (one) row of squares. Alternatively you have 30 squares per row, and in this case you have a grid.
Also, it would be nice to see how you draw your grid. I would do it like this:
for (let y = 0; y < ch; y += cw / numSquares) {
for (let x = 0; x < cw; x += cw / numSquares) {
let o = { x: x, y: y, w: blockWidth, h: blockWidth };
drawRect(o);
}
}
Where drawRect is a custom function to draw a rect. Since your blockWidth = screen.width / numSquares * 0.75; I'm assuming you let a gap between rects, but this is assuming.
As I've commented before you can't give your stats hard coded values. You will need to calculate these values in function of your grid cells. For example your stats.x may be the x of the 5'th column of your grid and the stats.y may be the y of the 3-rd row. But this is again assuming and I may be wrong.
Next comes a code example. Please take a look and let me know if this is what you need.
const screen = document.getElementById("canvas");
const ctx = screen.getContext("2d");
let cw = (screen.width = window.innerWidth);
let ch = (screen.height = window.innerHeight);
let stats = {};
let numSquares = 30;
let blockWidth;
generateGrid();
function Init() {
// a function to re-generate the grid and re-draw the stats when screen.width and/or screen.height changed
cw = screen.width = window.innerWidth;
ch = screen.height = window.innerHeight;
generateGrid();
drawStats(5, 3);
}
// recalculate everything on resize
setTimeout(function() {
Init();
addEventListener("resize", Init, false);
}, 15);
// some useful functions
function drawRect(o) {
ctx.beginPath();
ctx.fillStyle = "rgba(0,0,0,.05)";
ctx.fillRect(o.x, o.y, o.w, o.h);
}
function drawStats(x, y) {
stats.x = x * cw / numSquares;
stats.y = y * cw / numSquares;
stats.width = blockWidth * 3 * 0.37;
stats.height = blockWidth * 3;
ctx.fillStyle = "red";
ctx.beginPath();
ctx.fillRect(stats.x, stats.y, stats.width, stats.height);
}
function generateGrid() {
blockWidth = cw / numSquares * 0.75;
for (let y = 0; y < ch; y += cw / numSquares) {
for (let x = 0; x < cw; x += cw / numSquares) {
let o = { x: x, y: y, w: blockWidth, h: blockWidth };
drawRect(o);
}
}
}
*{margin:0;padding:0}
<canvas id="canvas"></canvas>
If this is not what you need, please update your question, and add more explanations and more code.

Get div's width for p5.js

I'm learning on my own JavaScript so I'm doing something like a website using p5.js
The thing is that a div holding my canvas for p5.js and I want it to be responsive. In this canvas, I do have an object that needs the div width and height to be constructed.
Problem is that I don't know how to get this information. I tried jQuery but I don't know how to extract value out of a jQuery function and I'm don't know if it's an excessive way to do it.
//--------------------------constant--------------------------------------------
//Canvas
const DROPNUMBER = 1500;
//--------------------------classe---------------------------------------------
function Drop(width, heigth) {
//declaring and setting drop's attribute
this.spawn = function(width) {
//size and position
this.x = Math.random() * -width*1.5;
this.y = Math.random() * heigth;
this.size = Math.random() * 20 ;
//color
this.colorR = 138 + Math.random() * 50;
this.colorV = 43 + Math.random() * 50;
this.colorB = 226 + Math.random() * 50;
this.colorA = Math.random() * 127 +50;
//speed and landing
this.speed = Math.random();
this.hasLanded = false;
}
//call func to set the attribute
this.spawn(width);
//make the drop falls
this.fall = function() {
//if the drop can fall
if (this.x < width) {
this.x = this.x + this.speed;
this.speed = this.speed + 0.01;
//if the drop did land
if (this.y + this.size > width && this.hasLanded == false) {
this.hasLanded = true;
}
}
//if the drop did fall
else {
this.spawn(width);
}
}
//display the drop
this.display = function() {
noStroke();
//Some kind of purple color
fill(this.colorR, this.colorV, this.colorB, this.colorA);
rect(this.x, this.y, this.size, this.size)
}
}
//--------------------------setup---------------------------------------------
function setup() {
clientHeight = document.getElementById('header').clientHeight;
clientWidth = document.getElementById('header').clientWidth;
canvas = createCanvas(clientWidth, clientHeight);
canvas.parent('sketch-holder');
window.canvas = canvas;
}
//-------------------------Variable---------------------------------------------
var n = DROPNUMBER;
var drops = new Array();
//creating an array of drop for the rain
for (let i = 0; i < n; i++) {
//800 800 is height and the width that i want to change !
drops.push(new Drop(800,800));
}
//--------------------------draw------------------------------------------------
function draw() {
background(48, 64, 96);
//each drop
for (let i = 0; i < n; i++) {
//Make them falling
drops[i].fall();
//display the result
drops[i].display();
}
}
The code is just showing that drops ( the object that needs height and width) are being constructed out of draw or setup function.
I did search already on Stack Overflow to this kind of problem too.
use a regular DOM js it's worked:
let b = document.getElementById("parentElement");
let w = b.clientWidth;
let h = b.clientHeight;
console.log(w, h);
You don't need to use JQuery for this. Take a look at the P5.dom library that provides a bunch of functionality for manipulating HTML elements in P5.js.
I'm not sure exactly what you're trying to do, but here's a simple example. If we have a div on the page with and ID of myId, then to get its width we could use P5.dom to do this:
var myDiv = select('myId');
var myWidth = myDiv.style.width;
console.log('width: ' + myWidth);
To get the width of an element in JavaScript, you can select that element using document.querySelector(). Any CSS selector is a valid first argument to that function. E.g. the following will select the <body> tag:
document.querySelector('body')
Once you have an element selected, you can get it's width by accessing the clientWidth property. E.g. the following tells you the width of the <body> tag:
document.querySelector('body').clientWidth
So just replace body with a CSS selector for the element you want to select.

How to make width and height of a rectangle updates after resize in fabric js?

I want to contain the rectangles within the image and wrote the following code. It does not work after resize because the width and height of the active object doesn't seem to update after resize.
This is the jsfiddle: https://jsfiddle.net/yxchng/0hL2khro/191/
canvas.on("object:moving", function(opt) {
activeObject = canvas.getActiveObject()
if (activeObject.left < 0) {
activeObject.left = 0;
}
if (activeObject.top < 0) {
activeObject.top = 0;
}
if (activeObject.left + activeObject.width > 1000) {
activeObject.left = 1000 - activeObject.width;
}
if (activeObject.top + activeObject.height > 1000) {
activeObject.top = 1000 - activeObject.height;
}
activeObject.setCoords();
});
Is there a better way to contain objects within image?
If you consider only scaling (not skewing);
Updated values will be
height = object.height * object.scaleY;
width = object.width * object.scaleX;
For fabricjs V 2.x
Use getScaledHeight and getScaledWidth which returns height and width of object bounding box counting transformations respectively.

how to speed up display of large images from local file system in html / javascript

I have an html app that I am working on to process large number of large images. We're talking about possibly 5,000 photos that are around 3-5MB each.
So far I am testing with around 1000 images and things are already getting pretty slow.
I am using drag and drop and a FileReader to load the images then setting the FileReader result as the source of the image:
private loadImageFromDisk(image: IImage): Rx.Observable<IImage> {
return Rx.Observable.defer( () => {
console.log( `loading ${image.file.name} from disc` );
console.time( `file ${image.file.name} loaded from file system` );
const reader = new FileReader();
setTimeout( () => reader.readAsDataURL(image.file), 0 ) ;
const subject = new Rx.Subject();
reader.onload = event => {
subject.onNext( reader.result );
subject.onCompleted();
}
return subject
.safeApply(
this.$rootScope,
result => {
console.timeEnd( `file ${image.file.name} loaded from file system` );
image.content = reader.result;
}
)
.flatMap( result => Rx.Observable.return( image ) );
} );
}
html:
<div
ng-repeat="photo in controller.pendingPhotos"
class="mdl-card photo-frame mdl-card--border mdl-shadow--4dp">
<div class="mdl-card__title">
{{photo.file.name}}
</div>
<div class="img-placeholder mdl-card__media">
<div
ng-if="!photo.content"
class="mdl-spinner mdl-js-spinner is-active"
mdl-upgrade
></div>
<img class="img-preview" ng-if="photo.content" ng-src="{{photo.content}}"/>
</div>
<div class="mdl-card__supporting-text" ng-if="photo.response">
{{controller.formatResponse(photo.response)}}
</div>
</div>
I know that ng-repeat can be a performance issue and I will sort that but at the moment even displaying one image can take a few seconds. If I load the image from disc but don't actually display it it only takes around 50-100 ms per image to load from disc. If I display it things get MUCH slower.
I suspect that the slowdown is the browser (chrome) having to resize the image.
In a test I did with 70 images I loaded all of them into the browser and after everything was loaded and rendered scrolling performance was slow the first few times I scrolled up and down the page, after that it was smooth.
These images are around 3,000 pixels by 2,000. I am resizing them to be 200 pixels long to display them.
What are the best approaches to speed this up?
I was facing the same problem some time ago (when doing service for photographers, using angular).
Problem is not about RxJS or angular, it is rather about browser itself - it is not optimised for displaying lots of big images this way.
At first if you need to display lots of images(does not matter is it local or remote files):
Resize them before displaying (faster loading, no need for resize, lower memory consumption).
If you can - display only visible images (otherwise page would be really slow until all images would be loaded). Check this answer: How do I get the x and y positions of an element in an AngularJS directive originally trackVisibility was written to display images only when they become visible.
About displaying images from local files, things are even more complicated:
In your case you are loading files as data urls, and there is a problem: 70 images you mentioned for 3 mb each will consume at least 2.1 Gb of RAM(actually more, and obliviously will affect performance)
First recommendation is - if you can: do not use data urls, better use URL.createObjectURL and use URL.revokeObjectURL when you do not need it anymore.
Second: if you need just thumbnails - resize images locally(using canvas) before displaying them. There would be an issue with antialiasing, if it is important for you case - take a look at step-down technic described here: Html5 canvas drawImage: how to apply antialiasing And if you are supporting iOS - there can be a problem with canvas size limitation, so you will need to detect it somehow. (both issues were addressed in example below)
And the last one: if you need to create thumbnails for lots of images - do not do this at once, instead - schedule work pieces over event loop (otherwise browser would be not responsive while resizing images). And for better preformance: do this sequentially(not in parallel for all images), it may sound strange - but, it would be faster(due too lower memory consumption, and less disk reads at the same time).
In summary:
Use trackVisibility directive mentioned above to display only visible images
Do not use, data urls especially for big images.
Create resized thumbnails, before displaying them
Libraries you may find useful for implementing this:
https://github.com/blueimp/JavaScript-Load-Image
https://github.com/blueimp/JavaScript-Canvas-to-Blob
Rough code example about doing image thumbnails (most of code was copied from working project - so, it is expected to work. canvasToJpegBlob and makeThumbnail was written just now and was not tested, so there can be small mistakes):
function loadImage(imagePath) {
return Rx.Observable.create(function(observer) {
var img = new Image();
img.src = imagePath;
image.onload = function() {
observer.onNext(image);
observer.onCompleted();
}
image.onError = function(err) {
observer.onError(err);
}
});
}
// canvas edge cases detection
var maxDimm = 32000;
var ios5 = false, ios3 = false;
(function() {
if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
maxDimm = 8000;
} else {
var canvas = document.createElement('canvas');
canvas.width = 1024 * 3;
canvas.height = 1025;
if (canvas.toDataURL('image/jpeg') === 'data:,') {
ios3 = true;
} else {
canvas = document.createElement('canvas');
canvas.width = 1024 * 5;
canvas.height = 1025;
if (canvas.toDataURL('image/jpeg') === 'data:,') {
ios5 = true;
}
}
}
}());
function stepDown(src, width, height) {
var
steps,
resultCanvas = document.createElement('canvas'),
srcWidth = src.width,
srcHeight = src.height,
context;
resultCanvas.width = width;
resultCanvas.height = height;
if ((srcWidth / width) > (srcHeight / height)) {
steps = Math.ceil(Math.log(srcWidth / width) / Math.log(2));
} else {
steps = Math.ceil(Math.log(srcHeight / height) / Math.log(2));
}
if (steps <= 1) {
context = resultCanvas.getContext('2d');
context.drawImage(src, 0, 0, width, height);
} else {
var tmpCanvas = document.createElement('canvas');
var
currentWidth = width * Math.pow(2, steps - 1),
currentHeight = height * Math.pow(2, steps - 1),
newWidth = currentWidth,
newHeight = currentHeight;
if (ios3 && currentWidth * currentHeight > 3 * 1024 * 1024) {
newHeight = 1024 * Math.sqrt(3 * srcHeight / srcWidth);
newWidth = newHeight * srcWidth / srcHeight;
} else {
if (ios5 && currentWidth * currentHeight > 5 * 1024 * 1024) {
newHeight = 1024 * Math.sqrt(5 * srcHeight / srcWidth);
newWidth = newHeight * srcWidth / srcHeight;
} else {
if (currentWidth > maxDimm || currentHeight > maxDimm) {
if (currentHeight > currentWidth) {
newHeight = maxDimm;
newWidth = maxDimm * currentWidth / currentHeight;
} else {
newWidth = maxDimm;
newHeight = maxDimm * currentWidth / currentHeight;
}
}
}
}
currentWidth = newWidth;
currentHeight = newHeight;
if ((currentWidth / width) > (currentHeight / height)) {
steps = Math.ceil(Math.log(currentWidth / width) / Math.log(2));
} else {
steps = Math.ceil(Math.log(currentHeight / height) / Math.log(2));
}
context = tmpCanvas.getContext('2d');
tmpCanvas.width = Math.ceil(currentWidth);
tmpCanvas.height = Math.ceil(currentHeight);
context.drawImage(src, 0, 0, srcWidth, srcHeight, 0, 0, currentWidth, currentHeight);
while (steps > 1) {
newWidth = currentWidth * 0.5;
newHeight = currentHeight * 0.5;
context.drawImage(tmpCanvas, 0, 0, currentWidth, currentHeight, 0, 0, newWidth, newHeight);
steps -= 1;
currentWidth = newWidth;
currentHeight = newHeight;
}
context = resultCanvas.getContext('2d');
context.drawImage(tmpCanvas, 0, 0, currentWidth, currentHeight, 0, 0, width, height);
}
return resultCanvas;
}
function canvasToJpegBlob(canvas) {
return Rx.Observable.create(function(observer) {
try {
canvas.toBlob(function(blob) {
observer.onNext(blob);
observer.onCompleted();
}, 'image/jpeg');
} catch (err) {
observer.onError(err);
}
});
}
function makeThumbnail(file) {
return Observable.defer(()=> {
const fileUrl = URL.createObjectURL(file);
return loadImage(fileUrl)
.map(image => {
const width = 200;
const height = image.height * width / image.width;
const thumbnailCanvas = stepDown(image, width, height);
URL.revokeObjectURL(fileUrl);
return thubnailCanvas;
})
.flatMap(canvasToJpegBlob)
.map(canvasBlob=>URL.createObjectURL(canvasBlob))
.map(thumbnailUrl => {
return {
file,
thumbnailUrl
}
})
});
}

identify an object on canvas by a click event

Into this simple code I use an eventListener which doesn't look to work at all. The canvas display an image and the given hitpaint() function is supposed determines whether a click occurs. I cant understand why the eventListener behaves like that. Any insight would be helpful.
mycanv.addEventListener("click", function(e) {
var output = document.getElementByID("output");
ctx.fillStyle = 'blue';
//ctx.clearRect(0,0,100,20);
if (hitpaint) {
//ctx.fillText("hit",100,20);
output.innerHTML = "hit";
} else {
//ctx.fillText("miss",100,20);
output.innerHTML = "miss";
}
}, false);
The hitpaint() function is defined as:
function hitpaint(mouse_event) {
var bounding_box = mycanv.getBoundingClientRect();
var mousex = (mouse_event.clientX - bounding_box.left) *
(mycanv.width / bounding_box.width);
var mousey = (mouse_event.clientY - bounding_box.top) *
(mycanv.height / bounding_box.height);
var pixels = ctx.getImageData(mousex, mousey, 1, 1);
for (var i = 3; i < pixels.data.length; i += 4) {
// If we find a non-zero alpha we can just stop and return
// "true" - the click was on a part of the canvas that's
// got colour on it.
if (pixels.data[i] !== 0) return true;
}
// The function will only get here if none of the pixels matched in
return false;
}
Finally, the main loop which display the picture in random location into the canvas:
function start() {
// main game function, called on page load
setInterval(function() {
ctx.clearRect(cat_x, cat_y, 100, 100);
cat_x = Math.random() * mycanv.width - 20;
cat_y = Math.random() * mycanv.height - 20;
draw_katy(cat_x, cat_y);
}, 1000);
}
There are a some issues here:
As Grundy points out in the comment, the hitpaint is never called; right now it checks for it's existence and will always return true
The mouse coordinates risk ending up as fractional values which is no-go with getImageData
Scaling the mouse coordinates is usually not necessary. Canvas should preferably have a fixed size without an additional CSS size
Add boundary check for x/y to make sure they are inside canvas bitmap
I would suggest this rewrite:
mycanv.addEventListener("click", function(e) {
var output = document.getElementByID("output");
ctx.fillStyle = 'blue';
//ctx.clearRect(0,0,100,20);
if (hitpaint(e)) { // here, call hitpaint()
//ctx.fillText("hit",100,20);
output.innerHTML = "hit";
} else {
//ctx.fillText("miss",100,20);
output.innerHTML = "miss";
}
}, false);
Then in hitpaint:
function hitpaint(mouse_event) {
var bounding_box = mycanv.getBoundingClientRect();
var x = ((mouse_event.clientX - bounding_box.left) *
(mycanv.width / bounding_box.width))|0; // |0 cuts off any fraction
var y = ((mouse_event.clientY - bounding_box.top) *
(mycanv.height / bounding_box.height))|0;
if (x >= 0 && x < mycanv.width && y >= 0 && y < mycanv.height) {
// as we only have one pixel, we can address alpha channel directly
return ctx.getImageData(x, y, 1, 1).data[3] !== 0;
}
else return false; // x/y out of range
}

Categories