Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vislib #16

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: animated arcs stay along path
Fixed animated arcs so they stay along a circular path. The SVG below
demonstrates the before (red) and after (green) effect of this. This was
accomplished by interpolating a variable number of points between
animation frames. The interpolation is done by angle to create a smooth
effect.

Some refactoring was done in the process. The long-term path should be
to replace the `Point` struct with a `glam::Vec2` for better
interoperability.

Fixed stylesheet to always have black background while I was at it.

```svg
<svg height="500" width="500" xmlns="http://www.w3.org/2000/svg">
<style>
svg {
    background-color: black;
}
circle.inputCircle {
    stroke: white;
    fill-opacity: 0;
}

path.outputArc {
    stroke: green;
    fill-opacity: 0;
}

</style>
<svg viewBox="0 0 300 200" xmlns="http://www.w3.org/2000/svg" x="-200" y="-150" width="100%">
<circle class="inputCircle" cx="120" cy="100" r="80" stroke-width="0.3"/>
<path class="outputArc" stroke-width="2">
<animate attributeName="d" dur="4" repeatCount="indefinite" values="M175.30815 157.80145 A80 80 0 0 1 171.67628 161.07013;M179.01257 154.01404 A80 80 0 0 1 175.60501 157.51593;M182.45819 149.98975 A80 80 0 0 1 179.28989 153.70947;M185.62991 145.7462 A80 80 0 0 1 182.71475 149.66748;M188.5138 141.30205 A80 80 0 0 1 185.86456 145.40768;M191.09723 136.67676 A80 80 0 0 1 188.72556 140.94873;M193.36884 131.89063 A80 80 0 0 1 191.28513 136.31021;M195.31871 126.964645 A80 80 0 0 1 193.53207 131.51245;M193.36884 131.89063 A80 80 0 0 1 191.28513 136.31021;M191.09723 136.67676 A80 80 0 0 1 188.72556 140.94873;M188.5138 141.30205 A80 80 0 0 1 185.86456 145.40768;M185.62991 145.7462 A80 80 0 0 1 182.71475 149.66748;M182.45819 149.98975 A80 80 0 0 1 179.28989 153.70947;M179.01257 154.01404 A80 80 0 0 1 175.60501 157.51593;M175.30815 157.80145 A80 80 0 0 1 171.67628 161.07013"/>
</path>
<path stroke-width="2" stroke="red">
<animate attributeName="d" dur="4" repeatCount="indefinite" values="M175.30815 157.80145 A80 80 0 0 1 171.67628 161.07013; M195.31871 126.964645 A80 80 0 0 1 193.53207 131.51245; M175.30815 157.80145 A80 80 0 0 1 171.67628 161.07013"/>
</path>
</svg>
</svg>
```
blairfrandeen committed Mar 20, 2023
commit 491eec1d69500c5c5bd0e50e61246b13b6166942
2 changes: 2 additions & 0 deletions holoviz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -8,3 +8,5 @@ edition = "2021"
[dependencies]
svg = "0.13.0"
clap = {version = "4", features=["derive"]}
glam = "0.23.0"
is_close = "0.1.3"
133 changes: 123 additions & 10 deletions holoviz/src/lib.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,11 @@ use svg::node::element::{Animate, Circle, Path, Style, SVG};
use svg::parser::Event;
use svg::Document;

use glam::Vec2;

#[macro_use]
extern crate is_close;

extern crate test;
// width of reflected hologram segments in degrees
const HOLO_WIDTH_DEG: f32 = 3.5;
@@ -72,6 +77,7 @@ impl Visualizer {
Ok(Self::from_svg_contents(content))
}

// TODO: Add file to allow this test to run
/// Build a static hologram from the visualizer
/// ```no_run
/// # use std::io;
@@ -109,6 +115,7 @@ impl Visualizer {
.add(viewbox)
}

// TODO: Add file to allow this test to run
/// Build an animated hologram from the visualizer. Requires starting
/// and ending positions of light source relative to the canvas. The
/// animation will loop back & forth from one light source to the other
@@ -273,8 +280,31 @@ fn circular_arc_hologram_path(
path_data
}

/*
*/
#[allow(unused)]
/// Given a circle and a light source, find the angle from the +X axis
/// to the vector connecting the two. If light source is inside the circle
/// then return None. This function is unused, but is left in because the
/// logic and the associated unit tests are important, and need to be
/// incorporated elsewhere in the library.
fn incidence_angle(circle: &Circle, light_source: &Vec2) -> Option<f32> {
let circle_attrs = circle.get_attributes();
let cx = circle_attrs["cx"]
.parse::<f32>()
.expect("Circle should have an x-coordinate");
let cy = circle_attrs["cy"]
.parse::<f32>()
.expect("Circle should have a y-coordinate");
let r = circle_attrs["r"]
.parse::<f32>()
.expect("Circle should have a radius");
// check that light source is not inside circle
let center_to_light = *light_source - Vec2::new(cx, cy);
if center_to_light.length() <= r {
return None;
}
let theta = center_to_light.angle_between(Vec2::X);
Some(theta)
}

