Does the Fabric.js Path Array have a size limit? - javascript

I am trying to plot a line graph using Fabric.js. The fabric.Path seems the way to go but it stops drawing after 8 segments.
I've tried loops and individually coding each segment and it always stops drawing after 8 segments
const canvas = Controller.Canvas;
line = new fabric.Path('M 90 104', { stroke: 'black', fill: '' });
lastLeft = 90;
for (i = 1; i < 20; i++) {
lastLeft += 20;
line.path[i] = ['L', lastLeft, 104];
}
canvas.add(line);
I would expect the code to draw a line of 20 segments. It stops at 8. The canvas is plenty large enough.

Looking at the fabric.js code, Line's path property is not really supposed to be modified like that - it's more of an internal property. After the path is parsed from the path data passed into the constructor, fabric calculates the Line's dimensions and position, which it then uses during a render call. This means that if you've modified path after constructor is run, you're going to end up with wrong dimensions, hence the missing lines, etc.
There is a way to make fabric re-calculate dimensions, although it uses a private call. Which means that it could change between fabric versions (the snippet below works with 3.4.0) and is therefore not recommended, unless you really want to mess with path for some reason:
const canvas = new fabric.Canvas('c')
const initialX = 10
const initialY = 10
const line = new fabric.Path(`M ${initialX} ${initialY}`, { stroke: 'black', fill: '' })
for (let i = 1; i < 20; i++) {
line.path[i] = ['L', initialX + i * 20, initialY + Math.pow(i, 2)]
}
/* WARNING: Hacky stuff start */
fabric.Polyline.prototype._setPositionDimensions.call(line, {})
/* Hacky stuff end */
canvas.add(line)
body {
background: ivory;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.4.0/fabric.min.js"></script>
<canvas id="c" width="500" height="400"></canvas>

Related

Fabric.Path Object's object.path does not give updated path when scaled or changed position in Fabric js

I am using Fabric js for my project.
I have a use case where I want an object to animate along the boundary of other fabric object. Similar to motion paths in power point. To implement this, I am creating a fabric.Path object and using this path, I am getting all the boundary points of the object and animating the object along these points. The code is as shown below.
<script src="./js/fabric.js"></script>
<canvas
id="c"
width="500"
height="500"
style="border: 1px solid #ccc"
></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
<script id="main">
var canvas = new fabric.Canvas("c");
var circle = new fabric.Circle({
radius: 30,
fill: "#f55"
});
canvas.add(circle);
var line = new fabric.Path(
"M 0 0 L 200 100 L 170 200 z",
{
fill: "",
stroke: "black",
objectCaching : true
}
);
line.set({ name: "dummy" });
canvas.add(line);
var points = getPathValues("M 0 0 L 200 100 L 170 200 z", 1000);
function getPathValues(path_val, samples) {
var path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
path.setAttribute("d", path_val);
var points = [];
var len = path.getTotalLength();
var step = (step = len / samples);
for (var i = 0; i <= len; i += step) {
var p = path.getPointAtLength(i);
points.push(p.x);
points.push(p.y);
}
return points;
}
var i = 0;
var interval = setInterval(function animate() {
i = i + 2;
if (i > points.length) {
// clearInterval(interval);
i = 0;
}
circle.left = line.left + points[i] - circle.radius;
circle.top = line.top + points[i + 1] - circle.radius;
canvas.renderAll();
}, 10);
With all this working well, Now when I scale or change position of the path object, I want to take the changed path, get the updated points and animate the object along those points. Now the problem is that when scale or change the position if the path object, The object.path for it is not getting updated automatically. I am not able to get the change path values which is needed for me to generate boundary points.
Is there any way to get the update path of the Fabric.Path object?
Is there any way to get the path of a normal fabric object?
Indeed the path data is transformed such that it becomes relative to the object's plane.
You should familiarize with relative planes and basics of matrix multiplication, at least the concepts.
This is why scale etc. don't affect it.
You need to apply the object's transformation matrix (via preTransform) to the points.
I can imagine this is too much.
That is why I have exposed fabric.util.sendPointToPlane. Check that out and it will save you a lot of headache.
fabric.util.sendPointToPlane(point, from, to), in your case fabric.util.sendPointToPlane(point, object.calcTransformMatrix(), null) will send the point to the canvas plane.
I wrote a post regarding relative planes but I can't find it, somewhere in fabric discussions

JavaScript canvas, manually cloning a canvas onto another generates a weird pattern

I'm trying to make a text effect similar to the effect found at the bottom of this article
My proposed approach is:
Make two canvasses, one is visible, the other is invisible I use this as a buffer.
Draw some text on the buffer canvas
Loop over getImageData pixels
if pixel alpha is not equal to zero (when there is a pixel drawn on the canvas buffer) with a small chance, ie 2%, draw a randomly generated circle with cool effecs at that pixel on the visible canvas.
I'm having trouble at step 4. With the code below, I'm trying to replicate the text on the second canvas, in full red. Instead I get this weird picture.
code
// create the canvas to replicate the buffer text on.
var draw = new Drawing(true);
var bufferText = function (size, textFont) {
// set the font to Georgia if it isn't defined
textFont = textFont || "Georgia";
// create a new canvas buffer, true means that it's visible on the screen
// Note, Drawing is a small library I wrote, it's just a wrapper over the canvas API
// it creates a new canvas and adds some functions to the context
// it doesn't change any of the original functions
var buffer = new Drawing(true);
// context is just a small wrapper library I wrote to make the canvas API a little more bearable.
with (buffer.context) {
font = util.format("{size}px {font}", {size: size, font: textFont});
fillText("Hi there", 0, size);
}
// get the imagedata and store the actual pixels array in data
var imageData = buffer.context.getImageData(0, 0, buffer.canvas.width, buffer.canvas.height);
var data = imageData.data;
var index, alpha, x, y;
// loop over the pixels
for (x = 0; x < imageData.width; x++) {
for (y = 0; y < imageData.height; y++) {
index = x * y * 4;
alpha = data[index + 3];
// if the alpha is not equal to 0, draw a red pixel at (x, y)
if (alpha !== 0) {
with (draw.context) {
dot(x/4, y/4, {fillColor: "red"})
}
}
}
}
};
bufferText(20);
Note that here, my buffer is actually visible to show where the red pixels are supposed to go compared to where they actually go.
I'm really confused by this problem.
If anybody knows an alternative approach, that's very welcome too.
replace this...
index = x * y * 4;
with...
index = (imageData.width * y) + x;
the rest is good :)

