Skip to content

Commit

Permalink
gpu,op/paint: add paint.RadialGradientOp
Browse files Browse the repository at this point in the history
Signed-off-by: Egon Elbre <[email protected]>
  • Loading branch information
egonelbre committed Mar 7, 2021
1 parent 82f63d2 commit 2ae1967
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 8 deletions.
78 changes: 70 additions & 8 deletions gpu/gpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,13 @@ type drawState struct {
// Current paint.ColorOp, if any.
color color.NRGBA

// Current paint.LinearGradientOp.
// Current paint.LinearGradientOp and paint.RadialGradientOp.
stop1 f32.Point
stop2 f32.Point
color1 color.NRGBA
color2 color.NRGBA
// Current paint.RadialGradientOp.
radiusy float32
}

type pathOp struct {
Expand Down Expand Up @@ -187,9 +189,11 @@ type material struct {
opaque bool
// For materialTypeColor.
color f32color.RGBA
// For materialTypeLinearGradient.
// For materialTypeLinearGradient and materialTypeRadialGradient.
color1 f32color.RGBA
color2 f32color.RGBA
// For materialTypeRadialGradient.
radiusy float32
// For materialTypeTexture.
data imageOpData
uvTrans f32.Affine2D
Expand All @@ -212,9 +216,20 @@ type imageOpData struct {
}

type linearGradientOpData struct {
stop1 f32.Point
stop1 f32.Point
stop2 f32.Point

color1 color.NRGBA
color2 color.NRGBA
}

type radialGradientOpData struct {
stop1 f32.Point
stop2 f32.Point

radiusy float32

color1 color.NRGBA
stop2 f32.Point
color2 color.NRGBA
}

Expand Down Expand Up @@ -294,6 +309,36 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData {
}
}

func decodeRadialGradientOp(data []byte) radialGradientOpData {
if opconst.OpType(data[0]) != opconst.TypeRadialGradient {
panic("invalid op")
}
bo := binary.LittleEndian
return radialGradientOpData{
stop1: f32.Point{
X: math.Float32frombits(bo.Uint32(data[1:])),
Y: math.Float32frombits(bo.Uint32(data[5:])),
},
stop2: f32.Point{
X: math.Float32frombits(bo.Uint32(data[9:])),
Y: math.Float32frombits(bo.Uint32(data[13:])),
},
radiusy: math.Float32frombits(bo.Uint32(data[17:])),
color1: color.NRGBA{
R: data[21+0],
G: data[21+1],
B: data[21+2],
A: data[21+3],
},
color2: color.NRGBA{
R: data[25+0],
G: data[25+1],
B: data[25+2],
A: data[25+3],
},
}
}

type clipType uint8

type resource interface {
Expand Down Expand Up @@ -975,6 +1020,14 @@ loop:
state.stop2 = op.stop2
state.color1 = op.color1
state.color2 = op.color2
case opconst.TypeRadialGradient:
state.matType = materialRadialGradient
op := decodeRadialGradientOp(encOp.Data)
state.stop1 = op.stop1
state.stop2 = op.stop2
state.radiusy = op.radiusy
state.color1 = op.color1
state.color2 = op.color2
case opconst.TypeImage:
state.matType = materialTexture
state.image = decodeImageOp(encOp.Data, encOp.Refs)
Expand Down Expand Up @@ -1086,15 +1139,15 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
m.color2 = f32color.LinearFromSRGB(d.color2)
m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0

m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2))
m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2, -1))
case materialRadialGradient:
m.material = materialRadialGradient

m.color1 = f32color.LinearFromSRGB(d.color1)
m.color2 = f32color.LinearFromSRGB(d.color2)
m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0

m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2))
m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2, d.radiusy))
case materialTexture:
m.material = materialTexture
dr := boundRectF(rect.Add(off))
Expand Down Expand Up @@ -1286,19 +1339,28 @@ func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point, f32.Poin
}

// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)].
func gradientSpaceTransform(clip image.Rectangle, off f32.Point, stop1, stop2 f32.Point) f32.Affine2D {
func gradientSpaceTransform(clip image.Rectangle, off f32.Point, stop1, stop2 f32.Point, radiusy float32) f32.Affine2D {
d := stop2.Sub(stop1)
l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y)))
a := float32(math.Atan2(float64(-d.Y), float64(d.X)))

scalex := 1 / l

var scaley float32
if radiusy == -1 {
scaley = scalex
} else if radiusy != 0 {
scaley = 1 / radiusy
}

// TODO: optimize
zp := f32.Point{}
return f32.Affine2D{}.
Scale(zp, layout.FPt(clip.Size())). // scale to pixel space
Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space
Offset(zp.Sub(stop1)). // offset to first stop point
Rotate(zp, a). // rotate to align gradient
Scale(zp, f32.Pt(1/l, 1/l)) // scale gradient to right size
Scale(zp, f32.Pt(scalex, scaley)) // scale gradient to right size
}

