viewBox resizing changes inner attributes - javascript

Via this forum and other sources I finally have a tidy "zoom-and-recenter" Javascript solution that fits my specific needs and requirements:
<script>
var viewPortSize = 500; // fixed for now
var viewBoxSize = 500; // the zoom factor
function updateViewBox()
{
min = (viewPortSize - viewBoxSize) / 2;
max = viewBoxSize;
str = " ";
str = str.concat( "", min );
str = str.concat( " ", min );
str = str.concat( " ", max );
str = str.concat( " ", max );
trackBox = document.getElementById("trackBox");
trackBox.setAttribute( "viewBox", str );
}
function zoomIn()
{
viewBoxSize -= 50;
updateViewBox();
}
function zoomOut()
{
viewBoxSize += 50;
updateViewBox();
}
</script>
The target of this operation is:
<svg id="trackBox" x="600"
width="500" height="500"
viewBox="0 0 500 500">
<g>
<circle id="trackPoint"
cx="250" cy="250" r="5" fill="red"/>
<path id="track" d=
"
M 250 250
L 245 225
L 200 225
L 200 250
"
stroke="blue" stroke-width="1" fill="transparent"/>
</g>
</svg>
However, when applying the updateViewBox() function, the circle radius and the path stroke-width are also magnified or reduced.
Does there exist a simple way to make these attributes invariant with respect to changing the viewBox size?

Related

Fit SVG transformed element into the rect bounds with JavaScript

