package d2raster
import (
"context"
"fmt"
"image"
"math"
"github.com/pancsta/d2/d2renderers/d2raster/internal/scanline"
"github.com/pancsta/d2/d2renderers/d2scene"
)
const (
maxCurveErrorPixels = 0.25
maxCurveDepth = 24
geometryEpsilon = 1e-12
)
type subpath struct {
points []d2scene .Point
closed bool
}
type strokeRun struct {
points []d2scene .Point
closed bool
}
func flattenTolerance(transform d2scene .Matrix ) float64 {
maxCoefficient := math .Max (
math .Max (math .Abs (transform .A ), math .Abs (transform .B )),
math .Max (math .Abs (transform .C ), math .Abs (transform .D )),
)
if !finite (maxCoefficient ) {
return math .SmallestNonzeroFloat64
}
if maxCoefficient == 0 {
return math .MaxFloat64
}
factor := (d2scene .Matrix {
A : transform .A / maxCoefficient ,
B : transform .B / maxCoefficient ,
C : transform .C / maxCoefficient ,
D : transform .D / maxCoefficient ,
}).MaxScale ()
normalizedTolerance := maxCurveErrorPixels / factor
if maxCoefficient < normalizedTolerance /math .MaxFloat64 {
return math .MaxFloat64
}
tolerance := normalizedTolerance / maxCoefficient
if tolerance == 0 {
return math .SmallestNonzeroFloat64
}
return tolerance
}
func flattenScenePath(ctx context .Context , path d2scene .Path , transform d2scene .Matrix , count func () error ) ([]subpath , error ) {
tolerance := flattenTolerance (transform )
var result []subpath
var current subpath
var cursor , start d2scene .Point
haveCursor := false
flush := func (closed bool ) {
if len (current .points ) != 0 {
current .closed = closed
result = append (result , current )
}
current = subpath {}
}
startAt := func (point d2scene .Point ) error {
if !finitePoint (point ) {
return fmt .Errorf ("non-finite point" )
}
if err := count (); err != nil {
return err
}
current .points = append (current .points , point )
cursor , start , haveCursor = point , point , true
return nil
}
appendPoint := func (point d2scene .Point ) error {
if !finitePoint (point ) {
return fmt .Errorf ("non-finite point" )
}
if err := count (); err != nil {
return err
}
if len (current .points ) == 0 || !samePoint (current .points [len (current .points )-1 ], point ) {
current .points = append (current .points , point )
}
return nil
}
ensureCurrent := func () {
if len (current .points ) == 0 {
current .points = append (current .points , cursor )
}
}
for i , command := range path .Commands {
if err := ctx .Err (); err != nil {
return nil , err
}
switch command .Kind {
case d2scene .MoveCommand :
flush (false )
if err := startAt (command .P1 ); err != nil {
return nil , fmt .Errorf ("command %d: %w" , i , err )
}
case d2scene .LineCommand :
if !haveCursor {
return nil , fmt .Errorf ("command %d: line before move" , i )
}
ensureCurrent ()
if err := appendPoint (command .P1 ); err != nil {
return nil , fmt .Errorf ("command %d: %w" , i , err )
}
cursor = command .P1
case d2scene .QuadraticCommand :
if !haveCursor {
return nil , fmt .Errorf ("command %d: quadratic before move" , i )
}
if !finitePoint (command .P1 ) || !finitePoint (command .P2 ) {
return nil , fmt .Errorf ("command %d: non-finite point" , i )
}
ensureCurrent ()
if err := flattenQuadratic (ctx , cursor , command .P1 , command .P2 , tolerance , 0 , appendPoint ); err != nil {
return nil , fmt .Errorf ("command %d: %w" , i , err )
}
cursor = command .P2
case d2scene .CubicCommand :
if !haveCursor {
return nil , fmt .Errorf ("command %d: cubic before move" , i )
}
if !finitePoint (command .P1 ) || !finitePoint (command .P2 ) || !finitePoint (command .P3 ) {
return nil , fmt .Errorf ("command %d: non-finite point" , i )
}
ensureCurrent ()
if err := flattenCubic (ctx , cursor , command .P1 , command .P2 , command .P3 , tolerance , 0 , appendPoint ); err != nil {
return nil , fmt .Errorf ("command %d: %w" , i , err )
}
cursor = command .P3
case d2scene .ArcCommand :
if !haveCursor {
return nil , fmt .Errorf ("command %d: arc before move" , i )
}
if !finitePoint (command .P1 ) || !finite (command .RadiusX ) || !finite (command .RadiusY ) || !finite (command .Rotation ) {
return nil , fmt .Errorf ("command %d: non-finite arc" , i )
}
ensureCurrent ()
if err := flattenArc (ctx , cursor , command , transform , maxCurveErrorPixels , appendPoint ); err != nil {
return nil , fmt .Errorf ("command %d: %w" , i , err )
}
cursor = command .P1
case d2scene .CloseCommand :
if !haveCursor {
return nil , fmt .Errorf ("command %d: close before move" , i )
}
if err := count (); err != nil {
return nil , fmt .Errorf ("command %d: %w" , i , err )
}
flush (true )
cursor = start
haveCursor = true
default :
return nil , fmt .Errorf ("command %d: unknown kind %d" , i , command .Kind )
}
}
flush (false )
return result , nil
}
func roundedRectSubpaths(ctx context .Context , rect d2scene .Rect , tolerance float64 , count func () error ) ([]subpath , error ) {
b := rect .Box
if b .Width == 0 || b .Height == 0 {
return nil , nil
}
rx := math .Min (rect .RadiusX , b .Width /2 )
ry := math .Min (rect .RadiusY , b .Height /2 )
if rx == 0 || ry == 0 {
points := []d2scene .Point {
{X : b .X , Y : b .Y },
{X : b .X + b .Width , Y : b .Y },
{X : b .X + b .Width , Y : b .Y + b .Height },
{X : b .X , Y : b .Y + b .Height },
}
for range points {
if err := count (); err != nil {
return nil , err
}
}
return []subpath {{points : points , closed : true }}, nil
}
points := make ([]d2scene .Point , 0 , 8 )
appendPoint := func (point d2scene .Point ) error {
if err := count (); err != nil {
return err
}
if len (points ) == 0 || !samePoint (points [len (points )-1 ], point ) {
points = append (points , point )
}
return nil
}
if err := appendPoint (d2scene .Point {X : b .X + rx , Y : b .Y }); err != nil {
return nil , err
}
if err := appendPoint (d2scene .Point {X : b .X + b .Width - rx , Y : b .Y }); err != nil {
return nil , err
}
k := 0.5522847498307936
curves := [][4 ]d2scene .Point {
{{X : b .X + b .Width - rx , Y : b .Y }, {X : b .X + b .Width - rx + k *rx , Y : b .Y }, {X : b .X + b .Width , Y : b .Y + ry - k *ry }, {X : b .X + b .Width , Y : b .Y + ry }},
{{X : b .X + b .Width , Y : b .Y + b .Height - ry }, {X : b .X + b .Width , Y : b .Y + b .Height - ry + k *ry }, {X : b .X + b .Width - rx + k *rx , Y : b .Y + b .Height }, {X : b .X + b .Width - rx , Y : b .Y + b .Height }},
{{X : b .X + rx , Y : b .Y + b .Height }, {X : b .X + rx - k *rx , Y : b .Y + b .Height }, {X : b .X , Y : b .Y + b .Height - ry + k *ry }, {X : b .X , Y : b .Y + b .Height - ry }},
{{X : b .X , Y : b .Y + ry }, {X : b .X , Y : b .Y + ry - k *ry }, {X : b .X + rx - k *rx , Y : b .Y }, {X : b .X + rx , Y : b .Y }},
}
lines := []d2scene .Point {
{X : b .X + b .Width , Y : b .Y + b .Height - ry },
{X : b .X + rx , Y : b .Y + b .Height },
{X : b .X , Y : b .Y + ry },
}
for i , curve := range curves {
if err := ctx .Err (); err != nil {
return nil , err
}
if i != 0 {
if err := appendPoint (lines [i -1 ]); err != nil {
return nil , err
}
}
if err := flattenCubic (ctx , curve [0 ], curve [1 ], curve [2 ], curve [3 ], tolerance , 0 , appendPoint ); err != nil {
return nil , err
}
}
return []subpath {{points : points , closed : true }}, nil
}
func ellipseSubpaths(ctx context .Context , center d2scene .Point , rx , ry , tolerance float64 , count func () error ) ([]subpath , error ) {
if rx == 0 || ry == 0 {
return nil , nil
}
k := 0.5522847498307936
curves := [][4 ]d2scene .Point {
{{X : center .X + rx , Y : center .Y }, {X : center .X + rx , Y : center .Y + k *ry }, {X : center .X + k *rx , Y : center .Y + ry }, {X : center .X , Y : center .Y + ry }},
{{X : center .X , Y : center .Y + ry }, {X : center .X - k *rx , Y : center .Y + ry }, {X : center .X - rx , Y : center .Y + k *ry }, {X : center .X - rx , Y : center .Y }},
{{X : center .X - rx , Y : center .Y }, {X : center .X - rx , Y : center .Y - k *ry }, {X : center .X - k *rx , Y : center .Y - ry }, {X : center .X , Y : center .Y - ry }},
{{X : center .X , Y : center .Y - ry }, {X : center .X + k *rx , Y : center .Y - ry }, {X : center .X + rx , Y : center .Y - k *ry }, {X : center .X + rx , Y : center .Y }},
}
points := make ([]d2scene .Point , 0 , 32 )
appendPoint := func (point d2scene .Point ) error {
if err := count (); err != nil {
return err
}
if len (points ) == 0 || !samePoint (points [len (points )-1 ], point ) {
points = append (points , point )
}
return nil
}
if err := appendPoint (curves [0 ][0 ]); err != nil {
return nil , err
}
for _ , curve := range curves {
if err := flattenCubic (ctx , curve [0 ], curve [1 ], curve [2 ], curve [3 ], tolerance , 0 , appendPoint ); err != nil {
return nil , err
}
}
return []subpath {{points : points , closed : true }}, nil
}
func flattenQuadratic(ctx context .Context , p0 , p1 , p2 d2scene .Point , tolerance float64 , depth int , appendPoint func (d2scene .Point ) error ) error {
if err := ctx .Err (); err != nil {
return err
}
if pointLineDistance (p1 , p0 , p2 ) <= tolerance && controlsProgress (p0 , p2 , tolerance , p1 ) {
return appendPoint (p2 )
}
if depth >= maxCurveDepth {
return fmt .Errorf ("quadratic cannot be flattened within tolerance" )
}
p01 := midpoint (p0 , p1 )
p12 := midpoint (p1 , p2 )
p012 := midpoint (p01 , p12 )
if err := flattenQuadratic (ctx , p0 , p01 , p012 , tolerance , depth +1 , appendPoint ); err != nil {
return err
}
return flattenQuadratic (ctx , p012 , p12 , p2 , tolerance , depth +1 , appendPoint )
}
func flattenCubic(ctx context .Context , p0 , p1 , p2 , p3 d2scene .Point , tolerance float64 , depth int , appendPoint func (d2scene .Point ) error ) error {
if err := ctx .Err (); err != nil {
return err
}
if math .Max (pointLineDistance (p1 , p0 , p3 ), pointLineDistance (p2 , p0 , p3 )) <= tolerance &&
controlsProgress (p0 , p3 , tolerance , p1 , p2 ) {
return appendPoint (p3 )
}
if depth >= maxCurveDepth {
return fmt .Errorf ("cubic cannot be flattened within tolerance" )
}
p01 := midpoint (p0 , p1 )
p12 := midpoint (p1 , p2 )
p23 := midpoint (p2 , p3 )
p012 := midpoint (p01 , p12 )
p123 := midpoint (p12 , p23 )
p0123 := midpoint (p012 , p123 )
if err := flattenCubic (ctx , p0 , p01 , p012 , p0123 , tolerance , depth +1 , appendPoint ); err != nil {
return err
}
return flattenCubic (ctx , p0123 , p123 , p23 , p3 , tolerance , depth +1 , appendPoint )
}
func flattenArc(ctx context .Context , start d2scene .Point , command d2scene .PathCommand , transform d2scene .Matrix , tolerance float64 , appendPoint func (d2scene .Point ) error ) error {
if err := ctx .Err (); err != nil {
return err
}
if !finite (tolerance ) || tolerance <= 0 {
return fmt .Errorf ("invalid arc flattening tolerance" )
}
end := command .P1
if start == end {
return appendPoint (end )
}
if command .RadiusX == 0 || command .RadiusY == 0 {
return appendPoint (end )
}
arc , ok , err := d2scene .EndpointArcToCenter (start , command )
if err != nil {
return err
}
if !ok {
return appendPoint (end )
}
radiusBound := arc .TransformedRadiusBound (transform )
if !finite (radiusBound ) {
return fmt .Errorf ("transformed arc radius is outside the numeric domain" )
}
return flattenArcInterval (ctx , arc , arc .StartAngle (), arc .StartAngle ()+arc .DeltaAngle (), end , radiusBound , tolerance , 0 , appendPoint )
}
func flattenArcInterval(ctx context .Context , arc d2scene .CenterArc , theta0 , theta1 float64 , end d2scene .Point , radiusBound , tolerance float64 , depth int , appendPoint func (d2scene .Point ) error ) error {
if err := ctx .Err (); err != nil {
return err
}
span := math .Abs (theta1 - theta0 )
deviationFactor := span * span / 8
if deviationFactor == 0 || radiusBound <= tolerance /deviationFactor {
return appendPoint (end )
}
if depth >= maxCurveDepth {
return fmt .Errorf ("arc cannot be flattened within tolerance" )
}
middleTheta := theta0 + (theta1 -theta0 )/2
middle := arc .PointAt (middleTheta )
if !finitePoint (middle ) {
return fmt .Errorf ("arc is outside the numeric domain" )
}
if err := flattenArcInterval (ctx , arc , theta0 , middleTheta , middle , radiusBound , tolerance , depth +1 , appendPoint ); err != nil {
return err
}
return flattenArcInterval (ctx , arc , middleTheta , theta1 , end , radiusBound , tolerance , depth +1 , appendPoint )
}
func pointLineDistance(point , start , end d2scene .Point ) float64 {
dx , dy := end .X -start .X , end .Y -start .Y
length := math .Hypot (dx , dy )
if length == 0 {
return math .Hypot (point .X -start .X , point .Y -start .Y )
}
return math .Abs (dx *(start .Y -point .Y )-(start .X -point .X )*dy ) / length
}
func controlsProgress(start , end d2scene .Point , tolerance float64 , controls ...d2scene .Point ) bool {
dx , dy := end .X -start .X , end .Y -start .Y
length := math .Hypot (dx , dy )
if length == 0 {
for _ , control := range controls {
if math .Hypot (control .X -start .X , control .Y -start .Y ) > tolerance {
return false
}
}
return true
}
unitX , unitY := dx /length , dy /length
previous := 0.0
for _ , control := range controls {
controlX , controlY := control .X -start .X , control .Y -start .Y
projection := controlX *unitX + controlY *unitY
perpendicular := math .Abs (controlX *unitY - controlY *unitX )
excess := 0.0
if projection < 0 {
excess = -projection
} else if projection > length {
excess = projection - length
}
if !finite (projection ) || !finite (perpendicular ) || math .Hypot (perpendicular , excess ) > tolerance || projection < previous -tolerance {
return false
}
previous = projection
}
return true
}
func midpoint(a , b d2scene .Point ) d2scene .Point {
return d2scene .Point {X : (a .X + b .X ) / 2 , Y : (a .Y + b .Y ) / 2 }
}
func samePoint(a , b d2scene .Point ) bool {
return a == b
}
func interpolate(a , b d2scene .Point , t float64 ) d2scene .Point {
return d2scene .Point {X : a .X + (b .X -a .X )*t , Y : a .Y + (b .Y -a .Y )*t }
}
func appendDashPoint(points []d2scene .Point , point d2scene .Point ) []d2scene .Point {
if len (points ) == 3 && cap (points ) == 3 {
grown := make ([]d2scene .Point , 4 )
copy (grown , points )
grown [3 ] = point
return grown
}
return append (points , point )
}
func drawStroke(ctx context .Context , dst *image .RGBA , runs []strokeRun , transform d2scene .Matrix , stroke *preparedStroke , scratch *rasterScratch ) error {
if stroke .paint .kind == preparedSolidPaint {
rasterizer := scratch .reset (dst .Bounds ())
origin := setRasterizerReference (rasterizer , dst .Bounds ().Min , stroke .referenceBounds .Min , scratch .regionFilters )
shifted := d2scene .Translate (-float64 (origin .X ), -float64 (origin .Y )).Mul (transform )
for _ , run := range runs {
if err := ctx .Err (); err != nil {
return err
}
if err := addStrokeRun (ctx , rasterizer , run , shifted , stroke ); err != nil {
return err
}
}
if err := rasterizer .DrawRGBA (ctx , scratch .workBudget (), dst , stroke .paint .solid ); err != nil {
return fmt .Errorf ("d2raster: solid stroke: %w" , err )
}
return ctx .Err ()
}
bounds := paintedStrokePixelBounds (runs , transform , stroke , dst .Bounds ())
return drawRasterizedPaint (ctx , dst , bounds , stroke .paint , scratch , "gradient stroke" , func (rasterizer *scanline .Rasterizer ) error {
reference := bounds .Min
if scratch .regionFilters {
reference = paintedStrokePixelBounds (runs , transform , stroke , stroke .referenceBounds ).Min
}
origin := setRasterizerReference (rasterizer , bounds .Min , reference , scratch .regionFilters )
shifted := d2scene .Translate (-float64 (origin .X ), -float64 (origin .Y )).Mul (transform )
for _ , run := range runs {
if err := ctx .Err (); err != nil {
return err
}
if err := addStrokeRun (ctx , rasterizer , run , shifted , stroke ); err != nil {
return err
}
}
return ctx .Err ()
})
}
func paintedStrokePixelBounds(runs []strokeRun , transform d2scene .Matrix , stroke *preparedStroke , canvas image .Rectangle ) image .Rectangle {
return strokeRunPixelBounds (runs , transform , strokeExtent (stroke , transform ), canvas )
}
func strokeExtent(stroke *preparedStroke , transform d2scene .Matrix ) float64 {
expansion := stroke .width * transform .MaxScale () / 2
factor := 1.0
if stroke .cap == d2scene .CapSquare {
factor = math .Sqrt2
}
if stroke .join == d2scene .JoinMiter && stroke .miterLimit > factor {
factor = stroke .miterLimit
}
return expansion * factor
}
func strokeRunPixelBounds(runs []strokeRun , transform d2scene .Matrix , expansion float64 , canvas image .Rectangle ) image .Rectangle {
minX , minY := math .Inf (1 ), math .Inf (1 )
maxX , maxY := math .Inf (-1 ), math .Inf (-1 )
for _ , run := range runs {
for _ , local := range run .points {
point := transform .Point (local )
minX = math .Min (minX , point .X )
minY = math .Min (minY , point .Y )
maxX = math .Max (maxX , point .X )
maxY = math .Max (maxY , point .Y )
}
}
if math .IsInf (minX , 1 ) {
return image .Rectangle {}
}
expansion ++
minX , minY = minX -expansion , minY -expansion
maxX , maxY = maxX +expansion , maxY +expansion
if maxX < float64 (canvas .Min .X ) || maxY < float64 (canvas .Min .Y ) || minX > float64 (canvas .Max .X ) || minY > float64 (canvas .Max .Y ) {
return image .Rectangle {}
}
minX = math .Max (minX , float64 (canvas .Min .X ))
minY = math .Max (minY , float64 (canvas .Min .Y ))
maxX = math .Min (maxX , float64 (canvas .Max .X ))
maxY = math .Min (maxY , float64 (canvas .Max .Y ))
return image .Rect (int (math .Floor (minX )), int (math .Floor (minY )), int (math .Ceil (maxX )), int (math .Ceil (maxY ))).Intersect (canvas )
}
func makeStrokeRuns(ctx context .Context , path subpath , dashes []float64 , offset float64 , count func () error ) ([]strokeRun , error ) {
points := cleanPoints (path .points , path .closed )
if len (points ) < 2 {
return nil , nil
}
if len (dashes ) == 0 {
return []strokeRun {{points : points , closed : path .closed }}, nil
}
pattern := dashes
if len (pattern )%2 != 0 {
pattern = make ([]float64 , len (dashes )*2 )
copy (pattern , dashes )
copy (pattern [len (dashes ):], dashes )
}
total := 0.0
for _ , length := range pattern {
total += length
}
phase := math .Mod (offset , total )
if phase < 0 {
phase += total
}
patternIndex := 0
for phase >= pattern [patternIndex ] {
nextPhase := phase - pattern [patternIndex ]
if nextPhase == phase {
return nil , fmt .Errorf ("dash offset is too large relative to the dash pattern" )
}
phase = nextPhase
patternIndex = (patternIndex + 1 ) % len (pattern )
}
remaining := pattern [patternIndex ] - phase
on := patternIndex %2 == 0
initialOn := on
var runs []strokeRun
var current []d2scene .Point
edgeCount := len (points ) - 1
if path .closed {
edgeCount = len (points )
}
for edge := 0 ; edge < edgeCount ; edge ++ {
if err := ctx .Err (); err != nil {
return nil , err
}
start := points [edge ]
nextIndex := edge + 1
if nextIndex == len (points ) {
nextIndex = 0
}
end := points [nextIndex ]
dx , dy := end .X -start .X , end .Y -start .Y
length := math .Hypot (dx , dy )
if length == 0 {
continue
}
position := 0.0
for position < length {
if err := ctx .Err (); err != nil {
return nil , err
}
available := length - position
hitBoundary := remaining <= available
step := available
if hitBoundary {
step = remaining
}
nextPosition := position + step
if nextPosition <= position {
return nil , fmt .Errorf ("dash pattern is below the path's floating-point resolution" )
}
a := interpolate (start , end , position /length )
b := interpolate (start , end , nextPosition /length )
if on {
if len (current ) == 0 {
if err := count (); err != nil {
return nil , err
}
if samePoint (a , b ) {
current = append (current , a )
} else {
if err := count (); err != nil {
return nil , err
}
current = make ([]d2scene .Point , 2 , 3 )
current [0 ], current [1 ] = a , b
}
} else {
if !samePoint (current [len (current )-1 ], a ) {
if err := count (); err != nil {
return nil , err
}
current = appendDashPoint (current , a )
}
if !samePoint (current [len (current )-1 ], b ) {
if err := count (); err != nil {
return nil , err
}
current = appendDashPoint (current , b )
}
}
}
position = nextPosition
if hitBoundary {
if on && len (current ) >= 2 {
runs = append (runs , strokeRun {points : current })
current = nil
}
patternIndex = (patternIndex + 1 ) % len (pattern )
on = patternIndex %2 == 0
remaining = pattern [patternIndex ]
} else {
remaining -= step
}
}
}
if on && len (current ) >= 2 {
runs = append (runs , strokeRun {points : current })
}
if !path .closed || len (runs ) == 0 {
return runs , nil
}
origin := points [0 ]
firstStartsAtOrigin := samePoint (runs [0 ].points [0 ], origin )
lastRun := &runs [len (runs )-1 ]
lastEndsAtOrigin := samePoint (lastRun .points [len (lastRun .points )-1 ], origin )
if len (runs ) == 1 && firstStartsAtOrigin && lastEndsAtOrigin {
runs [0 ].points = runs [0 ].points [:len (runs [0 ].points )-1 ]
runs [0 ].closed = true
return runs , nil
}
if initialOn && firstStartsAtOrigin && lastEndsAtOrigin && len (runs ) > 1 {
last := runs [len (runs )-1 ].points
first := runs [0 ].points
merged := make ([]d2scene .Point , 0 , len (last )+len (first )-1 )
merged = append (merged , last ...)
merged = append (merged , first [1 :]...)
runs [0 ] = strokeRun {points : merged }
runs = runs [:len (runs )-1 ]
}
return runs , nil
}
func addStrokeRun(ctx context .Context , rasterizer *scanline .Rasterizer , run strokeRun , transform d2scene .Matrix , stroke *preparedStroke ) error {
points := run .points
halfWidth := stroke .width / 2
if len (points ) < 2 || halfWidth <= 0 {
return nil
}
edgeCount := len (points ) - 1
if run .closed {
edgeCount = len (points )
}
for edge := 0 ; edge < edgeCount ; edge ++ {
if err := ctx .Err (); err != nil {
return err
}
start := points [edge ]
nextIndex := edge + 1
if nextIndex == len (points ) {
nextIndex = 0
}
end := points [nextIndex ]
dx , dy := end .X -start .X , end .Y -start .Y
length := math .Hypot (dx , dy )
if length == 0 {
continue
}
unitX , unitY := dx /length , dy /length
if !run .closed && stroke .cap == d2scene .CapSquare {
if edge == 0 {
start .X -= unitX * halfWidth
start .Y -= unitY * halfWidth
}
if edge == edgeCount -1 {
end .X += unitX * halfWidth
end .Y += unitY * halfWidth
}
}
normal := d2scene .Point {X : -unitY * halfWidth , Y : unitX * halfWidth }
addPolygon (rasterizer , transform , []d2scene .Point {
{X : start .X + normal .X , Y : start .Y + normal .Y },
{X : end .X + normal .X , Y : end .Y + normal .Y },
{X : end .X - normal .X , Y : end .Y - normal .Y },
{X : start .X - normal .X , Y : start .Y - normal .Y },
})
}
joinStart , joinEnd := 1 , len (points )-1
if run .closed {
joinStart , joinEnd = 0 , len (points )
}
switch stroke .join {
case d2scene .JoinRound :
for index := joinStart ; index < joinEnd ; index ++ {
if err := ctx .Err (); err != nil {
return err
}
addCircle (rasterizer , transform , points [index ], halfWidth )
}
case d2scene .JoinMiter , d2scene .JoinBevel :
previousIndex := joinStart - 1
if previousIndex < 0 {
previousIndex = len (points ) - 1
}
incoming , incomingOK := unitVector (points [previousIndex ], points [joinStart ])
for index := joinStart ; index < joinEnd ; index ++ {
if err := ctx .Err (); err != nil {
return err
}
nextIndex := index + 1
if nextIndex == len (points ) {
nextIndex = 0
}
vertex := points [index ]
outgoing , outgoingOK := unitVector (vertex , points [nextIndex ])
var polygon [4 ]d2scene .Point
if count := strokeJoinPolygonFromUnits (&polygon , vertex , incoming , outgoing , incomingOK && outgoingOK , halfWidth , stroke .join , stroke .miterLimit ); count != 0 {
addPolygon (rasterizer , transform , polygon [:count ])
}
incoming , incomingOK = outgoing , outgoingOK
}
}
if !run .closed && stroke .cap == d2scene .CapRound {
addCircle (rasterizer , transform , points [0 ], halfWidth )
addCircle (rasterizer , transform , points [len (points )-1 ], halfWidth )
}
return nil
}
func strokeJoinPolygonFromUnits(destination *[4 ]d2scene .Point , vertex , incoming , outgoing d2scene .Point , unitsOK bool , halfWidth float64 , join d2scene .LineJoin , miterLimit float64 ) int {
if !unitsOK {
return 0
}
turn := crossProduct (incoming , outgoing )
if math .Abs (turn ) <= geometryEpsilon {
return 0
}
side := 1.0
if turn > 0 {
side = -1
}
incomingNormal := d2scene .Point {X : -incoming .Y * halfWidth * side , Y : incoming .X * halfWidth * side }
outgoingNormal := d2scene .Point {X : -outgoing .Y * halfWidth * side , Y : outgoing .X * halfWidth * side }
outerIncoming := d2scene .Point {X : vertex .X + incomingNormal .X , Y : vertex .Y + incomingNormal .Y }
outerOutgoing := d2scene .Point {X : vertex .X + outgoingNormal .X , Y : vertex .Y + outgoingNormal .Y }
destination [0 ], destination [1 ], destination [2 ] = outerIncoming , vertex , outerOutgoing
if join != d2scene .JoinMiter {
return 3
}
denominator := crossProduct (incoming , outgoing )
delta := d2scene .Point {X : outerOutgoing .X - outerIncoming .X , Y : outerOutgoing .Y - outerIncoming .Y }
distanceAlongIncoming := crossProduct (delta , outgoing ) / denominator
miter := d2scene .Point {
X : outerIncoming .X + distanceAlongIncoming *incoming .X ,
Y : outerIncoming .Y + distanceAlongIncoming *incoming .Y ,
}
miterDistance := math .Hypot (miter .X -vertex .X , miter .Y -vertex .Y )
ratio := miterDistance / halfWidth
if !finitePoint (miter ) || !finite (ratio ) || ratio > miterLimit {
return 3
}
destination [0 ], destination [1 ], destination [2 ], destination [3 ] = outerIncoming , miter , outerOutgoing , vertex
return 4
}
func unitVector(start , end d2scene .Point ) (d2scene .Point , bool ) {
dx , dy := end .X -start .X , end .Y -start .Y
length := math .Hypot (dx , dy )
if !finite (length ) || length == 0 {
return d2scene .Point {}, false
}
return d2scene .Point {X : dx / length , Y : dy / length }, true
}
func crossProduct(a , b d2scene .Point ) float64 {
return a .X *b .Y - a .Y *b .X
}
func addPolygon(rasterizer *scanline .Rasterizer , transform d2scene .Matrix , points []d2scene .Point ) {
if len (points ) < 3 {
return
}
area := transformedPolygonSignedArea2 (points , transform )
if area == 0 || !finite (area ) {
return
}
wantNegative := transform .Determinant () >= 0
reverse := (area < 0 ) != wantNegative
firstIndex := 0
if reverse {
firstIndex = len (points ) - 1
}
first := transform .Point (points [firstIndex ])
rasterizer .MoveTo64 (first .X , first .Y )
if reverse {
for index := len (points ) - 2 ; index >= 0 ; index -- {
point := transform .Point (points [index ])
rasterizer .LineTo64 (point .X , point .Y )
}
} else {
for _ , localPoint := range points [1 :] {
point := transform .Point (localPoint )
rasterizer .LineTo64 (point .X , point .Y )
}
}
rasterizer .ClosePath ()
}
func transformedPolygonSignedArea2(points []d2scene .Point , transform d2scene .Matrix ) float64 {
if len (points ) < 3 {
return 0
}
origin := transform .Point (points [0 ])
area := 0.0
for index := 1 ; index +1 < len (points ); index ++ {
pointA := transform .Point (points [index ])
pointB := transform .Point (points [index +1 ])
a := d2scene .Point {X : pointA .X - origin .X , Y : pointA .Y - origin .Y }
b := d2scene .Point {X : pointB .X - origin .X , Y : pointB .Y - origin .Y }
area += crossProduct (a , b )
}
return area
}
func addCircle(rasterizer *scanline .Rasterizer , transform d2scene .Matrix , center d2scene .Point , radius float64 ) {
k := 0.5522847498307936 * radius
start := transform .Point (d2scene .Point {X : center .X + radius , Y : center .Y })
rasterizer .MoveTo64 (start .X , start .Y )
c1 := transform .Point (d2scene .Point {X : center .X + radius , Y : center .Y - k })
c2 := transform .Point (d2scene .Point {X : center .X + k , Y : center .Y - radius })
end := transform .Point (d2scene .Point {X : center .X , Y : center .Y - radius })
rasterizer .CubeTo64 (c1 .X , c1 .Y , c2 .X , c2 .Y , end .X , end .Y )
c1 = transform .Point (d2scene .Point {X : center .X - k , Y : center .Y - radius })
c2 = transform .Point (d2scene .Point {X : center .X - radius , Y : center .Y - k })
end = transform .Point (d2scene .Point {X : center .X - radius , Y : center .Y })
rasterizer .CubeTo64 (c1 .X , c1 .Y , c2 .X , c2 .Y , end .X , end .Y )
c1 = transform .Point (d2scene .Point {X : center .X - radius , Y : center .Y + k })
c2 = transform .Point (d2scene .Point {X : center .X - k , Y : center .Y + radius })
end = transform .Point (d2scene .Point {X : center .X , Y : center .Y + radius })
rasterizer .CubeTo64 (c1 .X , c1 .Y , c2 .X , c2 .Y , end .X , end .Y )
c1 = transform .Point (d2scene .Point {X : center .X + k , Y : center .Y + radius })
c2 = transform .Point (d2scene .Point {X : center .X + radius , Y : center .Y + k })
end = transform .Point (d2scene .Point {X : center .X + radius , Y : center .Y })
rasterizer .CubeTo64 (c1 .X , c1 .Y , c2 .X , c2 .Y , end .X , end .Y )
rasterizer .ClosePath ()
}
func cleanPoints(points []d2scene .Point , closed bool ) []d2scene .Point {
firstDuplicate := -1
for index := 1 ; index < len (points ); index ++ {
if samePoint (points [index -1 ], points [index ]) {
firstDuplicate = index
break
}
}
if firstDuplicate < 0 {
if closed && len (points ) > 1 && samePoint (points [0 ], points [len (points )-1 ]) {
return points [:len (points )-1 ]
}
return points
}
cleaned := make ([]d2scene .Point , 0 , len (points ))
cleaned = append (cleaned , points [:firstDuplicate ]...)
for _ , point := range points [firstDuplicate :] {
if len (cleaned ) == 0 || !samePoint (cleaned [len (cleaned )-1 ], point ) {
cleaned = append (cleaned , point )
}
}
if closed && len (cleaned ) > 1 && samePoint (cleaned [0 ], cleaned [len (cleaned )-1 ]) {
cleaned = cleaned [:len (cleaned )-1 ]
}
return cleaned
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .