New update and new issues below
Currently, my pathIcon move follow the path-svg with center screen.
My issues: When I resize the screen, path-svg (notes: I set witdh and height of svg is vw unit to easy resize) has a new size and then the pathIcon dont move follow along the path-svg when I scroll down, but it moves along the old path instead (i think so).
let svg = document.querySelector(".svg-path");
let mPath = document.getElementById("Path_440");
let strokePath = document.getElementById("theFill");
let pathIcon = document.getElementById("pathIcon");
// 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;
}
function SVGToScreen(matrix, pt) {
let p = new DOMPoint(pt.x, pt.y);
p = p.matrixTransform(matrix);
return p;
}
.svg-path {
overflow: visible;
width: 93vw;
height: 159vw;
}
#pathIcon {
position: absolute;
inset: 0;
width: 100px;
height: 200px;
background-size: 25px;
offset-rotate: 0rad;
transition: 0.2s;
offset-distance: 0%;
}
#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 class="svg-path" viewBox="0 0 1783 3038" fill="none">
<defs>
<path
id="Path_440"
d="M1292 1C1292 1 1276 75.4667 1196 92.5C1104.55 111.972 887.329 90.0678 771.5 99.5C544.5 99.5 301.61 124.941 278 294.5C267 373.5 264.112 418.83 267 502C269.888 585.17 268.896 644.646 304 740C345.771 853.462 769 837.5 1093.5 831C1365 841.5 1675.02 791 1765 1010C1802.18 1100.5 1765 1456.5 1765 1492.5C1765 1560 1695 1612.5 1617 1626.5C1464 1626.5 1187.11 1631.5 1002.5 1631.5C731.5 1631.5 368.526 1604.69 311 1716.5C270.612 1795 275 1919 275 1981C276.117 2073.28 271.553 2143.17 311 2257C338.857 2337.39 720.155 2323.5 980 2323.5C1296 2323.5 1676.34 2306.5 1738.5 2402.5C1792.57 2486 1771.73 2615.18 1771.73 2764C1771.73 2892 1769.73 2996.5 1651 3031C1611.67 3042.43 1236 3031 892 3031C404.5 3031 1 3029 1 3029"
stroke="#020878"
stroke-width="2"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-dasharray="20 20"
/>
</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>
<svg
id="pathIcon"
fill="none"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect width="100" height="200" fill="url(#pattern0)" />
<defs>
<pattern
id="pattern0"
patternContentUnits="objectBoundingBox"
width="1"
height="1"
>
<use
xlink:href="#image0_873_8619"
transform="scale(0.00353357 0.00176678)"
/>
</pattern>
<image
id="image0_873_8619"
width="283"
height="566"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAA"
/>
</defs>
</svg>
</div>
<div id="midline"></div>
I tried to calculate new path-svg when i resize screen but it doesn't exactly perform what I need. I'm not sure if the issue I identified is correct or not.
Update (24/03/2024): Successfully to resize but I have new issues
I followed this post enter link description here, and successfully ensured that the pathicon always stays on the SVG path each time the screen is resized. However, there is an unexpected issue with my scroll code; when I scroll, the pathicon no longer moves along the midline (middle of window). I don't know why this is happening.
Here is my code after I add my function scroll and it not move center of windown
let svg = document.querySelector(".svg-path");
let mPath = document.getElementById("Path_440");
let strokePath = document.getElementById("theFill");
let pathIcon = document.getElementById("pathIcon");
document.addEventListener("DOMContentLoaded", function () {
// auto adjust viewBox
let svgRect = svg.getBBox();
let width = svgRect.width;
let height = svgRect.height;
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
// update offset path
defineNewOffsetPath();
});
function defineNewOffsetPath() {
// retrieve the current scale from SVG transformation matrix
let matrix = svg.getCTM();
let scale = matrix.a;
// parse path data
let d = mPath.getAttribute("d");
let pathData = parsePathData(d);
//scale pathdata
pathData = scalePathData(pathData, scale);
// apply scaled pathdata as stringified d attribute value
d = pathDataToD(pathData);
pathIcon.style.offsetPath = `path('${d}')`;
}
// recalculate offset path on resize
window.addEventListener("resize", (e) => {
defineNewOffsetPath();
matrix = svg.getScreenCTM();
updateLengthLookup();
});
// just for illustration
resizeObserver();
function resizeObserver() {
defineNewOffsetPath();
}
new ResizeObserver(resizeObserver).observe(scrollDiv);
/**
* sclae path data proportional
*/
function scalePathData(pathData, scale = 1) {
let pathDataScaled = [];
pathData.forEach((com, i) => {
let { type, values } = com;
let comT = {
type: type,
values: [],
};
switch (type.toLowerCase()) {
// lineto shorthands
case "h":
comT.values = [values[0] * scale]; // horizontal - x-only
break;
case "v":
comT.values = [values[0] * scale]; // vertical - x-only
break;
// arcto
case "a":
comT.values = [
values[0] * scale, // rx: scale
values[1] * scale, // ry: scale
values[2], // x-axis-rotation: keep it
values[3], // largeArc: dito
values[4], // sweep: dito
values[5] * scale, // final x: scale
values[6] * scale, // final y: scale
];
break;
/**
* Other point based commands: L, C, S, Q, T
* scale all values
*/
default:
if (values.length) {
comT.values = values.map((val, i) => {
return val * scale;
});
}
}
pathDataScaled.push(comT);
});
return pathDataScaled;
}
/**
* parse stringified path data used in d attribute
* to an array of computable command data
*/
function parsePathData(d) {
d = d
// remove new lines, tabs an comma with whitespace
.replace(/[\n\r\t|,]/g, " ")
// pre trim left and right whitespace
.trim()
// add space before minus sign
.replace(/(\d)-/g, "$1 -")
// decompose multiple adjacent decimal delimiters like 0.5.5.5 => 0.5 0.5 0.5
.replace(/(\.)(?=(\d+\.\d+)+)(\d+)/g, "$1$3 ");
let pathData = [];
let cmdRegEx = /([mlcqazvhst])([^mlcqazvhst]*)/gi;
let commands = d.match(cmdRegEx);
// valid command value lengths
let comLengths = {
m: 2,
a: 7,
c: 6,
h: 1,
l: 2,
q: 4,
s: 4,
t: 2,
v: 1,
z: 0,
};
commands.forEach((com) => {
let type = com.substring(0, 1);
let typeRel = type.toLowerCase();
let isRel = type === typeRel;
let chunkSize = comLengths[typeRel];
// split values to array
let values = com
.substring(1, com.length)
.trim()
.split(" ")
.filter(Boolean);
/**
* A - Arc commands
* large arc and sweep flags
* are boolean and can be concatenated like
* 11 or 01
* or be concatenated with the final on path points like
* 1110 10 => 1 1 10 10
*/
if (typeRel === "a" && values.length != comLengths.a) {
let n = 0,
arcValues = [];
for (let i = 0; i < values.length; i++) {
let value = values[i];
// reset counter
if (n >= chunkSize) {
n = 0;
}
// if 3. or 4. parameter longer than 1
if ((n === 3 || n === 4) && value.length > 1) {
let largeArc = n === 3 ? value.substring(0, 1) : "";
let sweep =
n === 3 ? value.substring(1, 2) : value.substring(0, 1);
let finalX = n === 3 ? value.substring(2) : value.substring(1);
let comN = [largeArc, sweep, finalX].filter(Boolean);
arcValues.push(comN);
n += comN.length;
} else {
// regular
arcValues.push(value);
n++;
}
}
values = arcValues.flat().filter(Boolean);
}
// string to number
values = values.map(Number);
// if string contains repeated shorthand commands - split them
let hasMultiple = values.length > chunkSize;
let chunk = hasMultiple ? values.slice(0, chunkSize) : values;
let comChunks = [
{
type: type,
values: chunk,
},
];
// has implicit or repeated commands – split into chunks
if (hasMultiple) {
let typeImplicit = typeRel === "m" ? (isRel ? "l" : "L") : type;
for (let i = chunkSize; i < values.length; i += chunkSize) {
let chunk = values.slice(i, i + chunkSize);
comChunks.push({
type: typeImplicit,
values: chunk,
});
}
}
comChunks.forEach((com) => {
pathData.push(com);
});
});
/**
* first M is always absolute/uppercase -
* unless it adds relative linetos
* (facilitates d concatenating)
*/
pathData[0].type = "M";
return pathData;
}
/**
* serialize pathData array to
* d attribute string
*/
function pathDataToD(pathData, decimals = 3) {
let d = ``;
pathData.forEach((com) => {
d += `${com.type}${com.values
.map((val) => {
return +val.toFixed(decimals);
})
.join(" ")}`;
});
return 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) => {
scrollPathicon();
});
function scrollPathicon() {
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;
}
function SVGToScreen(matrix, pt) {
let p = new DOMPoint(pt.x, pt.y);
p = p.matrixTransform(matrix);
return p;
}
html {
margin: 0;
padding: 0;
}
.svg-path {
overflow: visible;
width: 100%;
}
#pathIcon {
position: absolute;
inset: 0;
width: 5vw;
height: 5vw;
offset-rotate: 0deg;
offset-distance: 10%;
}
#scrollDiv {
resize:both;
overflow:auto;
border: 1px solid #ccc;
margin:10px;
}
#midline {
display: block;
position: absolute;
width: 100%;
height: 1px;
border-top: 1px solid orange;
}
<div id="scrollDiv" style="position: relative">
<svg class="svg-path" viewBox="0 0 0 0" fill="none">
<defs>
<path
id="Path_440"
d="M1293 2 s-16 74.47-96 91.5c-91.45 19.47-308.67-2.43-424.5 7-227 0-469.89 25.44-493.5 195-11 79-13.89 124.33-11 207.5s1.9 142.65 37 238c41.77 113.46 465 97.5 789.5 91 271.5 10.5 581.52-40 671.5 179 37.18 90.5 0 446.5 0 482.5 0 67.5-70 120-148 134-153 0-429.89 5-614.5 5-271 0-633.97-26.81-691.5 85-40.39 78.5-36 202.5-36 264.5 1.12 92.28-3.45 162.17 36 276 27.86 80.39 409.15 66.5 669 66.5 316 0 696.34-17 758.5 79 54.07 83.5 33.23 212.68 33.23 361.5 0 128-2 232.5-120.73 267-39.33 11.43-415 0-759 0-487.5 0-891-2-891-2"
stroke="#020878"
stroke-width="2"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-dasharray="20 20"
/>
</defs>
<use
class="stroke"
href="#Path_440"
stroke="#ccc"
stroke-width="10"
stroke-dasharray="20 10"
></use>
<use
class="stroke"
id="theFill"
href="#Path_440"
stroke-dasharray="925.988 9259.88"
stroke-width="10"
stroke="#4cacff"
></use>
</svg>
<svg id="pathIcon" fill="none">
<rect width="100" height="100" fill="red" fill-opacity="0.5" />
</svg>
<div id="midline"></div>
</div>