Animate drawing of path on HTML5 canvas - javascript

My problem is how to animate the drawing of a path between two points.
Consider a curved line or path between two points, A & B. I can draw this easily on the canvas using the line drawing functions in Konvajs.
However, what I actually want is to animate the revealing of the line so that it starts from point A and progressively draws to point B. The reveal should be animated so I can apply pleasing easings.
As a comparable example, see the brief video on this site https://coggle.it/ where the video shows the creation of a new box and the line draws to connect it to the old.

Here is a potential answer (special thanks to #markov00 re same technique in SVG). It works by manipulating the path dashOffset and dash attributes. There is an excellent explanation of the technique here in a post by Jake Archibald which also includes an interactive experiment with sliders which I found very useful.
I have tried to make the demo as lightweight as possible and just show the technique - though I added a slider and some UI to help understand the process. The use of jquery is only for the UI parts which are not needed for the technique.
Couple of points:
The demo here uses a path from 3 straight line segments. But I tried curves and combination paths and the technique works in those cases too - so any path should work.
I found that using a close-path command (z) on the path caused the path length function to be short on the true distance. This appears as an amount of the path remaining stroked or gapped at either end, with the size depending on the jump between first & last to close the path.
The path length is virtually always going to be decimal so don't try to do everything as integers as you will ultimately find your stroke is slightly overlong or short.
To adopt this for animation and easing etc, take the couple of lines from the slider change event and stick them inside the frame callback, manipulating the maths to suit your case.
// Set up the canvas / stage
var stage = new Konva.Stage({container: 'container1', width: 320, height: 180});
// Add a layer
var layer = new Konva.Layer({draggable: false});
stage.add(layer);
// show where the start of the path is.
var circle = new Konva.Circle({
x: 66,
y: 15,
radius: 5,
stroke: 'red'
})
layer.add(circle);
// draw a path.
var path = new Konva.Path({
x: 0,
y: 0,
data: 'M66 15 L75 100 L225 120 L100 17 L66 15',
stroke: 'green'
});
// get the path length and set this as the dash and dashOffset.
var pathLen = path.getLength();
path.dashOffset(pathLen);
path.dash([pathLen]);
layer.add(path)
stage.draw();
// Some UI bits
$('#dist').attr('max', parseInt(pathLen)); // set slider max to length of path
$('#pathLen').html('Path : ' + pathLen); // display path length
// jquery event listener on slider change
$('#dist').on('input', function(){
// compute the new dash lenth as original path length - current slider value.
// Means that dashLen initially = path len and moves toward zero as slider val increases.
var dashLen = pathLen - $(this).val();;
path.dashOffset(dashLen); // set new value
layer.draw(); // refresh the layer to see effect
// update the UI elements
$('#dashLen').html('Dash: ' + dashLen);
$('#pathPC').html(parseInt(100-(100 * (dashLen/pathLen)), 10) + '%');
})
.info
{
padding-left: 20px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.js"></script>
<div class="slidecontainer">
<input class='slider' id='dist' type="range" min="0" max="100" value="0" class="slider" id="myRange"/>
<span class='info' id='pathPC'></span>
<span class='info' id='pathLen'></span>
<span class='info' id='dashLen'></span>
</div>
<div id='container1' style="display: inline-block; width: 300px, height: 200px; background-color: silver; overflow: hidden; position: relative;"></div>
<div id='img'></div>

My solution with animation:
var width = window.innerWidth;
var height = window.innerHeight;
// Set up the canvas / stage
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});
// Add a layer
var layer = new Konva.Layer({
draggable: false
});
stage.add(layer);
// show where the start of the path is.
var circle = new Konva.Circle({
x: 66,
y: 15,
radius: 5,
stroke: 'red'
})
layer.add(circle);
// draw a path.
var path = new Konva.Path({
x: 0,
y: 0,
data: 'M66 15 L75 100 L225 120 L100 17 L66 15',
stroke: 'green'
});
// get the path length and set this as the dash and dashOffset.
var pathLen = path.getLength();
path.dashOffset(pathLen);
path.dash([pathLen]);
// make some animation with stop
var anim = new Konva.Animation(function (frame) {
var dashLen = pathLen - frame.time / 5;
path.dashOffset(dashLen);
if (dashLen < 0) {
anim.stop();
}
}, layer);
anim.start();
layer.add(path)
stage.draw();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.js"></script>
<div id='container' style="display: inline-block; width: 300px, height: 200px; background-color: silver; overflow: hidden; position: relative;"></div>
<div id='img'></div>

