Skip to main content

ferroui_base/rendering/composition/animations/
composition_easing_function.rs

1//! The easing functions of WinUI composition (`CompositionEasingFunction`
2//! and its subclasses, Windows App SDK 1.4+), as [`IEasing`] implementations
3//! that key frames take.
4//!
5//! The parametric curves (back, bounce, circle, elastic, exponential, power,
6//! sine) are those of the easing functions of WPF and XAML
7//! (`EasingFunctionBase`): each is defined by its ease-in curve `f(t)`, and
8//! the mode makes the curve
9//!
10//! * `In`: `f(t)`;
11//! * `Out`: `1 - f(1 - t)`;
12//! * `InOut`: `f(2t) / 2` for `t < 0.5`, `(1 - f(2 - 2t)) / 2 + 0.5` after.
13//!
14//! The parameters of these functions are read-only, as in WinUI; the types
15//! are plain values that are `Send + Sync`, so the render thread evaluates
16//! the same object. The step function has settable members, as in WinUI, and
17//! shares a copy of them.
18
19use crate::animation::easings::{Easing, IEasing, SharedEasing};
20use crate::animation::KeySplineSolver;
21use crate::numerics::Vector2;
22use crate::rendering::composition::Compositor;
23use std::cell::Cell;
24use std::f64::consts::PI;
25use std::rc::Rc;
26use std::sync::{Arc, Mutex};
27
28/// The mode of a parametric easing function (WinUI
29/// `CompositionEasingFunctionMode`).
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
31pub enum CompositionEasingFunctionMode {
32    /// The curve as defined (ease in).
33    #[default]
34    In,
35    /// The curve mirrored in time and value (ease out).
36    Out,
37    /// Ease in for the first half, ease out for the second half.
38    InOut,
39}
40
41/// Applies `mode` to the ease-in curve `ease_in` (`EasingFunctionBase.Ease`).
42fn ease_with_mode(mode: CompositionEasingFunctionMode, t: f64, ease_in: impl Fn(f64) -> f64) -> f64 {
43    match mode {
44        CompositionEasingFunctionMode::In => ease_in(t),
45        CompositionEasingFunctionMode::Out => 1.0 - ease_in(1.0 - t),
46        CompositionEasingFunctionMode::InOut => {
47            if t < 0.5 {
48                ease_in(t * 2.0) * 0.5
49            } else {
50                (1.0 - ease_in((1.0 - t) * 2.0)) * 0.5 + 0.5
51            }
52        }
53    }
54}
55
56/// Implements [`IEasing`] for an easing that is a plain `Send + Sync`
57/// value, and the conversions into [`Easing`].
58macro_rules! value_easing {
59    ($ty:ty) => {
60        impl IEasing for $ty {
61            fn to_shared(&self) -> Arc<SharedEasing> {
62                Arc::new(*self)
63            }
64
65            fn ease(&self, progress: f64) -> f64 {
66                self.ease_core(progress)
67            }
68        }
69
70        easing_conversions!($ty);
71    };
72}
73
74/// The conversions of an easing function into the handle key frames take.
75macro_rules! easing_conversions {
76    ($ty:ty) => {
77        impl From<$ty> for Easing {
78            fn from(value: $ty) -> Easing {
79                Easing::new(value)
80            }
81        }
82
83        impl From<Rc<$ty>> for Easing {
84            fn from(value: Rc<$ty>) -> Easing {
85                Easing::from_rc(value)
86            }
87        }
88    };
89}
90
91/// A cubic Bezier curve from `(0, 0)` to `(1, 1)` with two control points
92/// (WinUI `CubicBezierEasingFunction`); the curve of
93/// [`SplineEasing`](crate::animation::easings::SplineEasing).
94pub struct CubicBezierEasingFunction {
95    control_point1: Vector2,
96    control_point2: Vector2,
97    solver: Mutex<KeySplineSolver>,
98}
99
100impl CubicBezierEasingFunction {
101    /// Creates the curve. Panics when the X coordinate of a control point is
102    /// outside `[0, 1]` (WinUI throws).
103    pub fn new(control_point1: Vector2, control_point2: Vector2) -> CubicBezierEasingFunction {
104        for x in [control_point1.x, control_point2.x] {
105            if !(0.0..=1.0).contains(&x) {
106                panic!("The X coordinate of a control point must be between 0 and 1");
107            }
108        }
109        CubicBezierEasingFunction {
110            control_point1,
111            control_point2,
112            solver: Mutex::new(KeySplineSolver::new(
113                control_point1.x as f64,
114                control_point1.y as f64,
115                control_point2.x as f64,
116                control_point2.y as f64,
117            )),
118        }
119    }
120
121    /// The first control point.
122    pub fn control_point1(&self) -> Vector2 {
123        self.control_point1
124    }
125
126    /// The second control point.
127    pub fn control_point2(&self) -> Vector2 {
128        self.control_point2
129    }
130}
131
132impl IEasing for CubicBezierEasingFunction {
133    fn to_shared(&self) -> Arc<SharedEasing> {
134        Arc::new(CubicBezierEasingFunction::new(self.control_point1, self.control_point2))
135    }
136
137    fn ease(&self, progress: f64) -> f64 {
138        self.solver.lock().unwrap_or_else(|e| e.into_inner()).get_spline_progress(progress)
139    }
140}
141
142easing_conversions!(CubicBezierEasingFunction);
143
144/// The identity curve (WinUI `LinearEasingFunction`).
145#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
146pub struct LinearEasingFunction;
147
148impl LinearEasingFunction {
149    pub fn new() -> LinearEasingFunction {
150        LinearEasingFunction
151    }
152
153    fn ease_core(&self, progress: f64) -> f64 {
154        progress
155    }
156}
157
158value_easing!(LinearEasingFunction);
159
160/// The members of a step function, as the render thread evaluates them.
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162struct StepParameters {
163    step_count: i32,
164    initial_step: i32,
165    final_step: i32,
166    is_initial_step_single_frame: bool,
167    is_final_step_single_frame: bool,
168}
169
170impl StepParameters {
171    /// The output is `step / StepCount`, the step running from
172    /// `InitialStep` to `FinalStep`. The steps share the duration equally;
173    /// a step that is "single frame" is shown only at the very start
174    /// (`t = 0`, the initial step) or at the very end (`t = 1`, the final
175    /// step) and gets no share.
176    // Deviation (winui-composition.md): an easing function does not know the
177    // frames of the animation, so a single-frame step lasts no time rather
178    // than one frame; the division of the duration between the steps is
179    // this port's reading of the WinUI members.
180    fn ease_core(&self, t: f64) -> f64 {
181        let count = self.step_count.max(1);
182        let initial = self.initial_step.clamp(0, count);
183        let final_step = self.final_step.clamp(initial, count);
184        let step = if t <= 0.0 {
185            initial
186        } else if t >= 1.0 {
187            final_step
188        } else {
189            let first = initial + i32::from(self.is_initial_step_single_frame);
190            let last = final_step - i32::from(self.is_final_step_single_frame);
191            if last < first {
192                first.min(final_step)
193            } else {
194                let steps = (last - first + 1) as f64;
195                (first + (t * steps).floor() as i32).min(last)
196            }
197        };
198        step as f64 / count as f64
199    }
200}
201
202impl IEasing for StepParameters {
203    fn to_shared(&self) -> Arc<SharedEasing> {
204        Arc::new(*self)
205    }
206
207    fn ease(&self, progress: f64) -> f64 {
208        self.ease_core(progress)
209    }
210}
211
212/// A function that advances in steps (WinUI `StepEasingFunction`). The
213/// output is `step / step_count` for a step between
214/// [`initial_step`](Self::initial_step) and [`final_step`](Self::final_step).
215pub struct StepEasingFunction {
216    step_count: Cell<i32>,
217    initial_step: Cell<i32>,
218    /// `None`: the step count.
219    final_step: Cell<Option<i32>>,
220    is_initial_step_single_frame: Cell<bool>,
221    is_final_step_single_frame: Cell<bool>,
222}
223
224impl StepEasingFunction {
225    /// A function with one step.
226    pub fn new() -> StepEasingFunction {
227        StepEasingFunction::with_step_count(1)
228    }
229
230    /// A function with `step_count` steps.
231    pub fn with_step_count(step_count: i32) -> StepEasingFunction {
232        StepEasingFunction {
233            step_count: Cell::new(step_count),
234            initial_step: Cell::new(0),
235            final_step: Cell::new(None),
236            is_initial_step_single_frame: Cell::new(false),
237            is_final_step_single_frame: Cell::new(false),
238        }
239    }
240
241    /// The number of steps between the starting value and the end value.
242    pub fn step_count(&self) -> i32 {
243        self.step_count.get()
244    }
245
246    pub fn set_step_count(&self, value: i32) {
247        self.step_count.set(value)
248    }
249
250    /// The step to start at (0 by default).
251    pub fn initial_step(&self) -> i32 {
252        self.initial_step.get()
253    }
254
255    pub fn set_initial_step(&self, value: i32) {
256        self.initial_step.set(value)
257    }
258
259    /// The step to end at: the step count until it is set.
260    pub fn final_step(&self) -> i32 {
261        self.final_step.get().unwrap_or(self.step_count.get())
262    }
263
264    pub fn set_final_step(&self, value: i32) {
265        self.final_step.set(Some(value))
266    }
267
268    /// Whether the initial step lasts the smallest possible duration.
269    pub fn is_initial_step_single_frame(&self) -> bool {
270        self.is_initial_step_single_frame.get()
271    }
272
273    pub fn set_is_initial_step_single_frame(&self, value: bool) {
274        self.is_initial_step_single_frame.set(value)
275    }
276
277    /// Whether the final step lasts the smallest possible duration.
278    pub fn is_final_step_single_frame(&self) -> bool {
279        self.is_final_step_single_frame.get()
280    }
281
282    pub fn set_is_final_step_single_frame(&self, value: bool) {
283        self.is_final_step_single_frame.set(value)
284    }
285
286    fn parameters(&self) -> StepParameters {
287        StepParameters {
288            step_count: self.step_count(),
289            initial_step: self.initial_step(),
290            final_step: self.final_step(),
291            is_initial_step_single_frame: self.is_initial_step_single_frame(),
292            is_final_step_single_frame: self.is_final_step_single_frame(),
293        }
294    }
295}
296
297impl Default for StepEasingFunction {
298    fn default() -> Self {
299        StepEasingFunction::new()
300    }
301}
302
303impl IEasing for StepEasingFunction {
304    /// A copy with the members the function has now.
305    fn to_shared(&self) -> Arc<SharedEasing> {
306        Arc::new(self.parameters())
307    }
308
309    fn ease(&self, progress: f64) -> f64 {
310        self.parameters().ease_core(progress)
311    }
312}
313
314easing_conversions!(StepEasingFunction);
315
316/// A curve that retracts before it moves (WinUI `BackEasingFunction`):
317/// `f(t) = t^3 - t * a * sin(pi t)`, `a` the amplitude (at least 0).
318#[derive(Clone, Copy, Debug, PartialEq)]
319pub struct BackEasingFunction {
320    mode: CompositionEasingFunctionMode,
321    amplitude: f32,
322}
323
324impl BackEasingFunction {
325    pub fn new(mode: CompositionEasingFunctionMode, amplitude: f32) -> BackEasingFunction {
326        BackEasingFunction { mode, amplitude }
327    }
328
329    /// The mode of the curve.
330    pub fn mode(&self) -> CompositionEasingFunctionMode {
331        self.mode
332    }
333
334    /// How far the curve retracts.
335    pub fn amplitude(&self) -> f32 {
336        self.amplitude
337    }
338
339    fn ease_core(&self, t: f64) -> f64 {
340        let amplitude = (self.amplitude as f64).max(0.0);
341        ease_with_mode(self.mode, t, |t| t * t * t - t * amplitude * (PI * t).sin())
342    }
343}
344
345value_easing!(BackEasingFunction);
346
347/// A bouncing curve (WinUI `BounceEasingFunction`): `bounces` bounces, each
348/// `bounciness` times smaller than the one before.
349#[derive(Clone, Copy, Debug, PartialEq)]
350pub struct BounceEasingFunction {
351    mode: CompositionEasingFunctionMode,
352    bounces: i32,
353    bounciness: f32,
354}
355
356impl BounceEasingFunction {
357    pub fn new(mode: CompositionEasingFunctionMode, bounces: i32, bounciness: f32) -> BounceEasingFunction {
358        BounceEasingFunction { mode, bounces, bounciness }
359    }
360
361    /// The mode of the curve.
362    pub fn mode(&self) -> CompositionEasingFunctionMode {
363        self.mode
364    }
365
366    /// The number of bounces.
367    pub fn bounces(&self) -> i32 {
368        self.bounces
369    }
370
371    /// The bounciness of the curve: a larger value makes the bounces shrink
372    /// faster.
373    pub fn bounciness(&self) -> f32 {
374        self.bounciness
375    }
376
377    /// `BounceEase.EaseInCore`.
378    fn ease_in(t: f64, bounces: i32, bounciness: f32) -> f64 {
379        let bounces = (bounces as f64).max(0.0);
380        let mut bounciness = bounciness as f64;
381        // Just over one avoids a division by zero.
382        if bounciness < 1.0 || (bounciness - 1.0).abs() < 1e-9 {
383            bounciness = 1.001;
384        }
385        let pow = bounciness.powf(bounces);
386        let one_minus_bounciness = 1.0 - bounciness;
387        // The bounces grow geometrically in time: the total in units of the
388        // first bounce, with only half of the last one.
389        let sum_of_units = (1.0 - pow) / one_minus_bounciness + pow * 0.5;
390        let unit_at_t = t * sum_of_units;
391        // The bounce `t` is in.
392        let bounce_at_t = (-unit_at_t * (1.0 - bounciness) + 1.0).log(bounciness);
393        let start = bounce_at_t.floor();
394        let end = start + 1.0;
395        let start_time = (1.0 - bounciness.powf(start)) / (one_minus_bounciness * sum_of_units);
396        let end_time = (1.0 - bounciness.powf(end)) / (one_minus_bounciness * sum_of_units);
397        // A parabola through (start, 0) and (end, 0) that peaks at the
398        // amplitude of the bounce.
399        let mid_time = (start_time + end_time) * 0.5;
400        let time_relative_to_peak = t - mid_time;
401        let radius = mid_time - start_time;
402        let amplitude = (1.0 / bounciness).powf(bounces - start);
403        (-amplitude / (radius * radius)) * (time_relative_to_peak - radius) * (time_relative_to_peak + radius)
404    }
405
406    fn ease_core(&self, t: f64) -> f64 {
407        ease_with_mode(self.mode, t, |t| Self::ease_in(t, self.bounces, self.bounciness))
408    }
409}
410
411value_easing!(BounceEasingFunction);
412
413/// A circular curve (WinUI `CircleEasingFunction`): `f(t) = 1 - sqrt(1 - t^2)`.
414#[derive(Clone, Copy, Debug, PartialEq, Eq)]
415pub struct CircleEasingFunction {
416    mode: CompositionEasingFunctionMode,
417}
418
419impl CircleEasingFunction {
420    pub fn new(mode: CompositionEasingFunctionMode) -> CircleEasingFunction {
421        CircleEasingFunction { mode }
422    }
423
424    /// The mode of the curve.
425    pub fn mode(&self) -> CompositionEasingFunctionMode {
426        self.mode
427    }
428
429    fn ease_core(&self, t: f64) -> f64 {
430        ease_with_mode(self.mode, t, |t| {
431            let t = t.clamp(0.0, 1.0);
432            1.0 - (1.0 - t * t).sqrt()
433        })
434    }
435}
436
437value_easing!(CircleEasingFunction);
438
439/// An oscillating curve (WinUI `ElasticEasingFunction`): `oscillations`
440/// oscillations whose amplitude grows exponentially with `springiness`.
441#[derive(Clone, Copy, Debug, PartialEq)]
442pub struct ElasticEasingFunction {
443    mode: CompositionEasingFunctionMode,
444    oscillations: i32,
445    springiness: f32,
446}
447
448impl ElasticEasingFunction {
449    pub fn new(mode: CompositionEasingFunctionMode, oscillations: i32, springiness: f32) -> ElasticEasingFunction {
450        ElasticEasingFunction { mode, oscillations, springiness }
451    }
452
453    /// The mode of the curve.
454    pub fn mode(&self) -> CompositionEasingFunctionMode {
455        self.mode
456    }
457
458    /// The number of oscillations.
459    pub fn oscillations(&self) -> i32 {
460        self.oscillations
461    }
462
463    /// The stiffness of the spring: a larger value damps the oscillations
464    /// faster.
465    pub fn springiness(&self) -> f32 {
466        self.springiness
467    }
468
469    fn ease_core(&self, t: f64) -> f64 {
470        let oscillations = (self.oscillations as f64).max(0.0);
471        let springiness = (self.springiness as f64).max(0.0);
472        ease_with_mode(self.mode, t, |t| {
473            let expo = if springiness == 0.0 { t } else { ((springiness * t).exp() - 1.0) / (springiness.exp() - 1.0) };
474            expo * ((PI * 2.0 * oscillations + PI * 0.5) * t).sin()
475        })
476    }
477}
478
479value_easing!(ElasticEasingFunction);
480
481/// An exponential curve (WinUI `ExponentialEasingFunction`):
482/// `f(t) = (e^(k t) - 1) / (e^k - 1)`, `k` the exponent (linear for 0).
483#[derive(Clone, Copy, Debug, PartialEq)]
484pub struct ExponentialEasingFunction {
485    mode: CompositionEasingFunctionMode,
486    exponent: f32,
487}
488
489impl ExponentialEasingFunction {
490    pub fn new(mode: CompositionEasingFunctionMode, exponent: f32) -> ExponentialEasingFunction {
491        ExponentialEasingFunction { mode, exponent }
492    }
493
494    /// The mode of the curve.
495    pub fn mode(&self) -> CompositionEasingFunctionMode {
496        self.mode
497    }
498
499    /// The exponent of the curve.
500    pub fn exponent(&self) -> f32 {
501        self.exponent
502    }
503
504    fn ease_core(&self, t: f64) -> f64 {
505        let factor = self.exponent as f64;
506        ease_with_mode(self.mode, t, |t| if factor == 0.0 { t } else { ((factor * t).exp() - 1.0) / (factor.exp() - 1.0) })
507    }
508}
509
510value_easing!(ExponentialEasingFunction);
511
512/// A power curve (WinUI `PowerEasingFunction`): `f(t) = t^p`, `p` the power
513/// (at least 0).
514#[derive(Clone, Copy, Debug, PartialEq)]
515pub struct PowerEasingFunction {
516    mode: CompositionEasingFunctionMode,
517    power: f32,
518}
519
520impl PowerEasingFunction {
521    pub fn new(mode: CompositionEasingFunctionMode, power: f32) -> PowerEasingFunction {
522        PowerEasingFunction { mode, power }
523    }
524
525    /// The mode of the curve.
526    pub fn mode(&self) -> CompositionEasingFunctionMode {
527        self.mode
528    }
529
530    /// The power of the curve.
531    pub fn power(&self) -> f32 {
532        self.power
533    }
534
535    fn ease_core(&self, t: f64) -> f64 {
536        let power = (self.power as f64).max(0.0);
537        ease_with_mode(self.mode, t, |t| t.powf(power))
538    }
539}
540
541value_easing!(PowerEasingFunction);
542
543/// A sine curve (WinUI `SineEasingFunction`): `f(t) = 1 - sin(pi/2 (1 - t))`.
544#[derive(Clone, Copy, Debug, PartialEq, Eq)]
545pub struct SineEasingFunction {
546    mode: CompositionEasingFunctionMode,
547}
548
549impl SineEasingFunction {
550    pub fn new(mode: CompositionEasingFunctionMode) -> SineEasingFunction {
551        SineEasingFunction { mode }
552    }
553
554    /// The mode of the curve.
555    pub fn mode(&self) -> CompositionEasingFunctionMode {
556        self.mode
557    }
558
559    fn ease_core(&self, t: f64) -> f64 {
560        ease_with_mode(self.mode, t, |t| 1.0 - (PI * 0.5 * (1.0 - t)).sin())
561    }
562}
563
564value_easing!(SineEasingFunction);
565
566/// The factories of the easing functions (the statics of WinUI
567/// `CompositionEasingFunction`, Windows App SDK 1.4+). Each returns the
568/// [`Easing`] handle key frames take; create the typed function
569/// (`BackEasingFunction::new`) to read its members.
570// Deviation (winui-composition.md): the easing functions are easings of
571// FerroUI, not composition objects; the owner compositor is not needed.
572pub struct CompositionEasingFunction;
573
574impl CompositionEasingFunction {
575    /// A cubic Bezier curve with the given control points.
576    pub fn create_cubic_bezier_easing_function(
577        _owner: &Compositor,
578        control_point1: Vector2,
579        control_point2: Vector2,
580    ) -> Easing {
581        CubicBezierEasingFunction::new(control_point1, control_point2).into()
582    }
583
584    /// The linear curve.
585    pub fn create_linear_easing_function(_owner: &Compositor) -> Easing {
586        LinearEasingFunction::new().into()
587    }
588
589    /// A step function with one step.
590    pub fn create_step_easing_function(_owner: &Compositor) -> Easing {
591        StepEasingFunction::new().into()
592    }
593
594    /// A step function with `step_count` steps.
595    pub fn create_step_easing_function_with_step_count(_owner: &Compositor, step_count: i32) -> Easing {
596        StepEasingFunction::with_step_count(step_count).into()
597    }
598
599    /// A back curve.
600    pub fn create_back_easing_function(
601        _owner: &Compositor,
602        mode: CompositionEasingFunctionMode,
603        amplitude: f32,
604    ) -> Easing {
605        BackEasingFunction::new(mode, amplitude).into()
606    }
607
608    /// A bounce curve.
609    pub fn create_bounce_easing_function(
610        _owner: &Compositor,
611        mode: CompositionEasingFunctionMode,
612        bounces: i32,
613        bounciness: f32,
614    ) -> Easing {
615        BounceEasingFunction::new(mode, bounces, bounciness).into()
616    }
617
618    /// A circle curve.
619    pub fn create_circle_easing_function(_owner: &Compositor, mode: CompositionEasingFunctionMode) -> Easing {
620        CircleEasingFunction::new(mode).into()
621    }
622
623    /// An elastic curve.
624    pub fn create_elastic_easing_function(
625        _owner: &Compositor,
626        mode: CompositionEasingFunctionMode,
627        oscillations: i32,
628        springiness: f32,
629    ) -> Easing {
630        ElasticEasingFunction::new(mode, oscillations, springiness).into()
631    }
632
633    /// An exponential curve.
634    pub fn create_exponential_easing_function(
635        _owner: &Compositor,
636        mode: CompositionEasingFunctionMode,
637        exponent: f32,
638    ) -> Easing {
639        ExponentialEasingFunction::new(mode, exponent).into()
640    }
641
642    /// A power curve.
643    pub fn create_power_easing_function(_owner: &Compositor, mode: CompositionEasingFunctionMode, power: f32) -> Easing {
644        PowerEasingFunction::new(mode, power).into()
645    }
646
647    /// A sine curve.
648    pub fn create_sine_easing_function(_owner: &Compositor, mode: CompositionEasingFunctionMode) -> Easing {
649        SineEasingFunction::new(mode).into()
650    }
651}
652
653/// The easing factories of WinUI `Compositor`. They return the typed
654/// function (`Rc`), which key frames take as it is
655/// (`insert_key_frame_with_easing`) or as an [`Easing`] (`.into()`).
656impl Compositor {
657    /// A cubic Bezier curve with the given control points (WinUI
658    /// `CreateCubicBezierEasingFunction`).
659    pub fn create_cubic_bezier_easing_function(
660        &self,
661        control_point1: Vector2,
662        control_point2: Vector2,
663    ) -> Rc<CubicBezierEasingFunction> {
664        Rc::new(CubicBezierEasingFunction::new(control_point1, control_point2))
665    }
666
667    /// The linear curve (WinUI `CreateLinearEasingFunction`).
668    pub fn create_linear_easing_function(&self) -> Rc<LinearEasingFunction> {
669        Rc::new(LinearEasingFunction::new())
670    }
671
672    /// A step function with one step (WinUI `CreateStepEasingFunction()`).
673    pub fn create_step_easing_function(&self) -> Rc<StepEasingFunction> {
674        Rc::new(StepEasingFunction::new())
675    }
676
677    /// A step function with `step_count` steps (WinUI
678    /// `CreateStepEasingFunction(stepCount)`).
679    pub fn create_step_easing_function_with_step_count(&self, step_count: i32) -> Rc<StepEasingFunction> {
680        Rc::new(StepEasingFunction::with_step_count(step_count))
681    }
682}