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();
});
// 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;
}
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;
}
<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"
/>
</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>