⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/
animation.rs

1//! Animation Framework
2//!
3//! Provides transition timers and easing functions for smooth window
4//! animations (opacity changes, position moves, resize transitions).
5//! All math is integer-only (fixed-point 8.8) to avoid floating-point
6//! operations in the kernel.
7
8#![allow(dead_code)]
9
10use alloc::vec::Vec;
11
12// ---------------------------------------------------------------------------
13// Easing functions
14// ---------------------------------------------------------------------------
15
16/// Easing function type
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum EasingFunction {
19    Linear,
20    EaseInQuad,
21    EaseOutQuad,
22    EaseInOutQuad,
23    EaseInCubic,
24    EaseOutCubic,
25    EaseInOutCubic,
26    /// Slight overshoot at the end
27    EaseOutBack,
28    /// Bounce at the end
29    EaseOutBounce,
30}
31
32/// Evaluate an easing function with fixed-point arithmetic (no floating point).
33///
34/// `t_256` is the progress from 0 to 256 (representing 0.0 to 1.0).
35/// Returns a value in approximately 0..256 (may slightly exceed 256 for
36/// overshoot easings like `EaseOutBack`).
37pub fn evaluate_easing_fixed(easing: EasingFunction, t_256: u32) -> u32 {
38    // Clamp input
39    let t = t_256.min(256);
40
41    match easing {
42        EasingFunction::Linear => t,
43
44        EasingFunction::EaseInQuad => {
45            // t^2
46            (t * t) >> 8
47        }
48
49        EasingFunction::EaseOutQuad => {
50            // 1 - (1-t)^2
51            let inv = 256 - t;
52            256 - ((inv * inv) >> 8)
53        }
54
55        EasingFunction::EaseInOutQuad => {
56            // Piecewise: 2t^2 for first half, 1-(-2t+2)^2/2 for second half
57            if t < 128 {
58                // 2 * (t/256)^2 * 256 = 2*t^2/256
59                (2 * t * t) >> 8
60            } else {
61                let inv = 256 - t;
62                256 - ((2 * inv * inv) >> 8)
63            }
64        }
65
66        EasingFunction::EaseInCubic => {
67            // t^3 in fixed point: (t * t * t) >> 16
68            let t2 = (t * t) >> 8;
69            (t2 * t) >> 8
70        }
71
72        EasingFunction::EaseOutCubic => {
73            // 1 - (1-t)^3
74            let inv = 256 - t;
75            let inv2 = (inv * inv) >> 8;
76            let inv3 = (inv2 * inv) >> 8;
77            256 - inv3
78        }
79
80        EasingFunction::EaseInOutCubic => {
81            if t < 128 {
82                // 4 * t^3 in fixed point
83                let t2 = (t * t) >> 8;
84                let t3 = (t2 * t) >> 8;
85                (4 * t3) >> 8
86            } else {
87                let inv = 256 - t;
88                let inv2 = (inv * inv) >> 8;
89                let inv3 = (inv2 * inv) >> 8;
90                256 - ((4 * inv3) >> 8)
91            }
92        }
93
94        EasingFunction::EaseOutBack => {
95            // Overshoot: goes slightly past 256 then settles.
96            // Approximation: EaseOutQuad + 10% overshoot bump.
97            let inv = 256 - t;
98            let base = 256 - ((inv * inv) >> 8);
99            // Add a small overshoot that peaks at t~192 (3/4)
100            // overshoot = sin-like bump approximated as parabola
101            let mid_dist = if t > 128 { 256 - t } else { t };
102            let bump = (mid_dist * 20) >> 8; // ~8% overshoot
103                                             // Only apply bump in second half
104            if t > 128 {
105                base + bump
106            } else {
107                base
108            }
109        }
110
111        EasingFunction::EaseOutBounce => {
112            // Simplified bounce using 3 segments.
113            // Each segment is a parabola opening downward.
114            if t < 192 {
115                // Main arc: covers 0..75% of time, reaches ~100% height
116                // Parabola: 4*(t/192)*(1 - t/192) scaled to 256
117                let t_seg = (t * 256) / 192;
118                let inv = 256 - t_seg;
119                let val = (4 * t_seg * inv) >> 8;
120                // Scale to reach 256 at peak
121                (val * 256) >> 8
122            } else if t < 240 {
123                // First bounce: smaller arc
124                let seg_start = 192;
125                let seg_len = 48;
126                let t_seg = ((t - seg_start) * 256) / seg_len;
127                let inv = 256 - t_seg;
128                let bounce_h = 32; // bounce height ~12.5%
129                let base = 256 - bounce_h;
130                let val = (4 * t_seg * inv) >> 8;
131                base + ((val * bounce_h) >> 8)
132            } else {
133                // Final settle: tiny bounce
134                let seg_start = 240;
135                let seg_len = 16;
136                let t_local = t - seg_start;
137                let t_seg = if seg_len > 0 {
138                    (t_local * 256) / seg_len
139                } else {
140                    256
141                };
142                let inv = 256 - t_seg;
143                let bounce_h = 8; // tiny bounce
144                let base = 256 - bounce_h;
145                let val = (4 * t_seg * inv) >> 8;
146                base + ((val * bounce_h) >> 8)
147            }
148        }
149    }
150}
151
152// ---------------------------------------------------------------------------
153// Animation properties
154// ---------------------------------------------------------------------------
155
156/// Animation target property
157#[derive(Debug, Clone, Copy)]
158pub enum AnimationProperty {
159    Opacity {
160        from: u8,
161        to: u8,
162    },
163    PositionX {
164        from: i32,
165        to: i32,
166    },
167    PositionY {
168        from: i32,
169        to: i32,
170    },
171    Width {
172        from: u32,
173        to: u32,
174    },
175    Height {
176        from: u32,
177        to: u32,
178    },
179    /// Fixed-point 8.8 scale factor (256 = 1.0x)
180    Scale {
181        from_256: u32,
182        to_256: u32,
183    },
184}
185
186// ---------------------------------------------------------------------------
187// Animation
188// ---------------------------------------------------------------------------
189
190/// Active animation instance.
191pub struct Animation {
192    pub id: u32,
193    pub window_id: u32,
194    pub property: AnimationProperty,
195    pub easing: EasingFunction,
196    /// Total duration in milliseconds
197    pub duration_ms: u32,
198    /// Elapsed time in milliseconds
199    pub elapsed_ms: u32,
200    /// Whether this animation has finished
201    pub completed: bool,
202}
203
204impl Animation {
205    /// Compute the current interpolated value of this animation.
206    ///
207    /// Returns the value as an i64 (covers both signed and unsigned
208    /// properties).
209    pub fn current_value(&self) -> i64 {
210        if self.completed || self.duration_ms == 0 {
211            return self.target_value();
212        }
213
214        // Progress 0..256
215        let t_256 = ((self.elapsed_ms as u64 * 256) / self.duration_ms as u64).min(256) as u32;
216        let eased = evaluate_easing_fixed(self.easing, t_256);
217
218        // Interpolate: from + (to - from) * eased / 256
219        match self.property {
220            AnimationProperty::Opacity { from, to } => {
221                let from_i = from as i64;
222                let to_i = to as i64;
223                from_i + ((to_i - from_i) * eased as i64) / 256
224            }
225            AnimationProperty::PositionX { from, to }
226            | AnimationProperty::PositionY { from, to } => {
227                let from_i = from as i64;
228                let to_i = to as i64;
229                from_i + ((to_i - from_i) * eased as i64) / 256
230            }
231            AnimationProperty::Width { from, to } | AnimationProperty::Height { from, to } => {
232                let from_i = from as i64;
233                let to_i = to as i64;
234                from_i + ((to_i - from_i) * eased as i64) / 256
235            }
236            AnimationProperty::Scale { from_256, to_256 } => {
237                let from_i = from_256 as i64;
238                let to_i = to_256 as i64;
239                from_i + ((to_i - from_i) * eased as i64) / 256
240            }
241        }
242    }
243
244    /// Get the final target value.
245    fn target_value(&self) -> i64 {
246        match self.property {
247            AnimationProperty::Opacity { to, .. } => to as i64,
248            AnimationProperty::PositionX { to, .. } | AnimationProperty::PositionY { to, .. } => {
249                to as i64
250            }
251            AnimationProperty::Width { to, .. } | AnimationProperty::Height { to, .. } => to as i64,
252            AnimationProperty::Scale { to_256, .. } => to_256 as i64,
253        }
254    }
255}
256
257// ---------------------------------------------------------------------------
258// AnimationManager
259// ---------------------------------------------------------------------------
260
261/// Manages all active animations and advances them each frame.
262pub struct AnimationManager {
263    animations: Vec<Animation>,
264    next_id: u32,
265}
266
267impl AnimationManager {
268    /// Create a new animation manager.
269    pub fn new() -> Self {
270        Self {
271            animations: Vec::new(),
272            next_id: 1,
273        }
274    }
275
276    /// Start a new animation. Returns the animation ID.
277    pub fn start(
278        &mut self,
279        window_id: u32,
280        property: AnimationProperty,
281        easing: EasingFunction,
282        duration_ms: u32,
283    ) -> u32 {
284        let id = self.next_id;
285        self.next_id += 1;
286
287        self.animations.push(Animation {
288            id,
289            window_id,
290            property,
291            easing,
292            duration_ms,
293            elapsed_ms: 0,
294            completed: false,
295        });
296
297        id
298    }
299
300    /// Advance all animations by `delta_ms` milliseconds.
301    pub fn tick(&mut self, delta_ms: u32) {
302        for anim in &mut self.animations {
303            if anim.completed {
304                continue;
305            }
306            anim.elapsed_ms += delta_ms;
307            if anim.elapsed_ms >= anim.duration_ms {
308                anim.elapsed_ms = anim.duration_ms;
309                anim.completed = true;
310            }
311        }
312    }
313
314    /// Get the current interpolated value for an animation by ID.
315    pub fn get_current_value(&self, animation_id: u32) -> Option<i64> {
316        self.animations
317            .iter()
318            .find(|a| a.id == animation_id)
319            .map(|a| a.current_value())
320    }
321
322    /// Check whether an animation has completed.
323    pub fn is_complete(&self, animation_id: u32) -> bool {
324        self.animations
325            .iter()
326            .find(|a| a.id == animation_id)
327            .is_none_or(|a| a.completed)
328    }
329
330    /// Remove all completed animations.
331    pub fn remove_completed(&mut self) {
332        self.animations.retain(|a| !a.completed);
333    }
334
335    /// Cancel all animations for a specific window.
336    pub fn cancel(&mut self, window_id: u32) {
337        self.animations.retain(|a| a.window_id != window_id);
338    }
339
340    /// Returns `true` if there are any active (non-completed) animations.
341    pub fn has_active_animations(&self) -> bool {
342        self.animations.iter().any(|a| !a.completed)
343    }
344
345    /// Get the number of active animations.
346    pub fn active_count(&self) -> usize {
347        self.animations.iter().filter(|a| !a.completed).count()
348    }
349
350    /// Get all animations for a specific window.
351    pub fn get_window_animations(&self, window_id: u32) -> Vec<&Animation> {
352        self.animations
353            .iter()
354            .filter(|a| a.window_id == window_id && !a.completed)
355            .collect()
356    }
357}
358
359impl Default for AnimationManager {
360    fn default() -> Self {
361        Self::new()
362    }
363}
364
365// ---------------------------------------------------------------------------
366// Compositing effects helper
367// ---------------------------------------------------------------------------
368
369/// Render a soft box shadow behind a window.
370///
371/// The shadow is drawn into `shadow_buffer`, which should be pre-allocated
372/// with `(width + 2 * radius) * (height + 2 * radius)` u32 elements.
373/// The shadow is centered, so the window content starts at offset
374/// `(radius, radius)` within the shadow buffer.
375///
376/// Uses a 3-pass box blur approximating a Gaussian shadow.
377///
378/// `buf_width` is the stride of `shadow_buffer` in pixels.
379pub fn render_shadow(
380    shadow_buffer: &mut [u32],
381    buf_width: u32,
382    width: u32,
383    height: u32,
384    radius: u32,
385    opacity: u8,
386) {
387    let bw = buf_width as usize;
388    let total_w = width + 2 * radius;
389    let total_h = height + 2 * radius;
390    let r = radius as usize;
391    let w = width as usize;
392    let h = height as usize;
393
394    // Clear to transparent
395    for px in shadow_buffer.iter_mut().take(bw * total_h as usize) {
396        *px = 0;
397    }
398
399    // Seed: fill the window footprint area with full opacity
400    for y in 0..h {
401        for x in 0..w {
402            let px = r + x;
403            let py = r + y;
404            let idx = py * bw + px;
405            if idx < shadow_buffer.len() {
406                shadow_buffer[idx] = opacity as u32;
407            }
408        }
409    }
410
411    // 3-pass box blur (horizontal + vertical + horizontal) approximating Gaussian
412    if radius == 0 {
413        // Convert alpha values to shadow pixels
414        convert_alpha_to_shadow(shadow_buffer, bw, total_w as usize, total_h as usize);
415        return;
416    }
417
418    // Temporary buffer for blur passes (stores alpha channel only as u32)
419    let buf_len = bw * total_h as usize;
420    let mut temp = alloc::vec![0u32; buf_len];
421
422    // Pass 1: horizontal blur
423    box_blur_h(
424        shadow_buffer,
425        &mut temp,
426        bw,
427        total_w as usize,
428        total_h as usize,
429        r,
430    );
431
432    // Pass 2: vertical blur
433    box_blur_v(
434        &temp,
435        shadow_buffer,
436        bw,
437        total_w as usize,
438        total_h as usize,
439        r,
440    );
441
442    // Pass 3: horizontal blur again
443    let shadow_copy: Vec<u32> = shadow_buffer[..buf_len].to_vec();
444    box_blur_h(
445        &shadow_copy,
446        shadow_buffer,
447        bw,
448        total_w as usize,
449        total_h as usize,
450        r,
451    );
452
453    // Convert alpha values to ARGB shadow color
454    convert_alpha_to_shadow(shadow_buffer, bw, total_w as usize, total_h as usize);
455}
456
457/// Horizontal box blur pass.
458fn box_blur_h(src: &[u32], dst: &mut [u32], bw: usize, w: usize, h: usize, radius: usize) {
459    let diam = 2 * radius + 1;
460    for y in 0..h {
461        let row_off = y * bw;
462        let mut sum: u32 = 0;
463
464        // Initialize sum for first pixel (include left padding)
465        for x in 0..=radius.min(w.saturating_sub(1)) {
466            sum += src.get(row_off + x).copied().unwrap_or(0);
467        }
468        // Mirror left edge
469        for _ in 0..radius.saturating_sub(0) {
470            sum += src.get(row_off).copied().unwrap_or(0);
471        }
472
473        for x in 0..w {
474            let idx = row_off + x;
475            if idx < dst.len() {
476                dst[idx] = sum / diam as u32;
477            }
478
479            // Slide window: add right pixel, remove left pixel
480            let add_x = x + radius + 1;
481            let rem_x = x.wrapping_sub(radius);
482
483            let add_val = if add_x < w {
484                src.get(row_off + add_x).copied().unwrap_or(0)
485            } else {
486                // Clamp to edge
487                src.get(row_off + w.saturating_sub(1)).copied().unwrap_or(0)
488            };
489
490            let rem_val = if rem_x < w {
491                src.get(row_off + rem_x).copied().unwrap_or(0)
492            } else {
493                src.get(row_off).copied().unwrap_or(0)
494            };
495
496            sum = sum.wrapping_add(add_val).wrapping_sub(rem_val);
497        }
498    }
499}
500
501/// Vertical box blur pass.
502fn box_blur_v(src: &[u32], dst: &mut [u32], bw: usize, w: usize, h: usize, radius: usize) {
503    let diam = 2 * radius + 1;
504    for x in 0..w {
505        let mut sum: u32 = 0;
506
507        // Initialize sum
508        for y in 0..=radius.min(h.saturating_sub(1)) {
509            sum += src.get(y * bw + x).copied().unwrap_or(0);
510        }
511        for _ in 0..radius {
512            sum += src.get(x).copied().unwrap_or(0);
513        }
514
515        for y in 0..h {
516            let idx = y * bw + x;
517            if idx < dst.len() {
518                dst[idx] = sum / diam as u32;
519            }
520
521            let add_y = y + radius + 1;
522            let rem_y = y.wrapping_sub(radius);
523
524            let add_val = if add_y < h {
525                src.get(add_y * bw + x).copied().unwrap_or(0)
526            } else {
527                src.get(h.saturating_sub(1) * bw + x).copied().unwrap_or(0)
528            };
529
530            let rem_val = if rem_y < h {
531                src.get(rem_y * bw + x).copied().unwrap_or(0)
532            } else {
533                src.get(x).copied().unwrap_or(0)
534            };
535
536            sum = sum.wrapping_add(add_val).wrapping_sub(rem_val);
537        }
538    }
539}
540
541/// Convert alpha-only values in the buffer to ARGB shadow pixels.
542/// Shadow color is black (0x00000000) with the alpha from the blur.
543fn convert_alpha_to_shadow(buffer: &mut [u32], bw: usize, w: usize, h: usize) {
544    for y in 0..h {
545        for x in 0..w {
546            let idx = y * bw + x;
547            if idx < buffer.len() {
548                let alpha = buffer[idx].min(255);
549                buffer[idx] = alpha << 24; // Black shadow with alpha
550            }
551        }
552    }
553}