From 0fb22b5f50111dc3d80cb9c9dc4885a1b94c52e6 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Mon, 30 Mar 2020 15:48:23 -0700 Subject: [PATCH] sloppy (and incorrect!) implementation of gradients. temporarily breaks non-glium backends, about to cleanup. --- abstutil/src/collections.rs | 2 +- data/system/assets/pregame/start.svg | 13 +++++ ezgui/src/backend_glium.rs | 16 +++++-- ezgui/src/color.rs | 71 ++++++++++++++++++++++++++++ ezgui/src/drawing.rs | 27 ++++++----- ezgui/src/geom.rs | 44 ++++++++++------- ezgui/src/lib.rs | 2 +- ezgui/src/svg.rs | 33 ++++++++----- ezgui/src/text.rs | 4 +- ezgui/src/widgets/plot.rs | 2 +- game/src/info/trip.rs | 2 +- game/src/pregame.rs | 4 +- game/src/render/bus_stop.rs | 8 ++-- game/src/render/map.rs | 6 +-- game/src/render/traffic_signal.rs | 2 +- geom/src/line.rs | 27 ++++++++++- 16 files changed, 198 insertions(+), 65 deletions(-) create mode 100644 data/system/assets/pregame/start.svg diff --git a/abstutil/src/collections.rs b/abstutil/src/collections.rs index 84d2d9d9c1..1ec658755d 100644 --- a/abstutil/src/collections.rs +++ b/abstutil/src/collections.rs @@ -148,7 +148,7 @@ pub struct VecMap { inner: Vec<(K, V)>, } -impl VecMap { +impl VecMap { pub fn new() -> VecMap { VecMap { inner: Vec::new() } } diff --git a/data/system/assets/pregame/start.svg b/data/system/assets/pregame/start.svg new file mode 100644 index 0000000000..14f951deeb --- /dev/null +++ b/data/system/assets/pregame/start.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/ezgui/src/backend_glium.rs b/ezgui/src/backend_glium.rs index 81a11a9a74..fa03d2cd2b 100644 --- a/ezgui/src/backend_glium.rs +++ b/ezgui/src/backend_glium.rs @@ -1,5 +1,5 @@ use crate::drawing::Uniforms; -use crate::{Canvas, Color, ScreenDims, ScreenRectangle}; +use crate::{Canvas, Color, FancyColor, ScreenDims, ScreenRectangle}; use geom::Polygon; use glium::uniforms::UniformValue; use glium::Surface; @@ -180,7 +180,7 @@ pub struct PrerenderInnards { } impl PrerenderInnards { - pub fn actually_upload(&self, permanent: bool, list: Vec<(Color, &Polygon)>) -> Drawable { + pub fn actually_upload(&self, permanent: bool, list: Vec<(FancyColor, &Polygon)>) -> Drawable { let mut vertices: Vec = Vec::new(); let mut indices: Vec = Vec::new(); @@ -189,10 +189,16 @@ impl PrerenderInnards { let (pts, raw_indices) = poly.raw_for_rendering(); for pt in pts { let style = match color { - Color::RGBA(r, g, b, a) => [r, g, b, a], + FancyColor::Plain(Color::RGBA(r, g, b, a)) => [r, g, b, a], // Two special cases - Color::HatchingStyle1 => [100.0, 0.0, 0.0, 0.0], - Color::HatchingStyle2 => [101.0, 0.0, 0.0, 0.0], + FancyColor::Plain(Color::HatchingStyle1) => [100.0, 0.0, 0.0, 0.0], + FancyColor::Plain(Color::HatchingStyle2) => [101.0, 0.0, 0.0, 0.0], + FancyColor::LinearGradient(ref line, ref lg) => { + match FancyColor::interp_lg(line, lg, *pt) { + Color::RGBA(r, g, b, a) => [r, g, b, a], + _ => unreachable!(), + } + } }; vertices.push(Vertex { position: [pt.x() as f32, pt.y() as f32], diff --git a/ezgui/src/color.rs b/ezgui/src/color.rs index 48280a35f9..69c08ef46c 100644 --- a/ezgui/src/color.rs +++ b/ezgui/src/color.rs @@ -1,3 +1,4 @@ +use geom::{Line, Pt2D}; use serde_derive::{Deserialize, Serialize}; use std::fmt; @@ -19,6 +20,15 @@ impl fmt::Display for Color { } } +// TODO Not sure if this is hacky or not. Maybe Color should be specialized to RGBA, and these are +// other cases... +#[derive(Clone, PartialEq)] +pub enum FancyColor { + Plain(Color), + // The line, then stops (percent along, color) + LinearGradient(Line, Vec<(f64, Color)>), +} + impl Color { // TODO Won't this confuse the shader? :P pub const INVISIBLE: Color = Color::rgba_f(1.0, 0.0, 0.0, 0.0); @@ -95,4 +105,65 @@ impl Color { _ => unreachable!(), } } + + fn lerp(self, other: Color, pct: f32) -> Color { + match (self, other) { + (Color::RGBA(r1, g1, b1, a1), Color::RGBA(r2, g2, b2, a2)) => Color::RGBA( + lerp(pct, (r1, r2)), + lerp(pct, (g1, g2)), + lerp(pct, (b1, b2)), + lerp(pct, (a1, a2)), + ), + _ => unreachable!(), + } + } +} + +impl FancyColor { + pub(crate) fn linear_gradient(lg: &usvg::LinearGradient) -> FancyColor { + let line = Line::new(Pt2D::new(lg.x1, lg.y1), Pt2D::new(lg.x2, lg.y2)); + let mut stops = Vec::new(); + for stop in &lg.stops { + let color = Color::rgba( + stop.color.red as usize, + stop.color.green as usize, + stop.color.blue as usize, + stop.opacity.value() as f32, + ); + stops.push((stop.offset.value(), color)); + } + FancyColor::LinearGradient(line, stops) + } + + pub(crate) fn interp_lg(line: &Line, stops: &Vec<(f64, Color)>, corner: Pt2D) -> Color { + // https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient is the best reference + // I've found, even though it's technically for CSS, not SVG + let pct = line + .percent_along_of_point(line.project_pt(corner)) + .unwrap(); + if pct < stops[0].0 { + return stops[0].1; + } + if pct > stops.last().unwrap().0 { + return stops.last().unwrap().1; + } + // In between two + for ((pct1, c1), (pct2, c2)) in stops.iter().zip(stops.iter().skip(1)) { + if pct >= *pct1 && pct <= *pct2 { + return c1.lerp(*c2, to_pct(pct, (*pct1, *pct2)) as f32); + } + } + unreachable!() + } +} + +fn to_pct(value: f64, (low, high): (f64, f64)) -> f64 { + assert!(low <= high); + assert!(value >= low); + assert!(value <= high); + (value - low) / (high - low) +} + +fn lerp(pct: f32, (x1, x2): (f32, f32)) -> f32 { + x1 + pct * (x2 - x1) } diff --git a/ezgui/src/drawing.rs b/ezgui/src/drawing.rs index f422cb351d..aa4893e3ee 100644 --- a/ezgui/src/drawing.rs +++ b/ezgui/src/drawing.rs @@ -1,8 +1,8 @@ use crate::assets::Assets; use crate::backend::{GfxCtxInnards, PrerenderInnards}; use crate::{ - Canvas, Color, Drawable, GeomBatch, HorizontalAlignment, ScreenDims, ScreenPt, ScreenRectangle, - Text, VerticalAlignment, + Canvas, Color, Drawable, FancyColor, GeomBatch, HorizontalAlignment, ScreenDims, ScreenPt, + ScreenRectangle, Text, VerticalAlignment, }; use geom::{Bounds, Circle, Distance, Line, Polygon, Pt2D}; use std::cell::Cell; @@ -136,14 +136,19 @@ impl<'a> GfxCtx<'a> { } pub fn draw_polygon(&mut self, color: Color, poly: &Polygon) { - let obj = self.prerender.upload_temporary(vec![(color, poly)]); + let obj = self + .prerender + .upload_temporary(vec![(FancyColor::Plain(color), poly)]); self.redraw(&obj); } pub fn draw_polygons(&mut self, color: Color, polygons: &Vec) { - let obj = self - .prerender - .upload_temporary(polygons.iter().map(|p| (color, p)).collect()); + let obj = self.prerender.upload_temporary( + polygons + .iter() + .map(|p| (FancyColor::Plain(color), p)) + .collect(), + ); self.redraw(&obj); } @@ -285,12 +290,8 @@ pub struct Prerender { } impl Prerender { - pub fn upload_borrowed(&self, list: Vec<(Color, &Polygon)>) -> Drawable { - self.actually_upload(true, list) - } - pub fn upload(&self, batch: GeomBatch) -> Drawable { - let borrows = batch.list.iter().map(|(c, p)| (*c, p)).collect(); + let borrows = batch.list.iter().map(|(c, p)| (c.clone(), p)).collect(); self.actually_upload(true, borrows) } @@ -298,11 +299,11 @@ impl Prerender { self.inner.total_bytes_uploaded.get() } - pub(crate) fn upload_temporary(&self, list: Vec<(Color, &Polygon)>) -> Drawable { + pub(crate) fn upload_temporary(&self, list: Vec<(FancyColor, &Polygon)>) -> Drawable { self.actually_upload(false, list) } - fn actually_upload(&self, permanent: bool, list: Vec<(Color, &Polygon)>) -> Drawable { + fn actually_upload(&self, permanent: bool, list: Vec<(FancyColor, &Polygon)>) -> Drawable { // println!("{:?}", backtrace::Backtrace::new()); self.num_uploads.set(self.num_uploads.get() + 1); self.inner.actually_upload(permanent, list) diff --git a/ezgui/src/geom.rs b/ezgui/src/geom.rs index 95bd1cd947..d242f4070f 100644 --- a/ezgui/src/geom.rs +++ b/ezgui/src/geom.rs @@ -1,10 +1,10 @@ -use crate::{svg, Color, Drawable, EventCtx, GfxCtx, Prerender, ScreenDims}; +use crate::{svg, Color, Drawable, EventCtx, FancyColor, GfxCtx, Prerender, ScreenDims}; use geom::{Angle, Bounds, Polygon, Pt2D}; /// A mutable builder for a group of colored polygons. #[derive(Clone)] pub struct GeomBatch { - pub(crate) list: Vec<(Color, Polygon)>, + pub(crate) list: Vec<(FancyColor, Polygon)>, // TODO A weird hack for text. pub(crate) dims_text: bool, } @@ -21,20 +21,26 @@ impl GeomBatch { /// Creates a batch of colored polygons. pub fn from(list: Vec<(Color, Polygon)>) -> GeomBatch { GeomBatch { - list, + list: list + .into_iter() + .map(|(c, p)| (FancyColor::Plain(c), p)) + .collect(), dims_text: false, } } /// Adds a single colored polygon. pub fn push(&mut self, color: Color, p: Polygon) { + self.list.push((FancyColor::Plain(color), p)); + } + pub fn fancy_push(&mut self, color: FancyColor, p: Polygon) { self.list.push((color, p)); } /// Applies one color to many polygons. pub fn extend(&mut self, color: Color, polys: Vec) { for p in polys { - self.list.push((color, p)); + self.list.push((FancyColor::Plain(color), p)); } } @@ -44,13 +50,17 @@ impl GeomBatch { } /// Returns the colored polygons in this batch, destroying the batch. - pub fn consume(self) -> Vec<(Color, Polygon)> { + pub fn consume(self) -> Vec<(FancyColor, Polygon)> { self.list } /// Draws the batch, consuming it. Only use this for drawing things once. pub fn draw(self, g: &mut GfxCtx) { - let refs = self.list.iter().map(|(color, p)| (*color, p)).collect(); + let refs = self + .list + .iter() + .map(|(color, p)| (color.clone(), p)) + .collect(); let obj = g.prerender.upload_temporary(refs); g.redraw(&obj); } @@ -101,17 +111,19 @@ impl GeomBatch { /// Transforms all colors in a batch. pub fn rewrite_color(&mut self, transformation: RewriteColor) { - for (c, _) in self.list.iter_mut() { - match transformation { - RewriteColor::NoOp => {} - RewriteColor::Change(from, to) => { - if *c == from { + for (fancy, _) in self.list.iter_mut() { + if let FancyColor::Plain(ref mut c) = fancy { + match transformation { + RewriteColor::NoOp => {} + RewriteColor::Change(from, to) => { + if *c == from { + *c = to; + } + } + RewriteColor::ChangeAll(to) => { *c = to; } } - RewriteColor::ChangeAll(to) => { - *c = to; - } } } } @@ -158,7 +170,7 @@ impl GeomBatch { if rotate != Angle::ZERO { poly = poly.rotate(rotate); } - self.push(color, poly); + self.fancy_push(color, poly); } } @@ -166,7 +178,7 @@ impl GeomBatch { /// Adds geometry from another batch to the current batch, first translating it. pub fn add_translated(&mut self, other: GeomBatch, dx: f64, dy: f64) { for (color, poly) in other.consume() { - self.push(color, poly.translate(dx, dy)); + self.fancy_push(color, poly.translate(dx, dy)); } } } diff --git a/ezgui/src/lib.rs b/ezgui/src/lib.rs index 209bfa5c88..d9e81ee840 100644 --- a/ezgui/src/lib.rs +++ b/ezgui/src/lib.rs @@ -40,7 +40,7 @@ mod widgets; pub use crate::backend::Drawable; pub use crate::canvas::{Canvas, HorizontalAlignment, VerticalAlignment}; -pub use crate::color::Color; +pub use crate::color::{Color, FancyColor}; pub use crate::drawing::{GfxCtx, Prerender}; pub use crate::event::{hotkey, hotkeys, lctrl, Event, Key, MultiKey}; pub use crate::event_ctx::EventCtx; diff --git a/ezgui/src/svg.rs b/ezgui/src/svg.rs index b5c877bf61..e110be5d82 100644 --- a/ezgui/src/svg.rs +++ b/ezgui/src/svg.rs @@ -1,4 +1,4 @@ -use crate::{Color, GeomBatch, Prerender}; +use crate::{Color, FancyColor, GeomBatch, Prerender}; use abstutil::VecMap; use geom::{Bounds, Polygon, Pt2D}; use lyon::math::Point; @@ -51,14 +51,15 @@ pub fn add_svg_inner( ) -> Result { let mut fill_tess = tessellation::FillTessellator::new(); let mut stroke_tess = tessellation::StrokeTessellator::new(); - let mut mesh_per_color: VecMap> = VecMap::new(); + // TODO This breaks on start.svg; the order there matters. color1, color2, then color1 again. + let mut mesh_per_color: VecMap> = VecMap::new(); for node in svg_tree.root().descendants() { if let usvg::NodeKind::Path(ref p) = *node.borrow() { // TODO Handle transforms if let Some(ref fill) = p.fill { - let color = convert_color(&fill.paint, fill.opacity.value()); + let color = convert_color(&fill.paint, fill.opacity.value(), &svg_tree); let geom = mesh_per_color.mut_or_insert(color, VertexBuffers::new); if fill_tess .tessellate( @@ -73,7 +74,7 @@ pub fn add_svg_inner( } if let Some(ref stroke) = p.stroke { - let (color, stroke_opts) = convert_stroke(stroke, tolerance); + let (color, stroke_opts) = convert_stroke(stroke, tolerance, &svg_tree); let geom = mesh_per_color.mut_or_insert(color, VertexBuffers::new); stroke_tess .tessellate(convert_path(p), &stroke_opts, &mut simple_builder(geom)) @@ -83,7 +84,7 @@ pub fn add_svg_inner( } for (color, mesh) in mesh_per_color.consume() { - batch.push( + batch.fancy_push( color, Polygon::precomputed( mesh.vertices @@ -204,8 +205,12 @@ fn convert_path<'a>(p: &'a usvg::Path) -> PathConvIter<'a> { } } -fn convert_stroke(s: &usvg::Stroke, tolerance: f32) -> (Color, tessellation::StrokeOptions) { - let color = convert_color(&s.paint, s.opacity.value()); +fn convert_stroke( + s: &usvg::Stroke, + tolerance: f32, + tree: &usvg::Tree, +) -> (FancyColor, tessellation::StrokeOptions) { + let color = convert_color(&s.paint, s.opacity.value(), tree); let linecap = match s.linecap { usvg::LineCap::Butt => tessellation::LineCap::Butt, usvg::LineCap::Square => tessellation::LineCap::Square, @@ -225,15 +230,17 @@ fn convert_stroke(s: &usvg::Stroke, tolerance: f32) -> (Color, tessellation::Str (color, opt) } -fn convert_color(paint: &usvg::Paint, opacity: f64) -> Color { - if let usvg::Paint::Color(c) = paint { - Color::rgba( +fn convert_color(paint: &usvg::Paint, opacity: f64, tree: &usvg::Tree) -> FancyColor { + match paint { + usvg::Paint::Color(c) => FancyColor::Plain(Color::rgba( c.red as usize, c.green as usize, c.blue as usize, opacity as f32, - ) - } else { - panic!("Unsupported paint {:?}", paint); + )), + usvg::Paint::Link(name) => match *tree.defs_by_id(name).unwrap().borrow() { + usvg::NodeKind::LinearGradient(ref lg) => FancyColor::linear_gradient(lg), + _ => panic!("Unsupported color style {}", name), + }, } } diff --git a/ezgui/src/text.rs b/ezgui/src/text.rs index 80105e56d5..777bd172cb 100644 --- a/ezgui/src/text.rs +++ b/ezgui/src/text.rs @@ -272,7 +272,7 @@ impl Text { // Add all of the padding at the bottom of the line. let offset = line_height / SCALE_LINE_HEIGHT * 0.2; for (color, poly) in line_batch.consume() { - master_batch.push(color, poly.translate(0.0, y - offset)); + master_batch.fancy_push(color, poly.translate(0.0, y - offset)); } max_width = max_width.max(line_dims.width); @@ -282,7 +282,7 @@ impl Text { output_batch.push(c, Polygon::rectangle(max_width, y)); } for (color, poly) in master_batch.consume() { - output_batch.push(color, poly); + output_batch.fancy_push(color, poly); } output_batch.dims_text = true; diff --git a/ezgui/src/widgets/plot.rs b/ezgui/src/widgets/plot.rs index 02e601bb5c..dcbe7a8cf5 100644 --- a/ezgui/src/widgets/plot.rs +++ b/ezgui/src/widgets/plot.rs @@ -182,7 +182,7 @@ impl> Plot // TODO Need ticks now to actually see where this goes let mut batch = GeomBatch::new(); for (color, poly) in Text::from(Line(t.to_string())).render_ctx(ctx).consume() { - batch.push(color, poly.rotate(Angle::new_degs(-15.0))); + batch.fancy_push(color, poly.rotate(Angle::new_degs(-15.0))); } row.push(Widget::draw_batch(ctx, batch.autocrop())); } diff --git a/game/src/info/trip.rs b/game/src/info/trip.rs index 22258309ea..2a2dae3dfb 100644 --- a/game/src/info/trip.rs +++ b/game/src/info/trip.rs @@ -194,7 +194,7 @@ pub fn details(ctx: &mut EventCtx, app: &App, trip: TripID, details: &mut Detail let mut hovered = GeomBatch::from(vec![(color.alpha(1.0), rect.clone())]); for (c, p) in normal.clone().consume().into_iter().skip(1) { - hovered.push(c, p); + hovered.fancy_push(c, p); } timeline.push( diff --git a/game/src/pregame.rs b/game/src/pregame.rs index 7ffaacdac7..8ea338cedd 100644 --- a/game/src/pregame.rs +++ b/game/src/pregame.rs @@ -30,9 +30,9 @@ impl TitleScreen { Composite::new( Widget::col(vec![ Widget::draw_svg(ctx, "../data/system/assets/pregame/logo.svg").margin(5), - // TODO that nicer font // TODO Any key - Btn::text_bg2("PLAY") + // TODO The hover color is wacky + Btn::svg_def("../data/system/assets/pregame/start.svg") .build(ctx, "start game", hotkeys(vec![Key::Space, Key::Enter])) .margin(5), ]) diff --git a/game/src/render/bus_stop.rs b/game/src/render/bus_stop.rs index 721f72640b..d1b885d93f 100644 --- a/game/src/render/bus_stop.rs +++ b/game/src/render/bus_stop.rs @@ -1,7 +1,7 @@ use crate::app::App; use crate::helpers::{ColorScheme, ID}; use crate::render::{DrawOptions, Renderable, OUTLINE_THICKNESS}; -use ezgui::{Color, Drawable, GfxCtx, Prerender}; +use ezgui::{Color, Drawable, GeomBatch, GfxCtx, Prerender}; use geom::{Distance, PolyLine, Polygon, Pt2D}; use map_model::{BusStop, BusStopID, Map}; @@ -46,10 +46,10 @@ impl DrawBusStop { ])); let polygon = polyline.make_polygons(lane.width * 0.25); - let draw_default = prerender.upload_borrowed(vec![( + let draw_default = prerender.upload(GeomBatch::from(vec![( cs.get_def("bus stop marking", Color::CYAN), - &polygon, - )]); + polygon.clone(), + )])); DrawBusStop { id: stop.id, diff --git a/game/src/render/map.rs b/game/src/render/map.rs index 87fdf359ca..501851b142 100644 --- a/game/src/render/map.rs +++ b/game/src/render/map.rs @@ -208,10 +208,10 @@ impl DrawMap { let draw_all_areas = all_areas.upload(ctx); timer.stop("upload all areas"); - let boundary_polygon = ctx.prerender.upload_borrowed(vec![( + let boundary_polygon = ctx.prerender.upload(GeomBatch::from(vec![( cs.get_def("map background", Color::grey(0.87)), - map.get_boundary_polygon(), - )]); + map.get_boundary_polygon().clone(), + )])); timer.start("create quadtree"); let mut quadtree = QuadTree::default(map.get_bounds().as_bbox()); diff --git a/game/src/render/traffic_signal.rs b/game/src/render/traffic_signal.rs index 6239a203da..9d5c299030 100644 --- a/game/src/render/traffic_signal.rs +++ b/game/src/render/traffic_signal.rs @@ -306,7 +306,7 @@ pub fn make_signal_diagram( normal.push(Color::BLACK, bbox.clone()); // Move to the origin and apply zoom for (color, poly) in orig_batch.consume() { - normal.push( + normal.fancy_push( color, poly.translate(-bounds.min_x, -bounds.min_y).scale(zoom), ); diff --git a/geom/src/line.rs b/geom/src/line.rs index 7186c1af70..0459dcec1e 100644 --- a/geom/src/line.rs +++ b/geom/src/line.rs @@ -1,4 +1,5 @@ use crate::{Angle, Distance, PolyLine, Polygon, Pt2D, EPSILON_DIST}; +use geo::prelude::ClosestPoint; use serde_derive::{Deserialize, Serialize}; use std::fmt; @@ -131,13 +132,16 @@ impl Line { self.percent_along(dist / len) } - pub fn percent_along(&self, percent: f64) -> Pt2D { - assert!(percent >= 0.0 && percent <= 1.0); + pub fn unbounded_percent_along(&self, percent: f64) -> Pt2D { Pt2D::new( self.pt1().x() + percent * (self.pt2().x() - self.pt1().x()), self.pt1().y() + percent * (self.pt2().y() - self.pt1().y()), ) } + pub fn percent_along(&self, percent: f64) -> Pt2D { + assert!(percent >= 0.0 && percent <= 1.0); + self.unbounded_percent_along(percent) + } pub fn unbounded_dist_along(&self, dist: Distance) -> Pt2D { let len = self.length(); @@ -162,6 +166,25 @@ impl Line { None } } + pub fn percent_along_of_point(&self, pt: Pt2D) -> Option { + let dist = self.dist_along_of_point(pt)?; + Some(dist / self.length()) + } + + // Returns a point on the line segment. + pub fn project_pt(&self, pt: Pt2D) -> Pt2D { + let line: geo::LineString = vec![ + geo::Point::new(self.0.x(), self.0.y()), + geo::Point::new(self.1.x(), self.1.y()), + ] + .into(); + match line.closest_point(&geo::Point::new(pt.x(), pt.y())) { + geo::Closest::Intersection(hit) | geo::Closest::SinglePoint(hit) => { + Pt2D::new(hit.x(), hit.y()) + } + geo::Closest::Indeterminate => unreachable!(), + } + } } impl fmt::Display for Line {