Drawing images in a 'grid' layout on the HTML5 canvas using JavaScript and the KineticJS library

I have an array of images which I want to draw to the HTML5 canvas in specific locations (i.e. using a 'grid' type layout).
I have a function called drawImage(imageObj) which currently draws all of the images in the array to random locations on the canvas. However, I want to be able to predetermine a set number of locations at which the images should be drawn, and then choose which image is drawn at which of these set locations randomly (it doesn't matter if more than one image is drawn to the same location).
My drawImage(imageObj) function currently looks like this:
function drawImage(imageObj) {
var canvasImage = new Kinetic.Image({
image: imageObj,
width: 50,
height: 50,
/* puts the images in random locations on the canvas */
x: stage.getWidth() / 20*Math.floor(Math.random()*20),
y: stage.getHeight() / 15*Math.floor(Math.random()*8+2),
draggable: true
});
// add cursor styling
canvasImage.on('mouseover', function() {
document.body.style.cursor = 'pointer';
});
canvasImage.on('mouseout', function() {
document.body.style.cursor = 'default';
});
imagesLayer.add(canvasImage);
}
I tried altering the function slightly to create a 'grid' of locations, and then draw each image to a random 'cell' in the grid:
function drawImage(imageObj) {
/* Create arrays of X & Y coordinates, and select the array elements randomly for use as
coordinates at which to draw the images*/
var xPos = new Array();
xPos[0] = 10;
xPos[1] = 70;
xPos[2] = 130;
xPos[3] = 190;
xPos[4] = 250;
xPos[5] = 310;
xPos[6] = 370;
xPos[7] = 430;
xPos[8] = 490;
xPos[9] = 550;
xPos[10] = 610;
xPos[11] = 670;
xPos[12] = 730;
xPos[13] = 790;
xPos[14] = 850;
xPos[15] = 910;
var yPos = new Array();
yPos[0] = 10;
yPos[1] = 70;
yPos[2] = 130;
yPos[3] = 190;
yPos[4] = 250;
yPos[5] = 310;
yPos[6] = 370;
yPos[7] = 430;
var canvasImage = new Kinetic.Image({
image: imageObj,
width: 50,
height: 50,
/* Now select a random X & Y position from the arrays to draw the images to */
x: xPos(getRandomXPosition),
y: yPos(getRandomYPosition),
draggable: true
/* puts the images in random locations on the canvas
x: stage.getWidth() / 20*Math.floor(Math.random()*20),
y: stage.getHeight() / 15*Math.floor(Math.random()*8+2),
draggable: true */
});
I had expected that the lines
x: xPos(getRandomXPosition),
y: yPos(getRandomYPosition),
would set the x and y coordinates of the image that is being drawn to the canvas to a random 'cell' in my 'grid' determined by which random elements of the xPos and yPos arrays were set as the x and y values of the image that was to be drawn.
However, when I view my page in the browser, I'm getting a console error which says that "xPos is not a function" on the line
x: xPos(getRandomXPosition),
I can't figure out why this is- does anyone have any ideas? I assume I will have the same error on the line
y: yPos(getRandomYPosition),
for the same reason.
I know that xPos is not a function- it is an array, and I am simply trying to retrieve the array element at position 'getRandomXPosition'.
I thought that this might be because 'getRandomXPosition' is not an int itself, it is a function, so I tried storing its output in a variable by changing those function definition lines to:
var randomXPosition = function getRandomXPosition(minX, maxX){
return Math.floor(Math.random()*(maxX - minX +1)) +minX;
}
var randomYPosition = function getRandomYPosition(minY, maxY){
return Math.floor(Math.random()*(maxY - minY +1)) +minY;
}
and then updating where I was using them so that I was now passing the variables as parameters instead of the functions:
x: xPos(randomXPosition),
y: yPos(randomYPosition),
draggable: true
However, when viewing the page in the browser, I am still getting the console error that says that "xPos is not a function" on the line
x: xPos(randomXPosition),
I can't figure out why this is- can anyone point me in the right direction? It's probably also worth mentioning that I'm using the kineticJS library to make the images 'draggable' when they're drawn to the canvas- just to give a more complete picture of my code.
Any advice would be much appreciated!
Edited 28/01/2013 # 18:05
Ok, I think I know why the images are all being drawn in the top left corner- the drawImage function that is being called is the one from the KineticJS library, not my own one. I am using a copy of the library that I've saved locally, as there are a few things that I have changed regarding the functionality that the library provides. Would it make sense to copy the code creating the arrays of positions and selecting the random elements from those positions into the drawImage function in the library instead?
Edited 29/01/2013 # 23:15
Right, I've had a look at the drawImage function in the kineticJS library (I'm using a local copy of the library), and it looks like this:
drawImage: function() {
var context = arguments[0];
context.save();
var a = Array.prototype.slice.call(arguments);
if(a.length === 6 || a.length === 10) {
if(a.length === 6) {
context.drawImage(a[1], a[2], a[3], a[4], a[5]);
}
else {
context.drawImage(a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9]);
}
}
context.restore();
}
I'm not sure that I fully understand all of the code here... What is the line
var a = Array.prototype.slice.call(arguments);
doing? I've not seen this 'slice' function before...
Can anyone point out how I would edit this function to include the code I've written to enable the images all to be drawn in separate locations by selecting the coordinates randomly from the arrays of coordinates? Presumably, I should just be able to copy and paste this code into the function, but I'm not sure where in the function I should put it... any suggestions?
xPos is an array , so you need to use array format to get its element , like xPos[key] ,
and randomXPosition is a function , you need to execute it to get its return value , like randomXPosition() .
In conclusion ,
X: xPos[randomXPosition()],
Well, to access a certain index in the array you have to use [] not (), like so:
xPos[randomXPosition] // if randomXPosition = 1, then this gives you the value at xPos[1].
xPos(randomXPosition) // <-- this is a function call, what it is expecting is something like:
function xPos(number){
var value = number * Math.random();
return value; //return some value maybe?
};

