I want to transfer full control to worker and use off screen canvas. But there is an Image() which is tied to UI, see function makeImg. I am not intending to show the image, it has pure data usage for building a mesh. The highlighted code fully depends on UI. Is it possible (and how exactly) to do it entirely in web worker, without interchanging data with UI until calculations done and the final mesh fully generated, ready to be shown? For instance following bitmap contains the heights:
The page without perspective with full source codes is here.
I am building height terrain using above bitmap as heightmap, code is here on GitHub, in the page heightMap.html. So, I use pixel values being used to generate vertices, calculate normals, texture coordinate. The result is the terrain going to be shown in the page, here shown without texture:
async function readImgHeightMap (src, crossOrigin) {
return new Promise ((resolve, reject) => {
readImg (src, crossOrigin).then ((imgData) => {
let heightmap = [];
//j, row -- z coordinate; i, column -- x coordinate
//imgData.data, height -- y coordinate
for (let j = 0, j0 = 0; j < imgData.height; j++, j0 += imgData.width * 4) {
heightmap[j] = [];
for (let i = 0, i0 = 0; i < imgData.width; i++, i0 += 4)
heightmap[j][i] = imgData.data[j0 + i0];
}
resolve( {data:heightmap, height:imgData.height, width:imgData.width} );
});
});
}
async function readImg (src, crossOrigin) {
return new Promise ( (resolve, reject) => {
makeOffscreenFromImg (src, crossOrigin).then((canvas) => {
let ctx = canvas.getContext("2d");
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height, { colorSpace: "srgb" });
resolve(imgData);
});
});
}
async function makeOffscreenFromImg (src, crossOrigin) {
let img = makeImg(src, crossOrigin);
return new Promise((resolve, reject) => {
img.addEventListener('load', () => {
let cnv = new OffscreenCanvas(img.width, img.height);
cnv.getContext("2d").drawImage(img, 0, 0);
resolve(cnv);
});
img.addEventListener('error', (event) => { console.log(event); reject (event); } );
});
}
function makeImg (src, crossOrigin)
{
let image = new Image ();
let canvas = document.createElement("canvas");
if (crossOrigin) image.crossOrigin = crossOrigin;
image.src = src;
return image;
}
##################
PS: Just in case, to see the crater from different angles, camera can be moved with mouse when pressing SHIFT, or rotated when pressing CTRL. Also click event for permanent animation, or tap if on mobile device.
PS1: Please do not use heightmap images for personal purposes. These have commercial copyright.
Use createImageBitmap from your Worker, passing a Blob you'd have fetched from the image URL:
const resp = await fetch(imageURL);
if (!resp.ok) {
throw "network error";
}
const blob = await resp.blob();
const bmp = await createImageBitmap(blob);
const { width, height } = bmp;
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d");
ctx.drawImage(bmp, 0, 0);
bmp.close();
const imgData = ctx.getImageData(0, 0, width, height);
If required, you could also create the ImageBitmap from the <img> tag in the main thread, and transfer it to your Worker.
Related
I'm trying to download PDF with SVG content using jsPDF library, it is able to download the file, but there is no content inside it, it is empty PDF.
This is my code:
const downloadPDF = (goJSDiagram) => {
const svg = goJSDiagram.makeSvg({scale: 1, background: "white"});
const svgStr = new XMLSerializer().serializeToString(svg);
const pdfDoc = new jsPDF();
pdfDoc.addSvgAsImage(svgStr, 0, 0, pdfDoc.internal.pageSize.width, pdfDoc.internal.pageSize.height)
pdfDoc.save(props.model[0].cName?.split(" (")[0] + ".pdf");
}
When I do console.log(svgStr), I can see the SVG XML string. What changes should I make to render the content inside PDF?
I think I know what is going on after getting a good hint from this Github issue:
There's the issue that addSvgAsImage() is asynchronous
You are not awaiting the call to finish before calling save! That means you are trying to save before the SVG has started rendering to the PDF.
See the quite simple code in question:
jsPDFAPI.addSvgAsImage = function(
// ... bla bla
return loadCanvg()
.then(
function(canvg) {
return canvg.fromString(ctx, svg, options);
},
function() {
return Promise.reject(new Error("Could not load canvg."));
}
)
.then(function(instance) {
return instance.render(options);
})
.then(function() {
doc.addImage(
canvas.toDataURL("image/jpeg", 1.0),
x,
y,
w,
h,
compression,
rotation
);
});
As you see, it is just a chain of Thenables. So you simply need to await the Promise, which means your code would look something like this in ES2015+:
const downloadPDF = async (goJSDiagram) => {
const svg = goJSDiagram.makeSvg({scale: 1, background: "white"});
const svgStr = new XMLSerializer().serializeToString(svg);
const pdfDoc = new jsPDF();
await pdfDoc.addSvgAsImage(svgStr, 0, 0, pdfDoc.internal.pageSize.width, pdfDoc.internal.pageSize.height)
pdfDoc.save(props.model[0].cName?.split(" (")[0] + ".pdf");
}
After lot of searching, I found the right way to do this, though the content rendered is little blurred.
const waitForImage = imgElem => new Promise(resolve => imgElem.complete ? resolve() : imgElem.onload = imgElem.onerror = resolve);
const downloadPDF = async (goJSDiagram) => {
const svg = goJSDiagram.makeSvg({scale: 1, background: "white"});
const svgStr = new XMLSerializer().serializeToString(svg);
const img = document.createElement('img');
img.src = 'data:image/svg+xml;base64,' + window.btoa(svgStr);
waitForImage(img)
.then(_ => {
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
canvas.getContext('2d').drawImage(img, 0, 0, 500, 500);
const pdfDoc = new jsPDF('p', 'pt', 'a4');
pdfDoc.addImage(canvas.toDataURL('image/png', 1.0), 0, 200, 500, 500);
pdfDoc.save(props.model[0].cName?.split(" (")[0] + ".pdf");
});
}
I'm trying to learn JavaScript, making my first game. How I can make all images onload in one function and later draw it in the canvas making my code shorter?
How can I put a lot of images in an array and later us it in a function.
This is my third day of learning JavaScript.
Thanks in advance.
var cvs = document.getElementById('canvas');
var ctx = cvs.getContext('2d');
//load images
var bird = new Image();
var bg = new Image();
var fg = new Image();
var pipeNorth = new Image();
var pipeSouth = new Image();
//images directions
bg.src = "assets/bg.png";
bird.src = "assets/bird.png";
fg.src = "assets/fg.png";
pipeNorth.src = "assets/pipeNorth.png";
pipeSouth.src = "assets/pipeSouth.png";
var heightnum = 80;
var myHeight = pipeSouth.height+heightnum;
var bX = 10;
var bY = 150;
var gravity = 0.5;
// Key Control :D
document.addEventListener("keydown",moveUP)
function moveUP(){
bY -= 20;
}
//pipe coordinates
var pipe = [];
pipe[0] = {
x : cvs.width,
y : 0
}
//draw images
//Background img
bg.onload = function back(){
ctx.drawImage(bg,0,0);
}
//pipe north
pipeNorth.onload = function tubo(){
for(var i = 0; i < pipe.length; i++){
ctx.drawImage(pipeNorth,pipe[i].x,pipe[i].y);
pipe[i].x--;
}
}
pipeSouth.onload = function tuba(){
ctx.drawImage(pipeSouth,pipe[i].x,pipe[i].y+myHeight);
}
bird.onload = function pajaro(){
ctx.drawImage(bird,bX,bY);
bY += gravity;
requestAnimationFrame(pajaro);
}
fg.onload = function flor(){
ctx.drawImage(fg,0,cvs.height - fg.height);
}
moveUP();
back();
tuba();
pajaro();
flor();
This can be done with Promise.all. We'll make a new promise for each image we want to load, resolving when onload is called. Once Promise.all resolves, we can call our initialize function and continue on with the rest of our logic. This avoids race conditions where the main game loop's requestAnimationFrame is called from bird.onload, but it's possible that pipe entities and so forth haven't loaded yet.
Here's a minimal, complete example:
const initialize = images => {
// images are loaded here and we can go about our business
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = 400;
canvas.height = 200;
const ctx = canvas.getContext("2d");
Object.values(images).forEach((e, i) =>
ctx.drawImage(e, i * 100, 0)
);
};
const imageUrls = [
"http://placekitten.com/90/100",
"http://placekitten.com/90/130",
"http://placekitten.com/90/160",
"http://placekitten.com/90/190",
];
Promise.all(imageUrls.map(e =>
new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = e;
})
)).then(initialize);
Notice that I used an array in the above example to store the images. The problem this solves is that the
var foo = ...
var bar = ...
var baz = ...
var qux = ...
foo.src = ...
bar.src = ...
baz.src = ...
qux.src = ...
foo.onload = ...
bar.onload = ...
baz.onload = ...
qux.onload = ...
pattern is extremely difficult to manage and scale. If you decide to add another thing into the game, then the code needs to be re-written to account for it and game logic becomes very wet. Bugs become difficult to spot and eliminate. Also, if we want a specific image, we'd prefer to access it like images.bird rather than images[1], preserving the semantics of the individual variables, but giving us the power to loop through the object and call each entity's render function, for example.
All of this motivates an object to aggregate game entities. Some information we'd like to have per entity might include, for example, the entity's current position, dead/alive status, functions for moving and rendering it, etc.
It's also a nice idea to have some kind of separate raw data object that contains all of the initial game state (this would typically be an external JSON file).
Clearly, this can turn into a significant refactor, but it's a necessary step when the game grows beyond small (and we can incrementally adopt these design ideas). It's generally a good idea to bite the bullet up front.
Here's a proof-of-concept illustrating some of the the musings above. Hopefully this offers some ideas for how you might manage game state and logic.
const entityData = [
{
name: "foo",
path: "http://placekitten.com/80/80",
x: 0,
y: 0
},
{
name: "baz",
path: "http://placekitten.com/80/150",
x: 0,
y: 90
},
{
name: "quux",
path: "http://placekitten.com/100/130",
x: 90,
y: 110
},
{
name: "corge",
path: "http://placekitten.com/200/240",
x: 200,
y: 0
},
{
name: "bar",
path: "http://placekitten.com/100/100",
x: 90,
y: 0
}
/* you can add more properties and functions
(movement, etc) to each entity
... try adding more entities ...
*/
];
const entities = entityData.reduce((a, e) => {
a[e.name] = {...e, image: new Image(), path: e.path};
return a;
}, {});
const initialize = () => {
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.width = innerWidth;
canvas.height = innerHeight;
const ctx = canvas.getContext("2d");
for (const key of Object.keys(entities)) {
entities[key].alpha = Math.random();
}
(function render () {
ctx.clearRect(0, 0, canvas.width, canvas.height);
Object.values(entities).forEach(e => {
ctx.globalAlpha = Math.abs(Math.sin(e.alpha += 0.005));
ctx.drawImage(e.image, e.x, e.y);
ctx.globalAlpha = 1;
});
requestAnimationFrame(render);
})();
};
Promise.all(Object.values(entities).map(e =>
new Promise((resolve, reject) => {
e.image.onload = () => resolve(e.image);
e.image.onerror = () =>
reject(`${e.path} failed to load`)
;
e.image.src = e.path;
})
))
.then(initialize)
.catch(err => console.error(err))
;
In javascript, I have a reference to a DOM <img> element. I need to change the image displayed by the <img> from within the javascript.
So far, I have tried this by changing the image.src attribute to the URL of the new image. This worked, but there is still a problem: I need the image to be changed many times per second, and changing the src attribute causes the browser to do a new GET request for the image (which is already cached), which puts strain on the web server since there will be hundreds of simultanious clients.
My current (inefficient) solution is done like this:
let image = document.querySelector("img");
setInterval(function(){
image.src = getNextImageURL();//this causes a GET request
}, 10);
function getNextImageURL() {
/*...implementation...*/
}
<img>
I am looking for a more efficient way of changing the image, that does not cause any unnecessary HTTP requests.
I think you need to code in a different way...
if you want to reduce requests you should combine all image in one sprite Sheet.
this trick is used so many in-game animations.
But you will need to change <img/> tag to another tag like <div></div>
the idea here is that we have one image and we just change the viewport for what we want
sample For sprite sheet
let element = document.querySelector("div");
let i = 1
setInterval(function(){
element.style.backgroundPosition= getNextImagePostion(i++);//this will change the posion
}, 100);
function getNextImagePostion(nextPos) {
/*...implementation...*/
// this just for try
// 6 is the number of images in sheet
// 20 is the height for one segment in sheet
var posX = 100*(nextPos%6);
// first is position on x and latter is position on y
return "-"+posX+"px 0px ";
}
.image {
width: 100px; /* width of single image*/
height: 100px; /*height of single image*/
background-image: url(https://picsum.photos/500/100?image=8) /*path to your sprite sheet*/
}
<div class="image">
</div>
If the sprite-sheet idea doesn't work (e.g because you have big images), then use a canvas.
You just need to preload all your images, store them in an Array and then draw them one after the other on your canvas:
const urls = new Array(35).fill(0).map((v, i) =>
'https://picsum.photos/500/500?image=' + i
);
// load all the images
const loadImg = Promise.all(
urls.map(url =>
new Promise((res, rej) => {
// each url will have its own <img>
const img = new Image();
img.onload = e => res(img);
img.onerror = rej;
img.src = url;
})
)
);
// when they're all loaded
loadImg.then(imgs => {
// prepare the canvas
const canvas = document.getElementById('canvas');
// set its size
canvas.width = canvas.height = 500;
// get the drawing context
const ctx = canvas.getContext('2d');
let i = 0;
// start the animation
anim();
function anim() {
// do it again at next screen refresh (~16ms on a 60Hz monitor)
requestAnimationFrame(anim);
// increment our index
i = (i + 1) % imgs.length;
// draw the required image
ctx.drawImage(imgs[i], 0, 0);
}
})
.catch(console.error);
<canvas id="canvas"></canvas>
And if you wish time control:
const urls = new Array(35).fill(0).map((v, i) =>
'https://picsum.photos/500/500?image=' + i
);
// load all the images
const loadImg = Promise.all(
urls.map(url =>
new Promise((res, rej) => {
// each url will have its own <img>
const img = new Image();
img.onload = e => res(img);
img.onerror = rej;
img.src = url;
})
)
);
// when they're all loaded
loadImg.then(imgs => {
// prepare the canvas
const canvas = document.getElementById('canvas');
// set its size
canvas.width = canvas.height = 500;
// get the drawing context
const ctx = canvas.getContext('2d');
const duration = 100; // the number of ms each image should last
let i = 0;
let lastTime = performance.now();
// start the animation
requestAnimationFrame(anim);
// rAF passes a timestamp
function anim(time) {
// do it again at next screen refresh (~16ms on a 60Hz monitor)
requestAnimationFrame(anim);
const timeDiff = time - lastTime;
if(timeDiff < duration) { // duration has not yet elapsed
return;
}
// update lastTime
lastTime = time - (timeDiff - duration);
// increment our index
i = (i + 1) % (imgs.length);
// draw the required image
ctx.drawImage(imgs[i], 0, 0);
}
})
.catch(console.error);
<canvas id="canvas"></canvas>
I made a simple application, which takes photo from video tag and make it gray, available in full here: Canvas WebWorker PoC:
const photoParams = [
0, //x
0, //y
320, //width
240, //height
];
async function startVideo () {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: true,
});
const video = document.querySelector('#video');
video.srcObject = stream;
video.play();
return video;
}
function takePhoto () {
const video = document.querySelector('#video');
const canvas = document.querySelector('#canvas');
canvas.width = 320;
canvas.height = 240;
const context = canvas.getContext('2d');
context.drawImage(video, ...photoParams);
const imageData = applyFilter({imageData:
context.getImageData(...photoParams)});
context.putImageData(imageData, 0, 0);
return canvas.toDataURL("image/png")
}
function setPhoto () {
const photo = takePhoto();
const image = document.querySelector('#image');
image.src = photo;
}
startVideo();
const button = document.querySelector('#button');
button.addEventListener('click', setPhoto);
In one of functions, I placed long, unnecessary for loop to make it really slow:
function transformPixels ({data}) {
let rawPixels;
const array = new Array(2000);
for (element of array) {
rawPixels = [];
const pixels = getPixels({
data,
});
const filteredPixels = [];
for (const pixel of pixels) {
const average = getAverage(pixel);
filteredPixels.push(new Pixel({
red: average,
green: average,
blue: average,
alpha: pixel.alpha,
}));
}
for (const pixel of filteredPixels) {
rawPixels.push(...pixel.toRawPixels());
}
}
return rawPixels;
};
And I created Web Worker version which, as I thougt, should be faster cause it not break the main thread:
function setPhotoWorker () {
const video = document.querySelector('#video');
const canvas = document.querySelector('#canvas');
canvas.width = 320;
canvas.height = 240;
const context = canvas.getContext('2d');
context.drawImage(video, ...photoParams);
const imageData = context.getImageData(...photoParams);
const worker = new Worker('filter-worker.js');
worker.onmessage = (event) => {
const rawPixelsArray = [...JSON.parse(event.data)];
rawPixelsArray.forEach((element, index) => {
imageData.data[index] = element;
});
context.putImageData(imageData, 0, 0);
const image = document.querySelector('#image');
image.src = canvas.toDataURL("image/png");
}
worker.postMessage(JSON.stringify([...imageData.data]));
}
Which could be run in this way:
button.addEventListener('click', setPhotoWorker);
Worker code is almost exactly the same as single-threaded version, except one thing - to improve messaging performance, string is sent instead of array of numbers:
worker.onmessage = (event) => {
const rawPixelsArray = [...JSON.parse(event.data)];
};
//...
worker.postMessage(JSON.stringify([...imageData.data]));
And inside filter-worker.js:
onmessage = (data) => {
const rawPixels = transformPixels({data: JSON.parse(data.data)});
postMessage(JSON.stringify(rawPixels));
};
The problem is that worker version is always about 20-25% slower than main thread version. First I thought it may be size of message, but in my laptop I have 640 x 480 camera, which gives 307 200 items - which I don't think are expensive enough to be reason why, for 2000 for iterations, leads to results: main thread: about 160 seconds, worker about 200 seconds. You can download app from Github repo and check it on your own. The pattern is quite the same here - worker is always 20-25% slower. Without using JSON API, worker needs something like 220 seconds to finish its job. The only one reason which I thought is that worker thread has very low priority, and in my application, where main thread has not too much things to do it is simply slower - and in real-world app, where main thread might be busier, worker will win. Do you have any ideas why worker is so slow? Thank you for every answer.
I'm currently making a Word Web Add-in.
This uses Internet Explorer as engine.
My Add-in needs to load multiple selected images from the users computer.
Because some of the selected images might be quite big, I resize them using HTML5 canvas. This is my code to resize:
function makeSmallImage(imageContainer, retries)
{
if (retries === undefined)
retries = 0;
console.log('Resizing image..')
return new Promise(function (resolve, reject)
{
img = img || new Image();
img.onload = function ()
{
// calculate new size
var width = 200;
var height = Math.floor((width / img.naturalWidth) * img.naturalHeight);
console.log('new size', width, height);
try
{
// create an off-screen canvas
canvas = canvas || document.createElement('canvas'),
ctx = ctx || canvas.getContext('2d');
// antialiasing
ctx.imageSmoothingEnabled = true;
// set its dimension to target size
canvas.width = width;
canvas.height = height;
// draw source image into the off-screen canvas:
ctx.drawImage(img, 0, 0, width, height);
// clean up
imageContainer.largeData = undefined;
if (img.src.substr(0, 4) === 'blob')
URL.revokeObjectURL(img.src);
img.src = '';
// encode image to data-uri with base64 version of compressed image
var newDataUri = canvas.toDataURL();
ctx.clearRect(0, 0, canvas.width, canvas.height);
console.log('Image resized!');
imageContainer.resizedData = newDataUri;
resolve(imageContainer);
}
catch (e)
{
if (img.src !== undefined && img.src.substr(0, 4) === 'blob')
URL.revokeObjectURL(img.src);
console.log(e);
if (e.message === "Unspecified error." && retries < 5)
{
setTimeout(function (imgContainer, re)
{
makeSmallImage(imgContainer, re).then(resolve).catch(reject);
}, 2000, imageContainer, retries + 1);
}
else
reject('There was an error while trying to resize one of the images!');
}
};
try
{
var blob = new Blob([imageContainer.largeData]);
img.src = URL.createObjectURL(blob);
} catch (e)
{
reject(e);
}
});
}
'img', 'canvas' and 'ctx' are global variables, so the same elements are reused.
'imgcontainer.largedata' is an uint8array. To avoid a lot of memory usage i'm loading and resizing the images one by one.
Despite of that, after loading for example 120 images of 10mb, it might happen that I get an error:
Unable to decode image at URL:
'blob:D5EFA3E0-EDE2-47E8-A91E-EAEAD97324F6'
I then get an exception "Unspecified error", with not a lot more info.
You can see in the code now that I added a litle mechanism to try again, but all new attempts fail.
I think the reason is that internet explorer is using too much memory. I think some resources are not being cleaned up correctly, but I can't seem to spot a memory leak in my code here (if you can, please let me know).
Does anybody have an idea of how I could fix this, or work around this?
if you try to resize the image, why not directly use the office APIs? you can first get the images, then use the height/width property to resize it, such as
image1.height = 5; image1.width = 5;