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