raphaelJS 2.1 animate along path

I want to animate a path (actually a set of paths, but I'll get to that) along a curved path.
RaphaelJS 2 removed the animateAlong method, for reasons I haven't been able to discern. Digging into the Raphael documentation's gears demo as abstracted by Zevan, I have got this far:
//adding a custom attribute to Raphael
(function() {
Raphael.fn.addGuides = function() {
this.ca.guide = function(g) {
return {
guide: g
};
};
this.ca.along = function(percent) {
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "t" + [point.x, point.y]
};
return t;
};
};
})();
var paper = Raphael("container", 600, 600);
paper.addGuides();
// the paths
var circ1 = paper.circle(50, 150, 40);
var circ2 = paper.circle(150, 150, 40);
var circ3 = paper.circle(250, 150, 40);
var circ4 = paper.circle(350, 150, 40);
var arc1 = paper.path("M179,204c22.667-7,37,5,38,9").attr({'stroke-width': '2', 'stroke': 'red'});
// the animation
// works but not at the right place
circ3.attr({guide : arc1, along : 1})
.animate({along : 0}, 2000, "linear");
http://jsfiddle.net/hKGLG/4/
I want the third circle to animate along the red path. It is animating now, but at a distance from the red path equal to the third circle's original coordinates. The weird thing is that this happens whether the transform translate in the along object is relative (lowercase "t") or absolute (uppercase "T"). It also always animates in the same spot, even if I nudge it with a transform translation just before the animate call.
Any help very appreciated. I just got off the boat here in vector-land. Pointers are helpful--a working fiddle is even better.
You're just a hop, skip, and jump away from the functionality that you want. The confusion here concerns the interaction between transformations and object properties -- specifically, that transformations do not modify the original object properties. Translating simply adds to, rather than replaces, the original coordinates of your circles.
The solution is extremely straightforward. In your along method:
this.ca.along = function(percent) {
var box = this.getBBox( false ); // determine the fundamental location of the object before transformation occurs
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "...T" + [point.x - ( box.x + ( box.width / 2 ) ), point.y - ( box.y + ( box.height / 2 ) )] // subtract the center coordinates of the object from the translation offset at this point in the guide.
};
return t;
Obviously, there's some room for optimization here (i.e., it might make sense to create all your circles at 0,0 and then translate them to the display coordinates you want, avoiding a lot of iterative math). But it's functional... see here.
One other caveat: the ...T translation won't effect any other transforms that have already been applied to a given circle. This implementation is not guaranteed to play nicely with other transforms.

