Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions system/ui/lib/shader_shimmer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import platform
import pyray as rl
from openpilot.system.ui.lib.application import gui_app

VERSION = """
#version 300 es
precision mediump float;
"""
if platform.system() == "Darwin":
VERSION = """
#version 330 core
"""

VERTEX_SHADER = VERSION + """
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec3 vertexNormal;
in vec4 vertexColor;
uniform mat4 mvp;
out vec2 fragTexCoord;
out vec4 fragColor;
void main() {
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
gl_Position = mvp * vec4(vertexPosition, 1.0);
}
"""

SHIMMER_FRAGMENT_SHADER = VERSION + """
in vec2 fragTexCoord;
in vec4 fragColor;
uniform sampler2D texture0;
uniform float time;
uniform float shimmerWidth;
uniform float shimmerSpeed;
uniform float sliderPercentage;
uniform float opacity;
out vec4 finalColor;

void main() {
vec4 texColor = texture(texture0, fragTexCoord);
float xPos = fragTexCoord.x;
float shimmerPos = mod(-time * shimmerSpeed, 1.0 + shimmerWidth);
float distFromShimmer = abs(xPos - shimmerPos);
float mask = 1.0 - smoothstep(0.0, shimmerWidth, distFromShimmer);
vec3 shimmerColor = vec3(1.0, 1.0, 1.0);
vec3 finalRGB = mix(texColor.rgb, shimmerColor, mask);
float alphaFade = (1.0 - sliderPercentage) * opacity;
float finalAlpha = texColor.a * alphaFade;
finalColor = vec4(finalRGB, finalAlpha) * fragColor;
}
"""

UNIFORM_FLOAT = rl.ShaderUniformDataType.SHADER_UNIFORM_FLOAT


class ShimmerShader:
_instance: 'ShimmerShader' | None = None

@classmethod
def get_instance(cls) -> 'ShimmerShader':
if cls._instance is None:
cls._instance = cls()
cls._instance.initialize()
return cls._instance

def __init__(self):
if ShimmerShader._instance is not None:
raise Exception("This class is a singleton. Use get_instance() instead.")

self.initialized = False
self.shader = None

self.locations = {
'time': None,
'shimmerWidth': None,
'shimmerSpeed': None,
'sliderPercentage': None,
'opacity': None,
'mvp': None,
}

self.time_ptr = rl.ffi.new("float[]", [0.0])
self.shimmer_width_ptr = rl.ffi.new("float[]", [0.15])
self.shimmer_speed_ptr = rl.ffi.new("float[]", [0.6])
self.slider_percentage_ptr = rl.ffi.new("float[]", [0.0])
self.opacity_ptr = rl.ffi.new("float[]", [1.0])

def initialize(self):
if self.initialized:
return

self.shader = rl.load_shader_from_memory(VERTEX_SHADER, SHIMMER_FRAGMENT_SHADER)

for uniform in self.locations.keys():
self.locations[uniform] = rl.get_shader_location(self.shader, uniform)

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)
rl.set_shader_value(self.shader, self.locations['shimmerWidth'], self.shimmer_width_ptr, UNIFORM_FLOAT)
rl.set_shader_value(self.shader, self.locations['shimmerSpeed'], self.shimmer_speed_ptr, UNIFORM_FLOAT)

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 set_uniforms(self, time: float, slider_percentage: float, opacity: float):
if not self.initialized:
self.initialize()

self.time_ptr[0] = time
self.slider_percentage_ptr[0] = slider_percentage
self.opacity_ptr[0] = opacity

rl.set_shader_value(self.shader, self.locations['time'], self.time_ptr, UNIFORM_FLOAT)
rl.set_shader_value(self.shader, self.locations['sliderPercentage'], self.slider_percentage_ptr, UNIFORM_FLOAT)
rl.set_shader_value(self.shader, self.locations['opacity'], self.opacity_ptr, UNIFORM_FLOAT)


def cleanup_shimmer_shader_resources():
state = ShimmerShader.get_instance()
state.cleanup()