I am struggling with an issue to fit pragmatically transformed SVG element into the given rect bounds.
Destination rect is given and not transformed.
Input rect has any type of transformations.
Input rect can be a child of any transformed groups.
Transformations should be applied only to the input rect.
This question is only about the JavaScript element transformations.
It's an easy task when the element has only transformations by itself:
In this case proportion between the destination and input getBoundingClientRect (bounding rect in screen coordinates) is equals to a proper scaling factor.
But it's not working when parent elements are also transformed:
var inputElement = document.getElementById("input");
var destinationElement = document.getElementById("destination");
var inputBB = inputElement.getBoundingClientRect();
var outputBB = destinationElement.getBoundingClientRect();
var scaleX = outputBB.width / inputBB.width;
var scaleY = outputBB.height / inputBB.height;
// get offsets between figure center and destination rect center:
var offsetX = outputBB.x + outputBB.width / 2 - (inputBB.x + inputBB.width / 2);
var offsetY =
outputBB.y + outputBB.height / 2 - (inputBB.y + inputBB.height / 2);
// get current figure transformation
let currentMatrix = (
inputElement.transform.baseVal.consolidate() ||
inputElement.ownerSVGElement.createSVGTransform()
).matrix;
// Get center of figure in element coordinates:
const inputBBox = inputElement.getBBox();
const centerTransform = inputElement.ownerSVGElement.createSVGPoint();
centerTransform.x = inputBBox.x + inputBBox.width / 2;
centerTransform.y = inputBBox.y + inputBBox.height / 2;
// create scale matrix:
const svgTransform = inputElement.ownerSVGElement.createSVGTransform();
svgTransform.setScale(scaleX, scaleY);
let scalingMatrix = inputElement.ownerSVGElement
.createSVGMatrix()
// move the figure to the center of the destination rect.
.translate(offsetX, offsetY)
// Apply current matrix, so old transformations are not lost
.multiply(currentMatrix)
.translate(centerTransform.x, centerTransform.y)
// multiply is used instead of the scale method while for some reasons matrix scale is giving proportional scaling...
// From a transforms proper matrix is generated.
.multiply(svgTransform.matrix)
.translate(-centerTransform.x, -centerTransform.y);
// Apply new created matrix to element back:
const newTransform = inputElement.ownerSVGElement.createSVGTransform();
newTransform.setMatrix(scalingMatrix);
inputElement.transform.baseVal.initialize(newTransform);
var bboundsTest= document.getElementById("bboundsTest");
const resultBBounds = inputElement.getBoundingClientRect();
bboundsTest.setAttribute('x', resultBBounds .x);
bboundsTest.setAttribute('y', resultBBounds .y);
bboundsTest.setAttribute('width', resultBBounds .width);
bboundsTest.setAttribute('height', resultBBounds .height);
document.getElementById('test2').innerHTML = 'expected: 100x100 . Results: ' + resultBBounds.width + 'x' + resultBBounds.height
<svg
version="1.2"
viewBox="0 0 480 150"
width="480"
height="150"
xmlns="http://www.w3.org/2000/svg"
>
<g transform="skewX(10) translate(95,1) rotate(30)">
<g transform="skewX(30) translate(-3,3) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<rect
id="input"
transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
width="30"
height="30"
fill="red"
/>
</g>
</g>
</g>
<rect
id="destination"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="blue"
/>
<rect
id="bboundsTest"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="black"
/>
</svg>
<div id="test2"></div>
Any ideas on how to take parent transformations into the count to find proper scaling factors?
Thanks in advance for the ideas!
The given answer from Dipen Shah is focused on applying transformations to the parent element and this is also an option, but my goal is transforming the element to the destination rect bounds.
As you have discovered, this is a tricky problem. It's even trickier than you think (see later).
You have rectangles in two different corrdinate spaces. One of them is transformed. So you are trying to map one transformed rectangle to another, possibly transformed, rectangle. Since they are transformed, one or both of those rectangles is (probably) no longer a rectangle.
Since your requirement is to transform the "input" to the "destination", the way to get your head around the problem is to switch your coordinate space to the point of view of the "input" rect. What does the "destination" look like from the point of view of "input"? To see, we need to transform "destination" with the inverse of the transform that "input" has.
What the destination looks like to the <rect id="input" transform=""/>
<svg
version="1.2"
viewBox="-50 -50 160 260"
height="500"
xmlns="http://www.w3.org/2000/svg"
>
<rect
id="input"
transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
width="30"
height="30"
fill="red"
/>
<g transform="rotate(-10) translate(3,-4) skewX(-10)">
<g transform="rotate(-30) translate(3,-3) skewX(-30)">
<g transform="rotate(-30) translate(-95,-1) skewX(-10)">
<rect
id="destination"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="blue"
/>
</g>
</g>
</g>
What the destination looks like to the <rect id="input"/>
<svg
version="1.2"
viewBox="-80 -70 120 230"
height="500"
xmlns="http://www.w3.org/2000/svg"
>
<rect
id="input"
width="30"
height="30"
fill="red"
/>
<g transform="rotate(-45) translate(0,0) translate(50,50) scale(0.67) translate(-50,-50) skewX(-25) translate(-95,-76.5)">
<g transform="rotate(-10) translate(3,-4) skewX(-10)">
<g transform="rotate(-30) translate(3,-3) skewX(-30)">
<g transform="rotate(-30) translate(-95,-1) skewX(-10)">
<rect
id="destination"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="blue"
/>
</g>
</g>
</g>
</g>
So, you can see why it's so tricky now. We either have to find the transform that maps a parallelogram to another parallelogram, or a rectangle to a parallelogram. Obviously we'll want to choose the latter. You'd expect it to be the simpler of the two options.
We are also helped because we can assume that the transformations are affine. Straight lines stay straight, and parallel lines stay parallel.
So our task is to scale up our rectangle, so that it neatly fits inside our destination parallelogram. Also, because the parallelogram has 180° rotational symmetry, we know that the centre of our fitted rectangle will coincide with the centre of the parallelogram.
So, let's imagine the "input" rectangle is sitting at the centre of the "destination" parallelogram, then shoot imaginary rays out of the rectangle until they hit the sides of the parallelogram. Whichever ray hits the destination parallelogram first, gives us the scale we should apply to the rectangle to make it fit.
.ray {
stroke: lightgrey;
stroke-dasharray: 2 2;
}
<svg
version="1.2"
viewBox="0 0 120 230"
height="500"
xmlns="http://www.w3.org/2000/svg"
>
<g transform="translate(47.1,101.2)"><!-- positioning conveniently for our figure -->
<!-- scaling rays -->
<line class="ray" x1="-100" y1="0" x2="100" y2="0"/>
<line class="ray" x1="-100" y1="30" x2="100" y2="30"/>
<line class="ray" x1="0" y1="-100" x2="0" y2="100"/>
<line class="ray" x1="30" y1="-100" x2="30" y2="100"/>
<rect
id="input"
width="30"
height="30"
fill="red"
/>
</g>
<g transform="translate(80,70)"><!-- positioning conveniently for our figure -->
<g transform="rotate(-45) translate(0,0) translate(50,50) scale(0.67) translate(-50,-50) skewX(-25) translate(-95,-76.5)">
<g transform="rotate(-10) translate(3,-4) skewX(-10)">
<g transform="rotate(-30) translate(3,-3) skewX(-30)">
<g transform="rotate(-30) translate(-95,-1) skewX(-10)">
<rect
id="destination"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="blue"
/>
</g>
</g>
</g>
</g>
</g>
var inputElement = document.getElementById("input");
var destinationElement = document.getElementById("destination");
var svg = inputElement.ownerSVGElement;
// Get the four corner points of rect "input"
var inX = inputElement.x.baseVal.value;
var inY = inputElement.y.baseVal.value;
var inW = inputElement.width.baseVal.value;
var inH = inputElement.height.baseVal.value;
// Get the four corner points of rect "destination"
var destX = destinationElement.x.baseVal.value;
var destY = destinationElement.y.baseVal.value;
var destW = destinationElement.width.baseVal.value;
var destH = destinationElement.height.baseVal.value;
var destPoints = [
createPoint(svg, destX, destY),
createPoint(svg, destX + destW, destY),
createPoint(svg, destX + destW, destY + destH),
createPoint(svg, destX, destY + destH)
];
// Get total transform applied to input rect
var el = inputElement;
var totalMatrix = el.transform.baseVal.consolidate().matrix;
// Step up ancestor tree till we get to the element before the root SVG element
while (el.parentElement.ownerSVGElement != null) {
el = el.parentElement;
if (el.transform) {
totalMatrix = el.transform.baseVal.consolidate().matrix.multiply( totalMatrix );
}
}
//console.log("totalMatrix = ",totalMatrix);
// Transform the four "destination" rect corner points by the inverse of the totalMatrix
// We will then have the corner points in the same coordinate space as the "input" rect
for (var i=0; i<4; i++) {
destPoints[i] = destPoints[i].matrixTransform(totalMatrix.inverse());
}
//console.log("transformed destPoints=",destPoints);
// Find the equation for the rays that start at the centre of the "input" rect & "destination" parallelogram
// and pass through the corner points of the "input" rect.
var destMinX = Math.min(destPoints[0].x, destPoints[1].x, destPoints[2].x, destPoints[3].x);
var destMaxX = Math.max(destPoints[0].x, destPoints[1].x, destPoints[2].x, destPoints[3].x);
var destMinY = Math.min(destPoints[0].y, destPoints[1].y, destPoints[2].y, destPoints[3].y);
var destMaxY = Math.max(destPoints[0].y, destPoints[1].y, destPoints[2].y, destPoints[3].y);
var destCentreX = (destMinX + destMaxX) / 2;
var destCentreY = (destMinY + destMaxY) / 2;
// Find the scale in the X direction by shooting rays horizontally from the top and bottom of the "input" rect
var scale1 = findDistanceToDestination(destCentreX, destCentreY - inH/2, inW/2, 0, // line equation of ray line 1
destPoints);
var scale2 = findDistanceToDestination(destCentreX, destCentreY + inH/2, inW/2, 0, // line equation of ray line 2
destPoints);
var scaleX = Math.min(scale1, scale2);
// Find the scale in the Y direction by shooting rays vertically from the left and right of the "input" rect
scale1 = findDistanceToDestination(destCentreX - inW/2, destCentreY, 0, inH/2, // line equation of ray line 1
destPoints);
scale2 = findDistanceToDestination(destCentreX + inW/2, destCentreY, 0, inH/2, // line equation of ray line 2
destPoints);
var scaleY = Math.min(scale1, scale2);
// Now we can position and scale the "input" element to fit the "destination" rect
inputElement.transform.baseVal.appendItem( makeTranslate(svg, destCentreX, destCentreY));
inputElement.transform.baseVal.appendItem( makeScale(svg, scaleX, scaleY));
inputElement.transform.baseVal.appendItem( makeTranslate(svg, -(inX + inW)/2, -(inY + inH)/2));
function createPoint(svg, x, y)
{
var pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
return pt;
}
function makeTranslate(svg, x, y)
{
var t = svg.createSVGTransform();
t.setTranslate(x, y);
return t;
}
function makeScale(svg, sx, sy)
{
var t = svg.createSVGTransform();
t.setScale(sx, sy);
return t;
}
function findDistanceToDestination(centreX, centreY, rayX, rayY, // line equation of ray
destPoints) // parallelogram points
{
// Test ray against each side of the dest parallelogram
for (var i=0; i<4; i++) {
var from = destPoints[i];
var to = destPoints[(i + 1) % 4];
var dx = to.x - from.x;
var dy = to.y - from.y;
var k = intersection(centreX, centreY, rayX, rayY, // line equation of ray
from.x, from.y, dx, dy); // line equation of parallogram side
if (k >= 0 && k <= 1) {
// Ray intersected with this side
var interceptX = from.x + k * dx;
var interceptY = from.y + k * dy;
var distanceX = interceptX - centreX;
var distanceY = interceptY - centreY;
if (rayX != 0)
return Math.abs(distanceX / rayX);
else if (rayY != 0)
return Math.abs(distanceY / rayY);
else
return 0; // How to handle case where "input" rect has zero width or height?
}
}
throw 'Should have intersected one of the sides!'; // Shouldn't happen
}
// Returns the position along the 'side' line, that the ray hits.
// If it intersects the line, thre return value will be between 0 and 1.
function intersection(rayX, rayY, rayDX, rayDY,
sideX, sideY, sideDX, sideDY)
{
// We want to find where:
// rayXY + t * rayDXDY = sideXY + k * sideDXDY
// Returning k.
// See: https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
var den = -rayDX * -sideDY - -rayDY * -sideDX;
return (den != 0) ? - (-rayDX * (rayY-sideY) - -rayDY * (rayX-sideX)) / den
: -9999; // Lines don't intersect. Return a value outside range 0..1.
}
<svg
version="1.2"
viewBox="0 0 480 150"
width="480"
height="150"
xmlns="http://www.w3.org/2000/svg"
>
<g transform="skewX(10) translate(95,1) rotate(30)">
<g transform="skewX(30) translate(-3,3) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<rect
id="input"
transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
width="30"
height="30"
fill="red"
/>
</g>
</g>
</g>
<rect
id="destination"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="blue"
/>
</svg>
<div id="test2"></div>
We got close, but we're a little oversized. What happened?
If we go back to looking at it in "input" rect space, like before, we can see the problem better.
<svg width="500" height="500" viewBox="-40 -40 50 180">
<polygon points="-38.5008, 79.5321,
-32.7704, -35.2044,
3.5896, 12.3685,
-2.1406, 127.1050"
fill="none"
stroke="blue"
stroke-width="0.5"/>
<!-- input -->
<rect x="-32.4555" y="30.9503" width="30" height="30"
fill="red"/>
<!-- centre of dest -->
<circle cx="-17.4555" cy="45.9503" r="1"/>
<!-- intercepts X -->
<circle cx="-36.0744" cy="30.9503" r="1" fill="green"/>
<circle cx="-37.5727" cy="60.9503" r="1" fill="green"/>
<!-- intercepts Y -->
<circle cx="-32.4555" cy="-34.7923" r="1" fill="green"/>
<circle cx="-2.4555" cy="4.4590" r="1" fill="green"/>
<!-- scaled input -->
<rect x="-32.4555" y="30.9503" width="30" height="30"
fill="red" fill-opacity="0.2"
transform="translate(-17.4556 45.9503) scale(1.24126 2.76608) translate(17.4556 -45.9503)"/>
</svg>
The green dots represent the intersection points we got from shooting the rays horizontally and vertically from our "input" rectangle. The faded red rectangle represents the "input" rectangle scaled up to touch our intercept points. It overflows our "destination" shape. Which is why our shape from the previous snippet overflows, also.
This is what I meant, at the very top, when I said it is trickier than you think. To make the "input" match the "destination", you have to tweak two inter-dependent X and Y scales. If you adjust the X scale to fit, it'll no long fit in the Y direction. And vice versa.
This is as far as I want to go. I've spent a couple of hours on this answer already.
Perhaps their's a mathematical solution for finding a rectangle that fits inside a
parallelogram and touches all four sides. But I don't really want to spend the time to
work it out. Sorry. :)
Perhaps you or someone else can take this further. You could also try an iterative
solution that nudges the X and Y scales iteratively until it gets close enough.
Finally, if you are prepared to accept the condition that you don't stretch the input both horizontally and vertically, and if you are okay with just scaling up (or down) the input to fit (ie keeping the aspect ratio the same), then that's a simpler thing to solve.
var inputElement = document.getElementById("input");
var destinationElement = document.getElementById("destination");
var svg = inputElement.ownerSVGElement;
// Get the four corner points of rect "input"
var inX = inputElement.x.baseVal.value;
var inY = inputElement.y.baseVal.value;
var inW = inputElement.width.baseVal.value;
var inH = inputElement.height.baseVal.value;
// Get the four corner points of rect "destination"
var destX = destinationElement.x.baseVal.value;
var destY = destinationElement.y.baseVal.value;
var destW = destinationElement.width.baseVal.value;
var destH = destinationElement.height.baseVal.value;
var destPoints = [
createPoint(svg, destX, destY),
createPoint(svg, destX + destW, destY),
createPoint(svg, destX + destW, destY + destH),
createPoint(svg, destX, destY + destH)
];
// Get total transform applied to input rect
var el = inputElement;
var totalMatrix = el.transform.baseVal.consolidate().matrix;
// Step up ancestor tree till we get to the element before the root SVG element
while (el.parentElement.ownerSVGElement != null) {
el = el.parentElement;
if (el.transform) {
totalMatrix = el.transform.baseVal.consolidate().matrix.multiply( totalMatrix );
}
}
//console.log("totalMatrix = ",totalMatrix);
// Transform the four "destination" rect corner points by the inverse of the totalMatrix
// We will then have the corner points in the same coordinate space as the "input" rect
for (var i=0; i<4; i++) {
destPoints[i] = destPoints[i].matrixTransform(totalMatrix.inverse());
}
//console.log("transformed destPoints=",destPoints);
// Find the equation for the rays that start at the centre of the "input" rect & "destination" parallelogram
// and pass through the corner points of the "input" rect.
var destMinX = Math.min(destPoints[0].x, destPoints[1].x, destPoints[2].x, destPoints[3].x);
var destMaxX = Math.max(destPoints[0].x, destPoints[1].x, destPoints[2].x, destPoints[3].x);
var destMinY = Math.min(destPoints[0].y, destPoints[1].y, destPoints[2].y, destPoints[3].y);
var destMaxY = Math.max(destPoints[0].y, destPoints[1].y, destPoints[2].y, destPoints[3].y);
var destCentreX = (destMinX + destMaxX) / 2;
var destCentreY = (destMinY + destMaxY) / 2;
// Shoot diagonal rays from the centre through two adjacent corners of the "input" rect.
// Whichever one hits the destination shape first, provides the scaling factor we need.
var scale1 = findDistanceToDestination(destCentreX, destCentreY, inW/2, inH/2, // line equation of ray line 1
destPoints);
var scale2 = findDistanceToDestination(destCentreX, destCentreY, -inW/2, inW/2, // line equation of ray line 2
destPoints);
var scale = Math.min(scale1, scale2);
// Now we can position and scale the "input" element to fit the "destination" rect
inputElement.transform.baseVal.appendItem( makeTranslate(svg, destCentreX, destCentreY));
inputElement.transform.baseVal.appendItem( makeScale(svg, scale, scale));
inputElement.transform.baseVal.appendItem( makeTranslate(svg, -(inX + inW)/2, -(inY + inH)/2));
function createPoint(svg, x, y)
{
var pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
return pt;
}
function makeTranslate(svg, x, y)
{
var t = svg.createSVGTransform();
t.setTranslate(x, y);
return t;
}
function makeScale(svg, sx, sy)
{
var t = svg.createSVGTransform();
t.setScale(sx, sy);
return t;
}
function findDistanceToDestination(centreX, centreY, rayX, rayY, // line equation of ray
destPoints) // parallelogram points
{
// Test ray against each side of the dest parallelogram
for (var i=0; i<4; i++) {
var from = destPoints[i];
var to = destPoints[(i + 1) % 4];
var dx = to.x - from.x;
var dy = to.y - from.y;
var k = intersection(centreX, centreY, rayX, rayY, // line equation of ray
from.x, from.y, dx, dy); // line equation of parallogram side
if (k >= 0 && k <= 1) {
// Ray intersected with this side
var interceptX = from.x + k * dx;
var interceptY = from.y + k * dy;
var distanceX = interceptX - centreX;
var distanceY = interceptY - centreY;
if (rayX != 0)
return Math.abs(distanceX / rayX);
else if (rayY != 0)
return Math.abs(distanceY / rayY);
else
return 0; // How to handle case where "input" rect has zero width or height?
}
}
throw 'Should have intersected one of the sides!'; // Shouldn't happen
}
// Returns the position along the 'side' line, that the ray hits.
// If it intersects the line, thre return value will be between 0 and 1.
function intersection(rayX, rayY, rayDX, rayDY,
sideX, sideY, sideDX, sideDY)
{
// We want to find where:
// rayXY + t * rayDXDY = sideXY + k * sideDXDY
// Returning k.
// See: https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
var den = -rayDX * -sideDY - -rayDY * -sideDX;
return (den != 0) ? - (-rayDX * (rayY-sideY) - -rayDY * (rayX-sideX)) / den
: -9999; // Lines don't intersect. Return a value outside range 0..1.
}
<svg
version="1.2"
viewBox="0 0 480 150"
width="480"
height="150"
xmlns="http://www.w3.org/2000/svg"
>
<g transform="skewX(10) translate(95,1) rotate(30)">
<g transform="skewX(30) translate(-3,3) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<rect
id="input"
transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
width="30"
height="30"
fill="red"
/>
</g>
</g>
</g>
<rect
id="destination"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="blue"
/>
</svg>
<div id="test2"></div>
Update:
I was able to fit source element to match target element. The way I was able to achieve that is by translating top most container of the source element relative to target element and scaling container based on size ratio between source and target elements.
function applyTransformations(source, sourceContainer, target, includeMagicScaleMargin) {
var sourceBB = source.getBoundingClientRect();
var inputBB = sourceContainer.getBoundingClientRect();
var outputBB = target.getBoundingClientRect();
var scaleX = (outputBB.width - (includeMagicScaleMargin ? 10 : 0)) / sourceBB.width;
var scaleY = (outputBB.height - (includeMagicScaleMargin ? 10 : 0)) / sourceBB.height;
// get offsets between figure center and destination rect center:
var offsetX = outputBB.x + outputBB.width / 2 - (inputBB.x + inputBB.width / 2);
var offsetY =
outputBB.y + outputBB.height / 2 - (inputBB.y + inputBB.height / 2);
// get current figure transformation
let currentMatrix = (
sourceContainer.transform.baseVal.consolidate() ||
sourceContainer.ownerSVGElement.createSVGTransform()
).matrix;
// Get center of figure in element coordinates:
const inputBBox = sourceContainer.getBBox();
const centerTransform = sourceContainer.ownerSVGElement.createSVGPoint();
centerTransform.x = inputBBox.x + inputBBox.width / 2;
centerTransform.y = inputBBox.y + inputBBox.height / 2;
// create scale matrix:
const svgTransform = sourceContainer.ownerSVGElement.createSVGTransform();
svgTransform.setScale(scaleX, scaleY);
let scalingMatrix = sourceContainer.ownerSVGElement
.createSVGMatrix()
// move the figure to the center of the destination rect.
.translate(offsetX, offsetY)
// Apply current matrix, so old transformations are not lost
.multiply(currentMatrix)
.translate(centerTransform.x, centerTransform.y)
// multiply is used instead of the scale method while for some reasons matrix scale is giving proportional scaling...
// From a transforms proper matrix is generated.
.multiply(svgTransform.matrix)
.translate(-centerTransform.x, -centerTransform.y);
// Apply new created matrix to element back:
const newTransform = sourceContainer.ownerSVGElement.createSVGTransform();
newTransform.setMatrix(scalingMatrix);
sourceContainer.transform.baseVal.initialize(newTransform);
}
function isDescendant(parent, child) {
var node = child.parentNode;
while (node != null) {
if (node == parent) {
return true;
}
node = node.parentNode;
}
return false;
}
function transformSVG1() {
var destinationElem = document.getElementById("destination");
var inputElem = document.getElementById("input");
var inputContainerElem = inputElem;
while (inputContainerElem.parentNode != null) {
let candidateParent = inputContainerElem.parentNode;
if (isDescendant(candidateParent, destinationElem)) {
break;
}
inputContainerElem = candidateParent;
}
applyTransformations(inputElem, inputContainerElem, destinationElem);
}
function transformSVG2() {
var destinationElem = document.getElementById("destination2");
var inputElem = document.getElementById("input2");
var inputContainerElem = inputElem;
while (inputContainerElem.parentNode != null) {
let candidateParent = inputContainerElem.parentNode;
if (isDescendant(candidateParent, destinationElem)) {
break;
}
inputContainerElem = candidateParent;
}
applyTransformations(inputElem, inputContainerElem, destinationElem, true);
}
transformSVG1();
transformSVG2();
<svg version="1.2" viewBox="0 0 480 200" width="480" height="200" xmlns="http://www.w3.org/2000/svg">
<g>
<text x="0" y="20" font-size="20">No magic margins</text>
<g transform="skewX(10) translate(95,1) rotate(30)">
<g transform="skewX(30) translate(-3,3) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<rect id="input" transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)" width="30" height="30" fill="red" />
</g>
</g>
</g>
<rect id="destination" x="40" y="40" width="100" height="100" fill="transparent" stroke="blue" />
</g>
</svg>
<svg version="1.2" viewBox="0 0 480 200" width="480" height="200" xmlns="http://www.w3.org/2000/svg">
<g>
<text x="0" y="20" font-size="20">Magic margins!</text>
<g transform="skewX(10) translate(95,1) rotate(30)">
<g transform="skewX(30) translate(-3,3) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<rect id="input2" transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)" width="30" height="30" fill="red" />
</g>
</g>
</g>
<rect id="destination2" x="40" y="40" width="100" height="100" fill="transparent" stroke="blue" />
</g>
</svg>
Original answer:
I don't think this is an exact answer to what you are looking for but easier thing to do would be either:
Approach 1:
keep on applying same transformation as input element and its parent until common parent node is found.
function applyTransformations(source, target) {
var inputBB = source.getBoundingClientRect();
var outputBB = target.getBoundingClientRect();
var scaleX = outputBB.width / inputBB.width;
var scaleY = outputBB.height / inputBB.height;
// get offsets between figure center and destination rect center:
var offsetX = outputBB.x + outputBB.width / 2 - (inputBB.x + inputBB.width / 2);
var offsetY =
outputBB.y + outputBB.height / 2 - (inputBB.y + inputBB.height / 2);
// get current figure transformation
let currentMatrix = (
source.transform.baseVal.consolidate() ||
source.ownerSVGElement.createSVGTransform()
).matrix;
// Get center of figure in element coordinates:
const inputBBox = source.getBBox();
const centerTransform = source.ownerSVGElement.createSVGPoint();
centerTransform.x = inputBBox.x + inputBBox.width / 2;
centerTransform.y = inputBBox.y + inputBBox.height / 2;
// create scale matrix:
const svgTransform = source.ownerSVGElement.createSVGTransform();
svgTransform.setScale(scaleX, scaleY);
let scalingMatrix = source.ownerSVGElement
.createSVGMatrix()
// move the figure to the center of the destination rect.
.translate(offsetX, offsetY)
// Apply current matrix, so old transformations are not lost
.multiply(currentMatrix)
.translate(centerTransform.x, centerTransform.y)
// multiply is used instead of the scale method while for some reasons matrix scale is giving proportional scaling...
// From a transforms proper matrix is generated.
.multiply(svgTransform.matrix)
.translate(-centerTransform.x, -centerTransform.y);
// Apply new created matrix to element back:
const newTransform = source.ownerSVGElement.createSVGTransform();
newTransform.setMatrix(scalingMatrix);
source.transform.baseVal.initialize(newTransform);
}
function isDescendant(parent, child) {
var node = child.parentNode;
while (node != null) {
if (node == parent) {
return true;
}
node = node.parentNode;
}
return false;
}
var destinationElement = document.getElementById("destination");
var inputElement = document.getElementById("input");
while (inputElement.parentNode != null) {
applyTransformations(inputElement, destinationElement);
let candidateParent = inputElement.parentNode;
if (isDescendant(candidateParent, destinationElement)) {
break;
}
inputElement = candidateParent;
}
// Test:
var bboundsTest= document.getElementById("bboundsTest");
const resultBBounds = document.getElementById("input").getBoundingClientRect();
bboundsTest.setAttribute('x', resultBBounds.x);
bboundsTest.setAttribute('y', resultBBounds.y);
bboundsTest.setAttribute('width', resultBBounds.width);
bboundsTest.setAttribute('height', resultBBounds.height);
<svg version="1.2" viewBox="0 0 480 240" width="480" height="240" xmlns="http://www.w3.org/2000/svg">
<g>
<g transform="skewX(10) translate(95,1) rotate(30)">
<g transform="skewX(30) translate(-3,3) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<rect
id="input"
transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
width="30"
height="30"
fill="red"
/>
</g>
</g>
</g>
<rect
id="destination"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="blue"
/>
<rect
id="bboundsTest"
fill="transparent"
stroke="black"
/>
</g>
</svg>
Approach 2:
Or find parent of input that is not parent of destination first and than apply same transformations as parent node.
function applyTransformations(source, target) {
var inputBB = source.getBoundingClientRect();
var outputBB = target.getBoundingClientRect();
var scaleX = outputBB.width / inputBB.width;
var scaleY = outputBB.height / inputBB.height;
// get offsets between figure center and destination rect center:
var offsetX = outputBB.x + outputBB.width / 2 - (inputBB.x + inputBB.width / 2);
var offsetY =
outputBB.y + outputBB.height / 2 - (inputBB.y + inputBB.height / 2);
// get current figure transformation
let currentMatrix = (
source.transform.baseVal.consolidate() ||
source.ownerSVGElement.createSVGTransform()
).matrix;
// Get center of figure in element coordinates:
const inputBBox = source.getBBox();
const centerTransform = source.ownerSVGElement.createSVGPoint();
centerTransform.x = inputBBox.x + inputBBox.width / 2;
centerTransform.y = inputBBox.y + inputBBox.height / 2;
// create scale matrix:
const svgTransform = source.ownerSVGElement.createSVGTransform();
svgTransform.setScale(scaleX, scaleY);
let scalingMatrix = source.ownerSVGElement
.createSVGMatrix()
// move the figure to the center of the destination rect.
.translate(offsetX, offsetY)
// Apply current matrix, so old transformations are not lost
.multiply(currentMatrix)
.translate(centerTransform.x, centerTransform.y)
// multiply is used instead of the scale method while for some reasons matrix scale is giving proportional scaling...
// From a transforms proper matrix is generated.
.multiply(svgTransform.matrix)
.translate(-centerTransform.x, -centerTransform.y);
// Apply new created matrix to element back:
const newTransform = source.ownerSVGElement.createSVGTransform();
newTransform.setMatrix(scalingMatrix);
source.transform.baseVal.initialize(newTransform);
}
function isDescendant(parent, child) {
var node = child.parentNode;
while (node != null) {
if (node == parent) {
return true;
}
node = node.parentNode;
}
return false;
}
var destinationElement = document.getElementById("destination");
var inputElement = document.getElementById("input");
while (inputElement.parentNode != null) {
let candidateParent = inputElement.parentNode;
if (isDescendant(candidateParent, destinationElement)) {
break;
}
inputElement = candidateParent;
}
applyTransformations(inputElement, destinationElement);
// Test:
var bboundsTest= document.getElementById("bboundsTest");
const resultBBounds = document.getElementById("input").getBoundingClientRect();
bboundsTest.setAttribute('x', resultBBounds.x);
bboundsTest.setAttribute('y', resultBBounds.y);
bboundsTest.setAttribute('width', resultBBounds.width);
bboundsTest.setAttribute('height', resultBBounds.height);
<svg version="1.2" viewBox="0 0 480 240" width="480" height="240" xmlns="http://www.w3.org/2000/svg">
<g>
<g transform="skewX(10) translate(95,1) rotate(30)">
<g transform="skewX(30) translate(-3,3) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<rect
id="input"
transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
width="30"
height="30"
fill="red"
/>
</g>
</g>
</g>
<rect
id="destination"
x="20"
y="20"
width="100"
height="100"
fill="transparent"
stroke="blue"
/>
<rect
id="bboundsTest"
fill="transparent"
stroke="black"
/>
</g>
</svg>
Note: Both approach will yield different results based on transformations involved on parent elements as second approach doesn't apply all transformations to destination but rather same transformations as parent node of input that is not also parent for destination.
It took me some time to realize an answer, but finally, I got it and it's quite simple!
Get the bounding boxes of both rectangles in the 'screen' coordinates.
For example: getBoundingClientRect.
By comparing the rectangle boxes you can get the desired scaling factors.
While scaling should be done in screen coordinates, we should convert the current element transformation including all the parent transformations to the screen coordinates, transform all those with given scale and convert back to the element coordinates.
Exact line is:
var toScreenMatrix = inputElement.getScreenCTM();
// Scale element by a matrix in screen coordinates and convert it back to the element coordinates:
currentMatrix = currentMatrix.multiply(toScreenMatrix.inverse().multiply(scaleAndTransform).multiply(toScreenMatrix));
This code is generic for all the svg elements, so any shape can be fit into the given rect:
function fitElement(from, to, changePosition) {
var inputElement = document.getElementById(from);
var destinationElement = document.getElementById(to);
// Get center of figure in element coordinates:
var inputScreenBBox = inputElement.getBoundingClientRect();
var destinationScreenBBox = destinationElement.getBoundingClientRect();
var scaleX = destinationScreenBBox.width / inputScreenBBox.width;
var scaleY = destinationScreenBBox.height / inputScreenBBox.height;
var inputCenter = getCenter(inputScreenBBox);
var offsetX = 0;
var offsetY = 0;
if (changePosition) {
var destCenter = getCenter(destinationScreenBBox);
offsetX = destCenter.x - inputCenter.x;
offsetY = destCenter.y - inputCenter.y;
}
// create scale matrix:
var scaleMatrix = getScaleMatrix(scaleX, scaleY, inputElement);
// get element self transformation matrix:
var currentMatrix = getElementMatrix(inputElement);
scaleAndTransform = inputElement.ownerSVGElement.createSVGMatrix()
.translate(offsetX, offsetY)
// Scale in screen coordinates around the element center:
.translate(inputCenter.x, inputCenter.y)
.multiply(scaleMatrix)
.translate(-inputCenter.x, -inputCenter.y)
var toScreenMatrix = inputElement.getScreenCTM();
// Scale element by a matrix in screen coordinates and convert it back to the element coordinates:
currentMatrix = currentMatrix.multiply(toScreenMatrix.inverse().multiply(scaleAndTransform).multiply(toScreenMatrix));
// Apply new created transform back to the element:
var newTransform = inputElement.ownerSVGElement.createSVGTransform();
newTransform.setMatrix(currentMatrix);
inputElement.transform.baseVal.initialize(newTransform);
}
function getElementMatrix(element) {
// Get consolidated element matrix:
var currentMatrix =
(element.transform.baseVal.consolidate() ||
element.ownerSVGElement.createSVGTransform()).matrix;
return currentMatrix;
}
function getScaleMatrix(scaleX, scaleY, el) {
// Return DOM matrix
var svgTransform = el.ownerSVGElement.createSVGTransform();
// Transform type is used because of the bug in chrome applying scale to the DOM matrix:
svgTransform.setScale(scaleX, scaleY);
var scaleMatrix = svgTransform.matrix;
return scaleMatrix
}
function getCenter(rect) {
return new DOMPoint((rect.x + rect.width / 2), (rect.y + rect.height / 2));
}
fitElement('source', 'destination', true);
<svg width="1380" height="1340" xmlns="http://www.w3.org/2000/svg">
<g transform="skewX(10) translate(-3,4) rotate(30)">
<g transform="skewX(30) translate(-3,4) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<g transform="translate(350,30) skewX(10) rotate(30)">
<rect id="source" transform="scale(2) rotate(30) skewX(10)" x="20" y="50" width="30" height="30"
fill="red" />
</g>
</g>
</g>
</g>
<rect id="destination" x="30" y="30" width="120" height="100" fill="transparent" stroke="blue" />
</svg>
GitHub gist link

