How could I get each field in the circle to be the same size and clickable?

Viewed 122

For the following code: REPL

How could I make each field of the circle be the same size in a simple way? The fields on the top and bottom are currently bigger than the other ones for obvious reasons.

I could do some complicated calculations with position relative and absolute, but I wonder if there's an easy solution for my problem?

This is what it should look like: enter image description here

3 Answers

Here's a variable <svg> solution REPL
Unfortunately not every field is seperately clickable which I just saw you mentioned in a comment. That's a crucial fact which you might add to your question.

Edit: By changing to fill: none; actually every field is seperately clickable, thanks @herrstrietzel

<script>
    const size = 300
    let strokeWidth = 25
    let gap = 20

    $: circumference = Math.PI * (size - strokeWidth)
    $: r = size/2 - strokeWidth/2

    let pieces = [
        {stroke: 'teal'},
        {stroke: 'magenta'},
        {stroke: 'orange'},
    ]

    let color = '#1411DF'
</script>

<div style:width="{size}px">
    <svg width={size} height={size} style="transform: rotate({-90+(gap/2/r/Math.PI*180)}deg)">
        {#each pieces as piece, index}
        {@const ownLength = circumference / pieces.length - gap}
        <circle r={r}
                        cx={size/2}
                        cy={size/2}
                        style:stroke-width={strokeWidth}
                        style:stroke={piece.stroke}
                        style:stroke-dasharray="{ownLength} {circumference}"
                        style="fill: none; transform-origin: center;"
                        style:transform="rotate({index * 360 / pieces.length}deg)"
                        on:click="{() => console.log(piece.stroke)}"
                        />
        {/each}
    </svg>

    <input type="color" bind:value={color}>
    <button on:click={() => pieces = [...pieces, {stroke: color}]}>
        add piece
    </button>

    <label>
        stroke-width:
        <input type="range" bind:value={strokeWidth} min="1" max={size/5}>
        {strokeWidth}
    </label>

    <label>
        gap:
        <input type="range" bind:value={gap} min="1" max={size/5}>
        {gap}
    </label>

</div>

<style>
    circle:hover {
        stroke: black !important;
    }
    div {
        margin: 0 auto;
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    svg {
        display: block;
        margin-bottom: 2rem;
    }
    label {
        width: 100%;
        font-size: .9rem;
        padding: 1rem 0;
    }
    input {
        padding: 0;
    }
</style>

And inspired by H.B.'s answer a different solution with path elements REPL

<script>
    let colors = ['teal', 'DarkOrchid', 'orange']
    let color = '#2E15D1'

    const size = 300
    let strokeWidth = 10
    let gap = 10 // degree

    $: r = size/2 - strokeWidth/2

    $: deg = (180 - (360 / colors.length) + gap) / 2
    $: x = r * Math.cos(rad(deg))
    $: y = r * Math.sin(rad(deg))

    function rad(angle) {
        return angle * Math.PI / 180;
    } 
</script>

<div style:width="{size}px">
    <svg width={size} height={size} viewbox="{-size/2} {-size/2} {size} {size}" xmlns="http://www.w3.org/2000/svg">
        {#each colors as color, index}
        <path d="M -{x} -{y} A {r} {r} 0 0 1 {x} -{y}"
                    style="fill: none;"
                    style:stroke={color}
                    style:stroke-width="{strokeWidth}"
                    style:transform="rotate({360 / colors.length * index}deg)"
                    on:click="{() => console.log(color)}"
                    />
        {/each}
<!--        <circle cx="0" cy="0" {r} fill="none" stroke="black"></circle> -->
<!--        <circle cx="0" cy="0" r="2"></circle> -->
    </svg>

    <input type="color" bind:value={color}>
    <button on:click={() => colors = [...colors, color]}>
        add piece
    </button>

    <label>
        stroke-width:
        <input type="range" bind:value={strokeWidth} min="1" max={size/5}>
        {strokeWidth}
    </label>

    <label>
        gap:
        <input type="range" bind:value={gap} min="1" max={(360/colors.length)-1}>
        {gap}°
    </label>

</div>

<style>
    div {
        margin: 0 auto;
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    svg {
        margin: 2rem;
        /*      transform: rotate(180deg); */
    }
    path:hover {
        stroke: black !important;
    }
    input {
        padding: 0;
        margin: .4rem;
    }
    label {
        display: grid;
        align-items: center;
        grid-template-columns: 1fr max-content 1fr;
        font-size: .9rem;
        white-space: nowrap;
    }
</style>

I could do some complicated calculations with position relative and absolute, but I wonder if there's an easy solution for my problem?

I'm afraid there is no solution completely without calculations, but at least this one is not complicated.

You can use conic-gradient and split it by degrees (in my example by 60deg) but the corners tend to be too pixelated. You can also use repeating-conic-gradient.

div {
  position: relative;
  width: 200px;
  height: 200px;
  border-radius: 100%;
  background: conic-gradient( 
     /* per 60deg - 5*2deg for white space */
     white 5deg,
     red 5deg 55deg, white 55deg 65deg,
     orange 65deg 115deg, white 115deg 125deg,
     blue 125deg 175deg, white 175deg 185deg,
     pink 185deg 235deg, white 235deg 245deg,
     gray 245deg 295deg, white 295deg 305deg,
     yellow 305deg 355deg, white 355deg 360deg
  );
}
div:after {
  content: '';
  position: absolute;
  left: 20px;
  top: 20px;
  width: 160px;
  height: 160px;
  background: white;
  border-radius: 100%;
}
<div></div>

You could use an SVG. You can create path elements with arcs and stroke them in the respective color.

Arcs are fairly complicated with many parameters:

 A rx ry x-axis-rotation large-arc-flag sweep-flag x y
 a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy

I would recommend reading the MDN documentation or the spec.

Example of a segment:

<svg width="320" height="320" xmlns="http://www.w3.org/2000/svg">
  <path d="M 50 50 a 50 50 0 0 1 50 0" stroke="black" stroke-width="20" fill="none"/>
</svg>


The easiest method is probably calculating the angles (start/end) and converting from polar to cartesian. The paths only need two commands: Absolute move to start location & absolute arc to end location.

Full example with clickable segments and keyboard support for additional accessibility (could still be improved, e.g. by supplying screen reader texts):

<script>
    let radius = 150;
    let stroke = 20;
    let gap = 5;
    let segments = [
        '#ff0000',
        '#00ff00',
        '#0000ff',
    ];
    
    function getCoordinates(i, gap) {
        const angleDelta = 360 / segments.length;
        
        const start = polarToCartesian(radius, i * angleDelta + gap);
        const end = polarToCartesian(radius, i * angleDelta + angleDelta);

        return { start, end };
    }
    
    const polarToCartesian = (r, angle) => {
        return {
            x: r * Math.cos(rad(angle)),
            y: r * Math.sin(rad(angle)),
        }
    }
    const rad = x => x * Math.PI / 180;
    const onClick = i => alert('Segment ' + i);
</script>

<div>
    <svg width="320" height="320" xmlns="http://www.w3.org/2000/svg">
        <g transform="translate(160, 160) rotate({-90 - gap/2})">
            {#each segments as segment, i (i)}
                {@const { start, end } = getCoordinates(i, gap)}
                <path d="M {start.x} {start.y}
                         A {radius} {radius} 0 0 1 {end.x} {end.y}"
                      stroke={segment} stroke-width={stroke} fill="none"
                      tabindex={0}
                      on:keydown={e => { if (e.key == 'Enter') onClick(i); }}
                      on:click={() => onClick(i)} />
            {/each}
        </g>
    </svg>
</div>

REPL

Related