Como calcular o caminho SVG para um arco (de um círculo)

191

Dado um círculo centrado em (200.200), raio 25, como faço para desenhar um arco de 270 graus a 135 graus e um arco que varia de 270 a 45 graus?

0 grau significa que está bem no eixo x (o lado direito) (o que significa que é a posição das 3 horas) 270 graus significa que está na posição de 12 horas e 90 significa que está na posição de 6 horas

De maneira mais geral, o que é um caminho para um arco para parte de um círculo com

x, y, r, d1, d2, direction

significado

center (x,y), radius r, degree_start, degree_end, direction
falta de polaridade
fonte

Respostas:

379

Expandindo a ótima resposta do @ wdebeaum, aqui está um método para gerar um caminho em arco:

function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
  var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;

  return {
    x: centerX + (radius * Math.cos(angleInRadians)),
    y: centerY + (radius * Math.sin(angleInRadians))
  };
}

function describeArc(x, y, radius, startAngle, endAngle){

    var start = polarToCartesian(x, y, radius, endAngle);
    var end = polarToCartesian(x, y, radius, startAngle);

    var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";

    var d = [
        "M", start.x, start.y, 
        "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
    ].join(" ");

    return d;       
}

usar

document.getElementById("arc1").setAttribute("d", describeArc(200, 400, 100, 0, 180));

e no seu html

<path id="arc1" fill="none" stroke="#446688" stroke-width="20" />

Demonstração ao vivo

opsb
fonte
9
Isso é super! Observe que a arcSweepvariável está realmente controlando o large-arc-flagparâmetro svg A. No código acima, o valor para o sweep-flagparâmetro é sempre zero. arcSweepprovavelmente deve ser renomeado para algo como longArc.
Steven Grosmark
Obrigado @PocketLogic, (finalmente) atualizei conforme sua sugestão.
Opsb
2
Realmente útil, obrigado. A única coisa que descobri é que a lógica largeArc não funciona se você usar ângulos negativos. Isso funciona de -360 a +360: jsbin.com/kopisonewi/2/edit?html,js,output
Xcodo 7/16
Percebo o motivo pelo qual padrão não define arcos da mesma maneira.
polkovnikov.ph
4
e não se esqueça de cortar uma pequena quantidade de comprimento do arco, como: endAngle - 0.0001caso contrário, o arco completo não será renderizado.
Saike
128

Você deseja usar o comando elíptico Arc . Infelizmente para você, isso exige que você especifique as coordenadas cartesianas (x, y) dos pontos inicial e final, em vez das coordenadas polares (raio, ângulo) que você possui, portanto, é necessário fazer algumas contas. Aqui está uma função JavaScript que deve funcionar (embora eu não a tenha testado) e espero que seja bastante auto-explicativa:

function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
  var angleInRadians = angleInDegrees * Math.PI / 180.0;
  var x = centerX + radius * Math.cos(angleInRadians);
  var y = centerY + radius * Math.sin(angleInRadians);
  return [x,y];
}

Quais ângulos correspondem a quais posições do relógio dependerão do sistema de coordenadas; apenas troque e / ou negue os termos sin / cos, conforme necessário.

O comando arc possui estes parâmetros:

rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y

Para o seu primeiro exemplo:

rx= ry= 25 e x-axis-rotation= 0, pois você deseja um círculo e não uma elipse. Você pode calcular as coordenadas iniciais (que você deve Mdedicar) e as coordenadas finais (x, y) usando a função acima, produzindo (200, 175) e aproximadamente (182.322, 217.678), respectivamente. Dadas essas restrições até agora, na verdade existem quatro arcos que podem ser desenhados, então as duas bandeiras selecionam uma delas. Acho que você provavelmente deseja desenhar um pequeno arco (significado large-arc-flag= 0), na direção do ângulo decrescente (significado sweep-flag= 0). Todos juntos, o caminho SVG é:

M 200 175 A 25 25 0 0 0 182.322 217.678

Para o segundo exemplo (supondo que você queira ir na mesma direção e, portanto, um arco grande), o caminho SVG é:

M 200 175 A 25 25 0 1 0 217.678 217.678

Novamente, eu não testei isso.

(editar 2016-06-01) Se, como @clocksmith, você está se perguntando por que eles escolheram essa API, dê uma olhada nas notas de implementação . Eles descrevem duas possíveis parametrizações de arco, "parametrização de ponto final" (a que eles escolheram) e "parametrização central" (que é como o que a pergunta usa). Na descrição da "parametrização do terminal", eles dizem:

