1package basemap23import (4 "math"56 "golang.org/x/image/vector"7)89// How a line becomes an area.10//11// There is no stroker here in the usual sense -- no joins, no miter limit, no12// offset curves. Every segment of a polyline is added to the path as its own13// closed capsule: the segment widened to the stroke width, with a semicircular14// cap at each end. Capsules overlap at a vertex, and because they all wind the15// same way and the rasterizer saturates rather than alternating, the overlap16// reads as one continuous line with a round join for free. It is the cheapest17// stroker that never shows a seam, which is what a deterministic offline18// renderer wants.1920// kappa is the control-point offset that makes four cubic Béziers spell a21// circle as closely as cubics can.22const kappa = 0.55228474983079362324// capsule adds one segment's stroked outline to the path, in tile pixels.25func capsule(into *vector.Rasterizer, a, b [2]float64, width float64) {26 half := width / 227 dx, dy := b[0]-a[0], b[1]-a[1]28 length := math.Hypot(dx, dy)29 if length == 0 {30 return31 }32 // The unit direction, and the left normal already scaled to the half33 // width. Because the normal carries the half width, kappa applied to it is34 // the same offset k as along the tangent.35 ux, uy := dx/length, dy/length36 nx, ny := -uy*half, ux*half37 k := kappa * half3839 moveTo(into, a[0]+nx, a[1]+ny)40 lineTo(into, b[0]+nx, b[1]+ny)41 // The far cap: two quarter arcs from b's left shoulder, around the nose at42 // b + u*half, to b's right shoulder.43 cubeTo(into,44 b[0]+nx+ux*k, b[1]+ny+uy*k,45 b[0]+ux*half+nx*kappa, b[1]+uy*half+ny*kappa,46 b[0]+ux*half, b[1]+uy*half)47 cubeTo(into,48 b[0]+ux*half-nx*kappa, b[1]+uy*half-ny*kappa,49 b[0]-nx+ux*k, b[1]-ny+uy*k,50 b[0]-nx, b[1]-ny)51 lineTo(into, a[0]-nx, a[1]-ny)52 // And the near cap, back around a - u*half.53 cubeTo(into,54 a[0]-nx-ux*k, a[1]-ny-uy*k,55 a[0]-ux*half-nx*kappa, a[1]-uy*half-ny*kappa,56 a[0]-ux*half, a[1]-uy*half)57 cubeTo(into,58 a[0]-ux*half+nx*kappa, a[1]-uy*half+ny*kappa,59 a[0]+nx-ux*k, a[1]+ny-uy*k,60 a[0]+nx, a[1]+ny)61 into.ClosePath()62}6364// dashedCapsules walks a segment in the dash rhythm and strokes only the65// on-runs. The phase is handed in and rides along the polyline, so the rhythm66// survives a vertex instead of restarting at every corner.67func dashedCapsules(into *vector.Rasterizer, a, b [2]float64, width float64, dash [2]float64, phase float64) {68 on, off := dash[0], dash[1]69 period := on + off70 if period <= 0 {71 capsule(into, a, b, width)72 return73 }74 dx, dy := b[0]-a[0], b[1]-a[1]75 length := math.Hypot(dx, dy)76 if length == 0 {77 return78 }79 ux, uy := dx/length, dy/length80 for at := 0.0; at < length; {81 place := math.Mod(phase+at, period)82 if place >= on {83 at += period - place84 continue85 }86 run := math.Min(on-place, length-at)87 capsule(into,88 [2]float64{a[0] + ux*at, a[1] + uy*at},89 [2]float64{a[0] + ux*(at+run), a[1] + uy*(at+run)},90 width)91 at += run92 }93}9495// The rasterizer speaks float32; every coordinate crosses that boundary in one96// place so the narrowing is a property of the drawing rather than a detail97// scattered through it.98func moveTo(into *vector.Rasterizer, x, y float64) { into.MoveTo(float32(x), float32(y)) }99func lineTo(into *vector.Rasterizer, x, y float64) { into.LineTo(float32(x), float32(y)) }100func cubeTo(into *vector.Rasterizer, x1, y1, x2, y2, x3, y3 float64) {101 into.CubeTo(float32(x1), float32(y1), float32(x2), float32(y2), float32(x3), float32(y3))102}