//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 16 Jul 08 Brian Frank Creation
// 31 Mar 10 Brian Frank Rewrite to support full CSS model
//
**
** Fills a shape with a linear or radial color gradient.
**
** NOTE: SWT only supports linear two stop gradients with no
** angle using the Graphics.fillRect method.
**
@Js
@Serializable { simple = true }
const class Gradient : Brush
{
** Percent unit constant
const static Unit percent := loadUnit("percent", "%")
** Pixel unit constant
const static Unit pixel := loadUnit("pixel", "px")
** Mode is linear or radial
const GradientMode mode := GradientMode.linear
** Starting point x coordinate with unit defined by `x1Unit`
const Int x1 := 0
** Starting point y coordinate with unit defined by `y1Unit`
const Int y1 := 0
** Ending point x coordinate with unit defined by `x2Unit`
const Int x2 := 100
** Ending point y coordinate with unit defined by `y2Unit`
const Int y2 := 100
** Unit of `x1` which must be `percent` or `pixel`
const Unit x1Unit := pixel
** Unit of `y1` which must be `percent` or `pixel`
const Unit y1Unit := pixel
** Unit of `x2` which must be `percent` or `pixel`
const Unit x2Unit := pixel
** Unit of `y2` which must be `percent` or `pixel`
const Unit y2Unit := pixel
** List of gradient stops, default is "white 0.0" to "black 1.0".
const GradientStop[] stops := defStops
**
** Parse a gradient from string (see `toStr`). If invalid
** and checked is true then throw ParseErr otherwise
** return null:
**
** <gradient> := <linear> | <radial> | <impliedLinear>
** <linear> := "linear(" <args> ")"
** <radial> := "radial(" <args> ")"
** <impliedLinear> := <args>
** <args> := <start> "," <end> ("," <stop>)*
** <start> := <pos> <pos>
** <end> := <pos> <pos>
** <pos> := <int> <unit> // no space allowed between
** <stop> := <color> [<float>] // 0f..1f
** <color> := #AARRGGBB, #RRGGBB, #RGB
** <unit> := "px" | "%"
**
** The general format is a start and end position followed by a comma list of
** gradient stops. The start and end positions are x, y coordinates (% or pixel).
** The stops are a color followed by a position in the range (0..1). If the
** position is omitted it is calcaulated as percentage:
** #000, #fff => #000 0.0, #fff 1.0
** #000, #abc, #fff => #000 0.0, #000 0.5, #fff 1.0
**
** Examples:
** Gradient("linear(0% 0%, 100% 100%, #f00, #00f)") => linear(0% 0%, 100% 100%, #ff0000 0.0, #0000ff 1.0)
** Gradient("5px 3px, 25px 30px, #f00, #00f") => linear(5px 3px, 25px 30px, #ff0000 0.0, #0000ff 1.0)
** Gradient("0% 50%, 100% 50%, #f00 0.1, #00f 0.9") => linear(0% 50%, 100% 50%, #ff0000 0.1, #0000ff 0.9)
**
static new fromStr(Str str, Bool checked := true)
{
try
{
return makeStr(str)
}
catch {}
if (checked) throw ParseErr("Invalid Gradient: $str")
return null
}
private new makeStr(Str str)
{
// if function syntax
if (str[-1] == ')')
{
if (str.startsWith("radial(")) this.mode = GradientMode.radial
else if (!str.startsWith("linear(")) throw Err()
str = str["linear(".size .. -2]
}
// tokenize into parts by comma
parts := str.split(',')
// first two parts are pos: // x y as ##% or ##px
for (i := 0; i<2; ++i)
{
pos := parts[i]
coor := pos.split
if (coor.size != 2) throw Err()
Int? x; Int? y
Unit? xUnit; Unit? yUnit
xs := coor[0]
if (xs.endsWith("%")) { x = xs[0..-2].toInt; xUnit = percent }
else if (xs.endsWith("px")) { x = xs[0..-3].toInt; xUnit = pixel }
else throw Err()
ys := coor[1]
if (ys.endsWith("%")) { y = ys[0..-2].toInt; yUnit = percent }
else if (ys.endsWith("px")) { y = ys[0..-3].toInt; yUnit = pixel }
else throw Err()
if (i == 0)
{
this.x1 = x; this.x1Unit = xUnit
this.y1 = y; this.y1Unit = yUnit
}
else
{
this.x2 = x; this.x2Unit = xUnit
this.y2 = y; this.y2Unit = yUnit
}
}
// stop colors and optional positions
stopColors := Color[,]
stopPos := Str?[,]
for (i := 2; i<parts.size; ++i)
{
stopPart := parts[i]
space := stopPart.index(" ")
if (space == null)
{
stopColors.add(Color.fromStr(stopPart))
stopPos.add(null)
}
else
{
stopColors.add(Color.fromStr(stopPart[0..<space]))
stopPos.add(stopPart[space+1..-1])
}
}
if (stopColors.size < 2) throw Err()
// compute final stops
this.stops = stopColors.map |color, i|
{
pos := stopPos[i]?.toFloat ?: (i * 100 / (stopPos.size - 1)).toFloat / 100f
return GradientStop(color, pos)
}
}
**
** Construct for it-block.
** Throw ArgErr if any units are invalid or less than 2 stops.
**
new make(|This|? f := null)
{
if (f != null) f(this)
if (x1Unit !== percent && x1Unit !== pixel) throw ArgErr("Invalid x1Unit: $x1Unit")
if (y1Unit !== percent && y1Unit !== pixel) throw ArgErr("Invalid y1Unit: $y1Unit")
if (x2Unit !== percent && x2Unit !== pixel) throw ArgErr("Invalid x2Unit: $x2Unit")
if (y2Unit !== percent && y2Unit !== pixel) throw ArgErr("Invalid y2Unit: $y2Unit")
if (stops.size < 2) throw ArgErr("Must have 2 or more stops")
}
**
** Hash the fields.
**
override Int hash()
{
return (mode.hash.shiftl(28))
.xor(x1.hash.shiftl(21))
.xor(y1.hash.shiftl(14))
.xor(x2.hash.shiftl(21))
.xor(y2.hash.shiftl(14))
.xor(stops.hash)
}
**
** Equality is based on fields.
**
override Bool equals(Obj? obj)
{
that := obj as Gradient
if (that == null) return false
return this.mode == that.mode &&
this.x1 == that.x1 &&
this.y1 == that.y1 &&
this.x1Unit == that.x1Unit &&
this.y1Unit == that.y1Unit &&
this.x2 == that.x2 &&
this.y2 == that.y2 &&
this.x2Unit == that.x2Unit &&
this.y2Unit == that.y2Unit &&
this.stops == that.stops
}
**
** Return '"[point1:color1; point2:color2]"'.
** This string format is subject to change.
**
override Str toStr()
{
s := StrBuf()
s.add(mode.name).addChar('(')
s.add(x1).add(x1Unit.symbol).addChar(' ')
s.add(y1).add(y1Unit.symbol).addChar(',')
s.add(x2).add(x2Unit.symbol).addChar(' ')
s.add(y2).add(y2Unit.symbol)
stops.each |stop| { s.addChar(',').add(stop) }
return s.addChar(')').toStr
}
** Just in case unit database is not available, create unit as fallback
private static Unit loadUnit(Str name, Str symbol)
{
try
return Unit(name)
catch (Err e)
return Unit.define("$name,$symbol")
}
** white 0% to black 100%
private static const GradientStop[] defStops :=
[
GradientStop(Color.white, 0f),
GradientStop(Color.black, 1f),
]
}
**************************************************************************
** GradientStop
**************************************************************************
**
** GradientStop is used with `Gradient` to model a color stop.
**
@Js
const class GradientStop
{
**
** Construct with color, pos, and unit.
**
new make(Color color, Float pos)
{
this.color = color
this.pos = pos
}
** Color for the stop
const Color color
** Position of the stop within range (0f..1f)
const Float pos
**
** Hash the fields.
**
override Int hash() { pos.hash.xor(color.hash) }
**
** Equality is based on fields.
**
override Bool equals(Obj? obj)
{
that := obj as GradientStop
if (that == null) return false
return this.pos == that.pos &&
this.color == that.color
}
**
** Return stop formatted as "{color} {pos}".
**
override Str toStr()
{
"${color} ${pos}"
}
}