And here is an alternative to #Roxane's animated version but using a tween.
var width = window.innerWidth;
var height = window.innerHeight;
// Set up the canvas / stage
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});
// Add a layer
var layer = new Konva.Layer({
draggable: false
});
stage.add(layer);
// show where the start of the path is.
var circle = new Konva.Circle({
x: 66,
y: 15,
radius: 5,
stroke: 'red'
})
layer.add(circle);
// draw a path.
var path = new Konva.Path({
x: 0,
y: 0,
data: 'M66 15 L75 100 L225 120 L100 17 L66 15',
stroke: 'green'
});
// get the path length and set this as the dash and dashOffset.
var pathLen = path.getLength();
path.dashOffset(pathLen);
path.dash([pathLen]);
layer.add(path); // have to add to layer for tweening.
// create the tween
var tween = new Konva.Tween({
node: path,
dashOffset: 0,
easing: Konva.Easings['BounceEaseOut'],
duration: 1.5
});
tween.play(); // execute the tween
stage.draw();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.js"></script>
<div id='container' style="display: inline-block; width: 300px, height: 200px; background-color: silver; overflow: hidden; position: relative;"></div>
<div id='img'></div>

Related

Konva JS. Change Image position, rotate

I'm adding an image.
circle = new Image ();
circle.src = '/img/logo.png';
circle.onload = function () {
anyimage = new Konva.Image ({
x: 150,
y: 150,
image: circle,
width: 106,
height: 118
});
layer.add (anyimage);
stage.add (layer);
};
How to get and change the position and angle of this picture?
How to change these settings later. By events. For example, clicking on the buttons.
Method this.setX(),this.rotare(), this.x= not work for image obj.
Solution. Need use anyimage obj. Not cirle.
<script src="https://cdn.rawgit.com/konvajs/konva/1.3.0/konva.js"></script>
<button onclick='rotate_image()'>rotate_image</button>
<button onclick='setPos_image()'>rotsetPos_imageate_image</button>
<div id="container"></div>
var stage = new Konva.Stage({
container: 'container', // индификатор div контейнера
width: 500,
height: 500
});
var layer = new Konva.Layer();
circle = new Image();
circle.src = 'https://im0-tub-ru.yandex.net/i?id=caa07f7c7eb5b2788719c85cd6028d23&n=13';
circle.onload = function() {
anyimage = new Konva.Image({
x: 10,
y: 10,
image: circle,
width: 106,
height: 118
});
layer.add(anyimage);
stage.add(layer);
};
function rotate_image(){
anyimage.rotate(45);
stage.draw();
console.log('loaded');
}
function setPos_image(){
//code for change x,y coord of 'circle' obj
anyimage.setX(45);
stage.draw();
console.log('loaded');
}
The position and size are as you have set in the new Konva.Image() call. Rotation is demonstrated in this example and below in the working snippet. Basically there is a rotation point set by the 'offset' property of the shape. By default this is at the top left of the image rectangle. You apply the shape's rotate() function setting the single parameter to the amount of degrees to rotate by, with rotation happening around the shapes offset(x,y) position.
See snippet below for a playpen.
Note: I have raised a question with the author over the apparent unexpected behaviour that means when you change the offset position, intending to change the center of rotation, the shape is physically moved.
// Add a stage for the shapes
var stage = new Konva.Stage({
container: 'container',
width: 1600,
height: 400
});
// add a layer
var layer = new Konva.Layer();
stage.add(layer);
// add a rect to demonstrate rotation
var r = new Konva.Rect({
x: 60,
y: 30,
width: 50,
height: 50,
fill: 'red',
opacity: 0.5,
strokeWidth: 0})
layer.add(r);
// add a spot to mark the rotate pt
var c = new Konva.Circle({
x: 45,
y: 45,
radius: 4,
fill: 'red',
stroke: 'black',
strokeWidth: 4})
layer.add(c);
stage.draw();
// event for plus & minus buttons
$('#plus').on('click', function(evt){
evt.preventDefault()
r.rotate(10)
stage.draw();
})
$('#minus').on('click', function(evt){
evt.preventDefault()
r.rotate(-10)
stage.draw();
})
// function to set rotate point and shape
function setPos(pos){
r.setAttr('offsetX', pos.x);
r.setAttr('offsetY', pos.y);
c.position({
x: r.x(),
y: r.y()
});
c.moveToTop();
sayPos();
stage.draw();
}
$('#ctr').on('click', function(evt){
evt.preventDefault()
setPos({x:25, y:25});
})
$('#topLeft').on('click', function(evt){
evt.preventDefault()
setPos({x:0, y:0});
})
$('#topRight').on('click', function(evt){
evt.preventDefault()
setPos({x:50, y:0});
})
$('#botCtr').on('click', function(evt){
evt.preventDefault()
setPos({x:25, y:50});
})
function sayPos(){
$('#info').html('Rotate pt=' + r.offsetX() + ", " + r.offsetY());
}
// call the setPos() and sayPos() funcs on load.
setPos({x:0, y:0});
sayPos();
p
{
padding: 4px;
}
#container
{
background-color: silver;
overflow: hidden;
}
div
{
padding: 4px;
}
<div id='info1'></div>
<div id='info2'></div>
<div>Click row 1 buttons to set rotate pt and row 2 buttons to rotate by 10 degrees</div>
<div>
<button id='topLeft'>Move rotate pt to top left</button>
<button id='ctr'>Move rotate pt to center</button>
<button id='topRight'>Move rotate pt to top right</button>
<button id='botCtr'>Move rotate pt to bottom center</button>
</div>
<div>
<button id='plus'>+10</button>
<button id='minus'>-10</button>
<span id='info'>Info:</span>
</div>
<div id="container"></div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/konvajs/konva/1.7.6/konva.min.js"></script>
<script type="text/javascript" src="test.js"></script>

