Issue
I am trying to create a scroll animation, for that i created a svg path and added a element which moves along the path when the user scrolls
Is there any way to make that element stay in the center of the viewport vertically while moving along the path when sccrolled?
I tried the below code, but as I scroll down initially the element doesn't come into view even though its moving along the path, halfway through the scroll it comes into the view and i can see that the element is moving along the path
const pathLength = Path_440.getTotalLength();
function clamp(min, val, max) {
return Math.min(Math.max(min, val), max);
}
function updatePath() {
const docElt = document.documentElement;
const pathBox = theFill.getBoundingClientRect();
const scrollProgress = clamp(
0, -pathBox.y / (pathBox.height - docElt.clientHeight),
1
);
pathIcon.style.offsetDistance = `${scrollProgress * 100}%`;
// These lines fill in the dashes as you scroll down.
const drawLength = pathLength * scrollProgress;
const rest = pathLength - drawLength;
theFill.style.strokeDasharray = `${drawLength}px ${rest}px`;
}
updatePath();
window.addEventListener("scroll", () => updatePath());
#pathIcon2 {
position: absolute;
inset: 0;
width: 10px;
height: 10px;
background-size: 25px;
}
body {
display: grid;
align-items: center;
align-content: center;
justify-content: center;
justify-items: center;
}
#pathIcon {
position: absolute;
inset: 0;
width: 10px;
height: 10px;
background-size: 25px;
offset-rotate: 0rad;
}
#Path_440 {
stroke-width: 2;
stroke: #001d36
}
#Path_444 {
stroke-width: 4;
stroke: #001d36
}
#theFill {
stroke-width: 4;
stroke: #55b0ff
}
<div style="height: 175px"></div>
<div style="position: relative">
<svg width="543" height="7907" viewBox="0 0 543 7907" fill="none">
<defs>
<path
id="Path_440"
d="M125.5 1V1C125.5 79.173 188.872 142.545 267.045 142.545H393.878C419.836 142.545 445.339 149.367 467.83 162.327V162.327C513.721 188.769 542 237.703 542 290.666V313.599L539.827 333.733C534.083 386.937 499.558 432.677 449.965 452.783L424.708 463.022C419.644 465.075 415.15 468.319 411.607 472.48V472.48C406.374 478.625 403.5 486.433 403.5 494.505V504.159V504.159C403.5 517.05 393.05 527.5 380.159 527.5H11.5C5.70101 527.5 1 532.201 1 538V538V943.5V943.5C1 949.299 5.701 954 11.5 954H389.5V954C397.232 954 403.5 960.268 403.5 968V1352.34V2115.19C403.5 2149.43 375.742 2177.19 341.5 2177.19V2177.19C307.258 2177.19 279.5 2204.95 279.5 2239.19V2420C279.5 2427.18 273.68 2433 266.5 2433V2433H17C10.3726 2433 5 2438.37 5 2445V2445V2845V2845C5 2853.84 12.1634 2861 21 2861H266.5V2861C273.68 2861 279.5 2866.82 279.5 2874V4003.55C279.5 4057.53 235.736 4101.3 181.75 4101.3V4101.3C127.764 4101.3 84 4145.06 84 4199.05V4332.5V4332.5C84 4335.81 86.6863 4338.5 90 4338.5H182.5V4338.5C185.814 4338.5 188.5 4341.19 188.5 4344.5V4762C188.5 4764.76 186.261 4767 183.5 4767V4767H91C87.134 4767 84 4770.13 84 4774V4774V5105.66C84 5183.53 147.128 5246.66 225 5246.66H261.159C319.061 5246.66 366 5293.6 366 5351.5V5351.5V5456V5456C366 5463.46 359.956 5469.5 352.5 5469.5H112.5C103.94 5469.5 97 5476.44 97 5485V5485V5861.5V5861.5C97 5871.44 105.059 5879.5 115 5879.5H348V5879.5C357.941 5879.5 366 5887.56 366 5897.5V6447C366 6456.94 357.941 6465 348 6465V6465H111C104.096 6465 98.5 6470.6 98.5 6477.5V6477.5V6858.5V6858.5C98.5 6867.61 105.887 6875 115 6875H348V6875C357.941 6875 366 6883.06 366 6893V7574.93C366 7627.95 323.019 7670.93 270 7670.93V7670.93C216.981 7670.93 174 7713.91 174 7766.93V7906.5"
/></path>
</defs>
<use href="#Path_440" stroke-width="10" stroke-dasharray="20 10"></use>
<use
id="theFill"
href="#Path_440"
style="stroke-dasharray: 1991.82px, 9259.88px"
stroke-width="10"
stroke="#4cacff"
></use>
</svg>
<div id="pathIcon" stroke="#4cacff" style="
offset-path: path(
'M 125.5 1 V 1 C 125.5 79.173 188.872 142.545 267.045 142.545 H 393.878 C 419.836 142.545 445.339 149.367 467.83 162.327 V 162.327 C 513.721 188.769 542 237.703 542 290.666 V 313.599 L 539.827 333.733 C 534.083 386.937 499.558 432.677 449.965 452.783 L 424.708 463.022 C 419.644 465.075 415.15 468.319 411.607 472.48 V 472.48 C 406.374 478.625 403.5 486.433 403.5 494.505 V 504.159 V 504.159 C 403.5 517.05 393.05 527.5 380.159 527.5 H 11.5 C 5.70101 527.5 1 532.201 1 538 V 538 V 943.5 V 943.5 C 1 949.299 5.701 954 11.5 954 H 389.5 V 954 C 397.232 954 403.5 960.268 403.5 968 V 1352.34 V 2115.19 C 403.5 2149.43 375.742 2177.19 341.5 2177.19 V 2177.19 C 307.258 2177.19 279.5 2204.95 279.5 2239.19 V 2420 C 279.5 2427.18 273.68 2433 266.5 2433 V 2433 H 17 C 10.3726 2433 5 2438.37 5 2445 V 2445 V 2845 V 2845 C 5 2853.84 12.1634 2861 21 2861 H 266.5 V 2861 C 273.68 2861 279.5 2866.82 279.5 2874 V 4003.55 C 279.5 4057.53 235.736 4101.3 181.75 4101.3 V 4101.3 C 127.764 4101.3 84 4145.06 84 4199.05 V 4332.5 V 4332.5 C 84 4335.81 86.6863 4338.5 90 4338.5 H 182.5 V 4338.5 C 185.814 4338.5 188.5 4341.19 188.5 4344.5 V 4762 C 188.5 4764.76 186.261 4767 183.5 4767 V 4767 H 91 C 87.134 4767 84 4770.13 84 4774 V 4774 V 5105.66 C 84 5183.53 147.128 5246.66 225 5246.66 H 261.159 C 319.061 5246.66 366 5293.6 366 5351.5 V 5351.5 V 5456 V 5456 C 366 5463.46 359.956 5469.5 352.5 5469.5 H 112.5 C 103.94 5469.5 97 5476.44 97 5485 V 5485 V 5861.5 V 5861.5 C 97 5871.44 105.059 5879.5 115 5879.5 H 348 V 5879.5 C 357.941 5879.5 366 5887.56 366 5897.5 V 6447 C 366 6456.94 357.941 6465 348 6465 V 6465 H 111 C 104.096 6465 98.5 6470.6 98.5 6477.5 V 6477.5 V 6858.5 V 6858.5 C 98.5 6867.61 105.887 6875 115 6875 H 348 V 6875 C 357.941 6875 366 6883.06 366 6893 V 7574.93 C 366 7627.95 323.019 7670.93 270 7670.93 V 7670.93 C 216.981 7670.93 174 7713.91 174 7766.93 V 7906.5'
);
offset-distance: 17.7024%;
background-image: url('https://via.placeholder.com/25x25/FF0000?text=red');
"></div>
</div>
Solution
Length at point?
In your case you need to find a path length value to calculate a offset-distance
value according to the current scroll y positions.
Unfortunately we can't accurately calculate path length values according to x/y values since there are probably multiple results e.g. if you path is self intersecting or has flat segments (running horizontally or vertically at certain segments).
However, we can calculate point coordinates at certain lengths via getPointAtLength()
.
Another problem ... getPointAtLength()
is quite expensive when called hundreds or thousands of times or more - so we shouldn't call it in a scroll event handler (which is also notorious for massively degrading performance).
let svg = document.querySelector("svg");
let mPath = document.getElementById('Path_440')
let strokePath = document.getElementById('theFill')
// add offset path based on svg
pathIcon.style.offsetPath = `path('${mPath.getAttribute('d')}')`
// steps for pathlength lookup
let precision = 1000;
// get transform matrix to translate svg units to screen coordinates
let matrix = svg.getScreenCTM()
function getLengthLookup(path, precision = 100) {
//create pathlength lookup
let pathLength = path.getTotalLength();
let lengthLookup = {
yArr: [],
lengthArr: [],
pathLength: pathLength
};
// sample point to calculate Y at pathLengths
let step = Math.floor(pathLength / precision);
for (let l = 0; l < pathLength; l += step) {
let pt = SVGToScreen(matrix, path.getPointAtLength(l));
let y = pt.y;
lengthLookup.yArr.push(y);
lengthLookup.lengthArr.push(l);
}
return lengthLookup;
}
const lengthLookup = getLengthLookup(mPath, precision);
const {
lengthArr,
yArr,
pathLength
} = lengthLookup;
const maxHeight = document.documentElement.scrollHeight - window.innerHeight;
window.addEventListener("scroll", (e) => {
let scrollPosMid = getViewportMiddleY();
midline.style.top = scrollPosMid + "px";
// get y pos length
let found = false;
for (let i = 0; i < yArr.length && !found; i++) {
// find next largest y in lookup
let y = yArr[i];
if (y >= scrollPosMid) {
let length = lengthArr[i]
// adjust length via interpolated approximation
let yPrev = yArr[i - 1] ? yArr[i - 1] : yArr[i];
let lengthPrev = lengthArr[i - 1] ? lengthArr[i - 1] : length;
let ratioL = 1 / lengthArr[i] * lengthPrev;
let ratioY = 1 / y * scrollPosMid;
let ratio = Math.max(ratioL, ratioY)
let dashLength = lengthArr[i] * ratio;
// calculate offsetDistance
let offsetDist = 100 / pathLength * dashLength
pathIcon.style.offsetDistance = offsetDist + '%';
// change dasharray
strokePath.setAttribute("stroke-dasharray", `${dashLength} ${pathLength}`);
// stop loop
found = true;
}
}
});
/**
* Get the absolute center/middle y-coordinate
* of the current scroll viewport
*/
function getViewportMiddleY() {
const viewportHeight = window.innerHeight;
const scrollY = window.scrollY || window.pageYOffset;
const element = document.documentElement;
const elementOffsetTop = element.offsetTop;
const middleY = scrollY + viewportHeight / 2 + elementOffsetTop;
return middleY;
}
/** Based on @Paul LeBeau's answer
* https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates#48354404
*/
function SVGToScreen(matrix, pt) {
let p = new DOMPoint(pt.x, pt.y);
p = p.matrixTransform(matrix);
return p
}
svg {
overflow: visible;
}
body {
display: grid;
align-items: center;
align-content: center;
justify-content: center;
justify-items: center;
}
#pathIcon {
position: absolute;
inset: 0;
width: 10px;
height: 10px;
background-size: 25px;
offset-rotate: 0rad;
transition: 0.2s;
}
#Path_440 {
stroke-width: 2;
stroke: #001d36
}
#midline {
display: block;
position: absolute;
width: 100%;
height: 1px;
border-top: 1px solid orange
}
<div style="height: 175px"></div>
<div id="scrollDiv" style="position: relative">
<svg width="543" height="7907" viewBox="0 0 543 7907" fill="none">
<defs>
<path id="Path_440" d="M125.5 1v0c0 78.2 63.4 141.5 141.5 141.5h126.9c25.9 0 51.4 6.9 73.9 19.8v0c45.9 26.5 74.2 75.4 74.2 128.4v22.9l-2.2 20.1c-5.7 53.2-40.2 99-89.8 119.1l-25.3 10.2c-5.1 2.1-9.6 5.3-13.1 9.5v0c-5.2 6.1-8.1 13.9-8.1 22v9.7v0c0 12.8-10.4 23.3-23.3 23.3h-368.7c-5.8 0-10.5 4.7-10.5 10.5v0v405.5v0c0 5.8 4.7 10.5 10.5 10.5h378v0c7.7 0 14 6.3 14 14v384.3v762.9c0 34.2-27.8 62-62 62v0c-34.2 0-62 27.7-62 62v180.8c0 7.2-5.8 13-13 13v0h-249.5c-6.6 0-12 5.4-12 12v0v400v0c0 8.8 7.2 16 16 16h245.5v0c7.2 0 13 5.8 13 13v1129.6c0 53.9-43.8 97.7-97.7 97.7v0c-54 0-97.8 43.8-97.8 97.8v133.4v0c0 3.3 2.7 6 6 6h92.5v0c3.3 0 6 2.7 6 6v417.5c0 2.8-2.2 5-5 5v0h-92.5c-3.9 0-7 3.1-7 7v0v331.7c0 77.8 63.1 141 141 141h36.2c57.9 0 104.8 46.9 104.8 104.8v0v104.5v0c0 7.5-6 13.5-13.5 13.5h-240c-8.6 0-15.5 6.9-15.5 15.5v0v376.5v0c0 9.9 8.1 18 18 18h233v0c9.9 0 18 8.1 18 18v549.5c0 9.9-8.1 18-18 18v0h-237c-6.9 0-12.5 5.6-12.5 12.5v0v381v0c0 9.1 7.4 16.5 16.5 16.5h233v0c9.9 0 18 8.1 18 18v681.9c0 53-43 96-96 96v0c-53 0-96 43-96 96v139.6" />
</path>
</defs>
<use href="#Path_440" stroke-width="10" stroke-dasharray="20 10"></use>
<use id="theFill" href="#Path_440" stroke-dasharray="1991.82, 9259.88" stroke-width="10" stroke="#4cacff"></use>
</svg>
<div id="pathIcon" stroke="#4cacff" style="
offset-distance: 0%;
background-image: url('https://via.placeholder.com/25x25/FF0000?text=red');
"></div>
</div>
<div id="midline"></div>
Translate svg to screen coordinates
If your svg dimensions equal the size it's placed in HTML (e.g. the viewBox values equal the element css width and height and there are not margins) you may skip this step.
Otherwise we need to translate values: See also Paul LeBeaus's answer here: "How to convert svg element coordinates to screen coordinates?"
Create a length-at-Y lookup
Before running any scroll event related scripts
we collect and save several length at y values in a lookup object.
We're saving Y values translated to screen/HTMLDom values.
This way we can directly compare the current mid-viewport Y values (retrieved via getViewportMiddleY()
helper) against the lookup values within the scroll event.
When a lookup Y value larger than the current mid-viewport Y is found (looping through the y-value array in the lookup object) we can use the corresponded length value from the lookup.
Support for offset-path
In 2024 it is still a quite new CSS feature so you should run cross browser tests. Most notably safari has just recently implemented parts of this spec – so there is currently a good chance your animation won't run on older iOS devices.
See caniuse statistics
Answered By - herrstrietzel
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.