Uma das vantagens da parametrização do ponto de extremidade é que ela permite uma sintaxe de caminho consistente na qual todos os comandos de caminho terminam nas coordenadas do novo "ponto atual".

Então, basicamente, é um efeito colateral dos arcos serem considerados parte de um caminho maior, em vez de seu próprio objeto separado. Suponho que, se o seu renderizador SVG estiver incompleto, ele poderá pular qualquer componente do caminho que não saiba como renderizar, desde que saiba quantos argumentos eles levam. Ou talvez ele permita a renderização paralela de diferentes pedaços de um caminho com muitos componentes. Ou talvez tenham feito isso para garantir que os erros de arredondamento não se acumulem ao longo de um caminho complexo.

As notas de implementação também são úteis para a pergunta original, pois possuem mais pseudocódigo matemático para a conversão entre as duas parametrizações (o que eu não percebi quando escrevi essa resposta pela primeira vez).

wdebeaum
fonte
1
parece ser bom, vai tentar a seguir. o fato de que, se for "mais" do arco (quando o arco for mais da metade do círculo) e large-arc-flagtiver que ser alternado de 0 a 1, poderá ocorrer algum erro.
polaridade
ugh, por que essa é a API para svg arcs?
clocksmith
16

Modifiquei levemente a resposta do opsb e fiz o preenchimento de suporte para o setor do círculo. http://codepen.io/anon/pen/AkoGx

JS

function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
  var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;

  return {
    x: centerX + (radius * Math.cos(angleInRadians)),
    y: centerY + (radius * Math.sin(angleInRadians))
  };
}

function describeArc(x, y, radius, startAngle, endAngle){

    var start = polarToCartesian(x, y, radius, endAngle);
    var end = polarToCartesian(x, y, radius, startAngle);

    var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";

    var d = [
        "M", start.x, start.y, 
        "A", radius, radius, 0, arcSweep, 0, end.x, end.y,
        "L", x,y,
        "L", start.x, start.y
    ].join(" ");

    return d;       
}

document.getElementById("arc1").setAttribute("d", describeArc(200, 400, 100, 0, 220));

HTML

<svg>
  <path id="arc1" fill="orange" stroke="#446688" stroke-width="0" />
</svg>
Ahtenus
fonte
5
O link codepen não parece trabalhar para mim (Chrome)
Ray Hulha
O arco é desenhado fora dos limites do SVG. Aumentar a altura e mudança de SVG centerX, centerYa 100, por exemplo.
A.Akram
Você também pode definir uma caixa de exibição explicitamente no elemento svg pai. Por exemploviewBox="0 0 500 500"
Håken Lid
7

Esta é uma pergunta antiga, mas achei o código útil e me poupou três minutos de pensamento :) Então, estou adicionando uma pequena expansão à resposta do @opsb .

Se você deseja converter esse arco em uma fatia (para permitir preenchimento), podemos modificar um pouco o código:

function describeArc(x, y, radius, spread, startAngle, endAngle){
    var innerStart = polarToCartesian(x, y, radius, endAngle);
  	var innerEnd = polarToCartesian(x, y, radius, startAngle);
    var outerStart = polarToCartesian(x, y, radius + spread, endAngle);
    var outerEnd = polarToCartesian(x, y, radius + spread, startAngle);

    var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";

    var d = [
        "M", outerStart.x, outerStart.y,
        "A", radius + spread, radius + spread, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
        "L", innerEnd.x, innerEnd.y, 
        "A", radius, radius, 0, largeArcFlag, 1, innerStart.x, innerStart.y, 
        "L", outerStart.x, outerStart.y, "Z"
    ].join(" ");

    return d;
}

function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
  var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;

  return {
    x: centerX + (radius * Math.cos(angleInRadians)),
    y: centerY + (radius * Math.sin(angleInRadians))
  };
}

var path = describeArc(150, 150, 50, 30, 0, 50)
document.getElementById("p").innerHTML = path
document.getElementById("path").setAttribute('d',path)
<p id="p">
</p>
<svg width="300" height="300" style="border:1px gray solid">
  <path id="path" fill="blue" stroke="cyan"></path>
</svg>

e lá vai você!

SumNeuron
fonte
5

As respostas da @ opsb são claras, mas o ponto central não é preciso, além disso, como @Jithin observou, se o ângulo for 360, nada será traçado.

@Jithin corrigiu o problema 360, mas se você selecionou menos de 360 ​​graus, obterá uma linha que fecha o loop do arco, o que não é necessário.

Corrigi isso e adicionei algumas animações no código abaixo:

function myArc(cx, cy, radius, max){       
       var circle = document.getElementById("arc");
        var e = circle.getAttribute("d");
        var d = " M "+ (cx + radius) + " " + cy;
        var angle=0;
        window.timer = window.setInterval(
        function() {
            var radians= angle * (Math.PI / 180);  // convert degree to radians
            var x = cx + Math.cos(radians) * radius;  
            var y = cy + Math.sin(radians) * radius;
           
            d += " L "+x + " " + y;
            circle.setAttribute("d", d)
            if(angle==max)window.clearInterval(window.timer);
            angle++;
        }
      ,5)
 }     

  myArc(110, 110, 100, 360);
    
<svg xmlns="http://www.w3.org/2000/svg" style="width:220; height:220;"> 
    <path d="" id="arc" fill="none" stroke="red" stroke-width="2" />
</svg>

Hasan A Yousef
fonte
4

Eu queria comentar a resposta @Ahtenus, especificamente o comentário de Ray Hulha dizendo que o codepen não mostra nenhum arco, mas minha reputação não é alta o suficiente.

A razão para esse codepen não funcionar é que seu html está com defeito com uma largura de traço igual a zero.

Corrigi-o e adicionei um segundo exemplo aqui: http://codepen.io/AnotherLinuxUser/pen/QEJmkN .

O html:

<svg>
    <path id="theSvgArc"/>
    <path id="theSvgArc2"/>
</svg>

O CSS relevante:

svg {
    width  : 500px;
    height : 500px;
}

path {
    stroke-width : 5;
    stroke       : lime;
    fill         : #151515;
}

O javascript:

document.getElementById("theSvgArc").setAttribute("d", describeArc(150, 150, 100, 0, 180));
document.getElementById("theSvgArc2").setAttribute("d", describeArc(300, 150, 100, 45, 190));
Alex
fonte
3

Uma imagem e algum Python

Apenas para esclarecer melhor e oferecer outra solução. ArcO Acomando [ ] usa a posição atual como ponto de partida, portanto você deve usar o Movetocomando [M] primeiro.

Os parâmetros de Arcsão os seguintes:

rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, xf, yf

Se definirmos, por exemplo, o seguinte arquivo svg:

<svg viewBox="0 0 500 500">
    <path fill="red" d="
    M 250 250
    A 100 100 0 0 0 450 250
    Z"/> 
</svg>

insira a descrição da imagem aqui

Você definirá o ponto inicial com Mo ponto final com os parâmetros xfe yfdeA .

Estamos procurando por círculos, então definimos como rxiguais rybasicamente agora ele tentará encontrar todo o círculo de raio rxque cruza o ponto inicial e final.

import numpy as np

def write_svgarc(xcenter,ycenter,r,startangle,endangle,output='arc.svg'):
    if startangle > endangle: 
        raise ValueError("startangle must be smaller than endangle")

    if endangle - startangle < 360:
        large_arc_flag = 0
        radiansconversion = np.pi/180.
        xstartpoint = xcenter + r*np.cos(startangle*radiansconversion)
        ystartpoint = ycenter - r*np.sin(startangle*radiansconversion)
        xendpoint = xcenter + r*np.cos(endangle*radiansconversion)
        yendpoint = ycenter - r*np.sin(endangle*radiansconversion)
        #If we want to plot angles larger than 180 degrees we need this
        if endangle - startangle > 180: large_arc_flag = 1
        with open(output,'a') as f:
            f.write(r"""<path d=" """)
            f.write("M %s %s" %(xstartpoint,ystartpoint))
            f.write("A %s %s 0 %s 0 %s %s" 
                    %(r,r,large_arc_flag,xendpoint,yendpoint))
            f.write("L %s %s" %(xcenter,ycenter))
            f.write(r"""Z"/>""" )

    else:
        with open(output,'a') as f:
            f.write(r"""<circle cx="%s" cy="%s" r="%s"/>"""
                    %(xcenter,ycenter,r))

Você pode ter uma explicação mais detalhada neste post que escrevi.

GM
fonte
3

Nota para os que procuram respostas (quem eu também era) - se o uso de arco não é obrigatório , uma solução muito mais simples de desenhar um círculo parcial é usar o stroke-dasharraySVG<circle> .

Divida a matriz do traço em dois elementos e dimensione seu alcance no ângulo desejado. O ângulo inicial pode ser ajustado usando stroke-dashoffset.

Nem um único cosseno à vista.

Exemplo completo com explicações: https://codepen.io/mjurczyk/pen/wvBKOvP

