How to draw a trace of moving object in SVG - svg

I have object in SVG that I move using keyboard.
var svg = $('svg');
var avatar = svg.find('#avatar');
var left, top, right, bottom;
$(document).keydown(function(e) {
var svg = avatar[0].ownerSVGElement;
var matrix = avatar[0].getCTM();
if (e.which === 37 || left) { //left
matrix.translate(-1, 0);
e.preventDefault();
left = true;
}
if (e.which === 38 || top) { // top
matrix.translate(0, -1);
e.preventDefault();
top = true;
}
if (e.which === 39 || right) { // right
matrix.translate(1, 0);
e.preventDefault();
right = true;
}
if (e.which === 40 || bottom) { // bottom
matrix.translate(0, 1);
e.preventDefault();
bottom = true;
}
avatar.attr('transform', matrix.asString());
}).keyup(function(e) {
if (e.which === 37) {
left = false;
} else if (e.which === 38) {
top = false;
} else if (e.which === 39) {
right = false;
} else if (e.which === 40) {
bottom = false;
}
});
How can I add a trace (line that show the path of the moving avatar) in SVG? It need to be visible when I move my avatar. It would be nice if it can be not only a line but also a pattern.

I found a way, I could just put a path and add lines to it on keydown (appending it to d attribute), to simplify the path I put a line every 5 moves.
var trace;
(function() {
var start = $('#start')[0].getBBox();
var x = start.x+(start.width/2);
var y = start.y+(start.height/2);
trace = $('<path/>').attr({
id: 'trace',
stroke: '#000',
fill: 'none',
d: 'M ' + x + ' ' + y
}).appendTo(svg);
// in normal case trace path need to be added before svg is inserted to the DOM
// in my case I modify jquery library to use document.createElementNS instead of
// document.createElement
avatar.setPosition(x, y);
})();
var i = 0;
$(document).keydown(function(e) {
...
if (++i % 5 === 0) {
var avatar_size = avatar.size(); // size if jquery plugin that return width and height using BBox
var x = matrix.matrix[0][2];
var y = matrix.matrix[1][2];
trace.attr('d', trace.attr('d') + ', ' + x + ' ' + y);
}
});

Related

fabricjs on retina: new object jumps to left top

I continue my work on collaborative sketch tool and trying to add retina devices support. Currently i have following behavior if user creating drawing on ipad air:
small movie
Here is my code:
this.getZoomLevel = function (height) {
if (height > 1024) {
return 1024 / height;
} else {
return height / 1024;
}
};
this.calculateCanvasSize = function(pHeight, pWidth) {
var result = {
height: 0,
width: 0
};
while (result.width < pWidth - 1 && result.height < pHeight - 1) {
result.height = result.height + 1;
result.width = result.height * 4 / 3;
}
return result;
};
this.initCanvas = function () {
try {
var parent = document.getElementsByClassName('komaso-canvas-container')[0];
var canvasSize = this.calculateCanvasSize(parent.clientHeight, parent.clientWidth);
var canvasHtml = "<div id='wrapper-" + this.Id + "' class='whiteboard-canvas-wrapper' data-ng-show='CurrentPage.Id==" + this.Id + "'><canvas width='" + canvasSize.width + "' height='" + canvasSize.height + "' id='whiteboard-" + this.Id + "' class='whiteboard'><p>Your brower does not support Canvas/p></canvas></div>";
$(parent).append($compile(canvasHtml)(scope));
this.Canvaso = document.getElementById(this.HtmlId);
if (!this.Canvaso) {
console.log('Error: Cannot find the imageView canvas element!');
return;
}
if (!this.Canvaso.getContext) {
console.log('Error: no canvas.getContext!');
return;
}
this.FabricCanvas = new fabric.Canvas(this.HtmlId, { selectionColor: 'transparent' });
this.FabricCanvas.setWidth(canvasSize.width);
this.FabricCanvas.setHeight(canvasSize.height);
fabric.Object.prototype.transparentCorners = false;
this.FabricCanvas.on('mouse:down', this.onMouseDown);
this.FabricCanvas.on('mouse:up', this.onMouseUp);
this.FabricCanvas.on('mouse:move', this.onMouseMove);
this.FabricCanvas.on('object:added', this.onObjectAdded);
this.FabricCanvas.on('text:editing:exited', self.onTextObjectEdited);
if (window.devicePixelRatio !== 1) {
var c = this.FabricCanvas.getElement();
var w = c.width, h = c.height;
c.setAttribute('width', w * window.devicePixelRatio);
c.setAttribute('height', h * window.devicePixelRatio);
$(c).width(canvasSize.width);
$(c).height(canvasSize.height);
c.getContext('2d').scale(window.devicePixelRatio, window.devicePixelRatio);
}
this.FabricCanvas.setZoom(this.getZoomLevel(this.Canvaso.height));
this.ToggleTool(self.CurrentTool.ToolName);
this.WhiteboardInitiated = true;
} catch (e) {
console.log(e);
}
};
getZoomLevel returns value to pass into SetZoom method of fabric js canvas object. We decided to have all clients canvas aspects are 4:3 and default dimension is 1024*768. So based on this dimensions we calculation zoom factor.
calculateCanvasSize - returns width and height for canvas according to 4:3 rule.
If you have any idea about how to fix this wrong behavior then post your comment please. Thank you in advance!
I would suggest you yo update to a retina enabled version of fabricjs (grab 1.6.2).
If, for any reason you can't, i think the problem is here:
if (window.devicePixelRatio !== 1) {
var c = this.FabricCanvas.getElement();
...
c.getContext('2d').scale(window.devicePixelRatio, window.devicePixelRatio);
}
getContext return a new context. This is not the context where fabric is gonna render later. If you want to have retina enabled lowerCanvas you have to scale this.FabricCanvas.contextContainer that gets created and referenced on fabric.Canvas initialization.
I suggest you to switch to newer fabric anyway.

