Skip to content

Commit

Permalink
Add Paint type for Color/Pattern painting of fill and stroke; add Rad…
Browse files Browse the repository at this point in the history
…ialGradient, see tdewolff#207
  • Loading branch information
tdewolff committed Mar 25, 2023
1 parent a6e8e04 commit 6aa80bf
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 169 deletions.
65 changes: 31 additions & 34 deletions canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,24 +98,29 @@ const (

////////////////////////////////////////////////////////////////

type Fill struct {
// Paint is the type of paint used to fill or stroke a path. It can be either a color or a pattern. Default is transparent (no paint).
type Paint struct {
Color color.RGBA
Pattern Pattern
// TODO: add hatch image and hatch path
}

func (fill Fill) HasColor() bool {
return fill.Color.A != 0 && fill.Pattern == nil
func (paint Paint) Has() bool {
return paint.Color.A != 0 || paint.Pattern != nil
}

func (fill Fill) HasPattern() bool {
return fill.Pattern != nil
func (paint Paint) IsColor() bool {
return paint.Color.A != 0 && paint.Pattern == nil
}

func (paint Paint) IsPattern() bool {
return paint.Pattern != nil
}

// Style is the path style that defines how to draw the path. When Fill is not set it will not fill the path. If StrokeColor is transparent or StrokeWidth is zero, it will not stroke the path. If Dashes is an empty array, it will not draw dashes but instead a solid stroke line. FillRule determines how to fill the path when paths overlap and have certain directions (clockwise, counter clockwise).
type Style struct {
Fill Fill
StrokeColor color.RGBA
Fill Paint
Stroke Paint
StrokeWidth float64
StrokeCapper Capper
StrokeJoiner Joiner
Expand All @@ -126,12 +131,12 @@ type Style struct {

// HasFill returns true if the style has a fill
func (style Style) HasFill() bool {
return style.Fill.Color.A != 0 || style.Fill.Pattern != nil
return style.Fill.Has()
}

// HasStroke returns true if the style has a stroke
func (style Style) HasStroke() bool {
return style.StrokeColor.A != 0 && 0.0 < style.StrokeWidth
return style.Stroke.Has() && 0.0 < style.StrokeWidth
}

// IsDashed returns true if the style has dashes
Expand All @@ -141,8 +146,8 @@ func (style Style) IsDashed() bool {

// DefaultStyle is the default style for paths. It fills the path with a black color and has no stroke.
var DefaultStyle = Style{
Fill: Fill{Color: Black},
StrokeColor: Transparent,
Fill: Paint{Color: Black},
Stroke: Paint{},
StrokeWidth: 1.0,
StrokeCapper: ButtCap,
StrokeJoiner: MiterJoin,
Expand Down Expand Up @@ -329,14 +334,6 @@ func (c *Context) ShearAbout(sx, sy, x, y float64) {
c.view = c.view.Mul(Identity.ShearAbout(sx, sy, x, y))
}

// FillColor returns the color used for filling operations.
func (c *Context) FillColor() color.Color {
if c.Style.Fill.HasColor() {
return c.Style.Fill.Color
}
return Transparent
}

// SetFillColor sets the color to be used for filling operations.
func (c *Context) SetFillColor(col color.Color) {
r, g, b, a := col.RGBA()
Expand All @@ -354,16 +351,9 @@ func (c *Context) SetFillColor(col color.Color) {
c.Style.Fill.Pattern = nil
}

// FillPattern returns the pattern used for filling operations.
func (c *Context) FillPattern() Pattern {
if c.Style.Fill.HasPattern() {
return c.Style.Fill.Pattern
}
return nil
}

// SetFillPattern sets the pattern (such as gradients) to be used for filling operations.
func (c *Context) SetFillPattern(pattern Pattern) {
c.Style.Fill.Color = Transparent
c.Style.Fill.Pattern = pattern
}

Expand All @@ -380,7 +370,14 @@ func (c *Context) SetStrokeColor(col color.Color) {
if a < b {
b = a
}
c.Style.StrokeColor = color.RGBA{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8), uint8(a >> 8)}
c.Style.Stroke.Color = color.RGBA{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8), uint8(a >> 8)}
c.Style.Stroke.Pattern = nil
}

// SetStrokePattern sets the pattern (such as gradients) to be used for stroking operations.
func (c *Context) SetStrokePattern(pattern Pattern) {
c.Style.Stroke.Color = Transparent
c.Style.Stroke.Pattern = pattern
}

// SetStrokeWidth sets the width in millimeters for stroking operations.
Expand Down Expand Up @@ -463,17 +460,17 @@ func (c *Context) Close() {

// Fill fills the current path and resets the path.
func (c *Context) Fill() {
strokeColor := c.Style.StrokeColor
c.Style.StrokeColor = Transparent
stroke := c.Style.Stroke
c.Style.Stroke = Paint{}
c.DrawPath(0.0, 0.0, c.path)
c.Style.StrokeColor = strokeColor
c.Style.Stroke = stroke
c.path = &Path{}
}

// Stroke strokes the current path and resets the path.
func (c *Context) Stroke() {
fill := c.Style.Fill
c.Style.Fill = Fill{}
c.Style.Fill = Paint{}
c.DrawPath(0.0, 0.0, c.path)
c.Style.Fill = fill
c.path = &Path{}
Expand Down Expand Up @@ -570,7 +567,7 @@ func (c *Context) DrawPath(x, y float64, paths ...*Path) {
style := c.Style
style.Dashes, ok = path.checkDash(c.Style.DashOffset, c.Style.Dashes)
if !ok {
style.StrokeColor = Transparent
style.Stroke = Paint{}
}
c.RenderPath(path, style, m)
}
Expand Down Expand Up @@ -685,7 +682,7 @@ func (c *Canvas) Fit(margin float64) {
bounds := Rect{}
if l.path != nil {
bounds = l.path.Bounds()
if l.style.StrokeColor.A != 0 && 0.0 < l.style.StrokeWidth {
if l.style.HasStroke() {
bounds.X -= l.style.StrokeWidth / 2.0
bounds.Y -= l.style.StrokeWidth / 2.0
bounds.W += l.style.StrokeWidth
Expand Down
180 changes: 86 additions & 94 deletions colors.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package canvas

import (
"image"
"image/color"
"math"
)

type Pattern interface {
ToImage(int, int, int, Resolution, ColorSpace) image.Image
SetColorSpace(ColorSpace) Pattern
At(float64, float64) color.RGBA
}

type Stop struct {
Expand Down Expand Up @@ -49,126 +49,118 @@ func (stops Stops) At(t float64) color.RGBA {
return stops[len(stops)-1].Color
}

func colorLerp(c0, c1 color.RGBA, t float64) color.RGBA {
r0, g0, b0, a0 := c0.RGBA()
r1, g1, b1, a1 := c1.RGBA()
return color.RGBA{
lerp(r0, r1, t),
lerp(g0, g1, t),
lerp(b0, b1, t),
lerp(a0, a1, t),
}
}

func lerp(a, b uint32, t float64) uint8 {
return uint8(uint32((1.0-t)*float64(a)+t*float64(b)) >> 8)
}

type LinearGradient struct {
Start, End Point
Stops

d Point
d2 float64
}

func NewLinearGradient(start, end Point) *LinearGradient {
d := end.Sub(start)
return &LinearGradient{
Start: start,
End: end,

d: d,
d2: d.Dot(d),
}
}

type LinearGradientImage struct {
LinearGradient
dx, dy, h int
dpmm float64
func (g *LinearGradient) SetColorSpace(colorSpace ColorSpace) Pattern {
if _, ok := colorSpace.(LinearColorSpace); ok {
return g
}
pattern := *g
for i := range pattern.Stops {
pattern.Stops[i].Color = colorSpace.ToLinear(pattern.Stops[i].Color)
}
return &pattern
}

func (g *LinearGradient) ToImage(dx, dy, h int, res Resolution, colorSpace ColorSpace) image.Image {
img := &LinearGradientImage{
LinearGradient: *g,
dx: dx,
dy: dy,
h: h,
dpmm: res.DPMM(),
func (g *LinearGradient) At(x, y float64) color.RGBA {
if len(g.Stops) == 0 {
return Transparent
}
for i := range img.Stops {
img.Stops[i].Color = colorSpace.ToLinear(img.Stops[i].Color)

p := Point{x, y}.Sub(g.Start)
if Equal(g.d.Y, 0.0) && !Equal(g.d.X, 0.0) {
return g.Stops.At(p.X / g.d.X) // horizontal
} else if !Equal(g.d.Y, 0.0) && Equal(g.d.X, 0.0) {
return g.Stops.At(p.Y / g.d.Y) // vertical
}
return img
t := p.Dot(g.d) / g.d2
return g.Stops.At(t)
}

func (g *LinearGradientImage) ColorModel() color.Model {
return color.RGBAModel
}
type RadialGradient struct {
C0, C1 Point
R0, R1 float64
Stops

func (g *LinearGradientImage) Bounds() image.Rectangle {
return image.Rectangle{image.Point{-1e9, -1e9}, image.Point{1e9, 1e9}}
cd Point
dr, a float64
}

func (g *LinearGradientImage) At(x, y int) color.Color {
if len(g.Stops) == 0 {
return color.Transparent
func NewRadialGradient(c0 Point, r0 float64, c1 Point, r1 float64) *RadialGradient {
cd := c1.Sub(c0)
dr := r1 - r0
return &RadialGradient{
C0: c0,
R0: r0,
C1: c1,
R1: r1,

cd: cd,
dr: dr,
a: cd.Dot(cd) - dr*dr,
}
}

d := g.End.Sub(g.Start)
p := Point{float64(g.dx+x) / g.dpmm, float64(g.h-g.dx-y) / g.dpmm}.Sub(g.Start)
if Equal(d.Y, 0.0) && !Equal(d.X, 0.0) {
return g.Stops.At(p.X / d.X) // horizontal
} else if !Equal(d.Y, 0.0) && Equal(d.X, 0.0) {
return g.Stops.At(p.Y / d.Y) // vertical
func (g *RadialGradient) SetColorSpace(colorSpace ColorSpace) Pattern {
if _, ok := colorSpace.(LinearColorSpace); ok {
return g
}
t := p.Dot(d) / d.Dot(d)
return g.Stops.At(t)
pattern := *g
for i := range pattern.Stops {
pattern.Stops[i].Color = colorSpace.ToLinear(pattern.Stops[i].Color)
}
return &pattern
}

//type RadialGradient struct {
// Center0, Center1 Point
// Radius0, Radius1 float64
// GradientStops
//}
//
//func NewRadialGradient(c0, c1 Point, r0, r1 float64) *RadialGradient {
// return &RadialGradient{
// Center: center,
// Radius: radius,
// }
//}
//
//func dot3(x0, y0, z0, x1, y1, z1 float64) float64 {
// return x0*x1 + y0*y1 + z0*z1
//}
//
//func (g *RadialGradient) At(x, y int) color.RGBA {
// if len(g.GradientStops) == 0 {
// return color.Transparent
// }
//
// dx, dy := float64(x)+0.5-g.c0.x, float64(y)+0.5-g.c0.y
// b := dot3(dx, dy, g.c0.r, g.cd.x, g.cd.y, g.cd.r)
// c := dot3(dx, dy, -g.c0.r, dx, dy, g.c0.r)
// if g.a == 0 {
// if b == 0 {
// return color.Transparent
// }
// t := 0.5 * c / b
// if t*g.cd.r >= g.mindr {
// return getColor(t, g.GradientStops)
// }
// return color.Transparent
// }
//
// discr := dot3(b, g.a, 0, b, -c, 0)
// if discr >= 0 {
// sqrtdiscr := math.Sqrt(discr)
// t0 := (b + sqrtdiscr) * g.inva
// t1 := (b - sqrtdiscr) * g.inva
//
// if t0*g.cd.r >= g.mindr {
// return getColor(t0, g.GradientStops)
// } else if t1*g.cd.r >= g.mindr {
// return getColor(t1, g.GradientStops)
// }
// }
// return color.Transparent
//}

func colorLerp(c0, c1 color.RGBA, t float64) color.RGBA {
r0, g0, b0, a0 := c0.RGBA()
r1, g1, b1, a1 := c1.RGBA()
return color.RGBA{
lerp(r0, r1, t),
lerp(g0, g1, t),
lerp(b0, b1, t),
lerp(a0, a1, t),
func (g *RadialGradient) At(x, y float64) color.RGBA {
if len(g.Stops) == 0 {
return Transparent
}
}

func lerp(a, b uint32, t float64) uint8 {
return uint8(uint32((1.0-t)*float64(a)+t*float64(b)) >> 8)
// see reference implementation of pixman-radial-gradient
// https://github.com/servo/pixman/blob/master/pixman/pixman-radial-gradient.c#L161
pd := Point{x, y}.Sub(g.C0)
b := pd.Dot(g.cd) + g.R0*g.dr
c := pd.Dot(pd) - g.R0*g.R0
t0, t1 := solveQuadraticFormula(g.a, -2.0*b, c)
if !math.IsNaN(t1) {
return g.Stops.At(t1)
} else if !math.IsNaN(t0) {
return g.Stops.At(t0)
}
return Transparent
}

// ColorSpace defines the color space within the RGB color model. All colors passed to this library are assumed to be in the sRGB color space, which is a ubiquitous assumption in most software. This works great for most applications, but fails when blending semi-transparent layers. See an elaborate explaination at https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/, which goes into depth of the problems of using sRGB for blending and the need for gamma correction. In short, we need to transform the colors, which are in the sRGB color space, to the linear color space, perform blending, and then transform them back to the sRGB color space.
Expand Down
Loading

0 comments on commit 6aa80bf

Please sign in to comment.