import platform
import pyray as rl
import numpy as np
from dataclasses import dataclass
from typing import Any , Optional , cast
from openpilot . system . ui . lib . application import gui_app
MAX_GRADIENT_COLORS = 15 # includes stops as well
@dataclass
class Gradient :
start : tuple [ float , float ]
end : tuple [ float , float ]
colors : list [ rl . Color ]
stops : list [ float ]
def __post_init__ ( self ) :
if len ( self . colors ) > MAX_GRADIENT_COLORS :
self . colors = self . colors [ : MAX_GRADIENT_COLORS ]
print ( f " Warning: Gradient colors truncated to { MAX_GRADIENT_COLORS } entries " )
if len ( self . stops ) > MAX_GRADIENT_COLORS :
self . stops = self . stops [ : MAX_GRADIENT_COLORS ]
print ( f " Warning: Gradient stops truncated to { MAX_GRADIENT_COLORS } entries " )
if not len ( self . stops ) :
color_count = min ( len ( self . colors ) , MAX_GRADIENT_COLORS )
self . stops = [ i / max ( 1 , color_count - 1 ) for i in range ( color_count ) ]
VERSION = """
#version 300 es
precision highp float ;
"""
if platform . system ( ) == " Darwin " :
VERSION = """
#version 330 core
"""
FRAGMENT_SHADER = VERSION + """
in vec2 fragTexCoord ;
out vec4 finalColor ;
uniform vec4 fillColor ;
/ / Gradient line defined in * screen pixels *
uniform int useGradient ;
uniform vec2 gradientStart ; / / e . g . vec2 ( 0 , 0 )
uniform vec2 gradientEnd ; / / e . g . vec2 ( 0 , screenHeight )
uniform vec4 gradientColors [ 15 ] ;
uniform float gradientStops [ 15 ] ;
uniform int gradientColorCount ;
vec4 getGradientColor ( vec2 p ) {
/ / Compute t from screen - space position
vec2 d = gradientStart - gradientEnd ;
float len2 = max ( dot ( d , d ) , 1e-6 ) ;
float t = clamp ( dot ( p - gradientEnd , d ) / len2 , 0.0 , 1.0 ) ;
/ / Clamp to range
float t0 = gradientStops [ 0 ] ;
float tn = gradientStops [ gradientColorCount - 1 ] ;
if ( t < = t0 ) return gradientColors [ 0 ] ;
if ( t > = tn ) return gradientColors [ gradientColorCount - 1 ] ;
for ( int i = 0 ; i < gradientColorCount - 1 ; i + + ) {
float a = gradientStops [ i ] ;
float b = gradientStops [ i + 1 ] ;
if ( t > = a & & t < = b ) {
float k = ( t - a ) / max ( b - a , 1e-6 ) ;
return mix ( gradientColors [ i ] , gradientColors [ i + 1 ] , k ) ;
}
}
return gradientColors [ gradientColorCount - 1 ] ;
}
void main ( ) {
/ / TODO : do proper antialiasing
finalColor = useGradient == 1 ? getGradientColor ( gl_FragCoord . xy ) : fillColor ;
}
"""
# Default vertex shader
VERTEX_SHADER = VERSION + """
in vec3 vertexPosition ;
in vec2 vertexTexCoord ;
out vec2 fragTexCoord ;
uniform mat4 mvp ;
void main ( ) {
fragTexCoord = vertexTexCoord ;
gl_Position = mvp * vec4 ( vertexPosition , 1.0 ) ;
}
"""
UNIFORM_INT = rl . ShaderUniformDataType . SHADER_UNIFORM_INT
UNIFORM_FLOAT = rl . ShaderUniformDataType . SHADER_UNIFORM_FLOAT
UNIFORM_VEC2 = rl . ShaderUniformDataType . SHADER_UNIFORM_VEC2
UNIFORM_VEC4 = rl . ShaderUniformDataType . SHADER_UNIFORM_VEC4
class ShaderState :
_instance : Any = None
@classmethod
def get_instance ( cls ) :
if cls . _instance is None :
cls . _instance = cls ( )
return cls . _instance
def __init__ ( self ) :
if ShaderState . _instance is not None :
raise Exception ( " This class is a singleton. Use get_instance() instead. " )
self . initialized = False
self . shader = None
# Shader uniform locations
self . locations = {
' fillColor ' : None ,
' useGradient ' : None ,
' gradientStart ' : None ,
' gradientEnd ' : None ,
' gradientColors ' : None ,
' gradientStops ' : None ,
' gradientColorCount ' : None ,
' mvp ' : None ,
}
# Pre-allocated FFI objects
self . fill_color_ptr = rl . ffi . new ( " float[] " , [ 0.0 , 0.0 , 0.0 , 0.0 ] )
self . use_gradient_ptr = rl . ffi . new ( " int[] " , [ 0 ] )
self . color_count_ptr = rl . ffi . new ( " int[] " , [ 0 ] )
self . gradient_colors_ptr = rl . ffi . new ( " float[] " , MAX_GRADIENT_COLORS * 4 )
self . gradient_stops_ptr = rl . ffi . new ( " float[] " , MAX_GRADIENT_COLORS )
def initialize ( self ) :
if self . initialized :
return
self . shader = rl . load_shader_from_memory ( VERTEX_SHADER , FRAGMENT_SHADER )
# Cache all uniform locations
for uniform in self . locations . keys ( ) :
self . locations [ uniform ] = rl . get_shader_location ( self . shader , uniform )
# Orthographic MVP (origin top-left)
proj = rl . matrix_ortho ( 0 , gui_app . width , gui_app . height , 0 , - 1 , 1 )
rl . set_shader_value_matrix ( self . shader , self . locations [ ' mvp ' ] , proj )
self . initialized = True
def cleanup ( self ) :
if not self . initialized :
return
if self . shader :
rl . unload_shader ( self . shader )
self . shader = None
self . initialized = False
def _configure_shader_color ( state : ShaderState , color : Optional [ rl . Color ] , # noqa: UP045
gradient : Gradient | None , origin_rect : rl . Rectangle ) :
assert ( color is not None ) != ( gradient is not None ) , " Either color or gradient must be provided "
use_gradient = 1 if ( gradient is not None and len ( gradient . colors ) > = 1 ) else 0
state . use_gradient_ptr [ 0 ] = use_gradient
rl . set_shader_value ( state . shader , state . locations [ ' useGradient ' ] , state . use_gradient_ptr , UNIFORM_INT )
if use_gradient :
gradient = cast ( Gradient , gradient )
state . color_count_ptr [ 0 ] = len ( gradient . colors )
for i in range ( len ( gradient . colors ) ) :
c = gradient . colors [ i ]
base = i * 4
state . gradient_colors_ptr [ base : base + 4 ] = [ c . r / 255.0 , c . g / 255.0 , c . b / 255.0 , c . a / 255.0 ]
rl . set_shader_value_v ( state . shader , state . locations [ ' gradientColors ' ] , state . gradient_colors_ptr , UNIFORM_VEC4 , len ( gradient . colors ) )
for i in range ( len ( gradient . stops ) ) :
s = float ( gradient . stops [ i ] )
state . gradient_stops_ptr [ i ] = 0.0 if s < 0.0 else 1.0 if s > 1.0 else s
rl . set_shader_value_v ( state . shader , state . locations [ ' gradientStops ' ] , state . gradient_stops_ptr , UNIFORM_FLOAT , len ( gradient . stops ) )
rl . set_shader_value ( state . shader , state . locations [ ' gradientColorCount ' ] , state . color_count_ptr , UNIFORM_INT )
# Map normalized start/end to screen pixels
start_vec = rl . Vector2 ( origin_rect . x + gradient . start [ 0 ] * origin_rect . width , origin_rect . y + gradient . start [ 1 ] * origin_rect . height )
end_vec = rl . Vector2 ( origin_rect . x + gradient . end [ 0 ] * origin_rect . width , origin_rect . y + gradient . end [ 1 ] * origin_rect . height )
rl . set_shader_value ( state . shader , state . locations [ ' gradientStart ' ] , start_vec , UNIFORM_VEC2 )
rl . set_shader_value ( state . shader , state . locations [ ' gradientEnd ' ] , end_vec , UNIFORM_VEC2 )
else :
color = color or rl . WHITE
state . fill_color_ptr [ 0 : 4 ] = [ color . r / 255.0 , color . g / 255.0 , color . b / 255.0 , color . a / 255.0 ]
rl . set_shader_value ( state . shader , state . locations [ ' fillColor ' ] , state . fill_color_ptr , UNIFORM_VEC4 )
def triangulate ( pts : np . ndarray ) - > list [ tuple [ float , float ] ] :
""" Only supports simple polygons with two chains (ribbon). """
# TODO: consider deduping close screenspace points
# interleave points to produce a triangle strip
assert len ( pts ) % 2 == 0 , " Interleaving expects even number of points "
tri_strip = [ ]
for i in range ( len ( pts ) / / 2 ) :
tri_strip . append ( pts [ i ] )
tri_strip . append ( pts [ - i - 1 ] )
return cast ( list , np . array ( tri_strip ) . tolist ( ) )
def draw_polygon ( origin_rect : rl . Rectangle , points : np . ndarray ,
color : Optional [ rl . Color ] = None , gradient : Gradient | None = None ) : # noqa: UP045
"""
Draw a ribbon polygon ( two chains ) with a triangle strip and gradient .
- Input must be [ L0 . . Lk - 1 , Rk - 1. . R0 ] , even count , no crossings / holes .
"""
if len ( points ) < 3 :
return
# Initialize shader on-demand
state = ShaderState . get_instance ( )
state . initialize ( )
# Ensure (N,2) float32 contiguous array
pts = np . ascontiguousarray ( points , dtype = np . float32 )
assert pts . ndim == 2 and pts . shape [ 1 ] == 2 , " points must be (N,2) "
# Configure gradient shader
_configure_shader_color ( state , color , gradient , origin_rect )
# Triangulate via interleaving
tri_strip = triangulate ( pts )
# Draw strip, color here doesn't matter
rl . begin_shader_mode ( state . shader )
rl . draw_triangle_strip ( tri_strip , len ( tri_strip ) , rl . WHITE )
rl . end_shader_mode ( )
def cleanup_shader_resources ( ) :
state = ShaderState . get_instance ( )
state . cleanup ( )