Drag object programmatically

I have 3 rectangles on top of eachother like so:
new Fabric.Rect({
width: 200 - index * 30,
height: 20,
hasBorders: false,
selectable: false,
hasControls: false
});
and then I have a click event which detects clicks NEAR (not on the rectangle) the rectangles and makes the top rectangle (highest one of the stack of 3) selectable (to drag it):
var first = this.first().shape; // Fabric.Rect
canvas.setActiveObject(first);
However, this does not set the object on the cursor, to drag the object.
How can I make it so the object is selected, immediately moved to the cursor and enabled to be dragged around once the click event fires?
This should get you fairly close, if I understood you correctly.
Click anywhere inside the black square of the canvas and outside the red object.
var canvas = new fabric.Canvas('c', {
selection: false,
});
var rectangle = new fabric.Rect({
fill: 'red',
left: 10,
top: 10,
width: 100,
height: 100 //,
//padding: 50
});
canvas.on('mouse:down', function(env) {
var x = env.e.offsetX;
var y = env.e.offsetY;
rectangle.setLeft(x - rectangle.width / 2);
rectangle.setTop(y - rectangle.height / 2);
canvas.setActiveObject(rectangle);
rectangle.setCoords();
canvas.renderAll();
canvas.on('mouse:move', function(env) {
var x = env.e.offsetX;
var y = env.e.offsetY;
rectangle.setLeft(x - rectangle.width / 2);
rectangle.setTop(y - rectangle.height / 2);
rectangle.setCoords();
canvas.renderAll();
});
canvas.on('mouse:up', function(env) {
canvas.off('mouse:move');
});
});
canvas.add(rectangle);
canvas.renderAll();
#c {
border: 1px solid black;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.0/fabric.min.js"></script>
<canvas id="c" width="200" height="200"></canvas>
I intentionally commented out padding on the rectangle, but left it in the code, in case you wanted to use that as your NEAR logic instead of what you already have. If you do choose to use padding as your NEAR logic you will then need to change the on canvas mouse:down event to an on canvas object:selected event.
Also, if you haven't done so already, you might like to take a close look at this Objects Bounding Rectangles example for some further ideas for your NEAR logic, http://fabricjs.com/bounding-rectangle.
Any-who, let me know how you get on, matey!

Creating complex clipping path for image?