Moving SVG object on guideline using javascript

I'm trying to move SVG object on <path> guideline I created, my code is based on this one, but I can't figure out why mine doesn't work and the codepen I shared does.
function positionTheElement() {
var html = document.documentElement;
var body = document.body;
var scrollPercentage = (html.scrollTop + body.scrollTop) / (html.scrollHeight - html.clientHeight);
var path = document.getElementById("trace");
var pathLen = path.getTotalLength();
var pt = path.getPointAtLength(scrollPercentage * pathLen );
var element = document.getElementById("etoile");
element.setAttribute("transform", "translate("+ pt.x + "," + pt.y + ")");
};
window.addEventListener("scroll", positionTheElement);
positionTheElement();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1920 4000" style="enable-background:new 0 0 1920 4000;" xml:space="preserve">
<g>
<path class="st12" id="trace" d="M1491.8,3999l-7.8-365c0,0-1156-14-1240.5-1.3S155,3552,155,3552s3-594-0.3-688.1
c-3.3-94.1,94.3-81.9,94.3-81.9l689,5c0,0,645,5,751.9,6s90.1-93,90.1-93s20-468,14-576s-96.2-89.6-96.2-89.6S288,2039,208,2034.4
S120,1931,120,1931s13-1156,6-1184s4-60,10-89c0-33,141-28,141-28s764-1,867,0s154-11,169-19c36.8-10.9,26.8-27.7,28-53l2.5-525.5"
stroke="tomato" fill="none" stroke-width="4"/>
<polygon class="st11" id="etoile" stroke="tomato" fill="none" stroke-width="4" points="1367.7,59.8 1345.4,49.8 1324.5,62.3 1327.3,38.3 1308.8,22.4 1332.8,17.6 1342.3,-4.7 1354.3,16.3
1378.7,18.4 1362.2,36.2 "/>
</g>
</svg>
How can I animate the SVG polygon on the path guideline correctly?
You have 2 errors:
the star should have the center in the origin of the svg canvas (x=0; y=0).
the path trace has to be reversed. The way it's drawn (from the bottom up) would make the star begin it's movement from the bottom of the document to the top.
function positionTheElement() {
var html = document.documentElement;
var body = document.body;
var scrollPercentage = (html.scrollTop + body.scrollTop) / (html.scrollHeight - html.clientHeight);
var path = document.getElementById("trace");
var pathLen = path.getTotalLength();
var pt = path.getPointAtLength(scrollPercentage * pathLen );
var element = document.getElementById("etoile");
element.setAttribute("transform", "translate("+ pt.x + "," + pt.y + ")");
};
window.addEventListener("scroll", positionTheElement);
positionTheElement();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg viewBox="0 0 1920 4000" >
<g>
<path class="st12" id="trace" d="M1343.5,32.5
L1341,558C1339.8,583.3 1349.8,600.1 1313,611C1298,619 1247,631 1144,630C1041,629 277,630 277,630C277,630 136,625 136,658C130,687 119,719 126,747C133,775 120,1931 120,1931C120,1931 128,2029.8000000000002 208,2034.4C288,2039 1697.8,2034.4 1697.8,2034.4C1697.8,2034.4 1788,2016 1794,2124C1800,2232 1780,2700 1780,2700C1780,2700 1796.8000000000002,2794 1689.9,2793C1583,2792 938,2787 938,2787L249,2782C249,2782 151.39999999999998,2769.8 154.7,2863.9C158,2958 155,3552 155,3552C155,3552 159,3645.3999999999996 243.5,3632.7C328,3620 1484,3634 1484,3634L1491.8,3999"
stroke="tomato" fill="none" stroke-width="4"/>
<path class="st11" id="etoile" stroke="tomato" fill="none" stroke-width="4" d="M25,33.5l-22.3,-10l-20.9,12.5l2.8,-24l-18.5,-15.9l24,-4.8l9.5,-22.3l12,21l24.4,2.1l-16.5,17.8z"/>
</g>
</svg>

