| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| anyhow::Error, |
| argh::FromArgs, |
| carnelian::{ |
| color::Color, make_app_assistant, render::*, App, AppAssistant, Point, RenderOptions, Size, |
| ViewAssistant, ViewAssistantContext, ViewAssistantPtr, ViewKey, |
| }, |
| chrono::{Local, Timelike}, |
| euclid::{Angle, Point2D, Rect, Size2D, Transform2D, Vector2D}, |
| fuchsia_trace::{self, duration}, |
| fuchsia_trace_provider, |
| fuchsia_zircon::{AsHandleRef, Event, Signals}, |
| std::{collections::BTreeMap, f32}, |
| }; |
| |
| const BACKGROUND_COLOR: Color = Color { r: 235, g: 213, b: 179, a: 255 }; |
| |
| /// Clockface. |
| #[derive(Debug, FromArgs)] |
| #[argh(name = "clockface_rs")] |
| struct Args { |
| /// use spinel (GPU rendering back-end) |
| #[argh(switch, short = 's')] |
| use_spinel: bool, |
| } |
| |
| #[derive(Default)] |
| struct ClockfaceAppAssistant { |
| use_spinel: bool, |
| } |
| |
| impl AppAssistant for ClockfaceAppAssistant { |
| fn setup(&mut self) -> Result<(), Error> { |
| let args: Args = argh::from_env(); |
| self.use_spinel = args.use_spinel; |
| Ok(()) |
| } |
| |
| fn create_view_assistant(&mut self, _: ViewKey) -> Result<ViewAssistantPtr, Error> { |
| Ok(Box::new(ClockfaceViewAssistant::new())) |
| } |
| |
| fn get_render_options(&self) -> RenderOptions { |
| RenderOptions { use_spinel: self.use_spinel, ..RenderOptions::default() } |
| } |
| } |
| |
| struct RoundedLine { |
| path: Path, |
| } |
| |
| impl RoundedLine { |
| fn new(mut path_builder: PathBuilder, pos: Point, length: f32, thickness: f32) -> Self { |
| let radius = thickness / 2.0; |
| let tl = pos.to_vector(); |
| let tr = pos.to_vector() + Vector2D::new(length, 0.0); |
| let br = pos.to_vector() + Vector2D::new(length, thickness); |
| let bl = pos.to_vector() + Vector2D::new(0.0, thickness); |
| let radiush = Vector2D::new(radius, 0.0); |
| let radiusv = Vector2D::new(0.0, radius); |
| |
| let path = { |
| macro_rules! c { |
| ( $v:expr ) => { |
| Point::new($v.x, $v.y) |
| }; |
| } |
| |
| path_builder.move_to(c!(tl + radiush)); |
| path_builder.line_to(c!(tr - radiush)); |
| path_builder.rat_quad_to(c!(tr), c!(tr + radiusv), 0.7071); |
| path_builder.rat_quad_to(c!(br), c!(br - radiush), 0.7071); |
| path_builder.line_to(c!(bl + radiush)); |
| path_builder.rat_quad_to(c!(bl), c!(bl - radiusv), 0.7071); |
| path_builder.rat_quad_to(c!(tl), c!(tl + radiush), 0.7071); |
| |
| path_builder.build() |
| }; |
| |
| Self { path } |
| } |
| } |
| |
| struct Hand { |
| line: RoundedLine, |
| raster: Option<Raster>, |
| color: Color, |
| } |
| |
| impl Hand { |
| fn new( |
| path_builder: PathBuilder, |
| thickness: f32, |
| length: f32, |
| offset: f32, |
| color: Color, |
| ) -> Self { |
| let line = RoundedLine::new( |
| path_builder, |
| Point::new(-(thickness / 2.0 + offset), -thickness / 2.0), |
| length, |
| thickness, |
| ); |
| |
| Self { line, raster: None, color } |
| } |
| |
| fn update(&mut self, context: &mut Context, scale: f32, angle: f32) { |
| let rotation = Transform2D::create_rotation(Angle::radians(angle)).post_scale(scale, scale); |
| let mut raster_builder = context.raster_builder().unwrap(); |
| raster_builder.add(&self.line.path, Some(&rotation)); |
| self.raster.replace(raster_builder.build()); |
| } |
| } |
| |
| struct Scene { |
| size: Size, |
| hour_hand: Hand, |
| minute_hand: Hand, |
| second_hand: Hand, |
| hour_index: usize, |
| minute_index: usize, |
| second_index: usize, |
| } |
| |
| impl Scene { |
| fn new(context: &mut Context) -> Self { |
| const HOUR_HAND_COLOR: Color = Color { r: 254, g: 72, b: 100, a: 255 }; |
| const MINUTE_HAND_COLOR: Color = Color { r: 255, g: 114, b: 132, a: 127 }; |
| const SECOND_HAND_COLOR: Color = Color::white(); |
| const RADIUS: f32 = 0.4; |
| |
| let thickness = RADIUS / 20.0; |
| let offset = RADIUS / 5.0; |
| let hour_hand = Hand::new( |
| context.path_builder().unwrap(), |
| thickness * 2.0, |
| RADIUS, |
| offset, |
| HOUR_HAND_COLOR, |
| ); |
| let minute_hand = |
| Hand::new(context.path_builder().unwrap(), thickness, RADIUS, 0.0, MINUTE_HAND_COLOR); |
| let second_hand = Hand::new( |
| context.path_builder().unwrap(), |
| thickness / 2.0, |
| RADIUS + offset, |
| offset, |
| SECOND_HAND_COLOR, |
| ); |
| |
| Self { |
| size: Size::new(1.0, 1.0), |
| hour_hand, |
| minute_hand, |
| second_hand, |
| hour_index: std::usize::MAX, |
| minute_index: std::usize::MAX, |
| second_index: std::usize::MAX, |
| } |
| } |
| |
| fn update(&mut self, context: &mut Context, size: &Size, scale: f32) { |
| if self.size != *size { |
| self.size = *size; |
| self.hour_index = std::usize::MAX; |
| self.minute_index = std::usize::MAX; |
| self.second_index = std::usize::MAX; |
| } |
| const MICROSECONDS_PER_SECOND: f32 = 1e+6; |
| let now = Local::now(); |
| let (_is_pm, hour12) = now.hour12(); |
| let us = now.nanosecond() as f32 / 1000.0; |
| let second = now.second() as f32 + us / MICROSECONDS_PER_SECOND; |
| let minute = now.minute() as f32 + second / 60.0; |
| let hour = hour12 as f32 + minute / 60.0; |
| const R0: f32 = -0.25; // Rotate from 3 to 12. |
| const STEPS: usize = 60 * 60; // Enough steps to ensure smooth movement |
| // of second hand each frame on a 60hz display. |
| let index = ((R0 + hour / 12.0).rem_euclid(1.0) * STEPS as f32) as usize; |
| if index != self.hour_index { |
| let angle = index as f32 * 2.0 * f32::consts::PI / STEPS as f32; |
| self.hour_hand.update(context, scale, -angle); |
| self.hour_index = index; |
| } |
| let index = ((R0 + minute / 60.0).rem_euclid(1.0) * STEPS as f32) as usize; |
| if index != self.minute_index { |
| let angle = index as f32 * 2.0 * f32::consts::PI / STEPS as f32; |
| self.minute_hand.update(context, scale, -angle); |
| self.minute_index = index; |
| } |
| let index = ((R0 + second / 60.0).rem_euclid(1.0) * STEPS as f32) as usize; |
| if index != self.second_index { |
| let angle = index as f32 * 2.0 * f32::consts::PI / STEPS as f32; |
| self.second_hand.update(context, scale, -angle); |
| self.second_index = index; |
| } |
| } |
| } |
| |
| struct Contents { |
| image: Image, |
| composition: Composition, |
| size: Size, |
| previous_rasters: Vec<Raster>, |
| } |
| |
| impl Contents { |
| fn new(image: Image) -> Self { |
| let composition = Composition::new(BACKGROUND_COLOR); |
| |
| Self { image, composition, size: Size::zero(), previous_rasters: Vec::new() } |
| } |
| |
| fn update(&mut self, context: &mut Context, scene: &Scene, size: &Size, scale: f32) { |
| const SHADOW_COLOR: Color = Color { r: 0, g: 0, b: 0, a: 13 }; |
| const ELEVATION: f32 = 0.01; |
| |
| let center = Vector2D::new(size.width as i32 / 2, size.height as i32 / 2); |
| let elevation = (ELEVATION * scale) as i32; |
| let shadow_offset = center + Vector2D::new(elevation, elevation * 2); |
| |
| let clip = Rect::new( |
| Point2D::new(0, 0), |
| Size2D::new(size.width.floor() as u32, size.height.floor() as u32), |
| ); |
| |
| let ext = if self.size != *size { |
| self.size = *size; |
| RenderExt { |
| pre_clear: Some(PreClear { color: BACKGROUND_COLOR }), |
| ..Default::default() |
| } |
| } else { |
| RenderExt::default() |
| }; |
| |
| let hands = [&scene.hour_hand, &scene.minute_hand, &scene.second_hand]; |
| |
| let layers = hands |
| .iter() |
| .map(|hand| Layer { |
| raster: hand.raster.clone().unwrap().translate(center), |
| style: Style { |
| fill_rule: FillRule::NonZero, |
| fill: Fill::Solid(hand.color), |
| blend_mode: BlendMode::Over, |
| }, |
| }) |
| .chain(std::iter::once(Layer { |
| raster: hands |
| .iter() |
| .fold(None, |raster_union: Option<Raster>, hand| { |
| let raster = hand.raster.clone().unwrap().translate(shadow_offset); |
| if let Some(raster_union) = raster_union { |
| Some(raster_union + raster) |
| } else { |
| Some(raster) |
| } |
| }) |
| .unwrap(), |
| style: Style { |
| fill_rule: FillRule::NonZero, |
| fill: Fill::Solid(SHADOW_COLOR), |
| blend_mode: BlendMode::Over, |
| }, |
| })) |
| .chain(std::iter::once(Layer { |
| raster: hands |
| .iter() |
| .enumerate() |
| .fold(None, |raster_union: Option<Raster>, (i, hand)| { |
| if i != 1 { |
| let raster = hand.raster.clone().unwrap().translate(shadow_offset); |
| if let Some(raster_union) = raster_union { |
| Some(raster_union + raster) |
| } else { |
| Some(raster) |
| } |
| } else { |
| raster_union |
| } |
| }) |
| .unwrap(), |
| style: Style { |
| fill_rule: FillRule::NonZero, |
| fill: Fill::Solid(SHADOW_COLOR), |
| blend_mode: BlendMode::Over, |
| }, |
| })) |
| .chain(self.previous_rasters.drain(..).map(|raster| Layer { |
| raster, |
| style: Style { |
| fill_rule: FillRule::WholeTile, |
| fill: Fill::Solid(BACKGROUND_COLOR), |
| blend_mode: BlendMode::Over, |
| }, |
| })); |
| self.composition.replace(.., layers); |
| |
| context.render(&self.composition, Some(clip), self.image, &ext); |
| |
| // Keep reference to rasters for clearing. |
| self.previous_rasters.extend( |
| hands.iter().map(|hand| hand.raster.clone().unwrap().translate(center)).chain( |
| hands.iter().map(|hand| hand.raster.clone().unwrap().translate(shadow_offset)), |
| ), |
| ); |
| } |
| } |
| |
| struct Clockface { |
| scene: Scene, |
| contents: BTreeMap<u64, Contents>, |
| } |
| |
| impl Clockface { |
| pub fn new(context: &mut Context) -> Self { |
| let scene = Scene::new(context); |
| |
| Self { scene, contents: BTreeMap::new() } |
| } |
| |
| fn update( |
| &mut self, |
| render_context: &mut Context, |
| context: &ViewAssistantContext, |
| ) -> Result<(), Error> { |
| duration!("gfx", "update"); |
| |
| let size = &context.size; |
| let image_id = context.image_id; |
| let scale = size.width.min(size.height); |
| |
| self.scene.update(render_context, size, scale); |
| |
| let image = render_context.get_current_image(context); |
| let content = self.contents.entry(image_id).or_insert_with(|| Contents::new(image)); |
| |
| content.update(render_context, &self.scene, size, scale); |
| |
| Ok(()) |
| } |
| } |
| |
| struct ClockfaceViewAssistant { |
| size: Size, |
| clockface: Option<Clockface>, |
| } |
| |
| impl ClockfaceViewAssistant { |
| pub fn new() -> Self { |
| Self { size: Size::zero(), clockface: None } |
| } |
| } |
| |
| impl ViewAssistant for ClockfaceViewAssistant { |
| fn render( |
| &mut self, |
| render_context: &mut Context, |
| ready_event: Event, |
| context: &ViewAssistantContext, |
| ) -> Result<(), Error> { |
| if context.size != self.size || self.clockface.is_none() { |
| self.size = context.size; |
| self.clockface = Some(Clockface::new(render_context)); |
| } |
| |
| if let Some(clockface) = self.clockface.as_mut() { |
| clockface.update(render_context, context).expect("clockface.update"); |
| } |
| |
| ready_event.as_handle_ref().signal(Signals::NONE, Signals::EVENT_SIGNALED)?; |
| |
| context.request_render(); |
| |
| Ok(()) |
| } |
| } |
| |
| fn main() -> Result<(), Error> { |
| fuchsia_trace_provider::trace_provider_create_with_fdio(); |
| |
| println!("Clockface Example"); |
| App::run(make_app_assistant::<ClockfaceAppAssistant>()) |
| } |