Can you "bend" a SVG around bezier path while animating? - animation

Is there any way to "bend" an SVG object as its being animated along a bezier path? Ive been using mostly GSAP for animating things. The effect would look something like this: https://www.behance.net/gallery/49401667/Twisted-letters-2 (the one with the blue pencil). I have managed to get the red arrow to animate along the path but the shape stays the same the whole time. Id like for it to follow along the green path and bend as it goes around the curve so that at the end of the animation it has the shape of the purple arrow. Here is the codepen.
GSAP code:
var motionPath = MorphSVGPlugin.pathDataToBezier("#motionPath", {align:"#arrow1"});
var tl1 = new TimelineMax({paused:true, reversed:true});
tl1.set("#arrow1", {xPercent:-50, yPercent:-50});
tl1.to("#arrow1", 4, {bezier:{values:motionPath, type:"cubic"}});
$("#createAnimation").click(function(){
tl1.reversed() ? tl1.play() : tl1.reverse();
});
Is there a way to do this with just GSAP? Or will I need something like Pixi?

This is how I would do it:
First I need an array of points to draw the arrow and a track. I want to move the arrow on the track, and the arrow should bend following the track. In order to achieve this effect with every frame of the animation I'm calculating the new position of the points for the arrow.
Also: the track is twice as long as it seams to be.
Please read the comments in the code
let track = document.getElementById("track");
let trackLength = track.getTotalLength();
let t = 0.1;// the position on the path. Can take values from 0 to .5
// an array of points used to draw the arrow
let points = [
[0, 0],[6.207, -2.447],[10.84, -4.997],[16.076, -7.878],[20.023, -10.05],[21.096, -4.809],[25.681, -4.468],[31.033, -4.069],[36.068, -3.695],[40.81, -3.343],[45.971, -2.96],[51.04, -2.584],[56.075, -2.21],[60.838, -1.856],[65.715, -1.49],[71.077, -1.095],[75.956, -0.733],[80, 0],[75.956, 0.733],[71.077, 1.095],[65.715, 1.49],[60.838, 1.856],[56.075, 2.21],[51.04, 2.584],[45.971, 2.96],[40.81, 3.343],[36.068, 3.695],[31.033, 4.069],[25.681, 4.468],[21.096, 4.809],[20.023, 10.05],[16.076, 7.878],[10.84, 4.997],[6.207, 2.447],[0, 0]
];
function move() {
requestAnimationFrame(move);
if (t > 0) {
t -= 0.001;
} else {
t = 0.5;
}
let ry = newPoints(track, t);
drawArrow(ry);
}
move();
function newPoints(track, t) {
// a function to change the value of every point on the points array
let ry = [];
points.map(p => {
ry.push(getPos(track, t, p[0], p[1]));
});
return ry;
}
function getPos(track, t, d, r) {
// a function to get the position of every point of the arrow on the track
let distance = d + trackLength * t;
// a point on the track
let p = track.getPointAtLength(distance);
// a point near p used to calculate the angle of rotation
let _p = track.getPointAtLength((distance + 1) % trackLength);
// the angle of rotation on the path
let a = Math.atan2(p.y - _p.y, p.x - _p.x) + Math.PI / 2;
// returns an array of coordinates: the first is the x, the second is the y
return [p.x + r * Math.cos(a), p.y + r * Math.sin(a)];
}
function drawArrow(points) {
// a function to draw the arrow in base of the points array
let d = `M${points[0][0]},${points[0][1]}L`;
points.shift();
points.map(p => {
d += `${p[0]}, ${p[1]} `;
});
d += "Z";
arrow.setAttributeNS(null, "d", d);
}
svg {
display: block;
margin: 2em auto;
border: 1px solid;
overflow: visible;
width:140vh;
}
#track {
stroke: #d9d9d9;
vector-effect: non-scaling-stroke;
}
<svg viewBox="-20 -10 440 180">
<path id="track" fill="none"
d="M200,80
C-50,280 -50,-120 200,80
C450,280 450,-120 200,80
C-50,280 -50,-120 200,80
C450,280 450,-120 200,80Z" />
<path id="arrow" d="" />
</svg>

