| // Copyright 2016 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package gradient provides linear and radial gradient images. |
| package gradient |
| |
| import ( |
| "image" |
| "image/color" |
| "math" |
| |
| "golang.org/x/image/math/f64" |
| ) |
| |
| // TODO: gamma correction / non-linear color interpolation? |
| |
| // TODO: move this out of an internal directory, either under |
| // golang.org/x/image or under the standard library's image, so that |
| // golang.org/x/image/{draw,vector} and possibly image/draw can type switch on |
| // the gradient.Gradient type and provide fast path code. |
| // |
| // Doing so requires coming up with a stable API that we'd be happy to support |
| // in the long term. This would probably include an easier way to create |
| // linear, circular and elliptical gradients, without having to explicitly |
| // calculate the f64.Aff3 matrix. |
| |
| // Shape is the gradient shape. |
| type Shape uint8 |
| |
| const ( |
| ShapeLinear Shape = iota |
| ShapeRadial |
| ) |
| |
| // Spread is the gradient spread, or how to spread a gradient past its nominal |
| // bounds (from offset being 0.0 to offset being 1.0). |
| type Spread uint8 |
| |
| const ( |
| // SpreadNone means that offsets outside of the [0, 1] range map to |
| // transparent black. |
| SpreadNone Spread = iota |
| // SpreadPad means that offsets below 0 and above 1 map to the colors that |
| // 0 and 1 would map to. |
| SpreadPad |
| // SpreadReflect means that the offset mapping is reflected start-to-end, |
| // end-to-start, start-to-end, etc. |
| SpreadReflect |
| // SpreadRepeat means that the offset mapping is repeated start-to-end, |
| // start-to-end, start-to-end, etc. |
| SpreadRepeat |
| ) |
| |
| // Clamp clamps x to the range [0, 1]. If x is outside that range, it is |
| // converted to a value in that range according to s's semantics. It returns -1 |
| // if s is SpreadNone and x is outside the range [0, 1]. |
| func (s Spread) Clamp(x float64) float64 { |
| if x >= 0 { |
| if x <= 1 { |
| return x |
| } |
| switch s { |
| case SpreadPad: |
| return 1 |
| case SpreadReflect: |
| if int(x)&1 == 0 { |
| return x - math.Floor(x) |
| } |
| return math.Ceil(x) - x |
| case SpreadRepeat: |
| return x - math.Floor(x) |
| } |
| return -1 |
| } |
| switch s { |
| case SpreadPad: |
| return 0 |
| case SpreadReflect: |
| x = -x |
| if int(x)&1 == 0 { |
| return x - math.Floor(x) |
| } |
| return math.Ceil(x) - x |
| case SpreadRepeat: |
| return x - math.Floor(x) |
| } |
| return -1 |
| } |
| |
| // Stop is an offset and color. |
| type Stop struct { |
| Offset float64 |
| RGBA64 color.RGBA64 |
| } |
| |
| // Range is the range between two stops. |
| type Range struct { |
| Offset0 float64 |
| Offset1 float64 |
| Width float64 |
| R0 float64 |
| R1 float64 |
| G0 float64 |
| G1 float64 |
| B0 float64 |
| B1 float64 |
| A0 float64 |
| A1 float64 |
| } |
| |
| // MakeRange returns the range between two stops. |
| func MakeRange(s0, s1 Stop) Range { |
| return Range{ |
| Offset0: s0.Offset, |
| Offset1: s1.Offset, |
| Width: s1.Offset - s0.Offset, |
| R0: float64(s0.RGBA64.R), |
| R1: float64(s1.RGBA64.R), |
| G0: float64(s0.RGBA64.G), |
| G1: float64(s1.RGBA64.G), |
| B0: float64(s0.RGBA64.B), |
| B1: float64(s1.RGBA64.B), |
| A0: float64(s0.RGBA64.A), |
| A1: float64(s1.RGBA64.A), |
| } |
| } |
| |
| // AppendRanges appends to a the ranges defined by a's implicit final stop (if |
| // any exist) and stops. |
| func AppendRanges(a []Range, stops []Stop) []Range { |
| if len(stops) == 0 { |
| return nil |
| } |
| if len(a) != 0 { |
| z := a[len(a)-1] |
| a = append(a, MakeRange(Stop{ |
| Offset: z.Offset1, |
| RGBA64: color.RGBA64{ |
| R: uint16(z.R1), |
| G: uint16(z.G1), |
| B: uint16(z.B1), |
| A: uint16(z.A1), |
| }, |
| }, stops[0])) |
| } |
| for i := 0; i < len(stops)-1; i++ { |
| a = append(a, MakeRange(stops[i], stops[i+1])) |
| } |
| return a |
| } |
| |
| // Gradient is a very large image.Image (the same size as an image.Uniform) |
| // whose colors form a gradient. |
| type Gradient struct { |
| Shape Shape |
| Spread Spread |
| |
| // Pix2Grad transforms coordinates from pixel space (the arguments to the |
| // Image.At method) to gradient space. Gradient space is where a linear |
| // gradient ranges from x == 0 to x == 1, and a radial gradient has center |
| // (0, 0) and radius 1. |
| // |
| // This is an affine transform, so it can represent elliptical gradients in |
| // pixel space, including non-axis-aligned ellipses. |
| // |
| // For a linear gradient, the bottom row is ignored. |
| Pix2Grad f64.Aff3 |
| |
| Ranges []Range |
| |
| // First and Last are the first and last stop's colors. |
| First, Last color.RGBA64 |
| } |
| |
| // Init initializes g to a gradient whose geometry is defined by shape and |
| // pix2Grad and whose colors are defined by spread and stops. |
| func (g *Gradient) Init(shape Shape, spread Spread, pix2Grad f64.Aff3, stops []Stop) { |
| g.Shape = shape |
| g.Spread = spread |
| g.Pix2Grad = pix2Grad |
| g.Ranges = AppendRanges(g.Ranges[:0], stops) |
| if len(stops) == 0 { |
| g.First = color.RGBA64{} |
| g.Last = color.RGBA64{} |
| } else { |
| g.First = stops[0].RGBA64 |
| g.Last = stops[len(stops)-1].RGBA64 |
| } |
| } |
| |
| // ColorModel satisfies the image.Image interface. |
| func (g *Gradient) ColorModel() color.Model { |
| return color.RGBA64Model |
| } |
| |
| // Bounds satisfies the image.Image interface. |
| func (g *Gradient) Bounds() image.Rectangle { |
| return image.Rectangle{ |
| Min: image.Point{-1e9, -1e9}, |
| Max: image.Point{+1e9, +1e9}, |
| } |
| } |
| |
| // At satisfies the image.Image interface. |
| func (g *Gradient) At(x, y int) color.Color { |
| if len(g.Ranges) == 0 { |
| return color.RGBA64{} |
| } |
| |
| px := float64(x) + 0.5 |
| py := float64(y) + 0.5 |
| |
| offset := 0.0 |
| if g.Shape == ShapeLinear { |
| offset = g.Spread.Clamp(g.Pix2Grad[0]*px + g.Pix2Grad[1]*py + g.Pix2Grad[2]) |
| } else { |
| gx := g.Pix2Grad[0]*px + g.Pix2Grad[1]*py + g.Pix2Grad[2] |
| gy := g.Pix2Grad[3]*px + g.Pix2Grad[4]*py + g.Pix2Grad[5] |
| offset = g.Spread.Clamp(math.Sqrt(gx*gx + gy*gy)) |
| } |
| if !(offset >= 0) { |
| return color.RGBA64{} |
| } |
| |
| if offset < g.Ranges[0].Offset0 { |
| return g.First |
| } |
| for _, r := range g.Ranges { |
| if r.Offset0 <= offset && offset <= r.Offset1 { |
| t := (offset - r.Offset0) / r.Width |
| s := 1 - t |
| return color.RGBA64{ |
| uint16(s*r.R0 + t*r.R1), |
| uint16(s*r.G0 + t*r.G1), |
| uint16(s*r.B0 + t*r.B1), |
| uint16(s*r.A0 + t*r.A1), |
| } |
| } |
| } |
| return g.Last |
| } |