github.com/Seikaijyu/gio@v0.0.1/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 "github.com/Seikaijyu/gio/internal/f32" 33 "github.com/Seikaijyu/gio/internal/ops" 34 "github.com/Seikaijyu/gio/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 d := math.Hypot(float64(p.X), float64(p.Y)) 331 l64 := float64(l) 332 if math.Abs(d-l64) < 1e-10 { 333 return f32.Point{} 334 } 335 n := float32(l64 / d) 336 return f32.Point{X: p.X * n, Y: p.Y * n} 337 } 338 339 func lenPt(p f32.Point) float32 { 340 return float32(math.Hypot(float64(p.X), float64(p.Y))) 341 } 342 343 func perpDot(p, q f32.Point) float32 { 344 return p.X*q.Y - p.Y*q.X 345 } 346 347 func angleBetween(n0, n1 f32.Point) float64 { 348 return math.Atan2(float64(n1.Y), float64(n1.X)) - 349 math.Atan2(float64(n0.Y), float64(n0.X)) 350 } 351 352 // strokePathCurv returns the curvature at t, along the quadratic Bézier 353 // curve defined by the triplet (beg, ctl, end). 354 func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 { 355 var ( 356 d1p = quadBezierD1(beg, ctl, end, t) 357 d2p = quadBezierD2(beg, ctl, end, t) 358 359 // Negative when bending right, ie: the curve is CW at this point. 360 a = float64(perpDot(d1p, d2p)) 361 ) 362 363 // We check early that the segment isn't too line-like and 364 // save a costly call to math.Pow that will be discarded by dividing 365 // with a too small 'a'. 366 if math.Abs(a) < 1e-10 { 367 return float32(math.NaN()) 368 } 369 return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a) 370 } 371 372 // quadBezierSample returns the point on the Bézier curve at t. 373 // 374 // B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2 375 func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point { 376 t1 := 1 - t 377 c0 := t1 * t1 378 c1 := 2 * t1 * t 379 c2 := t * t 380 381 o := p0.Mul(c0) 382 o = o.Add(p1.Mul(c1)) 383 o = o.Add(p2.Mul(c2)) 384 return o 385 } 386 387 // quadBezierD1 returns the first derivative of the Bézier curve with respect to t. 388 // 389 // B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1) 390 func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point { 391 p10 := p1.Sub(p0).Mul(2 * (1 - t)) 392 p21 := p2.Sub(p1).Mul(2 * t) 393 394 return p10.Add(p21) 395 } 396 397 // quadBezierD2 returns the second derivative of the Bézier curve with respect to t: 398 // 399 // B''(t) = 2(P2 - 2P1 + P0) 400 func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point { 401 p := p2.Sub(p1.Mul(2)).Add(p0) 402 return p.Mul(2) 403 } 404 405 func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads { 406 // Gio strokes are only quadratic Bézier curves, w/o any inflection point. 407 // So we just have to flatten them. 408 var qs StrokeQuads 409 return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness) 410 } 411 412 // flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments, 413 // themselves also encoded as Bézier (degenerate, flat) quadratic curves. 414 func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, d, flatness float32) StrokeQuads { 415 var ( 416 t float32 417 flat64 = float64(flatness) 418 ) 419 for t < 1 { 420 s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X)) 421 den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y)) 422 if s2*den == 0.0 { 423 break 424 } 425 426 s2 /= den 427 t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2))) 428 if t >= 1.0 { 429 break 430 } 431 var q0, q1, q2 f32.Point 432 q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t) 433 qs.addLine(q0, q1, q2, 0, d) 434 } 435 qs.addLine(p0, p1, p2, 1, d) 436 return qs 437 } 438 439 func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) { 440 441 switch i := len(*qs); i { 442 case 0: 443 p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d)) 444 default: 445 // Address possible rounding errors and use previous point. 446 p0 = (*qs)[i-1].Quad.To 447 } 448 449 p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d)) 450 451 *qs = append(*qs, 452 StrokeQuad{ 453 Quad: QuadSegment{ 454 From: p0, 455 Ctrl: p0.Add(p1).Mul(0.5), 456 To: p1, 457 }, 458 }, 459 ) 460 } 461 462 // quadInterp returns the interpolated point at t. 463 func quadInterp(p, q f32.Point, t float32) f32.Point { 464 return f32.Pt( 465 (1-t)*p.X+t*q.X, 466 (1-t)*p.Y+t*q.Y, 467 ) 468 } 469 470 // quadBezierSplit returns the pair of triplets (from,ctrl,to) Bézier curve, 471 // split before (resp. after) the provided parametric t value. 472 func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32.Point, f32.Point, f32.Point, f32.Point) { 473 474 var ( 475 b0 = p0 476 b1 = quadInterp(p0, p1, t) 477 b2 = quadBezierSample(p0, p1, p2, t) 478 479 a0 = b2 480 a1 = quadInterp(p1, p2, t) 481 a2 = p2 482 ) 483 484 return b0, b1, b2, a0, a1, a2 485 } 486 487 // strokePathRoundJoin joins the two paths rhs and lhs, creating an arc. 488 func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { 489 rp := pivot.Add(n1) 490 lp := pivot.Sub(n1) 491 angle := angleBetween(n0, n1) 492 switch { 493 case angle <= 0: 494 // Path bends to the right, ie. CW (or 180 degree turn). 495 c := pivot.Sub(lhs.pen()) 496 lhs.arc(c, c, float32(angle)) 497 lhs.lineTo(lp) // Add a line to accommodate for rounding errors. 498 rhs.lineTo(rp) 499 default: 500 // Path bends to the left, ie. CCW. 501 c := pivot.Sub(rhs.pen()) 502 rhs.arc(c, c, float32(angle)) 503 rhs.lineTo(rp) // Add a line to accommodate for rounding errors. 504 lhs.lineTo(lp) 505 } 506 } 507 508 // strokePathCap caps the provided path qs, according to the provided stroke operation. 509 func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { 510 strokePathRoundCap(qs, hw, pivot, n0) 511 } 512 513 // strokePathRoundCap caps the start or end of a path with a round cap. 514 func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { 515 c := pivot.Sub(qs.pen()) 516 qs.arc(c, c, math.Pi) 517 } 518 519 // ArcTransform computes a transformation that can be used for generating quadratic bézier 520 // curve approximations for an arc. 521 // 522 // The math is extracted from the following paper: 523 // 524 // "Drawing an elliptical arc using polylines, quadratic or 525 // cubic Bezier curves", L. Maisonobe 526 // 527 // An electronic version may be found at: 528 // 529 // http://spaceroots.org/documents/ellipse/elliptical-arc.pdf 530 func ArcTransform(p, f1, f2 f32.Point, angle float32) (transform f32.Affine2D, segments int) { 531 const segmentsPerCircle = 16 532 const anglePerSegment = 2 * math.Pi / segmentsPerCircle 533 534 s := angle / anglePerSegment 535 if s < 0 { 536 s = -s 537 } 538 segments = int(math.Ceil(float64(s))) 539 if segments <= 0 { 540 segments = 1 541 } 542 543 var rx, ry, alpha float64 544 if f1 == f2 { 545 // degenerate case of a circle. 546 rx = dist(f1, p) 547 ry = rx 548 } else { 549 // semi-major axis: 2a = |PF1| + |PF2| 550 a := 0.5 * (dist(f1, p) + dist(f2, p)) 551 // semi-minor axis: c^2 = a^2 - b^2 (c: focal distance) 552 c := dist(f1, f2) * 0.5 553 b := math.Sqrt(a*a - c*c) 554 switch { 555 case a > b: 556 rx = a 557 ry = b 558 default: 559 rx = b 560 ry = a 561 } 562 if f1.X == f2.X { 563 // special case of a "vertical" ellipse. 564 alpha = math.Pi / 2 565 if f1.Y < f2.Y { 566 alpha = -alpha 567 } 568 } else { 569 x := float64(f1.X-f2.X) * 0.5 570 if x < 0 { 571 x = -x 572 } 573 alpha = math.Acos(x / c) 574 } 575 } 576 577 var ( 578 θ = angle / float32(segments) 579 ref f32.Affine2D // transform from absolute frame to ellipse-based one 580 rot f32.Affine2D // rotation matrix for each segment 581 inv f32.Affine2D // transform from ellipse-based frame to absolute one 582 ) 583 center := f32.Point{ 584 X: 0.5 * (f1.X + f2.X), 585 Y: 0.5 * (f1.Y + f2.Y), 586 } 587 ref = ref.Offset(f32.Point{}.Sub(center)) 588 ref = ref.Rotate(f32.Point{}, float32(-alpha)) 589 ref = ref.Scale(f32.Point{}, f32.Point{ 590 X: float32(1 / rx), 591 Y: float32(1 / ry), 592 }) 593 inv = ref.Invert() 594 rot = rot.Rotate(f32.Point{}, 0.5*θ) 595 596 // Instead of invoking math.Sincos for every segment, compute a rotation 597 // matrix once and apply for each segment. 598 // Before applying the rotation matrix rot, transform the coordinates 599 // to a frame centered to the ellipse (and warped into a unit circle), then rotate. 600 // Finally, transform back into the original frame. 601 return inv.Mul(rot).Mul(ref), segments 602 } 603 604 func dist(p1, p2 f32.Point) float64 { 605 var ( 606 x1 = float64(p1.X) 607 y1 = float64(p1.Y) 608 x2 = float64(p2.X) 609 y2 = float64(p2.Y) 610 dx = x2 - x1 611 dy = y2 - y1 612 ) 613 return math.Hypot(dx, dy) 614 } 615 616 func StrokePathCommands(style StrokeStyle, scene []byte) StrokeQuads { 617 quads := decodeToStrokeQuads(scene) 618 return quads.stroke(style) 619 } 620 621 // decodeToStrokeQuads decodes scene commands to quads ready to stroke. 622 func decodeToStrokeQuads(pathData []byte) StrokeQuads { 623 quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) 624 scratch := make([]QuadSegment, 0, 10) 625 for len(pathData) >= scene.CommandSize+4 { 626 contour := binary.LittleEndian.Uint32(pathData) 627 cmd := ops.DecodeCommand(pathData[4:]) 628 switch cmd.Op() { 629 case scene.OpLine: 630 var q QuadSegment 631 q.From, q.To = scene.DecodeLine(cmd) 632 q.Ctrl = q.From.Add(q.To).Mul(.5) 633 quad := StrokeQuad{ 634 Contour: contour, 635 Quad: q, 636 } 637 quads = append(quads, quad) 638 case scene.OpGap: 639 // Ignore gaps for strokes. 640 case scene.OpQuad: 641 var q QuadSegment 642 q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) 643 quad := StrokeQuad{ 644 Contour: contour, 645 Quad: q, 646 } 647 quads = append(quads, quad) 648 case scene.OpCubic: 649 from, ctrl0, ctrl1, to := scene.DecodeCubic(cmd) 650 scratch = SplitCubic(from, ctrl0, ctrl1, to, scratch[:0]) 651 for _, q := range scratch { 652 quad := StrokeQuad{ 653 Contour: contour, 654 Quad: q, 655 } 656 quads = append(quads, quad) 657 } 658 default: 659 panic("unsupported scene command") 660 } 661 pathData = pathData[scene.CommandSize+4:] 662 } 663 return quads 664 } 665 666 func SplitCubic(from, ctrl0, ctrl1, to f32.Point, quads []QuadSegment) []QuadSegment { 667 // Set the maximum distance proportionally to the longest side 668 // of the bounding rectangle. 669 hull := f32.Rectangle{ 670 Min: from, 671 Max: ctrl0, 672 }.Canon().Union(f32.Rectangle{ 673 Min: ctrl1, 674 Max: to, 675 }.Canon()) 676 l := hull.Dx() 677 if h := hull.Dy(); h > l { 678 l = h 679 } 680 maxDist := l * 0.001 681 approxCubeTo(&quads, 0, maxDist*maxDist, from, ctrl0, ctrl1, to) 682 return quads 683 } 684 685 // approxCubeTo approximates a cubic Bézier by a series of quadratic 686 // curves. 687 func approxCubeTo(quads *[]QuadSegment, splits int, maxDistSq float32, from, ctrl0, ctrl1, to f32.Point) int { 688 // The idea is from 689 // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html 690 // where a quadratic approximates a cubic by eliminating its t³ term 691 // from its polynomial expression anchored at the starting point: 692 // 693 // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen) 694 // 695 // The control point for the new quadratic Q1 that shares starting point, pen, with P is 696 // 697 // C1 = (3ctrl0 - pen)/2 698 // 699 // The reverse cubic anchored at the end point has the polynomial 700 // 701 // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to) 702 // 703 // The corresponding quadratic Q2 that shares the end point, to, with P has control 704 // point 705 // 706 // C2 = (3ctrl1 - to)/2 707 // 708 // The combined quadratic Bézier, Q, shares both start and end points with its cubic 709 // and use the midpoint between the two curves Q1 and Q2 as control point: 710 // 711 // C = (3ctrl0 - pen + 3ctrl1 - to)/4 712 // using, q0 := 3ctrl0 - pen, q1 := 3ctrl1 - to 713 // C = (q0 + q1)/4 714 q0 := ctrl0.Mul(3).Sub(from) 715 q1 := ctrl1.Mul(3).Sub(to) 716 c := q0.Add(q1).Mul(1.0 / 4.0) 717 const maxSplits = 32 718 if splits >= maxSplits { 719 *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) 720 return splits 721 } 722 // The maximum distance between the cubic P and its approximation Q given t 723 // can be shown to be 724 // 725 // d = sqrt(3)/36 * |to - 3ctrl1 + 3ctrl0 - pen| 726 // reusing, q0 := 3ctrl0 - pen, q1 := 3ctrl1 - to 727 // d = sqrt(3)/36 * |-q1 + q0| 728 // 729 // To save a square root, compare d² with the squared tolerance. 730 v := q0.Sub(q1) 731 d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) 732 if d2 <= maxDistSq { 733 *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) 734 return splits 735 } 736 // De Casteljau split the curve and approximate the halves. 737 t := float32(0.5) 738 c0 := from.Add(ctrl0.Sub(from).Mul(t)) 739 c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) 740 c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) 741 c01 := c0.Add(c1.Sub(c0).Mul(t)) 742 c12 := c1.Add(c2.Sub(c1).Mul(t)) 743 c0112 := c01.Add(c12.Sub(c01).Mul(t)) 744 splits++ 745 splits = approxCubeTo(quads, splits, maxDistSq, from, c0, c01, c0112) 746 splits = approxCubeTo(quads, splits, maxDistSq, c0112, c12, c2, to) 747 return splits 748 }