How to compensate translate when skewX and skewY are used on SVG - javascript

CONTEXT: #AnaTudor (#daredevil) is talking about the d distance of movement for SVGs when skewX or skewY are used, so I was wondering if there is any way to calculate that distance in order to compensate with translate, and avoid using chained translate.
Test case: In the below snipet, we WON'T USE SVG specific chaining and/or nesting, we ONLY use same transform values, but in specific order of transform functions;
LEFT green rectangle via style: translate, rotate, skewX, skewY, scale;
RIGHT olive rectangle via transform attribute: translate, scale, rotate, skewX, `skewY.
Now, as you can see, the two rectangles have different positioning, if you click the button, the second rectangle gonna get closer to what we would expect, but still need to compute more for all cases.
Question: how can we change the fixOrigin function to adjust the translation for all possible transform function combinations, in a way that looks same as the CSS3 transform?
var el1 = document.querySelectorAll('path')[0],
el2 = document.querySelectorAll('path')[1],
el2BB = el2.getBBox(), el2cx = el2BB.x + el2BB.width/2, el2cy = el2BB.y + el2BB.height/2,
btn = document.querySelectorAll('button')[0], btn1 = document.querySelectorAll('button')[1],
x = 20, y = 20, scale = 0.6, rotate = 45, skewX = 20, skewY = -20;
el1.style.transform = 'translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6)';
el1.style.transformOrigin = '50% 50% 0px';
el2.setAttribute('transform', 'translate('+x+','+y+') scale('+scale+') rotate('+rotate+' '+el2cx+','+el2cy+') skewX('+skewX+') skewY('+skewY+')');
function fixOrigin(){
x += (1-scale) * el2BB.width/2;
y += (1-scale) * el2BB.height/2;
el2.setAttribute('transform', 'translate('+x+','+y+') scale('+scale+') rotate('+rotate+' '+el2cx+','+el2cy+') skewX('+skewX+') skewY('+skewY+')');
}
btn.addEventListener('click',fixOrigin,false);
function fixEverything() {
// scale binds all transform functions together
if ( !!scale ) {
//most important make sure we have translation values
//!!(x) && (x=0); !!(y) && (y=0);
// first adjust translate based on scale value
x += (1-scale) * el2BB.width/2;
y += (1-scale) * el2BB.height/2;
//now we also adjust the rotation transform origin based on SKEWS
if (!!rotate) {
// el2cx += .... el2cy += ...
}
//almost there, now we adjust the translation based on SKEWS
// x += ... y += ...
// last case, when SKEWS are used alone
} else if ( !scale && !rotate ) {
// adjust translation here
// x += ... y += ...
}
el2.setAttribute('transform', 'translate(' + x + ',' + y + ') scale(' + scale + ') rotate(' + rotate + ' ' + el2cx + ',' + el2cy + ') skewX(' + skewX + ') skewY(' + skewY + ')');
}
btn1.addEventListener('click', fixEverything, false);
/* desired transform
transform-origin: 50% 50% 0px;
transform: translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6);
*/
svg {
overflow: visible; width:30%;
border: 1px solid #eee; /* some sort of ruler */
}
<button>Fix Transform Origin</button><button>Fix All</button><br>
<p>Click to change the `transform` attribute</p>
<svg id="svgMixedCSS" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="green" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z" ></path>
</svg>
<svg id="svgMixedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="indigo" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>
I made a super cool snipet for you to play around. UPDATE: Also it has a function draft with all possible cases.

Since your core question currently appears to be how to calculate that d distance. It comes from a talk where the skew operation is essentially described as (x, y) ↦ (x + d, y) for the case of skewX. If you look at the SVG spec you will find a matrix which essentially says that the exact formula for skewX(a) is (x, y) ↦ (x + cos(a) y, y). Likewise for skewY(a) you have (x, y) ↦ (x, y + cos(a) x). So I'd say that d := cos(a) * y in the first case and d := cos(a) * x in the second case.
The question title suggested a different question to me, though. That question could be formulated as follows: given transform="skewX(c) translate(a, b)" for some numbers a, b and c, find d and e such that the transformation I just gave is the same as translate(d, e) skewX(c). Or in other words: how do I have to change the entries of a transform if I want to move the transform to the outside of a skewX.
To find these numbers, look at the corresponding matrix products, as defined in the spec:
⎡1 tan(c) 0⎤ ⎡1 0 a⎤ ⎡1 tan(c) a + tan(c) b⎤ ⎡1 0 a + tan(c) b⎤ ⎡1 tan(c) 0⎤
⎢0 1 0⎥∙⎢0 1 b⎥ = ⎢0 1 b ⎥ = ⎢0 1 b ⎥∙⎢0 1 0⎥
⎣0 0 1⎦ ⎣0 0 1⎦ ⎣0 0 1 ⎦ ⎣0 0 1 ⎦ ⎣0 0 1⎦
So you'd have d = a + tan(c) * b and e = b. You simply apply the skew transformation to the translation vector. In other words:
skewX(c) translate(a, b) = translate(a + tan(c) * b, b) skewX(c)
You can do a similar computation for y and obtain:
skewY(c) translate(a, b) = translate(a, b + tan(c) * a) skewY(c)
If you have both skewX and skewY combined, you can move the translate out one step at a time, so that at each step you only have to deal with a single skew direction. If you want the opposite direction (i.e. move translate closer to the inside of a skew), use - tan(c) instead of + tan(c) in these formulas.

Your edited question and the example it contains makes it clearer that what you are really after is translate CSS3 style="transform: …" transformations into equivalent SVG transform="…" transformations. In particular in a way which allows for the CSS3 transform-origin: 50% 50% 0px which places the center of transformation at the center of the object, as opposed to the origin of the SVG coordinate system.
The snippet below demonstrates two ways to achieve this. One is fairly simple: First translate the center of the object (which you already computed in the snippet from your question) to the origin, then perform all the transformations, then translate the point back to its original coordinates. That's the object in the center, which has essentially
transform="translate(256,256)
translate(20, 20)
rotate(45) skewX(20) skewY(-20) scale(0.6)
translate(-256,-256)"
But in your question you wrote that you'd like to “avoid using chained translate”, which the above makes use of (in a sense). In order to avoid that, you can combine all the translate steps into one. The code below does that, moving translate steps to the outside i.e. to the beginning of the sequence. The end result is essentially
transform="translate(211.325,73.165)
rotate(45) skewX(20) skewY(-20) scale(0.6)"
except for the actual result having more digits for each of these numbers. Personally I think that the first approach is easier and cleaner, but the second is probably closer to what you had in mind.
One particular benefit is that the code iterates over the elementary transformations in the order in which they are given in the transformation description, so that users are free to give transformations in any order they like, and the translations can still get collected appropriately.
var el1 = document.querySelectorAll('path')[0],
el2 = document.querySelectorAll('path')[1],
el2BB = el2.getBBox(), el2cx = el2BB.x + el2BB.width/2, el2cy = el2BB.y + el2BB.height/2,
el3 = document.querySelectorAll('path')[2],
transform = 'translate(20px, 20px) rotate(45deg) skewX(20deg) skewY(-20deg) scale(0.6)';
el1.style.transform = transform;
el1.style.transformOrigin = '50% 50% 0px';
transform = 'translate('+el2cx+','+el2cy+') ' + transform.replace(/deg/g,'').replace(/px/g,'')+' translate('+(-el2cx)+','+(-el2cy)+')';
el2.setAttribute('transform', transform);
el3.setAttribute('transform', combineTranslates(transform));
function combineTranslates(transform) {
var ts = [], // will contain list of elementary transformations
r = /\s*([A-Za-z0-9]+\s*\([\-0-9.,\s]*\))/g,
match,
pos = 0, // used during tokenization
deg = Math.PI/180.0,
x = 0, y = 0, // translation gets accumulated here
tmp;
// Tokenize transform into individual elementary transformations
while (match = r.exec(transform)) {
if (match.index !== pos) throw Error('Invalid transform: ' + transform);
pos += match[0].length;
ts.push(match[1]);
}
// TODO: check that only whitespace remains after matches
//console.log(ts);
// Iterate over transformations from inside to outside
for (var i = ts.length - 1; i >= 0; --i) {
match = /([A-Za-z0-9]+)\s*\(([\-0-9.,\s]*)\)/.exec(ts[i]);
var op = match[1],
args = match[2].replace(/\s+/g, '').split(',').map(Number);
//console.log(op, args);
switch (op) {
// Apply given transformation to (x,y) vector
case 'translate':
x += args[0];
y += args[1];
ts.splice(i, 1); // Drop translate from ts array
break;
case 'rotate':
var angle = args[0]*deg,
cos = Math.cos(angle),
sin = Math.sin(angle);
tmp = cos*x - sin*y;
y = sin*x + cos*y;
x = tmp;
break;
case 'scale':
x *= args[0];
y *= (args.length === 1 ? args[0] : args[1]);
break;
case 'skewX':
x += y*Math.tan(args[0]*deg);
break;
case 'skewY':
y += x*Math.tan(args[0]*deg);
break;
default:
throw Error('Unknown transform ' + op)
}
}
ts.unshift('translate('+x+','+y+')'); // add as first element
//console.log('From '+transform+'\n to '+ts.join(' '));
return ts.join(' ');
};
svg { overflow: visible; width:30%; border: 1px solid #eee; }
<svg id="svgMixedCSS" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="green" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z" ></path>
</svg>
<svg id="svgMixedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="indigo" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>
<svg id="svgCombinedAttr" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 500">
<path fill="blue" d="M426.671 0h-341.328c-46.937 0-85.343 38.405-85.343 85.345v341.311c0 46.969 38.406 85.344 85.343 85.344h341.328c46.938 0 85.329-38.375 85.329-85.345v-341.31c0-46.94-38.391-85.345-85.329-85.345z"></path>
</svg>

Related

Get pivot cx / cy from rotation SVGMatrix

Given is an SVG element which has a rotation transformation among others.
Transformations can be iterated in JS, finding the rotation transformation by its type SVGTransform.SVG_TRANSFORM_ROTATE. The angle can be extracted using SVGTransform.angle.
Everything else is encoded in SVGTransform.matrix.
How to reconstruct the rotation center offset from the transform object?
console.clear();
let r = document.querySelector('svg rect');
let transforms = r.transform.baseVal;
for(let i=0; i<transforms.length; i++)
{
let t=transforms[i];
if(t.type==SVGTransform.SVG_TRANSFORM_TRANSLATE)
{
console.log('Found translate '+t.matrix.e+', '+t.matrix.f);
}
else if(t.type==SVGTransform.SVG_TRANSFORM_ROTATE)
{
console.log('Found rotation');
console.log(' - angle is '+t.angle); //30
console.log(' - cx is '+'???'); //50 wanted
console.log(' - cy is '+'???'); //25 wanted
}
}
<svg width="300" height="200">
<rect style="fill: blue;" width="100" height="50" transform="translate(30 30) rotate(30 50 25)" />
</svg>
After logging t it seems there is no straightforward way to get back cx and cy.
But we may calculate it from some equations.
The center of rotation is the only point that maps to itself under a rotation (if the angle is not zero or equivalent).
If the center is located at x,y , it needs to satisfy the following equation involving matrix multiplication:
One can solve the two equations a*x+c*y+e=x and b*x+d*y+f=y. After some further calculations it's possible to find x and y:
console.clear();
let r = document.querySelector('svg rect');
let transforms = r.transform.baseVal;
for(let i=0; i<transforms.length; i++)
{
let t=transforms[i];
if(t.type==SVGTransform.SVG_TRANSFORM_TRANSLATE)
{
console.log('Found translate '+t.matrix.e+', '+t.matrix.f);
}
else if(t.type==SVGTransform.SVG_TRANSFORM_ROTATE)
{
console.log('Found rotation');
let{a,b,c,d,e,f}= t.matrix ;
let denominator = a - a*d + b*c+d-1
let x = ((d-1)*e-c*f)/denominator
let y = ((a-1)*f-b*e)/denominator
console.log(t, 'angle' in t )
console.log(' - angle is '+t.angle); //30
console.log(' - cx is '+ x); // 50.00000000000001
console.log(' - cy is '+ y); // 25.000000000000004
}
}
<svg width="300" height="200">
<rect style="fill: blue;" width="100" height="50" transform="translate(30 30) rotate(30 50 25)" />
</svg>
As you can see, while the results are really close to the original, it can't exactly recover the input. But then you probably have no more than 4 decimal places in the input, you may want to use something like +cx.toFixed(4).
Also, if the angle is 0 or other multiples of 360 degrees, the center cannot be recovered, as such rotation is just identity matrix, any center gives the same results. Depending on your use case, it might be better to save the data about cx, cy etc. somewhere and write to the rectangle based on that, rather than getting the transforms from the rectangle and trying to recover them.

How to offset SVG path from its origin position using offset-path CSS property

I have a bunch of SVG paths that are text letters. On scroll I want to animate them along offset-path starting from their original position. So I give 0x and 0y starting position to offset-path property and then give randomized Line to offset-path along which I want to animate SVG letters, like so:
path.setAttribute("style", "offset-path: path('M" + 0 +" " + 0 + " L " + generateRandomAnimationPathLine() + " " + generateRandomAnimationPathLine() + "')");
But once I give all SVG paths offset-path with random L attributes they are already all over the screen with offset-distance set to 0%. Why is that? Shouldn't they stay in their origin if offset-distance is set to 0? Why L in offset-path: path() moves SVG from their origin even though M are set to 0?
You can inspect each SVG letter and check off offset-distance to see that they are already out of origin point. How can I animate SVG paths starting from their original position? I'm trying to achieve an effect where as you scroll it slowly destructs the "Hello, World" and when you scroll back up it goes back to original form.
Image to explain what I'm trying to achieve. When SVG is on screen, set offset-path to random Lines/L along which I want to animate SVG letters from their original position by changing offset-distance from 0% to 100% relative to the scroll, like so:
path.style.offsetDistance = element.intersectionRatio * 100 + "%";
Live example: https://codepen.io/Limpuls/pen/KKzZWEm
Code:
<section class="row first">
<div class="col-lg-12">
<div class="svg">
<svg viewBox="-145 -90 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26 2V30H19.52V18.52H6.80001V30H0.320007V2H6.80001V13.04H19.52V2H26Z" fill="black"/>
<path d="M53.2647 19.32C53.2647 19.4 53.2247 19.96 53.1447 21H36.8647C37.158 22.3333 37.8514 23.3867 38.9447 24.16C40.038 24.9333 41.398 25.32 43.0247 25.32C44.1447 25.32 45.1314 25.16 45.9847 24.84C46.8647 24.4933 47.678 23.96 48.4247 23.24L51.7447 26.84C49.718 29.16 46.758 30.32 42.8647 30.32C40.438 30.32 38.2914 29.8533 36.4247 28.92C34.558 27.96 33.118 26.64 32.1047 24.96C31.0914 23.28 30.5847 21.3733 30.5847 19.24C30.5847 17.1333 31.078 15.24 32.0647 13.56C33.078 11.8533 34.4514 10.5333 36.1847 9.6C37.9447 8.64 39.9047 8.16 42.0647 8.16C44.1714 8.16 46.078 8.61333 47.7847 9.52C49.4914 10.4267 50.8247 11.7333 51.7847 13.44C52.7714 15.12 53.2647 17.08 53.2647 19.32ZM42.1047 12.88C40.6914 12.88 39.5047 13.28 38.5447 14.08C37.5847 14.88 36.998 15.9733 36.7847 17.36H47.3847C47.1714 16 46.5847 14.92 45.6247 14.12C44.6647 13.2933 43.4914 12.88 42.1047 12.88Z" fill="black"/>
<path d="M57.4191 0.319998H63.6591V30H57.4191V0.319998Z" fill="black"/>
<path d="M69.4503 0.319998H75.6903V30H69.4503V0.319998Z" fill="black"/>
<path d="M91.7216 30.32C89.4549 30.32 87.4149 29.8533 85.6016 28.92C83.8149 27.96 82.4149 26.64 81.4016 24.96C80.3882 23.28 79.8816 21.3733 79.8816 19.24C79.8816 17.1067 80.3882 15.2 81.4016 13.52C82.4149 11.84 83.8149 10.5333 85.6016 9.6C87.4149 8.64 89.4549 8.16 91.7216 8.16C93.9882 8.16 96.0149 8.64 97.8016 9.6C99.5882 10.5333 100.988 11.84 102.002 13.52C103.015 15.2 103.522 17.1067 103.522 19.24C103.522 21.3733 103.015 23.28 102.002 24.96C100.988 26.64 99.5882 27.96 97.8016 28.92C96.0149 29.8533 93.9882 30.32 91.7216 30.32ZM91.7216 25.2C93.3216 25.2 94.6282 24.6667 95.6416 23.6C96.6816 22.5067 97.2016 21.0533 97.2016 19.24C97.2016 17.4267 96.6816 15.9867 95.6416 14.92C94.6282 13.8267 93.3216 13.28 91.7216 13.28C90.1216 13.28 88.8016 13.8267 87.7616 14.92C86.7216 15.9867 86.2016 17.4267 86.2016 19.24C86.2016 21.0533 86.7216 22.5067 87.7616 23.6C88.8016 24.6667 90.1216 25.2 91.7216 25.2Z" fill="black"/>
<path d="M109.819 22.56C110.939 22.56 111.859 22.92 112.579 23.64C113.299 24.3333 113.659 25.2533 113.659 26.4C113.659 26.9333 113.592 27.4667 113.459 28C113.326 28.5333 113.032 29.3333 112.579 30.4L110.299 36.16H106.339L108.099 29.8C107.432 29.5333 106.899 29.1067 106.499 28.52C106.126 27.9067 105.939 27.2 105.939 26.4C105.939 25.2533 106.299 24.3333 107.019 23.64C107.766 22.92 108.699 22.56 109.819 22.56Z" fill="black"/>
<path d="M171.976 2L162.816 30H155.856L149.696 11.04L143.336 30H136.416L127.216 2H133.936L140.256 21.68L146.856 2H152.856L159.256 21.84L165.776 2H171.976Z" fill="black"/>
<path d="M183.792 30.32C181.525 30.32 179.485 29.8533 177.672 28.92C175.885 27.96 174.485 26.64 173.472 24.96C172.459 23.28 171.952 21.3733 171.952 19.24C171.952 17.1067 172.459 15.2 173.472 13.52C174.485 11.84 175.885 10.5333 177.672 9.6C179.485 8.64 181.525 8.16 183.792 8.16C186.059 8.16 188.085 8.64 189.872 9.6C191.659 10.5333 193.059 11.84 194.072 13.52C195.085 15.2 195.592 17.1067 195.592 19.24C195.592 21.3733 195.085 23.28 194.072 24.96C193.059 26.64 191.659 27.96 189.872 28.92C188.085 29.8533 186.059 30.32 183.792 30.32ZM183.792 25.2C185.392 25.2 186.699 24.6667 187.712 23.6C188.752 22.5067 189.272 21.0533 189.272 19.24C189.272 17.4267 188.752 15.9867 187.712 14.92C186.699 13.8267 185.392 13.28 183.792 13.28C182.192 13.28 180.872 13.8267 179.832 14.92C178.792 15.9867 178.272 17.4267 178.272 19.24C178.272 21.0533 178.792 22.5067 179.832 23.6C180.872 24.6667 182.192 25.2 183.792 25.2Z" fill="black"/>
<path d="M205.723 11.32C206.469 10.28 207.469 9.49333 208.723 8.96C210.003 8.42667 211.469 8.16 213.123 8.16V13.92C212.429 13.8667 211.963 13.84 211.723 13.84C209.936 13.84 208.536 14.3467 207.523 15.36C206.509 16.3467 206.003 17.84 206.003 19.84V30H199.763V8.48H205.723V11.32Z" fill="black"/>
<path d="M216.833 0.319998H223.073V30H216.833V0.319998Z" fill="black"/>
<path d="M250.784 0.319998V30H244.824V27.52C243.278 29.3867 241.038 30.32 238.104 30.32C236.078 30.32 234.238 29.8667 232.584 28.96C230.958 28.0533 229.678 26.76 228.744 25.08C227.811 23.4 227.344 21.4533 227.344 19.24C227.344 17.0267 227.811 15.08 228.744 13.4C229.678 11.72 230.958 10.4267 232.584 9.52C234.238 8.61333 236.078 8.16 238.104 8.16C240.851 8.16 242.998 9.02667 244.544 10.76V0.319998H250.784ZM239.184 25.2C240.758 25.2 242.064 24.6667 243.104 23.6C244.144 22.5067 244.664 21.0533 244.664 19.24C244.664 17.4267 244.144 15.9867 243.104 14.92C242.064 13.8267 240.758 13.28 239.184 13.28C237.584 13.28 236.264 13.8267 235.224 14.92C234.184 15.9867 233.664 17.4267 233.664 19.24C233.664 21.0533 234.184 22.5067 235.224 23.6C236.264 24.6667 237.584 25.2 239.184 25.2Z" fill="black"/>
</svg>
</div>
</div>
</section>
JS:
let thresholdArray = []
for (let i = 10; i < 100; i += 1){
thresholdArray.push(i / 100);
}
let options = {
root: null,
rootMargin: "20px",
threshold: thresholdArray
};
let pathGenerated = false;
let callback = (entries, observer) => {
entries.forEach(element => {
element.target.querySelectorAll("path").forEach(path => {
if (element.isIntersecting) {
if (!pathGenerated) {
path.setAttribute("style", "offset-path: path('M" + 0 +" " + 0 + " L " + generateRandomAnimationPathLine() + " " + generateRandomAnimationPathLine() + "')");
}
path.style.offsetDistance = element.intersectionRatio * 100 + "%";
} else {
pathGenerated = false;
path.style.removeProperty("offset-path");
}
});
});
pathGenerated = true;
}
let generateRandomAnimationPathLine = (element) => {
return Math.floor(Math.random() * Math.floor(100));
}
let observer = new IntersectionObserver(callback, options);
document.querySelectorAll('section').forEach(section => {
console.log(section)
observer.observe(section);
});
I have to say I find the usage of offset-path for the goal you want to achieve suboptimal. Support for several properties of the Motion path CSS module is incomplete, and as we have found out, the relation of rotation to the path is counter-intuitive. SVG itself has a mechanism for shifting and rotating individual letters in a text that can do this without any compatibility issues. This assumes you can use a <text> element instead of letters converted to paths.
<text> and <tspan> elements accept a number of attributes that take a list of space-separated numbers as argument. The nth number is applied to the nth letter inside the element:
dx and dy shift each letter away from the position it would be rendered at. The shifting is accumulative. In other words: after the first letter is shifted according to the first values, the second letter is rendered next to the shifted first letter, and is shifted away from that position according to the second values.
rotate rotates each letter, but is not accumulative: each number represents the rotation away from its original orientation.
The following example is a bit reduced from your pen, setting the "distance" with a slider instead of using an Observer. The maximum distances and rotations are stored in a global object. On each slider change, the list of values in the dx, dy and rotate attributes is recomputed to a fraction of the maximum distance and angle. Because of the accumulative nature of the distance values, the shift is "reset" for each letter by subtracting the shift of the previous letter.
const offsets = [];
for (let i = 0; i < 11; i++) {
offsets.push({
x: Math.random() * 60 - 30,
y: Math.random() * 60 - 30,
a: Math.random() * 360 - 180
});
}
const text = document.querySelector('.mesg');
document.querySelector('#distance').addEventListener('change', (ev) => {
const dist = parseFloat(ev.target.value);
const attrs = offsets.reduce((attr, o, i) => {
attr.dx.push((i ? o.x - offsets[i-1].x : o.x) * dist);
attr.dy.push((i ? o.y - offsets[i-1].y : o.y) * dist);
attr.rotate.push(o.a * dist);
return attr;
}, {dx: [], dy: [], rotate: []});
for (let [key, value] of Object.entries(attrs)) {
text.setAttribute(key, value.join(" "));
}
})
text {
font-size: 20px;
font-family: sans-serif;
}
<div><input id="distance" type="range" min="0" max="1" step="0.05" value="0"></input></div>
<svg viewBox="0 0 200 100" width="100%" height="150">
<text x="10" y="60" class="mesg">Hello World</text>
</svg>
There are two misunderstandings. The first one concerns the way offset-path works. If you have a path defined along which to move a letter, this is done using a moving coordinate system:
The origin of the coordinate system is moved to the point on the path indicated by offset-distance.
If nothing else is set for the property offset-rotate, the coordinate system is rotated automatically such that the x-axis points along the tangent to the path at the current used point.
Then, the letter is drawn in the moved and rotated coordinate system.
Contrary to what your grafic above implies, the letters are not moved away from their original position with an increasing distance, each of them always has a distance from the origin of its unmoved coordinate system, which is rotated and moved along the offset path. (Since each path has a separate offset path, this means a separate coordinate system for each letter.)
In a sense, for your straight lines, the rotation is applied before there is even a small distance, and that is why the letters end up all over the place.
As soon as you set
path {
offset-rotate: 0deg;
}
the rotation of the coordinate system is suppressed, and at offset-distance: 0 the letters are displayed at their original place. For greater distances, they still move in different directions, but unrotated.
If you want to give the letters an increasing rotation dependent on the distance, you would have to compute that in the script. But remember, the center of rotation is not the moved letter position, but the origin of the original coordinate system! That has the consequence that a linear function that rotates the letter increasingly with declining intersection, combined with an increasing distance, will produce a movement along a spiral. (Think of the combination of distance and rotation as polar coordinates.)
The second misunderstanding lies in the value of intersectionRatio the IntersectionObserver gives back. An intersection of 0% means the object is outside the reference, 100% means is is inside. Therefore, if you want to positiion the letters "orderly" as long as the section is visible, you have to compute the offset distance as inverse to the intersection:
let callback = (entries, observer) => {
entries.forEach(element => {
element.target.querySelectorAll("path").forEach(path => {
if (element.isIntersecting) {
if (!pathGenerated) {
const x1 = 0;
const y1 = 0;
const x2 = generateRandomAnimationPathLine();
const y2 = generateRandomAnimationPathLine();
path.style.offsetPath = `path('M ${x1} ${y1} L ${x2} ${y2}')`;
}
path.style.offsetDistance = (1 - element.intersectionRatio) * 100 + "%";
} else {
pathGenerated = false;
path.style.removeProperty("offset-path");
}
});
});
pathGenerated = true;
}

D3 How to keep element same size while transform scale / translate

This example illustrates my problem: https://bl.ocks.org/feketegy/ce9ab2efa9439f3c59c381f567522dd3
I have a couple of paths in a group element and I want to pan/zoom these elements except the blue rectangle path, which is in another group element.
The zooming and panning is done by applying transform="translate(0,0) scale(1) to the outer most group element then capturing the zoom delta and applying it to the same-size group element to keep it the same size.
This is working, but the blue rectangle position, which should remain the same size, is messed up, I would like to keep it in the same relative position to the other paths.
The rendered html structure looks like this:
<svg width="100%" height="100%">
<g class="outer-group" transform="translate(0,0)scale(1)">
<path d="M100,100 L140,140 L200,250 L100,250 Z" fill="#cccccc" stroke="#8191A2" stroke-width="2px"></path>
<path d="M400,100 L450,100 L450,250 L400,250 Z" fill="#cccccc" stroke="#8191A2" stroke-width="2px"></path>
<g class="same-size-position" transform="translate(300,250)">
<g class="same-size" transform="scale(1)">
<path d="M0,0 L50,0 L50,50 L0,50 Z" fill="#0000ff"></path>
</g>
</g>
</g>
</svg>
I've tried to get the X/Y position of the same-size-position group and create a delta from the translate x/y of the outer-group, but that doesn't seem to work.
After dusting off my high school geometry books I found a solution.
You need to get the bounding box of the element you want to keep the same size of and calculate a matrix conversion on it like so:
const zoomDelta = 1 / d3.event.transform.k;
const sameSizeElem = d3.select('.same-size');
const bbox = sameSizeElem.node().getBBox();
const cx = bbox.x + (bbox.width / 2);
const cy = bbox.y + (bbox.height / 2);
const zx = cx - zoomDelta * cx;
const zy = cy - zoomDelta * cy;
sameSizeElem
.attr('transform', 'matrix(' + zoomDelta + ', 0, 0, ' + zoomDelta + ', ' + zx + ', ' + zy + ')');
The matrix transformation will keep the relative position of the element which size remains the same and the other elements will pan/zoom.

Calculate apprx. SVG Ellipse length? (calculate apprx. ellipse circumference with Javascript)

I have an SVG element like so:
<ellipse class="solidLine" cx="649.9" cy="341.09" rx="39.49" ry="8.41"/>
and I need to find its length so that I can animate it's dashoffset to have it being "drawn".
I've done these calculations with the following tags: line polyline circle path, but now I need to calculate the length of an ellipse and I'm a little.. stuck. I've done some googling and can't seem to find a Javascript way to calculate the circumference of an ellipse.
Any help? Here's my function format so far:
const getEllipseLength = (ellipse) => {
let rx = ellipse.getAttribute('rx');
let ry = ellipse.getAttribute('ry');
let totalLength = //function to calculate ellipse circumference using radius-x (rx) and radius-y (ry) here!
return totalLength;
};
Thanks!
edit: if there's a quick way to do this that isn't 100% accurate (but close) that would be fine also. I just need to get in the ballpark of the actual circumference in order to do a smooth animation.
edit 2: I think using this equation will give me a close estimate without having to delve into Euler's series shenanigans.. gonna translate it into Javascript and see if it works.
Alright this was actually easier than I thought.. welp.
Since I only needed an approximate length I translated this equation into Javascript and came up with this:
const getEllipseLength = (ellipse) => {
let rx = parseInt(ellipse.getAttribute('rx'));
let ry = parseInt(ellipse.getAttribute('ry'));
let h = Math.pow((rx-ry), 2) / Math.pow((rx+ry), 2);
let totalLength = (Math.PI * ( rx + ry )) * (1 + ( (3 * h) / ( 10 + Math.sqrt( 4 - (3 * h) )) ));
return totalLength;
};
When used with rx="39.49" and ry="8.41" it gave me a value of 164.20811705227723, and google tells me the actual circumference is about 166.79. Not too bad, and just fine for SVG animation.
Note that that approximation works good for circle-like ellipses and gives significant error for long ones (with high a/b ratio).
If you aware about the second case, use iterative Gauss-Kummer approach
A = Pi * (a + b) * (1 + h^2/4 + h^4/64 + h^6/256...)
summing until next addend h^k/2^m becomes small enough
There is a workaround. Using this code:
<path
d="
M cx cy
m -rx, 0
a rx,ry 0 1,1 (rx * 2),0
a rx,ry 0 1,1 -(rx * 2),0
"
/>
We can create a path that's similar to the ellipse. Then, it's just a matter of using getTotalLength(). Check the demo snippet (I'm using a different cx and cy just to save some SVG space):
var length = document.getElementById("path").getTotalLength();
console.log(length)
<svg width="200" height="200">
<path id="path" fill="none" stroke="black" stroke-width="1" d="M 49.9, 41.09 m -39.49, 0 a 39.49,8.41 0 1,0 78.98,0 a 39.49,8.41 0 1,0 -78.98,0"/>
</svg>
It logs 166.82369995117188, which is very close to 166.79 (the circumference calculated by Google's tool) .
Here's my best approximation for the ellipse perimeter:
e = (a - b) / a
P = (1 - e) * (𝜋 * ((𝜋/2) * a + (2-(𝜋/2)) * b)) + (e * 4 * a)
function getEllipseLength (ellipse) {
let a = ellipse.getAttribute('rx');
let b = ellipse.getAttribute('ry');
if (a < b) {
var t = a;
a = b;
b = t;
}
var hpi = Math.PI/2;
var e = (a - b) / a; // e = 0 circle or 1 = line
return (1 - e) * (Math.PI * (hpi*a + (2-hpi)*b)) + (e * 4 * a);
}

Javascript: How to determine a SVG path draw direction?

I'm trying to determine a SVG path draw orientation. I'm working on something like this
var length = path.getTotalLength();
var horizontal = path.getPointAtLength(length/4).x - path.getPointAtLength(0).x;
var vertical = path.getPointAtLength(length/4).y - path.getPointAtLength(0).y;
Then do some comparisons with these values horizontal > 0 and vertical > 0, but this above idea isn't, in my mind, very successful.
My question is: is there anything I can use to determine the draw direction or perhaps some built in SVG methods/options?
Thank you
Use Math.atan2(yDiff, xDiff) to get the angle between the two reference points. Two visually identical shapes that go in opposite directions will have an angle difference of pi.
Be aware of the edge case where your two reference points are unluckily the same point. Not likely, especially given rounding errors, but keep it in mind in case you need this to be rock solid.
var paths = document.getElementsByTagName("path");
for (var pathNum = 0; pathNum < paths.length; pathNum += 1) {
var path = paths[pathNum];
var message = document.createElement('p');
message.innerHTML = "path #" + pathNum + ": angle = " + pathDir(path);
document.body.appendChild(message);
};
function pathDir(path) {
var length = path.getTotalLength();
var pt14 = path.getPointAtLength(1/4 * length);
var pt34 = path.getPointAtLength(3/4 * length);
var angle = Math.atan2(pt14.y - pt34.y, pt14.x - pt34.x);
return angle;
}
<svg width="300" height="80">
<g fill="none" stroke="black" stroke-width="4">
<path d="M 10,10 C 90,10 -30,60 50,60Z"/>
<path d="M110,10 C190,10 70,60 150,60Z"/>
<path d="M250,60 C170,60 290,10 210,10Z"/>
</g>
</svg>
<div></div>

Categories