how to turn off sliding responsive menu in wordpress responsive select menu plugin?

I am using WP Responsive Select Menu plugin in wordpress and I have responsive menu while watching the website in the mobile devices.
In my IOS based phone, the responsive menu opens automatically when I scroll the screen to left (and sometimes also when I scroll down).
My .js file is as follows:
`
// Start Code
jQuery(document).ready(function( $ ) {
var bar = $('#wpresmenu_bar'), //top bar that shows/hides the menu
bar_height = bar.outerHeight(true), //the bar height
from_width = wpresmenu.from_width,
menu = $('#wpresmenu_menu'), //the menu div
menu_ul = $('#wpresmenu_menu_ul'), //the menu ul
menu_a = menu.find('a'), //single menu link
body = $('body'),
html = $('html'),
animation_speed = 300,
ab = $('#wpadminbar'),
menu_enabled = (bar.length > 0 && menu.length > 0)? true : false,
menu_width = menu.width(),
target_height = (window.innerHeight < body.height())? body.height() : window.innerHeight,
target_width = (window.innerWidth < body.width())? body.width() : window.innerWidth;
if(menu_enabled) {
menu_ul.find('li').first().css({'border-top':'none'});
$(document).mouseup(function (e) {
if ( !menu.is(e.target) && menu.has( e.target ).length === 0) {
if(menu.is(':visible') && (!menu.hasClass('top'))) {
$.sidr('close', 'wpresmenu_menu');
}
}
});
//ENABLE NESTING
//add arrow element to the parent li items and chide its child uls
menu.find('ul.sub-menu').each(function() {
var sub_ul = $(this),
parent_a = sub_ul.prev('a'),
parent_li = parent_a.parent('li').first();
parent_a.addClass('wpresmenu_parent_item');
parent_li.addClass('wpresmenu_parent_item_li');
var expand = parent_a.before('<span class="wpresmenu_icon wpresmenu_icon_par icon_default"></span> ').find('.wpresmenu_icon_par');
sub_ul.hide();
});
//adjust the a width on parent uls so it fits nicely with th eicon elemnt
function adjust_expandable_items() {
$('.wpresmenu_parent_item_li').each(function() {
var t = $(this),
main_ul_width = 0,
icon = t.find('.wpresmenu_icon_par').first(),
link = t.find('a.wpresmenu_parent_item').first();
if(menu.hasClass('top')) {
main_ul_width = window.innerWidth;
} else {
main_ul_width = menu_ul.innerWidth();
}
if(t.find('.wpresmenu_clear').length == 0) link.after('<br class="wpresmenu_clear"/>');
});
}
adjust_expandable_items();
//expand / collapse action (SUBLEVELS)
$('.wpresmenu_icon_par').on('click',function() {
var t = $(this),
//child_ul = t.next('a').next('ul');
child_ul = t.parent('li').find('ul.sub-menu').first();
child_ul.slideToggle(300);
t.toggleClass('wpresmenu_par_opened');
t.parent('li').first().toggleClass('wpresmenu_no_border_bottom');
});
//helper - close all submenus when menu is hiding
function close_sub_uls() {
menu.find('ul.sub-menu').each(function() {
var ul = $(this),
icon = ul.parent('li').find('.wpresmenu_icon_par'),
li = ul.parent('li');
if(ul.is(':visible')) ul.slideUp(300);
icon.removeClass('wpresmenu_par_opened');
li.removeClass('wpresmenu_no_border_bottom');
});
}
//fix the scaling issue by adding/replacing viewport metatag
var mt = $('meta[name=viewport]');
mt = mt.length ? mt : $('<meta name="viewport" />').appendTo('head');
if(wpresmenu.zooming == 'no') {
mt.attr('content', 'user-scalable=no, width=device-width, maximum-scale=1, minimum-scale=1');
} else {
mt.attr('content', 'user-scalable=yes, width=device-width, initial-scale=1.0, minimum-scale=1');
}
//Additional fixes on change device orientation
if( $.browser.mozilla ) {
screen.addEventListener("orientationchange", function() {updateOrientation()}); //firefox
} else if( window.addEventListener ) {
window.addEventListener('orientationchange', updateOrientation, false);
}
else {
window.attachEvent( "orientationchange" );
}
function updateOrientation() {
window.scrollBy(1,1);
window.scrollBy(-1,-1);
menu_width = menu.width();
//update the page posion for left menu
if(menu.is(':visible') && menu.hasClass('left')) {
body.css({'left':menu_width});
body.scrollLeft(0);
}
}
//apply the SIDR for the left/right menu
if(menu.hasClass('left') || menu.hasClass('right')) {
//appy sidr
var hor_pos = (menu.hasClass('left'))? 'left' : 'right';
bar.sidr({
name:'wpresmenu_menu',
side: hor_pos,
speed: animation_speed,
onOpen: function(){ bar.addClass('menu_is_opened'); },
onClose: function(){ bar.removeClass('menu_is_opened'); close_sub_uls(); }
});
//when link is clicked - hide the menu first and then change location to new page
menu_a.on('click', function(e) {
$.sidr('close', 'wpresmenu_menu');
});
if( wpresmenu.swipe != 'no' ) {
$('body').touchwipe({
wipeLeft: function() {
// Close
$.sidr('close', 'wpresmenu_menu');
},
wipeRight: function() {
// Open
$.sidr('open', 'wpresmenu_menu');
},
min_move_x: 60,
min_move_y: 60,
preventDefaultEvents: false
});
}
$(window).resize(function(){
target_width = (window.innerWidth < body.width())? body.width() : window.innerWidth;
if(target_width > from_width && menu.is(':visible')) {
$.sidr('close', 'wpresmenu_menu');
}
});
} else if(menu.hasClass('top')) { //The top positioned menu
body.prepend(menu);
//show / hide the menu
bar.on('click', function(e) {
//scroll window top
$("html, body").animate({ scrollTop: 0 }, animation_speed);
close_sub_uls();
menu.stop(true, false).slideToggle(animation_speed);
});
//when link is clicked - hide the menu first and then change location to new page
menu_a.on('click', function(e) {
e.preventDefault();
var url = $(this).attr('href');
menu.slideUp(animation_speed,function() {
//go to the url from the link
window.location.href = url;
});
});
$(window).resize(function(){
target_width = (window.innerWidth < body.width())? body.width() : window.innerWidth;
if(target_width > from_width && menu.is(':visible')) {
close_sub_uls();
menu.slideUp(animation_speed, function() {});
}
});
} //end if class left / top /right
} //end if menu enabled
}); `
I don't want the menu to load on sliding the screen to left or right. Any idea how to do that??
I have figured it out.
Commenting out the following code stops it.
if( wpresmenu.swipe != 'no' ) {
$('body').touchwipe({
wipeLeft: function() {
// Close
$.sidr('close', 'wpresmenu_menu');
},
wipeRight: function() {
// Open
$.sidr('open', 'wpresmenu_menu');
},
min_move_x: 60,
min_move_y: 60,
preventDefaultEvents: false
});
}
:)

