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/
Related
My research suggests that creating an async class method is possible but I've found myself going in loops trying to get the values from a promise in my camera config class for a three.js project. So far the geometry is all located on the server and is sent to the browser on a different API without issue. I also need to send some site parameters from the server for the camera animation to work correctly and that is where I'm having a problem.
On the client's browser I set up the camera config class which is then used by multiple functions to center and move the camera in relation to the overall geometry. I thought it would be helpful to reuse the offset2middle value since it's already precalculated on the node server. However, I can't seem to find a convenient way to get the values directly into this.cameracenter. I need the values directly because I plan to do further manipulation of the values in the various camera animation functions to make sure the motion looks polished.
Does anyone have any suggestions about how this can be accomplished?
//All in the render.js file for the client
async function get_field_parameters() {
// gets field parameters from server using standard node js api
const response = await fetch(`/get-field-parameters/`);
const data = await response.json();
return data;
}
// Camera Config Class
class SCREEN_DATA {
constructor(window) {
this.height = window.innerHeight;
this.width = window.innerWidth;
this.aspect = this.width / this.height;
this.fov = 80;
this.camerafocus = this.find_center();
}
//Method
async find_center() {
let p = await get_field_parameters();
return p.offset2middle;
}
}
camera_config = new SCREEN_DATA(window); // screen Configuration
// Example of how I'd like to use the values
function main_camera(config) {
console.log(config.camerafocus); // return [[PromiseResult]]: Object x: 50 y: 0 z: 50
// Animated Camera
const camera = new THREE.PerspectiveCamera(config.fov, config.aspect, 1, 400);
camera.position.set(200, 100, 100); // start
camera.lookAt(50, 0, 50); // Want to use camera focus values here
return camera;
}
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.
I am confused as the examples on how to use the Viewer don't seem to match the documentation of the API, some functions are not in the docs or their signature is different.
Base on the examples code, how do I pass options to the extensions I instantiate? I would like to pass my extension a callback.
Thanks!
We need to fix the doc so it does not rely anymore on the undocumented A360 viewer additional code, which is supposed to be internal. Sorry for the incovenience, we will do this asap...
For the time being, you can use the code from my viewer boilerplate sample:
function initializeViewer(containerId, urn) {
Autodesk.Viewing.Document.load(urn, function (model) {
var rootItem = model.getRootItem();
// Grab all 3D items
var geometryItems3d = Autodesk.Viewing.Document.getSubItemsWithProperties(
rootItem,
{ 'type': 'geometry', 'role': '3d' },
true);
// Grab all 2D items
var geometryItems2d = Autodesk.Viewing.Document.getSubItemsWithProperties(
rootItem,
{ 'type': 'geometry', 'role': '2d' },
true);
var domContainer = document.getElementById(containerId);
//UI-less Version: viewer without any Autodesk buttons and commands
//viewer = new Autodesk.Viewing.Viewer3D(domContainer);
//GUI Version: viewer with controls
viewer = new Autodesk.Viewing.Private.GuiViewer3D(domContainer);
viewer.initialize();
viewer.setLightPreset(8);
//Button events - two buttons to load/unload a sample extension
// Irrelevant to viewer code itself
var loadBtn = document.getElementById('loadBtn');
loadBtn.addEventListener("click", function(){
loadExtension(viewer);
});
var unloadBtn = document.getElementById('unloadBtn');
unloadBtn.addEventListener("click", function(){
unloadExtension(viewer);
});
// Illustrates how to listen to events
// Geometry loaded is fired once the model is fully loaded
// It is safe to perform operation involving model structure at this point
viewer.addEventListener(
Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
onGeometryLoaded);
//optional
var options = {
globalOffset: {
x: 0, y: 0, z: 0
}
}
// Pick the first 3D item ortherwise first 2D item
var viewablePath = (geometryItems3d.length ?
geometryItems3d[0] :
geometryItems2d[0]);
viewer.loadModel(
model.getViewablePath(viewablePath),
options);
}, function(err) {
logError(err);
});
}
Once the viewer is initialized, you can load independently each extension and pass a callback as follow:
var options = {
onCustomEventFiredByMyExtension: function() {
console.log('LMV rulez!')
}
}
viewer.loadExtension('MyExtensionId', options)
But I think a more elegant approach would be to fire events from the extension itself, which may look like this:
viewer.loadExtension('MyExtensionId')
var myExtension = viewer.getExtension('MyExtensionId')
myExtension.on('CustomEvent', function () {
console.log('LMV still rulez!')
})
See micro-events for a super simple event library.
I have close to 1000 (and at some point more) text sprite labels being rendered in a THREEJS scene. Creating the sprite objects is not an issue. When I go to add them to the scene it seems this is blocking and killing any other animations or user interactions that take place.
I have tried splitting the array of sprites into chunks and do a chunk at a time. Recently I have tried using setTimeout but am not having much luck. I am struggling with callbacks and setTimeout.
Here is what I have so far:
function addlabels(data){
label_array = [];
$.each(data.nodes, function (key, value) {
var position = value.pos;
var vector = new THREE.Vector3(position[0], position[1], position[2]);
value.label = makeTextSprite(key, {fontsize:32});
value.label.position.copy(vector);
label_array.push(value.label);
});
function chunk_labels(items, process, context){
var todo = items.concat();
setTimeout(function(){
do {
console.log(context, todo.shift());
scene.add(todo.shift());
} while(todo.length > 0);
if (todo.length > 0){
setTimeout(chunk_labels(todo), 25);
}else{
callback(items);
}
}, 25);
}
chunk_labels(label_array);
}
I am using PhaserIO in conjunction with Meteor to create a multiplayer html5 game and have run into a snag I cannot seem to figure out in a networking prototype I was making. First, the relevant code (also available as a gist):
if(Meteor.isClient) {
var game,
mainState,
mainStateInitialized,
characterData;
Template.game.game = function() {
characterData = Character.find().fetch();
if(!mainStateInitialized)
{
game = new Phaser.Game(500, 600, Phaser.AUTO, 'gameScreen');
createMainState()
}
}
}
function createMainState()
{
mainState = {
sprites: null,
playerLastFrame: characterData.length,
playerCurrentFrame: null,
charData: characterData,
preload: function() {
this.sprites = game.add.group();
game.stage.disableVisibilityChange = true;
game.load.image('hello', 'resources/hello.png');
},
create: function() {
$.each(characterData, function(index) {
var sprite = mainState.sprites.create(this.x, this.y, 'hello');
sprite.angle = this.angle;
});
},
update: function() {
this.charData = characterData;
this.playersCurrentFrame = this.charData.length;
if(this.playersLastFrame > this.playersCurrentFrame) {
//todo: remove player that left
}
else if(this.playersLastFrame < this.playersCurrentFrame) {
for(var i = playersLastFrame; i < playersCurrentFrame; i++) {
var thisData = this.charData[i],
sprite = null;
sprite = mainState.sprites.create(thisData.x, thisData.y, 'hello');
sprite.angle = thisData.angle;
}
}
for(var j = 0; j < mainState.sprites.length; j++) {
mainState.sprites.getAt(j).angle = this.charData[j].angle;
}
playersLastFrame = this.charData.length;
}
}
game.state.add('main', mainState);
game.state.start('main');
mainStateInitialized = true;
}
The idea of this prototype is to have a sprite shown in the canvas for each account in the DB. The main features I am testing are:
Dynamically adding sprites/player data seamlessly (as all proper multiplayer online games should be capable of. This will eventually pave the way for a proper join/leave system)
And to mess with creating efficient packets.
Right now, I am running into an issue with dynamically creating a new sprite when a player creates a new account. About 75% of the time, when a player makes a new account nothing happens. Meteor correctly pushes down the character data, which I can query, and mainState.sprites correctly shows the sprite data. However, nothing is rendered on the canvas.
The other 25% of the time, it works fine. In addition, if I have the code break-pointed it works 100% of the time as far as I can tell.
So, something intermittent is obviously occurring here but I can't figure out what the issue is. Is there something I am missing when adding a sprite during the update loop? Is there a better way to approach this?
I have my code on Nitrous.io, so I can run a localhost instance for you to hit if it would help in solving the problem.
The issue was I was not setting playersLastFrame = playersCurrentFrame.
I feel silly now considering this is a basic loop/compare structure. last = current at end of loop.
Sigh : (.