Issue
For example, I have a complex svg path in which I know a certain Y coordinate, I need to find the length of the entire path up to this coordinate.
The most effective method I could think of is to use a binary search to find the line segment of a line to a given Y coordinate.
function binarySearchLengthByYCoord(path, yCoord) {
const pathTotalLength = +path.getTotalLength();
let low = 0;
let high = pathTotalLength;
let result = 0;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const point = path.getPointAtLength(mid);
if (point.y < yCoord) {
low = mid + 1;
result = mid;
} else {
high = mid - 1;
}
}
return pathTotalLength - result;
}
And this code works correctly, but it has to be called every time a scroll event is triggered, so on weak devices and phones, there are noticeable performance issues. I need to get a similar effect to what I already have here and with more efficient code.
scrollLineAnimation();
function scrollLineAnimation() {
const decorParent = document.querySelector("[data-line-animation]"),
lines = decorParent.querySelectorAll("svg");
if (lines.length) {
const line = lines[0],
path = line.getElementsByTagName("path")[0],
pathTotalLength = +path.getTotalLength(),
lastYCoord = path.getPointAtLength(pathTotalLength).y,
offsetHeight = decorParent.dataset.lineAnimation
? decorParent.dataset.lineAnimation
: 0.8;
line.style.strokeDasharray = pathTotalLength;
line.style.strokeDashoffset = pathTotalLength;
line.style.display = "block";
let position = 0;
let prcPostion = 0;
updateStrokeDashoffset();
window.addEventListener("scroll", updateStrokeDashoffset);
window.addEventListener("resize", updateStrokeDashoffset);
// Function for getting the Y coordinate and finding the line segment
function updateStrokeDashoffset() {
const scrollPosition = window.scrollY,
windowHeight = window.innerHeight,
svgHeight = line.getBoundingClientRect().height,
yCoord =
(scrollPosition + windowHeight * offsetHeight) *
(lastYCoord / svgHeight),
newStrokeDashoffsetValue = binarySearchLengthByYCoord(path, yCoord);
prcPostion = (newStrokeDashoffsetValue / pathTotalLength) * 100;
}
// Function for smooth animation
function setStrokeDashoffset() {
let dist = prcPostion - position;
position = position + (dist * 70) / 1000;
line.style.strokeDashoffset = Math.round(
(position * pathTotalLength) / 100
);
requestAnimationFrame(setStrokeDashoffset);
}
requestAnimationFrame(setStrokeDashoffset);
}
function binarySearchLengthByYCoord(path, yCoord) {
const pathTotalLength = +path.getTotalLength();
let low = 0;
let high = pathTotalLength;
let result = 0;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const point = path.getPointAtLength(mid);
if (point.y < yCoord) {
low = mid + 1;
result = mid;
} else {
high = mid - 1;
}
}
return pathTotalLength - result;
}
}
body {
max-width: 100%;
position: relative;
padding: 0;
margin: 0;
min-height: 6000px;
}
.decor {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
max-width: 1440px;
}
.decor svg {
position: absolute;
display: none;
left: 0;
top: 0;
width: 100%;
height: auto;
min-height: 100%;
}
<div data-line-animation="0.8" class="decor">
<svg preserveAspectRatio="none" width="1440" height="5141" viewBox="0 0 1440 5141" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M886.805 0.0743205C986.888 -1.4439 1187.91 19.063 1065.22 176.635C911.848 373.6 1455.21 527.592 1367.57 373.601C1279.93 219.61 613.874 395.088 1152.86 656.514C1584.04 865.655 736.569 920.328 258.934 921.522C91.767 929.283 -164.804 1007.13 146.247 1256.43C535.061 1568.06 1238.55 1700.59 1195.7 1890.43C1152.86 2080.27 -384.754 2905.95 146.247 2905.95C1314.52 2905.95 250.102 3383.41 709.046 3545.41C849.007 3594.81 1440.94 3511.17 1410.12 3824.66C1352.7 4408.75 -60.5479 4131.44 95.6015 4451.98C290.788 4852.65 1410.12 5002.4 1440 5140" stroke="#2BB32A" stroke-width="1.5" stroke-linecap="square" />
</svg>
</div>
Could you please tell me a more efficient way to find the length of an svg path to a given Y-coordinate?
Solution
getPointAtLength()
is quite expensive, so you should try to reduce the amount of sample point calculations.
One way to go could be to create a lookup object first that can later be used to translate y positions to path lengths.
let svg = document.querySelector("svg");
let precision = 100;
function getLengthLookup(path, precision = 100) {
let pathLength = path.getTotalLength();
let { x, y, width, height } = path.getBBox();
//create pathlength lookup
let lengthLookup = {
x: x,
y: y,
width: width,
height: height,
pathLength: pathLength
};
let lastY = 0;
let lastLength = 0;
// sample point to calculate Y at pathLengths
let step = Math.floor(pathLength / precision);
for (let i = 0; i < pathLength; i += step) {
let pt = path.getPointAtLength(i);
let y = +pt.y.toFixed(0);
let yRel = Math.ceil((precision / height) * y);
/**
* percentage based y values were skipped
* interpolate pathlengths in between
*/
let diffY = Math.abs(yRel - lastY);
if (diffY) {
let diffL = (i - lastLength) / diffY;
let newY = yRel - diffY;
for (let d = 0; d < diffY; d++) {
lengthLookup[newY] = lastLength + diffL * d;
newY++;
}
}
lengthLookup[yRel] = i;
lastY = yRel;
lastLength = i;
}
return lengthLookup;
}
let lengthLookup = getLengthLookup(path, precision);
// console.log(lengthLookup);
inpHeight.setAttribute("max", precision);
inpHeight.addEventListener("input", (e) => {
let val = +e.currentTarget.value;
line.setAttribute("y1", (lengthLookup.height / precision) * val);
line.setAttribute("y2", (lengthLookup.height / precision) * val);
let dashLength = lengthLookup[val];
path2.setAttribute(
"stroke-dasharray",
`${dashLength} ${lengthLookup.pathLength}`
);
});
svg {
border: 1px solid #ccc;
height: 90vh;
width: auto;
}
path {
transition: 1s stroke-dasharray;
}
<p><label>relative height</label><input id="inpHeight" type="range" value="0" min="0" max="100" step="1"></p>
<svg preserveAspectRatio="none" viewBox="0 0 1440 5141" xmlns="http://www.w3.org/2000/svg">
<line id="line" x1="0" x2="100%" y1="0" y2="0" stroke="purple" stroke-width="2"></line>
<path id="path" d="M886.805 0.0743205C986.888 -1.4439 1187.91 19.063 1065.22 176.635C911.848 373.6 1455.21 527.592 1367.57 373.601C1279.93 219.61 613.874 395.088 1152.86 656.514C1584.04 865.655 736.569 920.328 258.934 921.522C91.767 929.283 -164.804 1007.13 146.247 1256.43C535.061 1568.06 1238.55 1700.59 1195.7 1890.43C1152.86 2080.27 -384.754 2905.95 146.247 2905.95C1314.52 2905.95 250.102 3383.41 709.046 3545.41C849.007 3594.81 1440.94 3511.17 1410.12 3824.66C1352.7 4408.75 -60.5479 4131.44 95.6015 4451.98C290.788 4852.65 1410.12 5002.4 1440 5140" fill="none" stroke="#2BB32A" stroke-width="3" stroke-linecap="square" />
<path id="path2" d="M886.805 0.0743205C986.888 -1.4439 1187.91 19.063 1065.22 176.635C911.848 373.6 1455.21 527.592 1367.57 373.601C1279.93 219.61 613.874 395.088 1152.86 656.514C1584.04 865.655 736.569 920.328 258.934 921.522C91.767 929.283 -164.804 1007.13 146.247 1256.43C535.061 1568.06 1238.55 1700.59 1195.7 1890.43C1152.86 2080.27 -384.754 2905.95 146.247 2905.95C1314.52 2905.95 250.102 3383.41 709.046 3545.41C849.007 3594.81 1440.94 3511.17 1410.12 3824.66C1352.7 4408.75 -60.5479 4131.44 95.6015 4451.98C290.788 4852.65 1410.12 5002.4 1440 5140" fill="none" stroke="red" stroke-linecap="square" stroke="red" stroke-width="10" stroke-dasharray="0 20000" />
</svg>
In the above example we're retrieving points at every percent of the total path length and save y values relative to the path's height.
This process won't return a continuous list of relative y values (i.e 0-100).
When a range is missing we're interpolating these values according to the start and end path length.
Obviously this will only return approximated values. Especially the path section with the self intersection loop can't be calculated exactly as we have multiple length at this y position.
But we can improve accuracy by increasing the number of sample points e.g. to 200.
For testing see codepen
Example 2: Animate on scroll
let svg = document.querySelector("svg");
let precision = 300;
function getLengthLookup(path, precision = 100) {
let pathLength = path.getTotalLength();
let { x, y, width, height } = path.getBBox();
//create pathlength lookup
let lengthLookup = {
x: x,
y: y,
width: width,
height: height,
pathLength: pathLength
};
let lastY = 0;
let lastLength = 0;
// sample point to calculate Y at pathLengths
let step = Math.floor(pathLength / precision);
for (let i = 0; i < pathLength; i += step) {
let pt = path.getPointAtLength(i);
let y = +pt.y.toFixed(0);
let yRel = Math.ceil((precision / height) * y);
/**
* percentage based y values were skipped
* interpolate pathlengths in between
*/
let diffY = Math.abs(yRel - lastY);
if (diffY) {
let diffL = (i - lastLength) / diffY;
let newY = yRel - diffY;
for (let d = 0; d < diffY; d++) {
lengthLookup[newY] = lastLength + diffL * d;
newY++;
}
}
lengthLookup[yRel] = i;
lastY = yRel;
lastLength = i;
}
return lengthLookup;
}
let lengthLookup = getLengthLookup(path, precision);
window.addEventListener("scroll", (e) => {
let maxHeight = document.documentElement.scrollHeight - window.innerHeight;
let windowOffset = window.pageYOffset;
let scrollPosRel = Math.floor((windowOffset * precision) / maxHeight);
line.setAttribute("y1", (lengthLookup.height / precision) * scrollPosRel);
line.setAttribute("y2", (lengthLookup.height / precision) * scrollPosRel);
let dashLength = lengthLookup[scrollPosRel];
path2.setAttribute(
"stroke-dasharray",
`${dashLength} ${lengthLookup.pathLength}`
);
});
svg {
border: 1px solid #ccc;
height: 300vh;
width: auto;
}
path {
transition: 1s stroke-dasharray;
}
<svg preserveAspectRatio="none" viewBox="0 0 1440 5141" xmlns="http://www.w3.org/2000/svg">
<line id="line" x1="0" x2="100%" y1="0" y2="0" stroke="purple" stroke-width="2"></line>
<path id="path" d="M886.805 0.0743205C986.888 -1.4439 1187.91 19.063 1065.22 176.635C911.848 373.6 1455.21 527.592 1367.57 373.601C1279.93 219.61 613.874 395.088 1152.86 656.514C1584.04 865.655 736.569 920.328 258.934 921.522C91.767 929.283 -164.804 1007.13 146.247 1256.43C535.061 1568.06 1238.55 1700.59 1195.7 1890.43C1152.86 2080.27 -384.754 2905.95 146.247 2905.95C1314.52 2905.95 250.102 3383.41 709.046 3545.41C849.007 3594.81 1440.94 3511.17 1410.12 3824.66C1352.7 4408.75 -60.5479 4131.44 95.6015 4451.98C290.788 4852.65 1410.12 5002.4 1440 5140" fill="none" stroke="#2BB32A" stroke-width="3" stroke-linecap="square" />
<path id="path2" d="M886.805 0.0743205C986.888 -1.4439 1187.91 19.063 1065.22 176.635C911.848 373.6 1455.21 527.592 1367.57 373.601C1279.93 219.61 613.874 395.088 1152.86 656.514C1584.04 865.655 736.569 920.328 258.934 921.522C91.767 929.283 -164.804 1007.13 146.247 1256.43C535.061 1568.06 1238.55 1700.59 1195.7 1890.43C1152.86 2080.27 -384.754 2905.95 146.247 2905.95C1314.52 2905.95 250.102 3383.41 709.046 3545.41C849.007 3594.81 1440.94 3511.17 1410.12 3824.66C1352.7 4408.75 -60.5479 4131.44 95.6015 4451.98C290.788 4852.65 1410.12 5002.4 1440 5140" fill="none" stroke="red" stroke-linecap="square" stroke="red" stroke-width="10" stroke-dasharray="0 20000" />
</svg>
Answered By - herrstrietzel
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.