Change SVG fill in a loop

I have simple SVG illustration, is there is any way to change it's color constantly ? like a loop non-stop random color change.
here is my svg
<svg width="533" height="499" viewBox="0 0 533 499" fill="none" xmlns="http://www.w3.org/2000/svg">
This is how I would do it: I'm using colors hsl for the fill and I'm animating the hue of the colors using requestAnimationFrame. I hope it helps.
let p1 = document.querySelectorAll("path")[0];
let p2 = document.querySelectorAll("path")[1]
let h = 0;
function changeColor(){
window.requestAnimationFrame(changeColor);
h+=.5;
h2=210+h;
p1.setAttributeNS(null,"fill", `hsl(${~~h},100%,50%)`);
p2.setAttributeNS(null,"fill", `hsl(${~~h2},100%,50%)`);
}
changeColor()
<svg width="533" height="499" viewBox="0 0 533 499" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M258.089 59.6035C309.803 -3.94652 379.363 78.1818 407.679 127.19C352.338 67.4782 301.718 129.7 287.076 167.787C272.435 205.874 233.694 210.043 205.199 217.679C187.359 222.459 146.446 248.26 128.6 264.085C109.864 289.466 48.3081 292.846 41.8378 268.698C27.0852 213.64 95.5238 148.37 137.644 123.97C163.705 101.458 206.375 123.154 258.089 59.6035Z" fill="blue"/>
<path d="M448.323 394.788C427.389 384.661 420.75 356.279 420.047 343.354C441.009 284.421 527.63 350.762 528.167 368.218C528.703 385.674 474.491 407.447 448.323 394.788Z" fill="red"/>
</svg>
Select the element and recursively call a function that sets the fill attribute of the SVG element you want to recolor with a random hex.
const recolor = element => {
const randomColor = '#'+Math.floor(Math.random()*16777215).toString(16)
circle.setAttribute('fill', randomColor)
setTimeout(() => recolor(element), 600)
}
recolor(document.querySelector('#circle'))
svg circle {
transition: fill .5s linear;
}
<svg height="100" width="100">
<circle id="circle" cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
</svg>
fill on <svg> doesnot work. Its for its elements. You can change its background
function random_rgba() {
var o = Math.round, r = Math.random, s = 255;
return 'rgba(' + o(r()*s) + ',' + o(r()*s) + ',' + o(r()*s) + ',' + r().toFixed(1) + ')';
}
function changeColor(){
document.querySelector('svg').style.background = random_rgba();
}
setInterval(changeColor,200);
<svg width="533" height="499" viewBox="0 0 533 499" fill="none" xmlns="http://www.w3.org/2000/svg">