Undo-Redo feature in Fabric.js

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();
});

How to return scrollbar in Sharepoint 2010

I want to fix scrollbar in popUp window,but unfortunately fix it in my other pages!
Please,tell me- how to return scrollbars in Sharepoint 2010.
When i want to fix scrolls- use :
function FixRibbonAndWorkspaceDimensions() {
ULSxSy:;
g_frl = true;
var elmRibbon = GetCachedElement("s4-ribbonrow");
var elmWorkspace = GetCachedElement("s4-workspace");
var elmTitleArea = GetCachedElement("s4-titlerow");
var elmBodyTable = GetCachedElement("s4-bodyContainer");
if (!elmRibbon || !elmWorkspace || !elmBodyTable) { return; }
if (!g_setWidthInited) {
var setWidth = true;
if (elmWorkspace.className.indexOf("s4-nosetwidth") > -1)
setWidth = false;
g_setWidth = setWidth;
g_setWidthInited = true;
}
else { var setWidth = g_setWidth; }
var baseRibbonHeight = RibbonIsMinimized() ? 44 : 135;
var ribbonHeight = baseRibbonHeight + g_wpadderHeight;
if (GetCurrentEltStyle(elmRibbon, "visibility") == "hidden") { ribbonHeight = 0; }
// Override default resizing behavior
// -- adds padding to the top of the "s4-workspace" <div> if the ribbon exists and has content
// -- allows the ribbon to be positioned using CSS instead of JavaScript (more accessible)
// -- checks to see if the page is inside a "no-ribbon" dialog
var elmRibbonContainer = GetCachedElement("RibbonContainer");
if (elmRibbonContainer != null) {
if (elmRibbonContainer.children.length > 0 && document.getElementsByTagName("html")[0].className.indexOf('ms-dialog-nr') == -1) {
elmWorkspace.style.paddingTop = ribbonHeight + 'px';
}
}
}
i think ,that i have to write

