I have an svg path and I want to use it to create a new path in JS to automatically adjust to different heights etc.
let w = window.innerWidth;
let h = document.querySelector("main").offsetHeight;
const pathSpec = "M305.91,0c8.54,101.44,17.07,203.69,5.27,304.8s-45.5,202.26-112.37,279c-...";
const yScale = h / 8000;
const xScale = w / 200;
let newPath = "";
const pathBits = pathSpec.matchAll(/([MmLlHhVvCcSsQqTtAa])([\d\.,\se-]*)/g);
pathBits.forEach((bit) => {
newPath = ; //not sure what goes in here
});
CodePudding user response:
try this :
let newPath = "";
const pathBits = pathSpec.matchAll(/([MmLlHhVvCcSsQqTtAa])([\d\.,\se-]*)/g);
pathBits.forEach((bit) => {
const command = bit[1];
const coordinates = bit[2];
newPath = command;
const coordArray = coordinates.split(/[,\s]/g);
coordArray.forEach((coord) => {
const coordComponents = coord.split(/[eE,]/g);
const x = parseFloat(coordComponents[0]) * xScale;
const y = parseFloat(coordComponents[1]) * yScale;
newPath = x "," y;
});
});
This should create a new path string that is scaled according to the xScale and yScale variables.
CodePudding user response:
Auto scaling svg
You might not need any javaScript to scale your pathdata. preserveAspectRatio="none"
might also do the trick.
.resize{
overflow:auto;
border:1px solid #ccc;
padding: 1em;
width:50%;
resize:both;
}
.svgFluid{
width:100%;
height:100%;
}
<h3>Resize me</h3>
<div >
<svg id="svg" viewBox="3 7 80 70" preserveAspectRatio="none">
<path id="path" d="M3,7 L13,7 m-10,10 l10,0 V27 H23 v10 h10C 33,43 38,47 43,47 c 0,5 5,10 10,10S 63,67 63,67 s -10,10 10,10Q 50,50 73,57q 20,-5 0,-10T 70,40t 0,-15A 5,5 45 1 0 40,20 a5,5 20 0 1 -10,-10Z" fill="#cccccc" stroke="#000000" stroke-width="1" stroke-linecap="butt" vector-effect="non-scaling-stroke"></path>
</svg>
</div>
Recalculate pathData / d
attribute
I highly recommend using a more "battle proof" path parser like Jarek Foksa's pathData polyfill..
Path d
strings allows different notations (comma or space as delimiter), horizontal or vertical shorthand commands (h
,v
) abbreviated coordinates (e.g. ".5.05" instead of ) etc.
So you might run into troubles using an overly simplified parsing function.
function scalePath(path, scaleX, scaleY) {
let pathData = path.getPathData();
let pathDataScaled = scalePathData(pathData, scaleX, scaleY);
path.setPathData(pathDataScaled);
adjustViewBox(svg)
}
/**
* scale pathData
*/
function scalePathData(pathData, scaleX, scaleY) {
let pathDataScaled = [];
pathData.forEach((com) => {
let [type, values] = [com.type, com.values];
let typeL = type.toLowerCase();
let valuesL = values.length;
let valsScaled = [];
switch (typeL) {
case "a":
pathDataScaled.push({
type: type,
values: [
values[0] * scaleX,
values[1] * scaleY,
values[2],
values[3],
values[4],
values[5] * scaleX,
values[6] * scaleY
]
});
break;
case "h":
pathDataScaled.push({
type: type,
values: [values[0] * scaleX]
});
break;
case "v":
pathDataScaled.push({
type: type,
values: [values[0] * scaleY]
});
break;
case "z":
pathDataScaled.push(com);
break;
default:
if (valuesL) {
valsScaled = [];
for (let i = 0; i < values.length; i = 2) {
let x = values[i] * scaleX;
let y = values[i 1] * scaleY;
valsScaled.push(x, y);
}
pathDataScaled.push({
type: type,
values: valsScaled
});
}
}
});
return pathDataScaled;
}
function adjustViewBox(svg) {
let bb = svg.getBBox();
let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
return val.toFixed(2);
});
let maxBB = Math.max(...bbVals);
let [x, y, width, height] = bbVals;
svg.setAttribute("viewBox", [x, y, width, height].join(" "));
}
svg {
width: 20em;
border: 1px solid #ccc;
overflow: visible
}
<!--
sample path y @phrogz:
https://stackoverflow.com/questions/9677885/convert-svg-path-to-absolute-commands
http://phrogz.net/svg/convert_path_to_absolute_commands.svg
-->
<p><button onclick="scalePath(path, 0.75, 0.5)">Scale path</button></p>
<svg id="svg" viewBox="3 7 80 70">
<path id="path" d="M3,7 L13,7 m-10,10 l10,0 V27 H23 v10 h10C 33,43 38,47 43,47 c 0,5 5,10 10,10S 63,67 63,67 s -10,10 10,10Q 50,50 73,57q 20,-5 0,-10T 70,40t 0,-15A 5,5 45 1 0 40,20 a5,5 20 0 1 -10,-10Z" fill="#cccccc" stroke="#000000" stroke-width="1" stroke-linecap="butt"></path>
</svg>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/path-data-polyfill.min.js"></script>
The above example
- parses the path via
getPathData()
- scales the pathData array (we need exceptions for commands like
a
,h
andv
- apply the scaled pathData to the
<path>
element
Standalone solution
This version will directly parse any d
data string (compatible with the official pathData spec).
So you can generate a scaled d
string without the need of appending a <path>
to your DOM.
let d = 'M3,7 L13,7 m-10,10 l10,0 V27 H23 v10 h10C 33,43 38,47 43,47 c 0,5 5,10 10,10S 63,67 63,67 s -10,10 10,10Q 50,50 73,57q 20,-5 0,-10T 70,40t 0,-15A 5,5 45 1 0 40,20 a5,5 20 0 1 -10,-10Z';
let pathData = parseDtoPathData(d);
let scaleX = 0.75;
let scaleY = 0.5;
let pathDataScaled = scalePathData(pathData, scaleX, scaleY);
let dScaled = pathDataToD(pathDataScaled);
path.setAttribute('d', dScaled);
adjustViewBox(svg)
function adjustViewBox(svg) {
let bb = svg.getBBox();
let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
return val.toFixed(2);
});
let maxBB = Math.max(...bbVals);
let [x, y, width, height] = bbVals;
svg.setAttribute("viewBox", [x, y, width, height].join(" "));
}
/**
* scale pathData
*/
function scalePathData(pathData, scaleX, scaleY) {
let pathDataScaled = [];
pathData.forEach((com) => {
let [type, values] = [com.type, com.values];
let typeL = type.toLowerCase();
let valuesL = values.length;
let valsScaled = [];
switch (typeL) {
case "a":
pathDataScaled.push({
type: type,
values: [
values[0] * scaleX,
values[1] * scaleY,
values[2],
values[3],
values[4],
values[5] * scaleX,
values[6] * scaleY
]
});
break;
case "h":
pathDataScaled.push({
type: type,
values: [values[0] * scaleX]
});
break;
case "v":
pathDataScaled.push({
type: type,
values: [values[0] * scaleY]
});
break;
case "z":
pathDataScaled.push(com);
break;
default:
if (valuesL) {
valsScaled = [];
for (let i = 0; i < values.length; i = 2) {
let x = values[i] * scaleX;
let y = values[i 1] * scaleY;
valsScaled.push(x, y);
}
pathDataScaled.push({
type: type,
values: valsScaled
});
}
}
});
return pathDataScaled;
}
/**
* create pathData from d attribute
**/
function parseDtoPathData(d, normalize = false) {
// sanitize d string
let commands = d
.replace(/[\n\r\t]/g, "")
.replace(/,/g, " ")
.replace(/-/g, " -")
.replace(/(\.)(\d )(\.)(\d )/g, "$1$2 $3$4")
.replace(/( )(0)(\d )/g, "$1 $2 $3")
.replace(/([a-z])/gi, "|$1 ")
.replace(/\s{2,}/g, " ")
.trim()
.split("|")
.filter(Boolean)
.map((val) => {
return val.trim();
});
// compile pathData
let pathData = [];
for (let i = 0; i < commands.length; i ) {
let com = commands[i].split(" ");
let type = com.shift();
let typeLc = type.toLowerCase();
let isRelative = type === typeLc ? true : false;
let values = com.map((val) => {
return parseFloat(val);
});
// analyze repeated (shorthanded) commands
let chunks = [];
let repeatedType = type;
// maximum values for a specific command type
let maxValues = 2;
switch (typeLc) {
case "v":
case "h":
maxValues = 1;
if (typeLc === "h") {
repeatedType = isRelative ? "h" : "H";
} else {
repeatedType = isRelative ? "v" : "V";
}
break;
case "m":
case "l":
case "t":
maxValues = 2;
repeatedType =
typeLc !== "t" ? (isRelative ? "l" : "L") : isRelative ? "t" : "T";
/**
* first starting point should be absolute/uppercase -
* unless it adds relative linetos
* (facilitates d concatenating)
*/
if (typeLc === "m") {
if (i == 0) {
type = "M";
}
}
break;
case "s":
case "q":
maxValues = 4;
repeatedType =
typeLc !== "q" ? (isRelative ? "s" : "S") : isRelative ? "q" : "Q";
break;
case "c":
maxValues = 6;
repeatedType = isRelative ? "c" : "C";
break;
case "a":
maxValues = 7;
repeatedType = isRelative ? "a" : "A";
break;
// z closepath
default:
maxValues = 0;
}
// if string contains repeated shorthand commands - split them
const arrayChunks = (array, chunkSize = 2) => {
let chunks = [];
for (let i = 0; i < array.length; i = chunkSize) {
let chunk = array.slice(i, i chunkSize);
chunks.push(chunk);
}
return chunks;
};
chunks = arrayChunks(values, maxValues);
// add 1st/regular command
let chunk0 = chunks.length ? chunks[0] : [];
pathData.push({
type: type,
values: chunk0
});
// add repeated commands
if (chunks.length > 1) {
for (let c = 1; c < chunks.length; c ) {
pathData.push({
type: repeatedType,
values: chunks[c]
});
}
}
}
return pathData;
}
function pathDataToD(pathData, decimals = -1) {
let d = "";
pathData.forEach((com, c) => {
if (decimals >= 0) {
com.values.forEach(function(val, v) {
pathData[c]["values"][v] = val.toFixed(decimals);
});
}
d = `${com.type}${com.values.join(" ")}`;
});
d = d.replaceAll(",", " ").replaceAll(" -", "-");
return d;
}
svg {
width: 20em;
border: 1px solid #ccc;
overflow: visible
}
<!--
sample path y @phrogz:
https://stackoverflow.com/questions/9677885/convert-svg-path-to-absolute-commands
http://phrogz.net/svg/convert_path_to_absolute_commands.svg
-->
<svg id="svg" viewBox="3 7 80 70">
<path id="path" d="" fill="#cccccc" stroke="#000000" stroke-width="1" stroke-linecap="butt"></path>
</svg>