Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
Aaron Fenyes
2aa1adcd07 Combine color and opacity on push to scene
All checks were successful
/ test (pull_request) Successful in 2m27s
2025-05-19 00:21:05 -07:00
Aaron Fenyes
592f327e62 Make points more transparent in ghost mode 2025-05-19 00:06:21 -07:00
Aaron Fenyes
fc230e4993 Introduce ghost mode
Elements in ghost mode are intangible. Spheres in ghost mode are also
more transparent. Points in ghost mode should probably be more
transparent too, but that hasn't been implemented yet.
2025-05-18 20:03:52 -07:00
Aaron Fenyes
ba1d87812f Allow each sphere to have its own opacity
To confirm that we haven't accidentally changed the rendering behavior,
we still give every sphere the same default opacity.
2025-05-18 13:35:01 -07:00
7 changed files with 77 additions and 31 deletions

View file

@ -90,6 +90,10 @@ summary > div, .regulator {
padding-right: 8px; padding-right: 8px;
} }
.element > input {
margin-left: 8px;
}
.element-switch { .element-switch {
width: 18px; width: 18px;
padding-left: 2px; padding-left: 2px;

View file

@ -101,6 +101,7 @@ pub trait Element: Serial + ProblemPoser + DisplayItem {
fn id(&self) -> &String; fn id(&self) -> &String;
fn label(&self) -> &String; fn label(&self) -> &String;
fn representation(&self) -> Signal<DVector<f64>>; fn representation(&self) -> Signal<DVector<f64>>;
fn ghost(&self) -> Signal<bool>;
// the regulators the element is subject to. the assembly that owns the // the regulators the element is subject to. the assembly that owns the
// element is responsible for keeping this set up to date // element is responsible for keeping this set up to date
@ -154,6 +155,7 @@ pub struct Sphere {
pub label: String, pub label: String,
pub color: ElementColor, pub color: ElementColor,
pub representation: Signal<DVector<f64>>, pub representation: Signal<DVector<f64>>,
pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>> column_index: Cell<Option<usize>>
@ -173,6 +175,7 @@ impl Sphere {
label: label, label: label,
color: color, color: color,
representation: create_signal(representation), representation: create_signal(representation),
ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into() column_index: None.into()
@ -210,6 +213,10 @@ impl Element for Sphere {
self.representation self.representation
} }
fn ghost(&self) -> Signal<bool> {
self.ghost
}
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> { fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators self.regulators
} }
@ -244,6 +251,7 @@ pub struct Point {
pub label: String, pub label: String,
pub color: ElementColor, pub color: ElementColor,
pub representation: Signal<DVector<f64>>, pub representation: Signal<DVector<f64>>,
pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>> column_index: Cell<Option<usize>>
@ -263,6 +271,7 @@ impl Point {
label, label,
color, color,
representation: create_signal(representation), representation: create_signal(representation),
ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into() column_index: None.into()
@ -296,6 +305,10 @@ impl Element for Point {
self.representation self.representation
} }
fn ghost(&self) -> Signal<bool> {
self.ghost
}
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> { fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators self.regulators
} }

View file

@ -20,11 +20,23 @@ use crate::{
assembly::{Element, ElementColor, ElementMotion, Point, Sphere} assembly::{Element, ElementColor, ElementMotion, Point, Sphere}
}; };
// --- color ---
const COLOR_SIZE: usize = 3;
type ColorWithOpacity = [f32; COLOR_SIZE + 1];
fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
let mut color_with_opacity = [0.0; COLOR_SIZE + 1];
color_with_opacity[..COLOR_SIZE].copy_from_slice(&color);
color_with_opacity[COLOR_SIZE] = opacity;
color_with_opacity
}
// --- scene data --- // --- scene data ---
struct SceneSpheres { struct SceneSpheres {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors: Vec<ElementColor>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32> highlights: Vec<f32>
} }
@ -32,7 +44,7 @@ impl SceneSpheres {
fn new() -> SceneSpheres{ fn new() -> SceneSpheres{
SceneSpheres { SceneSpheres {
representations: Vec::new(), representations: Vec::new(),
colors: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new() highlights: Vec::new()
} }
} }
@ -41,16 +53,16 @@ impl SceneSpheres {
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
} }
fn push(&mut self, representation: DVector<f64>, color: ElementColor, highlight: f32) { fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32) {
self.representations.push(representation); self.representations.push(representation);
self.colors.push(color); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
} }
} }
struct ScenePoints { struct ScenePoints {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors: Vec<ElementColor>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>, highlights: Vec<f32>,
selections: Vec<f32> selections: Vec<f32>
} }
@ -59,15 +71,15 @@ impl ScenePoints {
fn new() -> ScenePoints { fn new() -> ScenePoints {
ScenePoints { ScenePoints {
representations: Vec::new(), representations: Vec::new(),
colors: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new(), highlights: Vec::new(),
selections: Vec::new() selections: Vec::new()
} }
} }
fn push(&mut self, representation: DVector<f64>, color: ElementColor, highlight: f32, selected: bool) { fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32, selected: bool) {
self.representations.push(representation); self.representations.push(representation);
self.colors.push(color); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
self.selections.push(if selected { 1.0 } else { 0.0 }); self.selections.push(if selected { 1.0 } else { 0.0 });
} }
@ -98,11 +110,16 @@ pub trait DisplayItem {
impl DisplayItem for Sphere { impl DisplayItem for Sphere {
fn show(&self, scene: &mut Scene, selected: bool) { fn show(&self, scene: &mut Scene, selected: bool) {
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ /* SCAFFOLDING */
const DEFAULT_OPACITY: f32 = 0.5;
const GHOST_OPACITY: f32 = 0.2;
const HIGHLIGHT: f32 = 0.2;
let representation = self.representation.get_clone_untracked(); let representation = self.representation.get_clone_untracked();
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
let highlight = if selected { 1.0 } else { HIGHLIGHT }; let highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.spheres.push(representation, color, highlight); scene.spheres.push(representation, color, opacity, highlight);
} }
// this method should be kept synchronized with `sphere_cast` in // this method should be kept synchronized with `sphere_cast` in
@ -148,11 +165,15 @@ impl DisplayItem for Sphere {
impl DisplayItem for Point { impl DisplayItem for Point {
fn show(&self, scene: &mut Scene, selected: bool) { fn show(&self, scene: &mut Scene, selected: bool) {
const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ /* SCAFFOLDING */
const GHOST_OPACITY: f32 = 0.4;
const HIGHLIGHT: f32 = 0.5;
let representation = self.representation.get_clone_untracked(); let representation = self.representation.get_clone_untracked();
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
let highlight = if selected { 1.0 } else { HIGHLIGHT }; let highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.points.push(representation, color, highlight, selected); scene.points.push(representation, color, opacity, highlight, selected);
} }
/* SCAFFOLDING */ /* SCAFFOLDING */
@ -365,6 +386,7 @@ pub fn Display() -> View {
state.assembly.elements.with(|elts| { state.assembly.elements.with(|elts| {
for elt in elts { for elt in elts {
elt.representation().track(); elt.representation().track();
elt.ghost().track();
} }
}); });
state.selection.track(); state.selection.track();
@ -395,7 +417,6 @@ pub fn Display() -> View {
const SHRINKING_SPEED: f64 = 0.15; // in length units per second const SHRINKING_SPEED: f64 = 0.15; // in length units per second
// display parameters // display parameters
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
const DEBUG_MODE: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */
@ -469,7 +490,6 @@ pub fn Display() -> View {
); );
let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution");
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity");
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
@ -654,9 +674,9 @@ pub fn Display() -> View {
sphere_lt_locs[n].as_ref(), sphere_lt_locs[n].as_ref(),
v.rows(3, 2).as_slice() v.rows(3, 2).as_slice()
); );
ctx.uniform3fv_with_f32_array( ctx.uniform4fv_with_f32_array(
sphere_color_locs[n].as_ref(), sphere_color_locs[n].as_ref(),
&scene.spheres.colors[n] &scene.spheres.colors_with_opacity[n]
); );
ctx.uniform1f( ctx.uniform1f(
sphere_highlight_locs[n].as_ref(), sphere_highlight_locs[n].as_ref(),
@ -665,7 +685,6 @@ pub fn Display() -> View {
} }
// pass the display parameters // pass the display parameters
ctx.uniform1f(opacity_loc.as_ref(), OPACITY);
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
@ -703,7 +722,7 @@ pub fn Display() -> View {
// bind them to the corresponding attributes in the vertex // bind them to the corresponding attributes in the vertex
// shader // shader
bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice());
bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice()); bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice());
bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice()); bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
@ -851,7 +870,11 @@ pub fn Display() -> View {
let (dir, pixel_size) = event_dir(&event); let (dir, pixel_size) = event_dir(&event);
console::log_1(&JsValue::from(dir.to_string())); console::log_1(&JsValue::from(dir.to_string()));
let mut clicked: Option<(Rc<dyn Element>, f64)> = None; let mut clicked: Option<(Rc<dyn Element>, f64)> = None;
for elt in state.assembly.elements.get_clone_untracked() { let tangible_elts = state.assembly.elements
.get_clone_untracked()
.into_iter()
.filter(|elt| !elt.ghost().get());
for elt in tangible_elts {
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) {
Some(depth) => match clicked { Some(depth) => match clicked {
Some((_, best_depth)) => { Some((_, best_depth)) => {

View file

@ -202,7 +202,11 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
) { ) {
div(class="element-label") { (label) } div(class="element-label") { (label) }
div(class="element-representation") { (rep_components) } div(class="element-representation") { (rep_components) }
div(class="status") input(
r#type="checkbox",
bind:checked=element.ghost(),
on:click=|event: MouseEvent| event.stop_propagation()
)
} }
} }
ul(class="regulators") { ul(class="regulators") {

View file

@ -2,7 +2,7 @@
precision highp float; precision highp float;
in vec3 point_color; in vec4 point_color;
in float point_highlight; in float point_highlight;
in float total_radius; in float total_radius;
@ -13,6 +13,7 @@ void main() {
const float POINT_RADIUS = 4.; const float POINT_RADIUS = 4.;
float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r);
vec3 color = mix(point_color, vec3(1.), border * point_highlight); float disk = 1. - smoothstep(total_radius - 1., total_radius, r);
outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); vec4 color = mix(point_color, vec4(1.), border * point_highlight);
outColor = vec4(vec3(1.), disk) * color;
} }

View file

@ -1,11 +1,11 @@
#version 300 es #version 300 es
in vec4 position; in vec4 position;
in vec3 color; in vec4 color;
in float highlight; in float highlight;
in float selected; in float selected;
out vec3 point_color; out vec4 point_color;
out float point_highlight; out float point_highlight;
out float total_radius; out float total_radius;

View file

@ -17,7 +17,7 @@ struct vecInv {
const int SPHERE_MAX = 200; const int SPHERE_MAX = 200;
uniform int sphere_cnt; uniform int sphere_cnt;
uniform vecInv sphere_list[SPHERE_MAX]; uniform vecInv sphere_list[SPHERE_MAX];
uniform vec3 color_list[SPHERE_MAX]; uniform vec4 color_list[SPHERE_MAX];
uniform float highlight_list[SPHERE_MAX]; uniform float highlight_list[SPHERE_MAX];
// view // view
@ -25,7 +25,6 @@ uniform vec2 resolution;
uniform float shortdim; uniform float shortdim;
// controls // controls
uniform float opacity;
uniform int layer_threshold; uniform int layer_threshold;
uniform bool debug_mode; uniform bool debug_mode;
@ -69,7 +68,7 @@ struct Fragment {
vec4 color; vec4 color;
}; };
Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) {
// the expression for normal needs to be checked. it's supposed to give the // the expression for normal needs to be checked. it's supposed to give the
// negative gradient of the lorentz product between the impact point vector // negative gradient of the lorentz product between the impact point vector
// and the sphere vector with respect to the coordinates of the impact // and the sphere vector with respect to the coordinates of the impact
@ -79,7 +78,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) {
float incidence = dot(normal, light_dir); float incidence = dot(normal, light_dir);
float illum = mix(0.4, 1.0, max(incidence, 0.0)); float illum = mix(0.4, 1.0, max(incidence, 0.0));
return Fragment(pt, normal, vec4(illum * base_color, opacity)); return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a));
} }
float intersection_dist(Fragment a, Fragment b) { float intersection_dist(Fragment a, Fragment b) {
@ -192,10 +191,11 @@ void main() {
vec3 color = vec3(0.); vec3 color = vec3(0.);
int layer = layer_cnt - 1; int layer = layer_cnt - 1;
TaggedDepth hit = top_hits[layer]; TaggedDepth hit = top_hits[layer];
vec4 sphere_color = color_list[hit.id];
Fragment frag_next = sphere_shading( Fragment frag_next = sphere_shading(
sphere_list[hit.id], sphere_list[hit.id],
hit.depth * dir, hit.depth * dir,
hit.dimming * color_list[hit.id] vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
); );
float highlight_next = highlight_list[hit.id]; float highlight_next = highlight_list[hit.id];
--layer; --layer;
@ -206,10 +206,11 @@ void main() {
// shade the next fragment // shade the next fragment
hit = top_hits[layer]; hit = top_hits[layer];
sphere_color = color_list[hit.id];
frag_next = sphere_shading( frag_next = sphere_shading(
sphere_list[hit.id], sphere_list[hit.id],
hit.depth * dir, hit.depth * dir,
hit.dimming * color_list[hit.id] vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
); );
highlight_next = highlight_list[hit.id]; highlight_next = highlight_list[hit.id];