I'm making a vue.js app which uses tons of different icons, so I decided to make a small icons builder in node.js to standardize their use, and it also "crops" each svg so it fits its parent (using the viewbox attribute).
Though I stumbled upon an issue, scaling icons to fit a square makes more "squary" look larger than round ones, so I'd like to programatically scale them in a circle :
* {
box-sizing: border-box;
}
.container {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 2rem;
}
.title {
font-family: sans-serif;
font-weight: 600;
font-size: 24px;
margin: 0;
}
.grid {
display: flex;
gap: 1.5rem;
}
.group {
display: flex;
flex-direction: column;
gap: 1rem;
text-align: center;
}
.bg {
width: 80px;
height: 80px;
background-color: lightgray;
}
.zone {
border: 2px solid red;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.zone--wanted {
border-radius: 50%;
}
.icon {
width: 100%;
height: 100%;
background-color: coral;
}
.icon--rounded {
border-radius: 20%;
}
.icon--circle {
border-radius: 50%;
}
.icon-scaled--rounded {
width: 80%;
height: 80%;
}
.icon-scaled--square {
width: 70.71068%;
height: 70.71068%;
}
<div class="container">
<p class="title">What I have :</p>
<div class="grid">
<div class="bg">
<div class="zone">
<div class="icon"></div>
</div>
</div>
<div class="bg">
<div class="zone">
<div class="icon icon--rounded"></div>
</div>
</div>
<div class="bg">
<div class="zone">
<div class="icon icon--circle"></div>
</div>
</div>
</div>
<p class="title">What I want :</p>
<div class="grid">
<div class="bg">
<div class="zone zone--wanted">
<div class="icon icon-scaled--square"></div>
</div>
</div>
<div class="bg">
<div class="zone zone--wanted">
<div class="icon icon--rounded icon-scaled--rounded"></div>
</div>
</div>
<div class="bg">
<div class="zone zone--wanted">
<div class="icon icon--circle"></div>
</div>
</div>
</div>
</div>
Do you guys have an idea on how to do this ?
Here's a very primitive idea: since it is easy to get the horisontal and vertical extent of an element, if we rotate it and sample the width and height at each rotation we can get a decent estimate of the maximum diameter d
. Scaling by r/d
, where r
is the diameter of the circle you want to put it in, should work reasonably well. This works particularly well on your examples, since we know that the maximum diameter is at 45 degrees rotation, so we don't need many samples; one suffices. If you have an odd shape where the diameter is at, say, 10deg, you will need more samples to approximate the maximum. If you undersample, the SVG element will peek out from the circle a bit.
function unitCircleScale(element, angles=4) {
if (typeof angles == "number") {
angles = Array.from({length: angles}, (_, i) => i * 90 / angles)
}
const initTransform = element.style.transform
const transform = initTransform.replace(/rotate\([^)]*\)\s*;?/, '')
const sizes = []
for (const angle of angles) {
element.style.transform = `rotate(${angle}deg) ${transform}`
const bbox = element.parentNode.getBBox()
sizes.push(bbox.width, bbox.height)
}
element.style.transform = initTransform
return 2 / Math.max(...sizes)
}
const [rect, square, circle] = document.querySelectorAll("rect, circle")
// to fit into a 100-diameter (50-radius) circle, scale by:
// rough idea, at just 0deg and 45deg
console.log("rect at 2 samples:", 50 * unitCircleScale(rect, 2))
// better idea, with more samples
console.log("rect at 16 samples:", 50 * unitCircleScale(rect, 16))
// 45deg is ideal for a square, test just that
console.log("square at 45deg:", 50 * unitCircleScale(square, [45]))
// circle is same however you turn it, just do a single sample (i.e. 0deg)
console.log("circle at 1 sample:", 50 * unitCircleScale(circle, 1))
<svg width="400" height="120" xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="150" height="50" x="10" y="10" style="fill:none; stroke:red" />
</g>
<g>
<rect width="100" height="100" x="170" y="10" style="fill:none; stroke:blue" />
</g>
<g>
<circle cx="330" cy="60" r="50" style="fill:none; stroke:green" />
</g>
Sorry, your browser does not support inline SVG.
</svg>
Note that this will not be very fast, since it alternates changing properties then reading bounding box, which means it will force a re-layout for each iteration. Ideally, you would want to calculate the ideal scaling once, and cache it.