Generate SVG sine wave with one segment

I'm currently trying to generate a SVG path representing a sine wave that fit the width of the webpage.
The algorithm I'm currently using is drawing small line to between two point which is drawing the sine wave.
The algorithm :
for(var i = 0; i < options.w; i++) {
var operator = ' M ';
d += operator + ((i - 1) * rarity + origin.x) + ', ';
d += (Math.sin(freq * (i - 1 + phase)) * amplitude + origin.y);
if(operator !== ' L ') { operator = ' L '; }
d += ' L ' + (i * rarity + origin.x) + ', ';
d += (Math.sin(freq * (i + phase)) * amplitude + origin.y);
}
Which generates a path for the svg :
M 9, 82.66854866662797 L 10, 102.5192336707523
M 10, 102.5192336707523 L 11, 121.18508371540987
M 11, 121.18508371540987 L 12, 129.88725786264592
M 12, 129.88725786264592 L 13, 124.53298763579338
M 13, 124.53298763579338 L 14, 107.64046998532105
M 14, 107.64046998532105 L 15, 87.15451991511547
M 15, 87.15451991511547 L 16, 72.70999984499424
M 16, 72.70999984499424 L 17, 71.10039326578718
M 17, 71.10039326578718 L 18, 83.08272330249196
M 18, 83.08272330249196 L 19, 103.02151290977501
The thing is, at the end of the sinus I wanted to draw a line to close the rest of the path (with the Z)
Sorry for my drawing skills ! :D
The reason for closing the path and having a path linked is to be able to fill this path with a background or a gradient
I found that I could represent the sine waves in a single path where it's linked
M0 50 C 40 10, 60 10, 100 50 C 140 90, 160 90, 200 50 Z
Which looks like this :
But the thing is the algorithm I'm using lets me play with the sine function so that I could animate this waves (which is something I need) and I dont see how to animate the representation of the sine waves.
So to sum up, either you can help me find a way to link all the lines drawed by the actual algorithm ? or a way to animate the other representation to draw a waves without caring about the sinus.
Thanks in advance for your help !
You can animate the sine wave by just making the path the width of two wavelengths and then moving it left or right.
<svg width="200" height="100" viewBox="0 0 200 100">
<defs>
<path id="double-wave"
d="M0 50
C 40 10, 60 10, 100 50 C 140 90, 160 90, 200 50
C 240 10, 260 10, 300 50 C 340 90, 360 90, 400 50
L 400 100 L 0 100 Z" />
</defs>
<use xlink:href="#double-wave" x="0" y="0">
<animate attributeName="x" from="0" to="-200" dur="3s"
repeatCount="indefinite"/>
</use>
</svg>
I'm animating the x attribute of a <use> here because IMO it is more obvious what is going on.
What we are doing is animating the position that our two-wavelength path is rendered. Once it has moved one wavelength of distance, it jumps back to it's original position and repeats. The effect is seamless because the two waveshapes are identical. And the rest of the wave is off the edge of the SVG.
If you want to see what's going on behaind the scenes, we can make the SVG wider so you can see what's going on off to the left and right of the original SVG.
<svg width="400" height="100" viewBox="-200 0 600 100">
<defs>
<path id="double-wave"
d="M0 50
C 40 10, 60 10, 100 50 C 140 90, 160 90, 200 50
C 240 10, 260 10, 300 50 C 340 90, 360 90, 400 50
L 400 100 L 0 100 Z" />
</defs>
<use xlink:href="#double-wave" x="0" y="0">
<animate attributeName="x" from="0" to="-200" dur="3s"
repeatCount="indefinite"/>
</use>
<rect width="200" height="100" fill="none" stroke="red" stroke-width="2"/>
</svg>
Below is an example of sine wave across the width of the svg. It creates a polyline via a parametric equation. Animation can be had by adjusting the amplitude and/or phase angle.
Edit - added animation to phase angle.
<!DOCTYPE HTML>
<html>
<head>
<title>Sine Wave</title>
</head>
<body onload=amplitudeSelected() >
<div style=background:gainsboro;width:400px;height:400px;>
<svg id="mySVG" width="400" height="400">
<polyline id="sineWave" stroke="black" stroke-width="3" fill="blue" ></polyline>
</svg>
</div>
Amplitide:<select id="amplitudeSelect" onChange=amplitudeSelected() >
<option selected>10</option>
<option>20</option>
<option>30</option>
<option>40</option>
<option>50</option>
<option>60</option>
<option>70</option>
<option>80</option>
<option>90</option>
<option>100</option>
</select>
<button onClick=animatePhaseAngle();this.disabled=true >Animate Phase Angle</button>
<script>
//---onload & select---
function amplitudeSelected()
{
var startPoint=[0,400]
var endPoint=[400,400]
var originX=0
var originY=200
var width=400
var amplitude=+amplitudeSelect.options[amplitudeSelect.selectedIndex].text
var pointSpacing=1
var angularFrequency=.02
var phaseAngle=0
var origin = { //origin of axes
x: originX,
y: originY
}
var points=[]
points.push(startPoint)
var x,y
for (var i = 0; i < width/pointSpacing; i++)
{
x= i * pointSpacing + origin.x
y= Math.sin(angularFrequency*(i + phaseAngle)) * amplitude + origin.y
points.push([x,y])
}
points.push(endPoint)
sineWave.setAttribute("points",points.join(" "))
}
//---buton---
function animatePhaseAngle()
{
setInterval(animate,20)
var seg=.5
var cntr=0
var cntrAmp=0
var startPoint=[0,400]
var endPoint=[400,400]
var originX=0
var originY=200
var origin = { //origin of axes
x: originX,
y: originY
}
var width=400
var pointSpacing=1
var angularFrequency=.02
setInterval(animate,10)
function animate()
{
phaseAngle=seg*cntr++
var amplitude=+amplitudeSelect.options[amplitudeSelect.selectedIndex].text
var points=[]
points.push(startPoint)
var x,y
for (var i = 0; i < width/pointSpacing; i++)
{
x= i * pointSpacing + origin.x
y= Math.sin(angularFrequency*(i + phaseAngle)) * amplitude + origin.y
points.push([x,y])
}
points.push(endPoint)
sineWave.setAttribute("points",points.join(" "))
}
}
</script>
</body>
</html>

