EllipticArc.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <?php
  2. declare(strict_types = 1);
  3. namespace BaconQrCode\Renderer\Path;
  4. final class EllipticArc implements OperationInterface
  5. {
  6. private const ZERO_TOLERANCE = 1e-05;
  7. /**
  8. * @var float
  9. */
  10. private $xRadius;
  11. /**
  12. * @var float
  13. */
  14. private $yRadius;
  15. /**
  16. * @var float
  17. */
  18. private $xAxisAngle;
  19. /**
  20. * @var bool
  21. */
  22. private $largeArc;
  23. /**
  24. * @var bool
  25. */
  26. private $sweep;
  27. /**
  28. * @var float
  29. */
  30. private $x;
  31. /**
  32. * @var float
  33. */
  34. private $y;
  35. public function __construct(
  36. float $xRadius,
  37. float $yRadius,
  38. float $xAxisAngle,
  39. bool $largeArc,
  40. bool $sweep,
  41. float $x,
  42. float $y
  43. ) {
  44. $this->xRadius = abs($xRadius);
  45. $this->yRadius = abs($yRadius);
  46. $this->xAxisAngle = $xAxisAngle % 360;
  47. $this->largeArc = $largeArc;
  48. $this->sweep = $sweep;
  49. $this->x = $x;
  50. $this->y = $y;
  51. }
  52. public function getXRadius() : float
  53. {
  54. return $this->xRadius;
  55. }
  56. public function getYRadius() : float
  57. {
  58. return $this->yRadius;
  59. }
  60. public function getXAxisAngle() : float
  61. {
  62. return $this->xAxisAngle;
  63. }
  64. public function isLargeArc() : bool
  65. {
  66. return $this->largeArc;
  67. }
  68. public function isSweep() : bool
  69. {
  70. return $this->sweep;
  71. }
  72. public function getX() : float
  73. {
  74. return $this->x;
  75. }
  76. public function getY() : float
  77. {
  78. return $this->y;
  79. }
  80. /**
  81. * @return self
  82. */
  83. public function translate(float $x, float $y) : OperationInterface
  84. {
  85. return new self(
  86. $this->xRadius,
  87. $this->yRadius,
  88. $this->xAxisAngle,
  89. $this->largeArc,
  90. $this->sweep,
  91. $this->x + $x,
  92. $this->y + $y
  93. );
  94. }
  95. /**
  96. * Converts the elliptic arc to multiple curves.
  97. *
  98. * Since not all image back ends support elliptic arcs, this method allows to convert the arc into multiple curves
  99. * resembling the same result.
  100. *
  101. * @see https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
  102. * @return array<Curve|Line>
  103. */
  104. public function toCurves(float $fromX, float $fromY) : array
  105. {
  106. if (sqrt(($fromX - $this->x) ** 2 + ($fromY - $this->y) ** 2) < self::ZERO_TOLERANCE) {
  107. return [];
  108. }
  109. if ($this->xRadius < self::ZERO_TOLERANCE || $this->yRadius < self::ZERO_TOLERANCE) {
  110. return [new Line($this->x, $this->y)];
  111. }
  112. return $this->createCurves($fromX, $fromY);
  113. }
  114. /**
  115. * @return Curve[]
  116. */
  117. private function createCurves(float $fromX, $fromY) : array
  118. {
  119. $xAngle = deg2rad($this->xAxisAngle);
  120. list($centerX, $centerY, $radiusX, $radiusY, $startAngle, $deltaAngle) =
  121. $this->calculateCenterPointParameters($fromX, $fromY, $xAngle);
  122. $s = $startAngle;
  123. $e = $s + $deltaAngle;
  124. $sign = ($e < $s) ? -1 : 1;
  125. $remain = abs($e - $s);
  126. $p1 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s);
  127. $curves = [];
  128. while ($remain > self::ZERO_TOLERANCE) {
  129. $step = min($remain, pi() / 2);
  130. $signStep = $step * $sign;
  131. $p2 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s + $signStep);
  132. $alphaT = tan($signStep / 2);
  133. $alpha = sin($signStep) * (sqrt(4 + 3 * $alphaT ** 2) - 1) / 3;
  134. $d1 = self::derivative($radiusX, $radiusY, $xAngle, $s);
  135. $d2 = self::derivative($radiusX, $radiusY, $xAngle, $s + $signStep);
  136. $curves[] = new Curve(
  137. $p1[0] + $alpha * $d1[0],
  138. $p1[1] + $alpha * $d1[1],
  139. $p2[0] - $alpha * $d2[0],
  140. $p2[1] - $alpha * $d2[1],
  141. $p2[0],
  142. $p2[1]
  143. );
  144. $s += $signStep;
  145. $remain -= $step;
  146. $p1 = $p2;
  147. }
  148. return $curves;
  149. }
  150. /**
  151. * @return float[]
  152. */
  153. private function calculateCenterPointParameters(float $fromX, float $fromY, float $xAngle)
  154. {
  155. $rX = $this->xRadius;
  156. $rY = $this->yRadius;
  157. // F.6.5.1
  158. $dx2 = ($fromX - $this->x) / 2;
  159. $dy2 = ($fromY - $this->y) / 2;
  160. $x1p = cos($xAngle) * $dx2 + sin($xAngle) * $dy2;
  161. $y1p = -sin($xAngle) * $dx2 + cos($xAngle) * $dy2;
  162. // F.6.5.2
  163. $rxs = $rX ** 2;
  164. $rys = $rY ** 2;
  165. $x1ps = $x1p ** 2;
  166. $y1ps = $y1p ** 2;
  167. $cr = $x1ps / $rxs + $y1ps / $rys;
  168. if ($cr > 1) {
  169. $s = sqrt($cr);
  170. $rX *= $s;
  171. $rY *= $s;
  172. $rxs = $rX ** 2;
  173. $rys = $rY ** 2;
  174. }
  175. $dq = ($rxs * $y1ps + $rys * $x1ps);
  176. $pq = ($rxs * $rys - $dq) / $dq;
  177. $q = sqrt(max(0, $pq));
  178. if ($this->largeArc === $this->sweep) {
  179. $q = -$q;
  180. }
  181. $cxp = $q * $rX * $y1p / $rY;
  182. $cyp = -$q * $rY * $x1p / $rX;
  183. // F.6.5.3
  184. $cx = cos($xAngle) * $cxp - sin($xAngle) * $cyp + ($fromX + $this->x) / 2;
  185. $cy = sin($xAngle) * $cxp + cos($xAngle) * $cyp + ($fromY + $this->y) / 2;
  186. // F.6.5.5
  187. $theta = self::angle(1, 0, ($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY);
  188. // F.6.5.6
  189. $delta = self::angle(($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY, (-$x1p - $cxp) / $rX, (-$y1p - $cyp) / $rY);
  190. $delta = fmod($delta, pi() * 2);
  191. if (! $this->sweep) {
  192. $delta -= 2 * pi();
  193. }
  194. return [$cx, $cy, $rX, $rY, $theta, $delta];
  195. }
  196. private static function angle(float $ux, float $uy, float $vx, float $vy) : float
  197. {
  198. // F.6.5.4
  199. $dot = $ux * $vx + $uy * $vy;
  200. $length = sqrt($ux ** 2 + $uy ** 2) * sqrt($vx ** 2 + $vy ** 2);
  201. $angle = acos(min(1, max(-1, $dot / $length)));
  202. if (($ux * $vy - $uy * $vx) < 0) {
  203. return -$angle;
  204. }
  205. return $angle;
  206. }
  207. /**
  208. * @return float[]
  209. */
  210. private static function point(
  211. float $centerX,
  212. float $centerY,
  213. float $radiusX,
  214. float $radiusY,
  215. float $xAngle,
  216. float $angle
  217. ) : array {
  218. return [
  219. $centerX + $radiusX * cos($xAngle) * cos($angle) - $radiusY * sin($xAngle) * sin($angle),
  220. $centerY + $radiusX * sin($xAngle) * cos($angle) + $radiusY * cos($xAngle) * sin($angle),
  221. ];
  222. }
  223. /**
  224. * @return float[]
  225. */
  226. private static function derivative(float $radiusX, float $radiusY, float $xAngle, float $angle) : array
  227. {
  228. return [
  229. -$radiusX * cos($xAngle) * sin($angle) - $radiusY * sin($xAngle) * cos($angle),
  230. -$radiusX * sin($xAngle) * sin($angle) + $radiusY * cos($xAngle) * cos($angle),
  231. ];
  232. }
  233. }