| // 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, |
| input::{self}, |
| make_app_assistant, |
| render::*, |
| App, AppAssistant, Point, RenderOptions, Size, ViewAssistant, ViewAssistantContext, |
| ViewAssistantPtr, ViewKey, |
| }, |
| euclid::{ |
| default::{Rect, Transform2D, Vector2D}, |
| point2, size2, vec2, Angle, |
| }, |
| fidl_fuchsia_hardware_input as hid, |
| fuchsia_trace::{self, duration}, |
| fuchsia_trace_provider, |
| fuchsia_zircon::{self as zx, AsHandleRef, Event, Signals, Time}, |
| itertools::izip, |
| rand::{thread_rng, Rng}, |
| std::{ |
| collections::{BTreeMap, VecDeque}, |
| f32, fs, |
| ops::Range, |
| }, |
| }; |
| |
| const BACKGROUND_COLOR: Color = Color { r: 255, g: 255, b: 255, a: 255 }; |
| |
| // Stroke constants. |
| const STROKE_START_RADIUS: f32 = 0.25; |
| const STROKE_RADIUS_ADJUSTMENT_AMOUNT: f32 = 0.1; |
| const MAX_STROKES: usize = 1000; |
| |
| // Toolbar constants. |
| const TOOL_RADIUS: f32 = 25.0; |
| const TOOL_PADDING: f32 = 12.5; |
| |
| // Color palette constants. |
| const COLORS: [Color; 6] = [ |
| Color { r: 0, g: 0, b: 0, a: 255 }, |
| Color { r: 255, g: 255, b: 255, a: 255 }, |
| Color { r: 187, g: 74, b: 72, a: 205 }, |
| Color { r: 225, g: 210, b: 92, a: 205 }, |
| Color { r: 61, g: 133, b: 177, a: 205 }, |
| Color { r: 36, g: 128, b: 108, a: 205 }, |
| ]; |
| |
| // Pencil constants. |
| const PENCILS: [f32; 3] = [1.5, 3.0, 10.0]; |
| |
| // Delay before starting to draw flowers after clearing the screen. |
| const FLOWER_DELAY_SECONDS: i64 = 10; |
| |
| fn lerp(t: f32, p0: Point, p1: Point) -> Point { |
| point2(p0.x * (1.0 - t) + p1.x * t, p0.y * (1.0 - t) + p1.y * t) |
| } |
| |
| trait InkPathBuilder { |
| fn line_to(&mut self, p: Point); |
| fn cubic_to(&mut self, p0: Point, p1: Point, p2: Point, p3: Point, offset: Vector2D<f32>) { |
| let deviation_x = (p0.x + p2.x - 3.0 * (p1.x + p2.x)).abs(); |
| let deviation_y = (p0.y + p2.y - 3.0 * (p1.y + p2.y)).abs(); |
| let deviation_squared = deviation_x * deviation_x + deviation_y * deviation_y; |
| |
| const PIXEL_ACCURACY: f32 = 0.25; |
| |
| if deviation_squared < PIXEL_ACCURACY { |
| self.line_to(point2(p3.x, p3.y) + offset); |
| return; |
| } |
| |
| const TOLERANCE: f32 = 3.0; |
| |
| let subdivisions = 1 + (TOLERANCE * deviation_squared).sqrt().sqrt().floor() as usize; |
| let increment = (subdivisions as f32).recip(); |
| |
| let mut t = 0.0; |
| |
| for _ in 0..subdivisions - 1 { |
| t += increment; |
| let p_next = lerp( |
| t, |
| lerp(t, lerp(t, p0, p1), lerp(t, p1, p2)), |
| lerp(t, lerp(t, p1, p2), lerp(t, p2, p3)), |
| ); |
| |
| self.line_to(point2(p_next.x, p_next.y) + offset); |
| } |
| self.line_to(point2(p3.x, p3.y) + offset); |
| } |
| } |
| |
| struct PointPathBuilder<'a> { |
| points: &'a mut Vec<Point>, |
| } |
| |
| impl<'a> PointPathBuilder<'a> { |
| fn new(points: &'a mut Vec<Point>) -> Self { |
| Self { points } |
| } |
| } |
| |
| impl<'a> InkPathBuilder for PointPathBuilder<'a> { |
| fn line_to(&mut self, p: Point) { |
| self.points.push(p); |
| } |
| } |
| |
| struct PathBuilderWrapper<'a> { |
| path_builder: &'a mut PathBuilder, |
| } |
| |
| impl<'a> PathBuilderWrapper<'a> { |
| fn new(path_builder: &'a mut PathBuilder) -> Self { |
| Self { path_builder } |
| } |
| } |
| |
| impl<'a> InkPathBuilder for PathBuilderWrapper<'a> { |
| fn line_to(&mut self, p: Point) { |
| self.path_builder.line_to(p); |
| } |
| |
| fn cubic_to(&mut self, _p0: Point, p1: Point, p2: Point, p3: Point, offset: Vector2D<f32>) { |
| let p1 = p1 + offset; |
| let p2 = p2 + offset; |
| let p3 = p3 + offset; |
| self.path_builder.cubic_to(p1, p2, p3); |
| } |
| } |
| |
| struct Circle { |
| points: Vec<Point>, |
| } |
| |
| impl Circle { |
| fn new(center: Point, radius: f32) -> Self { |
| let offset = center.to_vector(); |
| let dist = 4.0 / 3.0 * (f32::consts::PI / 8.0).tan(); |
| let control_dist = dist * radius; |
| |
| let t = point2(0.0, -radius); |
| let r = point2(radius, 0.0); |
| let b = point2(0.0, radius); |
| let l = point2(-radius, 0.0); |
| let ct = point2(0.0, -control_dist).to_vector(); |
| let cr = point2(control_dist, 0.0).to_vector(); |
| let cb = point2(0.0, control_dist).to_vector(); |
| let cl = point2(-control_dist, 0.0).to_vector(); |
| |
| let mut points = Vec::new(); |
| points.push(t + offset); |
| let mut path_builder = PointPathBuilder::new(&mut points); |
| path_builder.cubic_to(t, t + cr, r + ct, r, offset); |
| path_builder.cubic_to(r, r + cb, b + cr, b, offset); |
| path_builder.cubic_to(b, b + cl, l + cb, l, offset); |
| path_builder.cubic_to(l, l + ct, t + cl, t, offset); |
| |
| Self { points } |
| } |
| } |
| |
| struct Flower { |
| points: Vec<Point>, |
| } |
| |
| impl Flower { |
| fn new(width: f32, height: f32) -> Self { |
| const FLOWER_SIZE: f32 = 100.0; |
| const FLOWER_MIN_PETALS: usize = 3; |
| const FLOWER_MAX_PETALS: usize = 8; |
| const FLOWER_MIN_R1: f32 = 60.0; |
| const FLOWER_MAX_R1: f32 = 95.0; |
| const FLOWER_MIN_R2: f32 = 20.0; |
| const FLOWER_MAX_R2: f32 = 60.0; |
| |
| let mut rng = thread_rng(); |
| let petal_count: usize = rng.gen_range(FLOWER_MIN_PETALS, FLOWER_MAX_PETALS); |
| let r1: f32 = rng.gen_range(FLOWER_MIN_R1, FLOWER_MAX_R1); |
| let r2: f32 = rng.gen_range(FLOWER_MIN_R2, FLOWER_MAX_R2); |
| // Random location in canvas. |
| let offset = vec2( |
| rng.gen_range(FLOWER_SIZE, width - FLOWER_SIZE), |
| rng.gen_range(FLOWER_SIZE, height - FLOWER_SIZE), |
| ); |
| |
| let mut points = Vec::new(); |
| let u: f32 = rng.gen_range(10.0, FLOWER_SIZE) / FLOWER_SIZE; |
| let v: f32 = rng.gen_range(0.0, FLOWER_SIZE - 10.0) / FLOWER_SIZE; |
| let dt: f32 = f32::consts::PI / (petal_count as f32); |
| let mut t: f32 = 0.0; |
| |
| let mut p0 = point2(t.cos() * r1, t.sin() * r1); |
| points.push(p0 + offset); |
| let mut path_builder = PointPathBuilder::new(&mut points); |
| for _ in 0..petal_count { |
| let x1 = t.cos() * r1; |
| let y1 = t.sin() * r1; |
| let x2 = (t + dt).cos() * r2; |
| let y2 = (t + dt).sin() * r2; |
| let x3 = (t + 2.0 * dt).cos() * r1; |
| let y3 = (t + 2.0 * dt).sin() * r1; |
| |
| let p1 = point2(x1 - y1 * u, y1 + x1 * u); |
| let p2 = point2(x2 + y2 * v, y2 - x2 * v); |
| let p3 = point2(x2, y2); |
| let p4 = point2(x2 - y2 * v, y2 + x2 * v); |
| let p5 = point2(x3 + y3 * u, y3 - x3 * u); |
| let p6 = point2(x3, y3); |
| |
| path_builder.cubic_to(p0, p1, p2, p3, offset); |
| path_builder.cubic_to(p3, p4, p5, p6, offset); |
| |
| p0 = p6; |
| t += dt * 2.0; |
| } |
| |
| Self { points } |
| } |
| } |
| |
| /// Ink. |
| #[derive(Debug, FromArgs)] |
| #[argh(name = "ink_rs")] |
| struct Args { |
| /// use spinel (GPU rendering back-end) |
| #[argh(switch, short = 's')] |
| use_spinel: bool, |
| } |
| |
| #[derive(Default)] |
| struct InkAppAssistant { |
| use_spinel: bool, |
| } |
| |
| impl AppAssistant for InkAppAssistant { |
| 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(InkViewAssistant::new())) |
| } |
| |
| fn get_render_options(&self) -> RenderOptions { |
| RenderOptions { use_spinel: self.use_spinel, ..RenderOptions::default() } |
| } |
| } |
| |
| struct InkFill { |
| raster: Raster, |
| color: Color, |
| } |
| |
| impl InkFill { |
| fn new(context: &mut Context, color: &Color, points: &Vec<Point>) -> Self { |
| let path = { |
| let mut path_builder = context.path_builder().unwrap(); |
| let mut p0 = Point::zero(); |
| for (i, &p) in points.iter().enumerate() { |
| if i == 0 { |
| path_builder.move_to(p); |
| p0 = p; |
| } else { |
| path_builder.line_to(p); |
| } |
| } |
| path_builder.line_to(p0); |
| path_builder.build() |
| }; |
| |
| let mut raster_builder = context.raster_builder().unwrap(); |
| raster_builder.add(&path, None); |
| let raster = raster_builder.build(); |
| |
| Self { raster, color: *color } |
| } |
| } |
| |
| struct Segment { |
| path: Path, |
| raster: Option<Raster>, |
| } |
| |
| struct StrokePoint { |
| point: Point, |
| normal0: Vector2D<f32>, |
| normal1: Vector2D<f32>, |
| thickness: f32, |
| } |
| |
| struct CurveFitter { |
| first_control_points: Vec<Vector2D<f32>>, |
| second_control_points: Vec<Vector2D<f32>>, |
| end_points: Vec<Vector2D<f32>>, |
| coefficients: Vec<f32>, |
| } |
| |
| impl CurveFitter { |
| fn new() -> Self { |
| Self { |
| first_control_points: Vec::new(), |
| second_control_points: Vec::new(), |
| end_points: Vec::new(), |
| coefficients: Vec::new(), |
| } |
| } |
| |
| // Takes a set of |points| and generates a fitted curve with two control points in between each |
| // point. Returns an iterator to (first control point, second control point, end point) |
| // for |range|. These items are ready to be used to build a path using cubic_to(). |
| // |
| // Guided and simplified from |
| // https://ovpwp.wordpress.com/2008/12/17/how-to-draw-a-smooth-curve-through-a-set-of-2d-points-with-bezier-methods/ |
| fn compute_control_points( |
| &mut self, |
| points: impl Iterator<Item = Point>, |
| range: Range<usize>, |
| ) -> impl Iterator<Item = (Point, Point, Point)> + '_ { |
| duration!("gfx", "CurveFitter::compute_control_points"); |
| self.end_points.splice(.., points.map(|p| p.to_vector())); |
| self.first_control_points.clear(); |
| self.second_control_points.clear(); |
| self.coefficients.clear(); |
| |
| let num_control_points = self.end_points.len() - 1; |
| match num_control_points { |
| // Do nothing for a single point. |
| 0 => {} |
| // Calculate average for two points. |
| 1 => { |
| let p0 = self.end_points[0]; |
| let p1 = self.end_points[1]; |
| self.first_control_points.push((p0 * 2.0 + p1) / 3.0); |
| self.second_control_points.push((p0 + p1 * 2.0) / 3.0); |
| } |
| // Run the algorithm to generate two control points. |
| _ => { |
| // Compute first control points. |
| let mut b: f32 = 2.0; |
| let p0 = self.end_points[0]; |
| let p1 = self.end_points[1]; |
| let mut rhs = p0 + p1 * 2.0; |
| self.coefficients.push(0.0); |
| self.first_control_points.push(rhs / b); |
| for i in 1..num_control_points - 1 { |
| self.coefficients.push(1.0 / b); |
| b = 4.0 - self.coefficients[i]; |
| let p = self.end_points[i]; |
| let p_next = self.end_points[i + 1]; |
| rhs = p * 4.0 + p_next * 2.0; |
| self.first_control_points.push((rhs - self.first_control_points[i - 1]) / b); |
| } |
| self.coefficients.push(1.0 / b); |
| let p_prev = self.end_points[num_control_points - 1]; |
| let p_last = self.end_points[num_control_points]; |
| rhs = (p_prev * 8.0 + p_last) / 2.0; |
| b = 3.5 - self.coefficients[num_control_points - 1]; |
| self.first_control_points |
| .push((rhs - self.first_control_points[num_control_points - 2]) / b); |
| |
| // Back substitution. |
| for i in 1..num_control_points { |
| let fcp = self.first_control_points[num_control_points - i - 1]; |
| let fcp_next = self.first_control_points[num_control_points - i]; |
| let c = self.coefficients[num_control_points - i]; |
| self.first_control_points[num_control_points - i - 1] = fcp - fcp_next * c; |
| } |
| |
| // Compute second control points. |
| for i in 0..num_control_points - 1 { |
| let p_next = self.end_points[i + 1]; |
| let fcp_next = self.first_control_points[i + 1]; |
| self.second_control_points.push(p_next * 2.0 - fcp_next); |
| } |
| let fcp_last = self.first_control_points[num_control_points - 1]; |
| self.second_control_points.push((p_last + fcp_last) / 2.0); |
| } |
| } |
| |
| izip!( |
| self.first_control_points[range.start..range.end - 1].iter().map(|v| v.to_point()), |
| self.second_control_points[range.start..range.end - 1].iter().map(|v| v.to_point()), |
| self.end_points[range.start + 1..range.end].iter().map(|v| v.to_point()) |
| ) |
| } |
| } |
| |
| struct InkStroke { |
| points: Vec<StrokePoint>, |
| segments: Vec<(usize, Segment)>, |
| color: Color, |
| thickness: f32, |
| transform: Transform2D<f32>, |
| curve_fitter: CurveFitter, |
| } |
| |
| impl InkStroke { |
| fn new(color: Color, thickness: f32, transform: Transform2D<f32>) -> Self { |
| Self { |
| points: Vec::new(), |
| segments: Vec::new(), |
| color, |
| thickness, |
| transform, |
| curve_fitter: CurveFitter::new(), |
| } |
| } |
| |
| fn raster(context: &mut Context, path: &Path, transform: &Transform2D<f32>) -> Raster { |
| let mut raster_builder = context.raster_builder().unwrap(); |
| raster_builder.add(path, Some(transform)); |
| raster_builder.build() |
| } |
| |
| fn push_point(&mut self, p: &Point) { |
| match self.points.len() { |
| // Just add the first point. |
| 0 => self.points.push(StrokePoint { |
| point: *p, |
| normal0: Vector2D::zero(), |
| normal1: Vector2D::zero(), |
| thickness: STROKE_START_RADIUS, |
| }), |
| // Add second point and compute the normal for line between points. |
| 1 => { |
| let p0 = self.points.pop().unwrap(); |
| let e = p0.point - *p; |
| let n = vec2(-e.y, e.x).normalize(); |
| self.points.push(StrokePoint { |
| point: p0.point, |
| normal0: n, |
| normal1: n, |
| thickness: p0.thickness, |
| }); |
| self.points.push(StrokePoint { |
| point: *p, |
| normal0: n, |
| normal1: n, |
| thickness: STROKE_START_RADIUS, |
| }); |
| } |
| // Add new point, compute the normal, and the average normal for last |
| // two lines. We also make a limited adjustment to the average normal |
| // distance to maintain the correct line thickness. |
| _ => { |
| let p1 = self.points.pop().unwrap(); |
| let p0 = self.points.pop().unwrap(); |
| let e = p1.point - *p; |
| let n = vec2(-e.y, e.x).normalize(); |
| let mut t1 = (p1.normal1 + n) / 2.0; |
| let l = t1.square_length().max(0.1); |
| t1 *= 1.0 / l; |
| self.points.push(StrokePoint { |
| point: p0.point, |
| normal0: p0.normal0, |
| normal1: p0.normal1, |
| thickness: p0.thickness, |
| }); |
| self.points.push(StrokePoint { |
| point: p1.point, |
| normal0: p1.normal1, |
| normal1: t1, |
| thickness: p1.thickness, |
| }); |
| self.points.push(StrokePoint { |
| point: *p, |
| normal0: n, |
| normal1: n, |
| thickness: STROKE_START_RADIUS, |
| }); |
| } |
| } |
| } |
| |
| fn push_segment(&mut self, context: &mut Context, i0: usize, i1: usize) { |
| let path = { |
| let mut path_builder = context.path_builder().unwrap(); |
| |
| // |
| // Convert stroke to fill and compute a bounding box. |
| // |
| let mut p_draw_start = Point::zero(); |
| if i1 > i0 { |
| p_draw_start = |
| self.points[i0].point + self.points[i0].normal1 * self.points[i0].thickness; |
| path_builder.move_to(p_draw_start); |
| } |
| |
| for p in self.curve_fitter.compute_control_points( |
| self.points.iter().map(|p| p.point + p.normal1 * p.thickness), |
| i0..i1, |
| ) { |
| path_builder.cubic_to(p.0, p.1, p.2); |
| } |
| |
| let p_first = &self.points.first().unwrap(); |
| let p_last = &self.points.last().unwrap(); |
| |
| macro_rules! cap { |
| ( $p:expr, $w:expr ) => { |
| let offset = $p.point.to_vector(); |
| let n = vec2($p.normal0.y, -$p.normal0.x); |
| let p0 = Point::zero() + $p.normal1 * $w; |
| let p1 = Point::zero() - n * $w; |
| let p2 = Point::zero() - $p.normal1 * $w; |
| |
| let dist = 4.0 / 3.0 * (f32::consts::PI / 8.0).tan(); |
| let control_dist = dist * $w; |
| |
| let c0 = p0 - n * control_dist; |
| let c1 = p1 + $p.normal1 * control_dist; |
| let c2 = p1 - $p.normal1 * control_dist; |
| let c3 = p2 - n * control_dist; |
| |
| let mut wrapper = PathBuilderWrapper::new(&mut path_builder); |
| wrapper.cubic_to(p0, c0, c1, p1, offset); |
| wrapper.cubic_to(p1, c2, c3, p2, offset); |
| }; |
| } |
| |
| // Produce end-cap if at the end of the line and not connected to first point. |
| if i1 == self.points.len() && p_first.point != p_last.point { |
| cap!(p_last, p_last.thickness); |
| } |
| // Walk from point i1 back to i0 and offset by radius at each point. |
| if i1 > i0 { |
| let p = &self.points[i1 - 1]; |
| path_builder.line_to(p.point - p.normal1 * p.thickness); |
| } |
| |
| for p in self.curve_fitter.compute_control_points( |
| self.points.iter().rev().map(|p| p.point - p.normal1 * p.thickness), |
| self.points.len() - i1..self.points.len() - i0, |
| ) { |
| path_builder.cubic_to(p.0, p.1, p.2); |
| } |
| |
| // Produce start-cap if at the beginning of line and not connected to last point. |
| if i0 == 0 && p_first.point != p_last.point { |
| cap!(p_first, -p_first.thickness); |
| } |
| |
| path_builder.line_to(p_draw_start); |
| |
| path_builder.build() |
| }; |
| |
| self.segments.push((i0, Segment { path, raster: None })); |
| } |
| |
| fn update_thickness(&mut self, context: &mut Context) { |
| assert_eq!(self.points.is_empty(), false); |
| |
| // No update needed if last point has correct thickness. This assumes |
| // that last point always needs most adjustment. |
| if self.points.last().unwrap().thickness == self.thickness { |
| return; |
| } |
| |
| let adjustment_amount = self.thickness * STROKE_RADIUS_ADJUSTMENT_AMOUNT; |
| |
| for p in self.points.iter_mut().rev() { |
| if p.thickness == self.thickness { |
| break; |
| } |
| p.thickness = if p.thickness > self.thickness { |
| (p.thickness - adjustment_amount).max(self.thickness) |
| } else { |
| (p.thickness + adjustment_amount).min(self.thickness) |
| }; |
| } |
| |
| // Remove and get index of first point in last segment. |
| let mut i0 = self.segments.pop().map_or(0, |v| v.0); |
| |
| // Index of last point with final thickness. |
| let i1 = self.points.iter().rposition(|v| v.thickness == self.thickness).unwrap_or(i0); |
| |
| const SEGMENT_SIZE: usize = 256; |
| |
| // Add segments with final thickness. |
| while (i1 - i0) > SEGMENT_SIZE { |
| let i = i0 + SEGMENT_SIZE; |
| self.push_segment(context, i0, i); |
| i0 = i - 1; |
| } |
| |
| // Add any remaining points to last segment. |
| if (self.points.len() - i0) > 0 { |
| self.push_segment(context, i0, self.points.len()); |
| } |
| } |
| |
| fn update(&mut self, context: &mut Context) -> bool { |
| self.update_thickness(context); |
| |
| let mut changed = false; |
| for (_, segment) in self.segments.iter_mut() { |
| if segment.raster.is_none() { |
| segment.raster = Some(Self::raster(context, &segment.path, &self.transform)); |
| changed = true; |
| } |
| } |
| |
| changed |
| } |
| |
| fn transform(&mut self, transform: &Transform2D<f32>) { |
| self.transform = self.transform.then(transform); |
| |
| // Re-create rasters during next call to update. |
| for (_, segment) in self.segments.iter_mut() { |
| segment.raster = None; |
| } |
| } |
| } |
| |
| struct Scene { |
| tools: Vec<(InkStroke, InkFill, Point)>, |
| strokes: Vec<InkStroke>, |
| } |
| |
| impl Scene { |
| fn new() -> Self { |
| Self { tools: Vec::new(), strokes: Vec::new() } |
| } |
| |
| fn setup(&mut self, context: &mut Context, size: Size, tools: &Vec<(&Color, &f32)>) { |
| const TOOL_SIZE: f32 = (TOOL_RADIUS + TOOL_PADDING) * 2.0; |
| |
| // Layout tools at top-center. |
| let mut x = size.width / 2.0 - (tools.len() as f32 * TOOL_SIZE) / 2.0; |
| let y = TOOL_PADDING * 2.0 + TOOL_RADIUS; |
| for (color, size) in tools { |
| let center = point2(x, y); |
| let circle = Circle::new(center, TOOL_RADIUS); |
| let mut stroke = |
| InkStroke::new(Color { r: 0, g: 0, b: 0, a: 255 }, 1.0, Transform2D::identity()); |
| while stroke.points.len() < circle.points.len() { |
| let p = &circle.points[stroke.points.len()]; |
| stroke.push_point(p); |
| } |
| let circle = Circle::new(center, **size); |
| let fill = InkFill::new(context, color, &circle.points); |
| self.tools.push((stroke, fill, center)); |
| |
| x += TOOL_SIZE; |
| } |
| } |
| |
| fn hit_test(&mut self, point: Point) -> Option<usize> { |
| for (i, (_, _, center)) in self.tools.iter().enumerate() { |
| if (point - *center).length() < TOOL_RADIUS { |
| return Some(i); |
| } |
| } |
| |
| None |
| } |
| |
| fn select_tools(&mut self, indices: &Vec<usize>) { |
| for (i, (stroke, _, _)) in self.tools.iter_mut().enumerate() { |
| stroke.thickness = if indices.contains(&i) { 2.0 } else { 1.0 }; |
| } |
| } |
| |
| fn push_stroke(&mut self, color: Color, radius: f32, p: &Point) { |
| let mut stroke = InkStroke::new(color, radius, Transform2D::identity()); |
| stroke.push_point(p); |
| self.strokes.push(stroke); |
| } |
| |
| fn last_stroke(&mut self) -> Option<&mut InkStroke> { |
| self.strokes.last_mut() |
| } |
| |
| fn clear_strokes(&mut self) { |
| self.strokes.clear(); |
| } |
| |
| fn update_tools(&mut self, context: &mut Context) -> Option<Range<usize>> { |
| let mut damage: Option<Range<usize>> = None; |
| |
| for (i, (stroke, _, _)) in self.tools.iter_mut().enumerate() { |
| let changed = stroke.update(context); |
| if changed { |
| if let Some(damage) = &mut damage { |
| damage.end = i + 1; |
| } else { |
| damage = Some(Range { start: i, end: i + 1 }); |
| } |
| } |
| } |
| |
| damage |
| } |
| |
| fn update_strokes(&mut self, context: &mut Context) -> Option<Range<usize>> { |
| let mut damage: Option<Range<usize>> = None; |
| |
| for (i, stroke) in self.strokes.iter_mut().enumerate() { |
| let changed = stroke.update(context); |
| if changed { |
| if let Some(value) = damage.take() { |
| damage = Some(Range { start: value.start, end: i + 1 }); |
| } else { |
| damage = Some(Range { start: i, end: i + 1 }); |
| } |
| } |
| } |
| |
| damage |
| } |
| |
| fn transform(&mut self, transform: &Transform2D<f32>) { |
| for stroke in self.strokes.iter_mut() { |
| stroke.transform(transform); |
| } |
| } |
| } |
| |
| struct Contents { |
| image: Image, |
| composition: Composition, |
| size: Size, |
| tool_count: usize, |
| tool_damage: Option<Range<usize>>, |
| stroke_count: usize, |
| stroke_damage: Option<Range<usize>>, |
| } |
| |
| impl Contents { |
| fn new(image: Image) -> Self { |
| let composition = Composition::new(BACKGROUND_COLOR); |
| |
| Self { |
| image, |
| composition, |
| size: Size::zero(), |
| tool_count: 0, |
| tool_damage: None, |
| stroke_count: 0, |
| stroke_damage: None, |
| } |
| } |
| |
| fn update(&mut self, context: &mut Context, scene: &Scene, size: &Size) { |
| let clip = |
| Rect::new(point2(0, 0), size2(size.width.floor() as u32, size.height.floor() as u32)); |
| |
| let ext = if self.size != *size { |
| self.size = *size; |
| self.tool_damage = Some(Range { start: 0, end: scene.tools.len() }); |
| self.stroke_damage = Some(Range { start: 0, end: scene.strokes.len() }); |
| RenderExt { |
| pre_clear: Some(PreClear { color: BACKGROUND_COLOR }), |
| ..Default::default() |
| } |
| } else { |
| RenderExt::default() |
| }; |
| |
| // Update damaged tool layers. |
| if let Some(damage) = self.tool_damage.take() { |
| let layers = |
| scene.tools[damage.start..damage.end].iter().flat_map(|(stroke, fill, _)| { |
| std::iter::once(Layer { |
| raster: stroke |
| .segments |
| .iter() |
| .fold(None, |raster_union: Option<Raster>, segment| { |
| if let Some(raster) = &segment.1.raster { |
| if let Some(raster_union) = raster_union { |
| Some(raster_union + raster.clone()) |
| } else { |
| Some(raster.clone()) |
| } |
| } else { |
| raster_union |
| } |
| }) |
| .unwrap(), |
| style: Style { |
| fill_rule: FillRule::NonZero, |
| fill: Fill::Solid(stroke.color), |
| blend_mode: BlendMode::Over, |
| }, |
| }) |
| .chain(std::iter::once(Layer { |
| raster: fill.raster.clone(), |
| style: Style { |
| fill_rule: FillRule::NonZero, |
| fill: Fill::Solid(fill.color), |
| blend_mode: BlendMode::Over, |
| }, |
| })) |
| }); |
| let range = (damage.start * 2)..(damage.end * 2); |
| // Add tool layers if needed. |
| if self.tool_count < damage.end { |
| self.composition.replace(range.start.., layers); |
| self.tool_count = damage.end; |
| } else { |
| self.composition.replace(range, layers); |
| } |
| } |
| |
| let bottom = self.tool_count * 2 + scene.strokes.len(); |
| // Update damaged stroke layers. |
| if let Some(damage) = self.stroke_damage.take() { |
| let layers = scene.strokes[damage.start..damage.end].iter().rev().map(|stroke| Layer { |
| raster: stroke |
| .segments |
| .iter() |
| .fold(None, |raster_union: Option<Raster>, segment| { |
| if let Some(raster) = &segment.1.raster { |
| if let Some(raster_union) = raster_union { |
| Some(raster_union + raster.clone()) |
| } else { |
| Some(raster.clone()) |
| } |
| } else { |
| raster_union |
| } |
| }) |
| .unwrap(), |
| style: Style { |
| fill_rule: FillRule::NonZero, |
| fill: Fill::Solid(stroke.color), |
| blend_mode: BlendMode::Over, |
| }, |
| }); |
| // Reverse range. |
| let range = (bottom - damage.end)..(bottom - damage.start); |
| // Add more stroke layers if needed. |
| if self.stroke_count < scene.strokes.len() { |
| let count = scene.strokes.len() - self.stroke_count; |
| self.composition.replace(range.start..(range.end - count), layers); |
| self.stroke_count = scene.strokes.len(); |
| } else { |
| self.composition.replace(range, layers); |
| } |
| } |
| |
| // Remove strokes that are no longer part of the scene. |
| if self.stroke_count > scene.strokes.len() { |
| self.composition.replace(bottom.., std::iter::empty::<Layer>()); |
| self.stroke_count = scene.strokes.len(); |
| } |
| |
| context.render(&self.composition, Some(clip), self.image, &ext); |
| } |
| |
| fn add_tool_damage(&mut self, range: &Range<usize>) { |
| self.tool_damage = Some(if let Some(damage) = self.tool_damage.take() { |
| Range { start: range.start.min(damage.start), end: range.end.max(damage.end) } |
| } else { |
| range.clone() |
| }); |
| } |
| |
| fn add_stroke_damage(&mut self, range: &Range<usize>) { |
| self.stroke_damage = Some(if let Some(damage) = self.stroke_damage.take() { |
| Range { start: range.start.min(damage.start), end: range.end.max(damage.end) } |
| } else { |
| range.clone() |
| }); |
| } |
| |
| fn full_damage(&mut self) { |
| // Empty size will trigger a clear during next update. |
| self.size = Size::zero(); |
| } |
| } |
| |
| struct Stylus { |
| _rpt_id: u8, |
| status: u8, |
| x: u16, |
| y: u16, |
| } |
| |
| // TODO: Remove stylus device when supported by carnelian. |
| struct StylusDevice { |
| device: hid::DeviceSynchronousProxy, |
| x_max: u16, |
| y_max: u16, |
| } |
| |
| impl StylusDevice { |
| fn open_input_device(path: &str) -> Result<hid::DeviceSynchronousProxy, Error> { |
| let (client, server) = zx::Channel::create()?; |
| fdio::service_connect(path, server)?; |
| Ok(hid::DeviceSynchronousProxy::new(client)) |
| } |
| |
| fn create() -> Result<StylusDevice, Error> { |
| static INPUT_DEVICES_DIRECTORY: &str = "/dev/class/input"; |
| let path = std::path::Path::new(INPUT_DEVICES_DIRECTORY); |
| let entries = fs::read_dir(path)?; |
| for entry in entries { |
| let entry = entry?; |
| let entry_path = entry.path(); |
| let path = entry_path.to_str().expect("bad path"); |
| let mut device = Self::open_input_device(path)?; |
| if let Ok(hid::DeviceIds { vendor_id: 0x00002d1f, product_id, .. }) = |
| device.get_device_ids(zx::Time::INFINITE) |
| { |
| // Paradise |
| if product_id == 0x00005143 { |
| println!("found stylus at {0}", path); |
| const PARADISE_STYLUS_X_MAX: u16 = 25919; |
| const PARADISE_STYLUS_Y_MAX: u16 = 17279; |
| return Ok(StylusDevice { |
| device, |
| x_max: PARADISE_STYLUS_X_MAX, |
| y_max: PARADISE_STYLUS_Y_MAX, |
| }); |
| } |
| // Slate |
| if product_id == 0x0000486c { |
| println!("found stylus at {0}", path); |
| const SLATE_STYLUS_X_MAX: u16 = 26009; |
| const SLATE_STYLUS_Y_MAX: u16 = 17339; |
| return Ok(StylusDevice { |
| device, |
| x_max: SLATE_STYLUS_X_MAX, |
| y_max: SLATE_STYLUS_Y_MAX, |
| }); |
| } |
| } |
| } |
| Err(std::io::Error::new(std::io::ErrorKind::NotFound, "no touch found").into()) |
| } |
| |
| fn get_events(&mut self) -> Result<Vec<Stylus>, Error> { |
| let mut stylus_events = Vec::<Stylus>::new(); |
| let reports = self.device.read_reports(zx::Time::INFINITE)?; |
| let reports = reports.1; |
| let mut report_index = 0; |
| while report_index < reports.len() { |
| let report = &reports[report_index..]; |
| if report[0] != 6 { |
| report_index += 55; |
| continue; |
| } |
| |
| report_index += 20; |
| stylus_events.push(Stylus { |
| _rpt_id: report[0], |
| status: report[1], |
| x: report[2] as u16 + ((report[3] as u16) << 8), |
| y: report[4] as u16 + ((report[5] as u16) << 8), |
| }); |
| } |
| Ok(stylus_events) |
| } |
| } |
| |
| struct Ink { |
| scene: Scene, |
| contents: BTreeMap<u64, Contents>, |
| pending_pointer_events: VecDeque<input::Event>, |
| touch_points: BTreeMap<input::touch::ContactId, Point>, |
| stylus_device: Option<StylusDevice>, |
| last_stylus_x: u16, |
| last_stylus_y: u16, |
| last_stylus_point: Option<Point>, |
| flower: Option<Flower>, |
| flower_start: Time, |
| color: usize, |
| pencil: usize, |
| pan_origin: Vector2D<f32>, |
| scale_distance: f32, |
| rotation_angle: f32, |
| clear_origin: Vector2D<f32>, |
| } |
| |
| impl Ink { |
| pub fn new(context: &mut Context, size: Size) -> Self { |
| let mut scene = Scene::new(); |
| |
| let color_iter = COLORS.iter().map(|color| (color, &TOOL_RADIUS)); |
| let pencil_iter = PENCILS.iter().map(|size| (&Color { r: 0, g: 0, b: 0, a: 255 }, size)); |
| let tools = color_iter.chain(pencil_iter).collect::<Vec<_>>(); |
| scene.setup(context, size, &tools); |
| let color = 0; |
| let pencil = 1; |
| scene.select_tools(&vec![color, COLORS.len() + pencil]); |
| |
| let stylus_device = StylusDevice::create().ok(); |
| let flower_start = Time::from_nanos( |
| Time::get_monotonic() |
| .into_nanos() |
| .saturating_add(zx::Duration::from_seconds(FLOWER_DELAY_SECONDS).into_nanos()), |
| ); |
| |
| Self { |
| scene, |
| contents: BTreeMap::new(), |
| pending_pointer_events: VecDeque::new(), |
| touch_points: BTreeMap::new(), |
| stylus_device, |
| last_stylus_x: std::u16::MAX, |
| last_stylus_y: std::u16::MAX, |
| last_stylus_point: None, |
| flower: None, |
| flower_start, |
| color, |
| pencil, |
| pan_origin: Vector2D::zero(), |
| scale_distance: 0.0, |
| rotation_angle: 0.0, |
| clear_origin: Vector2D::zero(), |
| } |
| } |
| |
| fn update( |
| &mut self, |
| render_context: &mut Context, |
| context: &ViewAssistantContext, |
| ) -> Result<(), Error> { |
| duration!("gfx", "update"); |
| |
| let time_now = Time::get_monotonic(); |
| let size = &context.size; |
| let mut full_damage = false; |
| |
| // Process touch events. |
| let previous_touch_points_count = self.touch_points.len(); |
| |
| while let Some(event) = self.pending_pointer_events.pop_front() { |
| if let input::EventType::Touch(touch_event) = event.event_type { |
| self.touch_points.clear(); |
| for contact in touch_event.contacts.iter() { |
| match contact.phase { |
| input::touch::Phase::Down(point, _) => { |
| self.touch_points.insert(contact.contact_id, point.to_f32()); |
| } |
| input::touch::Phase::Moved(point, _) => { |
| self.touch_points.insert(contact.contact_id, point.to_f32()); |
| } |
| _ => {} |
| } |
| } |
| } |
| } |
| |
| let mut transform = Transform2D::identity(); |
| |
| // Pan and select color. |
| match self.touch_points.len() { |
| 1 | 2 => { |
| let mut origin = Vector2D::zero(); |
| for (_, point) in &self.touch_points { |
| origin += point.to_vector(); |
| } |
| origin /= self.touch_points.len() as f32; |
| if self.touch_points.len() != previous_touch_points_count { |
| if let Some(index) = self.scene.hit_test(origin.to_point()) { |
| if index < COLORS.len() { |
| self.color = index; |
| } else { |
| self.pencil = index - COLORS.len(); |
| } |
| self.scene.select_tools(&vec![self.color, COLORS.len() + self.pencil]); |
| } |
| self.pan_origin = origin; |
| } |
| let distance = origin - self.pan_origin; |
| transform = transform.then_translate(distance); |
| self.pan_origin = origin; |
| } |
| _ => {} |
| } |
| |
| // Rotation & zoom. |
| if self.touch_points.len() == 2 { |
| let mut iter = self.touch_points.iter(); |
| let point0 = iter.next().unwrap().1; |
| let point1 = iter.next().unwrap().1; |
| |
| let origin = (point0.to_vector() + point1.to_vector()) / 2.0; |
| transform = transform.then_translate(-origin); |
| |
| // Rotation. |
| let line = *point0 - *point1; |
| let angle = line.x.atan2(line.y); |
| if self.touch_points.len() != previous_touch_points_count { |
| self.rotation_angle = angle; |
| } |
| let rotation_angle = angle - self.rotation_angle; |
| transform = transform.then_rotate(Angle::radians(rotation_angle)); |
| self.rotation_angle = angle; |
| |
| // Pinch to zoom. |
| let distance = (*point0 - *point1).length(); |
| if distance != 0.0 { |
| if self.touch_points.len() != previous_touch_points_count { |
| self.scale_distance = distance; |
| } |
| let sxsy = distance / self.scale_distance; |
| transform = transform.then_scale(sxsy, sxsy); |
| self.scale_distance = distance; |
| } |
| |
| transform = transform.then_translate(origin); |
| } |
| |
| // Clear using 3 finger swipe across screen. |
| if self.touch_points.len() >= 3 { |
| let mut origin = Vector2D::zero(); |
| for (_, point) in &self.touch_points { |
| origin += point.to_vector(); |
| } |
| origin /= self.touch_points.len() as f32; |
| if self.touch_points.len() != previous_touch_points_count { |
| self.clear_origin = origin; |
| } |
| const MIN_CLEAR_SWIPE_DISTANCE: f32 = 512.0; |
| let distance = (origin - self.clear_origin).length(); |
| if distance >= MIN_CLEAR_SWIPE_DISTANCE { |
| self.flower_start = |
| Time::from_nanos(time_now.into_nanos().saturating_add( |
| zx::Duration::from_seconds(FLOWER_DELAY_SECONDS).into_nanos(), |
| )); |
| self.flower = None; |
| self.scene.clear_strokes(); |
| full_damage = true; |
| } |
| } |
| |
| if transform != Transform2D::identity() { |
| self.scene.transform(&transform); |
| full_damage = true; |
| } |
| |
| // Process stylus device input. |
| if let Some(device) = self.stylus_device.as_mut() { |
| let reports = device.get_events()?; |
| for report in &reports { |
| const STYLUS_STATUS_TSWITCH: u8 = 0x01; |
| if (report.status & STYLUS_STATUS_TSWITCH) != 0 { |
| if report.x != self.last_stylus_x || report.y != self.last_stylus_y { |
| let point = point2( |
| size.width * report.x as f32 / device.x_max as f32, |
| size.height * report.y as f32 / device.y_max as f32, |
| ); |
| |
| // Start new stroke or select color. |
| if self.last_stylus_x == std::u16::MAX |
| || self.last_stylus_y == std::u16::MAX |
| { |
| if let Some(index) = self.scene.hit_test(point) { |
| if index < COLORS.len() { |
| self.color = index; |
| } else { |
| self.pencil = index - COLORS.len(); |
| } |
| self.scene |
| .select_tools(&vec![self.color, COLORS.len() + self.pencil]); |
| } else { |
| // Start stroke if we haven't reached the limit. |
| if self.scene.strokes.len() < MAX_STROKES { |
| self.scene.push_stroke( |
| COLORS[self.color], |
| PENCILS[self.pencil], |
| &point, |
| ); |
| self.last_stylus_point = Some(point); |
| } |
| // Disable flower demo. |
| self.flower_start = zx::Time::INFINITE; |
| self.flower = None; |
| } |
| } |
| |
| // Update stroke if distance from last point surpassed radius. |
| if let Some(last_stylus_point) = self.last_stylus_point { |
| if (point - last_stylus_point).length() > PENCILS[self.pencil] { |
| self.scene.last_stroke().unwrap().push_point(&point); |
| self.last_stylus_point = Some(point); |
| } |
| } |
| |
| self.last_stylus_x = report.x; |
| self.last_stylus_y = report.y; |
| } |
| } else { |
| self.last_stylus_x = std::u16::MAX; |
| self.last_stylus_y = std::u16::MAX; |
| self.last_stylus_point = None; |
| } |
| } |
| } |
| |
| // Generate flower when idle after clearing screen. |
| if time_now.into_nanos() > self.flower_start.into_nanos() { |
| let flower = self.flower.take().unwrap_or_else(|| { |
| let flower = Flower::new(size.width, size.height); |
| self.scene.push_stroke(COLORS[self.color], PENCILS[self.pencil], &flower.points[0]); |
| flower |
| }); |
| |
| // Points per second. |
| const SPEED: f32 = 100.0; |
| const SECONDS_PER_NANOSECOND: f32 = 1e-9; |
| |
| let n = ((time_now.into_nanos() - self.flower_start.into_nanos()) as f32 |
| * SECONDS_PER_NANOSECOND |
| * SPEED) as usize; |
| |
| let stroke = self.scene.last_stroke().unwrap(); |
| |
| // Extend set of points for current stroke. |
| while n > stroke.points.len() && stroke.points.len() < flower.points.len() { |
| let p = &flower.points[stroke.points.len()]; |
| stroke.push_point(p); |
| } |
| |
| if stroke.points.len() == flower.points.len() { |
| self.flower_start = if self.scene.strokes.len() < MAX_STROKES { |
| time_now |
| } else { |
| zx::Time::INFINITE |
| }; |
| } else { |
| self.flower = Some(flower); |
| } |
| } |
| |
| // Full damage for changes that require some amount of clearing. |
| if full_damage { |
| for content in self.contents.values_mut() { |
| content.full_damage(); |
| } |
| } |
| |
| // Update tools and add damage to each content. |
| if let Some(tool_damage) = self.scene.update_tools(render_context) { |
| for content in self.contents.values_mut() { |
| content.add_tool_damage(&tool_damage); |
| } |
| } |
| |
| // Update strokes and add damage to each content. |
| if let Some(stroke_damage) = self.scene.update_strokes(render_context) { |
| for content in self.contents.values_mut() { |
| content.add_stroke_damage(&stroke_damage); |
| } |
| } |
| |
| let image_id = context.image_id; |
| 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); |
| |
| Ok(()) |
| } |
| |
| fn handle_pointer_event(&mut self, event: &input::Event) { |
| self.pending_pointer_events.push_back(event.clone()); |
| } |
| } |
| |
| struct InkViewAssistant { |
| size: Size, |
| ink: Option<Ink>, |
| } |
| |
| impl InkViewAssistant { |
| pub fn new() -> Self { |
| Self { size: Size::zero(), ink: None } |
| } |
| } |
| |
| impl ViewAssistant for InkViewAssistant { |
| fn render( |
| &mut self, |
| render_context: &mut Context, |
| ready_event: Event, |
| context: &ViewAssistantContext, |
| ) -> Result<(), Error> { |
| if context.size != self.size || self.ink.is_none() { |
| let ink = Ink::new(render_context, context.size); |
| |
| self.size = context.size; |
| self.ink = Some(ink); |
| } |
| |
| if let Some(ink) = self.ink.as_mut() { |
| ink.update(render_context, context).expect("ink.update"); |
| } |
| |
| ready_event.as_handle_ref().signal(Signals::NONE, Signals::EVENT_SIGNALED)?; |
| |
| context.request_render(); |
| |
| Ok(()) |
| } |
| |
| fn handle_pointer_event( |
| &mut self, |
| _context: &mut ViewAssistantContext, |
| event: &input::Event, |
| _pointer_event: &input::pointer::Event, |
| ) -> Result<(), Error> { |
| if let Some(ink) = self.ink.as_mut() { |
| ink.handle_pointer_event(event); |
| } |
| Ok(()) |
| } |
| } |
| |
| fn main() -> Result<(), Error> { |
| fuchsia_trace_provider::trace_provider_create_with_fdio(); |
| |
| println!("Ink Example"); |
| App::run(make_app_assistant::<InkAppAssistant>()) |
| } |