SVG: How to Divide a Circle into Equal Segments of 12

78 views Asked by At

ignor the barI have been experimenting with SVG recently and to be more specific on creating a 12-part wheel.I came across a code snippet in a previous post that fit my needs but consists of 6 parts and i need 12 segments.My problem is that i can not understand the proccess of the division.

Below is the snippet that I'm trying to understand better and would appreciate some explanation or tips on how it worksor provide some insights on how to optimize it (adding 6 more segments).

.frag {
  fill: orange;
}

.center {
  fill: tomato;
}
<svg width="500" height="500" viewBox="-2 -2 202 203" shape-rendering="geometricPrecision">
  <a xlink:href="#">
    <path class="frag" d="M100,100 v-100 a100,100 1 0,1 86.6025,50" />
    <text x="135" y="42.5" text-anchor="middle">1</text>
  </a>
  <a xlink:href="#">
    <path class="frag" d="M100,100 l86.6025,-50 a100,100 1 0,1 0,100" />
    <text x="170" y="105" text-anchor="middle">2</text>
  </a>
  <a xlink:href="#">
    <path class="frag" d="M100,100 l86.6025,50 a100,100 1 0,1 -86.6025,50" />
    <text x="135" y="170" text-anchor="middle">3</text>
  </a>
  <a xlink:href="#">
    <path class="frag" d="M100,100 v100 a100,100 1 0,1 -86.6025,-50" />
    <text x="65" y="170" text-anchor="middle">4</text>
  </a>
  <a xlink:href="#">
    <path class="frag" d="M100,100 l-86.6025,50 a100,100 1 0,1 0,-100" />
    <text x="27.5" y="105" text-anchor="middle">5</text>
  </a>
  <a xlink:href="#">
    <path class="frag" d="M100,100 l-86.6025,-50 a100,100 1 0,1 86.0025,-50" />
    <text x="65" y="42.5" text-anchor="middle">6</text>
  </a>
  <a xlink:href="#">
    <path class="center" d="M100,100 v-50 a50,50 1 0,1 0,100 a50,50 1 0,1 0,-100" />
  </a>
</svg>

3

There are 3 answers

2
Danny '365CSI' Engelman On

Paste each of your d-path values in https://yqnn.github.io/svg-path-editor/# to see what they draw.
Its is going to take some effort to change those 6 segments into 12

Easiest with a libary

If its just about drawing 12 segments you can use the native JavaScript 999 Bytes <pie-chart> Web Component I wrote: https://pie-meister.github.io/

<script src="https://pie-meister.github.io/PieMeister.min.js"></script>

<pie-chart stroke-width="200" pull="-85" stroke="red,gold,blue,red,gold,blue,red,gold,blue,red,gold,blue">
    <style>
      text {
        font-size: 4em;
        text-anchor: middle;
      }
      [part="slice2"] text{
        fill:gold;
        font-size: 6em;
        font-weight:bold;
      }
    </style>
    <slice size="30">1</slice>
    <slice size="30">2</slice>
    <slice size="30" pull="40">3</slice>
    <slice size="30">4</slice>
    <slice size="30">5</slice>
    <slice size="30">6</slice>
    <slice size="30">7</slice>
    <slice size="30">8</slice>
    <slice size="30">9</slice>
    <slice size="30">10</slice>
    <slice size="30">11</slice>
    <slice size="30">12</slice>
  </pie-chart>

Drawing your own circle

You can draw you own circle segments, using pathLength and stroke-dasharray

Some minor Math to positional the label <text>

svg {
  height:180px; background:pink
}
<svg-circle id="C1" segments="12" width="600"></svg-circle>
<script>
  customElements.define("svg-circle", class extends HTMLElement {
    static get observedAttributes(){
        return ["segments"];
    }
    attributeChangedCallback(name,oldValue,newValue) {
        this.render(~~newValue)
    }
    render(segments = this.getAttribute("segments")){
      let colors = (this.getAttribute("colors") || "red,yellow,blue").split(",");
      let width = this.getAttribute("width") || 20;
      let r = 50;
      let labels = [];
      let circles = Array(segments).fill(0).map((_, idx) => {
        let stroke = colors.pop();
        colors.unshift(stroke);
        let circle = `<circle cx="50%" cy="50%" r="${r}%" fill="none" id="circle${idx}"` +
          ` stroke="${stroke}" stroke-width="${width}"` +
          ` stroke-dashoffset="${idx}"` +
          ` stroke-dasharray="1 ${segments-1}" pathLength="${segments}" mask="url(#shape)"/>`;
        let angleRadians = (2 * Math.PI / segments) * (idx + 0.5);
        let R = 200;
        let x = R * Math.cos(angleRadians) + 300;
        let y = R * Math.sin(angleRadians) + 300;
        labels.push(`<text stroke="black" fill="gold" font-size="3em" stroke-width="2"
                           x="${x}" y="${y+10}" text-anchor="middle">${idx+1}</text>`)
        return circle;
      }).join("");
      let mask = `<mask id="shape"><circle cx="50%" cy="50%" r="48%" fill="white" stroke="white"/></mask>`;
      let svg = `<svg viewBox="0 0 600 600">` + mask + circles + labels.join("") + `</svg>`;
      if(this.isConnected) this.innerHTML = svg;
    }
  });