Related

Find "curve + straight + curve" path

I'm trying to calculate a path from Start point (T-like black shape) with Start direction (green) to Finish point with finish direction in 2D space. The whole path (light blue) is a bunch of points. I need to find positions of two red points. The problem is that in most cases circle sections (I and III purple) has not equal amount of points i.e. different length. Start and finish directions can be any from 0 to 359 degrees.
To make this a programming question, here is an implementation for getting the two tangent points (the red points).
This implementation defines Vector and Circle classes, each with methods to create new results from them. The Circle class has a tangentWith method, which takes another circle as argument and returns an array with two Vectors, i.e. the coordinates of the two red points.
The snippet below is interactive. It starts with the initial two circles, as depicted in your question, but allows you to draw alternative circles using the mouse (click to determine center of circle, and drag to set its radius):
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
// Derive polar coordinates:
this.size = (x ** 2 + y ** 2) ** 0.5;
this.angle = Math.atan2(this.y, this.x);
}
subtract(v) {
return new Vector(this.x - v.x, this.y - v.y);
}
add(v) {
return new Vector(this.x + v.x, this.y + v.y);
}
multiplyBy(scalar) {
return new Vector(this.x * scalar, this.y * scalar);
}
resize(size) {
return this.multiplyBy(size / this.size);
}
rotate(angle) {
angle += this.angle;
return new Vector(this.size * Math.cos(angle), this.size * Math.sin(angle));
}
}
class Circle extends Vector {
constructor(x=0, y=0, radius=0) {
super(x, y);
this.radius = radius;
}
touch(v) { // Set radius so that v is on the circle
return Circle.fromVector(this, this.subtract(v).size);
}
tangentWith(other) { // Main algorithm
let v = this.subtract(other);
const sinus = (this.radius - other.radius) / v.size;
if (Math.abs(sinus) > 1) return []; // One circle includes the other: no tangent
v = v.rotate(Math.asin(sinus) + Math.PI / 2);
return [v.resize(this.radius).add(this), v.resize(other.radius).add(other)];
}
static fromVector(v, radius=0) {
return new Circle(v.x, v.y, radius);
}
}
// The circles as depicted in the question
const circles = [new Circle(50, 50, 25), new Circle(80, 120, 25)];
// I/O management, allowing to draw different circles
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
function mouseVector(e) {
return new Vector(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
}
canvas.addEventListener("mousedown", e => {
circles.reverse()[0] = Circle.fromVector(mouseVector(e));
});
canvas.addEventListener("mousemove", e => {
if (!e.buttons) return;
circles[0] = circles[0].touch(mouseVector(e));
draw();
});
function drawCircle(c) {
ctx.beginPath();
ctx.arc(c.x, c.y, c.radius, 0, 2*Math.PI);
ctx.stroke();
}
function drawSegment(start, end) {
if (!start) return;
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.map(drawCircle);
drawSegment(...circles[0].tangentWith(circles[1]));
}
draw();
canvas { border: 1px solid }
First draw C1, then C2 (use mouse drag)<br>
<canvas width="600" height="150"></canvas>
I drawn a simple figure:
As you see, red line directional component of the vector (C1 - C2) or (C2 - C1) equals difference of radius.
(Roughly) writing this relation as a equation,
Inner-Product( C1 -C2, U ) = dr
dr = | r1 - r2 |
where U is unit vector has direction along the red line, and 2 scalars {r1, r2} are circle radius.
This becomes to:
| C1 - C2 | * | U | * cos(theta) = | C1 - C2 | * 1 * cos(theta) = dr
where theta is the angle between (C1-C2) and U.
Now you can calculate the cos(theta) value as:
cos(theta) = dr / | C1-C2 |

Split Rectangle in Categories of sub rectangles

Given a rectangle i would like to split it in a given amount n of sub-rectangles where:
a is the amount of subrectangles which should have a defined z SMALL area
b is the amount of subrectangles which should have a defined y MEDIUM area
c is the amount of subrectangles which should have a defined x BIGGER area
may be I should be able to define d and e . But let's leave them for now.
With "defined area" i mean the geometric area should be the same but the rectangles could have different shape.
May I ask ..which algorithm would You suggest to apply in this case.
I should have a function like this PSEUDO code:
RectBoundaries[] function getRectangles(screenRectangle, amountOfRectangleCategories, amountOfRectanglesPerCategory[]) {
function1 => getRectanglesForCategory1(amountOfRectanglesPerCategory[0], screenRectangle)
function2 => getRectanglesForCategory2(amountOfRectanglesPerCategory[1], screenRectangle)
function3 => getRectanglesForCategory3(amountOfRectanglesPerCategory[2], screenRectangle)
return function1 + function2 + function3;
}
EDITED:
How to split a rectangle in multiple (defined amount of) categories of decreasing smaller rectangles.
By your question I understand you first split your main area in 3 area. One big, one medium and one small. Given this I made a small algo in Javascript, hope it solve your problem
let canvas = document.getElementById("output");
let ctx = canvas.getContext('2d');
ctx.strokeStyle = 'rgb(255, 255, 255)';
function draw(rect) {
ctx.rect(rect[0], rect[1], rect[2], rect[3]);
ctx.stroke();
}
function splitArea(rect, nbRect) {
let [nbColumns, nbLines] = getBestRatio(nbRect);
let x=rect[0];
let y=rect[1];
let width = rect[2]/nbColumns;
let height = rect[3]/nbLines;
let result = new Array();
for(let i=0; i<nbLines; i++) {
for(let j=0; j<nbColumns; j++) {
result.push([x+j*width, y+i*height, width, height]);
}
}
return result;
}
function getBestRatio(nb) {
for(let i=Math.round(Math.sqrt(nb)); i>0; i--) {
if(nb%i==0) {
return [i, nb/i];
}
}
}
//main rectangle
let rect = [10, 10, 280, 280]; //x, y, width, height
// 3 differents area
let bigArea = [rect[0], rect[1], rect[2], rect[3]*2/3]; // 2∕3 area
let mediumArea = [rect[0], rect[1]+rect[3]*2/3, rect[2]*2/3, rect[3]*1/3]; // 2/3 * 1/3 area
let smallArea = [rect[0]+rect[2]*2/3, rect[1]+rect[3]*2/3, rect[2]*1/3, rect[3]*1/3]; // 1/3 * 1/3 area
draw(bigArea);
draw(mediumArea);
draw(smallArea);
splitArea(bigArea, 6).forEach(area => draw(area));
splitArea(mediumArea, 12).forEach(area => draw(area));
splitArea(smallArea, 3).forEach(area => draw(area));
<canvas id="output" height="300px" width="300px" style="background-color: grey">

2d circle rect collision and reflection doesnt work

I have game with map built by rectangles, darker rectangles (named "closed") mean its place where balls should be able to move, ball should reflect from the lighter rectangles(named "open") border. In future I'll add more balls and they will reflect from each other.
The problem is with new Vector after collision.
I force function circleRectGetCollisionNormal() to return vector(-1,0) what i think its normal for this case (ball is moving in right direction).
Ball is starting with degrees and change it simply to vector, this reflection worked for 45 degrees but when I change angle to 10 degrees ball moved into lighter rectangles(named "open").
Here is how it looks like (Picture)
I'm doing like this:
1-check if ball collided with lighter rectangle,
2-if it collided, I want to change direction so I return vector, for example for right side of ball colliding with rectangle return [-1,0] (I think its normal of vertical line, and its pointing left direction).
3-calculate new ball move Vector from this equation: newMoveVector = oldMoveVector − (2 * dotProduct(oldMoveVector, normalVector) * normalVector)
Here is code for each step:
1.
circleRect(circlePos, circleSize, rectPos, rectSize) {
//its rectRect collision but it doesnt matter because reflection surface is always horizontal or vertical
let r1 = {
left: circlePos.x - circleSize.x/2,
right: circlePos.x + circleSize.x/2,
top: circlePos.y - circleSize.y/2,
bottom: circlePos.y + circleSize.y/2
};
let r2 = {
left: rectPos.x,
right: rectPos.x + rectSize.x,
top: rectPos.y,
bottom: rectPos.y + rectSize.y
};
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
}
isOnOpenTile(pos: Vector, size: Vector) {
let openTiles = this.getTiles('open');
let result = false;
openTiles.forEach(element => {
if( this.circleRect(pos,size,element.pos,element.size) ){
result = element;
return;
}
});
return result;
}
2.
circleRectGetCollisionNormal(c, r) {
if(c.pos.y <= r.pos.y - (r.size.y/2)) return new Vector(0,-1);
//Hit was from below the brick
if(c.pos.y >= r.pos.y + (r.size.y/2)) return new Vector(0,1);
//Hit was from above the brick
if(c.pos.x < r.pos.x) return new Vector(1,0);
//Hit was on left
if(c.pos.x > r.pos.x) return new Vector(-1,0);
//Hit was on right
return false;
}
3.
getNewMoveVector(moveVector, normalVector) {
normalVector = this.normalize(normalVector);
let dot = (moveVector.x * moveVector.y) + (normalVector.x * normalVector.y);
let dotProduct = new Vector(dot, dot);
return moveVector.sub(dotProduct.mult(normalVector).mult(new Vector(2,2)));
}
normalize(v) {
let length = Math.sqrt((v.x*v.x) + (v.y*v.y));
return new Vector(v.x/length,v.y/length);
}
And here is main function for this
getMoveVectorOnCollision(circle) {
let coll = this.isOnOpenTile( circle.pos, circle.size );
if( coll != false) {
let vector = this.circleRectGetCollisionNormal(circle, coll);
return this.getNewMoveVector(circle.moveVector, vector);
} else return false;
}
Object Vector always contain 2 values all of function (mult, sub, div, add) work like here.
sub(vector: Vector) {
return new Vector(this.x - vector.x, this.y - vector.y);
}
Please give me advice, actual solution or tell about different way to do this reflection. I wasted more than 3 days trying to solve this, I have to move on.
Yor dot product calculation is erroneous. Change these lines:
let dot = (moveVector.x * moveVector.y) + (normalVector.x * normalVector.y);
let dotProduct = new Vector(dot, dot);
by this one line:
let dotProduct = (moveVector.x * normalVector.x + moveVector.y * normalVector.y);
Note that dotProduct is scalar value, not vector, so you have to make vector for subtraction as
subvec.x = 2 * dotProduct * normalVector.x
subvec.y = 2 * dotProduct * normalVector.y
and
return moveVector.sub(subvec);

Developing an Algorithm to Transform Four Cartesian Coordinates Into Square Coordinates

I am working on a project where four randomly placed robots each have unique Cartesian coordinates. I need to find a way to transform these coordinates into the coordinates of a square with side length defined by the user of the program.
For example, let's say I have four coordinates (5,13), (8,17), (13,2), and (6,24) that represent the coordinates of four robots. I need to find a square's coordinates such that the four robots are closest to these coordinates.
Thanks in advance.
As far as I understand your question you are looking for the centroid of the four points, the point which has equal — and thus minimal — distance to all points. It is calculated as the average for each coordinate:
The square's edge length is irrelevant to the position, though.
Update
If you additionally want to minimize the square corners' distance to a robot position, you can do the following:
Calculate the centroid c like described above and place the square there.
Imagine a circle with center at c and diameter of the square's edge length.
For each robot position calculate the point on the circle with shortest distance to the robot and use that as a corner of the square.
It looks as if the original poster is not coming back to share his solution here, so I'll post what I was working on.
Finding the center point of the four robots and then drawing the square around this point is indeed a good way to start, but it doesn't necessarily give the optimal result. For the example given in the question, the center point is (8,14) and the total distance is 22.688 (assuming a square size of 10).
When you draw the vector from a corner of the square to the closest robot, this vector shows you in which direction the square should move to reduce the distance from that corner to its closest robot. If you calculate the sum of the direction of these four vectors (by changing the vectors to size 1 before adding them up) then moving the square in the resulting direction will reduce the total distance.
I dreaded venturing into differential equation territory here, so I devised a simple algorithm which repeatedly calculates the direction to move in, and moves the square in ever decreasing steps, until a certain precision is reached.
For the example in the question, the optimal location it finds is (10,18), and the total distance is 21.814, which is an improvement of 0.874 over the center position (assuming a square size of 10).
Press "run code snippet" to see the algorithm in action with randomly generated positions. The scattered green dots are the center points that are considered while searching the optimal location for the square.
function positionSquare(points, size) {
var center = {x: 0, y:0};
for (var i in points) {
center.x += points[i].x / points.length;
center.y += points[i].y / points.length;
}
paintSquare(canvas, square(center), 1, "#D0D0D0");
order(points);
textOutput("<P>center position: " + center.x.toFixed(3) + "," + center.y.toFixed(3) + "<BR>total distance: " + distance(center, points).toFixed(3) + "</P>");
for (var step = 1; step > 0.0001; step /= 2)
{
var point = center;
var shortest, dist = distance(center, points);
do
{
center = point;
shortest = dist;
var dir = direction();
paintDot(canvas, center.x, center.y, 1, "green");
point.x = center.x + Math.cos(dir) * step;
point.y = center.y + Math.sin(dir) * step;
dist = distance(point, points);
}
while (dist < shortest)
}
textOutput("<P>optimal position: " + center.x.toFixed(3) + "," + center.y.toFixed(3) + "<BR>total distance: " + distance(point, points).toFixed(3) + "</P>");
return square(center);
function order(points) {
var clone = [], best = 0;
for (var i = 0; i < 2; i++) {
clone[i] = points.slice();
for (var j in clone[i]) clone[i][j].n = j;
if (i) {
clone[i].sort(function(a, b) {return b.y - a.y});
if (clone[i][0].x > clone[i][1].x) swap(clone[i], 0, 1);
if (clone[i][2].x < clone[i][3].x) swap(clone[i], 2, 3);
} else {
clone[i].sort(function(a, b) {return a.x - b.x});
swap(clone[i], 1, 3);
if (clone[i][0].y < clone[i][3].y) swap(clone[i], 0, 3);
if (clone[i][1].y < clone[i][2].y) swap(clone[i], 1, 2);
}
}
if (distance(center, clone[0]) > distance(center, clone[1])) best = 1;
for (var i in points) points[i] = {x: clone[best][i].x, y: clone[best][i].y};
function swap(a, i, j) {
var temp = a[i]; a[i] = a[j]; a[j] = temp;
}
}
function direction() {
var d, dx = 0, dy = 0, corners = square(center);
for (var i in points) {
d = Math.atan2(points[i].y - corners[i].y, points[i].x - corners[i].x);
dx += Math.cos(d);
dy += Math.sin(d);
}
return Math.atan2(dy, dx);
}
function distance(center, points) {
var d = 0, corners = square(center);
for (var i in points) {
var dx = points[i].x - corners[i].x;
var dy = points[i].y - corners[i].y;
d += Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
}
return d;
}
function square(center) {
return [{x: center.x - size / 2, y: center.y + size / 2},
{x: center.x + size / 2, y: center.y + size / 2},
{x: center.x + size / 2, y: center.y - size / 2},
{x: center.x - size / 2, y: center.y - size / 2}];
}
}
// PREPARE CANVAS
var canvas = document.getElementById("canvas");
canvas.width = 200; canvas.height = 200;
canvas = canvas.getContext("2d");
// GENERATE TEST DATA AND RUN FUNCTION
var points = [{x:5, y:13}, {x:8, y:17}, {x:13, y:2}, {x:6, y:24}];
for (var i = 0; i < 4; i++) {
points[i].x = 1 + 23 * Math.random(); points[i].y = 1 + 23 * Math.random();
}
for (var i in points) textOutput("point: " + points[i].x.toFixed(3) + "," + points[i].y.toFixed(3) + "<BR>");
var size = 10;
var square = positionSquare(points, size);
// SHOW RESULT ON CANVAS
for (var i in points) {
paintDot(canvas, points[i].x, points[i].y, 5, "red");
paintLine(canvas, points[i].x, points[i].y, square[i].x, square[i].y, 1, "blue");
}
paintSquare(canvas, square, 1, "green");
function paintDot(canvas, x, y, size, color) {
canvas.beginPath();
canvas.arc(8 * x, 200 - 8 * y, size, 0, 6.2831853);
canvas.closePath();
canvas.fillStyle = color;
canvas.fill();
}
function paintLine(canvas, x1, y1, x2, y2, width, color) {
canvas.beginPath();
canvas.moveTo(8 * x1, 200 - 8 * y1);
canvas.lineTo(8 * x2, 200 - 8 * y2);
canvas.strokeStyle = color;
canvas.stroke();
}
function paintSquare(canvas, square, width, color) {
canvas.rect(8 * square[0].x , 200 - 8 * square[0].y, 8 * size, 8 * size);
canvas.strokeStyle = color;
canvas.stroke();
}
// TEXT OUTPUT
function textOutput(t) {
var output = document.getElementById("output");
output.innerHTML += t;
}
<BODY STYLE="margin: 0; border: 0; padding: 0;">
<CANVAS ID="canvas" STYLE="width: 200px; height: 200px; float: left; background-color: #F8F8F8;"></CANVAS>
<DIV ID="output" STYLE="width: 400px; height: 200px; float: left; margin-left: 10px;"></DIV>
</BODY>
Further improvements: I haven't yet taken into account what happens when a corner and a robot are in the same spot, but the overall position isn't optimal. Since the direction from the corner to the robot is undefined, it should probably be taken out of the equation temporarily.

CSS3 animation on transform: rotate. Way to fetch current deg of the rotating element?

i am working on a html5 interface wich uses drag and drop. While i am dragging an element, the target gets a css-class, which makes it bidirectionally rotate due to a -webkit-animation.
#-webkit-keyframes pulse {
0% { -webkit-transform: rotate(0deg); }
25% { -webkit-transform:rotate(-10deg); }
75% { -webkit-transform: rotate(10deg); }
100% { -webkit-transform: rotate(0deg); }
}
.drag
{
-webkit-animation-name: pulse;
-webkit-animation-duration: 1s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: ease-in-out;
}
When I drop the target, I want it to adopt the current state of rotation.
My first thought was to check the css property with jquery and the .css('-webkit-transform') method. But this method just returns 'none'.
So my question: Is there a way to get the current degree value of an element which is rotated via animation?
Thanks so far
Hendrik
I recently had to write a function that does exactly what you want! Feel free to use it:
// Parameter element should be a DOM Element object.
// Returns the rotation of the element in degrees.
function getRotationDegrees(element) {
// get the computed style object for the element
var style = window.getComputedStyle(element);
// this string will be in the form 'matrix(a, b, c, d, tx, ty)'
var transformString = style['-webkit-transform']
|| style['-moz-transform']
|| style['transform'] ;
if (!transformString || transformString == 'none')
return 0;
var splits = transformString.split(',');
// parse the string to get a and b
var parenLoc = splits[0].indexOf('(');
var a = parseFloat(splits[0].substr(parenLoc+1));
var b = parseFloat(splits[1]);
// doing atan2 on b, a will give you the angle in radians
var rad = Math.atan2(b, a);
var deg = 180 * rad / Math.PI;
// instead of having values from -180 to 180, get 0 to 360
if (deg < 0) deg += 360;
return deg;
}
Hope this helps!
EDIT I updated the code to work with matrix3d strings, but it still only gives the 2d rotation degrees (ie. rotation around the Z axis).
window.getComputedStyle(element,null)['-webkit-transform'] will return a string representing current transformation matrix.
You can calculate its rotation degree from that string by first parsing it, then applying a few maths on it.

Resources