Image editing with javascript

In the creation of my html5 game engine I've been able to do some nice things and get some cool features. On a contract to make a game I've been asked to see if I can remove the background color from sprite images. And I see the pluses with this since we could use jpgs instead on pngs and decrease the size of the images.
Is there any way I can do this with pure javascript? I'd like to be able to do this without using the a canvas element so it can be faster, but if I have to that's okay.
If I have to do that I have another question, I don't want the canvas object to show that I use, can I use a canvas object with document.createElement without applying it to the document? That would be nice since it wouldn't have to be rendered to the webpage. If not I guess I can just move the canvas object to the left out of view.
Lastly do you think a good way to preprocess the images be to send them to a server cgi script and have it return a json pixel array?
Here is the function for floodfill algorithm, it removed the background from an image which is already drawn on the canvas.
In the following code canvas is the HTML5 canvas element and context it canvas.getContext("2d"). You can change the value of colorRange and try the function with different colors. The last line of the function
imageElement.src=canvas.toDataURL("image/png");
is to show the image inside an img tag. So you need an img and a canvas on your page. If you don't want to show the image in img element just remove the last line.
// Remove backgroud without ajax call, can be used in non IE browsers.
function RemoveBackground(){
var startR,startG,startB;
var canvasData;
var canvasWidth=canvas.width;
var canvasHeight=canvas.height;
canvasData=mainContext.getImageData(0,0,canvasWidth,canvasHeight);
startR = canvasData.data[0];
startG = canvasData.data[1];
startB = canvasData.data[2];
if(startR==0&& startG==0 && startR==0) return;
var pixelStack = [[0, 0]];
while(pixelStack.length)
{
var newPos, x, y, pixelPos, reachLeft, reachRight;
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
pixelPos = (y*canvasWidth + x) * 4;
while(y-- >= 0 && matchStartColor(pixelPos,canvasData,startR,startG,startB)){
pixelPos -= canvasWidth * 4;
}
pixelPos += canvasWidth * 4;
++y;
reachLeft = false;
reachRight = false;
while(y++ < canvasHeight-1 && matchStartColor(pixelPos,canvasData,startR,startG,startB))
{
colorPixel(pixelPos,canvasData);
if(x > 0)
{
if(matchStartColor(pixelPos-4,canvasData,startR,startG,startB))
{
if(!reachLeft){
pixelStack.push([x - 1, y]);
reachLeft = true;
}
}
else if(reachLeft)
{
reachLeft = false;
}
}
if(x < canvasWidth-1)
{
if(matchStartColor(pixelPos+4,canvasData,startR,startG,startB))
{
if(!reachRight)
{
pixelStack.push([x + 1, y]);
reachRight = true;
}
}
else if(reachRight)
{
reachRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
context.putImageData(canvasData, 0, 0);
imageElement.src=canvas.toDataURL("image/png");
}
// Helper function for remove background color.
function matchStartColor(pixelPos,canvasData,startR,startG,startB)
{
var r = canvasData.data[pixelPos];
var g = canvasData.data[pixelPos+1];
var b = canvasData.data[pixelPos+2];
var colorRange=8;
return ((r >= startR-colorRange && r<=startR+colorRange)
&&( g >= startG-colorRange && g<=startG+colorRange)
&&( b >= startB-colorRange && b<= startB+colorRange));
}
// Helper function for remove background color.
function colorPixel(pixelPos,canvasData)
{
canvasData.data[pixelPos] = 255;
canvasData.data[pixelPos+1] = 255;
canvasData.data[pixelPos+2] = 255;
}
Removing background without choppy borders isn't a trivial task, even by hand in image-editing programs. You'll have to implement some sort of antialiasing, at least.
Moreover, it's not a good idea to manipulate an image compressed into a lossy format.
PNG compression is superior (in terms of size) to JPG on simpler images with continuous fill of the same color and certain types of gradients. JPG is only good for heterogeneous images with lots of different colors mixed in unpredictable manner. Like photos. Which one would not expect in game sprites, I guess. And again – JPG is a lossy format.
As for the Canvas element, it doesn't have to be added to the DOM tree at all.
The most naïve algorithm to make a given color transparent would be such: draw the image, get its pixel data, iterate over the data and compare every pixel color with your given color. If it matches, set the alpha to 0.
Canvas API methods you'll need:
drawImage
getImageData
The somewhat tricky in it's simplicity part is the CanvasPixelArray. To check each pixel in such arrays, you do something like that:
for (var i = 0; i < pixelAr.length; i += 4) {
var r = pixelAr[i];
var g = pixelAr[i + 1];
var b = pixelAr[i + 2];
var alpha = pixelAr[i + 3];
}
Personally I would not go down this path. JPEG images are compressed, which means that whatever you define as a background color may change slightly in the compressed file (ie. you'll get the classic JPEG artifacting). Furthermore, you won't be able to support partial transparency unless you define a range for your background color, which in turn makes the editing more complicated. The tradeoff between file size and performance/quality is nowhere near worth it here, in my opinion.
Having said that, the only way you can access the pixel data from an image is by placing it on a canvas first. You can, as you mentioned, work with the canvas off-screen in memory without having to append it to the document.
If I understand your last question correctly, you cannot work with a canvas element on the server side. To work with pixel data on your server, you'd have to use something like PHP's image library.
If all of that doesn't sway you in favor of just using PNG images, here's some sample code that will remove a specified background color from a loaded image:
$(document).ready(function() {
var matte_color = [0, 255, 0, 255]; // rgba: [0, 255];
// Handles the loaded image element by erasing the matte color
// and appending the transformed image to the document.
function handleLoadedImage() {
eraseMatte(); // Eliminate the matte.
// Append the canvas element to the document where it is needed.
document.getElementById("parent_container").appendChild(canvas);
}
// Given the matte color defined at the top, clear all pixels to 100% transparency
// within the loaded sprite.
function eraseMatte() {
canvas.width = sprite.width;
canvas.height = sprite.height;
context.drawImage(sprite, 0, 0); // Draw the image on the canvas so we can read the pixels.
// Get the pixel data from the canvas.
var image_data = context.getImageData(0, 0, sprite.width, sprite.height);
var data = image_data.data; // Obtaining a direct handle is a huge performance boost.
var end = sprite.width * sprite.height * 4; // W x H x RGBA
// Loop over each pixel from the image and clear matte pixels as needed.
for (var i = 0; i < end; i += 4) {
if (data[i] == matte_color[0] && data[i + 1] == matte_color[1] &&
data[i + 2] == matte_color[2] && data[i + 3] == matte_color[3]) { // Color match.
data[i] = data[i + 1] = data[i + 2] = data[i + 3] = 0; // Set pixel to transparent.
}
}
// Put the transformed image data back on the canvas.
context.putImageData(image_data, 0, 0);
}
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
var sprite = new Image();
sprite.onload = handleLoadedImage;
sprite.src = "sprite.jpg";
});
You can do that using a canvas, don't know if it is possible without it.
An easy way to achieve what you are trying to do is using the getImageData on your canvas's context:
imgData = myCanvasContext.getImageData(x1, y1, x2, y2);
x1, y1, x2, y2 are the coordenates of the area you want to get data, for the whole use 0, 0, width, height image. The getImageData will return you an ImageData, wich contains an array with rgba values from each pixel. The values will be ordered like this:
http://i.stack.imgur.com/tdHNJ.png
You can manipulate the array imgData.data[index], editing it values and, consequently, editing the image.
Here is a good article about editing images on html5 with canvas:
http://beej.us/blog/2010/02/html5s-canvas-part-ii-pixel-manipulation/
To doesn't show what you are doing, just create the canvas with the css command display:none;
(...)if I can remove the background color from sprite images. And I see the pluses with this since we could use jpgs instead on pngs(...)
I really recommend you to not do that. The jpg compression of the image can make image editing very hard. Removing the background of a jpg image isn't is easy, and it gets harder with the amount of borders on the image. I'm don't think the size that you will economize will compensate the hard work to remove a background from a jpg image.
Not exactly the same, but you can achieve that. I can give you an headstart on this - checkout this jsFiddle. I built this editor using FabricJS.
var canvas = new fabric.Canvas('c');
var imgInstance = new fabric.Image(imgElement);
canvas.add(imgInstance);//initialize the Canvas with the image

Categories

Resources