</script>
<input type="range" min="1" max="16" oninput="C1.setAttribute('segments',this.value);return false">

4
somethinghere On

To do this, all you need is some basic geometry math (break out your sinusees, cosinusses, and... thats it really!). Basically all you want to do is split your full circle arc into segments (which is 360deg or 1rad, and since internally computations happen with radians, we can say 1rad = Math.PI * 2), then compute their start and end point, and then use the arc function of an SVG path to render the curve between those points. I added some comments explaining how this setup works to construct the paths in this snippet:

function randomRGB(){
    
  const rgb = [];
  
  while( rgb.length < 3 ) rgb.push( Math.floor(Math.random() * 255) );
  
  return `rgb(${rgb.join(',')}`;
  
}
function renderPies(){
    
  let amount = input.valueAsNumber;
    
  if( isNaN( amount ) || amount < 2 ) amount = 2;
  
  const cx = .5;
  const cy = .5;
  const r = .5;
  const step = RADIAN / amount;
  
  // Ensure the amount of children matches the pie pieces
  while( svg.childElementCount > amount ) [...svg.children].pop().remove();
  while( svg.childElementCount < amount ) svg.appendChild( document.createElementNS( SVG_NS, 'path' ) );
  
  // Update every path
  for( let i = 0; i < amount; i++ ){
    
    const sa = i / amount * RADIAN;
    const ea = (i+1) / amount * RADIAN;
    const path = svg.children[i];
    const sx = Math.cos( sa ) * r + cx; // Start X
    const sy = Math.sin( sa ) * r + cy; // Start Y
    const ex = Math.cos( ea ) * r + cx; // End X
    const ey = Math.sin( ea ) * r + cy; // End Y
    const la = step > Math.PI ? 1 : 0; // Is the total arc bigger than 180?
    const sw = 1; // We are always moving clockwise, so sweep is clockwise
    const d = [
      `M ${cx} ${cy}`, // Center of the circle
      `L ${sx} ${sy}`, // Start point of the arc
      `A ${r} ${r} 0 ${la} ${sw} ${ex} ${ey}`, // Arc of same x and y radius, either the large arc, and sweep clockwise. End up in ex and ey.
      `z` // Close the path
    ];
    
    path.setAttribute( 'd', d.join( '' ) );
    path.setAttribute( 'fill', randomRGB() );
    
  }
  
}

const RADIAN = Math.PI * 2;
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.querySelector( 'svg' );
const input = document.querySelector( 'input' );

renderPies();

input.addEventListener( 'input', renderPies );
svg { width: 80vmin; height: 80vmin; border: 1px solid red;  }
<svg viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>
<input type="number" value="10" min="2" />

For constructing paths in SVG it might be helpful to read up in the MDN Web Docs which tries its best to explain how to use paths, arcs, points etc...

https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths

Even adding the labels isn't to hard once you know the math:

function randomRGB(){
    
  const rgb = [];
  
  while( rgb.length < 3 ) rgb.push( Math.floor(Math.random() * 255) );
  
  return `rgb(${rgb.join(',')}`;
  
}
function renderPies(){
    
  let amount = input.valueAsNumber;
    
  if( isNaN( amount ) || amount < 2 ) amount = 2;
  
  const cx = .5;
  const cy = .5;
  const r = .5;
  const step = RADIAN / amount;
  
  // Ensure the amount of children matches the pie pieces
  while( svg.childElementCount > amount ) [...svg.children].pop().remove();
  while( svg.childElementCount < amount ) svg.appendChild( document.createElementNS( SVG_NS, 'g' ) );
  
  // Update every path
  for( let i = 0; i < amount; i++ ){
    
    const g = svg.children[i];
    const path = g.querySelector( 'path' ) || g.appendChild( document.createElementNS( SVG_NS, 'path' ) );
    const label = g.querySelector( 'text' ) || g.appendChild( document.createElementNS( SVG_NS, 'text' ) );
    const sa = i / amount * RADIAN;
    const ea = (i+1) / amount * RADIAN;
    const sx = Math.cos( sa ) * r + cx; // Start X
    const sy = Math.sin( sa ) * r + cy; // Start Y
    const ex = Math.cos( ea ) * r + cx; // End X
    const ey = Math.sin( ea ) * r + cy; // End Y
    const la = step > Math.PI ? 1 : 0; // Is the total arc bigger than 180?
    const sw = 1; // We are always moving clockwise, so sweep is clockwise
    const d = [
      `M ${cx} ${cy}`, // Center of the circle
      `L ${sx} ${sy}`, // Start point of the arc
      `A ${r} ${r} 0 ${la} ${sw} ${ex} ${ey}`, // Arc of same x and y radius, either the large arc, and sweep clockwise. End up in ex and ey.
      `z` // Close the path
    ];
    const rgb = new Array(3).fill(0).map(v => Math.floor(Math.random() * 255));
    
    path.setAttribute( 'd', d.join( '' ) );
    path.setAttribute( 'fill', `rgb(${rgb.join(',')})` );
    
    const ma = sa + (ea - sa) / 2; // Middle angle
    const mr = r / 3 * 2;
    const tx = Math.cos( ma ) * mr + cx;
    const ty = Math.sin( ma ) * mr + cy;
    const tcolor = rgb.reduce((r,v) => r+v, 0) > 255 * 3 / 2 ? 'black' : 'white';
    
    label.textContent = i;
    label.setAttribute( 'text-align', 'center' );
    label.setAttribute( 'alignment-baseline', 'middle' );
    label.setAttribute( 'x', tx );
    label.setAttribute( 'y', ty );
    label.setAttribute( 'font-size', .05 );
    label.setAttribute( 'fill', tcolor );
    
  }
  
}

const RADIAN = Math.PI * 2;
const SVG_NS = 'http://www.w3.org/2000/svg';
const svg = document.querySelector( 'svg' );
const input = document.querySelector( 'input' );

renderPies();

input.addEventListener( 'input', renderPies );
svg { width: 80vmin; height: 80vmin; border: 1px solid red;  }
<svg viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>
<input type="number" value="10" min="2" />

0
chrwahl On

I can understand why other answers use JavaScript to generate the SVG, but here you have an example in plain SVG.

First I create a part of a circle using a circle element with a stroke-dasharray. The circle is then used, using the <use> element and rotated. That is the basic explanation. You can see that the <use> element reference to the circle element by it's ID.

In your case you have <a> elements that link to something. I use the <a> for rotation and the <use> element at a child, so that the <use> element aka. circle follows the rotation. The text is a bit tricky, in that it first needs to be rotated 15 degrees and then translated 375 to end up in the middle of the circle part. To make the text sand upright, it then needs to be rotated back again.

<svg xmlns="http://www.w3.org/2000/svg"
  viewbox="0 0 1000 1000" width="200">
  <defs>
    <circle id="c1" stroke-width="250" fill="none" r="375"
    pathLength="360" stroke-dasharray="30 360"/>
  </defs>
  <g transform="translate(500 500) rotate(-90)" font-size="80"
    text-anchor="middle" dominant-baseline="middle">
    <a href="#1" transform="rotate(0)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(75)">1</text>
    </a>
    <a href="#2" transform="rotate(30)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(45)">2</text>
    </a>
    <a href="#3" transform="rotate(60)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(15)">3</text>
    </a>
    <a href="#4" transform="rotate(90)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(-15)">4</text>
    </a>
    <a href="#5" transform="rotate(120)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(-45)">5</text>
    </a>
    <a href="#6" transform="rotate(150)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(-75)">6</text>
    </a>
    <a href="#7" transform="rotate(180)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(-105)">7</text>
    </a>
    <a href="#8" transform="rotate(210)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(-135)">8</text>
    </a>
    <a href="#9" transform="rotate(240)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(-165)">9</text>
    </a>
    <a href="#10" transform="rotate(270)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(165)">10</text>
    </a>
    <a href="#11" transform="rotate(300)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(135)">11</text>
    </a>
    <a href="#12" transform="rotate(330)">
      <use href="#c1" stroke="orange"/>
      <text transform="rotate(15) translate(375 0) rotate(105)">12</text>
    </a>
    <a href="#center">
      <circle r="250" fill="tomato" />
    </a>
  </g>
</svg>