streamduck_core/thread/rendering/
mod.rs

1//! Rendering functions that represent default Streamduck renderer
2
3/// Types for creating custom renderers
4pub mod custom;
5/// Renderer's component values
6pub mod component_values;
7
8use std::hash::{Hash, Hasher};
9use image::{DynamicImage, Rgba, RgbaImage};
10use rusttype::Scale;
11use image::imageops::{FilterType, tile};
12use streamdeck::{DeviceImage, StreamDeck};
13use std::collections::HashMap;
14use std::sync::Arc;
15use std::collections::hash_map::DefaultHasher;
16use std::time::Instant;
17use std::ops::Deref;
18use serde::{Serialize, Deserialize};
19use serde_json::Value;
20use crate::core::button::Component;
21use crate::core::{CoreHandle, UniqueButton};
22use crate::font::get_font_from_collection;
23use crate::images::{AnimationFrame, convert_image, SDImage};
24use crate::modules::UniqueSDModule;
25use crate::thread::rendering::custom::DeviceReference;
26use crate::thread::util::{image_from_horiz_gradient, image_from_solid, image_from_vert_gradient, render_aligned_shadowed_text_on_image, render_aligned_text_on_image, TextAlignment};
27use crate::util::hash_value;
28
29/// Animation counter that counts frames for animated images
30pub struct AnimationCounter {
31    frames: Vec<(AnimationFrame, f32)>,
32    time: Instant,
33    wakeup_time: f32,
34    index: usize,
35    duration: f32,
36    new_frame: bool,
37}
38
39impl AnimationCounter {
40    fn new(frames: Vec<AnimationFrame>) -> AnimationCounter {
41        let mut time_counter = 0.0;
42        let frames: Vec<(AnimationFrame, f32)> = frames.into_iter()
43            .map(|x| {
44                let end_time = time_counter + x.delay;
45                time_counter = end_time;
46                (x, end_time)
47            })
48            .collect();
49
50        let duration = time_counter;
51
52        AnimationCounter {
53            frames,
54            time: Instant::now(),
55            wakeup_time: 0.0,
56            index: 0,
57            duration,
58            new_frame: false
59        }
60    }
61
62    fn get_frame(&self) -> &AnimationFrame {
63        &self.frames[self.index].0
64    }
65
66    fn advance_counter(&mut self) {
67        let time = self.time.elapsed().as_secs_f32();
68
69        if time > self.wakeup_time {
70            let looped_time = time % self.duration;
71            for i in 0..self.frames.len() {
72                if looped_time < self.frames[i].1 {
73                    self.index = i;
74                    self.new_frame = true;
75                    self.wakeup_time = time + self.frames[i].0.delay;
76                    break;
77                }
78            }
79        }
80    }
81}
82
83/// Rendering code that's being called every loop
84pub async fn process_frame(
85    core: &CoreHandle,
86    streamdeck: &mut StreamDeck,
87    cache: &mut HashMap<u64, (Arc<DeviceImage>, u64)>,
88    counters: &mut HashMap<String, AnimationCounter>,
89    renderer_map: &mut HashMap<u8, (RendererComponent, UniqueButton, Vec<UniqueSDModule>)>,
90    previous_state: &mut HashMap<u8, u64>,
91    missing: &DynamicImage,
92    time: u64
93) {
94
95    for key in 0..core.core.key_count {
96        if let Some((component, button, modules)) = renderer_map.get(&key) {
97            if !component.renderer.is_empty() {
98                // Custom renderer detected
99                let lock = core.core.render_manager.read_renderers().await;
100
101                if let Some(renderer) = lock.get(&component.renderer) {
102                    // Stopping any further process if custom renderer is found
103                    renderer.render(key, button, core, &mut DeviceReference::new(streamdeck, key)).await;
104                    previous_state.insert(key, 1);
105                    continue;
106                }
107            }
108
109
110            if let ButtonBackground::ExistingImage(identifier) = &component.background {
111                let counter = if let Some(counter) = counters.get_mut(identifier) {
112                    Some(counter)
113                } else {
114                    if let Some(SDImage::AnimatedImage(frames)) = core.core.image_collection.read().await.get(identifier).cloned() {
115                        let counter = AnimationCounter::new(frames);
116                        counters.insert(identifier.clone(), counter);
117                        Some(counters.get_mut(identifier).unwrap())
118                    } else {
119                        None
120                    }
121                };
122
123                if let Some(counter) = counter {
124                    let frame = counter.get_frame();
125
126                    let mut hasher: Box<dyn Hasher> = Box::new(DefaultHasher::new());
127
128                    component.hash(&mut hasher);
129                    frame.index.hash(&mut hasher);
130
131                    for module in modules {
132                        module.render_hash(core.clone_for(module), &button, &mut hasher);
133                    }
134
135                    let hash = hasher.finish();
136
137                    if counter.new_frame || (hash != *previous_state.get(&key).unwrap_or(&1)) {
138                        let variant = cache.get_mut(&hash);
139
140                        if component.to_cache && variant.is_some() {
141                            let (variant, time_to_die) = variant.unwrap();
142                            *time_to_die = time + 20000;
143
144                            let previous = previous_state.get(&key).unwrap_or(&1);
145                            if hash != *previous {
146                                streamdeck.write_button_image(key, variant.deref()).ok();
147                            }
148
149                        } else {
150                            let device_image = convert_image(&core.core.kind, draw_foreground(&component, &button, modules,frame.image.clone(), core).await);
151
152                            let arc = Arc::new(device_image);
153
154                            if component.to_cache {
155                                cache.insert(hash, (arc.clone(), time + 20000));
156                            }
157
158                            streamdeck.write_button_image(key, arc.deref()).ok();
159                        }
160
161                        previous_state.insert(key, hash);
162                    }
163
164                    // Skipping anything else if we already processed an animated image
165                    continue;
166                }
167            }
168
169            // If not animated, continuing with normal process of rendering a button
170            let mut hasher: Box<dyn Hasher> = Box::new(DefaultHasher::new());
171
172            component.hash(&mut hasher);
173            for module in modules {
174                module.render_hash(core.clone_for(module), &button, &mut hasher);
175            }
176
177            let hash = hasher.finish();
178
179            let variant = cache.get_mut(&hash);
180
181            if component.to_cache && variant.is_some() {
182                let (variant, time_to_die) = variant.unwrap();
183                *time_to_die = time + 20000;
184
185                let previous = previous_state.get(&key).unwrap_or(&1);
186                if hash != *previous {
187                    streamdeck.write_button_image(key, variant.deref()).ok();
188                }
189            } else {
190                let device_image = convert_image(&core.core.kind, draw_foreground(&component, &button, modules, draw_background(component, core, missing).await, core).await);
191
192                let arc = Arc::new(device_image);
193
194                if component.to_cache {
195                    cache.insert(hash, (arc.clone(), time + 20000));
196                }
197
198                streamdeck.write_button_image(key, arc.deref()).ok();
199            }
200
201            previous_state.insert(key, hash);
202        } else {
203            let previous = previous_state.get(&key).unwrap_or(&1);
204
205            if *previous != 0 {
206                previous_state.insert(key, 0);
207                streamdeck.set_button_rgb(key, &streamdeck::Colour {
208                    r: 0,
209                    g: 0,
210                    b: 0
211                }).ok();
212            }
213        }
214    }
215
216    for (_, counter) in counters {
217        counter.new_frame = false;
218        counter.advance_counter()
219    };
220}
221
222/// Draws background for static images
223pub async fn draw_background(renderer: &RendererComponent, core: &CoreHandle, missing: &DynamicImage) -> DynamicImage {
224    match &renderer.background {
225        ButtonBackground::Solid(color) => {
226            image_from_solid(core.core.image_size, Rgba([color.0, color.1, color.2, 255]))
227        }
228
229        ButtonBackground::HorizontalGradient(start, end) => {
230            image_from_horiz_gradient(core.core.image_size, Rgba([start.0, start.1, start.2, 255]), Rgba([end.0, end.1, end.2, 255]))
231        }
232
233        ButtonBackground::VerticalGradient(start, end) => {
234            image_from_vert_gradient(core.core.image_size, Rgba([start.0, start.1, start.2, 255]), Rgba([end.0, end.1, end.2, 255]))
235        }
236
237        ButtonBackground::ExistingImage(identifier) => {
238            if let Some(image) = core.core.image_collection.read().await.get(identifier) {
239                match image {
240                    SDImage::SingleImage(image) => {
241                        image.resize_to_fill(core.core.image_size.0 as u32, core.core.image_size.1 as u32, FilterType::Triangle)
242                    }
243
244                    SDImage::AnimatedImage(frames) => {
245                        frames[0].image.clone().resize_to_fill(core.core.image_size.0 as u32, core.core.image_size.1 as u32, FilterType::Triangle)
246                    }
247                }
248            } else {
249                missing.clone()
250            }
251        }
252
253        ButtonBackground::NewImage(blob) => {
254            if let Ok(image) = SDImage::from_base64(blob, core.core.image_size).await {
255                image.get_image()
256            } else {
257                missing.clone()
258            }
259        }
260    }
261}
262
263/// Draws foreground of a button (text, plugin layers)
264pub async fn draw_foreground(renderer: &RendererComponent, button: &UniqueButton, modules: &Vec<UniqueSDModule>, mut background: DynamicImage, core: &CoreHandle) -> DynamicImage {
265    // Render any additional things plugins want displayed
266    for module in modules {
267        module.render(core.clone_for(module), button, &mut background).await;
268    }
269
270
271    for button_text in &renderer.text {
272        let text = button_text.text.as_str();
273        let scale = Scale { x: button_text.scale.0, y: button_text.scale.1 };
274        let align = button_text.alignment.clone();
275        let padding = button_text.padding;
276        let offset = button_text.offset.clone();
277        let color = button_text.color.clone();
278
279        if let Some(font) = get_font_from_collection(&button_text.font) {
280            if let Some(shadow) = &button_text.shadow {
281                render_aligned_shadowed_text_on_image(
282                    core.core.image_size,
283                    &mut background,
284                    font.as_ref(),
285                    text,
286                    scale,
287                    align,
288                    padding,
289                    offset,
290                    color,
291                    shadow.offset.clone(),
292                    shadow.color.clone(),
293                )
294            } else {
295                render_aligned_text_on_image(
296                    core.core.image_size,
297                    &mut background,
298                    font.as_ref(),
299                    text,
300                    scale,
301                    align,
302                    padding,
303                    offset,
304                    color,
305                )
306            }
307        }
308    }
309
310    background
311}
312
313/// Draws missing texture from HL2
314pub fn draw_missing_texture(size: (usize, usize)) -> DynamicImage {
315    let mut pattern = RgbaImage::new(16, 16);
316
317    for x in 0..16 {
318        for y in 0..16 {
319            let color = if y < 8 {
320                if x < 8 {
321                    Rgba([255, 0, 255, 255])
322                } else {
323                    Rgba([0, 0, 0, 255])
324                }
325            } else {
326                if x >= 8 {
327                    Rgba([255, 0, 255, 255])
328                } else {
329                    Rgba([0, 0, 0, 255])
330                }
331            };
332
333            pattern.put_pixel(x, y, color);
334        }
335    }
336
337    let (iw, ih) = size;
338    let mut frame = RgbaImage::new(iw as u32, ih as u32);
339
340    tile(&mut frame, &pattern);
341
342    let mut missing = DynamicImage::ImageRgba8(frame);
343
344    if let Some(font) = get_font_from_collection("default") {
345        render_aligned_shadowed_text_on_image(
346            (iw, ih),
347            &mut missing,
348            &font,
349            "ГДЕ",
350            Scale { x: 30.0, y: 30.0 },
351            TextAlignment::Center,
352            0,
353            (0.0, -13.0),
354            (255, 0, 255, 255),
355            (2, 2),
356            (0, 0, 0, 255),
357        );
358
359        render_aligned_shadowed_text_on_image(
360            (iw, ih),
361            &mut missing,
362            &font,
363            "Where",
364            Scale { x: 25.0, y: 25.0 },
365            TextAlignment::Center,
366            0,
367            (0.0, 8.0),
368            (255, 0, 255, 255),
369            (1, 1),
370            (0, 0, 0, 255),
371        );
372    }
373
374    missing
375}
376
377/// Draws texture that says "Custom Renderer"
378pub fn draw_custom_renderer_texture(size: (usize, usize)) -> DynamicImage {
379    let font = get_font_from_collection("default").unwrap();
380    let mut frame = image_from_solid(size, Rgba([55, 55, 55, 255]));
381
382    render_aligned_text_on_image(size, &mut frame, font.deref(), "Custom", Scale::uniform(16.0), TextAlignment::Center, 0, (0.0, -8.0), (255, 255, 255, 255));
383    render_aligned_text_on_image(size, &mut frame, font.deref(), "Renderer", Scale::uniform(16.0), TextAlignment::Center, 0, (0.0, 8.0), (255, 255, 255, 255));
384
385    frame
386}
387
388/// Definition for color format
389pub type Color = (u8, u8, u8, u8);
390
391/// Button Background definition for button renderer
392#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
393pub enum ButtonBackground {
394    /// Solid color background
395    Solid(Color),
396    /// Horizontal color gradient
397    HorizontalGradient(Color, Color),
398    /// Vertical color gradient
399    VerticalGradient(Color, Color),
400    /// Existing image that was already loaded into the image collection
401    ExistingImage(String),
402    /// New image as a base64 blob
403    NewImage(String),
404}
405
406impl Default for ButtonBackground {
407    fn default() -> Self {
408        Self::Solid((0, 0, 0, 0))
409    }
410}
411
412/// Button Text definition for button renderer
413#[derive(Serialize, Deserialize, Debug, Clone)]
414pub struct ButtonText {
415    /// Contents of the text
416    pub text: String,
417    /// Font that should be used
418    pub font: String,
419    /// Scale of the text
420    pub scale: (f32, f32),
421    /// Alignment of the text
422    pub alignment: TextAlignment,
423    /// Padding in pixels from the alignment
424    pub padding: u32,
425    /// Offset of the text from the alignment point
426    pub offset: (f32, f32),
427    /// Color of the text
428    pub color: Color,
429    /// Text shadow
430    pub shadow: Option<ButtonTextShadow>,
431}
432
433impl Hash for ButtonText {
434    fn hash<H: Hasher>(&self, state: &mut H) {
435        self.text.hash(state);
436        self.font.hash(state);
437        ((self.scale.0 * 100.0) as i32).hash(state);
438        ((self.scale.1 * 100.0) as i32).hash(state);
439        self.alignment.hash(state);
440        self.padding.hash(state);
441        ((self.offset.0 * 100.0) as i32).hash(state);
442        ((self.offset.1 * 100.0) as i32).hash(state);
443        self.color.hash(state);
444        self.shadow.hash(state);
445    }
446}
447
448/// Button text shadow
449#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
450pub struct ButtonTextShadow {
451    /// Shadow offset in pixels
452    pub offset: (i32, i32),
453    /// Color of the shadow
454    pub color: Color,
455}
456
457/// Renderer component that contains button background and array of text structs
458#[derive(Serialize, Deserialize, Clone, Debug)]
459pub struct RendererComponent {
460    /// Uses default renderer if empty
461    #[serde(default)]
462    pub renderer: String,
463    /// Background that should be used
464    #[serde(default)]
465    pub background: ButtonBackground,
466    /// Text objects
467    #[serde(default)]
468    pub text: Vec<ButtonText>,
469    /// Plugins that shouldn't be rendered on the button
470    #[serde(default)]
471    pub plugin_blacklist: Vec<String>,
472    /// If caching should be used
473    #[serde(default = "make_true")]
474    pub to_cache: bool,
475    /// Anything that custom renderers might want to remember
476    #[serde(default)]
477    pub custom_data: Value,
478}
479
480fn make_true() -> bool { true }
481
482impl Default for RendererComponent {
483    fn default() -> Self {
484        Self {
485            renderer: "".to_string(),
486            background: ButtonBackground::Solid((255, 255, 255, 255)),
487            text: vec![],
488            plugin_blacklist: vec![],
489            to_cache: true,
490            custom_data: Default::default()
491        }
492    }
493}
494
495impl Hash for RendererComponent {
496    fn hash<H: Hasher>(&self, state: &mut H) {
497        self.renderer.hash(state);
498        self.plugin_blacklist.hash(state);
499        self.text.hash(state);
500        self.to_cache.hash(state);
501        self.background.hash(state);
502        hash_value(&self.custom_data, state);
503    }
504}
505
506impl Component for RendererComponent {
507    const NAME: &'static str = "renderer";
508}
509
510/// Builder for renderer component
511#[derive(Default)]
512pub struct RendererComponentBuilder {
513    component: RendererComponent
514}
515
516impl RendererComponentBuilder {
517    /// Creates new builder
518    pub fn new() -> Self {
519        Self::default()
520    }
521
522    /// Sets custom renderer
523    pub fn renderer(mut self, renderer: &str) -> Self {
524        self.component.renderer = renderer.to_string(); self
525    }
526
527    /// Sets background
528    pub fn background(mut self, background: ButtonBackground) -> Self {
529        self.component.background = background; self
530    }
531
532    /// Adds a text object
533    pub fn add_text(mut self, text: ButtonText) -> Self {
534        self.component.text.push(text); self
535    }
536
537    /// Adds a plugin to rendering blacklist for the component
538    pub fn add_to_blacklist(mut self, plugin: &str) -> Self {
539        self.component.plugin_blacklist.push(plugin.to_string()); self
540    }
541
542    /// Sets caching state
543    pub fn caching(mut self, cache: bool) -> Self {
544        self.component.to_cache = cache; self
545    }
546
547    /// Builds the component
548    pub fn build(self) -> RendererComponent {
549        self.into()
550    }
551}
552
553impl From<RendererComponentBuilder> for RendererComponent {
554    fn from(builder: RendererComponentBuilder) -> Self {
555        builder.component
556    }
557}
558
559/// Renderer settings
560#[derive(Serialize, Deserialize, Default)]
561pub struct RendererSettings {
562    /// Blacklist of plugins that aren't allowed to render
563    pub plugin_blacklist: Vec<String>
564}
565
566#[allow(dead_code)]
567pub(crate) fn hash_renderer(renderer: &RendererComponent) -> u64 {
568    let mut hasher = DefaultHasher::new();
569    renderer.hash(&mut hasher);
570    hasher.finish()
571}