I'm new to fabricjs (and to Javascript development in general). I am "porting" a legacy Flex/Actionscript project and need to enable the user to create a complex clipping path for an image.
My approach in Actionscript was to use the Actionscript Graphics class using BlendMode.ERASE to "erase" from the yellow base rectangle (i.e. give the appearance of erasing) and then using that set of rects to create a bitmap to serve as an alpha channel for the final image (Step 3) created on the fly.
Can anyone suggest how I might accomplish a similar functionality in Fabric? It doesn't seem to support HTML5 Canvas Blend modes and while I see that it supports clipping paths for images, I'm not seeing how I can enable the user to interactively create a clipping path without doing lots of intersection checks to try to derive the points to create a new path on the fly.
Thanks!
Step 1: After the user has drawn a base rectangle, drag-option/alt-key enables them to draw a rectangle (the red line) which will be subtracted from the base rect.
Step 2: The base rect is shown with the subtraction.
Step 3: The base rect is used to clip or mask a section of the base image
Step 1
Step 2
Step 3
Will Tower,
There is no easy way to do it. Here are the steps:
Draw 'Yellow' rectangle
Draw 'Red' rectangle
Use clipping library like PolyBool for intersection and xor operations
Convert drawing result into the clipped path of combining rectangles
clip your image
I created some quick fiddle. You have to click on a each button to clip. It won't clip if you will not add 2 rectangles on the canvas. This is very simple example. In order to work properly you have to draw rectangles with mouse (make them dynamic). Also, this logic is not accounting for these variations (you have to work on them as well):
For these use cases Clipping Library will return to you 2 set of results, which means different logic should be implemented.
Actual code without jQuery, FabriJs, and PolyBool libraries:
var imgURL = 'http://fabricjs.com/lib/pug.jpg';
var clipYellowRect = null;
var clipRedRect = null;
var pug = null;
var canvas = new fabric.Canvas('c');
// insert image into canvas
var pugImg = new Image();
pugImg.onload = function (img) {
pug = new fabric.Image(pugImg, {
angle: 0,
width: 500,
height: 500,
left: 100,
top: 50,
scaleX: 0.5,
scaleY: 0.5,
clipName: 'pug',
});
canvas.add(pug);
};
pugImg.src = imgURL;
//draw yellow rectangle
$('#btnYellowRect').on('click', function(){
clipYellowRect = new fabric.Rect({
originX: 'left',
originY: 'top',
left: 120,
top: 60,
width: 200,
height: 200,
fill: 'rgba(255,255,0,0.5)',
strokeWidth: 0,
selectable: false
});
canvas.add(clipYellowRect);
});
//draw red rectangle
$('#btnRedRect').on('click', function(){
clipRedRect = new fabric.Rect({
originX: 'left',
originY: 'top',
left: 90,
top: 120,
width: 100,
height: 100,
strokeWidth: 3,
fill: 'transparent',
stroke: 'rgba(255,0,0,1)', /* use transparent for no fill */
strokeWidth: 0,
selectable: false
});
canvas.add(clipRedRect);
});
//clip
$('#btnClip').on('click', function(){
var yellowRectRegion = getRegion(clipYellowRect);
var redRectRegion = getRegion(clipRedRect);
//determine inersection
var intersectResult = PolyBool.intersect({
regions: [yellowRectRegion],
inverted: false
}, {
regions: [redRectRegion],
inverted: false
});
//generate clipping path
var xorResult = PolyBool.xor({
regions: [yellowRectRegion],
inverted: false
}, {
regions: intersectResult.regions,
inverted: false
});
clipImage(xorResult.regions[0]);
});
//prepare data for clipping library
function getRegion(rect){
return [[rect.left, rect.top],
[rect.left + rect.width, rect.top],
[rect.left + rect.width, rect.top + rect.height],
[rect.left, rect.top + rect.height]]
}
function clipImage(points){
//actual clipping
pug.clipTo = function (ctx) {
var scaleXTo1 = (1 / pug.scaleX);
var scaleYTo1 = (1 / pug.scaleY);
ctx.save();
var ctxLeft = -( pug.width / 2 );
var ctxTop = -( pug.height / 2 );
ctx.translate( ctxLeft, ctxTop );
ctx.scale(scaleXTo1, scaleYTo1);
ctx.beginPath();
console.log(points)
ctx.moveTo(points[0][0] - pug.oCoords.tl.x, points[0][1] - pug.oCoords.tl.y);
for (var i=1; i < points.length; i++){
ctx.lineTo(points[i][0] - pug.oCoords.tl.x, points[i][1] - pug.oCoords.tl.y);
}
ctx.closePath();
ctx.restore();
};
clipYellowRect.remove();
clipRedRect.remove();
canvas.renderAll();
}
Hopefully it will help you.

FabricJS: bug with opacity of circles in a group?

