///|
/// A single color stop in a gradient. `offset` is in 0.0..=1.0.
pub struct ColorStop {
offset : Double
color : Color
} derive(Eq)
///|
pub fn ColorStop::new(offset : Double, color : Color) -> ColorStop {
{ offset, color }
}
///|
pub impl Show for ColorStop with fn output(self, logger) {
logger.write_string("ColorStop { offset: ")
logger.write_object(self.offset)
logger.write_string(", color: ")
self.color.output(logger)
logger.write_string(" }")
}
///|
fn color_stops_to_string(stops : Array[ColorStop]) -> String {
let buffer = StringBuilder::new()
buffer.write_string("[")
for index, stop in stops {
if index > 0 {
buffer.write_string(", ")
}
buffer.write_string(stop.to_string())
}
buffer.write_string("]")
buffer.to_string()
}
///|
/// Linear gradient from (x0, y0) to (x1, y1) in user space.
/// Stops are sorted by `offset` at construction time; out-of-range t values
/// clamp to the first/last color.
pub struct LinearGradient {
x0 : Double
y0 : Double
x1 : Double
y1 : Double
stops : Array[ColorStop]
} derive(Eq)
///|
pub impl Show for LinearGradient with fn output(self, logger) {
logger.write_string("LinearGradient { x0: ")
logger.write_object(self.x0)
logger.write_string(", y0: ")
logger.write_object(self.y0)
logger.write_string(", x1: ")
logger.write_object(self.x1)
logger.write_string(", y1: ")
logger.write_object(self.y1)
logger.write_string(", stops: ")
logger.write_string(color_stops_to_string(self.stops))
logger.write_string(" }")
}
///|
/// Copies `stops` and sorts the copy ascending by `offset` via insertion
/// sort. Stops arrays are small (typically 2-5 entries) so O(n²) is fine.
fn sorted_stops(stops : Array[ColorStop]) -> Array[ColorStop] {
let out : Array[ColorStop] = []
for s in stops {
out.push(s)
}
let n = out.length()
for i in 1.. 0 && out[j - 1].offset > cur.offset {
out[j] = out[j - 1]
j = j - 1
}
out[j] = cur
}
out
}
///|
pub fn LinearGradient::new(
x0 : Double,
y0 : Double,
x1 : Double,
y1 : Double,
stops~ : Array[ColorStop],
) -> LinearGradient {
{ x0, y0, x1, y1, stops: sorted_stops(stops) }
}
///|
/// Radial gradient from circle (x0, y0, r0) to circle (x1, y1, r1) in user
/// space. Matches HTML Canvas `createRadialGradient`.
pub struct RadialGradient {
x0 : Double
y0 : Double
r0 : Double
x1 : Double
y1 : Double
r1 : Double
stops : Array[ColorStop]
} derive(Eq)
///|
pub impl Show for RadialGradient with fn output(self, logger) {
logger.write_string("RadialGradient { x0: ")
logger.write_object(self.x0)
logger.write_string(", y0: ")
logger.write_object(self.y0)
logger.write_string(", r0: ")
logger.write_object(self.r0)
logger.write_string(", x1: ")
logger.write_object(self.x1)
logger.write_string(", y1: ")
logger.write_object(self.y1)
logger.write_string(", r1: ")
logger.write_object(self.r1)
logger.write_string(", stops: ")
logger.write_string(color_stops_to_string(self.stops))
logger.write_string(" }")
}
///|
pub fn RadialGradient::new(
x0 : Double,
y0 : Double,
r0 : Double,
x1 : Double,
y1 : Double,
r1 : Double,
stops~ : Array[ColorStop],
) -> RadialGradient {
{ x0, y0, r0, x1, y1, r1, stops: sorted_stops(stops) }
}
///|
/// A draw-time fill source — solid color or gradient.
pub enum FillStyle {
Solid(Color)
Linear(LinearGradient)
Radial(RadialGradient)
} derive(Eq)
///|
pub impl Show for FillStyle with fn output(self, logger) {
match self {
Solid(c) => {
logger.write_string("Solid(")
c.output(logger)
logger.write_string(")")
}
Linear(g) => {
logger.write_string("Linear(")
g.output(logger)
logger.write_string(")")
}
Radial(g) => {
logger.write_string("Radial(")
g.output(logger)
logger.write_string(")")
}
}
}
///|
pub fn FillStyle::solid(c : Color) -> FillStyle {
FillStyle::Solid(c)
}
///|
pub fn FillStyle::linear(g : LinearGradient) -> FillStyle {
FillStyle::Linear(g)
}
///|
pub fn FillStyle::radial(g : RadialGradient) -> FillStyle {
FillStyle::Radial(g)
}
///|
/// Interpolate a color from a sorted, non-empty `stops` array at position
/// `t`. Values of `t` outside `[stops[0].offset, stops[last].offset]` clamp
/// to the first/last color. Interpolation is sRGB component-wise.
pub fn interp_stops(stops : Array[ColorStop], t : Double) -> Color {
if stops.length() == 1 {
return stops[0].color
}
let first = stops[0]
let last = stops[stops.length() - 1]
if t <= first.offset {
return first.color
}
if t >= last.offset {
return last.color
}
for i in 0..<(stops.length() - 1) {
let lo = stops[i]
let hi = stops[i + 1]
if t >= lo.offset && t <= hi.offset {
let span = hi.offset - lo.offset
let u = if span == 0.0 { 0.0 } else { (t - lo.offset) / span }
return {
r: lerp_u8(lo.color.r, hi.color.r, u),
g: lerp_u8(lo.color.g, hi.color.g, u),
b: lerp_u8(lo.color.b, hi.color.b, u),
a: lo.color.a + (hi.color.a - lo.color.a) * u,
}
}
}
last.color
}
///|
/// sRGB-space lerp between two channel values (0..=255) rounded to nearest.
fn lerp_u8(a : Int, b : Int, u : Double) -> Int {
let af = a.to_double()
let bf = b.to_double()
(af * (1.0 - u) + bf * u + 0.5).to_int()
}
///|
/// Sample a linear gradient at user-space `(u, v)`.
/// The gradient parameter `t` is the projection of `(u - x0, v - y0)` onto
/// the axis `(x1 - x0, y1 - y0)`, normalized by the axis squared length.
/// Values of `t` outside `[0, 1]` clamp to the endpoint colors.
pub fn sample_linear(g : LinearGradient, u : Double, v : Double) -> Color {
let dx = g.x1 - g.x0
let dy = g.y1 - g.y0
let len_sq = dx * dx + dy * dy
if len_sq == 0.0 {
return g.stops[0].color
}
let t_raw = ((u - g.x0) * dx + (v - g.y0) * dy) / len_sq
let t = if t_raw < 0.0 { 0.0 } else if t_raw > 1.0 { 1.0 } else { t_raw }
interp_stops(g.stops, t)
}
///|
/// Sample a 2-circle radial gradient at user-space `(u, v)`. Solves the
/// quadratic for the parameter `t` such that `(u, v)` lies on the circle
/// `P(t) = (1-t) * C0 + t * C1` with radius `r0 + t * dr`. Picks the larger
/// valid root (outer circle contribution). Out-of-range `t` clamps to the
/// endpoint colors. Returns `Color::transparent()` when no valid root exists
/// (the pixel is outside the gradient's drawable region).
pub fn sample_radial(g : RadialGradient, u : Double, v : Double) -> Color {
let dx = g.x1 - g.x0
let dy = g.y1 - g.y0
let dr = g.r1 - g.r0
let fx = u - g.x0
let fy = v - g.y0
let a = dx * dx + dy * dy - dr * dr
let b = -2.0 * (fx * dx + fy * dy + g.r0 * dr)
let c = fx * fx + fy * fy - g.r0 * g.r0
// Degenerate: the two circles coincide in position (a == 0) -> linear in t.
let abs_a = if a < 0.0 { -a } else { a }
if abs_a < 1.0e-12 {
let abs_b = if b < 0.0 { -b } else { b }
if abs_b < 1.0e-12 {
return Color::transparent()
}
let t = -c / b
if t < 0.0 || g.r0 + t * dr < 0.0 {
return Color::transparent()
}
let t_clamped = if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
return interp_stops(g.stops, t_clamped)
}
let discr = b * b - 4.0 * a * c
if discr < 0.0 {
return Color::transparent()
}
let sq = discr.sqrt()
let t_plus = (-b + sq) / (2.0 * a)
let t_minus = (-b - sq) / (2.0 * a)
// Prefer the larger root whose circle radius is non-negative.
let t_final = if g.r0 + t_plus * dr >= 0.0 {
t_plus
} else if g.r0 + t_minus * dr >= 0.0 {
t_minus
} else {
return Color::transparent()
}
let t_clamped = if t_final < 0.0 {
0.0
} else if t_final > 1.0 {
1.0
} else {
t_final
}
interp_stops(g.stops, t_clamped)
}