/// Given path data in the form of commands, return a string
/// as would be represented in a rendered SVG file.
@@ -324,6 +354,7 @@ fn animated_arc(
duration_secs: f32,
) -> Path {
assert!(duration_secs > 0f32);
// TODO: Break out code into a function that returns a tuple of cx, cy, r
let circle_attrs = input_circle.get_attributes();
let cx = circle_attrs["cx"]
.parse::<f32>()
@@ -334,14 +365,55 @@ fn animated_arc(
let r = circle_attrs["r"]
.parse::<f32>()
.expect("Circle should have a radius");
let frame_start = circular_arc_hologram_path(cx, cy, r, HOLO_WIDTH_DEG, light_source_start);
let frame_end = circular_arc_hologram_path(cx, cy, r, HOLO_WIDTH_DEG, light_source_end);
let animation_data: String = [
data_to_string(&frame_start),
data_to_string(&frame_end),
data_to_string(&frame_start),
]
.join(";");

let center = Vec2::new(cx, cy); // center of circle
// vectors from center of circle to light source start & end
let vec_start = Vec2::new(light_source_start.x, light_source_start.y) - center;
let vec_end = Vec2::new(light_source_end.x, light_source_end.y) - center;

// angle between vectors
let sweep_angle = vec_end.angle_between(vec_start);

// number of steps & step size for interpolation between points
let num_steps: usize = (sweep_angle.abs() / HOLO_WIDTH_DEG.to_radians()) as usize;
let step_size = sweep_angle / num_steps as f32;

// angle at which to draw arc
let start_angle = vec_start.angle_between(Vec2::X);
let mut frames: Vec<Data> = Vec::new(); // animation frames

// Create animation frames one by one
for step in 0..=num_steps {
let angle = start_angle + step as f32 * step_size;
// TODO: Check that light source isn't inside circle
// This isn't actually as trivial as it seems, and may require some
// resturcturing. This requires the ability to have arcs turn
// on and off, which I think requires another animation sequence
// for arcs that may turn on or off.

// If an arc goes "off" due to light source going through the circle
// (under the arc) it will need a new animation element with as many
// frames as the path animation; this animation will have attributeName
// of "stroke-opacity" and values being an array of booleans
frames.push(circular_arc_by_angle(&center, r, angle, HOLO_WIDTH_DEG));
}

// Generate the animation frames for the opposite direction
let mut rev_frames = frames.clone();
rev_frames.pop(); // don't draw the middle frame twice
// I.e. [ A, B, C ] should become [ A, B, C, B, A ]
// and not [A, B, C, C, B, A]
rev_frames.reverse();
frames.append(&mut rev_frames);

// Build the animation data to attach to the path element
let animation_data: String = frames
.iter()
.map(|frame| data_to_string(frame))
.collect::<Vec<String>>()
.join(";");

// Finally build the path element and return it
let animated_arc = Path::new().add(
Animate::new()
.set("dur", duration_secs)
@@ -353,6 +425,14 @@ fn animated_arc(
animated_arc
}

fn circular_arc_by_angle(center: &Vec2, radius: f32, angle: f32, width_deg: f32) -> Data {
let start = *center + Vec2::from_angle(angle - width_deg.to_radians() / 2f32) * radius;
let end = *center + Vec2::from_angle(angle + width_deg.to_radians() / 2f32) * radius;
Data::new()
.move_to((start.x, start.y))
.elliptical_arc_to((radius, radius, 0, 0, 1, end.x, end.y))
}

#[cfg(test)]
mod tests {
use super::*;
@@ -461,6 +541,39 @@ mod tests {
);
}

#[test]
fn test_incidence_angle() {
let c = Circle::new().set("cx", 0).set("cy", 0).set("r", 100);

// point inside circle, angle should be None
let ls = Vec2::new(10f32, 10f32);
assert_eq!(incidence_angle(&c, &ls), None);

// point directly above circle, angle should be 90
let ls = Vec2::new(0., -150.);
assert!(is_close!(
incidence_angle(&c, &ls).unwrap(),
std::f32::consts::FRAC_PI_2,
rel_tol = 1e-3
));

// point directly below circle, angle should be -90
let ls = Vec2::new(0., 150.);
assert!(is_close!(
incidence_angle(&c, &ls).unwrap(),
-std::f32::consts::FRAC_PI_2,
rel_tol = 1e-3
));

// point on x axis, should be 0
let ls = Vec2::new(150., 0.);
assert_eq!(incidence_angle(&c, &ls), Some(0.));

// point on -x axis, should be pi
let ls = Vec2::new(-150., 0.);
assert_eq!(incidence_angle(&c, &ls), Some(-std::f32::consts::PI));
}

/* "INTEGRATION TESTS" */
fn integration_test(input_path: PathBuf, output_path: PathBuf) -> Result<(), std::io::Error> {
let viz = Visualizer::from_file(input_path)?;
3 changes: 3 additions & 0 deletions holoviz/style.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
svg {
background-color: black;
}
circle.inputCircle {
stroke: white;
fill-opacity: 0;