Created
January 31, 2020 15:13
-
-
Save JTBrinkmann/67af1a44b93a43febca7811dfbe1c850 to your computer and use it in GitHub Desktop.
small tool to resize SVGs internal coordinates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<style> | |
* { | |
box-sizing: border-box; | |
} | |
html, | |
body { | |
margin: 0; | |
height: 100%; | |
} | |
body { | |
background: #afa; | |
padding: 2%; | |
font-family: sans-serif; | |
} | |
#wrapper { | |
flex: 0 0 auto; | |
display: flex; | |
flex-direction: column; | |
height: 100%; | |
padding: 10px 40px; | |
border-radius: 10px; | |
background: white; | |
} | |
#wrapper > * { | |
flex: 0 0 0; | |
} | |
textarea { | |
display: block; | |
width: 100%; | |
min-height: 20em; | |
flex: 1 1 auto !important; | |
margin-top: 1em; | |
background: #f4fff4; | |
box-shadow: 1px 1px 3px #aaa; | |
border: none; | |
padding: 1em; | |
} | |
.slider { | |
display: flex; | |
} | |
.slider > input { | |
flex: 1 0 auto; | |
} | |
.slider > span { | |
width: 6em; | |
text-align: right; | |
} | |
#buttons { | |
display: flex; | |
} | |
#buttons > button { | |
flex: 1 0 auto; | |
height: 3em; | |
font-size: 2em; | |
} | |
body:not(.dragging-file) #dragndrop-notice { | |
display: none; | |
} | |
#dragndrop-notice { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
z-index: 2; | |
background: rgba(255, 255, 255, 0.8); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
pointer-events: none; | |
} | |
</style> | |
<div id="wrapper"> | |
<h1>SVG Scaling Tool</h1> | |
<span id="instructions"> | |
<bold>Drag'n'drop your .svg file onto this page</bold> or paste it's content | |
into the field below. You can also just enter the SVG path directly (e.g. | |
copied from the database) | |
</span> | |
<textarea id="$input"></textarea> | |
<p id="options"> | |
<label class="slider"> | |
<span class="label">width: </span> | |
<input type="range" id="$width" min="1" max="1000000" /> | |
<span id="$widthSpan" class="slider-val"></span> | |
</label> | |
<label class="slider"> | |
<span class="label">height: </span> | |
<input type="range" id="$height" min="1" max="1000000" /> | |
<span id="$heightSpan" class="slider-val"></span> | |
</label> | |
</p> | |
<p id="buttons"> | |
<button id="$copyBtn">copy</button> | |
| |
<button id="$downloadBtn">download</button> | |
</p> | |
<div id="dragndrop-notice"> | |
Drop the file to read it! | |
</div> | |
</div> | |
<script> | |
/* | |
// get commandParams by going to https://svgwg.org/specs/paths/ | |
// and executing the following in the browser devtool's console: | |
// (the `copy(…)` at the end will copy the result to the clipboard) | |
commandParams = {} | |
for (const table of $$(".PathDataTable")) { | |
const rows = table.querySelectorAll("tr:not(:first-child)") | |
for (const row of rows) { | |
const commands = Array.from(row.children[0].querySelectorAll("strong")).map(c => c.textContent) | |
const rawParams = row.children[2].textContent | |
const params = rawParams | |
.replace(/[\(\)\+\n]|none/g, '') | |
.split(/\s+/g) | |
.filter(p => p != "") | |
const repeatedParams_ = /\(.*?\)\+$|[\w\-]+\+/.exec(rawParams) | |
const repeatedParams = repeatedParams_ ? | |
repeatedParams_[0] | |
.replace(/[\(\)\+\n]|none/g, '') | |
.split(/\s+/g) | |
.filter(p => p != "") | |
.length | |
: 0 | |
for (const command of commands) { | |
commandParams[command] = {params, repeatedParams} | |
} | |
// console.log(commands.join(", "), {params, repeatedParams}) | |
} | |
} | |
copy(JSON.stringify(commandParams)) | |
*/ | |
const commandParams = { | |
M: { params: ["x", "y"], repeatedParams: 2 }, | |
m: { params: ["x", "y"], repeatedParams: 2 }, | |
Z: { params: [""], repeatedParams: 0 }, | |
z: { params: [""], repeatedParams: 0 }, | |
L: { params: ["x", "y"], repeatedParams: 2 }, | |
l: { params: ["x", "y"], repeatedParams: 2 }, | |
H: { params: ["x"], repeatedParams: 1 }, | |
h: { params: ["x"], repeatedParams: 1 }, | |
V: { params: ["y"], repeatedParams: 1 }, | |
v: { params: ["y"], repeatedParams: 1 }, | |
C: { params: ["x1", "y1", "x2", "y2", "x", "y"], repeatedParams: 6 }, | |
c: { params: ["x1", "y1", "x2", "y2", "x", "y"], repeatedParams: 6 }, | |
S: { params: ["x2", "y2", "x", "y"], repeatedParams: 4 }, | |
s: { params: ["x2", "y2", "x", "y"], repeatedParams: 4 }, | |
Q: { params: ["x1", "y1", "x", "y"], repeatedParams: 4 }, | |
q: { params: ["x1", "y1", "x", "y"], repeatedParams: 4 }, | |
T: { params: ["x", "y"], repeatedParams: 2 }, | |
t: { params: ["x", "y"], repeatedParams: 2 }, | |
A: { | |
params: [ | |
"rx", | |
"ry", | |
"x-axis-rotation", | |
"large-arc-flag", | |
"sweep-flag", | |
"x", | |
"y", | |
], | |
repeatedParams: 0, | |
}, | |
a: { | |
params: [ | |
"rx", | |
"ry", | |
"x-axis-rotation", | |
"large-arc-flag", | |
"sweep-flag", | |
"x", | |
"y", | |
], | |
repeatedParams: 0, | |
}, | |
R: { params: ["x1", "y1", "x2", "y2", "x", "y"], repeatedParams: 2 }, | |
r: { params: ["x1", "y1", "x2", "y2", "x", "y"], repeatedParams: 2 }, | |
B: { params: ["angle"], repeatedParams: 1 }, | |
b: { params: ["angle"], repeatedParams: 1 }, | |
} | |
const coordinateParams = ["x", "y", "x1", "y1", "x2", "y2", "rx", "ry"] // non-coordinate params: 'x-axis-rotation', 'large-arc-flag', 'sweep-flag', 'angle' | |
const scalePath = (scaleFactor, d) => { | |
const regexp = /(z|[a-yA-Z]+)|(\d*\.\d+|\d+)/g | |
let paramNum = 0 | |
let lastCommand = "M" | |
return d.replace(regexp, (_, command, arg) => { | |
if (command) { | |
paramNum = 0 | |
lastCommand = command | |
return command | |
} else { | |
if (!(lastCommand in commandParams)) { | |
console.warn(`${lastCommand} is an unknown path command`) | |
debugger | |
paramNum++ | |
return arg | |
} | |
const firstParamCount = commandParams[lastCommand].params.length | |
const repeatedParamCount = commandParams[lastCommand].repeatedParams | |
const nonrepeatedParamCount = firstParamCount - repeatedParamCount | |
const effectiveParamNum = | |
paramNum < firstParamCount | |
? paramNum | |
: ((paramNum - nonrepeatedParamCount) % repeatedParamCount) + | |
nonrepeatedParamCount | |
const effectiveParam = | |
commandParams[lastCommand].params[effectiveParamNum] | |
paramNum++ | |
const isCoordinate = coordinateParams.includes(effectiveParam) | |
if (isCoordinate) { | |
return `${parseFloat(arg) * scaleFactor}` | |
} else { | |
return arg | |
} | |
} | |
}) | |
} | |
const updateScaleSlider = () => { | |
$width.value = width | |
$widthSpan.textContent = width.toLocaleString() | |
$height.value = height | |
$heightSpan.textContent = height.toLocaleString() | |
} | |
const isSvg = (str) => /^\s*</.test(str) | |
const scaleInput = (scaleFactor) => { | |
const scaleFloat = (coord) => { | |
return /^\d*\.\d+|\d+$/.test(coord) | |
? parseFloat(coord) * scaleFactor | |
: coord | |
} | |
if (isSvg(inputValue)) { | |
// SVG mode | |
$input.value = inputValue | |
.replace(/\bd="(.*?)"/g, (_, d) => `d="${scalePath(scaleFactor, d)}"`) | |
.replace( | |
/\b(x|y|width|height)="(\d*\.\d+|\d+)"/g, | |
(_, attr, val) => `${attr}="${scaleFloat(val)}"` | |
) | |
.replace( | |
/\b(points|viewBox)="(.*?)"/g, | |
(_, attr, points) => | |
`${attr}="${points.replace(/\d*\.\d+|\d+/g, scaleFloat)}"` | |
) | |
//.replace(/\b(points|viewBox|x|y|width|height)="(.*?)"/g, (_, attr, points) => `${attr}="${points.split(/\s+|,/g).map(scaleFloat).join(' ')}"`) | |
} else { | |
$input.value = scalePath(scaleFactor, inputValue) | |
} | |
width = Math.round(srcWidth * scaleFactor) | |
height = Math.round(srcHeight * scaleFactor) | |
updateScaleSlider() | |
} | |
const debounce = (cb, lock, delay) => { | |
return (...args) => { | |
if (scaleInputLock.id) { | |
clearTimeout(scaleInputLock.id) | |
} | |
scaleInputLock.id = setTimeout(() => cb(...args), delay) | |
} | |
} | |
/** width / height */ | |
let aspectRatio | |
let width, height, srcWidth, srcHeight | |
let inputValue | |
const onInputChanged = () => { | |
inputValue = $input.value | |
if (isSvg(inputValue)) { | |
const viewBox = /\bviewBox="\s*(\d*\.\d+|\d+)\s+(\d*\.\d+|\d+)\s+(\d*\.\d+|\d+)\s+(\d*\.\d+|\d+)\s*"/.exec( | |
inputValue | |
) | |
if (viewBox != null) { | |
srcWidth = width = Math.round(parseFloat(viewBox[3]) - parseFloat(viewBox[1])) // x2 - x1 | |
srcHeight = height = Math.round(parseFloat(viewBox[4]) - parseFloat(viewBox[2])) // y2 - y1 | |
aspectRatio = width / height | |
} else { | |
alert("SVGs without a viewBox attribute are unsupported!") | |
} | |
updateScaleSlider() | |
} | |
} | |
$input.addEventListener("input", onInputChanged) | |
if ($input.value != "") { | |
onInputChanged() | |
} | |
const scaleInputLock = {} | |
$width.addEventListener( | |
"input", | |
debounce( | |
() => scaleInput(parseFloat($width.value) / srcWidth), | |
scaleInputLock, | |
10 | |
) | |
) | |
$height.addEventListener( | |
"input", | |
debounce( | |
() => scaleInput(parseFloat($height.value) / srcHeight), | |
scaleInputLock, | |
10 | |
) | |
) | |
// enable file drag'n'drop | |
document.body.addEventListener("dragover", (e) => { | |
e.stopPropagation() | |
e.preventDefault() | |
document.body.classList.add("dragging-file") | |
e.dataTransfer.dropEffect = "copy" | |
}) | |
document.body.addEventListener("drop", (e) => { | |
e.stopPropagation() | |
e.preventDefault() | |
document.body.classList.remove("dragging-file") | |
const file = e.dataTransfer.files[0] | |
if (file != null) { | |
if (file.type != "image/svg+xml" && !file.type.startsWith('text/')) { | |
console.warn("file type:", file.type) | |
alert("invalid file format, must be a text file!") | |
} else { | |
var reader = new FileReader() | |
reader.onload = (e2) => { | |
$input.value = e2.target.result | |
onInputChanged() | |
} | |
reader.readAsText(file) | |
} | |
} | |
if (e.dataTransfer.files.length > 1) { | |
alert("dropping multiple files is not supported") | |
} | |
}) | |
document.body.addEventListener("dragleave", (e) => { | |
e.stopPropagation() | |
e.preventDefault() | |
document.body.classList.remove("dragging-file") | |
}) | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment