I'm trying to smoothly animate a path in FabricJS. I've seen examples of how to animate lines, but nothing about animating paths.
I've been able to iterate through an array of path points to get something functioning, but it's choppy and doesn't work well:
function animateLine(pathArray) {
var pathString = '';
var directionLine;
pathArray.forEach((item, i) => {
setTimeout(() => {
pathString = pathString + item + ',';
canvas.remove(directionLine);
directionLine = new fabric.Path(pathString, {
fill: '',
stroke: 'black',
strokeDashArray: [5, 5],
objectCaching: false
});
canvas.add(directionLine);
}, i * 20);
});
}
animateLine(pathString);
Fiddle here.
I'm looking to do something like this example, but it uses Raphael and I'm hoping to not have to switch libraries.
Was able to figure out a solution with help from one of the mods pointing me to strokeDashOffset.
var canvas = new fabric.Canvas('c');
var pathArray = "M78.2,0.1c0,0,9.4,79.1,2.3,117 c-4.5,24.1-31.8,106.2-56.3,108.7c-12.7,1.3-24.2-11.9-16.5-15.5C15,207,40.2,231.1,19,261.7c-9.8,14.1-24.7,31.9-12.5,44.9 c11.3,12,53-36.8,59.2-23.8c8.6,18-40.8,23-28,49.2c14.7,30.4,21.6,39.9,48,58.5c31.3,22,147,66.2,147.2,149.5";
var path = new fabric.Path(pathArray, {
fill: '',
stroke: 'black',
strokeDashArray: [5, 5],
strokeDashOffset: 0
});
canvas.add(path);
function animatePath(path) {
path.animate('strokeDashOffset', '-=3', {
duration: 100,
onChange: canvas.renderAll.bind(canvas),
onComplete: function(){
animatePath(path)
}
});
}
animatePath(path);
Fiddle
Related
I tried to use subclassing as shown in the example at http://fabricjs.com/polaroid.
The PolaroidPhoto subclass just adds a border on the image as shown on the following fiddle: https://jsfiddle.net/gusy54rr/6/
canvas = this.__canvas = new fabric.Canvas('c', {
backgroundColor: '#333',
HOVER_CURSOR: 'pointer'
});
var PolaroidPhoto = fabric.util.createClass(fabric.Image, {
H_PADDING: 20,
V_PADDING: 20,
originX: 'center',
originY: 'center',
initialize: function(src, options) {
this.callSuper('initialize', options);
this.image = new Image();
this.image.src = src;
console.log("In initialize, src is:" + src);
this.image.onload = (function() {
this.width = this.image.width;
this.height = this.image.height;
this.loaded = true;
this.setCoords();
this.fire('image:loaded');
}).bind(this);
},
_render: function(ctx) {
if (this.loaded) {
ctx.fillStyle = '#fff';
ctx.fillRect(
-(this.width / 2) - this.H_PADDING,
-(this.height / 2) - this.H_PADDING,
this.width + this.H_PADDING * 2,
this.height + this.V_PADDING * 2);
ctx.drawImage(this.image, -this.width / 2, -this.height / 2);
}
}
});
var photo = new PolaroidPhoto('https://i.stack.imgur.com/cqmQ9.png', { });
photo.on('image:loaded', canvas.renderAll.bind(canvas));
photo.set('scaleX', 1);
photo.set('scaleY', 1);
photo.set('top', 180);
photo.set('left', 150);
console.log("photo,src is :" + photo.get('src'));
// forcing src value (but ineffective)
photo.set('src', 'https://i.stack.imgur.com/cqmQ9.png');
canvas.add(photo);
canvas.add(
rect= new fabric.Rect({ top: 50, left: 100, width: 50, height: 50, fill: '#f55' }),
circle = new fabric.Circle({ top: 140, left: 230, radius: 75, fill: 'green' }),
triangle = new fabric.Triangle({ top: 300, left: 210, width: 100, height: 100, fill: 'blue' })
);
$("#group").on('click', function() {
var activegroup = canvas.getActiveGroup();
var objectsInGroup = activegroup.getObjects();
activegroup.clone(function(newgroup) {
canvas.discardActiveGroup();
objectsInGroup.forEach(function(object) {
canvas.remove(object);
});
canvas.add(newgroup);
});
});
$("#ungroup").click(function(){
var activeObject = canvas.getActiveObject();
if(activeObject.type=="group"){
var items = activeObject._objects;
alert(items);
activeObject._restoreObjectsState();
canvas.remove(activeObject);
for(var i = 0; i < items.length; i++) {
canvas.add(items[i]);
items[i].dirty = true;
canvas.item(canvas.size()-1).hasControls = true;
}
canvas.renderAll();
}
});
It works fine until I want to stringify or make some grouping with a subclassed object.
In the fiddle, I completed the Fabric demo's example by adding a few basis objects (a rectangle, a circle and a triangle).
If I select the subclassed image and any other object and then click on the group button:
The image disappears.
The scr property of the photo is not set (as shown by the alert on "ungroup" for the former group).
A stringification of the canvas also shows that "src" is missing.
Even if I force (see the fiddle) a src value using "photo.set('src',...)" :
- the grouping still makes the picture to disappear.
- The stringification still lacks the "src" attribute. (I tried to extend toObjects to no avail)
How to get grouping and stringification to work with subclassed objects?
Thanks for your help.
Here is a new jsfiddle showing correct grouping and JSON load with a sub-classed (PolaroidPhoto) image. Fabric version is 1.7.19
https://jsfiddle.net/rpzk7wL6/2/
I put some comments in the code to show my modifications.
The main problem in the former script was the absence of fromObjects() method.
I also added a handler listening to "image:loaded" to the sub-class instance created
by fromObjects, in order to render it after loading .
fabric.Object.prototype.transparentCorners = false;
canvas = this.__canvas = new fabric.Canvas('c', {
backgroundColor: '#333',
HOVER_CURSOR: 'pointer'
});
fabric.Polaroidphoto = fabric.util.createClass(fabric.Image, {
type: 'polaroidphoto',
H_PADDING: 20,
V_PADDING: 20,
originX: 'center',
originY: 'center',
initialize: function(src, options) {
this.image = new Image();
this.image.src = src;
this.callSuper('initialize',src, options);
console.log("initialize, src:" + src);
this.image.onload = (function() {
this.width = this.image.width;
console.log("initialize, scaleX:" + this.image.scaleX);
this.height = this.image.height;
this.src= this.image.src;
console.log("initialize image.onload, src:" + src);
this.loaded = true;
this.setCoords();
this.fire('image:loaded');
}).bind(this);
},
_render: function(ctx) {
if (this.loaded) {
console.log("_render:is_loaded");
ctx.fillStyle = '#fff';
ctx.fillRect(
-(this.width / 2) - this.H_PADDING,
-(this.height / 2) - this.H_PADDING,
this.width + this.H_PADDING * 2,
this.height + this.V_PADDING * 2);
ctx.drawImage(this.image, -this.width / 2, -this.height / 2);
} else {
console.log("_render:is_NOT__loaded");
}
}
});
// Added fromObject function for sub-class
fabric.Polaroidphoto.async = true;
fabric.Polaroidphoto.fromObject = function (object, callback) {
console.log("fabric.Polaroidphoto.fromObject object.src) :" + object.src);
var instance = new fabric.Polaroidphoto(object.src, object);
callback && callback(instance);
// added handler to render instance
instance.on('image:loaded', when_loaded );
};
var photo = new fabric.Polaroidphoto('https://i.stack.imgur.com/cqmQ9.png', { });
photo.on('image:loaded', when_loaded );
photo.set('scaleX', 1);
photo.set('scaleY', 1);
photo.set('top', 180);
photo.set('left', 150);
canvas.add(photo);
canvas.add(
rect= new fabric.Rect({ top: 50, left: 100, width: 50, height: 50, fill: '#f55' }),
circle = new fabric.Circle({ top: 140, left: 230, radius: 75, fill: 'green' }),
triangle = new fabric.Triangle({ top: 300, left: 210, width: 100, height: 100, fill: 'blue' })
);
// required at load to render sub-classed image in group
function when_loaded() {
console.log("when_loaded");
dirty(); // to set dirty : true
canvas.renderAll();
}
// required at load to display sub-classed image in group,
// set dirty:true for groups
function dirty() {
$.each(canvas._objects, function( index, obj ) {
if( typeof obj.type !== 'undefined' && obj.type == 'group') {
obj.dirty= true;
}
});
}
$("#group").on('click', function() {
var activegroup = canvas.getActiveGroup();
var objectsInGroup = activegroup.getObjects();
activegroup.clone(function(newgroup) {
canvas.discardActiveGroup();
objectsInGroup.forEach(function(object) {
canvas.remove(object);
});
canvas.add(newgroup);
newgroup.dirty = true;
});
});
$("#ungroup").click(function(){
var activeObject = canvas.getActiveObject();
if(activeObject.type=="group"){
var items = activeObject._objects;
alert(items);
activeObject._restoreObjectsState();
canvas.remove(activeObject);
for(var i = 0; i < items.length; i++) {
canvas.add(items[i]);
items[i].dirty = true;
canvas.item(canvas.size()-1).hasControls = true;
}
canvas.renderAll();
}
});
Thanks
I have a project that I'm working on, and am trying to get a Fabric.JS to allow me to rotate & scale items, but only to the max scale of a bounding box. I tried quite a few combinations on the object:scaling event, to no avail. I have left a portion of the scaling event commented out, but though all my tries (determining movingBox width and such), i was unable to constrain the proportions of the square to the box.
Notice, before transforming the box stays within the outer bounds... exactly the functionality I desire. I just need the same functionality during the rotate & resize methods... which I assume we'll need to tap into the rotating & scale methods. Any help in modifying/adding to this to make these things possible, would be incredibly helpful.
Thanks,
$(function () {
var canvas = new fabric.Canvas("c");
canvas.setHeight(600);
canvas.setWidth(400);
var boundingBox = new fabric.Rect({
fill: "rgba(255, 255, 255, 0.0)",
width: 98,
height: 200,
hasBorders: false,
hasControls: false,
lockMovementX: true,
lockMovementY: true,
evented: false,
stroke: "black"
});
var movingBox = new fabric.Rect({
width: 50,
height: 50,
hasBorders: false,
hasControls: true,
lockRotation: false
});
canvas.on("object:moving", function () {
var top = movingBox.top;
var bottom = top + movingBox.height;
var left = movingBox.left;
var right = left + movingBox.width;
var topBound = boundingBox.top;
var bottomBound = topBound + boundingBox.height;
var leftBound = boundingBox.left;
var rightBound = leftBound + boundingBox.width;
movingBox.setLeft(Math.min(Math.max(left, leftBound), rightBound - movingBox.width));
movingBox.setTop(Math.min(Math.max(top, topBound), bottomBound - movingBox.height));
});
//canvas.on("object:scaling", function () {
// var top = movingBox.top;
// var bottom = top + movingBox.height;
// var left = movingBox.left;
// var right = movingBox.width;
//
// var topBound = boundingBox.top;
// var bottomBound = topBound + boundingBox.height;
// var leftBound = boundingBox.left;
// var rightBound = leftBound + boundingBox.width;
//
// // movingBox.setWidth // need alg here
// //movingBox.setHeight // need alg here
//});
canvas.add(boundingBox);
canvas.add(movingBox);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div style="position: absolute; top: 149px; left: 151px;">
<canvas id="c"></canvas>
</div>
For the scale you can use the fiddle below to set the width and height when you are scaling and then you can limit the figure width and height
var canvas = new fabric.Canvas("c1");
reinit()
canvas.on({
'object:scaling': function(e) {
var obj = e.target,
w = obj.width * obj.scaleX,
h = obj.height * obj.scaleY,
s = obj.strokeWidth;
console.log(obj.width, obj.scaleX, h,w,s)
obj._objects[0].set({
'height' : obj.height,
'width' : obj.width,
'scaleX' : 1,
'scaleY' : 1,
h: h,
w: w
//top: 1,
//left: 1,
});
/*e.target.set({
'height' : h,
'width' : w,
'scaleX' : 1,
'scaleY' : 1
});*/
}
});
canvas.on({
'object:modified': function(e) {
console.log(e)
//e.target.set({scaleX:1, scaleY:1})
group = e.target
rect = e.target._objects[0]
rect.set({height:rect.h, width: rect.w})
console.log('r',rect.width, group.width)
text = group._objects[1]
canvas.remove(group)
canvas.add(new fabric.Group([rect,text], {
top: group.top,
left: group.left
}))
}
});
function reinit(){
var el = new fabric.Rect({
originX: "left",
originY: "top",
stroke: "rgb(0,0,0)",
strokeWidth: 1,
fill: 'transparent',
opacity: 1,
width: 200,
height: 200,
cornerSize: 6
});
var text = new fabric.IText('test', { fontSize: 16});
var group = new fabric.Group([ el, text ], {
width: 200,
height: 200,
left: 5,
top: 5,
});
canvas.add(group);
canvas.renderAll();
}
http://jsfiddle.net/davidtorroija/qs0ywh8k/
I am trying to switch to the Main Menu state in phaser through a function but couldn't get it to work. Below is my code snippet. The end function is called from the update function in game.js file.
end: function(){
player.kill();
ltext.setText("Over!!");
this.state.start('Menu');
},
You'd better not to call this function from the update function.
Instead, you can call it from a sprite or button event handler etc.
I tested game.state.start('xx') in update function and it worked as expected.
window.addEventListener('load', function(){
var game = new Phaser.Game(500, 190, Phaser.CANVAS, '', {
create : function (game) {
var textStyle = { font: "14px Arial", fill: "#ffcc00" };
game.add.text(60, 40, 'Phaser HTML5 Game Engine', textStyle);
game.add.text(60, 70, 'This is state 1', textStyle);
var textStyle = { font: "14px Arial", fill: "#00ff00"};
game.add.text(200, 130, 'Pointer here to enter state2', textStyle);
var graphics = game.add.graphics(0, 0);
graphics.beginFill(0x00ff00);
graphics.drawRect(200, 80, 50, 50);
graphics.endFill();
},
update : function (game) {
var x = game.input.x, y = game.input.y;
if(x > 200 && x < 250 && y > 80 && y < 130){
game.state.start('state2');
}
}
});
game.state.add('state2', {
create: function (game){
var textStyle = { font: "14px Arial", fill: "#00ff00"};
game.add.text(60, 40, 'Phaser HTML5 Game Engine', textStyle);
game.add.text(60, 70, 'Welcome to state 2', textStyle);
}
});
}, false);
body{text-align:center;margin:4px;padding:0;}
canvas{vertical-align:middle; background-color:#000;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/2.3.0/phaser.min.js"></script>
If I uncomment the shiftHSL line below, the sprite does not appear.
<script src='https://rawgit.com/photonstorm/phaser/master/build/phaser.js'></script>
<script>
var game = new Phaser.Game(800, 400, Phaser.AUTO, '', {
preload: function() { this.game.load.image('dwarf', 'res/dwarf.png') },
create: function() {
var bmd = game.make.bitmapData()
bmd.width = 80
bmd.height = 80
bmd.circle(bmd.width / 2, bmd.width / 2, bmd.width / 2, '#ffffff')
bmd.alphaMask('dwarf', bmd)
// bmd.shiftHSL(0, -.5, 0)
var sprite = game.add.sprite(100, 100, bmd)
}
});
</script>
Ok, I got it working. Looks like you need to call bmd.update() before calling shiftHSL.
I'm trying to output something like these:
counter is: 10 <= fixed line and auto updating
console.logs, etc... <= other console.logs, errors, defaul outputs
console.logs, etc...
console.logs, etc...
console.logs, etc...
Is this possible?
I have tried with process.stdout.write() but it is not working.
var counter = 0;
setInterval(function(){
counter++;
process.stdout.write("counter is " + counter + " \r");
}, 500);
setInterval(function(){
console.log('some output');
}, 1500);
Here's an example using blessed:
var blessed = require('blessed');
var screen = blessed.screen(),
body = blessed.box({
top: 1,
left: 0,
width: '100%',
height: '99%'
}),
statusbar = blessed.box({
top: 0,
left: 0,
width: '100%',
height: 1,
style: {
fg: 'white',
bg: 'blue'
}
});
screen.append(statusbar);
screen.append(body);
screen.key(['escape', 'q', 'C-c'], function(ch, key) {
return process.exit(0);
});
function status(text) { statusbar.setContent(text); screen.render(); }
function log(text) { body.insertLine(0, text); screen.render(); }
var c = 1;
setInterval(function() {
status((new Date()).toISOString());
log('This is line #' + (c++));
}, 100);
Here's a simpler example that has almost the same effect (the status bar doesn't fill in extra space with background color):
var screen = blessed.screen(),
body = blessed.box({
top: 0,
left: 0,
width: '100%',
height: '100%',
tags: true
});
screen.append(body);
screen.key(['escape', 'q', 'C-c'], function(ch, key) {
return process.exit(0);
});
function status(text) {
body.setLine(0, '{blue-bg}' + text + '{/blue-bg}');
screen.render();
}
function log(text) {
body.insertLine(1, text);
screen.render();
}
var c = 1;
setInterval(function() {
status((new Date()).toISOString());
log('This is line #' + (c++));
}, 100);
Aside there are a lot of node modules that can help you do this,(blessed, ncurses, ansi, termhelper), for educational purposes you can also do it with vanilla node easily using process.stdout.moveCursor:
var logs = [];
function log(text) {
logs.push(text);
console.log(text);
}
function changeCounter(n) {
process.stdout.moveCursor(0, -logs.length - 1);
printCounter(n);
logs.forEach(function (log) { console.log(log) });
}
function printCounter(n) {
console.log('Counter is:', n);
}
// Now lets test
printCounter(0);
var i = 1;
setInterval(function () {
log('meoww');
changeCounter(i++);
});
Though you have to write to extra code to prevent overflowing terminal.
A traditional library for doing that sort of thing (drawing text at other than the bottom of the screen) is "curses"...there are bindings for Node.js but there is also "blessed" (ha ha) which looks easier to use: https://github.com/chjj/blessed