// clipSpaceTransform returns the scale and offset that transforms the given
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions gpu/internal/rendertest/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,44 @@ func TestLinearGradientAngled(t *testing.T) {
}, func(r result) {})
}

func TestRadialGradient(t *testing.T) {
run(t, func(ops *op.Ops) {
paint.RadialGradientOp{
Stop1: f32.Pt(64, 64),
Color1: red,
Stop2: f32.Pt(64, 0),
Color2: black,
}.Add(ops)
paint.PaintOp{}.Add(ops)
}, func(r result) {})
}

func TestEllipseGradient(t *testing.T) {
run(t, func(ops *op.Ops) {
paint.RadialGradientOp{
Stop1: f32.Pt(64, 64),
Color1: red,
Stop2: f32.Pt(64, 0),
Color2: black,
RadiusY: 32,
}.Add(ops)
paint.PaintOp{}.Add(ops)
}, func(r result) {})
}

func TestEllipseGradientAngled(t *testing.T) {
run(t, func(ops *op.Ops) {
paint.RadialGradientOp{
Stop1: f32.Pt(64, 64),
Color1: red,
Stop2: f32.Pt(32, 96),
Color2: black,
RadiusY: 32,
}.Add(ops)
paint.PaintOp{}.Add(ops)
}, func(r result) {})
}

// lerp calculates linear interpolation with color b and p.
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
return f32color.RGBA{
Expand Down
3 changes: 3 additions & 0 deletions internal/opconst/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
TypePaint
TypeColor
TypeLinearGradient
TypeRadialGradient
TypeArea
TypePointerInput
TypePass
Expand Down Expand Up @@ -46,6 +47,7 @@ const (
TypePaintLen = 1
TypeColorLen = 1 + 4
TypeLinearGradientLen = 1 + 8*2 + 4*2
TypeRadialGradientLen = 1 + 8*2 + 4 + 4*2
TypeAreaLen = 1 + 1 + 4*4
TypePointerInputLen = 1 + 1 + 1
TypePassLen = 1 + 1
Expand Down Expand Up @@ -90,6 +92,7 @@ func (t OpType) Size() int {
TypePaintLen,
TypeColorLen,
TypeLinearGradientLen,
TypeRadialGradientLen,
TypeAreaLen,
TypePointerInputLen,
TypePassLen,
Expand Down
46 changes: 46 additions & 0 deletions op/paint/paint.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,30 @@ type ColorOp struct {

// LinearGradientOp sets the brush to a gradient starting at stop1 with color1 and
// ending at stop2 with color2.
//
// Note: this gradient does not work together with non-offset transforms.
type LinearGradientOp struct {
Stop1 f32.Point
Color1 color.NRGBA
Stop2 f32.Point
Color2 color.NRGBA
}

// RadialGradientOp sets the brush to a radial gradient center starting at stop1 with color1
// and ellipse X axis ending at stop2 with color2.
//
// Note: this does not work together with non-offset transforms.
type RadialGradientOp struct {
Stop1 f32.Point
Color1 color.NRGBA
Stop2 f32.Point
Color2 color.NRGBA

// RadiusY defines Y axis for the ellipse.
// RadiusY = 0 draws a circle.
RadiusY float32
}

// PaintOp fills fills the current clip area with the current brush.
type PaintOp struct {
}
Expand Down Expand Up @@ -132,6 +149,35 @@ func (c LinearGradientOp) Add(o *op.Ops) {
data[21+3] = c.Color2.A
}

func (c RadialGradientOp) Add(o *op.Ops) {
data := o.Write(opconst.TypeRadialGradientLen)
data[0] = byte(opconst.TypeRadialGradient)

bo := binary.LittleEndian
bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X))
bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y))
bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X))
bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y))

radiusY := c.RadiusY
if radiusY < 0 {
radiusY = 0
}
if radiusY == 0 {
radiusY = -1 // using -1 to avoid duplicate length calculation
}
bo.PutUint32(data[17:], math.Float32bits(radiusY))

data[21+0] = c.Color1.R
data[21+1] = c.Color1.G
data[21+2] = c.Color1.B
data[21+3] = c.Color1.A
data[25+0] = c.Color2.R
data[25+1] = c.Color2.G
data[25+2] = c.Color2.B
data[25+3] = c.Color2.A
}

func (d PaintOp) Add(o *op.Ops) {
data := o.Write(opconst.TypePaintLen)
data[0] = byte(opconst.TypePaint)
Expand Down

0 comments on commit 2ae1967

Please sign in to comment.