I'm trying to load paths from different SVG files and keep them in an array. I don't really want them in the canvas, since I'm mostly using them to create new paths from them. However, I do want from time to time to actually add them to the canvas to be rendered.
Here's a code snippet that shows my problem:
var canvas = new fabric.Canvas('c', {
backgroundColor: 'skyblue'
});
canvas.renderAll();
var orig_shapes = []; // I want to store the paths here
function loadShape(url){
fabric.loadSVGFromURL(url, function(paths, options){
let shape = paths[0];
orig_shapes.push(shape);
});
}
loadShape('path0.svg');
loadShape('path1.svg');
// Add all the loaded shapes to the canvas
console.log(orig_shapes.length);
orig_shapes.forEach(function(shape, index, arr) {
canvas.add(orig_shapes[index]);
});
canvas.renderAll();
This script is loaded at the end of the <body> element inside the web page.
I expected to see the paths rendered and a 2 in the console. Unfortunately, all I get is a blue background and a 0.
Despite this, if I check orig_shapes.length() inside the console, I do get a 2 back; so apparently the paths are eventually pushed to the array (but not when I need to). I can even add the paths to the canvas writing canvas.add(orig_shapes[i]) in the console. They are rendered with no problems.
So what's the problem? Why isn't this working as expected?
As Durga commented, loadSVGFromURL is an asynchronous function, so I needed to check first that the SVGs had been loaded. Here's a version of the script that uses Promises to make sure of this:
var canvas = new fabric.Canvas('c', {
backgroundColor: 'skyblue'
});
canvas.renderAll();
var orig_shapes = [];
function loadShape(url){
return new Promise(function(resolve, reject){
fabric.loadSVGFromURL(url, function(paths, options){
let shape = paths[0];
orig_shapes.push(shape);
resolve(shape);
});
});
}
function loadShapes(urls, shape_callback) {
let promises = [];
urls.forEach(function(url) {
promises.push(loadShape(url).then(shape_callback));
});
return Promise.all(promises);
}
function addToCanvas(object) {
canvas.add(object);
}
loadShapes(['path0.svg', 'path1.svg'])
.then(function(objs) {
objs.forEach(addToCanvas);
});
loadShape and loadShapes both allow me to set especific actions to execute when a single shape is loaded and when all shapes have been loaded, respectively.
Related
I am trying to implement a in browser raster drawing plugin for the leaflet library that that extends the leaflets GridLayer api. Essentially for every tile there is function createTile that returns a canvas with some drawing on it. and leaflet shows the tile in correct position.
initialize: function(raster_data){
this.raster_data = raster_data;
},
createTile: function (tile_coords) {
let _tile = document.createElement('canvas');
let _tile_ctx = _tile.getContext('2d');
// do some drawing here with values from this.raster_data
return _tile;
}
This implementation is so far working fine. Than I thought of offloading drawing with offscreen-canvas in a webworker. so I restructured the code like this
initialize: function(raster_data){
this.raster_data = raster_data;
this.tile_worker = new Worker('tile_renderer.js')
},
createTile: function (tile_coords) {
let _tile = document.createElement('canvas').transferControlToOffscreen();
this.tile_worker.postMessage({
_tile: _tile,
_raster_data: this.raster_data
},[_tile])
return _tile;
}
This works but every now and then i see a canvas that is just blank. That thing is quite random I don't know start from where and how should I debug this. can this be a problem that I am using a single worker for rendering every tile? any help is appreciated. Here is an example of a blank canvas.
This a known bug: https://crbug.com/1202481
The issue appears when too many OffscreenCanvases are sent to the Worker serially.
The workaround is then to batch send all these OffscreenCanvases in a single call to postMessage().
In your code you could achieve this by storing all the objects to be sent and use a simple debouncing strategy using a 0 timeout to send them all at once:
createTile: function (tile_coords) {
let _tile = document.createElement('canvas');
_tile.setAttribute('width', 512);
_tile.setAttribute('height', 512);
let _off_tile = _tile.transferControlToOffscreen();
this.tiles_to_add.push( _off_tile ); // store for later
clearTimeout( this.batch_timeout_id ); // so that the callback is called only once
this.batch_timeout_id = setTimeout( () => {
// send them all at once
this.tileWorker.postMessage( { tiles: this.tiles_to_add }, this.tiles_to_add );
this.tiles_to_add.length = 0;
});
return _tile;
}
Live example: https://artistic-quill-tote.glitch.me/
This is a specific problem so I'm going to do my best explaining it.
I have a single canvas on my page. The user uploads a batch of files taken from the same camera angle. The canvas displays one of the images and the user draws over the image in a way that depicts an area of interest. The program then goes over this area of interest and extracts relevant RGB data from the pixels inside the area.
Now, what I'm having an issue with is then switching the canvas over to the next image to process that one as well. I have the area of interest still specified as an array of indexes inside the area. But, when trying to obtain the RGB data from the rest of the images, it only gives me the info about the first image.
Below are some relevant code snippets as well as their function.
This function loads up the next image in the fileUpload to the canvas
function readNextImage(i) {
if (fileUpload.files && fileUpload.files[i]) {
var FR = new FileReader();
FR.onload = function(e) {
fabric.Image.fromURL(e.target.result, function(img) {
img.set({
left: 0,
top: 0,
evented: false
});
img.scaleToWidth(canvas.width);
img.setCoords();
canvas.add(img);
})
};
FR.readAsDataURL(fileUpload.files[i]);
}
}
This snippet loads up an array with the image data.
var imgData = context.getImageData(0,0,canvas.width,canvas.height);
var data = imgData.data;
So currently, I'm able to go through 'data' the first time and obtain the relevant RGB info I need. After I do that, I call the readNextImage() function, it loads it into the canvas, then I make another call to
var imgData = context.getImageData(0,0,canvas.width,canvas.height);
var data = imgData.data;
to hopefully load the arrays with the new values. This is where it seems to fail as the values are the same as the previous iteration.
Is there a chance the program isn't given enough time between loading the canvas with the new image and trying to get the new data values? Is there something else that's going on with the canvas that I'm unaware of?
Here's where I'm actually calling the readNextImage() function
for (var image_num=0; image_num<fileUpload.files.length; image_num+=1){
imgData = context.getImageData(0,0,canvas.width,canvas.height);
data = imgData.data;
/*Lines of code that parses through the data object specified above*/
readNextImage(image_num);
}
I'm hoping the problem is clear, let me know if anything is confusing.
Edit 2:
links to articles about promises: promises 1 promises 2 promises 3
Edit: Instead of using a global canvas, use a local one created for each image instance:
for (var image_num=0; image_num<fileUpload.files.length; image_num+=1){
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
readNextImage(image_num,canvas);
imgData = context.getImageData(0,0,canvas.width,canvas.height);
data = imgData.data;
/*Lines of code that parses through the data object specified above*/
}
function readNextImage(i,canvas) {
if (fileUpload.files && fileUpload.files[i]) {
var FR = new FileReader();
FR.onload = function(e) {
fabric.Image.fromURL(e.target.result, function(img) {
img.set({
left: 0,
top: 0,
evented: false
});
img.scaleToWidth(canvas.width);
img.setCoords();
canvas.add(img);
})
};
FR.readAsDataURL(fileUpload.files[i]);
}
}
The problem could be here (the previous image is still attached to the canvas):
canvas.add(img);
try removing the previous child(ren) before adding another one by replacing that line with:
while (canvas.firstChild) {
canvas.removeChild(canvas.firstChild);
}
canvas.add(img);
canvas.getContext("2d").drawImage(img, 0, 0);
I use this code to setup a texture atlas animation:
PIXI.loader
.add('out2', 'assets/out2.png')
.load(function (loader, resources){
onRotationsLoaded(loader, resources)
});
function onRotationsLoaded(loader, resources) {
first = new PIXI.extras.AnimatedSprite(setupFrames(resources["out2"].texture.baseTexture));
app.renderer.plugins.prepare.upload(first, function(){
console.log("loaded first");
// ready to go
});
}
function setupFrames(name) {
var frames = [];
array is an array that stores correct position for each frame of animation
for (var i = 0; i < array.length; i++) {
var rect = new PIXI.Rectangle(array[i].frame.x, array[i].frame.y, array[i].frame.w, array[i].frame.h);
frames.push(new PIXI.Texture(name, rect));
}
return frames;
}
I would like to change the texture of the AnimatedSprite first in a click event or something. The new texture needs to be fetched from the server(I do not want to load it at start, because there are too many of them). I could destroy first and create second AnimatedSprite, but is there a way to just change it's texture atlas image?
I'd say simply replacing AnimatedSprite._textures would work.
first.textures = setupFrames('secondOne');
If the new texture has different frame counts from the previous one, you might like to call AnimatedSprite.prototype.gotoAndPlay(frame) right after replacing texture to reset the current frame.
I have this method that grabs an image before it's saved to a parse.com database and scales it down.
Take a look at the code:
var Image = require("parse-image"); // module
Parse.Cloud.beforeSave("Garments", function(request, response) {
Parse.Cloud.httpRequest({
url: request.object.get("image").url()
}).then(function(response) {
var image = new Image();
return image.setData(response.buffer);
}).then(function(image) {
// Resize the image.
return image.scale({
width: 300,
height: 450
});
}).then(function(image) {
// Make sure it's a JPEG to save disk space and bandwidth.
return image.setFormat("JPEG");
}).then(function(image) {
// Get the image data in a Buffer.
return image.data();
}).then(function(buffer) {
// Save the image into a new file.
var base64 = buffer.toString("base64");
var cropped = new Parse.File("image.jpg", { base64: base64 });
return cropped.save();
}).then(function(cropped) {
// Attach the image file to the original object.
request.object.set("image", cropped);
}).then(function(result) {
response.success();
}, function(error) {
response.error(error);
});
});
Question:
Is it possible to do above for 5 more images?
I have 6 image columns altogether.
image, image2, image3, image4, image5, image6
A row will never exist without the "image" column being populated. The other images are optional. So when scaling I need to go ahead and scale "image" and if image2, image3, image4, image5 and image6 don't exist don't throw any errors. If they do exist then scale them down too.
I'm sitting here scratching my head trying to come up with an efficient way to code this. I'd really appreciate if a javascript expert could come up with something.
I don't feel me repeating this code a few more times is efficient at all.
Thanks for your time
Turn most of that code into a function that returns the final promise and use Parse.Promise.when() to wait for an array of promises to finish, here's a bit to get you started:
var imagePromises = [];
var garment = request.object;
// you said "image" is always populated, so always add it
imagePromises.push(createImagePromise(garment, "image", garment.get("image").url()));
// now conditionally add the other promises, using a loop to further reduce repeated code
for (var i = 2; i < 7; i++) {
var imageColumn = "image" + i;
if (garment.get(imageColumn) && garment.get(imageColumn).url()) {
imagePromises.push(createImagePromise(garment, imageColumn, garment.get(imageColumn).url()));
}
}
// now we have all the promises, wait for them all to finish before we're done
Parse.Promise.when(imagePromises).then(function () {
response.success();
}, function (error) {
response.error(error);
});
The only last part is to make the createImagePromise() function.
function createImagePromise(garment, imageColumn, url) {
// we want to return the promise
return Parse.Cloud.httpRequest({
url: url
}).then(function (response) {
// ... etc ...
}).then(function (cropped) {
// Attach the image file to the original object.
garment.set(imageColumn, cropped);
});
}
NOTE:
There is a limit to how long this is allowed to run, beforeSave only has 3 seconds to run before it gets terminated, which might not be long enough to process 6 images.
Im trying to load in jpeg images, frame by frame to create an sequence animation of jpeg images. I'm attempting to load them in a recursive loop using javascript. I need to load images in linearly to achieve progressive playback of the animation. (start playback before all frames are loaded) I get a Stack overflow at line: 0 error from IE due to the natural recursion of the function. (My real code loads in over 60+ frames)
Here is a basic example of how I'm doing this:
var paths = ['image1.jpg', 'image2.jpg', 'image3.jpg']; //real code has 60+ frames
var images = [];
var load_index = 0;
var load = function(){
var img = new Image();
img.onload = function(){
if(load_index<=paths.length){
load_index++;
load();
}else{
alert('done loading');
}
}
img.src = paths[load_index];
images.push(img);
}
It seems I can avoid this error by using a setTimeout with an interval of 1 when calling the next step of the load. This seems to let IE "breathe" before loading the next image, but decreases the speed at which the images load dramatically.
Any one know how to avoid this stack overflow error?
http://cappuccino.org/discuss/2010/03/01/internet-explorer-global-variables-and-stack-overflows/
The above link suggests that wrapping the function to remove it from the window object will help avoid stack overflow errors. But I then see strangeness with it only getting about 15 frames through the sequence and just dies.
Put simply, don't use a recursive function for this situation, there isn't any need:
var paths = ['image1.jpg', 'image2.jpg', 'image3.jpg'];
var images = [];
var loads = [];
/// all complete function, probably should be renamed to something with a
/// unique namespace unless you are working within your own function scope.
var done = function(){
alert('all loaded');
}
var loaded = function(e,t){
/// fallbacks for old IE
e = e||Event; t = e.target||e.srcElement;
/// keep a list of the loaded images, you can delete this later if wanted
loads.push( t.src );
if ( loads.length >= paths.length ) {
done();
}
}
var load = function(){
var i, l = paths.length, img;
for( i=0; i<l; i++ ){
images.push(img = new Image());
img.onload = loaded;
img.src = paths[i];
}
}
In fact, as you are finding, the method you are using currently is quite intensive. Instead, the above version doesn't create a new function for each onload listener (saves memory) and will trigger off as many concurrent loads as your browser will allow (rather than waiting for each image load).
(the above has been manually typed and not tested, as of yet)
update
Ah, then it makes more sense as to why you are doing things this way :) In that case then your first approach using the setTimeout would probably be the best solution (you should be able to use a timeout of 0). There is still room for rearranging things to see if you can avoid that though. The following may get around the problem...
var paths = ['image1.jpg', 'image2.jpg', 'image3.jpg'];
var images = []; /// will contain the image objects
var loads = []; /// will contain loaded paths
var buffer = []; /// temporary buffer
var done = function(){ alert('all loaded'); }
var loaded = function(e,t){
e = e||Event; t = e.target||e.srcElement; loads.push( t.src );
/// you can do your "timing/start animation" calculation here...
/// check to see if we are complete
if ( loads.length >= paths.length ) { done(); }
/// if not fire off the next image load
else { next(); }
}
var next = function(){
/// current will be the next image
var current = buffer.shift();
/// set the load going for the current image
if ( current ) { current.img.src = current.path; }
}
var load = function(){
var i, l = paths.length, img;
for( i=0; i<l; i++ ){
img = new Image();
img.onload = loaded;
/// build up a list of images and paths to load
buffer.push({ img: img, path: paths[i] });
}
/// set everything going
next();
}
If the above doesn't do it, another way of getting around the issue would be to step through your list of paths, one at a time, and append a string of image markup (that would render off-screen) to the DOM with it's own onload="next()" handler... next() would be responsible for inserting the next image. By doing this it would hand off the triggering of the load and the subsequent load event to outside of your code, and should get around stacking calls.