Animate the drawing of a dashed svg line

I'm trying to animate an arrow with a dashed line, something like this ------->, but with a horizontal and vertical path, using svg and css animations.
I've tried a couple of different things, animating height and width, using bottom and top, but each way has had something that doesn't quite look good or work well.
I managed to get a path drawn with svg, but the animation will actually remove the dashes and just draw a solid line.
No animation: http://jsfiddle.net/ehan4/2/
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="252px" height="396px" viewBox="0 0 252 396" enable-background="new 0 0 252 396" xml:space="preserve">
<path stroke="#000" stroke-width="1.5" fill="none" d="M50.887,170.063v-53.488h55.814" stroke-dasharray="5,5" stroke-dashoffset="0.00" />
With animation: http://jsfiddle.net/ehan4/1/
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="252px" height="396px" viewBox="0 0 252 396" enable-background="new 0 0 252 396" xml:space="preserve">
<path stroke="#000" stroke-width="1.5" fill="none" d="M50.887,170.063v-53.488h55.814" stroke-dasharray="5,5" stroke-dashoffset="0.00" />
var path = document.querySelector('path');
var length = path.getTotalLength();
path.style.transition = path.style.WebkitTransition = 'none';
path.style.strokeDasharray = length + ' ' + length;
path.style.strokeDashoffset = length;
path.getBoundingClientRect();
path.style.transition = path.style.WebkitTransition = 'stroke-dashoffset 2s ease-in-out';
path.style.strokeDashoffset = '0';
Any thoughts or ideas would be greatly appreciated!
Z
This function can animate the drawing of dashed path:
function drawPath(path, options) {
options = options || {}
var duration = options.duration || 5000
var easing = options.easing || 'ease-in-out'
var reverse = options.reverse || false
var undraw = options.undraw || false
var callback = options.callback || function () {}
var length = path.getTotalLength()
var dashOffsetStates = [length, 0]
if (reverse) {
dashOffsetStates = [length, 2 * length]
}
if (undraw) {
dashOffsetStates.reverse()
}
// Clear any previous transition
path.style.transition = path.style.WebkitTransition = 'none';
var dashArray = path.style.strokeDasharray || path.getAttribute("stroke-dasharray");
if (dashArray != '') {
// dashed path case
// repeats dash pattern as many times as needed to cover path length
var dashLength = dashArray.split(/[\s,]/).map(function (a) {
return parseFloat(a) || 0
}).reduce(function (a, b) {
return a + b
})
var dashCount = length / dashLength + 1
var a = new Array(Math.ceil(dashCount)).join(dashArray + " ")
path.style.strokeDasharray = a + '0' + ' ' + length
} else {
// non dashed path case
path.style.strokeDasharray = length + ' ' + length;
}
path.style.strokeDashoffset = dashOffsetStates[0];
path.getBoundingClientRect();
path.style.transition = path.style.WebkitTransition =
'stroke-dashoffset ' + duration + 'ms ' + easing;
path.style.strokeDashoffset = dashOffsetStates[1]
setTimeout(function() {
path.style.strokeDasharray = dashArray //set back original dash array
callback()
}, duration)
}
It repeats dash pattern as many times as needed to cover path length and then adds empty dash with length of path. It also animates dash offset, as you do in your example.
Working demo: http://jsfiddle.net/u88bm18b/
Duplicated the path as a solid color so it covers the dashed line. Then, animated the solid color line out, revealing the dashed line below.
http://jsfiddle.net/ehan4/4/
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="252px" height="396px" viewBox="0 0 252 396" enable-background="new 0 0 252 396" xml:space="preserve">
<path stroke="#000" stroke-width="1.5" fill="none" d="M50.887,170.063v-53.488h55.814" stroke-dasharray="5,5" stroke-dashoffset="0.00"/>
<path id="top" stroke="#fff" stroke-width="3" fill="none" d="M50.887,170.063v-53.488h55.814"/>
</svg>
<script>
var path = document.querySelector('#top');
var length = path.getTotalLength();
path.style.transition = path.style.WebkitTransition = 'none';
path.style.strokeDasharray = length + ' ' + length;
path.style.strokeDashoffset = '0';
path.getBoundingClientRect();
path.style.transition = path.style.WebkitTransition = 'stroke-dashoffset 2s ease-in-out';
path.style.strokeDashoffset = length;
</script>
SVG has a stroke attribute. Use CSS to change the stroke type to dashed, then change its offset in a javascript loop

Categories