package canvas import ( "image" "image/color" "math" "fyne.io/fyne/v2" ) // LinearGradient defines a Gradient travelling straight at a given angle. // The only supported values for the angle are `0.0` (vertical) and `90.0` (horizontal), currently. type LinearGradient struct { baseObject StartColor color.Color // The beginning color of the gradient EndColor color.Color // The end color of the gradient Angle float64 // The angle of the gradient (0/180 for vertical; 90/270 for horizontal) } // Generate calculates an image of the gradient with the specified width and height. func (g *LinearGradient) Generate(iw, ih int) image.Image { w, h := float64(iw), float64(ih) var generator func(x, y float64) float64 switch g.Angle { case 90: // horizontal flipped generator = func(x, _ float64) float64 { return (w - x) / w } case 270: // horizontal generator = func(x, _ float64) float64 { return x / w } case 45: // diagonal negative flipped generator = func(x, y float64) float64 { return math.Abs((w - x + y) / (w + h)) // ((w+h)-(x+h-y)) / (w+h) } case 225: // diagonal negative generator = func(x, y float64) float64 { return math.Abs((x + h - y) / (w + h)) } case 135: // diagonal positive flipped generator = func(x, y float64) float64 { return math.Abs((w + h - (x + y)) / (w + h)) } case 315: // diagonal positive generator = func(x, y float64) float64 { return math.Abs((x + y) / (w + h)) } case 180: // vertical flipped generator = func(_, y float64) float64 { return (h - y) / h } default: // vertical generator = func(_, y float64) float64 { return y / h } } return computeGradient(generator, iw, ih, g.StartColor, g.EndColor) } // Hide will set this gradient to not be visible func (g *LinearGradient) Hide() { g.baseObject.Hide() repaint(g) } // Move the gradient to a new position, relative to its parent / canvas func (g *LinearGradient) Move(pos fyne.Position) { g.baseObject.Move(pos) repaint(g) } // Refresh causes this gradient to be redrawn with its configured state. func (g *LinearGradient) Refresh() { Refresh(g) } // RadialGradient defines a Gradient travelling radially from a center point outward. type RadialGradient struct { baseObject StartColor color.Color // The beginning color of the gradient EndColor color.Color // The end color of the gradient // The offset of the center for generation of the gradient. // This is not a DP measure but relates to the width/height. // A value of 0.5 would move the center by the half width/height. CenterOffsetX, CenterOffsetY float64 } // Generate calculates an image of the gradient with the specified width and height. func (g *RadialGradient) Generate(iw, ih int) image.Image { w, h := float64(iw), float64(ih) // define center plus offset centerX := w/2 + w*g.CenterOffsetX centerY := h/2 + h*g.CenterOffsetY // handle negative offsets var a, b float64 if g.CenterOffsetX < 0 { a = w - centerX } else { a = centerX } if g.CenterOffsetY < 0 { b = h - centerY } else { b = centerY } generator := func(x, y float64) float64 { // calculate distance from center for gradient multiplier dx, dy := centerX-x, centerY-y da := math.Sqrt(dx*dx + dy*dy*a*a/b/b) if da > a { return 1 } return da / a } return computeGradient(generator, iw, ih, g.StartColor, g.EndColor) } // Hide will set this gradient to not be visible func (g *RadialGradient) Hide() { g.baseObject.Hide() repaint(g) } // Move the gradient to a new position, relative to its parent / canvas func (g *RadialGradient) Move(pos fyne.Position) { g.baseObject.Move(pos) repaint(g) } // Refresh causes this gradient to be redrawn with its configured state. func (g *RadialGradient) Refresh() { Refresh(g) } func calculatePixel(d float64, startColor, endColor color.Color) color.Color { // fetch RGBA values aR, aG, aB, aA := startColor.RGBA() bR, bG, bB, bA := endColor.RGBA() // Get difference dR := float64(bR) - float64(aR) dG := float64(bG) - float64(aG) dB := float64(bB) - float64(aB) dA := float64(bA) - float64(aA) // Apply gradations pixel := &color.RGBA64{ R: uint16(float64(aR) + d*dR), B: uint16(float64(aB) + d*dB), G: uint16(float64(aG) + d*dG), A: uint16(float64(aA) + d*dA), } return pixel } func computeGradient(generator func(x, y float64) float64, w, h int, startColor, endColor color.Color) image.Image { img := image.NewNRGBA(image.Rect(0, 0, w, h)) if startColor == nil && endColor == nil { return img } else if startColor == nil { startColor = color.Transparent } else if endColor == nil { endColor = color.Transparent } for x := 0; x < w; x++ { for y := 0; y < h; y++ { distance := generator(float64(x)+0.5, float64(y)+0.5) img.Set(x, y, calculatePixel(distance, startColor, endColor)) } } return img } // NewHorizontalGradient creates a new horizontally travelling linear gradient. // The start color will be at the left of the gradient and the end color will be at the right. func NewHorizontalGradient(start, end color.Color) *LinearGradient { g := &LinearGradient{StartColor: start, EndColor: end} g.Angle = 270 return g } // NewLinearGradient creates a linear gradient at the specified angle. // The angle parameter is the degree angle along which the gradient is calculated. // A NewHorizontalGradient uses 270 degrees and NewVerticalGradient is 0 degrees. func NewLinearGradient(start, end color.Color, angle float64) *LinearGradient { g := &LinearGradient{StartColor: start, EndColor: end} g.Angle = angle return g } // NewRadialGradient creates a new radial gradient. func NewRadialGradient(start, end color.Color) *RadialGradient { return &RadialGradient{StartColor: start, EndColor: end} } // NewVerticalGradient creates a new vertically travelling linear gradient. // The start color will be at the top of the gradient and the end color will be at the bottom. func NewVerticalGradient(start color.Color, end color.Color) *LinearGradient { return &LinearGradient{StartColor: start, EndColor: end} }