Skip to content

Commit c7ca56b

Browse files
Merge remote-tracking branch 'remotes/jonas/pixel-shifting'
2 parents e780177 + 8e2fac6 commit c7ca56b

6 files changed

Lines changed: 163 additions & 19 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ nix = { version = "0.27", features = ["poll"] }
2222
privdrop = "0.5.3"
2323
serde = { version = "1", features = ["derive"] }
2424
toml = "0.8"
25+
rand = "0.8"

share/tiny-dfr/config.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ MediaLayerDefault = false
99
# Set this to false if you want to hide the button outline,
1010
# leaving only the text/logo
1111
ShowButtonOutlines = true
12+
13+
# Set this to true to slowly shift the entire screen contents.
14+
# In theory this helps with screen longevity, but macos does not bother doint it
15+
# Disabling ShowButtonOutlines will make this effect less noticeable to the eye
16+
EnablePixelShift = false

src/backlight.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ use input::event::{
1010
};
1111
use crate::TIMEOUT_MS;
1212

13+
const BRIGHTNESS_DIM_TIMEOUT: i32 = TIMEOUT_MS * 3; // should be a multiple of TIMEOUT_MS
14+
const BRIGHTNESS_OFF_TIMEOUT: i32 = TIMEOUT_MS * 6; // should be a multiple of TIMEOUT_MS
15+
const DEFAULT_BRIGHTNESS: u32 = 128;
16+
const DIMMED_BRIGHTNESS: u32 = 1;
17+
1318
fn read_attr(path: &Path, attr: &str) -> u32 {
1419
fs::read_to_string(path.join(attr))
1520
.expect(&format!("Failed to read {attr}"))
@@ -74,10 +79,10 @@ impl BacklightManager {
7479
let since_last_active = (Instant::now() - self.last_active).as_millis() as u64;
7580
let new_bl = if self.lid_state == SwitchState::On {
7681
0
77-
} else if since_last_active < TIMEOUT_MS as u64 {
78-
128
79-
} else if since_last_active < TIMEOUT_MS as u64 * 2 {
80-
1
82+
} else if since_last_active < BRIGHTNESS_DIM_TIMEOUT as u64 {
83+
DEFAULT_BRIGHTNESS
84+
} else if since_last_active < BRIGHTNESS_OFF_TIMEOUT as u64 {
85+
DIMMED_BRIGHTNESS
8186
} else {
8287
0
8388
};

src/main.rs

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{
66
},
77
path::Path,
88
collections::HashMap,
9+
cmp::min
910
};
1011
use std::os::fd::AsFd;
1112
use cairo::{
@@ -32,26 +33,31 @@ use serde::Deserialize;
3233

3334
mod backlight;
3435
mod display;
36+
mod pixel_shift;
3537

3638
use backlight::BacklightManager;
3739
use display::DrmBackend;
40+
use pixel_shift::PixelShiftManager;
41+
use pixel_shift::PIXEL_SHIFT_WIDTH_PX;
3842

3943
const DFR_WIDTH: i32 = 2008;
4044
const DFR_HEIGHT: i32 = 64;
4145
const BUTTON_COLOR_INACTIVE: f64 = 0.200;
4246
const BUTTON_COLOR_ACTIVE: f64 = 0.400;
43-
const TIMEOUT_MS: i32 = 30 * 1000;
47+
const TIMEOUT_MS: i32 = 10 * 1000;
4448

4549
#[derive(Deserialize)]
4650
#[serde(rename_all = "PascalCase")]
4751
struct ConfigProxy {
4852
media_layer_default: Option<bool>,
49-
show_button_outlines: Option<bool>
53+
show_button_outlines: Option<bool>,
54+
enable_pixel_shift: Option<bool>
5055
}
5156

5257
struct Config {
5358
media_layer_default: bool,
54-
show_button_outlines: bool
59+
show_button_outlines: bool,
60+
enable_pixel_shift: bool
5561
}
5662

5763
enum ButtonImage {
@@ -76,12 +82,12 @@ impl Button {
7682
action, image: ButtonImage::Svg(svg)
7783
}
7884
}
79-
fn render(&self, c: &Context, left_edge: f64, button_width: f64) {
85+
fn render(&self, c: &Context, button_left_edge: f64, button_width: f64, y_shift: f64) {
8086
match &self.image {
8187
ButtonImage::Text(text) => {
8288
let extents = c.text_extents(text).unwrap();
8389
c.move_to(
84-
left_edge + button_width / 2.0 - extents.width() / 2.0,
90+
button_left_edge + button_width / 2.0 - extents.width() / 2.0,
8591
DFR_HEIGHT as f64 / 2.0 + extents.height() / 2.0
8692
);
8793
c.show_text(text).unwrap();
@@ -90,9 +96,10 @@ impl Button {
9096
let renderer = CairoRenderer::new(&svg);
9197
let y = 0.12 * DFR_HEIGHT as f64;
9298
let size = DFR_HEIGHT as f64 - y * 2.0;
93-
let x = left_edge + button_width / 2.0 - size / 2.0;
99+
let x = button_left_edge + button_width / 2.0 - size / 2.0;
100+
94101
renderer.render_document(c,
95-
&Rectangle::new(x, y, size, size)
102+
&Rectangle::new(x, y + y_shift, size, size)
96103
).unwrap();
97104
}
98105
}
@@ -104,21 +111,23 @@ struct FunctionLayer {
104111
}
105112

106113
impl FunctionLayer {
107-
fn draw(&self, config: &Config, surface: &Surface, active_buttons: &[bool]) {
114+
fn draw(&self, config: &Config, surface: &Surface, active_buttons: &[bool], pixel_shift: (f64, f64)) {
108115
let c = Context::new(&surface).unwrap();
109116
c.translate(DFR_HEIGHT as f64, 0.0);
110117
c.rotate((90.0f64).to_radians());
111-
let button_width = DFR_WIDTH as f64 / (self.buttons.len() + 1) as f64;
112-
let spacing_width = (DFR_WIDTH as f64 - self.buttons.len() as f64 * button_width) / (self.buttons.len() - 1) as f64;
118+
let button_width = (DFR_WIDTH as u64 - PIXEL_SHIFT_WIDTH_PX) as f64 / (self.buttons.len() + 1) as f64;
119+
let spacing_width = ((DFR_WIDTH as u64 - PIXEL_SHIFT_WIDTH_PX) as f64 - self.buttons.len() as f64 * button_width) / (self.buttons.len() - 1) as f64;
113120
let radius = 8.0f64;
114121
let bot = (DFR_HEIGHT as f64) * 0.2;
115122
let top = (DFR_HEIGHT as f64) * 0.85;
123+
let (pixel_shift_x, pixel_shift_y) = pixel_shift;
124+
116125
c.set_source_rgb(0.0, 0.0, 0.0);
117126
c.paint().unwrap();
118127
c.select_font_face("sans-serif", FontSlant::Normal, FontWeight::Bold);
119128
c.set_font_size(32.0);
120129
for (i, button) in self.buttons.iter().enumerate() {
121-
let left_edge = i as f64 * (button_width + spacing_width);
130+
let left_edge = i as f64 * (button_width + spacing_width) + pixel_shift_x + (PIXEL_SHIFT_WIDTH_PX / 2) as f64;
122131
let color = if active_buttons[i] {
123132
BUTTON_COLOR_ACTIVE
124133
} else if config.show_button_outlines {
@@ -163,7 +172,7 @@ impl FunctionLayer {
163172

164173
c.fill().unwrap();
165174
c.set_source_rgb(1.0, 1.0, 1.0);
166-
button.render(&c, left_edge, button_width);
175+
button.render(&c, left_edge, button_width, pixel_shift_y);
167176
}
168177
}
169178
}
@@ -222,17 +231,20 @@ fn load_config() -> Config {
222231
if let Ok(user) = user {
223232
base.media_layer_default = user.media_layer_default.or(base.media_layer_default);
224233
base.show_button_outlines = user.show_button_outlines.or(base.show_button_outlines);
234+
base.enable_pixel_shift = user.enable_pixel_shift.or(base.enable_pixel_shift);
225235
};
226236
Config {
227237
media_layer_default: base.media_layer_default.unwrap(),
228-
show_button_outlines: base.show_button_outlines.unwrap()
238+
show_button_outlines: base.show_button_outlines.unwrap(),
239+
enable_pixel_shift: base.enable_pixel_shift.unwrap(),
229240
}
230241
}
231242

232243
fn main() {
233244
let mut uinput = UInputHandle::new(OpenOptions::new().write(true).open("/dev/uinput").unwrap());
234245
let mut backlight = BacklightManager::new();
235246
let config = load_config();
247+
let mut pixel_shift = PixelShiftManager::new();
236248

237249
// drop privileges to input and video group
238250
let groups = ["input", "video"];
@@ -315,14 +327,31 @@ fn main() {
315327
let mut digitizer: Option<InputDevice> = None;
316328
let mut touches = HashMap::new();
317329
loop {
330+
let mut next_timeout_ms = TIMEOUT_MS;
331+
332+
if config.enable_pixel_shift {
333+
let (pixel_shift_needs_redraw, pixel_shift_next_timeout_ms) = pixel_shift.update();
334+
if pixel_shift_needs_redraw {
335+
needs_redraw = true;
336+
}
337+
next_timeout_ms = min(next_timeout_ms, pixel_shift_next_timeout_ms);
338+
}
339+
340+
318341
if needs_redraw {
319342
needs_redraw = false;
320-
layers[active_layer].draw(&config, &surface, &button_states[active_layer]);
343+
let shift = if config.enable_pixel_shift {
344+
pixel_shift.get()
345+
} else {
346+
(0.0, 0.0)
347+
};
348+
layers[active_layer].draw(&config, &surface, &button_states[active_layer], shift);
321349
let data = surface.data().unwrap();
322350
drm.map().unwrap().as_mut()[..data.len()].copy_from_slice(&data);
323351
drm.dirty(&[ClipRect::new(0, 0, DFR_HEIGHT as u16, DFR_WIDTH as u16)]).unwrap();
324352
}
325-
poll(&mut [pollfd_tb, pollfd_main], TIMEOUT_MS).unwrap();
353+
354+
poll(&mut [pollfd_tb, pollfd_main], next_timeout_ms).unwrap();
326355
input_tb.dispatch().unwrap();
327356
input_main.dispatch().unwrap();
328357
for event in &mut input_tb.clone().chain(input_main.clone()) {

src/pixel_shift.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use rand::Rng;
2+
use std::{
3+
time::Instant,
4+
};
5+
use crate::TIMEOUT_MS;
6+
7+
const INTERVAL_MS: i32 = TIMEOUT_MS * 1; // should be a multiple of TIMEOUT_MS
8+
const PROLONGED_INTERVAL_MS: i32 = TIMEOUT_MS * 1; // should be a multiple of TIMEOUT_MS and more than INTERVAL_MS
9+
const ANIMATION_INTERVAL_MS: i32 = 200; // should be less than TIMEOUT_MS
10+
const ANIMATION_DURATION_MS: i32 = 4000; // should be a multiple of ANIMATION_INTERVAL_MS
11+
12+
// This is the total range on the x-axis that pixels will shift by over time, ie. they will shift by
13+
// PIXEL_SHIFT_WIDTH_PX / 2 to the right and to the left.
14+
// To make sure that no pixel ends up being always on, the minimum value to be safe here is the
15+
// size of the largest continuous colored line in the x-direction. The higher this value, the less
16+
// strain is put on the panel.
17+
pub const PIXEL_SHIFT_WIDTH_PX: u64 = 22; // should be divisible by 2
18+
// in y direction we can't really shift by a lot since icons still need to appear centered,
19+
// 2 pixels in each direction seems to be the maximum before it gets really visible.
20+
const PIXEL_SHIFT_HEIGHT_PX: u64 = 4; // should be divisible by 2
21+
22+
#[derive(Clone, Copy)]
23+
enum ShiftState {
24+
WaitingAtEnd,
25+
ShiftingSubpixel,
26+
Normal
27+
}
28+
29+
pub struct PixelShiftManager {
30+
last_active: Instant,
31+
y_constant: f64,
32+
pixel_progress: u64,
33+
subpixel_progress: f64,
34+
direction: i64,
35+
state: ShiftState
36+
}
37+
38+
fn wait_for_state(state: ShiftState) -> i32 {
39+
match state {
40+
ShiftState::ShiftingSubpixel => ANIMATION_INTERVAL_MS,
41+
ShiftState::Normal => INTERVAL_MS,
42+
ShiftState::WaitingAtEnd => PROLONGED_INTERVAL_MS,
43+
}
44+
}
45+
46+
impl PixelShiftManager {
47+
pub fn new() -> PixelShiftManager {
48+
let pixel_progress = rand::thread_rng().gen_range(0..PIXEL_SHIFT_WIDTH_PX);
49+
50+
// add some randomness to the relationship between shifting on the x and y axis
51+
// so that pixel shifting doesn't follow the same 2d pattern every time
52+
let y_constant: f64 = rand::thread_rng().gen_range(0..PIXEL_SHIFT_HEIGHT_PX * 2) as f64;
53+
54+
PixelShiftManager {
55+
last_active: Instant::now(),
56+
y_constant,
57+
state: ShiftState::Normal,
58+
pixel_progress,
59+
subpixel_progress: 0.0,
60+
direction: 1,
61+
}
62+
}
63+
64+
pub fn update(&mut self) -> (bool, i32) {
65+
let time_now = Instant::now();
66+
let since_last_pixel_shift = (time_now - self.last_active).as_millis() as i32;
67+
68+
if since_last_pixel_shift < wait_for_state(self.state) {
69+
return (false, i32::MAX);
70+
}
71+
self.last_active = time_now;
72+
73+
match self.state {
74+
ShiftState::Normal => {
75+
self.state = ShiftState::ShiftingSubpixel;
76+
self.subpixel_progress = self.direction as f64;
77+
},
78+
ShiftState::ShiftingSubpixel => {
79+
let shift_by = ANIMATION_INTERVAL_MS as f64 / ANIMATION_DURATION_MS as f64;
80+
self.subpixel_progress += shift_by * self.direction as f64;
81+
if self.subpixel_progress <= 0.01 || self.subpixel_progress >= 0.99 {
82+
self.pixel_progress = (self.direction + self.pixel_progress as i64) as u64;
83+
self.state = ShiftState::Normal;
84+
self.subpixel_progress = 0.0;
85+
}
86+
if self.pixel_progress == 0 || self.pixel_progress >= PIXEL_SHIFT_WIDTH_PX {
87+
self.state = ShiftState::WaitingAtEnd;
88+
self.direction = -self.direction;
89+
}
90+
},
91+
ShiftState::WaitingAtEnd => {
92+
self.state = ShiftState::Normal;
93+
}
94+
}
95+
(true, wait_for_state(self.state))
96+
}
97+
98+
pub fn get(&self) -> (f64, f64) {
99+
let x_progress = self.pixel_progress as f64 + self.subpixel_progress;
100+
let y_progress = (x_progress + self.y_constant) % PIXEL_SHIFT_HEIGHT_PX as f64;
101+
(x_progress - (PIXEL_SHIFT_WIDTH_PX / 2) as f64, y_progress - (PIXEL_SHIFT_HEIGHT_PX / 2) as f64)
102+
}
103+
}

0 commit comments

Comments
 (0)