Maciek Jurczyk
fonte
2

A função original polarToCartesian de wdebeaum está correta:

var angleInRadians = angleInDegrees * Math.PI / 180.0;

Reversão dos pontos inicial e final usando:

var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);

É confuso (para mim) porque isso reverterá a bandeira de varredura. Usando:

var start = polarToCartesian(x, y, radius, startAngle);
var end = polarToCartesian(x, y, radius, endAngle);

com a bandeira de varredura = "0" desenha arcos "normais" no sentido anti-horário, o que eu acho mais direto. Consulte https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths

Wiley
fonte
2

Uma ligeira modificação na resposta do @ opsb. Não podemos desenhar um círculo completo com esse método. ou seja, se dermos (0, 360), isso não atrairá nada. Portanto, uma pequena modificação feita para corrigir isso. Pode ser útil exibir pontuações que às vezes atingem 100%.

function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
  var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;

  return {
    x: centerX + (radius * Math.cos(angleInRadians)),
    y: centerY + (radius * Math.sin(angleInRadians))
  };
}

function describeArc(x, y, radius, startAngle, endAngle){

    var endAngleOriginal = endAngle;
    if(endAngleOriginal - startAngle === 360){
        endAngle = 359;
    }

    var start = polarToCartesian(x, y, radius, endAngle);
    var end = polarToCartesian(x, y, radius, startAngle);

    var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";

    if(endAngleOriginal - startAngle === 360){
        var d = [
              "M", start.x, start.y, 
              "A", radius, radius, 0, arcSweep, 0, end.x, end.y, "z"
        ].join(" ");
    }
    else{
      var d = [
          "M", start.x, start.y, 
          "A", radius, radius, 0, arcSweep, 0, end.x, end.y
      ].join(" ");
    }

    return d;       
}

document.getElementById("arc1").setAttribute("d", describeArc(120, 120, 100, 0, 359));
Jithin B
fonte
2

Versão ES6:

const angleInRadians = angleInDegrees => (angleInDegrees - 90) * (Math.PI / 180.0);

const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
    const a = angleInRadians(angleInDegrees);
    return {
        x: centerX + (radius * Math.cos(a)),
        y: centerY + (radius * Math.sin(a)),
    };
};

const arc = (x, y, radius, startAngle, endAngle) => {
    const fullCircle = endAngle - startAngle === 360;
    const start = polarToCartesian(x, y, radius, endAngle - 0.01);
    const end = polarToCartesian(x, y, radius, startAngle);
    const arcSweep = endAngle - startAngle <= 180 ? '0' : '1';

    const d = [
        'M', start.x, start.y,
        'A', radius, radius, 0, arcSweep, 0, end.x, end.y,
    ].join(' ');

    if (fullCircle) d.push('z');
    return d;
};
MrE
fonte
2
Você poderia, sem dúvida, tornar o exemplo mais claro, aproveitando literais modelo ES6:const d = `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArc} 0 ${end.x} ${end.y}`
Roy Prins
0

Componente ReactJS com base na resposta selecionada:

import React from 'react';

const polarToCartesian = (centerX, centerY, radius, angleInDegrees) => {
    const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;

    return {
        x: centerX + (radius * Math.cos(angleInRadians)),
        y: centerY + (radius * Math.sin(angleInRadians))
    };
};

const describeSlice = (x, y, radius, startAngle, endAngle) => {

    const start = polarToCartesian(x, y, radius, endAngle);
    const end = polarToCartesian(x, y, radius, startAngle);

    const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";

    const d = [
        "M", 0, 0, start.x, start.y,
        "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
    ].join(" ");

    return d;
};

const path = (degrees = 90, radius = 10) => {
    return describeSlice(0, 0, radius, 0, degrees) + 'Z';
};

export const Arc = (props) => <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
    <g transform="translate(150,150)" stroke="#000" strokeWidth="2">
        <path d={path(props.degrees, props.radius)} fill="#333"/>
    </g>

</svg>;

export default Arc;
Cara
fonte
0

você pode usar o código JSFiddle que fiz para a resposta acima:

https://jsfiddle.net/tyw6nfee/

tudo o que você precisa fazer é alterar o código console.log da última linha e fornecer seu próprio parâmetro:

  console.log(describeArc(255,255,220,30,180));
  console.log(describeArc(CenterX,CenterY,Radius,startAngle,EndAngle))
javad bat
fonte
1
Fiz algumas alterações no seu snippet apenas para gerar um resultado visual. Dê uma olhada: jsfiddle.net/ed1nh0/96c203wj/3
ed1nh0