Opening tabs in Chrome news reader extension

I'm trying to create a simple chrome extension using the following google RSS reader sample,
http://code.google.com/chrome/extensions/samples.html#597015d3bcce3da693b02314afd607bec4f55291
I can add links in the pop-up window that open tabs, but not from the feeds themselves.
Looping through the items in the feed, grabbing title tags and link tags, I want the title to link the the appropriate sites
var entries = doc.getElementsByTagName('item');
var count = Math.min(entries.length, maxFeedItems);
for (var i = 0; i < count; i++) {
item = entries.item(i);
// Grab the title for the feed item.
var itemTitle = item.getElementsByTagName('title')[0];
if (itemTitle) {
itemTitle = itemTitle.textContent;
} else {
itemTitle = "Unknown title";
}
// Grab the link for this feed item
var itemLink = item.getElementsByTagName('link')[0];
if (itemLink) {
itemLink = itemLink.textContent;
} else {
itemLink = "Unknown link";
}
var title = document.createElement("a");
title.className = "item_title";
title.innerText = itemTitle; //display title in iframe
title.addEventListener("click", titleLink); // should open link when clicking on title, but does not.
}
// -------------------------------------------------------------------
// Show |url| in a new tab.
function showUrl(url) {
// Only allow http and https URLs.
if (url.indexOf("http:") != 0 && url.indexOf("https:") != 0) {
return;
}
chrome.tabs.create({url: url});
}
function moreStories(event) {
showUrl(moreStoriesUrl);
}
function titleLink(event) {
showUrl(itemLink);
}
Any thoughts on why this is not working.
If I replace title.addEventListener("click", titleLink); with title.addEventListener("click", moreStories); each title will link to moreStories, I cannot get each title to link to itemLink.
Thanks
Its a bit hard to answer your question without the whole code, but Ill give it a shot ;)
First up, titleLink() isnt going to work because itemLink isnt known. When you create title (the link) you should of attached it to that...say title.href=itemLink then in tiltleLinks you could access that href with showUrl(event.currentTarget.href)
Also did you fix the error in that example?...if not then change frameLoaded to....
function frameLoaded() {
var links = document.getElementsByTagName("A");
for (i = 0; i < links.length; i++) {
var clssName = links[i].className;
if (clssName != "item_title" && clssName != "open_box") {
links[i].addEventListener("click", showStory);
}
}
window.addEventListener("message", messageHandler);
}
If you still have probs could you attach the whole code so I can see what your doing and Ill give you a hand.
Thank you very much for your help.
code title.href=itemLink and code showUrl(event.currentTarget.href) was exactly what I needed.
For completeness, here is the full code,
<script id="iframe_script">
function reportHeight() {
var msg = JSON.stringify({type:"size", size:document.body.offsetHeight});
parent.postMessage(msg, "*");
}
function frameLoaded() {
var links = document.getElementsByTagName("A");
for (i = 0; i < links.length; i++) {
var class = links[i].className;
if (class != "item_title" && class != "open_box") {
links[i].addEventListener("click", showStory);
}
}
window.addEventListener("message", messageHandler);
}
function showStory(event) {
var href = event.currentTarget.href;
parent.postMessage(JSON.stringify({type:"show", url:href}), "*");
event.preventDefault();
}
function messageHandler(event) {
reportHeight();
}
</script>
<script>
// Feed URL.
var feedUrl = 'http://localhost/newsfeed.xml';
// The XMLHttpRequest object that tries to load and parse the feed.
var req;
function main() {
req = new XMLHttpRequest();
req.onload = handleResponse;
req.onerror = handleError;
req.open("GET", feedUrl, true);
req.send(null);
}
// Handles feed parsing errors.
function handleFeedParsingFailed(error) {
var feed = document.getElementById("feed");
feed.className = "error";
feed.innerText = "Error: " + error;
}
// Handles errors during the XMLHttpRequest.
function handleError() {
handleFeedParsingFailed('Failed to fetch RSS feed.');
}
// Handles parsing the feed data we got back from XMLHttpRequest.
function handleResponse() {
var doc = req.responseXML;
if (!doc) {
handleFeedParsingFailed("Not a valid feed.");
return;
}
buildPreview(doc);
}
// The maximum number of feed items to show in the preview.
var maxFeedItems = 10;
// Where the more stories link should navigate to.
var moreStoriesUrl;
function buildPreview(doc) {
// Get the link to the feed source.
var link = doc.getElementsByTagName("link");
var parentTag = link[0].parentNode.tagName;
if (parentTag != "item" && parentTag != "entry") {
moreStoriesUrl = link[0].textContent;
}
// Setup the title image.
var images = doc.getElementsByTagName("image");
var titleImg;
if (images.length != 0) {
var urls = images[0].getElementsByTagName("url");
if (urls.length != 0) {
titleImg = urls[0].textContent;
}
}
var img = document.getElementById("title");
// Listen for mouse and key events
if (titleImg) {
img.src = titleImg;
if (moreStoriesUrl) {
document.getElementById("title_a").addEventListener("click",moreStories);
document.getElementById("title_a").addEventListener("keydown",
function(event) {
if (event.keyCode == 13) {
moreStories(event);
}});
}
} else {
img.style.display = "none";
}
// Construct the iframe's HTML.
var iframe_src = "<!doctype html><html><head><script>" +
document.getElementById("iframe_script").textContent + "<" +
"/script></head><body onload='frameLoaded();' " +
"style='padding:0px;margin:0px;'>";
var feed = document.getElementById("feed");
// Set ARIA role indicating the feed element has a tree structure
feed.setAttribute("role", "tree");
var entries = doc.getElementsByTagName('item');
var count = Math.min(entries.length, maxFeedItems);
for (var i = 0; i < count; i++) {
item = entries.item(i);
// Grab the title for the feed item.
var itemTitle = item.getElementsByTagName('title')[0];
if (itemTitle) {
itemTitle = itemTitle.textContent;
} else {
itemTitle = "Unknown title";
}
// Grab the link for the feed item.
var itemLink = item.getElementsByTagName('link')[0];
if (itemLink) {
itemLink = itemLink.textContent;
} else {
itemLink = "Unknown link";
}
var item = document.createElement("div");
var title = document.createElement("a");
title.innerText = itemTitle; //display title in iframe
title.href=itemLink;
title.addEventListener("click", titleLink);
item.appendChild(title);
feed.appendChild(item);
}
if (moreStoriesUrl) {
var more = document.createElement("a");
more.className = "more";
more.innerText = "***Site Main Page*** \u00BB";
more.tabIndex = 0;
more.addEventListener("click", moreStories);
more.addEventListener("keydown", function(event) {
if (event.keyCode == 13) {
moreStories(event);
}});
feed.appendChild(more);
}
}
// -------------------------------------------------------------------
// Show |url| in a new tab.
function showUrl(url) {
// Only allow http and https URLs.
if (url.indexOf("http:") != 0 && url.indexOf("https:") != 0) {
return;
}
chrome.tabs.create({url: url});
}
// -------------------------------------------------------------------
function moreStories(event) {
showUrl(moreStoriesUrl);
}
function titleLink(event) {
showUrl(event.currentTarget.href);
}
function keyHandlerShowDesc(event) {
// Display content under heading when spacebar or right-arrow pressed
// Hide content when spacebar pressed again or left-arrow pressed
// Move to next heading when down-arrow pressed
// Move to previous heading when up-arrow pressed
if (event.keyCode == 32) {
showDesc(event);
} else if ((this.parentNode.className == "item opened") &&
(event.keyCode == 37)) {
showDesc(event);
} else if ((this.parentNode.className == "item") && (event.keyCode == 39)) {
showDesc(event);
} else if (event.keyCode == 40) {
if (this.parentNode.nextSibling) {
this.parentNode.nextSibling.children[1].focus();
}
} else if (event.keyCode == 38) {
if (this.parentNode.previousSibling) {
this.parentNode.previousSibling.children[1].focus();
}
}
}
function showDesc(event) {
var item = event.currentTarget.parentNode;
var items = document.getElementsByClassName("item");
for (var i = 0; i < items.length; i++) {
var iframe = items[i].getElementsByClassName("item_desc")[0];
if (items[i] == item && items[i].className == "item") {
items[i].className = "item opened";
iframe.contentWindow.postMessage("reportHeight", "*");
// Set the ARIA state indicating the tree item is currently expanded.
items[i].getElementsByClassName("item_title")[0].
setAttribute("aria-expanded", "true");
iframe.tabIndex = 0;
} else {
items[i].className = "item";
iframe.style.height = "0px";
// Set the ARIA state indicating the tree item is currently collapsed.
items[i].getElementsByClassName("item_title")[0].
setAttribute("aria-expanded", "false");
iframe.tabIndex = -1;
}
}
}
function iframeMessageHandler(e) {
// Only listen to messages from one of our own iframes.
var iframes = document.getElementsByTagName("IFRAME");
for (var i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow == e.source) {
var msg = JSON.parse(e.data);
if (msg) {
if (msg.type == "size") {
iframes[i].style.height = msg.size + "px";
}
else if (msg.type == "show") {
var url = msg.url;
if (url.indexOf("http://localhost/index.html") == 0) {
// If the URL is a redirect URL, strip of the destination and go to
// that directly. This is necessary because the Google news
// redirector blocks use of the redirects in this case.
var index = url.indexOf("&url=");
if (index >= 0) {
url = url.substring(index + 5);
index = url.indexOf("&");
if (index >= 0)
url = url.substring(0, index);
}
}
showUrl(url);
}
}
return;
}
}
}
window.addEventListener("message", iframeMessageHandler);
</script>
Thanks again for the help.
-Mike

Resources