let pathData = pathDataToLonghands(path.getPathData());
inputShift.setAttribute('max', pathData.length - 1);
inputShift.addEventListener("input", (e) => {
let off = +e.currentTarget.value;
if (off >= pathData.length - 1) {
off = 0;
inputShift.value = off;
} else if (off == 0) {
off = pathData.length - 1;
inputShift.value = off;
}
let pathDataShift = shiftSvgStartingPoint(pathData, off);
pathDataShift = roundPathData(pathDataShift, 3)
path.setPathData(pathDataShift);
svgOut.value = path.getAttribute("d");
});
/**
* shift starting point
*/
function shiftSvgStartingPoint(pathData, offset) {
let pathDataL = pathData.length;
let newStartIndex = 0;
if (offset == 0) {
return pathData;
}
//exclude Z/z (closepath) command if present
let lastCommand = pathData[pathDataL - 1]["type"];
let trimRight = lastCommand.toLowerCase() == "z" ? 1 : 0;
// M start offset
newStartIndex =
offset + 1 < pathData.length - 1 ?
offset + 1 :
pathData.length - 1 - trimRight;
// slice array to reorder
let pathDataStart = pathData.slice(newStartIndex);
let pathDataEnd = pathData.slice(0, newStartIndex);
// remove original M
pathDataEnd.shift();
let pathDataEndL = pathDataEnd.length;
let pathDataEndLastValues = pathDataEnd[pathDataEndL - 1]["values"];
let pathDataEndLastXY = [
pathDataEndLastValues[pathDataEndLastValues.length - 2],
pathDataEndLastValues[pathDataEndLastValues.length - 1]
];
//remove z(close path) from original pathdata array
if (trimRight) {
pathDataStart.pop();
pathDataEnd.push({
type: "Z",
values: []
});
}
// prepend new M command and concatenate array chunks
pathData = [{
type: "M",
values: pathDataEndLastXY
}]
.concat(pathDataStart)
.concat(pathDataEnd);
return pathData;
}
/**
* decompose/convert shorthands to "longhand" commands:
* H, V, S, T => L, L, C, Q
* reversed method: pathDataToShorthands()
*/
function pathDataToLonghands(pathData) {
pathData = pathDataToAbsolute(pathData);
let pathDataLonghand = [];
let comPrev = {
type: "M",
values: pathData[0].values
};
pathDataLonghand.push(comPrev);
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let type = com.type;
let values = com.values;
let valuesL = values.length;
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
let [prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
switch (type) {
case "H":
comPrev = {
type: "L",
values: [values[0], prevY]
};
break;
case "V":
comPrev = {
type: "L",
values: [prevX, values[0]]
};
break;
case "T":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// new control point
cpN1X = prevX + (prevX - cp1X);
cpN1Y = prevY + (prevY - cp1Y);
comPrev = {
type: "Q",
values: [cpN1X, cpN1Y, x, y]
};
break;
case "S":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] =
valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// new control points
cpN1X = 2 * prevX - cp2X;
cpN1Y = 2 * prevY - cp2Y;
cpN2X = values[0];
cpN2Y = values[1];
comPrev = {
type: "C",
values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
};
break;
default:
comPrev = {
type: type,
values: values
};
}
pathDataLonghand.push(comPrev);
}
return pathDataLonghand;
}
/**
* This is just a port of Dmitry Baranovskiy's
* pathToRelative/Absolute methods used in snap.svg
* https://github.com/adobe-webplatform/Snap.svg/
*/
function pathDataToAbsolute(pathData, decimals = -1) {
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
// loop through commands
for (let i = 1; i < pathData.length; i++) {
let cmd = pathData[i];
let type = cmd.type;
let typeAbs = type.toUpperCase();
let values = cmd.values;
if (type != typeAbs) {
type = typeAbs;
cmd.type = type;
// check current command types
switch (typeAbs) {
case "A":
values[5] = +(values[5] + x);
values[6] = +(values[6] + y);
break;
case "V":
values[0] = +(values[0] + y);
break;
case "H":
values[0] = +(values[0] + x);
break;
case "M":
mx = +values[0] + x;
my = +values[1] + y;
default:
// other commands
if (values.length) {
for (let v = 0; v < values.length; v++) {
// even value indices are y coordinates
values[v] = values[v] + (v % 2 ? y : x);
}
}
}
}
// is already absolute
let vLen = values.length;
switch (type) {
case "Z":
x = +mx;
y = +my;
break;
case "H":
x = values[0];
break;
case "V":
y = values[0];
break;
case "M":
mx = values[vLen - 2];
my = values[vLen - 1];
default:
x = values[vLen - 2];
y = values[vLen - 1];
}
}
// round coordinates
if (decimals >= 0) {
pathData = roundPathData(pathData, decimals);
}
return pathData;
}
// just rounding to prevent awful floating point values
function roundPathData(pathData, decimals = -1) {
pathData.forEach((com, c) => {
if (decimals >= 0) {
com.values.forEach((val, v) => {
pathData[c].values[v] = +val.toFixed(decimals);
});
}
});
return pathData;
}
svg {
width: 20em;
overflow: visible;
}
#path {
marker-start: url(#markerStart);
marker-mid: url(#markerRound);
stroke-width: 0.33%;
}
textarea {
display: block;
width: 100%;
min-height: 30em;
}
<p><label>Shift starting point <input type="range" id="inputShift" steps="1" min="0" max="100" value="0"></label></p>
<svg id="svgPrev" viewBox="1 1 216 194">
<path id="path" d="
M 50 0
Q 36.4 0 24.8 6.8
t -18 18
t -6.8 25.2
C 0 63.8 5.6 76.3 14.65 85.35
s 21.55 14.65 35.35 14.65
A 50 50 0 0 0100 50
h -12.5
v -25
H 50
V 0
z "></path>
</svg>
<h3>Output</h3>
<textarea id="svgOut"></textarea>
<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
<defs>
<marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="5" fill="green"></circle>
<marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="2.5" fill="red"></circle>
</marker>
</defs>
</svg>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>