Home > OS >  How to make a semicircle loader using SVG?
How to make a semicircle loader using SVG?

Time:01-06

The result I want to achieve is this:

Goal

And what I achieved until now is this:

Current situation

What I cannot figure out is:

  1. How to round the corners of the lines in the semicircle
  2. How to add a perpendicular line at the end of the filled semicircle

Still need to figure this out

Can someone help?

The full code is here:

function App() {
  return <SemiCircleProgress percentage={70} />;
}

function SemiCircleProgress({ percentage }) {
    const strokeWidth = 25;
    const background = "#D9D9D9";
    const diameter = 400;

    const coordinateForCircle = diameter / 2;
    const radius = (diameter - 2 * strokeWidth) / 2;
    const circumference = Math.PI * radius;
    const semiCirclePercentage = percentage * (circumference / 100);

    return (
        <div className="semicircle-container">
            <svg
                width={diameter}
                height={diameter / 2}
                style={{ transform: "rotateY(180deg)", overflow: "hidden" }}
            >
                <linearGradient id="gradient">
                    <stop offset="0%" stopColor="#B8F1C6" />
                    <stop offset="100%" stopColor="#EBF8A1" />
                </linearGradient>

                <circle
                    cx={coordinateForCircle}
                    cy={coordinateForCircle}
                    r={radius}
                    fill="none"
                    stroke={background}
                    strokeWidth={8}
                    strokeDasharray={circumference}
                    style={{
                        strokeDashoffset: circumference
                    }}
                />
                <circle
                    cx={coordinateForCircle}
                    cy={coordinateForCircle}
                    r={radius}
                    fill="none"
                    stroke="url(#gradient)"
                    strokeWidth={strokeWidth}
                    strokeDasharray={circumference}
                    style={{
                        strokeDashoffset: semiCirclePercentage,
                        transition:
                            "stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s"
                    }}
                />
            </svg>
        </div>
    );
}


ReactDOM.render( < App / > , document.getElementById("root"));
body {
  background: #292929;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>

CodePudding user response:

You could look into the line endcap properties to get the rounded end; it's generally possible to use multiple <circle> and <line> elements together. But for a complex element where you want really specific control over the shape, often it works better to use a <path> element. It takes one attribute, "d", which is just a list of path segments: line, arc, and quadratic or bezier curves. You will be able to construct the shape you want out of a sequence of arcs and lines.

I don't think you will be able to avoid having to compute the coordinates for the end tick somehow, but you may find that putting things inside <g> (group) elements with "transform" attributes helps. This is a generally useful animation trick. You could, for example, put that perpendicular line in a <g> whose origin is at the center of the large semicircle, and rotate the <g> to move the line around the arc. Then at least the SVG is doing your trig for you.

mozdev is a pretty great resource for this stuff, this tutorial about paths is pretty clear: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths

You're making something animated, and I should point out that D3 (https://d3js.org) is a widely used tool for generating and animating complex SVGs and I strongly recommend you check it out. Sometimes you find a component for D3 that can be configured to make the exact thing you want, or close enough; and if not, at least it will help you with animation and transitions.

Finally, if you're stuck with SVG just staring at a text editor uncertain about how to accomplish your goal, you can open up a tool like InkScape and try drawing the thing you want. The visual feedback is immensely helpful. Then, you can look at the SVG code the tool generates and even if you aren't able to use it straight, you can see how it's structured – it will be a bit messy, but just understanding the selection of elements and attributes used can help you understand SVG better.

CodePudding user response:

The line caps are controled by the stroke-linecap attribute. The thing about this particular example is that it is rounded in one end and square in the other. Therefor I use a mask to create the shape of the "meter". The "value" is controlled by the black circle in the mask.

The line that follows the "meter" is placed in the middle and then rotated.

document.forms.f1.range.addEventListener('change', e => {
  let angle = 180 / 100 * e.target.value;
  document.getElementById('c1').setAttribute('stroke-dasharray', `180 ${angle}`);
  document.getElementById('l1').setAttribute('transform', `translate(50 50) rotate(${angle})`);
  document.getElementById('t1').textContent = e.target.value;
});
body {
  background: #292929;
}
<form name="f1">
  <input type="range" name="range" min="0"
    max="100" value="90" />
</form>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 60"
  width="400">
  <defs>
    <linearGradient id="gradient">
      <stop offset="0%" stop-color="#B8F1C6" />
      <stop offset="100%" stop-color="#EBF8A1" />
    </linearGradient>
    <mask id="m1">
      <circle cx="50" cy="50" r="43" fill="none"
        stroke="white" stroke-width="6" stroke-dasharray="180"
        pathLength="360" stroke-linecap="round"
        transform="rotate(184 50 50)"/>
      <circle id="c1" cx="50" cy="50" r="43" fill="none"
        stroke="black" stroke-width="8"
        stroke-dasharray="180 162" pathLength="360" />
    </mask>
  </defs>
  <circle cx="50" cy="50" r="43" fill="none"
    stroke="gray" stroke-width="2" stroke-dasharray="175 360"
    pathLength="360" stroke-linecap="round" transform="rotate(184 50 50)"/>
  <rect width="100" height="60" fill="url(#gradient)" mask="url(#m1)"/>
  <line id="l1" x1="-36" y1="0" x2="-50" y2="0" stroke="white"
    stroke-width=".3" transform="translate(50 50) rotate(162)" />
  <text id="t1" x="50" y="40" text-anchor="middle" fill="#EBF8A1"
    font-family="sans-serif">90</text>
  <text x="7" y="57" text-anchor="middle" fill="gray"
    font-family="sans-serif" font-size="5">0</text>
  <text x="93" y="57" text-anchor="middle" fill="gray"
    font-family="sans-serif" font-size="5">100</text>
</svg>

  • Related