Skip to content

Instantly share code, notes, and snippets.

@cemelo
Created April 19, 2020 18:07
Show Gist options
  • Save cemelo/213fcccad2605c9c4e61cd85355a59eb to your computer and use it in GitHub Desktop.
Save cemelo/213fcccad2605c9c4e61cd85355a59eb to your computer and use it in GitHub Desktop.
[package]
name = "canvas_nanovg"
version = "0.1.0"
authors = ["Patrick Walton <pcwalton@mimiga.net>"]
edition = "2018"
[dependencies]
arrayvec = "0.5"
foreign-types = "0.3"
font-kit = "0.6"
gl = "0.14"
metal = "0.17"
sdl2 = "0.33"
sdl2-sys = "0.33"
[dependencies.image]
version = "0.23"
default-features = false
features = ["png"]
[dependencies.log]
version = "0.4"
features = ["release_max_level_info"]
[dependencies.pathfinder_canvas]
path = "../../canvas"
features = ["pf-text"]
[dependencies.pathfinder_color]
path = "../../color"
[dependencies.pathfinder_content]
path = "../../content"
[dependencies.pathfinder_geometry]
path = "../../geometry"
[dependencies.pathfinder_gl]
path = "../../gl"
[dependencies.pathfinder_metal]
path = "../../metal"
[dependencies.pathfinder_gpu]
path = "../../gpu"
[dependencies.pathfinder_renderer]
path = "../../renderer"
[dependencies.pathfinder_resources]
path = "../../resources"
[dependencies.pathfinder_simd]
path = "../../simd"
[target.'cfg(not(windows))'.dependencies]
jemallocator = "0.3"
// pathfinder/examples/canvas_nanovg/src/main.rs
//
// Copyright © 2020 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use arrayvec::ArrayVec;
use font_kit::handle::Handle;
use font_kit::sources::mem::MemSource;
use image;
use metal::{CAMetalLayer, CoreAnimationLayerRef};
use pathfinder_canvas::{Canvas, CanvasFontContext, CanvasRenderingContext2D, LineJoin, Path2D};
use pathfinder_canvas::{TextAlign, TextBaseline};
use pathfinder_color::{rgbau, rgbf, rgbu, ColorF, ColorU};
use pathfinder_content::fill::FillRule;
use pathfinder_content::gradient::Gradient;
use pathfinder_content::outline::ArcDirection;
use pathfinder_content::pattern::{Image, Pattern};
use pathfinder_content::stroke::LineCap;
use pathfinder_geometry::angle;
use pathfinder_geometry::line_segment::LineSegment2F;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::transform2d::Transform2F;
use pathfinder_geometry::util;
use pathfinder_geometry::vector::{vec2f, vec2i, Vector2F};
use pathfinder_gl::{GLDevice, GLVersion};
use pathfinder_metal::MetalDevice;
use pathfinder_renderer::concurrent::rayon::RayonExecutor;
use pathfinder_renderer::concurrent::scene_proxy::SceneProxy;
use pathfinder_renderer::gpu::options::{DestFramebuffer, RendererOptions};
use pathfinder_renderer::gpu::renderer::Renderer;
use pathfinder_renderer::options::BuildOptions;
use pathfinder_resources::fs::FilesystemResourceLoader;
use pathfinder_resources::ResourceLoader;
use pathfinder_simd::default::F32x2;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::video::GLProfile;
use sdl2_sys::SDL_RenderGetMetalLayer;
use std::collections::VecDeque;
use std::f32::consts::PI;
use std::iter;
use std::sync::Arc;
use std::time::Instant;
#[cfg(not(windows))]
use jemallocator;
use sdl2::hint;
#[cfg(not(windows))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
const PI_2: f32 = PI * 2.0;
const FRAC_PI_2_3: f32 = PI * 2.0 / 3.0;
const WINDOW_WIDTH: i32 = 1024;
const WINDOW_HEIGHT: i32 = WINDOW_WIDTH * 3 / 4;
const GRAPH_WIDTH: f32 = 200.0;
const GRAPH_HEIGHT: f32 = 35.0;
const GRAPH_HISTORY_COUNT: usize = 100;
static FONT_NAME_REGULAR: &'static str = "Roboto-Regular";
static FONT_NAME_BOLD: &'static str = "Roboto-Bold";
static FONT_NAME_EMOJI: &'static str = "NotoEmoji";
static PARAGRAPH_TEXT: &'static str = "This is a longer chunk of text.
I would have used lorem ipsum, but she was busy jumping over the lazy dog with the fox and all \
the men who came to the aid of the party. 🎉";
static HOVER_TEXT: &'static str = "Hover your mouse over the text to see the calculated caret \
position.";
fn render_demo(
context: &mut CanvasRenderingContext2D,
mouse_position: Vector2F,
window_size: Vector2F,
time: f32,
hidpi_factor: f32,
data: &DemoData,
) {
draw_eyes(
context,
RectF::new(vec2f(window_size.x() - 250.0, 50.0), vec2f(150.0, 100.0)),
mouse_position,
time,
);
draw_paragraph(
context,
vec2f(window_size.x() - 450.0, 50.0),
150.0,
mouse_position,
);
draw_graph(
context,
RectF::new(window_size * vec2f(0.0, 0.5), window_size * vec2f(1.0, 0.5)),
time,
);
draw_color_wheel(
context,
RectF::new(window_size - vec2f(300.0, 300.0), vec2f(250.0, 250.0)),
time,
hidpi_factor,
);
draw_lines(
context,
RectF::new(vec2f(120.0, window_size.y() - 50.0), vec2f(600.0, 50.0)),
time,
);
draw_widths(context, vec2f(10.0, 50.0), 30.0);
draw_caps(context, RectF::new(vec2f(10.0, 300.0), vec2f(30.0, 40.0)));
draw_clip(context, vec2f(50.0, window_size.y() - 80.0), time);
context.save();
// Draw widgets.
draw_window(
context,
"Widgets & Stuff",
RectF::new(vec2f(50.0, 50.0), vec2f(300.0, 400.0)),
hidpi_factor,
);
let mut position = vec2f(60.0, 95.0);
draw_search_box(
context,
"Search",
RectF::new(position, vec2f(280.0, 25.0)),
hidpi_factor,
);
position += vec2f(0.0, 40.0);
draw_dropdown(context, "Effects", RectF::new(position, vec2f(280.0, 28.0)));
let popup_position = position + vec2f(0.0, 14.0);
position += vec2f(0.0, 45.0);
// Draw login form.
draw_label(context, "Login", RectF::new(position, vec2f(280.0, 20.0)));
position += vec2f(0.0, 25.0);
draw_text_edit_box(
context,
"E-mail address",
RectF::new(position, vec2f(280.0, 28.0)),
hidpi_factor,
);
position += vec2f(0.0, 35.0);
draw_text_edit_box(
context,
"Password",
RectF::new(position, vec2f(280.0, 28.0)),
hidpi_factor,
);
position += vec2f(0.0, 38.0);
draw_check_box(
context,
"Remember me",
RectF::new(position, vec2f(140.0, 28.0)),
hidpi_factor,
);
draw_button(
context,
Some("🚪"),
"Sign In",
RectF::new(position + vec2f(138.0, 0.0), vec2f(140.0, 28.0)),
rgbu(0, 96, 128),
);
position += vec2f(0.0, 45.0);
// Draw slider form.
draw_label(
context,
"Diameter",
RectF::new(position, vec2f(280.0, 20.0)),
);
position += vec2f(0.0, 25.0);
draw_numeric_edit_box(
context,
"123.00",
"px",
RectF::new(position + vec2f(180.0, 0.0), vec2f(100.0, 28.0)),
hidpi_factor,
);
draw_slider(
context,
0.4,
RectF::new(position, vec2f(170.0, 28.0)),
hidpi_factor,
);
position += vec2f(0.0, 55.0);
// Draw dialog box buttons.
draw_button(
context,
Some("️❌"),
"Delete",
RectF::new(position, vec2f(160.0, 28.0)),
rgbu(128, 16, 8),
);
draw_button(
context,
None,
"Cancel",
RectF::new(position + vec2f(170.0, 0.0), vec2f(110.0, 28.0)),
rgbau(0, 0, 0, 0),
);
// Draw thumbnails.
draw_thumbnails(
context,
RectF::new(vec2f(365.0, popup_position.y() - 30.0), vec2f(160.0, 300.0)),
time,
hidpi_factor,
12,
&data.image,
);
context.restore();
}
fn draw_eyes(
context: &mut CanvasRenderingContext2D,
rect: RectF,
mouse_position: Vector2F,
time: f32,
) {
let eyes_radii = rect.size() * vec2f(0.23, 0.5);
let eyes_left_position = rect.origin() + eyes_radii;
let eyes_right_position = rect.origin() + vec2f(rect.width() - eyes_radii.x(), eyes_radii.y());
let eyes_center = f32::min(eyes_radii.x(), eyes_radii.y()) * 0.5;
let blink = 1.0 - f32::powf((time * 0.5).sin(), 200.0) * 0.8;
let mut gradient = Gradient::linear(
LineSegment2F::new(
vec2f(0.0, rect.height() * 0.5),
rect.size() * vec2f(0.1, 1.0),
) + rect.origin(),
);
gradient.add_color_stop(rgbau(0, 0, 0, 32), 0.0);
gradient.add_color_stop(rgbau(0, 0, 0, 16), 1.0);
let mut path = Path2D::new();
path.ellipse(
eyes_left_position + vec2f(3.0, 16.0),
eyes_radii,
0.0,
0.0,
PI_2,
);
path.ellipse(
eyes_right_position + vec2f(3.0, 16.0),
eyes_radii,
0.0,
0.0,
PI_2,
);
context.set_fill_style(gradient);
context.fill_path(path, FillRule::Winding);
let mut gradient = Gradient::linear(
LineSegment2F::new(
vec2f(0.0, rect.height() * 0.25),
rect.size() * vec2f(0.1, 1.0),
) + rect.origin(),
);
gradient.add_color_stop(rgbu(220, 220, 220), 0.0);
gradient.add_color_stop(rgbu(128, 128, 128), 1.0);
let mut path = Path2D::new();
path.ellipse(eyes_left_position, eyes_radii, 0.0, 0.0, PI_2);
path.ellipse(eyes_right_position, eyes_radii, 0.0, 0.0, PI_2);
context.set_fill_style(gradient);
context.fill_path(path, FillRule::Winding);
let mut delta = (mouse_position - eyes_right_position) / (eyes_radii * 10.0);
let distance = delta.length();
if distance > 1.0 {
delta *= 1.0 / distance;
}
delta *= eyes_radii * vec2f(0.4, 0.5);
let mut path = Path2D::new();
path.ellipse(
eyes_left_position + delta + vec2f(0.0, eyes_radii.y() * 0.25 * (1.0 - blink)),
vec2f(eyes_center, eyes_center * blink),
0.0,
0.0,
PI_2,
);
path.ellipse(
eyes_right_position + delta + vec2f(0.0, eyes_radii.y() * 0.25 * (1.0 - blink)),
vec2f(eyes_center, eyes_center * blink),
0.0,
0.0,
PI_2,
);
context.set_fill_style(rgbu(32, 32, 32));
context.fill_path(path, FillRule::Winding);
let gloss_position = eyes_left_position - eyes_radii * vec2f(0.25, 0.5);
let gloss_radii = F32x2::new(0.1, 0.75) * F32x2::splat(eyes_radii.x());
let mut gloss = Gradient::radial(gloss_position, gloss_radii);
gloss.add_color_stop(rgbau(255, 255, 255, 128), 0.0);
gloss.add_color_stop(rgbau(255, 255, 255, 0), 1.0);
context.set_fill_style(gloss);
let mut path = Path2D::new();
path.ellipse(eyes_left_position, eyes_radii, 0.0, 0.0, PI_2);
context.fill_path(path, FillRule::Winding);
let gloss_position = eyes_right_position - eyes_radii * vec2f(0.25, 0.5);
let mut gloss = Gradient::radial(gloss_position, gloss_radii);
gloss.add_color_stop(rgbau(255, 255, 255, 128), 0.0);
gloss.add_color_stop(rgbau(255, 255, 255, 0), 1.0);
context.set_fill_style(gloss);
let mut path = Path2D::new();
path.ellipse(eyes_right_position, eyes_radii, 0.0, 0.0, PI_2);
context.fill_path(path, FillRule::Winding);
}
fn draw_paragraph(
context: &mut CanvasRenderingContext2D,
origin: Vector2F,
line_width: f32,
mouse_position: Vector2F,
) {
const MAIN_LINE_HEIGHT: f32 = 24.0;
context.save();
context.set_font(&[FONT_NAME_REGULAR, FONT_NAME_EMOJI][..]);
context.set_font_size(18.0);
context.set_fill_style(ColorU::white());
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Alphabetic);
let main_text = MultilineTextBox::new(
context,
PARAGRAPH_TEXT,
origin + vec2f(0.0, 24.0),
line_width,
);
let main_text_hit_location = main_text.hit_test(context, mouse_position);
for (main_text_line_index, main_text_line) in main_text.lines.iter().enumerate() {
let bg_alpha = match main_text_hit_location {
Some(ref main_text_hit_location)
if main_text_hit_location.line_index == main_text_line_index as u32 =>
{
64
}
_ => 16,
};
main_text_line.draw(context, rgbau(255, 255, 255, bg_alpha), ColorU::white());
}
if let Some(text_location) = main_text_hit_location {
let caret_position = main_text.char_position(context, text_location);
context.set_fill_style(rgbau(255, 192, 0, 255));
context.fill_rect(RectF::new(caret_position, vec2f(1.0, MAIN_LINE_HEIGHT)));
let line_bounds = main_text.lines[text_location.line_index as usize].bounds();
let gutter_origin = line_bounds.origin() + vec2f(-10.0, MAIN_LINE_HEIGHT * 0.5);
context.set_font_size(12.0);
context.set_text_align(TextAlign::Right);
context.set_text_baseline(TextBaseline::Middle);
context.set_fill_style(rgbau(255, 192, 0, 255));
let gutter_text = format!("{}", text_location.line_index + 1);
let gutter_text_metrics = context.measure_text(&gutter_text);
let gutter_text_bounds = RectF::from_points(
vec2f(
gutter_text_metrics.actual_bounding_box_left,
-gutter_text_metrics.font_bounding_box_ascent,
),
vec2f(
gutter_text_metrics.actual_bounding_box_right,
-gutter_text_metrics.font_bounding_box_descent,
),
);
let gutter_path_bounds = gutter_text_bounds.dilate(vec2f(4.0, 2.0));
let gutter_path_radius = gutter_path_bounds.width() * 0.5 - 1.0;
let path = create_rounded_rect_path(gutter_path_bounds + gutter_origin, gutter_path_radius);
context.fill_path(path, FillRule::Winding);
context.set_fill_style(rgbau(32, 32, 32, 255));
context.fill_text(&gutter_text, gutter_origin);
}
// Fade out the tooltip when close to it.
context.set_font_size(11.0);
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Alphabetic);
let tooltip_origin = main_text.bounds.lower_left() + vec2f(0.0, 38.0);
let tooltip = MultilineTextBox::new(context, HOVER_TEXT, tooltip_origin, 150.0);
let mouse_vector = mouse_position.clamp(tooltip.bounds.origin(), tooltip.bounds.lower_right())
- mouse_position;
context.set_global_alpha(util::clamp(mouse_vector.length() / 30.0, 0.0, 1.0));
// Draw tooltip background.
context.set_fill_style(rgbau(220, 220, 220, 255));
let mut path = create_rounded_rect_path(tooltip.bounds.dilate(2.0), 3.0);
path.move_to(vec2f(
tooltip.bounds.center().x(),
tooltip.bounds.origin_y() - 10.0,
));
path.line_to(vec2f(
tooltip.bounds.center().x() + 7.0,
tooltip.bounds.origin_y() + 1.0,
));
path.line_to(vec2f(
tooltip.bounds.center().x() - 7.0,
tooltip.bounds.origin_y() + 1.0,
));
context.fill_path(path, FillRule::Winding);
// Draw tooltip.
context.set_fill_style(rgbau(0, 0, 0, 220));
tooltip.draw(context, rgbau(0, 0, 0, 0), rgbau(0, 0, 0, 220));
context.restore();
}
// This is nowhere near correct line layout, but it suffices to more or less match what NanoVG
// does.
struct MultilineTextBox {
lines: Vec<Line>,
bounds: RectF,
}
struct Line {
words: Vec<Word>,
origin: Vector2F,
ascent: f32,
descent: f32,
width: f32,
max_width: f32,
}
struct Word {
text: String,
origin_x: f32,
}
#[derive(Clone, Copy, Debug)]
struct TextLocation {
line_index: u32,
line_location: LineLocation,
}
#[derive(Clone, Copy, Debug)]
struct LineLocation {
word_index: u32,
char_index: u32,
}
impl MultilineTextBox {
fn new(
context: &mut CanvasRenderingContext2D,
text: &str,
mut origin: Vector2F,
max_width: f32,
) -> MultilineTextBox {
const LINE_SPACING: f32 = 3.0;
let a_b_measure = context.measure_text("A B");
let space_width = a_b_measure.width - context.measure_text("AB").width;
let line_height =
a_b_measure.em_height_ascent - a_b_measure.em_height_descent + LINE_SPACING;
let mut text: VecDeque<VecDeque<_>> = text
.split('\n')
.map(|paragraph| paragraph.split(' ').map(|word| word.to_owned()).collect())
.collect();
let mut lines = vec![];
let mut bounds = None;
while let Some(mut paragraph) = text.pop_front() {
while !paragraph.is_empty() {
let mut line = Line::new(origin, max_width);
line.layout(context, &mut paragraph, space_width);
origin += vec2f(0.0, line_height);
match bounds {
None => bounds = Some(line.bounds()),
Some(ref mut bounds) => *bounds = bounds.union_rect(line.bounds()),
}
lines.push(line);
}
}
MultilineTextBox {
bounds: bounds.unwrap_or_default(),
lines,
}
}
fn draw(&self, context: &mut CanvasRenderingContext2D, bg_color: ColorU, fg_color: ColorU) {
for line in &self.lines {
line.draw(context, bg_color, fg_color);
}
}
fn hit_test(
&self,
context: &CanvasRenderingContext2D,
mouse_position: Vector2F,
) -> Option<TextLocation> {
for (line_index, line) in self.lines.iter().enumerate() {
if line.bounds().contains_point(mouse_position) {
if let Some(line_location) = line.hit_test(context, mouse_position) {
return Some(TextLocation {
line_index: line_index as u32,
line_location,
});
}
}
}
None
}
fn char_position(
&self,
context: &CanvasRenderingContext2D,
text_location: TextLocation,
) -> Vector2F {
let line = &self.lines[text_location.line_index as usize];
line.bounds().origin()
+ vec2f(
line.char_position(context, text_location.line_location),
0.0,
)
}
}
impl Line {
fn new(origin: Vector2F, max_width: f32) -> Line {
Line {
words: vec![],
origin,
ascent: 0.0,
descent: 0.0,
width: 0.0,
max_width,
}
}
fn layout(
&mut self,
context: &mut CanvasRenderingContext2D,
text: &mut VecDeque<String>,
space_width: f32,
) {
while let Some(word) = text.pop_front() {
let mut word_origin_x = self.width;
if self.width > 0.0 {
word_origin_x += space_width;
}
let word_metrics = context.measure_text(&word);
let new_line_width = word_origin_x + word_metrics.width;
if self.width != 0.0 && new_line_width > self.max_width {
text.push_front(word);
return;
}
self.words.push(Word {
text: word,
origin_x: word_origin_x,
});
self.width = new_line_width;
self.ascent = self.ascent.max(word_metrics.em_height_ascent);
self.descent = self.descent.min(word_metrics.em_height_descent);
}
}
fn draw(&self, context: &mut CanvasRenderingContext2D, bg_color: ColorU, fg_color: ColorU) {
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Alphabetic);
if !bg_color.is_fully_transparent() {
context.set_fill_style(bg_color);
context.fill_rect(self.bounds());
}
context.set_fill_style(fg_color);
for word in &self.words {
context.fill_text(&word.text, self.origin + vec2f(word.origin_x, 0.0));
}
}
fn bounds(&self) -> RectF {
RectF::new(
self.origin - vec2f(0.0, self.ascent),
vec2f(self.width, self.ascent - self.descent),
)
}
fn hit_test(
&self,
context: &CanvasRenderingContext2D,
mut mouse_position: Vector2F,
) -> Option<LineLocation> {
let bounds = self.bounds();
mouse_position -= bounds.origin();
if mouse_position.y() < 0.0 || mouse_position.y() > bounds.height() {
return None;
}
// FIXME(pcwalton): This doesn't quite handle spaces correctly.
for (word_index, word) in self.words.iter().enumerate().rev() {
if word.origin_x <= mouse_position.x() {
return Some(LineLocation {
word_index: word_index as u32,
char_index: word.hit_test(context, mouse_position.x()),
});
}
}
None
}
fn char_position(
&self,
context: &CanvasRenderingContext2D,
line_location: LineLocation,
) -> f32 {
let word = &self.words[line_location.word_index as usize];
word.origin_x + word.char_position(context, line_location.char_index)
}
}
impl Word {
fn hit_test(&self, context: &CanvasRenderingContext2D, position_x: f32) -> u32 {
let (mut char_start_x, mut prev_char_index) = (self.origin_x, 0);
for char_index in self
.text
.char_indices()
.map(|(index, _)| index)
.skip(1)
.chain(iter::once(self.text.len()))
{
let char_end_x = self.origin_x + context.measure_text(&self.text[0..char_index]).width;
if position_x <= (char_start_x + char_end_x) * 0.5 {
return prev_char_index;
}
char_start_x = char_end_x;
prev_char_index = char_index as u32;
}
return self.text.len() as u32;
}
fn char_position(&self, context: &CanvasRenderingContext2D, char_index: u32) -> f32 {
context
.measure_text(&self.text[0..(char_index as usize)])
.width
}
}
fn draw_graph(context: &mut CanvasRenderingContext2D, rect: RectF, time: f32) {
let sample_spread = rect.width() / 5.0;
let samples = [
(1.0 + f32::sin(time * 1.2345 + f32::cos(time * 0.33457) * 0.44)) * 0.5,
(1.0 + f32::sin(time * 0.68363 + f32::cos(time * 1.30) * 1.55)) * 0.5,
(1.0 + f32::sin(time * 1.1642 + f32::cos(time * 0.33457) * 1.24)) * 0.5,
(1.0 + f32::sin(time * 0.56345 + f32::cos(time * 1.63) * 0.14)) * 0.5,
(1.0 + f32::sin(time * 1.6245 + f32::cos(time * 0.254) * 0.3)) * 0.5,
(1.0 + f32::sin(time * 0.345 + f32::cos(time * 0.03) * 0.6)) * 0.5,
];
let sample_scale = vec2f(sample_spread, rect.height() * 0.8);
let sample_points: ArrayVec<[Vector2F; 6]> = samples
.iter()
.enumerate()
.map(|(index, &sample)| rect.origin() + vec2f(index as f32, sample) * sample_scale)
.collect();
// Draw graph background.
let mut background = Gradient::linear(
LineSegment2F::new(vec2f(0.0, 0.0), vec2f(0.0, rect.height())) + rect.origin(),
);
background.add_color_stop(rgbau(0, 160, 192, 0), 0.0);
background.add_color_stop(rgbau(0, 160, 192, 64), 1.0);
context.set_fill_style(background);
let mut path = create_graph_path(&sample_points, sample_spread, Vector2F::zero());
path.line_to(rect.lower_right());
path.line_to(rect.lower_left());
context.fill_path(path, FillRule::Winding);
// Draw graph line shadow.
context.set_stroke_style(rgbau(0, 0, 0, 32));
context.set_line_width(3.0);
let path = create_graph_path(&sample_points, sample_spread, vec2f(0.0, 2.0));
context.stroke_path(path);
// Draw graph line.
context.set_stroke_style(rgbu(0, 160, 192));
context.set_line_width(3.0);
let path = create_graph_path(&sample_points, sample_spread, Vector2F::zero());
context.stroke_path(path);
// Draw sample position highlights.
for &sample_point in &sample_points {
let gradient_center = sample_point + vec2f(0.0, 2.0);
let mut background = Gradient::radial(gradient_center, F32x2::new(3.0, 8.0));
background.add_color_stop(rgbau(0, 0, 0, 32), 0.0);
background.add_color_stop(rgbau(0, 0, 0, 0), 1.0);
context.set_fill_style(background);
context.fill_rect(RectF::new(
sample_point + vec2f(-10.0, -10.0 + 2.0),
vec2f(20.0, 20.0),
));
}
// Draw sample positions.
context.set_fill_style(rgbu(0, 160, 192));
let mut path = Path2D::new();
for &sample_point in &sample_points {
path.ellipse(sample_point, vec2f(4.0, 4.0), 0.0, 0.0, PI_2);
}
context.fill_path(path, FillRule::Winding);
context.set_fill_style(rgbu(220, 220, 220));
let mut path = Path2D::new();
for &sample_point in &sample_points {
path.ellipse(sample_point, vec2f(2.0, 2.0), 0.0, 0.0, PI_2);
}
context.fill_path(path, FillRule::Winding);
// Reset state.
context.set_line_width(1.0);
}
fn draw_color_wheel(
context: &mut CanvasRenderingContext2D,
rect: RectF,
time: f32,
hidpi_factor: f32,
) {
let hue = (time * 0.12).sin() * PI_2;
context.save();
let center = rect.center();
let outer_radius = f32::min(rect.width(), rect.height()) * 0.5 - 5.0;
let inner_radius = outer_radius - 20.0;
// Half a pixel arc length in radians.
let half_arc_len = 0.5 / outer_radius;
// Draw outer circle.
for segment in 0..6 {
let start_angle = segment as f32 / 6.0 * PI_2 - half_arc_len;
let end_angle = (segment + 1) as f32 / 6.0 * PI_2 + half_arc_len;
let line = LineSegment2F::new(
vec2f(f32::cos(start_angle), f32::sin(start_angle)),
vec2f(f32::cos(end_angle), f32::sin(end_angle)),
);
let scale = util::lerp(inner_radius, outer_radius, 0.5);
let mut gradient = Gradient::linear(line * scale + center);
let start_color = ColorF::from_hsl(start_angle, 1.0, 0.55).to_u8();
let end_color = ColorF::from_hsl(end_angle, 1.0, 0.55).to_u8();
gradient.add_color_stop(start_color, 0.0);
gradient.add_color_stop(end_color, 1.0);
context.set_fill_style(gradient);
let mut path = Path2D::new();
path.arc(
center,
inner_radius,
start_angle,
end_angle,
ArcDirection::CW,
);
path.arc(
center,
outer_radius,
end_angle,
start_angle,
ArcDirection::CCW,
);
path.close_path();
context.fill_path(path, FillRule::Winding);
}
// Stroke outer circle.
context.set_stroke_style(rgbau(0, 0, 0, 64));
context.set_line_width(1.0);
let mut path = Path2D::new();
path.ellipse(center, inner_radius - 0.5, 0.0, 0.0, PI_2);
path.ellipse(center, outer_radius + 0.5, 0.0, 0.0, PI_2);
context.stroke_path(path);
// Prepare to draw the selector.
context.save();
context.translate(center);
context.rotate(hue);
// Draw marker.
context.set_shadow_blur(4.0 * hidpi_factor);
context.set_shadow_color(rgbu(0, 0, 0));
context.set_shadow_offset(vec2f(0.0, 0.0));
context.set_stroke_style(rgbau(255, 255, 255, 192));
context.set_line_width(2.0);
context.stroke_rect(RectF::new(
vec2f(inner_radius - 1.0, -3.0),
vec2f(outer_radius - inner_radius + 2.0, 6.0),
));
context.set_shadow_color(ColorU::transparent_black());
// Draw center triangle.
let triangle_radius = inner_radius - 6.0;
let triangle_vertex_a = vec2f(triangle_radius, 0.0);
let triangle_vertex_b = vec2f(FRAC_PI_2_3.cos(), FRAC_PI_2_3.sin()) * triangle_radius;
let triangle_vertex_c = vec2f((-FRAC_PI_2_3).cos(), (-FRAC_PI_2_3).sin()) * triangle_radius;
let mut gradient_0 = Gradient::linear_from_points(triangle_vertex_a, triangle_vertex_b);
gradient_0.add_color_stop(ColorF::from_hsl(hue, 1.0, 0.5).to_u8(), 0.0);
gradient_0.add_color_stop(ColorU::white(), 1.0);
let mut gradient_1 = Gradient::linear_from_points(
triangle_vertex_a.lerp(triangle_vertex_b, 0.5),
triangle_vertex_c,
);
gradient_1.add_color_stop(ColorU::transparent_black(), 0.0);
gradient_1.add_color_stop(ColorU::black(), 1.0);
let mut path = Path2D::new();
path.move_to(triangle_vertex_a);
path.line_to(triangle_vertex_b);
path.line_to(triangle_vertex_c);
path.close_path();
context.set_fill_style(gradient_0);
context.fill_path(path.clone(), FillRule::Winding);
context.set_fill_style(gradient_1);
context.fill_path(path.clone(), FillRule::Winding);
context.set_stroke_style(rgbau(0, 0, 0, 64));
context.stroke_path(path);
// Stroke the selection circle on the triangle.
let selection_circle_center =
vec2f(FRAC_PI_2_3.cos(), FRAC_PI_2_3.sin()) * triangle_radius * vec2f(0.3, 0.4);
context.set_stroke_style(rgbau(255, 255, 255, 192));
context.set_line_width(2.0);
let mut path = Path2D::new();
path.ellipse(selection_circle_center, vec2f(5.0, 5.0), 0.0, 0.0, PI_2);
context.stroke_path(path);
// Fill the selection circle.
let mut gradient = Gradient::radial(selection_circle_center, F32x2::new(7.0, 9.0));
gradient.add_color_stop(rgbau(0, 0, 0, 64), 0.0);
gradient.add_color_stop(rgbau(0, 0, 0, 0), 1.0);
context.set_fill_style(gradient);
let mut path = Path2D::new();
path.rect(RectF::new(
selection_circle_center - vec2f(20.0, 20.0),
vec2f(40.0, 40.0),
));
path.ellipse(selection_circle_center, vec2f(7.0, 7.0), 0.0, 0.0, PI_2);
context.fill_path(path, FillRule::EvenOdd);
context.restore();
context.restore();
}
fn draw_lines(context: &mut CanvasRenderingContext2D, rect: RectF, time: f32) {
const PADDING: f32 = 5.0;
let spacing = rect.width() / 9.0 - PADDING * 2.0;
context.save();
let points = [
vec2f(
-spacing * 0.25 + f32::cos(time * 0.3) * spacing * 0.5,
f32::sin(time * 0.3) * spacing * 0.5,
),
vec2f(-spacing * 0.25, 0.0),
vec2f(spacing * 0.25, 0.0),
vec2f(
spacing * 0.25 + f32::cos(time * -0.3) * spacing * 0.5,
f32::sin(time * -0.3) * spacing * 0.5,
),
];
for (cap_index, &cap) in [LineCap::Butt, LineCap::Round, LineCap::Square]
.iter()
.enumerate()
{
for (join_index, &join) in [LineJoin::Miter, LineJoin::Round, LineJoin::Bevel]
.iter()
.enumerate()
{
let origin = rect.origin()
+ vec2f(0.5, -0.5) * spacing
+ vec2f(
(cap_index * 3 + join_index) as f32 / 9.0 * rect.width(),
0.0,
)
+ PADDING;
context.set_line_cap(cap);
context.set_line_join(join);
context.set_line_width(spacing * 0.3);
context.set_stroke_style(rgbau(0, 0, 0, 160));
let mut path = Path2D::new();
path.move_to(points[0] + origin);
path.line_to(points[1] + origin);
path.line_to(points[2] + origin);
path.line_to(points[3] + origin);
context.stroke_path(path.clone());
context.set_line_cap(LineCap::Butt);
context.set_line_join(LineJoin::Bevel);
context.set_line_width(1.0);
context.set_stroke_style(rgbu(0, 192, 255));
context.stroke_path(path);
}
}
context.restore();
}
fn draw_widths(context: &mut CanvasRenderingContext2D, mut origin: Vector2F, width: f32) {
context.save();
context.set_stroke_style(rgbau(0, 0, 0, 255));
for index in 0..20 {
context.set_line_width((index as f32 + 0.5) * 0.1);
let mut path = Path2D::new();
path.move_to(origin);
path.line_to(origin + vec2f(1.0, 0.3) * width);
context.stroke_path(path);
origin += vec2f(0.0, 10.0);
}
context.restore();
}
fn draw_caps(context: &mut CanvasRenderingContext2D, rect: RectF) {
const LINE_WIDTH: f32 = 8.0;
context.save();
context.set_fill_style(rgbau(255, 255, 255, 32));
context.fill_rect(rect.dilate(vec2f(LINE_WIDTH / 2.0, 0.0)));
context.fill_rect(rect);
context.set_line_width(LINE_WIDTH);
for (cap_index, &cap) in [LineCap::Butt, LineCap::Round, LineCap::Square]
.iter()
.enumerate()
{
context.set_line_cap(cap);
context.set_stroke_style(ColorU::black());
let offset = cap_index as f32 * 10.0 + 5.0;
let mut path = Path2D::new();
path.move_to(rect.origin() + vec2f(0.0, offset));
path.line_to(rect.upper_right() + vec2f(0.0, offset));
context.stroke_path(path);
}
context.restore();
}
fn draw_clip(context: &mut CanvasRenderingContext2D, origin: Vector2F, time: f32) {
context.save();
// Draw first rect.
let original_transform = context.transform();
let transform_a = original_transform
* Transform2F::from_rotation(angle::angle_from_degrees(5.0)).translate(origin);
context.set_transform(&transform_a);
context.set_fill_style(rgbu(255, 0, 0));
let mut clip_path_a = Path2D::new();
let clip_rect_a = RectF::new(vec2f(-20.0, -20.0), vec2f(60.0, 40.0));
clip_path_a.rect(clip_rect_a);
context.fill_path(clip_path_a, FillRule::Winding);
// Draw second rectangle with no clip.
let transform_b = transform_a * Transform2F::from_rotation(time).translate(vec2f(40.0, 0.0));
context.set_transform(&transform_b);
context.set_fill_style(rgbau(255, 128, 0, 64));
let fill_rect = RectF::new(vec2f(-20.0, -10.0), vec2f(60.0, 30.0));
context.fill_rect(fill_rect);
// Draw second rectangle with clip.
let mut clip_path_b = Path2D::new();
let clip_rect_b = (transform_b.inverse() * transform_a * clip_rect_a)
.intersection(fill_rect)
.unwrap_or_default();
clip_path_b.rect(clip_rect_b);
context.clip_path(clip_path_b, FillRule::Winding);
context.set_fill_style(rgbu(255, 128, 0));
context.fill_rect(fill_rect);
context.restore();
}
fn draw_window(
context: &mut CanvasRenderingContext2D,
title: &str,
rect: RectF,
hidpi_factor: f32,
) {
const CORNER_RADIUS: f32 = 3.0;
context.save();
// Draw window with shadow.
context.set_fill_style(rgbau(28, 30, 34, 160));
context.set_shadow_blur(10.0 * hidpi_factor);
context.set_shadow_offset(vec2f(0.0, 2.0));
context.set_shadow_color(rgbau(0, 0, 0, 128));
context.fill_path(
create_rounded_rect_path(rect, CORNER_RADIUS),
FillRule::Winding,
);
context.set_shadow_color(rgbau(0, 0, 0, 0));
// Header.
let mut header_gradient =
Gradient::linear(LineSegment2F::new(Vector2F::zero(), vec2f(0.0, 15.0)) + rect.origin());
header_gradient.add_color_stop(rgbau(255, 255, 255, 8), 0.0);
header_gradient.add_color_stop(rgbau(0, 0, 0, 16), 1.0);
context.set_fill_style(header_gradient);
context.fill_path(
create_rounded_rect_path(
RectF::new(
rect.origin() + vec2f(1.0, 1.0),
vec2f(rect.width() - 2.0, 30.0),
),
CORNER_RADIUS - 1.0,
),
FillRule::Winding,
);
let mut path = Path2D::new();
path.move_to(rect.origin() + vec2f(0.5, 30.5));
path.line_to(rect.origin() + vec2f(rect.width() - 0.5, 30.5));
context.set_stroke_style(rgbau(0, 0, 0, 32));
context.stroke_path(path);
context.set_font(FONT_NAME_BOLD);
context.set_font_size(15.0);
context.set_text_align(TextAlign::Center);
context.set_text_baseline(TextBaseline::Middle);
context.set_fill_style(rgbau(220, 220, 220, 160));
context.set_shadow_blur(2.0 * hidpi_factor);
context.set_shadow_offset(vec2f(0.0, 1.0));
context.set_shadow_color(rgbu(0, 0, 0));
context.fill_text(title, rect.origin() + vec2f(rect.width() * 0.5, 16.0));
context.restore();
}
fn draw_search_box(
context: &mut CanvasRenderingContext2D,
text: &str,
rect: RectF,
hidpi_factor: f32,
) {
let corner_radius = rect.height() * 0.5 - 1.0;
let path = create_rounded_rect_path(rect, corner_radius);
context.set_fill_style(rgbau(0, 0, 0, 16));
context.fill_path(path.clone(), FillRule::Winding);
context.save();
context.clip_path(path, FillRule::Winding);
let shadow_path = create_rounded_rect_path(rect + vec2f(0.0, 1.5), corner_radius);
context.set_shadow_blur(5.0 * hidpi_factor);
context.set_shadow_offset(vec2f(0.0, 0.0));
context.set_shadow_color(rgbau(0, 0, 0, 92));
context.set_stroke_style(rgbau(0, 0, 0, 92));
context.set_line_width(1.0);
context.stroke_path(shadow_path);
context.restore();
context.set_font_size(rect.height() * 0.5);
context.set_font(FONT_NAME_EMOJI);
context.set_fill_style(rgbau(255, 255, 255, 64));
context.set_text_align(TextAlign::Center);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text("🔍", rect.origin() + Vector2F::splat(rect.height() * 0.55));
context.set_font(FONT_NAME_REGULAR);
context.set_font_size(17.0);
context.set_fill_style(rgbau(255, 255, 255, 32));
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text(text, rect.origin() + vec2f(1.05, 0.5) * rect.height());
context.set_font_size(rect.height() * 0.5);
context.set_font(FONT_NAME_EMOJI);
context.set_text_align(TextAlign::Center);
context.fill_text(
"️❌",
rect.upper_right() + vec2f(-1.0, 1.0) * (rect.height() * 0.55),
);
}
fn draw_dropdown(context: &mut CanvasRenderingContext2D, text: &str, rect: RectF) {
const CORNER_RADIUS: f32 = 4.0;
let mut background_gradient = Gradient::linear_from_points(rect.origin(), rect.lower_left());
background_gradient.add_color_stop(rgbau(255, 255, 255, 16), 0.0);
background_gradient.add_color_stop(rgbau(0, 0, 0, 16), 1.0);
context.set_fill_style(background_gradient);
context.fill_path(
create_rounded_rect_path(rect.contract(1.0), CORNER_RADIUS - 1.0),
FillRule::Winding,
);
context.set_stroke_style(rgbau(0, 0, 0, 48));
context.stroke_path(create_rounded_rect_path(
rect.contract(0.5),
CORNER_RADIUS - 0.5,
));
context.set_font(FONT_NAME_REGULAR);
context.set_font_size(17.0);
context.set_fill_style(rgbau(255, 255, 255, 160));
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text(text, rect.origin() + vec2f(0.3, 0.5) * rect.height());
// Draw chevron. This is a glyph in the original, but I don't want to grab an icon font just
// for this.
context.save();
context.translate(rect.upper_right() + vec2f(-0.5, 0.33) * rect.height());
context.scale(0.1);
context.set_fill_style(rgbau(255, 255, 255, 64));
let mut path = Path2D::new();
path.move_to(vec2f(0.0, 100.0));
path.line_to(vec2f(32.8, 50.0));
path.line_to(vec2f(0.0, 0.0));
path.line_to(vec2f(22.1, 0.0));
path.line_to(vec2f(54.2, 50.0));
path.line_to(vec2f(22.1, 100.0));
path.close_path();
context.fill_path(path, FillRule::Winding);
context.restore();
}
fn draw_label(context: &mut CanvasRenderingContext2D, text: &str, rect: RectF) {
context.set_font(FONT_NAME_REGULAR);
context.set_font_size(15.0);
context.set_fill_style(rgbau(255, 255, 255, 128));
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text(text, rect.origin() + vec2f(0.0, rect.height() * 0.5));
}
fn draw_edit_box(context: &mut CanvasRenderingContext2D, rect: RectF, hidpi_factor: f32) {
const CORNER_RADIUS: f32 = 4.0;
context.save();
let path = create_rounded_rect_path(rect.contract(1.0), CORNER_RADIUS - 1.0);
context.set_fill_style(rgbau(255, 255, 255, 32));
context.fill_path(path.clone(), FillRule::Winding);
context.clip_path(path.clone(), FillRule::Winding);
context.set_line_width(1.0);
context.set_shadow_blur(2.0 * hidpi_factor);
context.set_shadow_color(rgbau(32, 32, 32, 92));
context.set_shadow_offset(vec2f(0.0, 1.0));
context.set_stroke_style(rgbau(32, 32, 32, 92));
context.stroke_path(path);
context.restore();
context.set_stroke_style(rgbau(0, 0, 0, 48));
context.stroke_path(create_rounded_rect_path(
rect.contract(0.5),
CORNER_RADIUS - 0.5,
));
}
fn draw_text_edit_box(
context: &mut CanvasRenderingContext2D,
text: &str,
rect: RectF,
hidpi_factor: f32,
) {
draw_edit_box(context, rect, hidpi_factor);
context.set_font(FONT_NAME_REGULAR);
context.set_font_size(17.0);
context.set_fill_style(rgbau(255, 255, 255, 64));
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text(text, rect.origin() + vec2f(0.3, 0.5) * rect.height());
}
fn draw_numeric_edit_box(
context: &mut CanvasRenderingContext2D,
value: &str,
unit: &str,
rect: RectF,
hidpi_factor: f32,
) {
draw_edit_box(context, rect, hidpi_factor);
context.set_font(FONT_NAME_REGULAR);
context.set_font_size(15.0);
let unit_width = context.measure_text(unit).width;
context.set_fill_style(rgbau(255, 255, 255, 64));
context.set_text_align(TextAlign::Right);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text(unit, rect.upper_right() + vec2f(-0.3, 0.5) * rect.height());
context.set_font_size(17.0);
context.set_fill_style(rgbau(255, 255, 255, 128));
context.set_text_align(TextAlign::Right);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text(
value,
rect.upper_right() + vec2f(-unit_width - rect.height() * 0.5, rect.height() * 0.5),
);
}
fn draw_check_box(
context: &mut CanvasRenderingContext2D,
text: &str,
rect: RectF,
hidpi_factor: f32,
) {
const CORNER_RADIUS: f32 = 3.0;
context.set_font(FONT_NAME_REGULAR);
context.set_font_size(15.0);
context.set_fill_style(rgbau(255, 255, 255, 160));
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text(text, rect.origin() + vec2f(28.0, rect.height() * 0.5));
context.save();
let check_box_rect = RectF::new(
vec2f(rect.origin_x(), rect.center().y().floor() - 9.0),
vec2f(20.0, 20.0),
)
.contract(1.0);
let check_box_path = create_rounded_rect_path(check_box_rect, CORNER_RADIUS);
context.set_fill_style(rgbau(0, 0, 0, 32));
context.fill_path(check_box_path.clone(), FillRule::Winding);
context.clip_path(check_box_path, FillRule::Winding);
context.set_line_width(1.0);
context.set_stroke_style(rgbau(0, 0, 0, 92));
context.set_shadow_color(rgbau(0, 0, 0, 92));
context.set_shadow_blur(1.5 * hidpi_factor);
context.set_shadow_offset(vec2f(0.0, 0.0));
let shadow_path = create_rounded_rect_path(check_box_rect + vec2f(0.0, 1.0), CORNER_RADIUS);
context.stroke_path(shadow_path);
context.restore();
context.set_font(FONT_NAME_EMOJI);
context.set_font_size(17.0);
context.set_fill_style(rgbau(255, 255, 255, 128));
context.set_text_align(TextAlign::Center);
context.fill_text("✔︎", check_box_rect.center());
}
fn draw_button(
context: &mut CanvasRenderingContext2D,
pre_icon: Option<&str>,
text: &str,
rect: RectF,
color: ColorU,
) {
const CORNER_RADIUS: f32 = 4.0;
let path = create_rounded_rect_path(rect.contract(1.0), CORNER_RADIUS - 1.0);
if color != ColorU::transparent_black() {
context.set_fill_style(color);
context.fill_path(path.clone(), FillRule::Winding);
}
let alpha = if color == ColorU::transparent_black() {
16
} else {
32
};
let mut background_gradient = Gradient::linear_from_points(rect.origin(), rect.lower_left());
background_gradient.add_color_stop(rgbau(255, 255, 255, alpha), 0.0);
background_gradient.add_color_stop(rgbau(0, 0, 0, alpha), 1.0);
context.set_fill_style(background_gradient);
context.fill_path(path, FillRule::Winding);
context.set_stroke_style(rgbau(0, 0, 0, 48));
context.stroke_path(create_rounded_rect_path(
rect.contract(0.5),
CORNER_RADIUS - 0.5,
));
context.set_font(FONT_NAME_BOLD);
context.set_font_size(17.0);
let text_width = context.measure_text(text).width;
let icon_width;
match pre_icon {
None => icon_width = 0.0,
Some(icon) => {
context.set_font_size(rect.height() * 0.7);
context.set_font(FONT_NAME_EMOJI);
icon_width = context.measure_text(icon).width + rect.height() * 0.15;
context.set_fill_style(rgbau(255, 255, 255, 96));
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Middle);
context.fill_text(
icon,
rect.center() - vec2f(text_width * 0.5 + icon_width * 0.75, 0.0),
);
}
}
context.set_font(FONT_NAME_BOLD);
context.set_font_size(17.0);
let text_origin = rect.center() + vec2f(icon_width * 0.25 - text_width * 0.5, 0.0);
context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Middle);
context.set_shadow_color(rgbau(0, 0, 0, 160));
context.set_shadow_offset(vec2f(0.0, -1.0));
context.set_shadow_blur(0.0);
context.set_fill_style(rgbau(255, 255, 255, 160));
context.fill_text(text, text_origin);
context.set_shadow_color(ColorU::transparent_black());
}
fn draw_slider(context: &mut CanvasRenderingContext2D, value: f32, rect: RectF, hidpi_factor: f32) {
let (center_y, knob_radius) = (rect.center().y().floor(), (rect.height() * 0.25).floor());
context.save();
// Draw track.
context.save();
let track_rect = RectF::new(
vec2f(rect.origin_x(), center_y - 2.0),
vec2f(rect.width(), 4.0),
);
let track_path = create_rounded_rect_path(track_rect, 2.0);
context.clip_path(track_path.clone(), FillRule::Winding);
context.set_shadow_blur(2.0 * hidpi_factor);
context.set_shadow_color(rgbau(0, 0, 0, 32));
context.set_shadow_offset(vec2f(0.0, 1.0));
context.set_fill_style(rgbau(0, 0, 0, 32));
context.fill_path(track_path, FillRule::Winding);
context.restore();
// Fill knob.
let knob_position = vec2f(rect.origin_x() + (value * rect.width()).floor(), center_y);
let mut background_gradient = Gradient::linear_from_points(
knob_position - vec2f(0.0, knob_radius),
knob_position + vec2f(0.0, knob_radius),
);
background_gradient.add_color_stop(rgbau(255, 255, 255, 16), 0.0);
background_gradient.add_color_stop(rgbau(0, 0, 0, 16), 1.0);
let mut path = Path2D::new();
path.ellipse(knob_position, knob_radius - 1.0, 0.0, 0.0, PI_2);
context.set_fill_style(rgbu(40, 43, 48));
context.set_shadow_blur(6.0 * hidpi_factor);
context.set_shadow_color(rgbau(0, 0, 0, 128));
context.set_shadow_offset(vec2f(0.0, 1.0));
context.fill_path(path.clone(), FillRule::Winding);
context.set_shadow_color(rgbau(0, 0, 0, 0));
context.set_fill_style(background_gradient);
context.fill_path(path, FillRule::Winding);
// Outline knob.
let mut path = Path2D::new();
path.ellipse(knob_position, knob_radius - 0.5, 0.0, 0.0, PI_2);
context.set_stroke_style(rgbau(0, 0, 0, 92));
context.stroke_path(path);
context.restore();
}
fn draw_thumbnails(
context: &mut CanvasRenderingContext2D,
rect: RectF,
time: f32,
hidpi_factor: f32,
image_count: usize,
image: &Image,
) {
const CORNER_RADIUS: f32 = 3.0;
const THUMB_HEIGHT: f32 = 60.0;
const ARROW_Y_POSITION: f32 = 30.5;
const IMAGES_ACROSS: usize = 4;
let stack_height = image_count as f32 * 0.5 * (THUMB_HEIGHT + 10.0) + 10.0;
let scroll_height = rect.height() / stack_height * (rect.height() - 8.0);
let scroll_y = (1.0 + f32::cos(time * 0.5)) * 0.5;
let load_y = (1.0 - f32::cos(time * 0.2)) * 0.5;
let image_y_scale = 1.0 / (image_count as f32 - 1.0);
context.save();
// Draw window.
let mut path = create_rounded_rect_path(rect, CORNER_RADIUS);
path.move_to(rect.origin() + vec2f(-10.0, ARROW_Y_POSITION));
path.line_to(rect.origin() + vec2f(1.0, ARROW_Y_POSITION - 11.0));
path.line_to(rect.origin() + vec2f(1.0, ARROW_Y_POSITION + 11.0));
context.set_fill_style(rgbu(200, 200, 200));
context.set_shadow_blur(20.0 * hidpi_factor);
context.set_shadow_offset(vec2f(0.0, 4.0));
context.set_shadow_color(rgbau(0, 0, 0, 64));
context.fill_path(path, FillRule::Winding);
context.set_shadow_color(rgbau(0, 0, 0, 0));
// Draw images.
context.save();
let mut clip_path = Path2D::new();
clip_path.rect(rect);
context.clip_path(clip_path, FillRule::Winding);
context.translate(vec2f(0.0, -scroll_y * (stack_height - rect.height())));
for image_index in 0..image_count {
let image_origin = rect.origin()
+ vec2f(10.0, 10.0)
+ vec2i(image_index as i32 % 2, image_index as i32 / 2).to_f32()
* (THUMB_HEIGHT + 10.0);
let image_rect = RectF::new(image_origin, Vector2F::splat(THUMB_HEIGHT));
// Draw shadow.
let shadow_path = create_rounded_rect_path(image_rect.dilate(1.0) + vec2f(0.0, 1.0), 5.0);
context.set_fill_style(rgbu(200, 200, 200));
context.set_shadow_blur(3.0 * hidpi_factor);
context.set_shadow_offset(vec2f(0.0, 0.0));
context.set_shadow_color(rgbau(0, 0, 0, 255));
context.fill_path(shadow_path, FillRule::Winding);
context.set_shadow_color(rgbau(0, 0, 0, 0));
let image_y = image_index as f32 * image_y_scale;
let alpha = util::clamp((load_y - image_y) / image_y_scale, 0.0, 1.0);
if alpha < 1.0 {
draw_spinner(context, image_rect.center(), THUMB_HEIGHT * 0.25, time);
}
let image_path = create_rounded_rect_path(image_rect, 5.0);
let image_coord = vec2i(
(image_index % IMAGES_ACROSS) as i32,
(image_index / IMAGES_ACROSS) as i32,
);
let pattern_transform = Transform2F::from_translation(image_rect.origin())
* Transform2F::from_scale(0.5)
* Transform2F::from_translation(
-image_coord.to_f32() * (THUMB_HEIGHT * 2.0 + 2.0) - 1.0,
);
let mut pattern = Pattern::from_image((*image).clone());
pattern.apply_transform(pattern_transform);
context.set_fill_style(pattern);
context.set_global_alpha(alpha);
context.fill_path(image_path, FillRule::Winding);
context.set_global_alpha(1.0);
context.set_stroke_style(rgbau(255, 255, 255, 192));
context.stroke_path(create_rounded_rect_path(image_rect.dilate(0.5), 3.5));
}
context.restore();
// Draw fade-away gradients.
let mut fade_gradient =
Gradient::linear_from_points(rect.origin(), rect.origin() + vec2f(0.0, 6.0));
fade_gradient.add_color_stop(rgbau(200, 200, 200, 255), 0.0);
fade_gradient.add_color_stop(rgbau(200, 200, 200, 0), 1.0);
context.set_fill_style(fade_gradient);
context.fill_rect(RectF::new(
rect.origin() + vec2f(4.0, 0.0),
vec2f(rect.width() - 8.0, 6.0),
));
let mut fade_gradient =
Gradient::linear_from_points(rect.lower_left(), rect.lower_left() - vec2f(0.0, 6.0));
fade_gradient.add_color_stop(rgbau(200, 200, 200, 255), 0.0);
fade_gradient.add_color_stop(rgbau(200, 200, 200, 0), 1.0);
context.set_fill_style(fade_gradient);
context.fill_rect(RectF::new(
rect.lower_left() + vec2f(4.0, -6.0),
vec2f(rect.width() - 8.0, 6.0),
));
// Draw scroll bar.
context.save();
let scroll_bar_rect = RectF::new(
rect.upper_right() + vec2f(-12.0, 4.0),
vec2f(8.0, rect.height() - 8.0),
);
let path = create_rounded_rect_path(scroll_bar_rect, CORNER_RADIUS);
context.set_fill_style(rgbau(0, 0, 0, 32));
context.fill_path(path.clone(), FillRule::Winding);
context.clip_path(path, FillRule::Winding);
context.set_stroke_style(rgbau(0, 0, 0, 92));
context.set_shadow_offset(vec2f(0.0, 0.0));
context.set_shadow_color(rgbau(0, 0, 0, 92));
context.set_shadow_blur(4.0 * hidpi_factor);
let shadow_path = create_rounded_rect_path(scroll_bar_rect + vec2f(0.0, 1.0), CORNER_RADIUS);
context.stroke_path(shadow_path);
context.set_shadow_color(rgbau(0, 0, 0, 0));
context.restore();
let knob_rect = RectF::new(
scroll_bar_rect.origin() + vec2f(0.0, (rect.height() - 8.0 - scroll_height) * scroll_y),
vec2f(8.0, scroll_height),
);
context.set_fill_style(rgbu(220, 220, 220));
let path = create_rounded_rect_path(knob_rect.contract(1.0), 3.0);
context.fill_path(path.clone(), FillRule::Winding);
context.clip_path(path, FillRule::Winding);
context.set_stroke_style(rgbu(128, 128, 128));
context.set_line_width(1.0);
let shadow_path = create_rounded_rect_path(knob_rect, 3.0);
context.set_shadow_blur(2.0 * hidpi_factor);
context.set_shadow_color(rgbu(128, 128, 128));
context.set_shadow_offset(vec2f(0.0, 0.0));
context.stroke_path(shadow_path);
context.restore();
}
fn draw_spinner(context: &mut CanvasRenderingContext2D, center: Vector2F, radius: f32, time: f32) {
let (start_angle, end_angle) = (time * 6.0, PI + time * 6.0);
let (outer_radius, inner_radius) = (radius, radius * 0.75);
let average_radius = util::lerp(outer_radius, inner_radius, 0.5);
context.save();
let mut path = Path2D::new();
path.arc(
center,
outer_radius,
start_angle,
end_angle,
ArcDirection::CW,
);
path.arc(
center,
inner_radius,
end_angle,
start_angle,
ArcDirection::CCW,
);
path.close_path();
set_linear_gradient_fill_style(
context,
center + vec2f(outer_radius.cos(), outer_radius.sin()) * average_radius,
center + vec2f(inner_radius.cos(), inner_radius.sin()) * average_radius,
rgbau(0, 0, 0, 0),
rgbau(0, 0, 0, 128),
);
context.fill_path(path, FillRule::Winding);
context.restore();
}
struct PerfGraph {
style: GraphStyle,
values: VecDeque<f32>,
name: &'static str,
}
impl PerfGraph {
fn new(style: GraphStyle, name: &'static str) -> PerfGraph {
PerfGraph {
style,
name,
values: VecDeque::new(),
}
}
fn push(&mut self, frame_time: f32) {
if self.values.len() == GRAPH_HISTORY_COUNT {
self.values.pop_front();
}
self.values.push_back(frame_time);
}
fn render(&self, context: &mut CanvasRenderingContext2D, origin: Vector2F) {
let rect = RectF::new(origin, vec2f(GRAPH_WIDTH, GRAPH_HEIGHT));
context.set_fill_style(rgbau(0, 0, 0, 128));
context.fill_rect(rect);
let mut path = Path2D::new();
path.move_to(rect.lower_left());
let scale = vec2f(
rect.width() / (GRAPH_HISTORY_COUNT as f32 - 1.0),
rect.height(),
);
for (index, value) in self.values.iter().enumerate() {
let mut value = *value;
if self.style == GraphStyle::FPS && value != 0.0 {
value = 1.0 / value;
}
value = (value * self.style.scale()).min(self.style.max());
let point = rect.lower_left() + vec2f(index as f32, -value / self.style.max()) * scale;
path.line_to(point);
}
path.line_to(rect.lower_left() + vec2f(self.values.len() as f32 - 1.0, 0.0) * scale);
context.set_fill_style(rgbau(255, 192, 0, 128));
context.fill_path(path, FillRule::Winding);
context.set_font(FONT_NAME_REGULAR);
context.set_text_baseline(TextBaseline::Top);
if !self.name.is_empty() {
context.set_font_size(12.0);
context.set_text_align(TextAlign::Left);
context.set_fill_style(rgbau(240, 240, 240, 192));
context.fill_text(self.name, origin + vec2f(3.0, 3.0));
}
context.set_font_size(15.0);
context.set_text_align(TextAlign::Right);
context.set_fill_style(rgbau(240, 240, 240, 255));
self.draw_label(context, self.style, rect.upper_right() + vec2f(-3.0, 3.0));
if self.style == GraphStyle::FPS {
context.set_text_baseline(TextBaseline::Alphabetic);
context.set_fill_style(rgbau(240, 240, 240, 160));
self.draw_label(
context,
GraphStyle::MS,
rect.lower_right() + vec2f(-3.0, -3.0),
);
}
}
fn draw_label(
&self,
context: &mut CanvasRenderingContext2D,
style: GraphStyle,
origin: Vector2F,
) {
let mut average = self.average();
if style == GraphStyle::FPS && average != 0.0 {
average = 1.0 / average;
}
average *= style.scale();
context.fill_text(&format!("{}{}", average, style.label()), origin);
}
fn average(&self) -> f32 {
let mut sum: f32 = self.values.iter().sum();
if !self.values.is_empty() {
sum /= self.values.len() as f32;
}
sum
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum GraphStyle {
FPS,
MS,
}
impl GraphStyle {
fn scale(self) -> f32 {
match self {
GraphStyle::FPS => 1.0,
GraphStyle::MS => 1000.0,
}
}
fn max(self) -> f32 {
match self {
GraphStyle::MS => 20.0,
GraphStyle::FPS => 80.0,
}
}
fn label(self) -> &'static str {
match self {
GraphStyle::FPS => " FPS",
GraphStyle::MS => " ms",
}
}
}
fn set_linear_gradient_fill_style(
context: &mut CanvasRenderingContext2D,
from_position: Vector2F,
to_position: Vector2F,
from_color: ColorU,
to_color: ColorU,
) {
let mut gradient = Gradient::linear(LineSegment2F::new(from_position, to_position));
gradient.add_color_stop(from_color, 0.0);
gradient.add_color_stop(to_color, 1.0);
context.set_fill_style(gradient);
}
fn create_graph_path(sample_points: &[Vector2F], sample_spread: f32, offset: Vector2F) -> Path2D {
let mut path = Path2D::new();
path.move_to(sample_points[0] + vec2f(0.0, 2.0));
for pair in sample_points.windows(2) {
path.bezier_curve_to(
pair[0] + offset + vec2f(sample_spread * 0.5, 0.0),
pair[1] + offset - vec2f(sample_spread * 0.5, 0.0),
pair[1] + offset,
);
}
path
}
fn create_rounded_rect_path(rect: RectF, radius: f32) -> Path2D {
let mut path = Path2D::new();
path.move_to(rect.origin() + vec2f(radius, 0.0));
path.arc_to(
rect.upper_right(),
rect.upper_right() + vec2f(0.0, radius),
radius,
);
path.arc_to(
rect.lower_right(),
rect.lower_right() + vec2f(-radius, 0.0),
radius,
);
path.arc_to(
rect.lower_left(),
rect.lower_left() + vec2f(0.0, -radius),
radius,
);
path.arc_to(rect.origin(), rect.origin() + vec2f(radius, 0.0), radius);
path.close_path();
path
}
struct DemoData {
image: Image,
}
impl DemoData {
fn load(resources: &dyn ResourceLoader) -> DemoData {
let data = resources.slurp("textures/example-nanovg.png").unwrap();
let image = image::load_from_memory(&data).unwrap().to_rgba();
let image = Image::from_image_buffer(image);
DemoData { image }
}
}
use foreign_types::ForeignTypeRef;
fn main() {
// Set up SDL2.
assert!(hint::set("SDL_RENDER_DRIVER", "metal"));
let sdl_context = sdl2::init().unwrap();
let video = sdl_context.video().unwrap();
// Make sure we have at least a GL 3.0 context. Pathfinder requires this.
// let gl_attributes = video.gl_attr();
// gl_attributes.set_context_profile(GLProfile::Core);
// gl_attributes.set_context_version(3, 3);
// Open a window.
let window_size = vec2i(WINDOW_WIDTH, WINDOW_HEIGHT);
let window = video
.window(
"NanoVG example port",
window_size.x() as u32,
window_size.y() as u32,
)
.opengl()
.allow_highdpi()
.build()
.unwrap();
// Get the real window size (for HiDPI).
let (drawable_width, drawable_height) = window.drawable_size();
println!("{} {}", drawable_width, drawable_height);
let canvas = window.into_canvas().present_vsync().build().unwrap();
let metal_layer = unsafe {
CoreAnimationLayerRef::from_ptr(SDL_RenderGetMetalLayer(canvas.raw()) as *mut CAMetalLayer)
};
let drawable_size = vec2i(drawable_width as i32, drawable_height as i32);
let hidpi_factor = drawable_size.x() as f32 / window_size.x() as f32;
// Load demo data.
let resources = FilesystemResourceLoader::locate();
let font_data = vec![
Handle::from_memory(
Arc::new(resources.slurp("fonts/Roboto-Regular.ttf").unwrap()),
0,
),
Handle::from_memory(
Arc::new(resources.slurp("fonts/Roboto-Bold.ttf").unwrap()),
0,
),
Handle::from_memory(
Arc::new(resources.slurp("fonts/NotoEmoji-Regular.ttf").unwrap()),
0,
),
];
let demo_data = DemoData::load(&resources);
// Create a Pathfinder renderer.
let mut renderer = Renderer::new(
MetalDevice::new(metal_layer),
&resources,
DestFramebuffer::full_window(drawable_size),
RendererOptions {
background_color: Some(rgbf(0.3, 0.3, 0.32)),
},
);
// Initialize font state.
let font_source = Arc::new(MemSource::from_fonts(font_data.into_iter()).unwrap());
let font_context = CanvasFontContext::new(font_source.clone());
// Initialize general state.
let mut event_pump = sdl_context.event_pump().unwrap();
let mut mouse_position = Vector2F::zero();
let start_time = Instant::now();
// Initialize performance graphs.
let mut fps_graph = PerfGraph::new(GraphStyle::FPS, "Frame Time");
let mut cpu_graph = PerfGraph::new(GraphStyle::MS, "CPU Time");
let mut gpu_graph = PerfGraph::new(GraphStyle::MS, "GPU Time");
// Enter the main loop.
loop {
// Make a canvas.
let mut context = Canvas::new(drawable_size.to_f32()).get_context_2d(font_context.clone());
// Start performance timing.
let frame_start_time = Instant::now();
let frame_start_elapsed_time = (frame_start_time - start_time).as_secs_f32();
// Render the demo.
context.scale(hidpi_factor);
render_demo(
&mut context,
mouse_position,
window_size.to_f32(),
frame_start_elapsed_time,
hidpi_factor,
&demo_data,
);
// Render performance graphs.
let cpu_frame_elapsed_time = (Instant::now() - frame_start_time).as_secs_f32();
fps_graph.render(&mut context, vec2f(5.0, 5.0));
cpu_graph.render(&mut context, vec2f(210.0, 5.0));
gpu_graph.render(&mut context, vec2f(415.0, 5.0));
// Render the canvas to screen.
let canvas = context.into_canvas();
let scene = SceneProxy::from_scene(canvas.into_scene(), RayonExecutor);
scene.build_and_render(&mut renderer, BuildOptions::default());
renderer.device.present_drawable();
// Add stats to performance graphs.
if let Some(gpu_time) = renderer.shift_rendering_time() {
let cpu_build_time = renderer.stats.cpu_build_time.as_secs_f32();
let gpu_time = gpu_time.gpu_time.as_secs_f32();
fps_graph.push(cpu_frame_elapsed_time + cpu_build_time.max(gpu_time));
cpu_graph.push(cpu_frame_elapsed_time + cpu_build_time);
gpu_graph.push(gpu_time);
}
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => return,
Event::MouseMotion { x, y, .. } => mouse_position = vec2i(x, y).to_f32(),
_ => {}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment