ludic/packages/ludic.ui/draw_gradient.ludic

95 lines
3.7 KiB
Text

# draw_gradient.ludic - background: linear-gradient(to bottom, #102030, rgba(0,0,0,0) 80%): colour
# stops (a stop without a place is spread evenly), along `to top|bottom|left|right` or an angle
# (0deg is up, 90deg right, taken to the nearest side); drawn as whole-pixel bands, square-cornered
export property UiGradient {
dir: int = 0 # 0 down, 1 up, 2 right, 3 left
cols: []int = null
alphas: []float = null
at: []float = null # each stop's place, 0 to 1
}
function gr_is(s: string) -> bool { return len(s) > 16 and sat(s, 0, "linear-gradient(") }
# a gradient is parsed once per text and shared by every element that wears it - never changed after
function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
let m = mm_of(ui_st)
let i = mm_find(m, MM_GRAD, s0)
if i >= 0 { return m.grads[i] }
let g = gr_parse_new(ui_st, s0)
let j = mm_add(m, MM_GRAD, s0)
if j >= 0 { m.grads[j] = g }
return g
}
function gr_parse_new(ui_st: mut UiState, s0: string) -> UiGradient {
let s = tw(ui_st, s0)
let parts = el_commas(s[16..len(s) - 1])
let g = new UiGradient
g.cols = new []int
g.alphas = new []float
g.at = new []float
var from = 0
if len(parts) > 0 and gr_dir(g, tw(ui_st, parts[0])) { from = 1 }
for i in from .. len(parts) {
let words = mm_words(ui_st, tw(ui_st, parts[i]))
var col = tw(ui_st, parts[i])
var at = -1.0
let last = words[len(words) - 1]
if len(words) > 1 and last[len(last) - 1] == 37 {
at = el_num(last) / 100.0
col = tw(ui_st, col[0..len(col) - len(last)])
}
push(g.cols, ui_color(ui_st, col))
push(g.alphas, ui_st.cl_alpha)
push(g.at, at)
}
for i in 0 .. len(g.at) {
if g.at[i] < 0.0 { g.at[i] = float(i) / float(Math.max(len(g.at) - 1, 1)) }
}
return g
}
function gr_dir(g: UiGradient, w: string) -> bool {
if w == "to bottom" { g.dir = 0 } else if w == "to top" { g.dir = 1 } else if w == "to right" { g.dir = 2 } else if w == "to left" { g.dir = 3 } else if len(w) > 3 and sat(w, len(w) - 3, "deg") {
let d = int(Math.round(el_num(w) / 90.0)) % 4
g.dir = 1
if d == 1 or d == -3 { g.dir = 2 }
if d == 2 or d == -2 { g.dir = 0 }
if d == 3 or d == -1 { g.dir = 3 }
} else { return false }
return true
}
# the gradient over the box, a band every two pixels at most
function gr_draw(ui_st: mut UiState, g: UiGradient, x: float, y: float, w: float, h: float, a: float) -> void {
if ui_st.ui_be.rect == null or len(g.cols) == 0 { return }
let across = g.dir >= 2
var span = h
if across { span = w }
let bands = Math.clamp(int(span / 2.0), 1, 64)
for i in 0 .. bands {
let e0 = Math.floor(span * float(i) / float(bands) + 0.5)
let e1 = Math.floor(span * float(i + 1) / float(bands) + 0.5)
var t = (e0 + e1) / 2.0 / span
if g.dir == 1 or g.dir == 3 { t = 1.0 - t }
let c = gr_at(ui_st, g, t)
if across { ui_st.ui_be.rect(x + e0, y, e1 - e0, h, c, ui_st.gr_a * a) } else { ui_st.ui_be.rect(x, y + e0, w, e1 - e0, c, ui_st.gr_a * a) }
}
}
# the colour at `t` between the stops around it, its alpha in gr_a
function gr_at(ui_st: mut UiState, g: UiGradient, t: float) -> int {
var k = 0
while k + 1 < len(g.at) and g.at[k + 1] < t { k += 1 }
let j = Math.min(k + 1, len(g.at) - 1)
var f = 0.0
if g.at[j] > g.at[k] { f = Math.clamp((t - g.at[k]) / (g.at[j] - g.at[k]), 0.0, 1.0) }
if t <= g.at[0] { f = 0.0 }
ui_st.gr_a = g.alphas[k] + (g.alphas[j] - g.alphas[k]) * f
return cl_mix(g.cols[k], g.cols[j], f)
}
function cl_mix(a: int, b: int, f: float) -> int {
var out = 0
var shift = 65536
for i in 0 .. 3 {
let ca = a / shift % 256
let cb = b / shift % 256
out = out + int(Math.round(float(ca) + float(cb - ca) * f)) * shift
shift = shift / 256
}
return out
}