When I add a circle with an opacity lower than 1 to a group, its opacity becomes actually lower than the specified value. This does not happen if I don't specify opacity (i.e., opacity = 1). It also doesn't happen with a rectangle.
Here is the code to reproduce this issue:
HTML
<canvas id="stage" width="400" height="300">
JavaScript
var OPACITY = 0.65;
var FILL = '#fff';
var canvas = new fabric.Canvas('stage', {
backgroundColor: '#222'
});
/**
* Rectangles
* both appear to have the same color
*/
var rect1 = new fabric.Rect({
width: 40,
height: 40,
fill: FILL,
opacity: OPACITY,
left: 60,
top: 60
});
canvas.add(rect1);
var rect2 = new fabric.Rect({
width: 40,
height: 40,
fill: FILL
opacity: OPACITY,
});
var rect2Group = new fabric.Group([rect2], {
left: 120,
top: 60
});
canvas.add(rect2Group);
/**
* Circles
* the second circle is darker
*/
var circle1 = new fabric.Circle({
radius: 20,
fill: FILL,
opacity: OPACITY,
left: 60,
top: 120
});
canvas.add(circle1);
var circle2 = new fabric.Circle({
radius: 20,
fill: FILL,
opacity: OPACITY,
});
var circle2Group = new fabric.Group([circle2], {
left: 120,
top: 120
});
canvas.add(circle2Group);
Here is the JSFiddle.
If you run it, you can see that the second circle is darker than the first one, meaning that its opacity is lower.
Am I doing something wrong, or is this a bug? (Could be reproduced in 1.2.0 and 1.3.0.)
It's most likely because of this line in fabric.Circle:
// multiply by currently set alpha
// (the one that was set by path group where this object is contained, for example)
ctx.globalAlpha = this.group ? (ctx.globalAlpha * this.opacity) : this.opacity;
This has to do with circles being part of SVG group, IIRC.
In any case, it's definitely a bug — the opacity shouldn't be multiplied in your case. We need to have a better check.
Please file an issue on github.

How to calculate rotationDegree for fillPatternImage of wedges?

I have the following circle, made of wedges. Each wedge has a fillPatternImage. Each fillPatternImage has an image with a centered text. The text varies in length for each wedge.
The problem is I need to give each fillPatternImage a rotation degree so all text aligns perfectly along the circle’s perimeter. But I have no clue what rotation degree to give each fillPatternImage.
For example, when I give all fillPatternImages a rotation degree of 95°, short texts align well, but long texts turn out crooked.
I suppose rotation degree must be dynamic for each fillPatternImage. But I haven’t found out how. I have the wedge dimensions, the text width, etc.
Code example for fillPatternImage:
imageObj.src = Caption + ".png";
imageObj.onload = function () {
nts.setFillPatternImage(imageObj);
nts.setFillPatternRotationDeg(95);
// nts.setFillPatternOffset(-220, 100);
nts.setFillPatternX(radio - 15);
nts.setFillPatternY(leftSide);
nts.setFillPatternRepeat("no-repeat");
nts.draw();
}
Any ideas would be of great help. Thanks in advance.
How to calculate fillPatternRotation angle for a Kinetic Wedge
Your text will always be appropriately rotated in the Kinetic wedge with this fillPatternRotation:
fillPatternRotation:Math.PI/2+wedgeAngleDeg*Math.PI/360
Where wedgeAngleDeg is the value supplied to wedge.angleDeg:
angleDeg: wedgeAngleDeg
You also need to set the appropriate wedge fillPatternOffset to match your text-image
You must horizontally offset the fillPattern to the midpoint of the text on the image
Assuming your text is horizontally centered on your image, that offset is image.width/2.
fillPatternOffset X is image.width/2
You must vertically offset the fillPattern to align the image text to the largest width on the wedge.
You can calculate the largest wedge width like this:
var textTop=radius*Math.sin(wedgeAngleDeg*Math.PI/180);
So your fillPatternOffset becomes:
fillPatternOffset: [image.width/2, image.height-textTop],
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/Y53cH/
<!DOCTYPE HTML>
<html>
<head>
<style>
body {
margin: 0px;
padding: 0px;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v4.6.0.min.js"></script>
<script defer="defer">
var stage = new Kinetic.Stage({
container: 'container',
width: 350,
height: 350
});
var img=new Image();
img.onload=function(){
start();
}
img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/testing.png";
function start(){
var layer = new Kinetic.Layer();
var wedgeAngleDeg=60;
var radius=100;
var textTop=radius*Math.sin(wedgeAngleDeg*Math.PI/180);
var wedge = new Kinetic.Wedge({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: radius,
angleDeg: wedgeAngleDeg,
fillPatternImage: img,
fillPatternOffset: [img.width/2, img.height-textTop],
fillPatternRepeat:"no-repeat",
fillPatternRotation:Math.PI/2+wedgeAngleDeg*Math.PI/360,
stroke: 'black',
strokeWidth: 4,
rotationDeg: 0
});
layer.add(wedge);
wedge.setRotationDeg(-45);
// add the layer to the stage
stage.add(layer);
}
</script>
</body>
</html>

Categories