feat(theme): Implement comprehensive CRT Filter (scanlines, vignette, noise)
This commit is contained in:
22
src/gui_2.py
22
src/gui_2.py
@@ -131,7 +131,8 @@ class App:
|
|||||||
self._token_stats: dict[str, Any] = {}
|
self._token_stats: dict[str, Any] = {}
|
||||||
self._token_stats_dirty: bool = True
|
self._token_stats_dirty: bool = True
|
||||||
self.perf_history: dict[str, list] = {"frame_time": [0.0] * 100, "fps": [0.0] * 100}
|
self.perf_history: dict[str, list] = {"frame_time": [0.0] * 100, "fps": [0.0] * 100}
|
||||||
self._nerv_scanlines = theme_fx.ScanlineOverlay()
|
self._nerv_crt = theme_fx.CRTFilter()
|
||||||
|
self.ui_crt_filter = True
|
||||||
self._nerv_alert = theme_fx.AlertPulsing()
|
self._nerv_alert = theme_fx.AlertPulsing()
|
||||||
self._nerv_flicker = theme_fx.StatusFlicker()
|
self._nerv_flicker = theme_fx.StatusFlicker()
|
||||||
|
|
||||||
@@ -307,9 +308,8 @@ class App:
|
|||||||
ws = imgui.get_io().display_size
|
ws = imgui.get_io().display_size
|
||||||
self._nerv_alert.update(self.ai_status)
|
self._nerv_alert.update(self.ai_status)
|
||||||
self._nerv_alert.render(ws.x, ws.y)
|
self._nerv_alert.render(ws.x, ws.y)
|
||||||
self._nerv_scanlines.render(ws.x, ws.y)
|
self._nerv_crt.enabled = self.ui_crt_filter
|
||||||
|
self._nerv_crt.render(ws.x, ws.y)
|
||||||
pushed_prior_tint = False
|
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_gui_func")
|
||||||
if self.is_viewing_prior_session:
|
if self.is_viewing_prior_session:
|
||||||
imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20))
|
imgui.push_style_color(imgui.Col_.window_bg, vec4(50, 40, 20))
|
||||||
@@ -2895,17 +2895,21 @@ def hello():
|
|||||||
ch_ct, ctrans = imgui.slider_float("##ctrans", theme.get_child_transparency(), 0.1, 1.0, "%.2f")
|
ch_ct, ctrans = imgui.slider_float("##ctrans", theme.get_child_transparency(), 0.1, 1.0, "%.2f")
|
||||||
if ch_ct:
|
if ch_ct:
|
||||||
theme.set_child_transparency(ctrans)
|
theme.set_child_transparency(ctrans)
|
||||||
self._flush_to_config()
|
|
||||||
models.save_config(self.config)
|
|
||||||
|
|
||||||
imgui.separator()
|
|
||||||
bg = bg_shader.get_bg()
|
|
||||||
ch_bg, bg.enabled = imgui.checkbox("Animated Background Shader", bg.enabled)
|
ch_bg, bg.enabled = imgui.checkbox("Animated Background Shader", bg.enabled)
|
||||||
if ch_bg:
|
if ch_bg:
|
||||||
gui_cfg = self.config.setdefault("gui", {})
|
gui_cfg = self.config.setdefault("gui", {})
|
||||||
gui_cfg["bg_shader_enabled"] = bg.enabled
|
gui_cfg["bg_shader_enabled"] = bg.enabled
|
||||||
self._flush_to_config()
|
self._flush_to_config()
|
||||||
models.save_config(self.config)
|
models.save_config(self.config)
|
||||||
|
|
||||||
|
ch_crt, self.ui_crt_filter = imgui.checkbox("CRT Filter", self.ui_crt_filter)
|
||||||
|
if ch_crt:
|
||||||
|
gui_cfg = self.config.setdefault("gui", {})
|
||||||
|
gui_cfg["crt_filter_enabled"] = self.ui_crt_filter
|
||||||
|
self._flush_to_config()
|
||||||
|
models.save_config(self.config)
|
||||||
|
self._flush_to_config()
|
||||||
|
models.save_config(self.config)
|
||||||
imgui.end()
|
imgui.end()
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel")
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
import random
|
||||||
from imgui_bundle import imgui
|
from imgui_bundle import imgui
|
||||||
|
|
||||||
class ScanlineOverlay:
|
class CRTFilter:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
|
|
||||||
@@ -10,9 +11,28 @@ class ScanlineOverlay:
|
|||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
return
|
return
|
||||||
draw_list = imgui.get_foreground_draw_list()
|
draw_list = imgui.get_foreground_draw_list()
|
||||||
color = imgui.get_color_u32((0.0, 0.0, 0.0, 0.06))
|
|
||||||
|
# Scanlines
|
||||||
|
scanline_color = imgui.get_color_u32((0.0, 0.0, 0.0, 0.06))
|
||||||
for y in range(0, int(height), 2):
|
for y in range(0, int(height), 2):
|
||||||
draw_list.add_line((0.0, float(y)), (float(width), float(y)), color, 1.0)
|
draw_list.add_line((0.0, float(y)), (float(width), float(y)), scanline_color, 1.0)
|
||||||
|
|
||||||
|
# Subtle Vignette
|
||||||
|
v_steps = 15
|
||||||
|
for i in range(v_steps):
|
||||||
|
alpha = (i / v_steps) ** 2.0 * 0.15
|
||||||
|
v_color = imgui.get_color_u32((0.0, 0.0, 0.0, alpha))
|
||||||
|
inset = (v_steps - i) * 6.0
|
||||||
|
if width > inset * 2.0 and height > inset * 2.0:
|
||||||
|
draw_list.add_rect((inset, inset), (width - inset, height - inset), v_color, 40.0, 0, 10.0)
|
||||||
|
|
||||||
|
# Subtle Random Noise
|
||||||
|
for _ in range(30):
|
||||||
|
nx = random.uniform(0.0, width)
|
||||||
|
ny = random.uniform(0.0, height)
|
||||||
|
n_alpha = random.uniform(0.01, 0.03)
|
||||||
|
n_color = imgui.get_color_u32((1.0, 1.0, 1.0, n_alpha))
|
||||||
|
draw_list.add_rect_filled((nx, ny), (nx + 1.0, ny + 1.0), n_color)
|
||||||
|
|
||||||
class StatusFlicker:
|
class StatusFlicker:
|
||||||
def get_alpha(self) -> float:
|
def get_alpha(self) -> float:
|
||||||
|
|||||||
@@ -1,30 +1,45 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
import math
|
import math
|
||||||
from src.theme_nerv_fx import ScanlineOverlay, StatusFlicker
|
from src.theme_nerv_fx import CRTFilter, StatusFlicker, AlertPulsing
|
||||||
|
|
||||||
class TestThemeNervFx(unittest.TestCase):
|
class TestThemeNervFx(unittest.TestCase):
|
||||||
@patch("src.theme_nerv_fx.imgui")
|
@patch("src.theme_nerv_fx.imgui")
|
||||||
def test_scanline_overlay_render(self, mock_imgui):
|
def test_crt_filter_render(self, mock_imgui):
|
||||||
# Setup
|
# Setup
|
||||||
mock_draw_list = MagicMock()
|
mock_draw_list = MagicMock()
|
||||||
mock_imgui.get_foreground_draw_list.return_value = mock_draw_list
|
mock_imgui.get_foreground_draw_list.return_value = mock_draw_list
|
||||||
mock_imgui.get_color_u32.return_value = 0x12345678
|
mock_imgui.get_color_u32.return_value = 0x12345678
|
||||||
|
|
||||||
overlay = ScanlineOverlay()
|
overlay = CRTFilter()
|
||||||
width, height = 100.0, 10.0
|
width, height = 800.0, 600.0
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
overlay.render(width, height)
|
overlay.render(width, height)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
mock_imgui.get_foreground_draw_list.assert_called_once()
|
mock_imgui.get_foreground_draw_list.assert_called_once()
|
||||||
# height is 10, range(0, 10, 2) is [0, 2, 4, 6, 8] -> 5 calls
|
# height is 600, range(0, 600, 2) is 300 calls
|
||||||
self.assertEqual(mock_draw_list.add_line.call_count, 5)
|
self.assertEqual(mock_draw_list.add_line.call_count, 300)
|
||||||
|
# Vignette: v_steps = 15. height=600 is plenty for all insets.
|
||||||
|
self.assertEqual(mock_draw_list.add_rect.call_count, 15)
|
||||||
|
# Noise: 30 calls to add_rect_filled
|
||||||
|
self.assertEqual(mock_draw_list.add_rect_filled.call_count, 30)
|
||||||
|
|
||||||
# Verify some calls
|
# Verify some calls
|
||||||
mock_draw_list.add_line.assert_any_call((0.0, 0.0), (100.0, 0.0), 0x12345678, 1.0)
|
mock_draw_list.add_line.assert_any_call((0.0, 0.0), (800.0, 0.0), 0x12345678, 1.0)
|
||||||
mock_draw_list.add_line.assert_any_call((0.0, 8.0), (100.0, 8.0), 0x12345678, 1.0)
|
mock_draw_list.add_line.assert_any_call((0.0, 598.0), (800.0, 598.0), 0x12345678, 1.0)
|
||||||
|
|
||||||
|
@patch("src.theme_nerv_fx.imgui")
|
||||||
|
def test_crt_filter_disabled(self, mock_imgui):
|
||||||
|
mock_draw_list = MagicMock()
|
||||||
|
mock_imgui.get_foreground_draw_list.return_value = mock_draw_list
|
||||||
|
|
||||||
|
overlay = CRTFilter()
|
||||||
|
overlay.enabled = False
|
||||||
|
overlay.render(800.0, 600.0)
|
||||||
|
|
||||||
|
mock_imgui.get_foreground_draw_list.assert_not_called()
|
||||||
|
|
||||||
@patch("src.theme_nerv_fx.time")
|
@patch("src.theme_nerv_fx.time")
|
||||||
def test_status_flicker_get_alpha(self, mock_time):
|
def test_status_flicker_get_alpha(self, mock_time):
|
||||||
@@ -36,21 +51,42 @@ class TestThemeNervFx(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(flicker.get_alpha(), 0.85)
|
self.assertAlmostEqual(flicker.get_alpha(), 0.85)
|
||||||
|
|
||||||
# sin(pi/2) = 1 -> 1.0
|
# sin(pi/2) = 1 -> 1.0
|
||||||
# time * 20.0 = pi/2 -> time = pi/40
|
|
||||||
mock_time.time.return_value = math.pi / 40.0
|
mock_time.time.return_value = math.pi / 40.0
|
||||||
self.assertAlmostEqual(flicker.get_alpha(), 1.0)
|
self.assertAlmostEqual(flicker.get_alpha(), 1.0)
|
||||||
|
|
||||||
# sin(3pi/2) = -1 -> 0.7
|
# sin(3pi/2) = -1 -> 0.7
|
||||||
# time * 20.0 = 3pi/2 -> time = 3pi/40
|
|
||||||
mock_time.time.return_value = 3 * math.pi / 40.0
|
mock_time.time.return_value = 3 * math.pi / 40.0
|
||||||
self.assertAlmostEqual(flicker.get_alpha(), 0.7)
|
self.assertAlmostEqual(flicker.get_alpha(), 0.7)
|
||||||
|
|
||||||
|
def test_alert_pulsing_update(self):
|
||||||
|
pulse = AlertPulsing()
|
||||||
|
self.assertFalse(pulse.active)
|
||||||
|
|
||||||
# Verify range for many samples
|
pulse.update("error: something failed")
|
||||||
for i in range(100):
|
self.assertTrue(pulse.active)
|
||||||
mock_time.time.return_value = i * 0.1
|
|
||||||
alpha = flicker.get_alpha()
|
pulse.update("Error: alert")
|
||||||
self.assertGreaterEqual(alpha, 0.7)
|
self.assertTrue(pulse.active)
|
||||||
self.assertLessEqual(alpha, 1.0)
|
|
||||||
|
pulse.update("idle")
|
||||||
|
self.assertFalse(pulse.active)
|
||||||
|
|
||||||
|
@patch("src.theme_nerv_fx.imgui")
|
||||||
|
@patch("src.theme_nerv_fx.time")
|
||||||
|
def test_alert_pulsing_render(self, mock_time, mock_imgui):
|
||||||
|
mock_draw_list = MagicMock()
|
||||||
|
mock_imgui.get_foreground_draw_list.return_value = mock_draw_list
|
||||||
|
mock_imgui.get_color_u32.return_value = 0xFF0000FF
|
||||||
|
|
||||||
|
pulse = AlertPulsing()
|
||||||
|
pulse.active = True
|
||||||
|
|
||||||
|
# sin(0) = 0 -> alpha = 0.05 + 0.15 * (0+1)/2 = 0.125
|
||||||
|
mock_time.time.return_value = 0.0
|
||||||
|
pulse.render(800.0, 600.0)
|
||||||
|
|
||||||
|
mock_imgui.get_foreground_draw_list.assert_called()
|
||||||
|
mock_draw_list.add_rect.assert_called_with((0.0, 0.0), (800.0, 600.0), 0xFF0000FF, 0.0, 0, 10.0)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user