components_ui_Gradient.bs
import "pkg:/source/roku_modules/log/LogMixin.brs"
' Renders a smooth fade by stretching a tiny alpha-ramp bitmap (one Poster),
' tinted via blendColor. The assets are generated by
' scripts/generate/gradient-assets.js (npm run gradients:build) and are shared
' across every Gradient instance by the texture manager (~36KB decoded each,
' measured — texture cost follows the SOURCE bitmap dimensions, not the
' displayed size).
'
' History (#777): the previous implementation stacked up to 300 1–2px
' Rectangle strips. Zero texture memory, but the strips aliased into visible
' bars/gaps under the FHD→HD autoscale on 720p-UI devices, and each rebuild
' cost 37–77ms on the render thread of a low-end stick (measured; the Poster
' path is 0–2ms). A stretched ramp bitmap is scale-independent at every UI
' resolution and is the same technique Roku OS uses for its own player shade.
'
' Contract: fades one color out to nothing, with the transparent end in either
' position — startColor opaque → endColor alpha 0 (most call sites), or
' startColor alpha 0 → endColor opaque (ItemDetails' dropdownDimmer). The
' transparent end's RGB is ignored, since invisible is invisible. A fade
' between two VISIBLE colors needs a second hue that one blendColor cannot
' carry; that fires a debug warning.
sub init()
m.log = new log.Logger("Gradient")
' Watch for field changes
m.top.observeField("width", "recreateGradient")
m.top.observeField("height", "recreateGradient")
m.top.observeField("startColor", "recreateGradient")
m.top.observeField("endColor", "recreateGradient")
m.top.observeField("direction", "recreateGradient")
m.top.observeField("rotateDegrees", "recreateGradient")
' Create initial gradient
createGradient()
end sub
sub recreateGradient()
' Clear the existing poster
m.top.removeChildren(m.top.getChildren(-1, 0))
' Create new gradient
createGradient()
end sub
sub createGradient()
width = m.top.width
height = m.top.height
startColor = m.top.startColor
endColor = m.top.endColor
direction = m.top.direction
rotation = m.top.rotateDegrees
' 180°/270° rotations move the opaque end to the far edge (flipped ramp asset)
flipped = (rotation = 180 or rotation = 270)
' Determine actual rendering direction based on original direction + rotation
if direction = "vertical"
if rotation = 0 or rotation = 180
actualDirection = "vertical"
else ' rotation = 90 or 270
actualDirection = "horizontal"
end if
else ' direction = "horizontal"
if rotation = 0 or rotation = 180
actualDirection = "horizontal"
else ' rotation = 90 or 270
actualDirection = "vertical"
end if
end if
' The fade direction can be expressed two ways: rotateDegrees=180, or the
' inverted color order (startColor transparent, endColor opaque — see
' ItemDetails' dropdownDimmer). Normalize the inverted form: it's the flipped
' ramp tinted with the opaque end's color.
tintColor = startColor
if (startColor and &hFF) = 0 and (endColor and &hFF) <> 0
tintColor = endColor
flipped = not flipped
end if
' A single tinted ramp fades one color out to nothing, so a fade between two
' VISIBLE ends is the shape it cannot express. Surface that loudly in debug
' builds instead of rendering the wrong colors silently.
'
' Deliberately does NOT compare the two RGBs: a fully transparent end has no
' meaningful RGB, and callers set the two colors in separate statements, so
' the observer sees the half-updated state (PresentationBackdrop assigns
' endColor before startColor). An RGB comparison fired on that transient and
' warned on correct usage — confirmed on device during #777 review.
if (startColor and &hFF) <> 0 and (endColor and &hFF) <> 0
m.log.warn("Gradient needs one fully transparent end; got", startColor, endColor)
end if
if actualDirection = "vertical"
if flipped
assetName = "fade-v180"
else
assetName = "fade-v"
end if
else
if flipped
assetName = "fade-h180"
else
assetName = "fade-h"
end if
end if
poster = createObject("roSGNode", "Poster")
poster.width = width
poster.height = height
poster.loadDisplayMode = "scaleToFill"
' White ramp × blendColor = the opaque end's RGB, with alpha = ramp × that end's alpha
poster.blendColor = tintColor
poster.uri = "pkg:/images/gradients/" + assetName + ".png"
m.top.appendChild(poster)
end sub