From fc8103877b5031767d17c0b65b86f218688d6d25 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 23:20:51 +0000 Subject: [PATCH 01/11] Add bindings for the screenTint parameter. --- inox2d/src/formats/payload.rs | 3 +++ inox2d/src/params.rs | 23 ++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index cb25138..6543bbf 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -464,6 +464,9 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar BindingValues::Deform(Matrix2d::from_slice_vecs(&parsed, true)?) } + "screenTint.r" => BindingValues::ScreenTintR(deserialize_inner_binding_values(values)?), + "screenTint.g" => BindingValues::ScreenTintG(deserialize_inner_binding_values(values)?), + "screenTint.b" => BindingValues::ScreenTintB(deserialize_inner_binding_values(values)?), // TODO "opacity" => BindingValues::Opacity, param_name => return Err(InoxParseError::UnknownParamName(param_name.to_owned())), diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 358af1c..34c19b1 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -8,7 +8,7 @@ use crate::math::{ matrix::Matrix2d, }; use crate::node::{ - components::{DeformSource, DeformStack, Mesh, TransformStore, ZSort}, + components::{DeformSource, DeformStack, Drawable, Mesh, TransformStore, ZSort}, InoxNodeUuid, }; use crate::puppet::{InoxNodeTree, Puppet, World}; @@ -32,6 +32,9 @@ pub enum BindingValues { TransformRY(Matrix2d), TransformRZ(Matrix2d), Deform(Matrix2d>), + ScreenTintR(Matrix2d), + ScreenTintG(Matrix2d), + ScreenTintB(Matrix2d), // TODO Opacity, } @@ -228,6 +231,24 @@ impl Param { .expect("Nodes being deformed must have a DeformStack component.") .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); } + BindingValues::ScreenTintR(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.screen_tint.x += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } + BindingValues::ScreenTintG(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.screen_tint.y += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } + BindingValues::ScreenTintB(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.screen_tint.z += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } // TODO BindingValues::Opacity => {} } From 2b351dba208f77471ebc6cb4265984f2f603f5ef Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 23:43:19 +0000 Subject: [PATCH 02/11] Implement opacity bindings --- inox2d/src/formats/payload.rs | 3 +-- inox2d/src/params.rs | 11 +++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 6543bbf..aa1d2d4 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -467,8 +467,7 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar "screenTint.r" => BindingValues::ScreenTintR(deserialize_inner_binding_values(values)?), "screenTint.g" => BindingValues::ScreenTintG(deserialize_inner_binding_values(values)?), "screenTint.b" => BindingValues::ScreenTintB(deserialize_inner_binding_values(values)?), - // TODO - "opacity" => BindingValues::Opacity, + "opacity" => BindingValues::Opacity(deserialize_inner_binding_values(values)?), param_name => return Err(InoxParseError::UnknownParamName(param_name.to_owned())), }) } diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 34c19b1..82edf0c 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -35,8 +35,7 @@ pub enum BindingValues { ScreenTintR(Matrix2d), ScreenTintG(Matrix2d), ScreenTintB(Matrix2d), - // TODO - Opacity, + Opacity(Matrix2d), } #[derive(Debug, Clone)] @@ -249,8 +248,12 @@ impl Param { comps.get_mut::(binding.node).unwrap().blending.screen_tint.z += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } - // TODO - BindingValues::Opacity => {} + BindingValues::Opacity(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.opacity += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } } } } From b811bd1ab5c3e353760dff19fba5857ace8ab33d Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Wed, 25 Feb 2026 22:59:32 +0000 Subject: [PATCH 03/11] Implement tint parameter bindings --- inox2d/src/formats/payload.rs | 3 +++ inox2d/src/params.rs | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index aa1d2d4..37f4b1a 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -464,6 +464,9 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar BindingValues::Deform(Matrix2d::from_slice_vecs(&parsed, true)?) } + "tint.r" => BindingValues::TintR(deserialize_inner_binding_values(values)?), + "tint.g" => BindingValues::TintG(deserialize_inner_binding_values(values)?), + "tint.b" => BindingValues::TintB(deserialize_inner_binding_values(values)?), "screenTint.r" => BindingValues::ScreenTintR(deserialize_inner_binding_values(values)?), "screenTint.g" => BindingValues::ScreenTintG(deserialize_inner_binding_values(values)?), "screenTint.b" => BindingValues::ScreenTintB(deserialize_inner_binding_values(values)?), diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 82edf0c..d13b67b 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -32,6 +32,9 @@ pub enum BindingValues { TransformRY(Matrix2d), TransformRZ(Matrix2d), Deform(Matrix2d>), + TintR(Matrix2d), + TintG(Matrix2d), + TintB(Matrix2d), ScreenTintR(Matrix2d), ScreenTintG(Matrix2d), ScreenTintB(Matrix2d), @@ -230,6 +233,24 @@ impl Param { .expect("Nodes being deformed must have a DeformStack component.") .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); } + BindingValues::TintR(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.tint.x += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } + BindingValues::TintG(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.tint.y += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } + BindingValues::TintB(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.tint.z += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } BindingValues::ScreenTintR(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); From 8d1e4b709a92f97a7728b887d46cdff38758570a Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Wed, 4 Mar 2026 00:25:18 +0000 Subject: [PATCH 04/11] Support Z translation binding --- inox2d/src/formats/payload.rs | 1 + inox2d/src/params.rs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 37f4b1a..5808300 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -446,6 +446,7 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar "zSort" => BindingValues::ZSort(deserialize_inner_binding_values(values)?), "transform.t.x" => BindingValues::TransformTX(deserialize_inner_binding_values(values)?), "transform.t.y" => BindingValues::TransformTY(deserialize_inner_binding_values(values)?), + "transform.t.z" => BindingValues::TransformTZ(deserialize_inner_binding_values(values)?), "transform.s.x" => BindingValues::TransformSX(deserialize_inner_binding_values(values)?), "transform.s.y" => BindingValues::TransformSY(deserialize_inner_binding_values(values)?), "transform.r.x" => BindingValues::TransformRX(deserialize_inner_binding_values(values)?), diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index d13b67b..ebc888b 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -26,6 +26,7 @@ pub enum BindingValues { ZSort(Matrix2d), TransformTX(Matrix2d), TransformTY(Matrix2d), + TransformTZ(Matrix2d), TransformSX(Matrix2d), TransformSY(Matrix2d), TransformRX(Matrix2d), @@ -147,6 +148,16 @@ impl Param { .translation .y += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } + BindingValues::TransformTZ(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps + .get_mut::(binding.node) + .unwrap() + .relative + .translation + .z += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } BindingValues::TransformSX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); From 7ca6a435dea11190550e76576d09dd807ae661b4 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 8 Mar 2026 20:20:09 +0000 Subject: [PATCH 05/11] Reset all drawable parameters every frame. --- inox2d/src/formats/payload.rs | 8 ++++---- inox2d/src/node/components.rs | 22 ++++++++++++++++++++++ inox2d/src/render.rs | 4 ++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 5808300..dd0cb53 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -176,8 +176,8 @@ fn deserialize_simple_physics(obj: JsonObject) -> InoxParseResult } fn deserialize_drawable(obj: JsonObject) -> InoxParseResult { - Ok(Drawable { - blending: Blending { + Ok(Drawable::new( + Blending { mode: match obj.get_str("blend_mode")? { "Normal" => BlendMode::Normal, "Multiply" => BlendMode::Multiply, @@ -192,7 +192,7 @@ fn deserialize_drawable(obj: JsonObject) -> InoxParseResult { screen_tint: obj.get_vec3("screenTint").unwrap_or(vec3(0.0, 0.0, 0.0)), opacity: obj.get_f32("opacity").unwrap_or(1.0), }, - masks: { + { if let Ok(masks) = obj.get_list("masks") { Some(Masks { threshold: obj.get_f32("mask_threshold").unwrap_or(0.5), @@ -209,7 +209,7 @@ fn deserialize_drawable(obj: JsonObject) -> InoxParseResult { None } }, - }) + )) } fn deserialize_mesh(obj: JsonObject) -> InoxParseResult { diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index 882df76..c8750ae 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -30,10 +30,32 @@ pub struct Composite {} /// If has this as a component, the node should render something pub struct Drawable { pub blending: Blending, + + /// The original parameters for this drawable. + /// Copied over `blending` on reset. + initial_blending: Blending, + /// If Some, the node should consider masking when rendering pub masks: Option, } +impl Drawable { + pub fn new(blending: Blending, masks: Option) -> Self { + Self { + blending, + initial_blending: blending, + masks, + } + } + + /// Reset the drawable back to the initial configuration set when `new` + /// was called. + pub fn reset(&mut self) { + self.blending = self.initial_blending; + } +} + +#[derive(Copy, Clone)] pub struct Blending { pub mode: BlendMode, pub tint: Vec3, diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index c77fe74..f95107f 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -129,6 +129,10 @@ impl RenderCtx { if let Some(deform_stack) = comps.get_mut::(node.uuid) { deform_stack.reset(); } + + if let Some(drawable) = comps.get_mut::(node.uuid) { + drawable.reset(); + } } } From d3d8efad61dcf9ba7e0cd8294c86587a0adf908e Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 8 Mar 2026 20:30:24 +0000 Subject: [PATCH 06/11] Reset all drawable parameters every frame. --- inox2d/src/render.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index f95107f..0cb1826 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use std::mem::swap; use crate::node::{ - components::{DeformStack, Mask, Masks, ZSort}, + components::{DeformStack, Drawable, Mask, Masks, ZSort}, drawables::{CompositeComponents, DrawableKind, TexturedMeshComponents}, InoxNodeUuid, }; From 62537d6e91e9ad4a519e2e62a3c311746e37450f Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 8 Mar 2026 20:21:57 +0000 Subject: [PATCH 07/11] Interpolating within an out range with no delta should not turn the target parameter into a NaN. --- inox2d/src/math/interp.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/inox2d/src/math/interp.rs b/inox2d/src/math/interp.rs index 85c8e8c..76823d8 100644 --- a/inox2d/src/math/interp.rs +++ b/inox2d/src/math/interp.rs @@ -67,7 +67,15 @@ fn interpolate_linear(t: f32, range_in: InterpRange, range_out: InterpRange range_in.end, ); - (t - range_in.beg) * (range_out.end - range_out.beg) / (range_in.end - range_in.beg) + range_out.beg + let range_out_delta = range_out.end - range_out.beg; + let range_in_delta = range_in.end - range_in.beg; + if range_out_delta == 0.0 { + // Calculus teachers HATE this ONE WEIRD TRICK to getting rid of + // divide-by-zero errors in your code! + return range_out.beg; + } + + (t - range_in.beg) * range_out_delta / range_in_delta + range_out.beg } #[inline] From 7738ef2971948c80b456fa3d6fca039aaa5af318 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 9 Mar 2026 23:06:24 +0000 Subject: [PATCH 08/11] Tint, screen tint, and opacity are applied multiplicatively. --- inox2d/src/params.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index ebc888b..f246b89 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -247,43 +247,43 @@ impl Param { BindingValues::TintR(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.tint.x += + comps.get_mut::(binding.node).unwrap().blending.tint.x *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TintG(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.tint.y += + comps.get_mut::(binding.node).unwrap().blending.tint.y *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TintB(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.tint.z += + comps.get_mut::(binding.node).unwrap().blending.tint.z *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintR(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.x += + comps.get_mut::(binding.node).unwrap().blending.screen_tint.x *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintG(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.y += + comps.get_mut::(binding.node).unwrap().blending.screen_tint.y *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintB(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.z += + comps.get_mut::(binding.node).unwrap().blending.screen_tint.z *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::Opacity(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.opacity += + comps.get_mut::(binding.node).unwrap().blending.opacity *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } } From 59205fb6d4d58f27a94b624fb53069ffe70bc47c Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 9 Mar 2026 23:20:02 +0000 Subject: [PATCH 09/11] Well, ok, the screenTint is also multiplied I guess? --- inox2d/src/params.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index f246b89..a0d51e5 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -265,19 +265,19 @@ impl Param { BindingValues::ScreenTintR(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.x *= + comps.get_mut::(binding.node).unwrap().blending.screen_tint.x += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintG(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.y *= + comps.get_mut::(binding.node).unwrap().blending.screen_tint.y += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintB(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.z *= + comps.get_mut::(binding.node).unwrap().blending.screen_tint.z += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::Opacity(ref matrix) => { From 04084ba53a74ebf64afb0489996858c6128d1263 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 12 Mar 2026 13:52:25 +0000 Subject: [PATCH 10/11] We fixed the bug with bindings at zero, so don't skip them. --- inox2d/src/params.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index a0d51e5..8dd46e9 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -324,10 +324,7 @@ impl ParamCtx { pub(crate) fn apply(&self, params: &HashMap, nodes: &InoxNodeTree, comps: &mut World) { // a correct implementation should not care about the order of `.apply()` for (param_name, val) in self.values.iter() { - // TODO: a correct implementation should not fail on param value (0, 0) - if *val != Vec2::ZERO { - params.get(param_name).unwrap().apply(*val, nodes, comps); - } + params.get(param_name).unwrap().apply(*val, nodes, comps); } } } From e8080e33792a72040ccf2658e84c0cc786c8b019 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 12 Mar 2026 13:54:45 +0000 Subject: [PATCH 11/11] Allow retrieving param values --- inox2d/src/params.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 8dd46e9..8b87a23 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -320,6 +320,14 @@ impl ParamCtx { } } + pub fn get(&self, param_name: &str) -> Result { + if let Some(value) = self.values.get(param_name) { + Ok(*value) + } else { + Err(SetParamError::NoParameterNamed(param_name.to_string())) + } + } + /// Modify components as specified by all params. Must be called ONCE per frame. pub(crate) fn apply(&self, params: &HashMap, nodes: &InoxNodeTree, comps: &mut World) { // a correct implementation should not care about the order of `.apply()`