stroke.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. // SPDX-License-Identifier: Unlicense OR MIT
  2. // Most of the algorithms to compute strokes and their offsets have been
  3. // extracted, adapted from (and used as a reference implementation):
  4. // - github.com/tdewolff/canvas (Licensed under MIT)
  5. //
  6. // These algorithms have been implemented from:
  7. // Fast, precise flattening of cubic Bézier path and offset curves
  8. // Thomas F. Hain, et al.
  9. //
  10. // An electronic version is available at:
  11. // https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf
  12. //
  13. // Possible improvements (in term of speed and/or accuracy) on these
  14. // algorithms are:
  15. //
  16. // - Polar Stroking: New Theory and Methods for Stroking Paths,
  17. // M. Kilgard
  18. // https://arxiv.org/pdf/2007.00308.pdf
  19. //
  20. // - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
  21. // R. Levien
  22. // Package stroke implements conversion of strokes to filled outlines. It is used as a
  23. // fallback for stroke configurations not natively supported by the renderer.
  24. package stroke
  25. import (
  26. "encoding/binary"
  27. "math"
  28. "gioui.org/internal/f32"
  29. "gioui.org/internal/ops"
  30. "gioui.org/internal/scene"
  31. )
  32. // The following are copies of types from op/clip to avoid a circular import of
  33. // that package.
  34. // TODO: when the old renderer is gone, this package can be merged with
  35. // op/clip, eliminating the duplicate types.
  36. type StrokeStyle struct {
  37. Width float32
  38. }
  39. // strokeTolerance is used to reconcile rounding errors arising
  40. // when splitting quads into smaller and smaller segments to approximate
  41. // them into straight lines, and when joining back segments.
  42. //
  43. // The magic value of 0.01 was found by striking a compromise between
  44. // aesthetic looking (curves did look like curves, even after linearization)
  45. // and speed.
  46. const strokeTolerance = 0.01
  47. type QuadSegment struct {
  48. From, Ctrl, To f32.Point
  49. }
  50. type StrokeQuad struct {
  51. Contour uint32
  52. Quad QuadSegment
  53. }
  54. type strokeState struct {
  55. p0, p1 f32.Point // p0 is the start point, p1 the end point.
  56. n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point.
  57. r0, r1 float32 // r0 is the curvature at the start point, r1 at the end point.
  58. ctl f32.Point // ctl is the control point of the quadratic Bézier segment.
  59. }
  60. type StrokeQuads []StrokeQuad
  61. func (qs *StrokeQuads) pen() f32.Point {
  62. return (*qs)[len(*qs)-1].Quad.To
  63. }
  64. func (qs *StrokeQuads) lineTo(pt f32.Point) {
  65. end := qs.pen()
  66. *qs = append(*qs, StrokeQuad{
  67. Quad: QuadSegment{
  68. From: end,
  69. Ctrl: end.Add(pt).Mul(0.5),
  70. To: pt,
  71. },
  72. })
  73. }
  74. func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) {
  75. pen := qs.pen()
  76. m, segments := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle)
  77. for i := 0; i < segments; i++ {
  78. p0 := qs.pen()
  79. p1 := m.Transform(p0)
  80. p2 := m.Transform(p1)
  81. ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
  82. *qs = append(*qs, StrokeQuad{
  83. Quad: QuadSegment{
  84. From: p0, Ctrl: ctl, To: p2,
  85. },
  86. })
  87. }
  88. }
  89. // split splits a slice of quads into slices of quads grouped
  90. // by contours (ie: splitted at move-to boundaries).
  91. func (qs StrokeQuads) split() []StrokeQuads {
  92. if len(qs) == 0 {
  93. return nil
  94. }
  95. var (
  96. c uint32
  97. o []StrokeQuads
  98. i = len(o)
  99. )
  100. for _, q := range qs {
  101. if q.Contour != c {
  102. c = q.Contour
  103. i = len(o)
  104. o = append(o, StrokeQuads{})
  105. }
  106. o[i] = append(o[i], q)
  107. }
  108. return o
  109. }
  110. func (qs StrokeQuads) stroke(stroke StrokeStyle) StrokeQuads {
  111. var (
  112. o StrokeQuads
  113. hw = 0.5 * stroke.Width
  114. )
  115. for _, ps := range qs.split() {
  116. rhs, lhs := ps.offset(hw, stroke)
  117. switch lhs {
  118. case nil:
  119. o = o.append(rhs)
  120. default:
  121. // Closed path.
  122. // Inner path should go opposite direction to cancel outer path.
  123. switch {
  124. case ps.ccw():
  125. lhs = lhs.reverse()
  126. o = o.append(rhs)
  127. o = o.append(lhs)
  128. default:
  129. rhs = rhs.reverse()
  130. o = o.append(lhs)
  131. o = o.append(rhs)
  132. }
  133. }
  134. }
  135. return o
  136. }
  137. // offset returns the right-hand and left-hand sides of the path, offset by
  138. // the half-width hw.
  139. // The stroke handles how segments are joined and ends are capped.
  140. func (qs StrokeQuads) offset(hw float32, stroke StrokeStyle) (rhs, lhs StrokeQuads) {
  141. var (
  142. states []strokeState
  143. beg = qs[0].Quad.From
  144. end = qs[len(qs)-1].Quad.To
  145. closed = beg == end
  146. )
  147. for i := range qs {
  148. q := qs[i].Quad
  149. var (
  150. n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw)
  151. n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw)
  152. r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0)
  153. r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1)
  154. )
  155. states = append(states, strokeState{
  156. p0: q.From,
  157. p1: q.To,
  158. n0: n0,
  159. n1: n1,
  160. r0: r0,
  161. r1: r1,
  162. ctl: q.Ctrl,
  163. })
  164. }
  165. for i, state := range states {
  166. rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance))
  167. lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance))
  168. // join the current and next segments
  169. if hasNext := i+1 < len(states); hasNext || closed {
  170. var next strokeState
  171. switch {
  172. case hasNext:
  173. next = states[i+1]
  174. case closed:
  175. next = states[0]
  176. }
  177. if state.n1 != next.n0 {
  178. strokePathRoundJoin(&rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0)
  179. }
  180. }
  181. }
  182. if closed {
  183. rhs.close()
  184. lhs.close()
  185. return rhs, lhs
  186. }
  187. qbeg := &states[0]
  188. qend := &states[len(states)-1]
  189. // Default to counter-clockwise direction.
  190. lhs = lhs.reverse()
  191. strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1)
  192. rhs = rhs.append(lhs)
  193. strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1))
  194. rhs.close()
  195. return rhs, nil
  196. }
  197. func (qs *StrokeQuads) close() {
  198. p0 := (*qs)[len(*qs)-1].Quad.To
  199. p1 := (*qs)[0].Quad.From
  200. if p1 == p0 {
  201. return
  202. }
  203. *qs = append(*qs, StrokeQuad{
  204. Quad: QuadSegment{
  205. From: p0,
  206. Ctrl: p0.Add(p1).Mul(0.5),
  207. To: p1,
  208. },
  209. })
  210. }
  211. // ccw returns whether the path is counter-clockwise.
  212. func (qs StrokeQuads) ccw() bool {
  213. // Use the Shoelace formula:
  214. // https://en.wikipedia.org/wiki/Shoelace_formula
  215. var area float32
  216. for _, ps := range qs.split() {
  217. for i := 1; i < len(ps); i++ {
  218. pi := ps[i].Quad.To
  219. pj := ps[i-1].Quad.To
  220. area += (pi.X - pj.X) * (pi.Y + pj.Y)
  221. }
  222. }
  223. return area <= 0.0
  224. }
  225. func (qs StrokeQuads) reverse() StrokeQuads {
  226. if len(qs) == 0 {
  227. return nil
  228. }
  229. ps := make(StrokeQuads, 0, len(qs))
  230. for i := range qs {
  231. q := qs[len(qs)-1-i]
  232. q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To
  233. ps = append(ps, q)
  234. }
  235. return ps
  236. }
  237. func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads {
  238. switch {
  239. case len(ps) == 0:
  240. return qs
  241. case len(qs) == 0:
  242. return ps
  243. }
  244. // Consolidate quads and smooth out rounding errors.
  245. // We need to also check for the strokeTolerance to correctly handle
  246. // join/cap points or on-purpose disjoint quads.
  247. p0 := qs[len(qs)-1].Quad.To
  248. p1 := ps[0].Quad.From
  249. if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance {
  250. qs = append(qs, StrokeQuad{
  251. Quad: QuadSegment{
  252. From: p0,
  253. Ctrl: p0.Add(p1).Mul(0.5),
  254. To: p1,
  255. },
  256. })
  257. }
  258. return append(qs, ps...)
  259. }
  260. func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment {
  261. q.From = t.Transform(q.From)
  262. q.Ctrl = t.Transform(q.Ctrl)
  263. q.To = t.Transform(q.To)
  264. return q
  265. }
  266. // strokePathNorm returns the normal vector at t.
  267. func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
  268. switch t {
  269. case 0:
  270. n := p1.Sub(p0)
  271. if n.X == 0 && n.Y == 0 {
  272. return f32.Point{}
  273. }
  274. n = rot90CW(n)
  275. return normPt(n, d)
  276. case 1:
  277. n := p2.Sub(p1)
  278. if n.X == 0 && n.Y == 0 {
  279. return f32.Point{}
  280. }
  281. n = rot90CW(n)
  282. return normPt(n, d)
  283. }
  284. panic("impossible")
  285. }
  286. func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
  287. func normPt(p f32.Point, l float32) f32.Point {
  288. if (p.X == 0 && p.Y == l) || (p.Y == 0 && p.X == l) {
  289. return f32.Point{X: p.X, Y: p.Y}
  290. }
  291. d := math.Hypot(float64(p.X), float64(p.Y))
  292. l64 := float64(l)
  293. if math.Abs(d-l64) < 1e-10 {
  294. return f32.Point{}
  295. }
  296. n := float32(l64 / d)
  297. return f32.Point{X: p.X * n, Y: p.Y * n}
  298. }
  299. func lenPt(p f32.Point) float32 {
  300. return float32(math.Hypot(float64(p.X), float64(p.Y)))
  301. }
  302. func perpDot(p, q f32.Point) float32 {
  303. return p.X*q.Y - p.Y*q.X
  304. }
  305. func angleBetween(n0, n1 f32.Point) float64 {
  306. return math.Atan2(float64(n1.Y), float64(n1.X)) -
  307. math.Atan2(float64(n0.Y), float64(n0.X))
  308. }
  309. // strokePathCurv returns the curvature at t, along the quadratic Bézier
  310. // curve defined by the triplet (beg, ctl, end).
  311. func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 {
  312. var (
  313. d1p = quadBezierD1(beg, ctl, end, t)
  314. d2p = quadBezierD2(beg, ctl, end, t)
  315. // Negative when bending right, ie: the curve is CW at this point.
  316. a = float64(perpDot(d1p, d2p))
  317. )
  318. // We check early that the segment isn't too line-like and
  319. // save a costly call to math.Pow that will be discarded by dividing
  320. // with a too small 'a'.
  321. if math.Abs(a) < 1e-10 {
  322. return float32(math.NaN())
  323. }
  324. return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a)
  325. }
  326. // quadBezierSample returns the point on the Bézier curve at t.
  327. //
  328. // B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2
  329. func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point {
  330. t1 := 1 - t
  331. c0 := t1 * t1
  332. c1 := 2 * t1 * t
  333. c2 := t * t
  334. o := p0.Mul(c0)
  335. o = o.Add(p1.Mul(c1))
  336. o = o.Add(p2.Mul(c2))
  337. return o
  338. }
  339. // quadBezierD1 returns the first derivative of the Bézier curve with respect to t.
  340. //
  341. // B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1)
  342. func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point {
  343. p10 := p1.Sub(p0).Mul(2 * (1 - t))
  344. p21 := p2.Sub(p1).Mul(2 * t)
  345. return p10.Add(p21)
  346. }
  347. // quadBezierD2 returns the second derivative of the Bézier curve with respect to t:
  348. //
  349. // B''(t) = 2(P2 - 2P1 + P0)
  350. func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
  351. p := p2.Sub(p1.Mul(2)).Add(p0)
  352. return p.Mul(2)
  353. }
  354. func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads {
  355. // Gio strokes are only quadratic Bézier curves, w/o any inflection point.
  356. // So we just have to flatten them.
  357. var qs StrokeQuads
  358. return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness)
  359. }
  360. // flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments,
  361. // themselves also encoded as Bézier (degenerate, flat) quadratic curves.
  362. func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, d, flatness float32) StrokeQuads {
  363. var (
  364. t float32
  365. flat64 = float64(flatness)
  366. )
  367. for t < 1 {
  368. s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X))
  369. den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y))
  370. if s2*den == 0.0 {
  371. break
  372. }
  373. s2 /= den
  374. t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2)))
  375. if t >= 1.0 {
  376. break
  377. }
  378. var q0, q1, q2 f32.Point
  379. q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t)
  380. qs.addLine(q0, q1, q2, 0, d)
  381. }
  382. qs.addLine(p0, p1, p2, 1, d)
  383. return qs
  384. }
  385. func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) {
  386. switch i := len(*qs); i {
  387. case 0:
  388. p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d))
  389. default:
  390. // Address possible rounding errors and use previous point.
  391. p0 = (*qs)[i-1].Quad.To
  392. }
  393. p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d))
  394. *qs = append(*qs,
  395. StrokeQuad{
  396. Quad: QuadSegment{
  397. From: p0,
  398. Ctrl: p0.Add(p1).Mul(0.5),
  399. To: p1,
  400. },
  401. },
  402. )
  403. }
  404. // quadInterp returns the interpolated point at t.
  405. func quadInterp(p, q f32.Point, t float32) f32.Point {
  406. return f32.Pt(
  407. (1-t)*p.X+t*q.X,
  408. (1-t)*p.Y+t*q.Y,
  409. )
  410. }
  411. // quadBezierSplit returns the pair of triplets (from,ctrl,to) Bézier curve,
  412. // split before (resp. after) the provided parametric t value.
  413. func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32.Point, f32.Point, f32.Point, f32.Point) {
  414. var (
  415. b0 = p0
  416. b1 = quadInterp(p0, p1, t)
  417. b2 = quadBezierSample(p0, p1, p2, t)
  418. a0 = b2
  419. a1 = quadInterp(p1, p2, t)
  420. a2 = p2
  421. )
  422. return b0, b1, b2, a0, a1, a2
  423. }
  424. // strokePathRoundJoin joins the two paths rhs and lhs, creating an arc.
  425. func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
  426. rp := pivot.Add(n1)
  427. lp := pivot.Sub(n1)
  428. angle := angleBetween(n0, n1)
  429. switch {
  430. case angle <= 0:
  431. // Path bends to the right, ie. CW (or 180 degree turn).
  432. c := pivot.Sub(lhs.pen())
  433. lhs.arc(c, c, float32(angle))
  434. lhs.lineTo(lp) // Add a line to accommodate for rounding errors.
  435. rhs.lineTo(rp)
  436. default:
  437. // Path bends to the left, ie. CCW.
  438. c := pivot.Sub(rhs.pen())
  439. rhs.arc(c, c, float32(angle))
  440. rhs.lineTo(rp) // Add a line to accommodate for rounding errors.
  441. lhs.lineTo(lp)
  442. }
  443. }
  444. // strokePathCap caps the provided path qs, according to the provided stroke operation.
  445. func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
  446. strokePathRoundCap(qs, hw, pivot, n0)
  447. }
  448. // strokePathRoundCap caps the start or end of a path with a round cap.
  449. func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
  450. c := pivot.Sub(qs.pen())
  451. qs.arc(c, c, math.Pi)
  452. }
  453. // ArcTransform computes a transformation that can be used for generating quadratic bézier
  454. // curve approximations for an arc.
  455. //
  456. // The math is extracted from the following paper:
  457. //
  458. // "Drawing an elliptical arc using polylines, quadratic or
  459. // cubic Bezier curves", L. Maisonobe
  460. //
  461. // An electronic version may be found at:
  462. //
  463. // http://spaceroots.org/documents/ellipse/elliptical-arc.pdf
  464. func ArcTransform(p, f1, f2 f32.Point, angle float32) (transform f32.Affine2D, segments int) {
  465. const segmentsPerCircle = 16
  466. const anglePerSegment = 2 * math.Pi / segmentsPerCircle
  467. s := angle / anglePerSegment
  468. if s < 0 {
  469. s = -s
  470. }
  471. segments = int(math.Ceil(float64(s)))
  472. if segments <= 0 {
  473. segments = 1
  474. }
  475. var rx, ry, alpha float64
  476. if f1 == f2 {
  477. // degenerate case of a circle.
  478. rx = dist(f1, p)
  479. ry = rx
  480. } else {
  481. // semi-major axis: 2a = |PF1| + |PF2|
  482. a := 0.5 * (dist(f1, p) + dist(f2, p))
  483. // semi-minor axis: c^2 = a^2 - b^2 (c: focal distance)
  484. c := dist(f1, f2) * 0.5
  485. b := math.Sqrt(a*a - c*c)
  486. switch {
  487. case a > b:
  488. rx = a
  489. ry = b
  490. default:
  491. rx = b
  492. ry = a
  493. }
  494. if f1.X == f2.X {
  495. // special case of a "vertical" ellipse.
  496. alpha = math.Pi / 2
  497. if f1.Y < f2.Y {
  498. alpha = -alpha
  499. }
  500. } else {
  501. x := float64(f1.X-f2.X) * 0.5
  502. if x < 0 {
  503. x = -x
  504. }
  505. alpha = math.Acos(x / c)
  506. }
  507. }
  508. var (
  509. θ = angle / float32(segments)
  510. ref f32.Affine2D // transform from absolute frame to ellipse-based one
  511. rot f32.Affine2D // rotation matrix for each segment
  512. inv f32.Affine2D // transform from ellipse-based frame to absolute one
  513. )
  514. center := f32.Point{
  515. X: 0.5 * (f1.X + f2.X),
  516. Y: 0.5 * (f1.Y + f2.Y),
  517. }
  518. ref = ref.Offset(f32.Point{}.Sub(center))
  519. ref = ref.Rotate(f32.Point{}, float32(-alpha))
  520. ref = ref.Scale(f32.Point{}, f32.Point{
  521. X: float32(1 / rx),
  522. Y: float32(1 / ry),
  523. })
  524. inv = ref.Invert()
  525. rot = rot.Rotate(f32.Point{}, 0.5*θ)
  526. // Instead of invoking math.Sincos for every segment, compute a rotation
  527. // matrix once and apply for each segment.
  528. // Before applying the rotation matrix rot, transform the coordinates
  529. // to a frame centered to the ellipse (and warped into a unit circle), then rotate.
  530. // Finally, transform back into the original frame.
  531. return inv.Mul(rot).Mul(ref), segments
  532. }
  533. func dist(p1, p2 f32.Point) float64 {
  534. var (
  535. x1 = float64(p1.X)
  536. y1 = float64(p1.Y)
  537. x2 = float64(p2.X)
  538. y2 = float64(p2.Y)
  539. dx = x2 - x1
  540. dy = y2 - y1
  541. )
  542. return math.Hypot(dx, dy)
  543. }
  544. func StrokePathCommands(style StrokeStyle, scene []byte) StrokeQuads {
  545. quads := decodeToStrokeQuads(scene)
  546. return quads.stroke(style)
  547. }
  548. // decodeToStrokeQuads decodes scene commands to quads ready to stroke.
  549. func decodeToStrokeQuads(pathData []byte) StrokeQuads {
  550. quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
  551. scratch := make([]QuadSegment, 0, 10)
  552. for len(pathData) >= scene.CommandSize+4 {
  553. contour := binary.LittleEndian.Uint32(pathData)
  554. cmd := ops.DecodeCommand(pathData[4:])
  555. switch cmd.Op() {
  556. case scene.OpLine:
  557. var q QuadSegment
  558. q.From, q.To = scene.DecodeLine(cmd)
  559. q.Ctrl = q.From.Add(q.To).Mul(.5)
  560. quad := StrokeQuad{
  561. Contour: contour,
  562. Quad: q,
  563. }
  564. quads = append(quads, quad)
  565. case scene.OpGap:
  566. // Ignore gaps for strokes.
  567. case scene.OpQuad:
  568. var q QuadSegment
  569. q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
  570. quad := StrokeQuad{
  571. Contour: contour,
  572. Quad: q,
  573. }
  574. quads = append(quads, quad)
  575. case scene.OpCubic:
  576. from, ctrl0, ctrl1, to := scene.DecodeCubic(cmd)
  577. scratch = SplitCubic(from, ctrl0, ctrl1, to, scratch[:0])
  578. for _, q := range scratch {
  579. quad := StrokeQuad{
  580. Contour: contour,
  581. Quad: q,
  582. }
  583. quads = append(quads, quad)
  584. }
  585. default:
  586. panic("unsupported scene command")
  587. }
  588. pathData = pathData[scene.CommandSize+4:]
  589. }
  590. return quads
  591. }
  592. func SplitCubic(from, ctrl0, ctrl1, to f32.Point, quads []QuadSegment) []QuadSegment {
  593. // Set the maximum distance proportionally to the longest side
  594. // of the bounding rectangle.
  595. hull := f32.Rectangle{
  596. Min: from,
  597. Max: ctrl0,
  598. }.Canon().Union(f32.Rectangle{
  599. Min: ctrl1,
  600. Max: to,
  601. }.Canon())
  602. l := hull.Dx()
  603. if h := hull.Dy(); h > l {
  604. l = h
  605. }
  606. maxDist := l * 0.001
  607. approxCubeTo(&quads, 0, maxDist*maxDist, from, ctrl0, ctrl1, to)
  608. return quads
  609. }
  610. // approxCubeTo approximates a cubic Bézier by a series of quadratic
  611. // curves.
  612. func approxCubeTo(quads *[]QuadSegment, splits int, maxDistSq float32, from, ctrl0, ctrl1, to f32.Point) int {
  613. // The idea is from
  614. // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
  615. // where a quadratic approximates a cubic by eliminating its t³ term
  616. // from its polynomial expression anchored at the starting point:
  617. //
  618. // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen)
  619. //
  620. // The control point for the new quadratic Q1 that shares starting point, pen, with P is
  621. //
  622. // C1 = (3ctrl0 - pen)/2
  623. //
  624. // The reverse cubic anchored at the end point has the polynomial
  625. //
  626. // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to)
  627. //
  628. // The corresponding quadratic Q2 that shares the end point, to, with P has control
  629. // point
  630. //
  631. // C2 = (3ctrl1 - to)/2
  632. //
  633. // The combined quadratic Bézier, Q, shares both start and end points with its cubic
  634. // and use the midpoint between the two curves Q1 and Q2 as control point:
  635. //
  636. // C = (3ctrl0 - pen + 3ctrl1 - to)/4
  637. // using, q0 := 3ctrl0 - pen, q1 := 3ctrl1 - to
  638. // C = (q0 + q1)/4
  639. q0 := ctrl0.Mul(3).Sub(from)
  640. q1 := ctrl1.Mul(3).Sub(to)
  641. c := q0.Add(q1).Mul(1.0 / 4.0)
  642. const maxSplits = 32
  643. if splits >= maxSplits {
  644. *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
  645. return splits
  646. }
  647. // The maximum distance between the cubic P and its approximation Q given t
  648. // can be shown to be
  649. //
  650. // d = sqrt(3)/36 * |to - 3ctrl1 + 3ctrl0 - pen|
  651. // reusing, q0 := 3ctrl0 - pen, q1 := 3ctrl1 - to
  652. // d = sqrt(3)/36 * |-q1 + q0|
  653. //
  654. // To save a square root, compare d² with the squared tolerance.
  655. v := q0.Sub(q1)
  656. d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
  657. if d2 <= maxDistSq {
  658. *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
  659. return splits
  660. }
  661. // De Casteljau split the curve and approximate the halves.
  662. t := float32(0.5)
  663. c0 := from.Add(ctrl0.Sub(from).Mul(t))
  664. c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
  665. c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
  666. c01 := c0.Add(c1.Sub(c0).Mul(t))
  667. c12 := c1.Add(c2.Sub(c1).Mul(t))
  668. c0112 := c01.Add(c12.Sub(c01).Mul(t))
  669. splits++
  670. splits = approxCubeTo(quads, splits, maxDistSq, from, c0, c01, c0112)
  671. splits = approxCubeTo(quads, splits, maxDistSq, c0112, c12, c2, to)
  672. return splits
  673. }