64 changes: 59 additions & 5 deletions system/ui/widgets/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pyray as rl

from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.shader_shimmer import ShimmerShader
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.common.filter_simple import FirstOrderFilter
Expand Down Expand Up @@ -37,6 +38,13 @@ def __init__(self, title: str, confirm_callback: Callable | None = None):
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9)

self._shader_state = ShimmerShader.get_instance()

self._text_render_texture: rl.RenderTexture | None = None
self._text_render_texture_width = 0
self._text_render_texture_height = 0
self._last_text_color: rl.Color | None = None

def _load_assets(self):
self.set_rect(rl.Rectangle(0, 0, 316 + self.HORIZONTAL_PADDING * 2, 100))

Expand All @@ -57,6 +65,46 @@ def reset(self):
def set_opacity(self, opacity: float):
self._opacity = opacity

def _ensure_render_texture(self, width: int, height: int) -> rl.RenderTexture:
if (self._text_render_texture is None or
self._text_render_texture_width != width or
self._text_render_texture_height != height):
if self._text_render_texture is not None:
rl.unload_render_texture(self._text_render_texture)

self._text_render_texture = rl.load_render_texture(width, height)
rl.set_texture_filter(self._text_render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
self._text_render_texture_width = width
self._text_render_texture_height = height

return self._text_render_texture

def _render_text_to_texture(self, label_rect: rl.Rectangle, text_color: rl.Color):
# Only re-render if color changed
if (self._last_text_color is not None and
self._last_text_color.r == text_color.r and
self._last_text_color.g == text_color.g and
self._last_text_color.b == text_color.b and
self._last_text_color.a == text_color.a):
return

self._last_text_color = text_color
width = int(label_rect.width)
height = int(label_rect.height)

rl.begin_texture_mode(self._ensure_render_texture(width, height))
rl.clear_background(rl.Color(0, 0, 0, 0)) # Transparent background
self._label.render(rl.Rectangle(0, 0, width, height))
rl.end_texture_mode()

def close(self):
if self._text_render_texture is not None:
rl.unload_render_texture(self._text_render_texture)
self._text_render_texture = None

def __del__(self):
self.close()

@property
def slider_percentage(self):
activated_pos = -self._bg_txt.width + self._circle_bg_txt.width
Expand Down Expand Up @@ -115,8 +163,6 @@ def _update_state(self):
self._scroll_x_circle_filter.x = self._scroll_x_circle

def _render(self, _):
# TODO: iOS text shimmering animation

white = rl.Color(255, 255, 255, int(255 * self._opacity))

bg_txt_x = self._rect.x + (self._rect.width - self._bg_txt.width) / 2
Expand All @@ -126,15 +172,23 @@ def _render(self, _):
btn_x = bg_txt_x + self._bg_txt.width - self._circle_bg_txt.width + self._scroll_x_circle_filter.x
btn_y = self._rect.y + (self._rect.height - self._circle_bg_txt.height) / 2

if self._confirmed_time == 0.0 or self._scroll_x_circle > 0:
self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity)))
if self._confirmed_time == 0 or self._scroll_x_circle > 0:
text_color = rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity))
self._label.set_text_color(text_color)
label_rect = rl.Rectangle(
self._rect.x + 20,
self._rect.y,
self._rect.width - self._circle_bg_txt.width - 20 * 3,
self._rect.height,
)
self._label.render(label_rect)

self._render_text_to_texture(label_rect, text_color)
self._shader_state.set_uniforms(rl.get_time(), self.slider_percentage, self._opacity)

rl.begin_shader_mode(self._shader_state.shader)
src_rect = rl.Rectangle(0, 0, float(self._text_render_texture_width), -float(self._text_render_texture_height))
rl.draw_texture_pro(self._text_render_texture.texture, src_rect, label_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
rl.end_shader_mode()

# circle and arrow
rl.draw_texture_ex(self._circle_bg_txt, rl.Vector2(btn_x, btn_y), 0.0, 1.0, white)
Expand Down
Loading