I'm a beginner using web workers and I'm dealing with a little problem.
I'm creating several workers to process audio buffers and draw its waveform on a offscreen canvas:
main thread:
// foreach file
let worker = new Worker('js/worker.js');
let offscreenCanvas = canvas.transferControlToOffscreen();
worker.addEventListener('message', e => {
if (e.data == "finish") {
worker.terminate();
}
});
worker.postMessage({canvas: offscreenCanvas, pcm: pcm}, [offscreenCanvas]);
// end foreach
worker:
importScripts('waveform.js');
self.addEventListener('message', e => {
let canvas = e.data.canvas;
let pcm = e.data.pcm;
displayBuffer(canvas, pcm); // 2d draw function over canvas
self.postMessage('finish');
});
The result is strange. The thread is terminate immediately at displayBuffer() finish, but as you can see in the profiling, the GPU is still rendering the canvas, which sometimes causes that render crash. No error, only black canvas.
I'm running over Chrome 83.0
That is to be expected, "committing" to the main thread is not done synchronously, but when the browser dims it appropriate (i.e often at next painting frame) so when you call worker.terminate(), the actual painting may not have occurred yet, and won't ever.
Here is an live repro for curious:
const worker_script = `
self.addEventListener('message', (evt) => {
const canvas = evt.data;
const ctx = canvas.getContext( "2d" );
// draw a simple grid of balck squares
for( let y = 0; y<canvas.height; y+= 60 ) {
for( let x = 0; x<canvas.width; x+= 60 ) {
ctx.fillRect(x+10,y+10,40,40);
}
}
self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => worker.terminate();
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), sometimes it will work, sometimes it won't.</h3>
<canvas width="500" height="500"></canvas>
To circumvent this, there is an OffscreenCanvasRendering2DContext.commit() method that you could call before you terminate your worker, but it's currently hidden under chrome://flags/#enable-experimental-web-platform-features.
if( !( 'commit' in OffscreenCanvasRenderingContext2D.prototype ) ) {
throw new Error( "Your browser doesn't support the .commit() method," +
"please enable it from chrome://flags" );
}
const worker_script = `
self.addEventListener('message', (evt) => {
const canvas = evt.data;
const ctx = canvas.getContext( "2d" );
// draw a simple grid of balck squares
for( let y = 0; y<canvas.height; y+= 60 ) {
for( let x = 0; x<canvas.width; x+= 60 ) {
ctx.fillRect(x+10,y+10,40,40);
}
}
// force drawing to element
ctx.commit();
self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => worker.terminate();
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), it will always work ;-)</h3>
<canvas width="500" height="500"></canvas>
So a workaround, without this method is to wait before you terminate your Worker. Though there doesn't seem to be a precise amount of time nor a special event we can wait for, by tests-and-error I came up to wait three painting frames, but that may not do it on all devices, so you may want to be safest and wait a few plain seconds, or even just to let the GarbageCollector take care of it:
const worker_script = `
self.addEventListener('message', (evt) => {
const canvas = evt.data;
const ctx = canvas.getContext( "2d" );
// draw a simple grid of balck squares
for( let y = 0; y<canvas.height; y+= 60 ) {
for( let x = 0; x<canvas.width; x+= 60 ) {
ctx.fillRect(x+10,y+10,40,40);
}
}
self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );
worker.onmessage = (evt) =>
// trying a minimal timeout
// to be safe better do setTimeout( () => worker.terminate(), 2000 );
// or even just let GC collect it when needed
requestAnimationFrame( () => // before next frame
requestAnimationFrame( () => // end of next frame
requestAnimationFrame( () => // end of second frame
worker.terminate()
)
)
);
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), it should always work.</h3>
<canvas width="500" height="500"></canvas>
Now, I should note that creating a new Worker for a single job is generally a very bad design. Starting a new js context is a really heavy operation, and making the Worker thread's link with the main thread's GPU instructions is an other one, I do'nt know much about what you're doing, but you should really consider if you won't need to reuse both the Worker and the OffscreenCanvas, in which case you should rather keep them alive.
Related
I'm using the below code to create 3D model using the Three JS library. Code was running successfully in node JS, but when I'm trying to save an image, I'm getting output as a blank image.
I do have a ThreeUtil JS file which is holding the constants of my shader materials and a couple of functions. There is no problem with this utility file
Observation: I see my buffer data with all element values as zero itself. Can someone let me know where exactly I'm going wrong with my implementation?
Please find the code in below sandbox URL
https://codesandbox.io/s/boring-lewin-7msxm
Attached sample input file as sampleinput.json . from an endpoint need to class main typescript file which will perform the creation and capturing screenshot
import Jimp from 'jimp';
const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
const THREE = require('three');
const THREESTLLoader = require('three-stl-loader');
const THREEObjLoader = require('three-obj-loader');
const jsdom = require('jsdom');
const gl = require('gl');
import { WebGLRenderTarget } from 'three';
const HOMUNCULUS_OBJ_URL = './assets/3d-homunculus.obj';
const Canvas = require('canvas');
export class ThreeDimensionalModel {
private prostateMeshData: ProstateMeshData;
private width: number = 660;
private height: number = 660;
private dimensions: ThreeCanvasDimensions;
private scenes: Scenes;
private camera: THREE.PerspectiveCamera;
private matrix: THREE.Matrix4;
private rotationMatrix: THREE.Matrix4;
private prostateMeshBoundingBox: THREE.Box3;
private renderer: THREE.WebGLRenderer;
constructor() {
const { JSDOM } = jsdom;
const { window } = new JSDOM();
const { document } = (new JSDOM('')).window;
// #ts-ignore
global.window = window;
// #ts-ignore
global.document = document;
// #ts-ignore
global.window.decodeURIComponent = global.decodeURIComponent;
// Adding XML HTTP Requests library
// #ts-ignore
global.XMLHttpRequest = XMLHttpRequest;
this.dimensions = this.calculateDimensions();
this.scenes = this.initializeScenes();
this.camera = this.initializeCamera();
this.decorateScenes();
this.loadModels(this.prostateMeshData);
this.renderer = this.createRenderer();
this.animate();
}
private calculateDimensions = (): ThreeCanvasDimensions => ({
width: this.width,
height: this.height,
aspect: this.width / this.height,
})
private initializeScenes = (): Scenes => ({
main: new THREE.Scene(),
homunculus: new THREE.Scene(),
})
private initializeCamera(): THREE.PerspectiveCamera {
const camera = new THREE.PerspectiveCamera(
70,
this.dimensions.aspect,
1,
10000);
camera.position.z = 100;
return camera;
}
private decorateScenes(): void {
const ambientLight = new THREE.AmbientLight(0x505050);
const spotLight = this.getSpotLight();
const plane = this.getPlane();
ThreeUtil.addObjectToAllScenes(this.scenes, spotLight);
ThreeUtil.addObjectToAllScenes(this.scenes, ambientLight);
ThreeUtil.addObjectToAllScenes(this.scenes, plane);
}
private loadModels(prostateMeshData: ProstateMeshData): void {
const loader = new THREESTLLoader(THREE);
const dataUri = `data:text/plain;base64,${prostateMeshData.prostateMeshUrl}`;
loader.prototype.load(dataUri, (geometry: THREE.Geometry) => {
this.setModelMatricesAndProstateMeshBoundings(geometry);
this.setupHomunculusMesh();
const prostateMesh = this.createProstateMesh(geometry);
let biopsyMeshes;
if (prostateMeshData.biopsies) {
biopsyMeshes = this.createBiopsyMeshes(prostateMeshData.biopsies);
}
if (prostateMeshData.rois) {
this.loadRoiMeshesOnScene(prostateMeshData.rois);
}
this.addToMainScene(prostateMesh);
if (prostateMeshData.biopsies) {
biopsyMeshes.forEach(biopsyMesh => this.addToMainScene(biopsyMesh));
}
this.scaleInView();
});
}
public saveAsImage() {
// after init of renderer
const renderTarget = new WebGLRenderTarget(this.width, this.height);
const target = this.renderer.getRenderTarget() || renderTarget;
const size = this.renderer.getSize();
// #ts-ignore
target.setSize(size.width, size.height);
this.renderer.setRenderTarget(renderTarget);
this.renderer.render( this.scenes.main, this.camera, target );
// retrieve buffer from render target
const buffer = new Uint8Array(this.width * this.height * 4);
this.renderer.readRenderTargetPixels(renderTarget, 0, 0, this.width, this.height, buffer);
// create jimp image
const image = new Jimp({
data: buffer,
width: this.width,
height: this.height
}, (err: any, images: any) => {
images.write('output.jpeg');
});
}
private createRenderer(): THREE.WebGLRenderer {
const canvasWidth = 1024;
const canvasHeight = 1024;
const canvas = new Canvas.Canvas(canvasWidth, canvasHeight);
canvas.addEventListener = () => {
// noop
}
canvas.style = {};
const renderer = new THREE.WebGLRenderer({ context: gl(this.width, this.height), canvas , preserveDrawingBuffer: true , alpha: true});
renderer.autoClear = false;
renderer.setClearColor(0xFFFFFF);
renderer.setPixelRatio(2);
renderer.setSize(this.dimensions.width, this.dimensions.height);
renderer.sortObjects = false;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;
return renderer;
}
private getPlane = (): THREE.Mesh => new THREE.Mesh(
new THREE.PlaneBufferGeometry(2000, 2000, 8, 8),
new THREE.MeshBasicMaterial({ visible: false })
)
private animate = (): void => {
this.renderScenes();
}
private getSpotLight(): THREE.SpotLight {
const spotLight = new THREE.SpotLight(0xffffff, 1.5);
spotLight.position.set(0, 500, 2000);
spotLight.castShadow = true;
spotLight.shadow.camera.near = 200;
spotLight.shadow.camera.far = this.camera.far;
spotLight.shadow.camera.fov = 50;
spotLight.shadow.bias = -0.00022;
spotLight.shadow.mapSize.width = 2048;
spotLight.shadow.mapSize.height = 2048;
return spotLight;
}
private setModelMatricesAndProstateMeshBoundings(geometry: THREE.Geometry): void {
geometry.computeFaceNormals();
geometry.computeBoundingBox();
// A transformation matrix (matrix) is applied to prostate segmentation, target and biopsy,
// which contains a translation and a rotation
// The translation shifts the center of segmentation mesh to view center
this.prostateMeshBoundingBox = geometry.boundingBox.clone();
const center = this.prostateMeshBoundingBox.getCenter(new THREE.Vector3());
const translationMatrix = new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z);
// The default view (DICOM coordinate) is patient head pointing towards user.
// We like patient foot pointing towards user.
// Therefore a 180 degree rotation along x (left-right) axis is applied.
const rotAxis = new THREE.Vector3(1, 0, 0);
this.rotationMatrix = new THREE.Matrix4().makeRotationAxis(rotAxis, Math.PI);
// matrix is the multiplication of rotation and translation
this.matrix = new THREE.Matrix4().multiplyMatrices(this.rotationMatrix, translationMatrix);
}
private createProstateMesh(geometry: THREE.Geometry): THREE.Mesh {
const shaderMaterial = ThreeUtil.createShaderMaterial(new THREE.Vector4(0.8, 0.6, 0.2, 0));
geometry.computeFaceNormals();
geometry.computeBoundingBox();
// A transformation matrix (matrix) is applied to prostate segmentation, target and biopsy,
// which contains a translation and a rotation
// The translation shifts the center of segmentation mesh to view center
this.prostateMeshBoundingBox = geometry.boundingBox.clone();
const mesh = new THREE.Mesh(geometry, shaderMaterial);
mesh.applyMatrix(this.matrix);
mesh.name = 'prostate';
return mesh;
}
private renderScenes(): void {
this.renderer.clear();
this.renderer.setViewport(-150, 0, 210, 75);
this.renderer.render(this.scenes.homunculus, this.camera);
this.renderer.clearDepth();
this.renderer.setViewport(0, 0, this.dimensions.width, this.dimensions.height);
this.renderer.render(this.scenes.main, this.camera);
}
private toCameraCoordinates(position: THREE.Vector3): THREE.Vector3 {
return position.applyMatrix4(this.camera.matrixWorldInverse);
}
private scaleInView(): void {
const point1 = this.toCameraCoordinates(this.prostateMeshBoundingBox.min);
const point2 = this.toCameraCoordinates(this.prostateMeshBoundingBox.max);
this.camera.fov = ThreeUtil.angleInDegree(point1, point2, this.camera) * 2;
this.camera.updateProjectionMatrix();
}
private createBiopsyMeshes(biopsiesMeshData: BiopsyMeshData[]): THREE.Mesh[] {
return biopsiesMeshData.map((biopsyMeshData: BiopsyMeshData, index: number) => {
const startPoint = new THREE.Vector3(
biopsyMeshData.proximalX,
biopsyMeshData.proximalY,
biopsyMeshData.proximalZ
);
const endPoint = new THREE.Vector3(biopsyMeshData.distalX, biopsyMeshData.distalY, biopsyMeshData.distalZ);
const color = biopsyMeshData.adenocarcinoma ?
0x0000ff: 0x002233;
const opacity = biopsyMeshData.adenocarcinoma ? 0.65 : 0.3;
const material = new THREE.MeshLambertMaterial({
transparent: true,
depthWrite: false,
depthTest: false,
emissive: new THREE.Color(color),
opacity,
});
const cylinder = ThreeUtil.getCylinderBetweenPoints(startPoint, endPoint, material);
cylinder.applyMatrix(this.matrix);
cylinder.name = `biopsy_${index}`;
return cylinder;
});
}
private loadRoiMeshesOnScene(roisMeshData: RoiMeshData[]): void {
roisMeshData.forEach((roiMeshData: RoiMeshData, index: number) => this.setupRoiMesh(roiMeshData, index));
}
private setupRoiMesh(roiMeshData: RoiMeshData, index: number): void {
const loader = new THREESTLLoader(THREE);
const dataUri = `data:text/plain;base64,${roiMeshData.ROIMeshUrl}`;
if (roiMeshData.tooltipConfiguration) {
if (typeof(roiMeshData.tooltipConfiguration) === 'string' ) {
roiMeshData.tooltipConfiguration = JSON.parse(roiMeshData.tooltipConfiguration);
}
}
loader.prototype.load(dataUri, (roiGeometry: THREE.Geometry) => {
const color = new THREE.Color(0xA396CB);
const colorVector = new THREE.Vector4(color.r, color.g, color.b, 0);
const shaderMaterial = ThreeUtil.createShaderMaterial(colorVector, roiMeshData.adenocarcinoma);
const mesh = new THREE.Mesh(roiGeometry, shaderMaterial);
mesh.name = `roi_${roiMeshData.ROIId}`;
roiGeometry.applyMatrix(this.matrix);
this.addToMainScene(mesh);
this.saveAsImage();
});
}
private setupHomunculusMesh(): void {
new THREEObjLoader(THREE);
new THREE.OBJLoader().load(HOMUNCULUS_OBJ_URL, (group: THREE.Group) => {
group.traverse((child) => {
if (child instanceof THREE.Mesh) {
// apply custom material
// #ts-ignore
child.material = new THREE.MeshPhongMaterial({
color: 0xb09b70,
specular: 0x050505,
shininess: 100,
flatShading: false
});
const homunculusMatrix = new THREE.Matrix4();
homunculusMatrix.makeScale(0.020, 0.020, 0.020);
homunculusMatrix.multiply(this.rotationMatrix);
child.applyMatrix(homunculusMatrix);
}
});
this.scenes.homunculus.add(group);
});
}
private addToMainScene(object3D: THREE.Object3D): void {
this.scenes.main.add(object3D);
// Adding Ambient Light for all scenes inside main scenes except Humanoid
const ambientLight = new THREE.AmbientLight(0x000000);
this.scenes.main.add(ambientLight);
}
}
I have a background image and a separate ground image that I want to loop infinitely as long as the character is moving forward. When the character stops, the background and ground should not be moving. For similar games it is often suggested to add this.game.background.tilePosition.x -= 1
to the update function. This is not what I am looking for as it makes the background constantly move regardless of whether the character is moving. At the moment my background and ground images are repeating, but they are restricted to this.game.world.setBounds(0, 0, 1280, 800);. Any suggestions would be greatly appreciated. My code is below:
function Hero(game, x, y) {
Phaser.Sprite.call(this, game, x, y, 'player');
//rest of code for Hero constructor....
}
Hero.prototype = Object.create(Phaser.Sprite.prototype);
Hero.prototype.constructor = Hero;
//code for Hero.prototype....
PlayState = {};
PlayState.init = function () {
//code for keyboard...
};
PlayState.preload = function () {
this.game.load.json('level:1', 'data/level01.json');
this.game.load.image('ground', 'images/ground.png'); // I need this to
//repeat infinitely
this.game.load.image('background', 'images/background.png'); // I need
//this to repeat infinitely
this.game.load.spritesheet('player', 'images/player.png', 64, 64);
};
PlayState.create = function () {
this.game.world.setBounds(0, 0, 1280, 800);
this.game.background = this.game.add.tileSprite(0, 0,
this.game.world.width, 800, 'background');
this.game.ground = this.game.add.tileSprite(0, 680,
this.game.world.width, 166, 'ground');
this.game.physics.arcade.enable(this.game.ground);
this.game.ground.body.immovable = true;
this.game.ground.body.allowGravity = false;
this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this._loadLevel(this.game.cache.getJSON('level:1'));
};
PlayState.update = function () {
this.physics.arcade.collide(this.player, this.game.ground);
};
PlayState._loadLevel = function (data) {
this._spawnPlayer({player: data.player});
const GRAVITY = 1200;
this.game.physics.arcade.gravity.y = GRAVITY;
};
PlayState._spawnPlayer = function (data) {
this.player = new Hero(this.game, data.player.x, data.player.y);
this.game.add.existing(this.player);
this.game.camera.follow(this.player,
Phaser.Camera.FOLLOW_PLATFORMER);
};
window.onload = function () {
let game = new Phaser.Game(866, 520, Phaser.CANVAS, 'game');
game.state.add('play', PlayState);
game.state.start('play');
};
I have tried the following solution (see below) where I create a constructor function for the background. This idea is from an existing tutorial done in TypeScript that does exactly what I am looking for. However, I am not familiar with TypeScript so I have just tried to interpret the code from the tutorial as best I can and put it into Javascript but at the moment I am getting the error TypeError: "a is undefined"in the console. I am still learning Javascript and I can't see where I am going wrong. (I have included the keyboard logic and the character movement this time for clarity.)
function MyBackground(game, x, y) {
Phaser.Sprite.call(this, game, x, y, 'background');
}
MyBackground.prototype = Object.create(Phaser.Sprite.prototype);
MyBackground.prototype.constructor = MyScene;
MyBackground.prototype.repeatScene = function () {
this.nextFrame = new Phaser.Sprite(this.game, this.width, 0, "background", 0);
this.game.add.existing(this.nextFrame);
};
function Hero(game, x, y) {
Phaser.Sprite.call(this, game, x, y, 'player');
this.anchor.set(0.5, 0.5);
this.game.physics.enable(this);
this.body.collideWorldBounds = false;
this.animations.add('stop', [0]);
this.animations.add('run', [1, 2, 3, 4, 5], 14, true); // 14fps looped
this.animations.add('jump', [6]);
this.animations.add('fall', [7]);
this.animations.add('die', [8, 9, 8, 9, 8, 9, 8, 9], 12); // 12fps no loop
}
Hero.prototype = Object.create(Phaser.Sprite.prototype);
Hero.prototype.constructor = Hero;
Hero.prototype.move = function (direction) {
const SPEED = 200;
this.body.velocity.x = direction * SPEED;
// update image flipping & animations
if (this.body.velocity.x < 0) {
this.scale.x = -1;
}
else if (this.body.velocity.x > 0) {
this.scale.x = 1;
}
};
Hero.prototype.jump = function () {
const JUMP_SPEED = 600;
let canJump = this.body.touching.down;
if (canJump) {
this.body.velocity.y = -JUMP_SPEED;
}
return canJump;
};
Hero.prototype.bounce = function () {
const BOUNCE_SPEED = 200;
this.body.velocity.y = -BOUNCE_SPEED;
};
Hero.prototype.update = function () {
// update sprite animation, if it needs changing
let animationName = this._getAnimationName();
if (this.animations.name !== animationName) {
this.animations.play(animationName);
}
};
Hero.prototype.die = function () {
this.alive = false;
this.body.enable = false;
this.animations.play('die').onComplete.addOnce(function () {
this.kill();
}, this);
};
Hero.prototype._getAnimationName = function () {
let name = 'stop'; // default animation
if (!this.alive) {
name = 'die';
}
else if (this.body.velocity.y > 0 && !this.body.touching.down) {
name = 'fall';
}
else if (this.body.velocity.y < 0) {
name = 'jump';
}
else if (this.body.velocity.x !== 0 && this.body.touching.down ) {
name = 'run';
}
return name;
PlayState = {};
PlayState.init = function () {
this.game.renderer.renderSession.roundPixels = true;
this.keys = this.game.input.keyboard.addKeys({
left: Phaser.KeyCode.LEFT,
right: Phaser.KeyCode.RIGHT,
up: Phaser.KeyCode.UP
};
PlayState.preload = function () {
this.game.load.json('level:1', 'data/level01.json');
this.game.load.image('ground', 'images/ground.png'); // I need this to repeat infinitely
this.game.load.image('background', 'images/background.png'); // I need this to repeat infinitely
this.game.load.spritesheet('player', 'images/player.png', 64, 64);
};
PlayState.create = function () {
this.background = new MyBackground(this.game, 0, 0);
this.game.add.existing(this.MyBackground);
this.game.physics.arcade.enable(this.game.ground);
this.game.ground.body.immovable = true;
this.game.ground.body.allowGravity = false;
this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this._loadLevel(this.game.cache.getJSON('level:1'));
this.game.world.setBounds(0, 0, this.MyBackground.width * 2, 800);
};
PlayState.update = function () {
var backgroundWidth = this.game.stage.getChildAt(0).getBounds().width; //getChildAt(0) because the background is created first in create
if (this.x > backgroundWidth * .75) {
this.x = backgroundWidth * .25;
repeatScene();
};
this._handleInput();
this.physics.arcade.collide(this.player, this.game.ground);
};
PlayState._handleInput = function () {
if (this.keys.up.isDown) {
this.player.jump();
} else if (this.keys.right.isDown) { // move hero right
this.player.move(1);
} else if (this.keys.left.isDown) { // move hero left
this.player.move(-1);
} else { // stop
this.player.move(0);
}
};
PlayState._loadLevel = function (data) {
this._spawnPlayer({player: data.player});
const GRAVITY = 1200;
this.game.physics.arcade.gravity.y = GRAVITY;
};
PlayState._spawnPlayer = function (data) {
this.player = new Hero(this.game, data.player.x, data.player.y);
this.game.add.existing(this.player);
this.game.camera.follow(this.player, Phaser.Camera.FOLLOW_PLATFORMER);
};
window.onload = function () {
let game = new Phaser.Game(866, 520, Phaser.CANVAS, 'game');
game.state.add('play', PlayState);
game.state.start('play');
};
I am new to Three.js (3D) and have a simple question. I have the following code that will work properly, but I think the result lost their colors because I open the test.obj file whith 3D Buidler(WIN10), there are lots of colors on ther surface of model. why?
The Code
var loader = new THREE.OBJLoader()
loader.load( 'test.obj', function ( object ) {
object.position.y = 0;
scene.add( object );
} );
I think it's vertex color, How to show it's vertex color?
var loader = new THREE.OBJLoader2()
loader.load( 'test.obj', function ( object ) {
object.position.y = 0;
scene.add( object );
} );
I have tryed OBJLoader2.js , but it doesn't work, need some settings?
The result loaded by Three.js:
The result loaded by 3D Builder:
The obj file
use the .mtl file along with the .obj file.
var onProgress = function ( xhr ) {
if ( xhr.lengthComputable ) {
var percentComplete = xhr.loaded / xhr.total * 100;
console.log( Math.round(percentComplete, 2) + '% downloaded' );
}
};
var onError = function ( xhr ) { };
THREE.Loader.Handlers.add( /\.dds$/i, new THREE.DDSLoader() );
//Car model
var mtlLoader = new THREE.MTLLoader();
mtlLoader.setPath( '../materials/car/' );
mtlLoader.load( 'car.mtl', function( materials ) {
materials.preload();
var objLoader = new THREE.OBJLoader();
objLoader.setMaterials( materials );
objLoader.setPath( '../materials/car/' );
objLoader.load( 'car.obj', carObject, onProgress, onError );
});
function carObject(object){
object.rotation.y = 1.55;
object.position.z = 105;
object.position.y = 1.15;
object.scale.x = object.scale.y = object.scale.z = 0.15;
//object.rotation.x = 6.5;
//object.position.z = 50;
scene.add( object );
}
//end car model
Finally I found it out :
I changed OBJLoader2.js's version to 1.3.0.
Something strange is happening here.
I create a audio buffer store it in a variable and try to re-use it several times - but it seems to be corrupted
I make some buttons
<button onclick="play();">play(0)</button>
<button onclick="playsection();">play section</button>
<button onclick="stop();">stop()</button>
Get some audio data
context = new AudioContext();
var getWav = new XMLHttpRequest();
var wavbuf;
getWav.open("GET", "/wav/test.wav", true);
getWav.responseType = "arraybuffer";
getWav.onload = function() {
context.decodeAudioData(getWav.response, function(buffer){
wavbuf = buffer;
});
}
getWav.send();
var p;
I can evaluate play() multiple times without an error
function play(){
p = context.createBufferSource();
p.buffer = wavbuf;
p.connect(context.destination);
p.start(0);
}
playsection seems to work only once - or occasionally more than once if
I press stop before stop(10) evaluates
function playsection(){
p = context.createBufferSource();
p.buffer = wavbuf;
p.connect(context.destination);
p.start(0, 6); // start after 6 seconds
p.stop(10); // stop after 10 seconds
}
function stop(){
p.stop();
}
Seems like p.buffer = wavbuf creates a pointer into the buffer rather than a copy
Is this a bug or a feature?
So this is interesting, it will play the section consistently either without the stop:
function playsection(){
p = context.createBufferSource();
p.buffer = wavbuf;
p.connect(context.destination);
p.start(0, 6); // start after 6 seconds
}
or without the offset:
function playsection(){
p = context.createBufferSource();
p.buffer = wavbuf;
p.connect(context.destination);
p.start(0);
p.stop(10); // stop after 10 seconds
}
and even declaring the offset and duration within the start:
function playsection(){
p = context.createBufferSource();
p.buffer = wavbuf;
p.connect(context.destination);
p.start(0,6,10);
}
Is there any built-in support for for undo/redo in Fabric.js? Can you please guide me on how you used this cancel and repeat in [http://printio.ru/][1]
In http://jsfiddle.net/SpgGV/9/, move the object and change its size. If the object state is changed, and then we do undo/redo, its previous state will be deleted when the next change comes. It makes it easier to do undo/redo. All events of canvas should be called before any element is added to canvas. I didn't add an object:remove event here. You can add it yourself. If one element is removed, the state and list should be invalid if this element is in this array. The simpler way is to set state and list = [] and index = 0.
This will clear the state of your undo/redo queue. If you want to keep all states, such as add/remove, my suggestion is to add more properties to the element of your state array. For instance, state = [{"data":object.originalState, "event": "added"}, ....]. The "event" could be "modified" or "added" and set in a corresponding event handler.
If you have added one object, then set state[index].event="added" so that next time, when you use undo, you check it. If it's "added", then remove it anyway. Or when you use redo, if the target one is "added", then you added it. I've recently been quite busy. I will add codes to jsfiddle.net later.
Update: added setCoords() ;
var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;
canvas.on("object:added", function (e) {
var object = e.target;
console.log('object:modified');
if (action === true) {
state = [state[index2]];
list = [list[index2]];
action = false;
console.log(state);
index = 1;
}
object.saveState();
console.log(object.originalState);
state[index] = JSON.stringify(object.originalState);
list[index] = object;
index++;
index2 = index - 1;
refresh = true;
});
canvas.on("object:modified", function (e) {
var object = e.target;
console.log('object:modified');
if (action === true) {
state = [state[index2]];
list = [list[index2]];
action = false;
console.log(state);
index = 1;
}
object.saveState();
state[index] = JSON.stringify(object.originalState);
list[index] = object;
index++;
index2 = index - 1;
console.log(state);
refresh = true;
});
function undo() {
if (index <= 0) {
index = 0;
return;
}
if (refresh === true) {
index--;
refresh = false;
}
console.log('undo');
index2 = index - 1;
current = list[index2];
current.setOptions(JSON.parse(state[index2]));
index--;
current.setCoords();
canvas.renderAll();
action = true;
}
function redo() {
action = true;
if (index >= state.length - 1) {
return;
}
console.log('redo');
index2 = index + 1;
current = list[index2];
current.setOptions(JSON.parse(state[index2]));
index++;
current.setCoords();
canvas.renderAll();
}
Update: better solution to take edit history algorithm into account. Here we can use Editing.getInst().set(item) where the item could be {action, object, state}; For example, {"add", object, "{JSON....}"}.
/**
* Editing : we will save element states into an queue, and the length of queue
* is fixed amount, for example, 0..99, each element will be insert into the top
* of queue, queue.push, and when the queue is full, we will shift the queue,
* to remove the oldest element from the queue, queue.shift, and then we will
* do push.
*
* So the latest state will be at the top of queue, and the oldest one will be
* at the bottom of the queue (0), and the top of queue is changed, could be
* 1..99.
*
* The initialized action is "set", it will insert item into the top of queue,
* even if it arrived the length of queue, it will queue.shift, but still do
* the same thing, and queue only abandon the oldest element this time. When
* the current is changed and new state is coming, then this time, top will be
* current + 1.
*
* The prev action is to fetch "previous state" of the element, and it will use
* "current" to do this job, first, we will --current, and then we will return
* the item of it, because "current" always represent the "current state" of
* element. When the current is equal 0, that means, we have fetched the last
* element of the queue, and then it arrived at the bottom of the queue.
*
* The next action is to fetch "next state" after current element, and it will
* use "current++" to do the job, when the current is equal to "top", it means
* we have fetched the latest element, so we should stop.
*
* If the action changed from prev/next to "set", then we should reset top to
* "current", and abandon all rest after that...
*
* Here we should know that, if we keep the reference in the queue, the item
* in the queue will never be released.
*
*
* #constructor
*/
function Editing() {
this.queue = [];
this.length = 4;
this.bottom = 0;
this.top = 0;
this.current = 0;
this.empty = true;
// At the Begin of Queue
this.BOQ = true;
// At the End of Queue
this.EOQ = true;
// 0: set, 1: prev, 2: next
this._action = 0;
this._round = 0;
}
Editing.sharedInst = null;
Editing.getInst = function (owner) {
if (Editing.sharedInst === null) {
Editing.sharedInst = new Editing(owner);
}
return Editing.sharedInst;
};
/**
* To set the item into the editing queue, and mark the EOQ, BOQ, so we know
* the current position.
*
* #param item
*/
Editing.prototype.set = function (item) {
console.log("=== Editing.set");
var result = null;
if (this._action != 0) {
this.top = this.current + 1;
}
if (this.top >= this.length) {
result = this.queue.shift();
this.top = this.length - 1;
}
this._action = 0;
this.queue[this.top] = item;
this.current = this.top;
this.top++;
this.empty = false;
this.EOQ = true;
this.BOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return result;
};
/**
* To fetch the previous item just before current one
*
* #returns {item|boolean}
*/
Editing.prototype.prev = function () {
console.log("=== Editing.prev");
if (this.empty) {
return false;
}
if (this.BOQ) {
return false;
}
this._action = 1;
this.current--;
if (this.current == this.bottom) {
this.BOQ = true;
}
var item = this.queue[this.current];
this.EOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return item;
};
/**
* To fetch the next item just after the current one
*
* #returns {*|boolean}
*/
Editing.prototype.next = function () {
console.log("=== Editing.next");
if (this.empty) {
return false;
}
if (this.EOQ) {
return false;
}
this.current++;
if (this.current == this.top - 1 && this.top < this.length) {
this.EOQ = true;
}
if (this.current == this.top - 1 && this.top == this.length) {
this.EOQ = true;
}
this._action = 2;
var item = this.queue[this.current];
this.BOQ = false;
console.log("==> INFO : ");
console.log(item);
console.log("===========");
console.log("current: ", 0 + this.current);
console.log("start: ", 0 + this.bottom);
console.log("end: ", 0 + this.top);
return item;
};
/**
* To empty the editing and reset all state
*/
Editing.prototype.clear = function () {
this.queue = [];
this.bottom = 0;
this.top = 0;
this.current = 0;
this.empty = true;
this.BOQ = true;
this.EOQ = false;
};
Here is a solution that started with this simpler answer to the similar question, Undo Redo History for Canvas FabricJs.
My answer is along the same lines as Tom's answer and the other answers that are modifications of Tom's answer.
To track the state, I'm using JSON.stringify(canvas) and canvas.loadFromJSON() like the other answers and have an event registered on the object:modified to capture the state.
One important thing is that the final canvas.renderAll() should be called in a callback passed to the second parameter of loadFromJSON(), like this
canvas.loadFromJSON(state, function() {
canvas.renderAll();
}
This is because it can take a few milliseconds to parse and load the JSON and you need to wait until that's done before you render. It's also important to disable the undo and redo buttons as soon as they're clicked and to only re-enable in the same call back. Something like this
$('#undo').prop('disabled', true);
$('#redo').prop('disabled', true);
canvas.loadFromJSON(state, function() {
canvas.renderAll();
// now turn buttons back on appropriately
...
(see full code below)
}
I have an undo and a redo stack and a global for the last unaltered state. When some modification occurs, then the previous state is pushed into the undo stack and the current state is re-captured.
When the user wants to undo, then current state is pushed to the redo stack. Then I pop off the last undo and both set it to the current state and render it on the canvas.
Likewise when the user wants to redo, the current state is pushed to the undo stack. Then I pop off the last redo and both set it to the current state and render it on the canvas.
The Code
// Fabric.js Canvas object
var canvas;
// current unsaved state
var state;
// past states
var undo = [];
// reverted states
var redo = [];
/**
* Push the current state into the undo stack and then capture the current state
*/
function save() {
// clear the redo stack
redo = [];
$('#redo').prop('disabled', true);
// initial call won't have a state
if (state) {
undo.push(state);
$('#undo').prop('disabled', false);
}
state = JSON.stringify(canvas);
}
/**
* Save the current state in the redo stack, reset to a state in the undo stack, and enable the buttons accordingly.
* Or, do the opposite (redo vs. undo)
* #param playStack which stack to get the last state from and to then render the canvas as
* #param saveStack which stack to push current state into
* #param buttonsOn jQuery selector. Enable these buttons.
* #param buttonsOff jQuery selector. Disable these buttons.
*/
function replay(playStack, saveStack, buttonsOn, buttonsOff) {
saveStack.push(state);
state = playStack.pop();
var on = $(buttonsOn);
var off = $(buttonsOff);
// turn both buttons off for the moment to prevent rapid clicking
on.prop('disabled', true);
off.prop('disabled', true);
canvas.clear();
canvas.loadFromJSON(state, function() {
canvas.renderAll();
// now turn the buttons back on if applicable
on.prop('disabled', false);
if (playStack.length) {
off.prop('disabled', false);
}
});
}
$(function() {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the canvas
canvas = new fabric.Canvas('canvas');
canvas.setWidth(500);
canvas.setHeight(500);
// save initial state
save();
// register event listener for user's actions
canvas.on('object:modified', function() {
save();
});
// draw button
$('#draw').click(function() {
var imgObj = new fabric.Circle({
fill: '#' + Math.floor(Math.random() * 16777215).toString(16),
radius: Math.random() * 250,
left: Math.random() * 250,
top: Math.random() * 250
});
canvas.add(imgObj);
canvas.renderAll();
save();
});
// undo and redo buttons
$('#undo').click(function() {
replay(undo, redo, '#redo', this);
});
$('#redo').click(function() {
replay(redo, undo, '#undo', this);
})
});
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js" type="text/javascript"></script>
</head>
<body>
<button id="draw">circle</button>
<button id="undo" disabled>undo</button>
<button id="redo" disabled>redo</button>
<canvas id="canvas" style="border: solid 1px black;"></canvas>
</body>
I am allowing the user to remove the last added path (in my painting application), this works fine for me:
var lastItemIndex = (fabricCanvas.getObjects().length - 1);
var item = fabricCanvas.item(lastItemIndex);
if(item.get('type') === 'path') {
fabricCanvas.remove(item);
fabricCanvas.renderAll();
}
But you could also remove the IF statement and let people remove anything.
I know its late to answer this but this is my version of implementing this. Can be useful to someone.
I have implemented this feature by saving Canvas States as JSON. Whenever a user adds or modifies an object in the Canvas, it will save the changed canvas state and maintain it in an array. This array is then manipulated whenever user clicks on Undo or Redo button.
Take a look at this link. I have also provided a working Demo URL.
https://github.com/abhi06991/Undo-Redo-Fabricjs
HTML:
<canvas id="canvas" width="400" height="400"></canvas>
<button type="button" id="undo" >Undo</button>
<button type="button" id="redo" disabled>Redo</button>
JS:
var canvasDemo = (function(){
var _canvasObject = new fabric.Canvas('canvas',{backgroundColor : "#f5deb3"});
var _config = {
canvasState : [],
currentStateIndex : -1,
undoStatus : false,
redoStatus : false,
undoFinishedStatus : 1,
redoFinishedStatus : 1,
undoButton : document.getElementById('undo'),
redoButton : document.getElementById('redo'),
};
_canvasObject.on(
'object:modified', function(){
updateCanvasState();
}
);
_canvasObject.on(
'object:added', function(){
updateCanvasState();
}
);
var addObject = function(){
var rect = new fabric.Rect({
left : 100,
top : 100,
fill : 'red',
width : 200,
height : 200
});
_canvasObject.add(rect);
_canvasObject.setActiveObject(rect);
_canvasObject.renderAll();
}
var updateCanvasState = function() {
if((_config.undoStatus == false && _config.redoStatus == false)){
var jsonData = _canvasObject.toJSON();
var canvasAsJson = JSON.stringify(jsonData);
if(_config.currentStateIndex < _config.canvasState.length-1){
var indexToBeInserted = _config.currentStateIndex+1;
_config.canvasState[indexToBeInserted] = canvasAsJson;
var numberOfElementsToRetain = indexToBeInserted+1;
_config.canvasState = _config.canvasState.splice(0,numberOfElementsToRetain);
}else{
_config.canvasState.push(canvasAsJson);
}
_config.currentStateIndex = _config.canvasState.length-1;
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}
}
}
var undo = function() {
if(_config.undoFinishedStatus){
if(_config.currentStateIndex == -1){
_config.undoStatus = false;
}
else{
if (_config.canvasState.length >= 1) {
_config.undoFinishedStatus = 0;
if(_config.currentStateIndex != 0){
_config.undoStatus = true;
_canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex-1],function(){
var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex-1]);
_canvasObject.renderAll();
_config.undoStatus = false;
_config.currentStateIndex -= 1;
_config.undoButton.removeAttribute("disabled");
if(_config.currentStateIndex !== _config.canvasState.length-1){
_config.redoButton.removeAttribute('disabled');
}
_config.undoFinishedStatus = 1;
});
}
else if(_config.currentStateIndex == 0){
_canvasObject.clear();
_config.undoFinishedStatus = 1;
_config.undoButton.disabled= "disabled";
_config.redoButton.removeAttribute('disabled');
_config.currentStateIndex -= 1;
}
}
}
}
}
var redo = function() {
if(_config.redoFinishedStatus){
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}else{
if (_config.canvasState.length > _config.currentStateIndex && _config.canvasState.length != 0){
_config.redoFinishedStatus = 0;
_config.redoStatus = true;
_canvasObject.loadFromJSON(_config.canvasState[_config.currentStateIndex+1],function(){
var jsonData = JSON.parse(_config.canvasState[_config.currentStateIndex+1]);
_canvasObject.renderAll();
_config.redoStatus = false;
_config.currentStateIndex += 1;
if(_config.currentStateIndex != -1){
_config.undoButton.removeAttribute('disabled');
}
_config.redoFinishedStatus = 1;
if((_config.currentStateIndex == _config.canvasState.length-1) && _config.currentStateIndex != -1){
_config.redoButton.disabled= "disabled";
}
});
}
}
}
}
return {
addObject : addObject,
undoButton : _config.undoButton,
redoButton : _config.redoButton,
undo : undo,
redo : redo,
}
})();
canvasDemo.undoButton.addEventListener('click',function(){
canvasDemo.undo();
});
canvasDemo.redoButton.addEventListener('click',function(){
canvasDemo.redo();
});
canvasDemo.addObject();
My use case was drawing simple shapes akin to blueprints, so I didn't have to worry about the overhead of saving the whole canvas state. If you are in the same situation, this is very easy to accomplish. This code assumes you have a 'wrapper' div around the canvas, and that you want the undo/redo functionality bound to the standard windows keystrokes of 'CTRL+Z' and 'CTRL+Y'.
The purpose of the 'pause_saving' variable was to account for the fact that when a canvas is re-rendered it seemingly created each object one by one all over again, and we don't want to catch these events, as they aren't REALLY new events.
//variables for undo/redo
let pause_saving = false;
let undo_stack = []
let redo_stack = []
canvas.on('object:added', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object added, state saved', undo_stack);
}
});
canvas.on('object:modified', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object modified, state saved', undo_stack);
}
});
canvas.on('object:removed', function(event){
if (!pause_saving) {
undo_stack.push(JSON.stringify(canvas));
redo_stack = [];
console.log('Object removed, state saved', undo_stack);
}
});
//Listen for undo/redo
wrapper.addEventListener('keydown', function(event){
//Undo - CTRL+Z
if (event.ctrlKey && event.keyCode == 90) {
pause_saving=true;
redo_stack.push(undo_stack.pop());
let previous_state = undo_stack[undo_stack.length-1];
if (previous_state == null) {
previous_state = '{}';
}
canvas.loadFromJSON(previous_state,function(){
canvas.renderAll();
})
pause_saving=false;
}
//Redo - CTRL+Y
else if (event.ctrlKey && event.keyCode == 89) {
pause_saving=true;
state = redo_stack.pop();
if (state != null) {
undo_stack.push(state);
canvas.loadFromJSON(state,function(){
canvas.renderAll();
})
pause_saving=false;
}
}
});
You can use "object:added" and/or "object:removed" for that — fabricjs.com/events
You can follow this post:
Do we have canvas Modified Event in Fabric.js?
I know the answer is already chosen but here is my version, script is condensed, also added a reset to original state. After any event you want to save just call saveState(); jsFiddle
canvas = new fabric.Canvas('canvas', {
selection: false
});
function saveState(currentAction) {
currentAction = currentAction || '';
// if (currentAction !== '' && lastAction !== currentAction) {
$(".redo").val($(".undo").val());
$(".undo").val(JSON.stringify(canvas));
console.log("Saving After " + currentAction);
lastAction = currentAction;
// }
var objects = canvas.getObjects();
for (i in objects) {
if (objects.hasOwnProperty(i)) {
objects[i].setCoords();
}
}
}
canvas.on('object:modified', function (e) {
saveState("modified");
});
// Undo Canvas Change
function undo() {
canvas.loadFromJSON($(".redo").val(), canvas.renderAll.bind(canvas));
}
// Redo Canvas Change
function redo() {
canvas.loadFromJSON($(".undo").val(), canvas.renderAll.bind(canvas));
};
$("#reset").click(function () {
canvas.loadFromJSON($("#original_canvas").val(),canvas.renderAll.bind(canvas));
});
var bgnd = new fabric.Image.fromURL('https://s3-eu-west-1.amazonaws.com/kienzle.dev.cors/img/image2.png', function(oImg){
oImg.hasBorders = false;
oImg.hasControls = false;
// ... Modify other attributes
canvas.insertAt(oImg,0);
canvas.setActiveObject(oImg);
myImg = canvas.getActiveObject();
saveState("render");
$("#original_canvas").val(JSON.stringify(canvas.toJSON()));
});
$("#undoButton").click(function () {
undo();
});
$("#redoButton").click(function () {
redo();
});
i developed a small script for you,hope it will help you .see this demo Fiddle
although redo is not perfect you have to click minimum two time at undo button then redo work .you can easily solve this problem with giving simple conditions in redo code.
//Html
<canvas id="c" width="400" height="200" style=" border-radius:25px 25px 25px 25px"></canvas>
<br>
<br>
<input type="button" id="addtext" value="Add Text"/>
<input type="button" id="undo" value="Undo"/>
<input type="button" id="redo" value="redo"/>
<input type="button" id="clear" value="Clear Canvas"/>
//script
var canvas = new fabric.Canvas('c');
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 50,
top: 30,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
var vall=10;
var l=0;
var flag=0;
var k=1;
var yourJSONString = new Array();
canvas.observe('object:selected', function(e) {
//yourJSONString = JSON.stringify(canvas);
if(k!=10)
{
yourJSONString[k] = JSON.stringify(canvas);
k++;
}
j = k;
var activeObject = canvas.getActiveObject();
});
$("#undo").click(function(){
if(k-1!=0)
{
canvas.clear();
canvas.loadFromJSON(yourJSONString[k-1]);
k--;
l++;
}
canvas.renderAll();
});
$("#redo").click(function(){
if(l > 1)
{
canvas.clear();
canvas.loadFromJSON(yourJSONString[k+1]);
k++;
l--;
canvas.renderAll();
}
});
$("#clear").click(function(){
canvas.clear();
});
$("#addtext").click(function(){
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 100,
top: 100,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
});
I have answer to all your queries :) get a smile
check this link.. its all done ... copy & paste it :P
http://jsfiddle.net/SpgGV/27/
var canvas = new fabric.Canvas('c');
var current;
var list = [];
var state = [];
var index = 0;
var index2 = 0;
var action = false;
var refresh = true;
state[0] = JSON.stringify(canvas.toDatalessJSON());
console.log(JSON.stringify(canvas.toDatalessJSON()));
$("#clear").click(function(){
canvas.clear();
index=0;
});
$("#addtext").click(function(){
++index;
action=true;
var text = new fabric.Text('Sample', {
fontFamily: 'Hoefler Text',
left: 100,
top: 100,
//textAlign: 'center',
fill: 'navy',
});
canvas.add(text);
});
canvas.on("object:added", function (e) {
if(action===true){
var object = e.target;
console.log(JSON.stringify(canvas.toDatalessJSON()));
state[index] = JSON.stringify(canvas.toDatalessJSON());
refresh = true;
action=false;
canvas.renderAll();
}
});
function undo() {
if (index < 0) {
index = 0;
canvas.loadFromJSON(state[index]);
canvas.renderAll();
return;
}
console.log('undo');
canvas.loadFromJSON(state[index]);
console.log(JSON.stringify(canvas.toDatalessJSON()));
canvas.renderAll();
action = false;
}
function redo() {
action = false;
if (index >= state.length - 1) {
canvas.loadFromJSON(state[index]);
canvas.renderAll();
return;
}
console.log('redo');
canvas.loadFromJSON(state[index]);
console.log(JSON.stringify(canvas.toDatalessJSON()));
canvas.renderAll();
canvas.renderAll();
}
canvas.on("object:modified", function (e) {
var object = e.target;
console.log('object:modified');
console.log(JSON.stringify(canvas.toDatalessJSON()));
state[++index] = JSON.stringify(canvas.toDatalessJSON());
action=false;
});
$('#undo').click(function () {
index--;
undo();
});
$('#redo').click(function () {
index++;
redo();
});