VideoTools/vendor/fyne.io/fyne/v2/internal/painter/draw.go
Stu Leak 68df790d27 Fix player frame generation and video playback
Major improvements to UnifiedPlayer:

1. GetFrameImage() now works when paused for responsive UI updates
2. Play() method properly starts FFmpeg process
3. Frame display loop runs continuously for smooth video display
4. Disabled audio temporarily to fix video playback fundamentals
5. Simplified FFmpeg command to focus on video stream only

Player now:
- Generates video frames correctly
- Shows video when paused
- Has responsive progress tracking
- Starts playback properly

Next steps: Re-enable audio playback once video is stable
2026-01-07 22:20:00 -05:00

674 lines
26 KiB
Go

package painter
import (
"image"
"image/color"
"math"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"github.com/srwiley/rasterx"
"golang.org/x/image/math/fixed"
)
const quarterCircleControl = 1 - 0.55228
// DrawArc rasterizes the given arc object into an image.
// The scale function is used to understand how many pixels are required per unit of size.
// The arc is drawn from StartAngle to EndAngle (in degrees).
// 0°/360 is top, 90° is right, 180° is bottom, 270° is left
// 0°/-360 is top, -90° is left, -180° is bottom, -270° is right
func DrawArc(arc *canvas.Arc, vectorPad float32, scale func(float32) float32) *image.RGBA {
size := arc.Size()
width := int(scale(size.Width + vectorPad*2))
height := int(scale(size.Height + vectorPad*2))
raw := image.NewRGBA(image.Rect(0, 0, width, height))
scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds())
centerX := float64(width) / 2
centerY := float64(height) / 2
outerRadius := fyne.Min(size.Width, size.Height) / 2
innerRadius := float32(float64(outerRadius) * math.Min(1.0, math.Max(0.0, float64(arc.CutoutRatio))))
cornerRadius := fyne.Min(GetMaximumRadiusArc(outerRadius, innerRadius, arc.EndAngle-arc.StartAngle), arc.CornerRadius)
startAngle, endAngle := NormalizeArcAngles(arc.StartAngle, arc.EndAngle)
// convert to radians
startRad := float64(startAngle * math.Pi / 180.0)
endRad := float64(endAngle * math.Pi / 180.0)
sweep := endRad - startRad
if sweep == 0 {
// nothing to draw
return raw
}
if sweep > 2*math.Pi {
sweep = 2 * math.Pi
} else if sweep < -2*math.Pi {
sweep = -2 * math.Pi
}
cornerRadius = scale(cornerRadius)
outerRadius = scale(outerRadius)
innerRadius = scale(innerRadius)
if arc.FillColor != nil {
filler := rasterx.NewFiller(width, height, scanner)
filler.SetColor(arc.FillColor)
// rasterx.AddArc is not used because it does not support rounded corners
drawRoundArc(filler, centerX, centerY, float64(outerRadius), float64(innerRadius), startRad, sweep, float64(cornerRadius))
filler.Draw()
}
stroke := float64(scale(arc.StrokeWidth))
if arc.StrokeColor != nil && stroke > 0 {
dasher := rasterx.NewDasher(width, height, scanner)
dasher.SetColor(arc.StrokeColor)
dasher.SetStroke(fixed.Int26_6(stroke*64), 0, nil, nil, nil, 0, nil, 0)
// rasterx.AddArc is not used because it does not support rounded corners
drawRoundArc(dasher, centerX, centerY, float64(outerRadius), float64(innerRadius), startRad, sweep, float64(cornerRadius))
dasher.Draw()
}
return raw
}
// DrawCircle rasterizes the given circle object into an image.
// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
// The scale function is used to understand how many pixels are required per unit of size.
func DrawCircle(circle *canvas.Circle, vectorPad float32, scale func(float32) float32) *image.RGBA {
size := circle.Size()
radius := GetMaximumRadius(size)
width := int(scale(size.Width + vectorPad*2))
height := int(scale(size.Height + vectorPad*2))
stroke := scale(circle.StrokeWidth)
raw := image.NewRGBA(image.Rect(0, 0, width, height))
scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds())
if circle.FillColor != nil {
filler := rasterx.NewFiller(width, height, scanner)
filler.SetColor(circle.FillColor)
rasterx.AddCircle(float64(width/2), float64(height/2), float64(scale(radius)), filler)
filler.Draw()
}
dasher := rasterx.NewDasher(width, height, scanner)
dasher.SetColor(circle.StrokeColor)
dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0)
rasterx.AddCircle(float64(width/2), float64(height/2), float64(scale(radius)), dasher)
dasher.Draw()
return raw
}
// DrawLine rasterizes the given line object into an image.
// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
// The scale function is used to understand how many pixels are required per unit of size.
func DrawLine(line *canvas.Line, vectorPad float32, scale func(float32) float32) *image.RGBA {
col := line.StrokeColor
size := line.Size()
width := int(scale(size.Width + vectorPad*2))
height := int(scale(size.Height + vectorPad*2))
stroke := scale(line.StrokeWidth)
if stroke < 1 { // software painter doesn't fade lines to compensate
stroke = 1
}
raw := image.NewRGBA(image.Rect(0, 0, width, height))
scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds())
dasher := rasterx.NewDasher(width, height, scanner)
dasher.SetColor(col)
dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0)
position := line.Position()
p1x, p1y := scale(line.Position1.X-position.X+vectorPad), scale(line.Position1.Y-position.Y+vectorPad)
p2x, p2y := scale(line.Position2.X-position.X+vectorPad), scale(line.Position2.Y-position.Y+vectorPad)
if stroke <= 1.5 { // adjust to support 1px
if p1x == p2x {
p1x -= 0.5
p2x -= 0.5
}
if p1y == p2y {
p1y -= 0.5
p2y -= 0.5
}
}
dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y)))
dasher.Line(rasterx.ToFixedP(float64(p2x), float64(p2y)))
dasher.Stop(true)
dasher.Draw()
return raw
}
// DrawPolygon rasterizes the given regular polygon object into an image.
// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
// The scale function is used to understand how many pixels are required per unit of size.
func DrawPolygon(polygon *canvas.Polygon, vectorPad float32, scale func(float32) float32) *image.RGBA {
size := polygon.Size()
width := int(scale(size.Width + vectorPad*2))
height := int(scale(size.Height + vectorPad*2))
outerRadius := scale(fyne.Min(size.Width, size.Height) / 2)
cornerRadius := scale(fyne.Min(GetMaximumRadius(size), polygon.CornerRadius))
sides := int(polygon.Sides)
angle := polygon.Angle
raw := image.NewRGBA(image.Rect(0, 0, width, height))
scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds())
if polygon.FillColor != nil {
filler := rasterx.NewFiller(width, height, scanner)
filler.SetColor(polygon.FillColor)
drawRegularPolygon(float64(width/2), float64(height/2), float64(outerRadius), float64(cornerRadius), float64(angle), int(sides), filler)
filler.Draw()
}
if polygon.StrokeColor != nil && polygon.StrokeWidth > 0 {
dasher := rasterx.NewDasher(width, height, scanner)
dasher.SetColor(polygon.StrokeColor)
dasher.SetStroke(fixed.Int26_6(float64(scale(polygon.StrokeWidth))*64), 0, nil, nil, nil, 0, nil, 0)
drawRegularPolygon(float64(width/2), float64(height/2), float64(outerRadius), float64(cornerRadius), float64(angle), int(sides), dasher)
dasher.Draw()
}
return raw
}
// DrawRectangle rasterizes the given rectangle object with stroke border into an image.
// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges.
// The scale function is used to understand how many pixels are required per unit of size.
func DrawRectangle(rect *canvas.Rectangle, rWidth, rHeight, vectorPad float32, scale func(float32) float32) *image.RGBA {
topRightRadius := GetCornerRadius(rect.TopRightCornerRadius, rect.CornerRadius)
topLeftRadius := GetCornerRadius(rect.TopLeftCornerRadius, rect.CornerRadius)
bottomRightRadius := GetCornerRadius(rect.BottomRightCornerRadius, rect.CornerRadius)
bottomLeftRadius := GetCornerRadius(rect.BottomLeftCornerRadius, rect.CornerRadius)
return drawOblong(rect.FillColor, rect.StrokeColor, rect.StrokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, rWidth, rHeight, vectorPad, scale)
}
func drawOblong(fill, strokeCol color.Color, strokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, rWidth, rHeight, vectorPad float32, scale func(float32) float32) *image.RGBA {
// The maximum possible corner radii for a circular shape
size := fyne.NewSize(rWidth, rHeight)
topRightRadius = GetMaximumCornerRadius(topRightRadius, topLeftRadius, bottomRightRadius, size)
topLeftRadius = GetMaximumCornerRadius(topLeftRadius, topRightRadius, bottomLeftRadius, size)
bottomRightRadius = GetMaximumCornerRadius(bottomRightRadius, bottomLeftRadius, topRightRadius, size)
bottomLeftRadius = GetMaximumCornerRadius(bottomLeftRadius, bottomRightRadius, topLeftRadius, size)
width := int(scale(rWidth + vectorPad*2))
height := int(scale(rHeight + vectorPad*2))
stroke := scale(strokeWidth)
raw := image.NewRGBA(image.Rect(0, 0, width, height))
scanner := rasterx.NewScannerGV(int(rWidth), int(rHeight), raw, raw.Bounds())
scaledPad := scale(vectorPad)
p1x, p1y := scaledPad, scaledPad
p2x, p2y := scale(rWidth)+scaledPad, scaledPad
p3x, p3y := scale(rWidth)+scaledPad, scale(rHeight)+scaledPad
p4x, p4y := scaledPad, scale(rHeight)+scaledPad
if fill != nil {
filler := rasterx.NewFiller(width, height, scanner)
filler.SetColor(fill)
if topRightRadius == topLeftRadius && bottomRightRadius == bottomLeftRadius && topRightRadius == bottomRightRadius {
// If all corners are the same, we can draw a simple rectangle
radius := topRightRadius
if radius == 0 {
rasterx.AddRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), 0, filler)
} else {
r := float64(scale(radius))
rasterx.AddRoundRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), r, r, 0, rasterx.RoundGap, filler)
}
} else {
rTL, rTR, rBR, rBL := scale(topLeftRadius), scale(topRightRadius), scale(bottomRightRadius), scale(bottomLeftRadius)
// Top-left corner
c := quarterCircleControl * rTL
if c != 0 {
filler.Start(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL)))
filler.CubeBezier(rasterx.ToFixedP(float64(p1x), float64(p1y+c)), rasterx.ToFixedP(float64(p1x+c), float64(p1y)), rasterx.ToFixedP(float64(p1x+rTL), float64(p1y)))
} else {
filler.Start(rasterx.ToFixedP(float64(p1x), float64(p1y)))
}
// Top edge to top-right
c = quarterCircleControl * rTR
filler.Line(rasterx.ToFixedP(float64(p2x-rTR), float64(p2y)))
if c != 0 {
filler.CubeBezier(rasterx.ToFixedP(float64(p2x-c), float64(p2y)), rasterx.ToFixedP(float64(p2x), float64(p2y+c)), rasterx.ToFixedP(float64(p2x), float64(p2y+rTR)))
}
// Right edge to bottom-right
c = quarterCircleControl * rBR
filler.Line(rasterx.ToFixedP(float64(p3x), float64(p3y-rBR)))
if c != 0 {
filler.CubeBezier(rasterx.ToFixedP(float64(p3x), float64(p3y-c)), rasterx.ToFixedP(float64(p3x-c), float64(p3y)), rasterx.ToFixedP(float64(p3x-rBR), float64(p3y)))
}
// Bottom edge to bottom-left
c = quarterCircleControl * rBL
filler.Line(rasterx.ToFixedP(float64(p4x+rBL), float64(p4y)))
if c != 0 {
filler.CubeBezier(rasterx.ToFixedP(float64(p4x+c), float64(p4y)), rasterx.ToFixedP(float64(p4x), float64(p4y-c)), rasterx.ToFixedP(float64(p4x), float64(p4y-rBL)))
}
// Left edge to top-left
filler.Line(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL)))
filler.Stop(true)
}
filler.Draw()
}
if strokeCol != nil && strokeWidth > 0 {
dasher := rasterx.NewDasher(width, height, scanner)
dasher.SetColor(strokeCol)
dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0)
rTL, rTR, rBR, rBL := scale(topLeftRadius), scale(topRightRadius), scale(bottomRightRadius), scale(bottomLeftRadius)
c := quarterCircleControl * rTL
if c != 0 {
dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL)))
dasher.CubeBezier(rasterx.ToFixedP(float64(p1x), float64(p1y+c)), rasterx.ToFixedP(float64(p1x+c), float64(p1y)), rasterx.ToFixedP(float64(p1x+rTL), float64(p2y)))
} else {
dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y)))
}
c = quarterCircleControl * rTR
dasher.Line(rasterx.ToFixedP(float64(p2x-rTR), float64(p2y)))
if c != 0 {
dasher.CubeBezier(rasterx.ToFixedP(float64(p2x-c), float64(p2y)), rasterx.ToFixedP(float64(p2x), float64(p2y+c)), rasterx.ToFixedP(float64(p2x), float64(p2y+rTR)))
}
c = quarterCircleControl * rBR
dasher.Line(rasterx.ToFixedP(float64(p3x), float64(p3y-rBR)))
if c != 0 {
dasher.CubeBezier(rasterx.ToFixedP(float64(p3x), float64(p3y-c)), rasterx.ToFixedP(float64(p3x-c), float64(p3y)), rasterx.ToFixedP(float64(p3x-rBR), float64(p3y)))
}
c = quarterCircleControl * rBL
dasher.Line(rasterx.ToFixedP(float64(p4x+rBL), float64(p4y)))
if c != 0 {
dasher.CubeBezier(rasterx.ToFixedP(float64(p4x+c), float64(p4y)), rasterx.ToFixedP(float64(p4x), float64(p4y-c)), rasterx.ToFixedP(float64(p4x), float64(p4y-rBL)))
}
dasher.Stop(true)
dasher.Draw()
}
return raw
}
// drawRegularPolygon draws a regular n-sides centered at (cx,cy) with
// radius, rounded corners of cornerRadius, rotated by rot degrees.
func drawRegularPolygon(cx, cy, radius, cornerRadius, rot float64, sides int, p rasterx.Adder) {
if sides < 3 || radius <= 0 {
return
}
gf := rasterx.RoundGap
angleStep := 2 * math.Pi / float64(sides)
rotRads := rot*math.Pi/180 - math.Pi/2
// fully rounded, draw circle
if math.Min(cornerRadius, radius) == radius {
rasterx.AddCircle(cx, cy, radius, p)
return
}
// sharp polygon fast path
if cornerRadius <= 0 {
x0 := cx + radius*math.Cos(rotRads)
y0 := cy + radius*math.Sin(rotRads)
p.Start(rasterx.ToFixedP(x0, y0))
for i := 1; i < sides; i++ {
t := rotRads + angleStep*float64(i)
p.Line(rasterx.ToFixedP(cx+radius*math.Cos(t), cy+radius*math.Sin(t)))
}
p.Stop(true)
return
}
norm := func(x, y float64) (nx, ny float64) {
l := math.Hypot(x, y)
if l == 0 {
return 0, 0
}
return x / l, y / l
}
// regular polygon vertices
xs := make([]float64, sides)
ys := make([]float64, sides)
for i := 0; i < sides; i++ {
t := rotRads + angleStep*float64(i)
xs[i] = cx + radius*math.Cos(t)
ys[i] = cy + radius*math.Sin(t)
}
// interior angle and side length
alpha := math.Pi * (float64(sides) - 2) / float64(sides)
r := cornerRadius
// distances for tangency and center placement
tTrim := r / math.Tan(alpha/2) // along each edge from vertex to tangency
d := r / math.Sin(alpha/2) // from vertex to arc center along interior bisector
// precompute fillet geometry per vertex
type pt struct{ x, y float64 }
sPts := make([]pt, sides) // start tangency (on incoming edge)
vS := make([]pt, sides) // center->start vector
vE := make([]pt, sides) // center->end vector
cPts := make([]pt, sides) // arc centers
for i := 0; i < sides; i++ {
prv := (i - 1 + sides) % sides
nxt := (i + 1) % sides
// unit directions
uInX, uInY := xs[i]-xs[prv], ys[i]-ys[prv] // prev -> i
uOutX, uOutY := xs[nxt]-xs[i], ys[nxt]-ys[i] // i -> next
uInX, uInY = norm(uInX, uInY)
uOutX, uOutY = norm(uOutX, uOutY)
// tangency points along edges from the vertex
sx, sy := xs[i]-uInX*tTrim, ys[i]-uInY*tTrim // incoming (toward prev)
ex, ey := xs[i]+uOutX*tTrim, ys[i]+uOutY*tTrim // outgoing (toward next)
// interior bisector direction and arc center
bx, by := -uInX+uOutX, -uInY+uOutY
bx, by = norm(bx, by)
cxI, cyI := xs[i]+bx*d, ys[i]+by*d
// center->tangent vectors
vsx, vsy := sx-cxI, sy-cyI
velx, vely := ex-cxI, ey-cyI
sPts[i] = pt{sx, sy}
vS[i] = pt{vsx, vsy}
vE[i] = pt{velx, vely}
cPts[i] = pt{cxI, cyI}
}
// start at s0, arc corner 0, then line+arc around, close last edge
p.Start(rasterx.ToFixedP(sPts[0].x, sPts[0].y))
gf(p,
rasterx.ToFixedP(cPts[0].x, cPts[0].y),
rasterx.ToFixedP(vS[0].x, vS[0].y),
rasterx.ToFixedP(vE[0].x, vE[0].y),
)
for i := 1; i < sides; i++ {
p.Line(rasterx.ToFixedP(sPts[i].x, sPts[i].y))
gf(p,
rasterx.ToFixedP(cPts[i].x, cPts[i].y),
rasterx.ToFixedP(vS[i].x, vS[i].y),
rasterx.ToFixedP(vE[i].x, vE[i].y),
)
}
p.Line(rasterx.ToFixedP(sPts[0].x, sPts[0].y))
p.Stop(true)
}
// drawRoundArc constructs a rounded pie slice or annular sector
// it uses the Unit circle coordinate system
func drawRoundArc(adder rasterx.Adder, cx, cy, outer, inner, start, sweep, cr float64) {
if sweep == 0 {
return
}
cosSinPoint := func(cx, cy, r, ang float64) (x, y float64) {
return cx + r*math.Cos(ang), cy - r*math.Sin(ang)
}
// addCircularArc appends a circular arc to the current path using cubic Bezier approximation.
// 'adder' must already be positioned at the arc start point.
// sweep is signed (positive = CCW, negative = CW).
addCircularArc := func(adder rasterx.Adder, cx, cy, r, start, sweep float64) {
if sweep == 0 || r == 0 {
return
}
segCount := int(math.Ceil(math.Abs(sweep) / (math.Pi / 2.0)))
da := sweep / float64(segCount)
for i := 0; i < segCount; i++ {
a1 := start + float64(i)*da
a2 := a1 + da
x1, y1 := cosSinPoint(cx, cy, r, a1)
x2, y2 := cosSinPoint(cx, cy, r, a2)
k := 4.0 / 3.0 * math.Tan((a2-a1)/4.0)
// tangent unit vectors on our param (x = cx+rcos, y = cy-rsin)
c1x := x1 + k*r*(-math.Sin(a1))
c1y := y1 + k*r*(-math.Cos(a1))
c2x := x2 - k*r*(-math.Sin(a2))
c2y := y2 - k*r*(-math.Cos(a2))
adder.CubeBezier(
rasterx.ToFixedP(c1x, c1y),
rasterx.ToFixedP(c2x, c2y),
rasterx.ToFixedP(x2, y2),
)
}
}
// full-circle/donut paths (two closed subpaths: outer CCW, inner CW if inner > 0)
if math.Abs(sweep) >= 2*math.Pi {
// outer loop (CCW)
ox, oy := cosSinPoint(cx, cy, outer, 0)
adder.Start(rasterx.ToFixedP(ox, oy))
addCircularArc(adder, cx, cy, outer, 0, 2*math.Pi)
adder.Stop(true)
// inner loop reversed (CW) to create a hole
if inner > 0 {
ix, iy := cosSinPoint(cx, cy, inner, 0)
adder.Start(rasterx.ToFixedP(ix, iy))
addCircularArc(adder, cx, cy, inner, 0, -2*math.Pi)
adder.Stop(true)
}
return
}
if cr <= 0 {
// sharp-corner fallback
if inner <= 0 {
// pie slice
ox, oy := cosSinPoint(cx, cy, outer, start)
adder.Start(rasterx.ToFixedP(cx, cy))
adder.Line(rasterx.ToFixedP(ox, oy))
addCircularArc(adder, cx, cy, outer, start, sweep)
adder.Line(rasterx.ToFixedP(cx, cy))
adder.Stop(true)
return
}
// annular sector
outerStartX, outerStartY := cosSinPoint(cx, cy, outer, start)
adder.Start(rasterx.ToFixedP(outerStartX, outerStartY))
addCircularArc(adder, cx, cy, outer, start, sweep)
innerEndX, innerEndY := cosSinPoint(cx, cy, inner, start+sweep)
adder.Line(rasterx.ToFixedP(innerEndX, innerEndY))
addCircularArc(adder, cx, cy, inner, start+sweep, -sweep)
adder.Stop(true)
return
}
// rounded corners
sgn := 1.0
if sweep < 0 {
sgn = -1.0
}
absSweep := math.Abs(sweep)
// clamp the corner radius if the value is too large
cr = math.Min(cr, outer/2)
// trim angles due to rounds
sOut := math.Sqrt(math.Max(0, outer*(outer-2*cr)))
thetaOut := math.Atan2(cr, sOut) // positive
crIn := math.Min(cr, 0.5*math.Min(outer-inner, math.Abs(sweep)*inner))
var sIn, thetaIn float64
if inner > 0 {
sIn = math.Sqrt(math.Max(0, inner*(inner+2*crIn)))
thetaIn = math.Atan2(crIn, sIn)
}
// ensure the trim does not exceed half the sweep
thetaOut = math.Min(thetaOut, absSweep/2.0-1e-6)
if thetaOut < 0 {
thetaOut = 0
}
if inner > 0 {
thetaIn = math.Min(thetaIn, absSweep/2.0-1e-6)
if thetaIn < 0 {
thetaIn = 0
}
}
// trimmed arc angles
startOuter := start + sgn*thetaOut
endOuter := start + sweep - sgn*thetaOut
startInner := 0.0
endInner := 0.0
if inner > 0 {
startInner = start + sgn*thetaIn
endInner = start + sweep - sgn*thetaIn
}
// direction frames at start/end radial lines
// start side
vSx, vSy := math.Cos(start), -math.Sin(start)
tSx, tSy := -math.Sin(start), -math.Cos(start)
nSx, nSy := sgn*tSx, sgn*tSy // interior side normal at start
// end side
endRad := start + sweep
vEx, vEy := math.Cos(endRad), -math.Sin(endRad)
tEx, tEy := -math.Sin(endRad), -math.Cos(endRad)
nEx, nEy := -sgn*tEx, -sgn*tEy // interior side normal at end
// key points on arcs
pOutStartX, pOutStartY := cosSinPoint(cx, cy, outer, startOuter)
pOutEndX, pOutEndY := cosSinPoint(cx, cy, outer, endOuter)
var pInStartX, pInStartY, pInEndX, pInEndY float64
if inner > 0 {
pInStartX, pInStartY = cosSinPoint(cx, cy, inner, startInner)
pInEndX, pInEndY = cosSinPoint(cx, cy, inner, endInner)
}
angleAt := func(cx, cy, x, y float64) float64 {
return math.Atan2(cy-y, x-cx)
}
// round geometry at start/end
// outer rounds
aOutSx, aOutSy := cx+sOut*vSx, cy+sOut*vSy // radial tangent (start)
fOutSx, fOutSy := aOutSx+cr*nSx, aOutSy+cr*nSy // round center (start)
aOutEx, aOutEy := cx+sOut*vEx, cy+sOut*vEy // radial tangent (end)
fOutEx, fOutEy := aOutEx+cr*nEx, aOutEy+cr*nEy // round center (end)
phiOutEndB := angleAt(fOutEx, fOutEy, pOutEndX, pOutEndY) // outer end trimmed point
phiOutEndA := angleAt(fOutEx, fOutEy, aOutEx, aOutEy) // end radial tangent
phiOutStartA := angleAt(fOutSx, fOutSy, aOutSx, aOutSy) // start radial tangent
phiOutStartB := angleAt(fOutSx, fOutSy, pOutStartX, pOutStartY) // outer start trimmed point
// inner rounds
var aInSx, aInSy, fInSx, fInSy, aInEx, aInEy, fInEx, fInEy float64
var phiInEndA, phiInEndB, phiInStartA, phiInStartB float64
if inner > 0 {
aInSx, aInSy = cx+sIn*vSx, cy+sIn*vSy
fInSx, fInSy = aInSx+crIn*nSx, aInSy+crIn*nSy
aInEx, aInEy = cx+sIn*vEx, cy+sIn*vEy
fInEx, fInEy = aInEx+crIn*nEx, aInEy+crIn*nEy
phiInEndA = angleAt(fInEx, fInEy, aInEx, aInEy) // end radial tangent
phiInEndB = angleAt(fInEx, fInEy, pInEndX, pInEndY) // inner end trimmed point
phiInStartB = angleAt(fInSx, fInSy, pInStartX, pInStartY) // inner start trimmed point
phiInStartA = angleAt(fInSx, fInSy, aInSx, aInSy) // start radial tangent
}
angleDiff := func(delta float64) float64 {
return math.Atan2(math.Sin(delta), math.Cos(delta))
}
adder.Start(rasterx.ToFixedP(pOutStartX, pOutStartY)) // start at trimmed outer start
addCircularArc(adder, cx, cy, outer, startOuter, endOuter-startOuter) // outer arc (trimmed)
addCircularArc(adder, fOutEx, fOutEy, cr, phiOutEndB, angleDiff(phiOutEndA-phiOutEndB)) // end side: outer round to radial
if inner > 0 {
adder.Line(rasterx.ToFixedP(aInEx, aInEy)) // end side: radial line to inner
addCircularArc(adder, fInEx, fInEy, crIn, phiInEndA, angleDiff(phiInEndB-phiInEndA)) // end side: inner round to inner arc
addCircularArc(adder, cx, cy, inner, endInner, startInner-endInner) // inner arc (reverse, trimmed)
addCircularArc(adder, fInSx, fInSy, crIn, phiInStartB, angleDiff(phiInStartA-phiInStartB)) // start side: inner round to radial
adder.Line(rasterx.ToFixedP(aOutSx, aOutSy)) // start side: radial line to outer
} else {
// pie slice: close via center with radial lines
adder.Line(rasterx.ToFixedP(cx, cy)) // to center from end side
adder.Line(rasterx.ToFixedP(aOutSx, aOutSy)) // to start-side radial tangent
}
// start side: outer round from radial to outer start
addCircularArc(adder, fOutSx, fOutSy, cr, phiOutStartA, angleDiff(phiOutStartB-phiOutStartA))
adder.Stop(true)
}
// GetCornerRadius returns the effective corner radius for a rectangle or square corner.
// If the specific corner radius (perCornerRadius) is zero, it falls back to the baseCornerRadius.
// Otherwise, it uses the specific corner radius provided.
//
// This allows for per-corner customization while maintaining a default overall radius.
func GetCornerRadius(perCornerRadius, baseCornerRadius float32) float32 {
if perCornerRadius == 0.0 {
return baseCornerRadius
}
return perCornerRadius
}
// GetMaximumRadius returns the maximum possible corner radius that fits within the given size.
// It calculates half of the smaller dimension (width or height) of the provided fyne.Size.
//
// This is typically used for drawing circular corners in rectangles, circles or squares with the same radius for all corners.
func GetMaximumRadius(size fyne.Size) float32 {
return fyne.Min(size.Height, size.Width) / 2
}
// GetMaximumCornerRadius returns the maximum possible corner radius for an individual corner,
// considering the specified corner radius, the radii of adjacent corners, and the maximum radii
// allowed for the width and height of the shape. Corner radius may utilize unused capacity from adjacent corners with radius smaller than maximum value
// so this corner can grow up to double the maximum radius of the smaller dimension (width or height) without causing overlaps.
//
// This is typically used for drawing circular corners in rectangles or squares with different corner radii.
func GetMaximumCornerRadius(radius, adjacentWidthRadius, adjacentHeightRadius float32, size fyne.Size) float32 {
maxWidthRadius := size.Width / 2
maxHeightRadius := size.Height / 2
// fast path: corner radius fits within both per-axis maxima
if radius <= fyne.Min(maxWidthRadius, maxHeightRadius) {
return radius
}
// expand per-axis limits by borrowing any unused capacity from adjacent corners
expandedMaxWidthRadius := 2*maxWidthRadius - fyne.Min(maxWidthRadius, adjacentWidthRadius)
expandedMaxHeightRadius := 2*maxHeightRadius - fyne.Min(maxHeightRadius, adjacentHeightRadius)
// respect the smaller axis and never exceed the requested radius
expandedMaxRadius := fyne.Min(expandedMaxWidthRadius, expandedMaxHeightRadius)
return fyne.Min(expandedMaxRadius, radius)
}
// GetMaximumRadiusArc returns the maximum possible corner radius for an arc segment based on the outer radius,
// inner radius, and sweep angle in degrees.
// It calculates half of the smaller dimension (thickness or effective length) of the provided arc parameters
func GetMaximumRadiusArc(outerRadius, innerRadius, sweepAngle float32) float32 {
// height (thickness), width (length)
thickness := outerRadius - innerRadius
// TODO: length formula can be improved to get a fully rounded (pill shape) outer edge for thin (small sweep) arc segments
span := math.Sin(0.5 * math.Min(math.Abs(float64(sweepAngle))*math.Pi/180.0, math.Pi)) // span in (0,1)
length := 1.5 * float64(outerRadius) * span / (1 + span) // no division-by-zero risk
return GetMaximumRadius(fyne.NewSize(
thickness, float32(length),
))
}
// NormalizeArcAngles adjusts the given start and end angles for arc drawing.
// It converts the angles from the Unit circle coordinate system (where 0 degrees is along the positive X-axis)
// to the coordinate system used by the painter, where 0 degrees is at the top (12 o'clock position).
// The function also reverses the direction: positive is clockwise, negative is counter-clockwise
func NormalizeArcAngles(startAngle, endAngle float32) (float32, float32) {
return -(startAngle - 90), -(endAngle - 90)
}