Repository: GraphiteEditor/Graphite (language: rust)

Issue:
Tracking Issue: Lambda nodes
Because a Graphene document is turned at graph compilation time into a static call graph, where each input to a node (its construction argument) is hardcoded in the node's struct as the result of evaluating its input node with static inputs, we can't easily make runtime calls to other nodes with varying inputs. Maybe something equivalent to dynamic dispatch can be employed to solve this limitation, but this will require some discussions with @TrueDoctor about Graphene architecture.

From a UI perspective, this is basically going to be a frame around a group of nodes, where that frame itself can be wired in as an input to a node.

- [ ] Generalize `()` and `Footprint` call arguments with `Contexts` (#2232)
- [ ] Design and implement how we'll expose actual lambdas to the user


Discussion / hints:
We do have a provision for this which are the call arguments which can be supplied at runtime but using this for complex inputs would be a bit annoying atm

Fix patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 18c54c412e..6b4ee383f2 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -6,7 +6,7 @@
 		},
 		"ghcr.io/devcontainers/features/node:1": {}
 	},
-	"onCreateCommand": "cargo install cargo-watch wasm-pack cargo-about && cargo install -f wasm-bindgen-cli@0.2.99",
+	"onCreateCommand": "cargo install cargo-watch wasm-pack cargo-about && cargo install -f wasm-bindgen-cli@0.2.100",
 	"customizations": {
 		"vscode": {
 			// NOTE: Keep this in sync with `.vscode/extensions.json`
diff --git a/Cargo.lock b/Cargo.lock
index e04ac9aeae..7f3657621f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1397,6 +1397,12 @@ dependencies = [
  "syn 2.0.98",
 ]
 
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -2400,6 +2406,7 @@ dependencies = [
  "js-sys",
  "log",
  "num-traits",
+ "pretty_assertions",
  "reqwest 0.12.12",
  "rustc-hash 2.1.0",
  "serde",
@@ -3505,9 +3512,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
 
 [[package]]
 name = "js-sys"
-version = "0.3.76"
+version = "0.3.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
 dependencies = [
  "once_cell",
  "wasm-bindgen",
@@ -4907,6 +4914,16 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
 
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
 [[package]]
 name = "proc-macro-crate"
 version = "1.3.1"
@@ -7502,20 +7519,21 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
 dependencies = [
  "cfg-if",
  "once_cell",
+ "rustversion",
  "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
 dependencies = [
  "bumpalo",
  "log",
@@ -7527,9 +7545,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.49"
+version = "0.4.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -7540,9 +7558,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -7550,9 +7568,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -7563,9 +7581,12 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
 
 [[package]]
 name = "wasm-streams"
@@ -7691,9 +7712,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.76"
+version = "0.3.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -8824,6 +8845,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
 
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
 [[package]]
 name = "yoke"
 version = "0.7.5"
diff --git a/Cargo.toml b/Cargo.toml
index c949a405ba..fe7e330dbd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,10 +63,10 @@ spirv-std = { git = "https://github.com/Rust-GPU/rust-gpu.git" }
 wgpu-types = "23"
 wgpu = "23"
 once_cell = "1.13" # Remove when `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) is stabilized in Rust 1.80 and we bump our MSRV
-wasm-bindgen = "=0.2.99" # NOTICE: ensure this stays in sync with the `wasm-bindgen-cli` version in `website/content/volunteer/guide/project-setup/_index.md`. We pin this version because wasm-bindgen upgrades may break various things.
+wasm-bindgen = "=0.2.100" # NOTICE: ensure this stays in sync with the `wasm-bindgen-cli` version in `website/content/volunteer/guide/project-setup/_index.md`. We pin this version because wasm-bindgen upgrades may break various things.
 wasm-bindgen-futures = "0.4"
-js-sys = "=0.3.76"
-web-sys = "=0.3.76"
+js-sys = "=0.3.77"
+web-sys = "=0.3.77"
 winit = "0.29"
 url = "2.5"
 tokio = { version = "1.29", features = ["fs", "io-std"] }
@@ -80,6 +80,7 @@ base64 = "0.22"
 image = { version = "0.25", default-features = false, features = ["png"] }
 rustybuzz = "0.20"
 spirv = "0.3"
+pretty_assertions = "1.4.1"
 fern = { version = "0.7", features = ["colored"] }
 num_enum = "0.7"
 num-derive = "0.4"
diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
index 6e850b976e..848807322c 100644
--- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
+++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
@@ -293,9 +293,9 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(Footprint)),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![
-									NodeInput::network(graphene_core::Type::Fn(Box::new(concrete!(Footprint)), Box::new(concrete!(ArtboardGroup))), 0),
+									NodeInput::network(graphene_core::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(ArtboardGroup))), 0),
 									NodeInput::node(NodeId(1), 0),
 									NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
 								],
@@ -390,7 +390,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 		},
 		DocumentNodeDefinition {
 			identifier: "Load Image",
-			category: "Raster: Generator",
+			category: "Network",
 			node_template: NodeTemplate {
 				document_node: DocumentNode {
 					implementation: DocumentNodeImplementation::Network(NodeNetwork {
@@ -398,20 +398,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 						nodes: [
 							DocumentNode {
 								inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::scope("editor-api"), NodeInput::network(concrete!(String), 1)],
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::wasm_application_io::LoadResourceNode")),
 								..Default::default()
 							},
 							DocumentNode {
 								inputs: vec![NodeInput::node(NodeId(0), 0)],
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::wasm_application_io::DecodeImageNode")),
 								..Default::default()
 							},
 							DocumentNode {
 								inputs: vec![NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::CullNode")),
-								manual_composition: Some(concrete!(Footprint)),
+								manual_composition: Some(concrete!(Context)),
 								..Default::default()
 							},
 						]
@@ -484,7 +484,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(0), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 								..Default::default()
@@ -554,7 +554,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 								..Default::default()
@@ -638,20 +638,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 							DocumentNode {
 								inputs: vec![NodeInput::scope("editor-api")],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::wasm_application_io::CreateSurfaceNode")),
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								skip_deduplication: true,
 								..Default::default()
 							},
 							DocumentNode {
 								inputs: vec![NodeInput::node(NodeId(0), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								..Default::default()
 							},
 							DocumentNode {
 								inputs: vec![NodeInput::network(generic!(T), 0), NodeInput::network(concrete!(Footprint), 1), NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::wasm_application_io::RasterizeNode")),
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								..Default::default()
 							},
 						]
@@ -724,7 +724,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 			category: "Raster",
 			node_template: NodeTemplate {
 				document_node: DocumentNode {
-					manual_composition: Some(concrete!(Footprint)),
+					manual_composition: Some(concrete!(Context)),
 					implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::raster::NoisePatternNode")),
 					inputs: vec![
 						NodeInput::value(TaggedValue::Bool(false), false),
@@ -1000,7 +1000,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								NodeInput::network(concrete!(Vec<graphene_core::vector::brush_stroke::BrushStroke>), 2),
 								NodeInput::network(concrete!(BrushCache), 3),
 							],
-							manual_composition: Some(concrete!(Footprint)),
+							manual_composition: Some(concrete!(Context)),
 							implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::brush::BrushNode")),
 							..Default::default()
 						}]
@@ -1062,7 +1062,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 				document_node: DocumentNode {
 					implementation: DocumentNodeImplementation::proto("graphene_core::memo::MemoNode"),
 					inputs: vec![NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::default()), true)],
-					manual_composition: Some(concrete!(())),
+					manual_composition: Some(concrete!(Context)),
 					..Default::default()
 				},
 				persistent_node_metadata: DocumentNodePersistentMetadata {
@@ -1081,7 +1081,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 				document_node: DocumentNode {
 					implementation: DocumentNodeImplementation::proto("graphene_core::memo::ImpureMemoNode"),
 					inputs: vec![NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::default()), true)],
-					manual_composition: Some(concrete!(Footprint)),
+					manual_composition: Some(concrete!(Context)),
 					..Default::default()
 				},
 				persistent_node_metadata: DocumentNodePersistentMetadata {
@@ -1103,7 +1103,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 						nodes: vec![DocumentNode {
 							inputs: vec![NodeInput::network(concrete!(ImageFrameTable<Color>), 1)],
 							implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::CullNode")),
-							manual_composition: Some(concrete!(Footprint)),
+							manual_composition: Some(concrete!(Context)),
 							..Default::default()
 						}]
 						.into_iter()
@@ -1158,12 +1158,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 							},
 							DocumentNode {
 								inputs: vec![NodeInput::network(generic!(T), 0), NodeInput::node(NodeId(0), 0)],
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("wgpu_executor::UniformNode")),
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 								..Default::default()
@@ -1242,7 +1242,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 								..Default::default()
@@ -1321,7 +1321,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 								..Default::default()
@@ -1406,7 +1406,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 								..Default::default()
@@ -1515,7 +1515,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 								..Default::default()
@@ -1595,7 +1595,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(1), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 								..Default::default()
@@ -1665,13 +1665,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 						exports: vec![NodeInput::node(NodeId(1), 0)],
 						nodes: [
 							DocumentNode {
-								manual_composition: Some(concrete!(())),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::scope("editor-api")],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("wgpu_executor::CreateGpuSurfaceNode")),
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(Footprint)),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![NodeInput::node(NodeId(0), 0)],
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::ImpureMemoNode")),
 								..Default::default()
@@ -1736,7 +1736,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 								..Default::default()
 							},
 							DocumentNode {
-								manual_composition: Some(concrete!(Footprint)),
+								manual_composition: Some(concrete!(Context)),
 								inputs: vec![
 									NodeInput::network(concrete!(ShaderInputFrame), 0),
 									NodeInput::network(concrete!(Arc<wgpu_executor::Surface>), 1),
@@ -1947,35 +1947,36 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 		// Aims for interoperable compatibility with:
 		// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=levl%27%20%3D%20Levels-,%27curv%27%20%3D%20Curves,-%27expA%27%20%3D%20Exposure
 		// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Max%20input%20range-,Curves,-Curves%20settings%20files
-		DocumentNodeDefinition {
-			identifier: "Curves",
-			category: "Raster: Adjustment",
-			node_template: NodeTemplate {
-				document_node: DocumentNode {
-					implementation: DocumentNodeImplementation::proto("graphene_core::raster::CurvesNode"),
-					inputs: vec![
-						NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::default()), true),
-						NodeInput::value(TaggedValue::Curve(Default::default()), false),
-					],
-					..Default::default()
-				},
-				persistent_node_metadata: DocumentNodePersistentMetadata {
-					input_properties: vec!["Image".into(), "Curve".into()],
-					output_names: vec!["Image".to_string()],
-					..Default::default()
-				},
-			},
-			description: Cow::Borrowed("TODO"),
-			properties: None,
-		},
-		(*IMAGINATE_NODE).clone(),
+		// TODO: Fix this, it's currently broken
+		// DocumentNodeDefinition {
+		// 	identifier: "Curves",
+		// 	category: "Raster: Adjustment",
+		// 	node_template: NodeTemplate {
+		// 		document_node: DocumentNode {
+		// 			implementation: DocumentNodeImplementation::proto("graphene_core::raster::CurvesNode"),
+		// 			inputs: vec![
+		// 				NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::default()), true),
+		// 				NodeInput::value(TaggedValue::Curve(Default::default()), false),
+		// 			],
+		// 			..Default::default()
+		// 		},
+		// 		persistent_node_metadata: DocumentNodePersistentMetadata {
+		// 			input_properties: vec!["Image".into(), "Curve".into()],
+		// 			output_names: vec!["Image".to_string()],
+		// 			..Default::default()
+		// 		},
+		// 	},
+		// 	description: Cow::Borrowed("TODO"),
+		// 	properties: None,
+		// },
+		// (*IMAGINATE_NODE).clone(),
 		DocumentNodeDefinition {
 			identifier: "Line",
 			category: "Vector: Shape",
 			node_template: NodeTemplate {
 				document_node: DocumentNode {
 					implementation: DocumentNodeImplementation::proto("graphene_core::vector::generator_nodes::LineNode"),
-					manual_composition: Some(concrete!(())),
+					manual_composition: Some(concrete!(Context)),
 					inputs: vec![
 						NodeInput::value(TaggedValue::None, false),
 						NodeInput::value(TaggedValue::DVec2(DVec2::new(0., -50.)), false),
@@ -2089,7 +2090,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 			node_template: NodeTemplate {
 				document_node: DocumentNode {
 					implementation: DocumentNodeImplementation::proto("graphene_std::text::TextNode"),
-					manual_composition: Some(concrete!(())),
+					manual_composition: Some(concrete!(Context)),
 					inputs: vec![
 						NodeInput::scope("editor-api"),
 						NodeInput::value(TaggedValue::String("Lorem ipsum".to_string()), false),
@@ -2190,7 +2191,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 									NodeInput::network(concrete!(DVec2), 4),
 									NodeInput::network(concrete!(DVec2), 5),
 								],
-								manual_composition: Some(concrete!(Footprint)),
+								manual_composition: Some(concrete!(Context)),
 								implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::TransformNode")),
 								..Default::default()
 							},
@@ -2350,7 +2351,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 						NodeInput::value(TaggedValue::F64(0.), false),
 						NodeInput::value(TaggedValue::U32(0), false),
 					],
-					manual_composition: Some(concrete!(Footprint)),
+					manual_composition: Some(concrete!(Context)),
 					..Default::default()
 				},
 				persistent_node_metadata: DocumentNodePersistentMetadata {
@@ -2699,7 +2700,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 			.enumerate()
 			.map(|(index, (field, node_io_ty))| {
 				let ty = field.default_type.as_ref().unwrap_or(node_io_ty);
-				let exposed = if index == 0 { *ty != fn_type!(()) } else { field.exposed };
+				let exposed = if index == 0 { *ty != fn_type_fut!(Context, ()) } else { field.exposed };
 
 				match field.value_source {
 					RegistryValueSource::None => {}
@@ -2753,7 +2754,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
 
 pub static IMAGINATE_NODE: Lazy<DocumentNodeDefinition> = Lazy::new(|| DocumentNodeDefinition {
 	identifier: "Imaginate",
-	category: "Raster: Generator",
+	category: "Raster",
 	node_template: NodeTemplate {
 		document_node: DocumentNode {
 			implementation: DocumentNodeImplementation::Network(NodeNetwork {
@@ -2762,7 +2763,7 @@ pub static IMAGINATE_NODE: Lazy<DocumentNodeDefinition> = Lazy::new(|| DocumentN
 					DocumentNode {
 						inputs: vec![NodeInput::network(concrete!(ImageFrameTable<Color>), 0)],
 						implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
-						manual_composition: Some(concrete!(())),
+						manual_composition: Some(concrete!(Context)),
 						skip_deduplication: true,
 						..Default::default()
 					},
diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs
index 96c853e383..ee3c56f72f 100644
--- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs
+++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs
@@ -260,7 +260,7 @@ pub(crate) fn property_from_type(
 		}
 		Type::Generic(_) => vec![TextLabel::new("Generic type (not supported)").widget_holder()].into(),
 		Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, context),
-		Type::Future(_) => vec![TextLabel::new("Future type (not supported)").widget_holder()].into(),
+		Type::Future(out) => return property_from_type(node_id, index, out, number_options, context),
 	};
 
 	extra_widgets.push(widgets);
diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs
index 2e87897f21..8828dbc916 100644
--- a/editor/src/messages/portfolio/portfolio_message_handler.rs
+++ b/editor/src/messages/portfolio/portfolio_message_handler.rs
@@ -575,6 +575,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
 					};
 
 					let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else {
+						log::error!("could not get reference in deserialize_document");
 						continue;
 					};
 
@@ -584,6 +585,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
 					};
 					let inputs_count = node.inputs.len();
 
+					// Upgrade old nodes to use `Context` instead of `()` or `Footprint` for manual composition
+					if node.manual_composition == Some(graph_craft::concrete!(())) || node.manual_composition == Some(graph_craft::concrete!(graphene_std::transform::Footprint)) {
+						document.network_interface.set_manual_compostion(node_id, &[], graph_craft::concrete!(graphene_std::Context).into());
+					}
+
 					// Upgrade Fill nodes to the format change in #1778
 					if reference == "Fill" && inputs_count == 8 {
 						let node_definition = resolve_document_node_type(reference).unwrap();
diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs
index 28ab819b8e..00269dd477 100644
--- a/editor/src/node_graph_executor.rs
+++ b/editor/src/node_graph_executor.rs
@@ -15,6 +15,7 @@ use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment};
 use graphene_core::text::FontCache;
 use graphene_core::transform::Footprint;
 use graphene_core::vector::style::ViewMode;
+use graphene_core::Context;
 use graphene_std::renderer::{format_transform_matrix, RenderMetadata};
 use graphene_std::vector::{VectorData, VectorDataTable};
 use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
@@ -286,17 +287,17 @@ impl NodeRuntime {
 				continue;
 			};
 
-			if let Some(io) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElement>>() {
+			if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, graphene_core::GraphicElement>>() {
 				Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
 			} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), graphene_core::GraphicElement>>() {
 				Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
-			} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Footprint, graphene_core::Artboard>>() {
+			} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, graphene_core::Artboard>>() {
 				Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
 			} else if let Some(io) = introspected_data.downcast_ref::<IORecord<(), graphene_core::Artboard>>() {
 				Self::process_graphic_element(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
 			}
 			// Insert the vector modify if we are dealing with vector data
-			else if let Some(record) = introspected_data.downcast_ref::<IORecord<Footprint, VectorDataTable>>() {
+			else if let Some(record) = introspected_data.downcast_ref::<IORecord<Context, VectorDataTable>>() {
 				self.vector_modify.insert(parent_network_node_id, record.output.one_item().clone());
 			} else if let Some(record) = introspected_data.downcast_ref::<IORecord<(), VectorDataTable>>() {
 				self.vector_modify.insert(parent_network_node_id, record.output.one_item().clone());
diff --git a/libraries/dyn-any/src/lib.rs b/libraries/dyn-any/src/lib.rs
index 41e0c60506..2da3cd93b0 100644
--- a/libraries/dyn-any/src/lib.rs
+++ b/libraries/dyn-any/src/lib.rs
@@ -58,6 +58,13 @@ pub trait DynAny<'a>: 'a {
 	fn type_id(&self) -> TypeId;
 	#[cfg(feature = "log-bad-types")]
 	fn type_name(&self) -> &'static str;
+	fn reborrow_box<'short>(self: Box<Self>) -> Box<dyn DynAny<'short> + 'short>
+	where
+		'a: 'short;
+	fn reborrow_ref<'short>(&'a self) -> &'short (dyn DynAny<'short> + Send + Sync + 'short)
+	where
+		'a: 'short,
+		Self: Send + Sync;
 }
 
 impl<'a, T: StaticType + 'a> DynAny<'a> for T {
@@ -68,6 +75,20 @@ impl<'a, T: StaticType + 'a> DynAny<'a> for T {
 	fn type_name(&self) -> &'static str {
 		core::any::type_name::<T>()
 	}
+	fn reborrow_box<'short>(self: Box<Self>) -> Box<dyn DynAny<'short> + 'short>
+	where
+		'a: 'short,
+	{
+		self
+	}
+
+	fn reborrow_ref<'short>(&'a self) -> &'short (dyn DynAny<'short> + Send + Sync + 'short)
+	where
+		'a: 'short,
+		Self: Send + Sync,
+	{
+		self
+	}
 }
 pub fn downcast_ref<'a, V: StaticType + 'a>(i: &'a dyn DynAny<'a>) -> Option<&'a V> {
 	if i.type_id() == core::any::TypeId::of::<<V as StaticType>::Static>() {
diff --git a/node-graph/gcore/src/context.rs b/node-graph/gcore/src/context.rs
new file mode 100644
index 0000000000..3e21d722af
--- /dev/null
+++ b/node-graph/gcore/src/context.rs
@@ -0,0 +1,307 @@
+use crate::transform::Footprint;
+
+use core::{any::Any, borrow::Borrow, panic::Location};
+use std::sync::Arc;
+
+pub trait Ctx: Clone + Send {}
+
+pub trait ExtractFootprint {
+	#[track_caller]
+	fn try_footprint(&self) -> Option<&Footprint>;
+	#[track_caller]
+	fn footprint(&self) -> &Footprint {
+		self.try_footprint().unwrap_or_else(|| {
+			log::error!("Context did not have a footprint, called from: {}", Location::caller());
+			&const { Footprint::empty() }
+		})
+	}
+}
+
+pub trait ExtractTime {
+	fn try_time(&self) -> Option<f64>;
+}
+
+pub trait ExtractIndex {
+	fn try_index(&self) -> Option<usize>;
+}
+
+// Consider returning a slice or something like that
+pub trait ExtractVarArgs {
+	// Call this lifetime 'b so it is less likely to coflict when auto generating the function signature for implementation
+	fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult>;
+	fn varargs_len(&self) -> Result<usize, VarArgsResult>;
+}
+// Consider returning a slice or something like that
+pub trait CloneVarArgs: ExtractVarArgs {
+	// fn box_clone(&self) -> Vec<DynBox>;
+	fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>>;
+}
+
+pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractTime + ExtractVarArgs {}
+
+impl<T: ?Sized + ExtractFootprint + ExtractIndex + ExtractTime + ExtractVarArgs> ExtractAll for T {}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum VarArgsResult {
+	IndexOutOfBounds,
+	NoVarArgs,
+}
+impl<T: Ctx> Ctx for Option<T> {}
+impl<T: Ctx + Sync> Ctx for &T {}
+impl Ctx for () {}
+impl Ctx for Footprint {}
+impl ExtractFootprint for () {
+	fn try_footprint(&self) -> Option<&Footprint> {
+		log::error!("tried to extract footprint form (), {}", Location::caller());
+		None
+	}
+}
+
+impl<T: ExtractFootprint + Ctx + Sync + Send> ExtractFootprint for &T {
+	fn try_footprint(&self) -> Option<&Footprint> {
+		(*self).try_footprint()
+	}
+}
+
+impl<T: ExtractFootprint + Sync> ExtractFootprint for Option<T> {
+	fn try_footprint(&self) -> Option<&Footprint> {
+		self.as_ref().and_then(|x| x.try_footprint())
+	}
+	#[track_caller]
+	fn footprint(&self) -> &Footprint {
+		self.try_footprint().unwrap_or_else(|| {
+			log::warn!("trying to extract footprint from context None {} ", Location::caller());
+			&const { Footprint::empty() }
+		})
+	}
+}
+impl<T: ExtractTime + Sync> ExtractTime for Option<T> {
+	fn try_time(&self) -> Option<f64> {
+		self.as_ref().and_then(|x| x.try_time())
+	}
+}
+impl<T: ExtractIndex> ExtractIndex for Option<T> {
+	fn try_index(&self) -> Option<usize> {
+		self.as_ref().and_then(|x| x.try_index())
+	}
+}
+impl<T: ExtractVarArgs + Sync> ExtractVarArgs for Option<T> {
+	fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
+		let Some(ref inner) = self else { return Err(VarArgsResult::NoVarArgs) };
+		inner.vararg(index)
+	}
+
+	fn varargs_len(&self) -> Result<usize, VarArgsResult> {
+		let Some(ref inner) = self else { return Err(VarArgsResult::NoVarArgs) };
+		inner.varargs_len()
+	}
+}
+impl<T: ExtractFootprint + Sync> ExtractFootprint for Arc<T> {
+	fn try_footprint(&self) -> Option<&Footprint> {
+		(**self).try_footprint()
+	}
+}
+impl<T: ExtractTime + Sync> ExtractTime for Arc<T> {
+	fn try_time(&self) -> Option<f64> {
+		(**self).try_time()
+	}
+}
+impl<T: ExtractIndex> ExtractIndex for Arc<T> {
+	fn try_index(&self) -> Option<usize> {
+		(**self).try_index()
+	}
+}
+impl<T: ExtractVarArgs + Sync> ExtractVarArgs for Arc<T> {
+	fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
+		(**self).vararg(index)
+	}
+
+	fn varargs_len(&self) -> Result<usize, VarArgsResult> {
+		(**self).varargs_len()
+	}
+}
+impl<T: CloneVarArgs + Sync> CloneVarArgs for Option<T> {
+	fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
+		self.as_ref().and_then(CloneVarArgs::arc_clone)
+	}
+}
+
+impl<T: ExtractVarArgs + Sync> ExtractVarArgs for &T {
+	fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
+		(*self).vararg(index)
+	}
+
+	fn varargs_len(&self) -> Result<usize, VarArgsResult> {
+		(*self).varargs_len()
+	}
+}
+impl<T: CloneVarArgs + Sync> CloneVarArgs for Arc<T> {
+	fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
+		(**self).arc_clone()
+	}
+}
+
+impl Ctx for ContextImpl<'_> {}
+impl Ctx for Arc<OwnedContextImpl> {}
+
+impl ExtractFootprint for ContextImpl<'_> {
+	fn try_footprint(&self) -> Option<&Footprint> {
+		self.footprint
+	}
+}
+impl ExtractTime for ContextImpl<'_> {
+	fn try_time(&self) -> Option<f64> {
+		self.time
+	}
+}
+impl ExtractIndex for ContextImpl<'_> {
+	fn try_index(&self) -> Option<usize> {
+		self.index
+	}
+}
+impl ExtractVarArgs for ContextImpl<'_> {
+	fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
+		let Some(inner) = self.varargs else { return Err(VarArgsResult::NoVarArgs) };
+		inner.get(index).ok_or(VarArgsResult::IndexOutOfBounds).copied()
+	}
+
+	fn varargs_len(&self) -> Result<usize, VarArgsResult> {
+		let Some(inner) = self.varargs else { return Err(VarArgsResult::NoVarArgs) };
+		Ok(inner.len())
+	}
+}
+
+impl ExtractFootprint for OwnedContextImpl {
+	fn try_footprint(&self) -> Option<&Footprint> {
+		self.footprint.as_ref()
+	}
+}
+impl ExtractTime for OwnedContextImpl {
+	fn try_time(&self) -> Option<f64> {
+		self.time
+	}
+}
+impl ExtractIndex for OwnedContextImpl {
+	fn try_index(&self) -> Option<usize> {
+		self.index
+	}
+}
+impl ExtractVarArgs for OwnedContextImpl {
+	fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
+		let Some(ref inner) = self.varargs else {
+			let Some(ref parent) = self.parent else {
+				return Err(VarArgsResult::NoVarArgs);
+			};
+			return parent.vararg(index);
+		};
+		inner.get(index).map(|x| x.as_ref()).ok_or(VarArgsResult::IndexOutOfBounds)
+	}
+
+	fn varargs_len(&self) -> Result<usize, VarArgsResult> {
+		let Some(ref inner) = self.varargs else {
+			let Some(ref parent) = self.parent else {
+				return Err(VarArgsResult::NoVarArgs);
+			};
+			return parent.varargs_len();
+		};
+		Ok(inner.len())
+	}
+}
+
+impl CloneVarArgs for Arc<OwnedContextImpl> {
+	fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
+		Some(self.clone())
+	}
+}
+
+pub type Context<'a> = Option<Arc<OwnedContextImpl>>;
+type DynRef<'a> = &'a (dyn Any + Send + Sync);
+type DynBox = Box<dyn Any + Send + Sync>;
+
+#[derive(dyn_any::DynAny)]
+pub struct OwnedContextImpl {
+	footprint: Option<crate::transform::Footprint>,
+	varargs: Option<Arc<[DynBox]>>,
+	parent: Option<Arc<dyn ExtractVarArgs + Sync + Send>>,
+	// This could be converted into a single enum to save extra bytes
+	index: Option<usize>,
+	time: Option<f64>,
+}
+
+impl Default for OwnedContextImpl {
+	#[track_caller]
+	fn default() -> Self {
+		Self::empty()
+	}
+}
+
+impl core::hash::Hash for OwnedContextImpl {
+	fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
+		self.footprint.hash(state);
+		self.index.hash(state);
+		self.time.map(|x| x.to_bits()).hash(state);
+		self.parent.as_ref().map(|x| Arc::as_ptr(x).addr()).hash(state);
+		self.varargs.as_ref().map(|x| Arc::as_ptr(x).addr()).hash(state);
+	}
+}
+
+impl OwnedContextImpl {
+	#[track_caller]
+	pub fn from<T: ExtractAll + CloneVarArgs>(value: T) -> Self {
+		let footprint = value.try_footprint().copied();
+		let index = value.try_index();
+		let time = value.try_time();
+		let parent = value.arc_clone();
+		OwnedContextImpl {
+			footprint,
+			varargs: None,
+			parent,
+			index,
+			time,
+		}
+	}
+	pub const fn empty() -> Self {
+		OwnedContextImpl {
+			footprint: None,
+			varargs: None,
+			parent: None,
+			index: None,
+			time: None,
+		}
+	}
+}
+
+impl OwnedContextImpl {
+	pub fn set_footprint(&mut self, footprint: Footprint) {
+		self.footprint = Some(footprint);
+	}
+	pub fn with_footprint(mut self, footprint: Footprint) -> Self {
+		self.footprint = Some(footprint);
+		self
+	}
+	pub fn into_context(self) -> Option<Arc<Self>> {
+		Some(Arc::new(self))
+	}
+}
+
+#[derive(Default, Clone, Copy, dyn_any::DynAny)]
+pub struct ContextImpl<'a> {
+	pub(crate) footprint: Option<&'a crate::transform::Footprint>,
+	varargs: Option<&'a [DynRef<'a>]>,
+	// This could be converted into a single enum to save extra bytes
+	index: Option<usize>,
+	time: Option<f64>,
+}
+
+impl<'a> ContextImpl<'a> {
+	pub fn with_footprint<'f>(&self, new_footprint: &'f Footprint, varargs: Option<&'f impl (Borrow<[DynRef<'f>]>)>) -> ContextImpl<'f>
+	where
+		'a: 'f,
+	{
+		ContextImpl {
+			footprint: Some(new_footprint),
+			varargs: varargs.map(|x| x.borrow()),
+			..*self
+		}
+	}
+}
diff --git a/node-graph/gcore/src/generic.rs b/node-graph/gcore/src/generic.rs
index 49f3123f95..bd82d3f080 100644
--- a/node-graph/gcore/src/generic.rs
+++ b/node-graph/gcore/src/generic.rs
@@ -1,6 +1,7 @@
 use core::marker::PhantomData;
 
 use crate::Node;
+#[derive(Clone)]
 pub struct FnNode<T: Fn(I) -> O, I, O>(T, PhantomData<(I, O)>);
 
 impl<'i, T: Fn(I) -> O + 'i, O: 'i, I: 'i> Node<'i, I> for FnNode<T, I, O> {
diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs
index 331e30028a..4ffd545480 100644
--- a/node-graph/gcore/src/graphic_element.rs
+++ b/node-graph/gcore/src/graphic_element.rs
@@ -2,10 +2,10 @@ use crate::application_io::{TextureFrame, TextureFrameTable};
 use crate::instances::Instances;
 use crate::raster::image::{ImageFrame, ImageFrameTable};
 use crate::raster::BlendMode;
-use crate::transform::{ApplyTransform, Footprint, Transform, TransformMut};
+use crate::transform::{Transform, TransformMut};
 use crate::uuid::NodeId;
 use crate::vector::{VectorData, VectorDataTable};
-use crate::Color;
+use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
 
 use dyn_any::DynAny;
 
@@ -280,28 +280,8 @@ impl ArtboardGroup {
 }
 
 #[node_macro::node(category(""))]
-async fn layer<F: 'n + Send + Copy>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> GraphicGroupTable,
-		Footprint -> GraphicGroupTable,
-	)]
-	stack: impl Node<F, Output = GraphicGroupTable>,
-	#[implementations(
-		() -> GraphicElement,
-		Footprint -> GraphicElement,
-	)]
-	element: impl Node<F, Output = GraphicElement>,
-	node_path: Vec<NodeId>,
-) -> GraphicGroupTable {
-	let mut element = element.eval(footprint).await;
-	let stack = stack.eval(footprint).await;
-	let stack = stack.one_item();
-	let mut stack = stack.clone();
+async fn layer(_: impl Ctx, stack: GraphicGroupTable, mut element: GraphicElement, node_path: Vec<NodeId>) -> GraphicGroupTable {
+	let mut stack = stack.one_item().clone();
 
 	if stack.transform.matrix2.determinant() != 0. {
 		*element.transform_mut() = stack.transform.inverse() * element.transform();
@@ -318,72 +298,36 @@ async fn layer<F: 'n + Send + Copy>(
 }
 
 #[node_macro::node(category("Debug"))]
-async fn to_element<F: 'n + Send, Data: Into<GraphicElement> + 'n>(
+async fn to_element<Data: Into<GraphicElement> + 'n>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		(),
-		Footprint,
+		GraphicGroupTable,
+	 	VectorDataTable,
+		ImageFrameTable<Color>,
+	 	TextureFrameTable,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> GraphicGroupTable,
-	 	() -> VectorDataTable,
-		() -> ImageFrameTable<Color>,
-	 	() -> TextureFrameTable,
-	 	Footprint -> GraphicGroupTable,
-	 	Footprint -> VectorDataTable,
-		Footprint -> ImageFrameTable<Color>,
-	 	Footprint -> TextureFrameTable,
-	)]
-	data: impl Node<F, Output = Data>,
+	data: Data,
 ) -> GraphicElement {
-	data.eval(footprint).await.into()
+	data.into()
 }
 
 #[node_macro::node(category("General"))]
-async fn to_group<F: 'n + Send, Data: Into<GraphicGroupTable> + 'n>(
-	#[implementations(
-		(),
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+async fn to_group<Data: Into<GraphicGroupTable> + 'n>(
+	_: impl Ctx,
 	#[implementations(
-		() -> GraphicGroupTable,
-		() -> VectorDataTable,
-		() -> ImageFrameTable<Color>,
-		() -> TextureFrameTable,
-		Footprint -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> TextureFrameTable,
+		GraphicGroupTable,
+		VectorDataTable,
+		ImageFrameTable<Color>,
+		TextureFrameTable,
 	)]
-	element: impl Node<F, Output = Data>,
+	element: Data,
 ) -> GraphicGroupTable {
-	element.eval(footprint).await.into()
+	element.into()
 }
 
 #[node_macro::node(category("General"))]
-async fn flatten_group<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> GraphicGroupTable,
-		Footprint -> GraphicGroupTable,
-	)]
-	group: impl Node<F, Output = GraphicGroupTable>,
-	fully_flatten: bool,
-) -> GraphicGroupTable {
-	let nested_group = group.eval(footprint).await;
-	let nested_group = nested_group.one_item();
-	let nested_group = nested_group.clone();
+async fn flatten_group(_: impl Ctx, group: GraphicGroupTable, fully_flatten: bool) -> GraphicGroupTable {
+	let nested_group = group.one_item().clone();
 
 	let mut flat_group = GraphicGroup::default();
 
@@ -420,34 +364,28 @@ async fn flatten_group<F: 'n + Send>(
 }
 
 #[node_macro::node(category(""))]
-async fn to_artboard<F: 'n + Send + ApplyTransform, Data: Into<GraphicGroupTable> + 'n>(
-	#[implementations(
-		(),
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	mut footprint: F,
+async fn to_artboard<Data: Into<GraphicGroupTable> + 'n>(
+	ctx: impl ExtractAll + CloneVarArgs + Ctx,
 	#[implementations(
-		() -> GraphicGroupTable,
-		() -> VectorDataTable,
-		() -> ImageFrameTable<Color>,
-		() -> TextureFrame,
-		Footprint -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> TextureFrame,
+		Context -> GraphicGroupTable,
+		Context -> VectorDataTable,
+		Context -> ImageFrameTable<Color>,
+		Context -> TextureFrameTable,
 	)]
-	contents: impl Node<F, Output = Data>,
+	contents: impl Node<Context<'static>, Output = Data>,
 	label: String,
 	location: IVec2,
 	dimensions: IVec2,
 	background: Color,
 	clip: bool,
 ) -> Artboard {
-	footprint.apply_transform(&DAffine2::from_translation(location.as_dvec2()));
-	let graphic_group = contents.eval(footprint).await;
+	let footprint = ctx.try_footprint().copied();
+	let mut new_ctx = OwnedContextImpl::from(ctx);
+	if let Some(mut footprint) = footprint {
+		footprint.translate(location.as_dvec2());
+		new_ctx = new_ctx.with_footprint(footprint);
+	}
+	let graphic_group = contents.eval(new_ctx.into_context()).await;
 
 	Artboard {
 		graphic_group: graphic_group.into(),
@@ -460,28 +398,16 @@ async fn to_artboard<F: 'n + Send + ApplyTransform, Data: Into<GraphicGroupTable
 }
 
 #[node_macro::node(category(""))]
-async fn append_artboard<F: 'n + Send + Copy>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> ArtboardGroup,
-		Footprint -> ArtboardGroup,
-	)]
-	artboards: impl Node<F, Output = ArtboardGroup>,
-	#[implementations(
-		() -> Artboard,
-		Footprint -> Artboard,
-	)]
-	artboard: impl Node<F, Output = Artboard>,
-	node_path: Vec<NodeId>,
-) -> ArtboardGroup {
-	let artboard = artboard.eval(footprint).await;
-	let mut artboards = artboards.eval(footprint).await;
-
+async fn append_artboard(ctx: impl Ctx, mut artboards: ArtboardGroup, artboard: Artboard, node_path: Vec<NodeId>) -> ArtboardGroup {
+	// let mut artboards = artboards.eval(ctx.clone()).await;
+	// let artboard = artboard.eval(ctx).await;
+	// let foot = ctx.footprint();
+	// log::debug!("{:?}", foot);
 	// Get the penultimate element of the node path, or None if the path is too short
+
+	// TODO: Delete this line
+	let _ctx = ctx;
+
 	let encapsulating_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
 	artboards.append_artboard(artboard, encapsulating_node_id);
 
diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs
index 5883fbb9c5..e419bc4cf5 100644
--- a/node-graph/gcore/src/lib.rs
+++ b/node-graph/gcore/src/lib.rs
@@ -14,6 +14,7 @@ pub use crate as graphene_core;
 pub use ctor;
 
 pub mod consts;
+pub mod context;
 pub mod generic;
 pub mod instances;
 pub mod logic;
@@ -48,6 +49,7 @@ pub mod application_io;
 #[cfg(feature = "reflections")]
 pub mod registry;
 
+pub use context::*;
 use core::any::TypeId;
 pub use memo::MemoHash;
 pub use raster::Color;
@@ -56,7 +58,7 @@ pub use types::Cow;
 // pub trait Node: for<'n> NodeIO<'n> {
 /// The node trait allows for defining any node. Nodes can only take one call argument input, however they can store references to other nodes inside the struct.
 /// See `node-graph/README.md` for information on how to define a new node.
-pub trait Node<'i, Input: 'i>: 'i {
+pub trait Node<'i, Input> {
 	type Output: 'i;
 	/// Evaluates the node with the single specified input.
 	fn eval(&'i self, input: Input) -> Self::Output;
@@ -79,10 +81,10 @@ mod types;
 #[cfg(feature = "alloc")]
 pub use types::*;
 
-pub trait NodeIO<'i, Input: 'i>: 'i + Node<'i, Input>
+pub trait NodeIO<'i, Input>: Node<'i, Input>
 where
 	Self::Output: 'i + StaticTypeSized,
-	Input: 'i + StaticTypeSized,
+	Input: StaticTypeSized,
 {
 	fn input_type(&self) -> TypeId {
 		TypeId::of::<Input::Static>()
@@ -112,8 +114,7 @@ where
 	{
 		NodeIOTypes {
 			call_argument: concrete!(<Input as StaticTypeSized>::Static),
-			// TODO return actual future type
-			return_value: concrete!(<<Self::Output as Future>::Output as StaticTypeSized>::Static),
+			return_value: future!(<<Self::Output as Future>::Output as StaticTypeSized>::Static),
 			inputs,
 		}
 	}
@@ -122,7 +123,7 @@ where
 impl<'i, N: Node<'i, I>, I> NodeIO<'i, I> for N
 where
 	N::Output: 'i + StaticTypeSized,
-	I: 'i + StaticTypeSized,
+	I: StaticTypeSized,
 {
 }
 
@@ -152,13 +153,13 @@ use dyn_any::StaticTypeSized;
 use core::pin::Pin;
 
 #[cfg(feature = "alloc")]
-impl<'i, I: 'i, O: 'i> Node<'i, I> for Pin<Box<dyn Node<'i, I, Output = O> + 'i>> {
+impl<'i, I, O: 'i> Node<'i, I> for Pin<Box<dyn Node<'i, I, Output = O> + 'i>> {
 	type Output = O;
 	fn eval(&'i self, input: I) -> O {
 		(**self).eval(input)
 	}
 }
-impl<'i, I: 'i, O: 'i> Node<'i, I> for Pin<&'i (dyn NodeIO<'i, I, Output = O> + 'i)> {
+impl<'i, I, O: 'i> Node<'i, I> for Pin<&'i (dyn NodeIO<'i, I, Output = O> + 'i)> {
 	type Output = O;
 	fn eval(&'i self, input: I) -> O {
 		(**self).eval(input)
diff --git a/node-graph/gcore/src/logic.rs b/node-graph/gcore/src/logic.rs
index 5911109858..2abdc723ee 100644
--- a/node-graph/gcore/src/logic.rs
+++ b/node-graph/gcore/src/logic.rs
@@ -1,57 +1,40 @@
-use crate::transform::Footprint;
 use crate::vector::VectorDataTable;
-
+use crate::Context;
+use crate::Ctx;
 use glam::{DAffine2, DVec2};
 
 #[node_macro::node(category("Debug"))]
-async fn log_to_console<T: core::fmt::Debug, F: Send + 'n>(
-	#[implementations((), (), (), (), (), (), (), (), Footprint)] footprint: F,
-	#[implementations(
-		() -> String, () -> bool, () -> f64, () -> u32, () -> u64, () -> DVec2, () -> VectorDataTable, () -> DAffine2,
-		Footprint -> String, Footprint -> bool, Footprint -> f64, Footprint -> u32, Footprint -> u64, Footprint -> DVec2, Footprint -> VectorDataTable, Footprint -> DAffine2,
-	)]
-	value: impl Node<F, Output = T>,
-) -> T {
+fn log_to_console<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> T {
 	#[cfg(not(target_arch = "spirv"))]
 	// KEEP THIS `debug!()` - It acts as the output for the debug node itself
-	let value = value.eval(footprint).await;
 	debug!("{:#?}", value);
 	value
 }
 
-#[node_macro::node(category("Debug"))]
-async fn to_string<T: core::fmt::Debug + 'n, F: Send + 'n>(
-	#[implementations((), (), (), (), (), (), Footprint)] footprint: F,
-	#[implementations(
-		() -> String, () -> bool, () -> f64, () -> u32, () -> u64, () -> DVec2,
-		Footprint -> String, Footprint -> bool, Footprint -> f64, Footprint -> u32, Footprint -> u64, Footprint -> DVec2,
-	)]
-	value: impl Node<F, Output = T>,
-) -> String {
-	let value = value.eval(footprint).await;
+#[node_macro::node(category("Debug"), skip_impl)]
+fn to_string<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String {
 	format!("{:?}", value)
 }
 
 #[node_macro::node(category("Debug"))]
-async fn switch<T, F: Send + 'n>(
-	#[implementations((), (), (), (), (), (), (), (), Footprint)] footprint: F,
+async fn switch<T, C: Send + 'n + Clone>(
+	#[implementations(Context)] ctx: C,
 	condition: bool,
 	#[expose]
 	#[implementations(
-		() -> String, () -> bool, () -> f64, () -> u32, () -> u64, () -> DVec2, () -> VectorDataTable, () -> DAffine2,
-		Footprint -> String, Footprint -> bool, Footprint -> f64, Footprint -> u32, Footprint -> u64, Footprint -> DVec2, Footprint -> VectorDataTable, Footprint -> DAffine2
+		Context -> String, Context -> bool, Context -> f64, Context -> u32, Context -> u64, Context -> DVec2, Context -> VectorDataTable, Context -> DAffine2,
 	)]
-	if_true: impl Node<F, Output = T>,
+	if_true: impl Node<C, Output = T>,
 	#[expose]
 	#[implementations(
-		() -> String, () -> bool, () -> f64, () -> u32, () -> u64, () -> DVec2, () -> VectorDataTable, () -> DAffine2,
-		Footprint -> String, Footprint -> bool, Footprint -> f64, Footprint -> u32, Footprint -> u64, Footprint -> DVec2, Footprint -> VectorDataTable, Footprint -> DAffine2
+		Context -> String, Context -> bool, Context -> f64, Context -> u32, Context -> u64, Context -> DVec2, Context -> VectorDataTable, Context -> DAffine2,
 	)]
-	if_false: impl Node<F, Output = T>,
+	if_false: impl Node<C, Output = T>,
 ) -> T {
 	if condition {
-		if_true.eval(footprint).await
+		// We can't remove these calls because we only want to evaluate the branch that we actually need
+		if_true.eval(ctx).await
 	} else {
-		if_false.eval(footprint).await
+		if_false.eval(ctx).await
 	}
 }
diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs
index e2d26566a3..a44f379eea 100644
--- a/node-graph/gcore/src/ops.rs
+++ b/node-graph/gcore/src/ops.rs
@@ -2,6 +2,7 @@ use crate::raster::image::ImageFrameTable;
 use crate::raster::BlendMode;
 use crate::registry::types::Percentage;
 use crate::vector::style::GradientStops;
+use crate::Ctx;
 use crate::{Color, Node};
 
 use math_parser::ast;
@@ -39,7 +40,7 @@ impl ValueProvider for MathNodeContext {
 /// Calculates a mathematical expression with input values "A" and "B"
 #[node_macro::node(category("General"), properties("math_properties"))]
 fn math<U: num_traits::float::Float>(
-	_: (),
+	_: impl Ctx,
 	/// The value of "A" when calculating the expression
 	#[implementations(f64, f32)]
 	operand_a: U,
@@ -84,7 +85,7 @@ fn math<U: num_traits::float::Float>(
 /// The addition operation (+) calculates the sum of two numbers.
 #[node_macro::node(category("Math: Arithmetic"))]
 fn add<U: Add<T>, T>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] augend: U,
 	#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] addend: T,
 ) -> <U as Add<T>>::Output {
@@ -94,7 +95,7 @@ fn add<U: Add<T>, T>(
 /// The subtraction operation (-) calculates the difference between two numbers.
 #[node_macro::node(category("Math: Arithmetic"))]
 fn subtract<U: Sub<T>, T>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] minuend: U,
 	#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)] subtrahend: T,
 ) -> <U as Sub<T>>::Output {
@@ -104,7 +105,7 @@ fn subtract<U: Sub<T>, T>(
 /// The multiplication operation (×) calculates the product of two numbers.
 #[node_macro::node(category("Math: Arithmetic"))]
 fn multiply<U: Mul<T>, T>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, f64, DVec2)] multiplier: U,
 	#[default(1.)]
 	#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, DVec2, f64)]
@@ -116,7 +117,7 @@ fn multiply<U: Mul<T>, T>(
 /// The division operation (÷) calculates the quotient of two numbers.
 #[node_macro::node(category("Math: Arithmetic"))]
 fn divide<U: Div<T>, T>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, DVec2, f64)] numerator: U,
 	#[default(1.)]
 	#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, f64, DVec2)]
@@ -128,7 +129,7 @@ fn divide<U: Div<T>, T>(
 /// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled.
 #[node_macro::node(category("Math: Arithmetic"))]
 fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32, DVec2, DVec2, f64)] numerator: U,
 	#[default(2.)]
 	#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, f64, DVec2)]
@@ -145,7 +146,7 @@ fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy
 /// The exponent operation (^) calculates the result of raising a number to a power.
 #[node_macro::node(category("Math: Arithmetic"))]
 fn exponent<U: Pow<T>, T>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f64, &f64, f32, &f32, f32, &f32, u32, &u32, u32, &u32)] base: U,
 	#[default(2.)]
 	#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32)]
@@ -157,7 +158,7 @@ fn exponent<U: Pow<T>, T>(
 /// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n.
 #[node_macro::node(category("Math: Arithmetic"))]
 fn root<U: num_traits::float::Float>(
-	_: (),
+	_: impl Ctx,
 	#[default(2.)]
 	#[implementations(f64, f32)]
 	radicand: U,
@@ -177,7 +178,7 @@ fn root<U: num_traits::float::Float>(
 /// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e".
 #[node_macro::node(category("Math: Arithmetic"))]
 fn logarithm<U: num_traits::float::Float>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, f32)] value: U,
 	#[default(2.)]
 	#[implementations(f64, f32)]
@@ -196,7 +197,7 @@ fn logarithm<U: num_traits::float::Float>(
 
 /// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length.
 #[node_macro::node(category("Math: Trig"))]
-fn sine<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] theta: U, radians: bool) -> U {
+fn sine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
 	if radians {
 		theta.sin()
 	} else {
@@ -206,7 +207,7 @@ fn sine<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] theta:
 
 /// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
 #[node_macro::node(category("Math: Trig"))]
-fn cosine<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] theta: U, radians: bool) -> U {
+fn cosine<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
 	if radians {
 		theta.cos()
 	} else {
@@ -216,7 +217,7 @@ fn cosine<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] theta
 
 /// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length.
 #[node_macro::node(category("Math: Trig"))]
-fn tangent<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] theta: U, radians: bool) -> U {
+fn tangent<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] theta: U, radians: bool) -> U {
 	if radians {
 		theta.tan()
 	} else {
@@ -226,7 +227,7 @@ fn tangent<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] thet
 
 /// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value.
 #[node_macro::node(category("Math: Trig"))]
-fn sine_inverse<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U, radians: bool) -> U {
+fn sine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
 	if radians {
 		value.asin()
 	} else {
@@ -236,7 +237,7 @@ fn sine_inverse<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)]
 
 /// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value.
 #[node_macro::node(category("Math: Trig"))]
-fn cosine_inverse<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U, radians: bool) -> U {
+fn cosine_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
 	if radians {
 		value.acos()
 	} else {
@@ -246,7 +247,7 @@ fn cosine_inverse<U: num_traits::float::Float>(_: (), #[implementations(f64, f32
 
 /// The inverse tangent trigonometric function (atan) calculates the angle whose tangent is the specified value.
 #[node_macro::node(category("Math: Trig"))]
-fn tangent_inverse<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U, radians: bool) -> U {
+fn tangent_inverse<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U, radians: bool) -> U {
 	if radians {
 		value.atan()
 	} else {
@@ -257,7 +258,7 @@ fn tangent_inverse<U: num_traits::float::Float>(_: (), #[implementations(f64, f3
 /// The inverse tangent trigonometric function (atan2) calculates the angle whose tangent is the ratio of the two specified values.
 #[node_macro::node(name("Tangent Inverse 2-Argument"), category("Math: Trig"))]
 fn tangent_inverse_2_argument<U: num_traits::float::Float>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, f32)] y: U,
 	#[expose]
 	#[implementations(f64, f32)]
@@ -274,7 +275,7 @@ fn tangent_inverse_2_argument<U: num_traits::float::Float>(
 /// The random function (rand) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
 #[node_macro::node(category("Math: Numeric"))]
 fn random<U: num_traits::float::Float>(
-	_: (),
+	_: impl Ctx,
 	_primary: (),
 	seed: u64,
 	#[implementations(f64, f32)]
@@ -293,45 +294,45 @@ fn random<U: num_traits::float::Float>(
 
 /// Convert a number to an integer of the type u32, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
 #[node_macro::node(name("To u32"), category("Math: Numeric"))]
-fn to_u32<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U) -> u32 {
+fn to_u32<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u32 {
 	let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap());
 	value.to_u32().unwrap()
 }
 
 /// Convert a number to an integer of the type u64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
 #[node_macro::node(name("To u64"), category("Math: Numeric"))]
-fn to_u64<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U) -> u64 {
+fn to_u64<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> u64 {
 	let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap());
 	value.to_u64().unwrap()
 }
 
 /// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
 #[node_macro::node(category("Math: Numeric"))]
-fn round<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U) -> U {
+fn round<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
 	value.round()
 }
 
 /// The floor function (floor) reduces an input value to its nearest larger whole number, unless the input number is already whole.
 #[node_macro::node(category("Math: Numeric"))]
-fn floor<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U) -> U {
+fn floor<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
 	value.floor()
 }
 
 /// The ceiling function (ceil) increases an input value to its nearest smaller whole number, unless the input number is already whole.
 #[node_macro::node(category("Math: Numeric"))]
-fn ceiling<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U) -> U {
+fn ceiling<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
 	value.ceil()
 }
 
 /// The absolute value function (abs) removes the negative sign from an input value, if present.
 #[node_macro::node(category("Math: Numeric"))]
-fn absolute_value<U: num_traits::float::Float>(_: (), #[implementations(f64, f32)] value: U) -> U {
+fn absolute_value<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
 	value.abs()
 }
 
 /// The minimum function (min) picks the smaller of two numbers.
 #[node_macro::node(category("Math: Numeric"))]
-fn min<T: core::cmp::PartialOrd>(_: (), #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
+fn min<T: core::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
 	match value < other_value {
 		true => value,
 		false => other_value,
@@ -340,7 +341,7 @@ fn min<T: core::cmp::PartialOrd>(_: (), #[implementations(f64, &f64, f32, &f32,
 
 /// The maximum function (max) picks the larger of two numbers.
 #[node_macro::node(category("Math: Numeric"))]
-fn max<T: core::cmp::PartialOrd>(_: (), #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
+fn max<T: core::cmp::PartialOrd>(_: impl Ctx, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T, #[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] other_value: T) -> T {
 	match value > other_value {
 		true => value,
 		false => other_value,
@@ -350,7 +351,7 @@ fn max<T: core::cmp::PartialOrd>(_: (), #[implementations(f64, &f64, f32, &f32,
 /// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
 #[node_macro::node(category("Math: Numeric"))]
 fn clamp<T: core::cmp::PartialOrd>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] value: T,
 	#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] min: T,
 	#[implementations(f64, &f64, f32, &f32, u32, &u32, &str)] max: T,
@@ -368,7 +369,7 @@ fn clamp<T: core::cmp::PartialOrd>(
 /// The equality operation (==) compares two values and returns true if they are equal, or false if they are not.
 #[node_macro::node(category("Math: Logic"))]
 fn equals<U: core::cmp::PartialEq<T>, T>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
 	#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)]
 	#[min(100.)]
@@ -381,7 +382,7 @@ fn equals<U: core::cmp::PartialEq<T>, T>(
 /// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are.
 #[node_macro::node(category("Math: Logic"))]
 fn not_equals<U: core::cmp::PartialEq<T>, T>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
 	#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)]
 	#[min(100.)]
@@ -393,98 +394,98 @@ fn not_equals<U: core::cmp::PartialEq<T>, T>(
 
 /// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false.
 #[node_macro::node(category("Math: Logic"))]
-fn logical_or(_: (), value: bool, other_value: bool) -> bool {
+fn logical_or(_: impl Ctx, value: bool, other_value: bool) -> bool {
 	value || other_value
 }
 
 /// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false.
 #[node_macro::node(category("Math: Logic"))]
-fn logical_and(_: (), value: bool, other_value: bool) -> bool {
+fn logical_and(_: impl Ctx, value: bool, other_value: bool) -> bool {
 	value && other_value
 }
 
 /// The logical not operation (!) reverses true and false value of the input.
 #[node_macro::node(category("Math: Logic"))]
-fn logical_not(_: (), input: bool) -> bool {
+fn logical_not(_: impl Ctx, input: bool) -> bool {
 	!input
 }
 
 /// Constructs a bool value which may be set to true or false.
 #[node_macro::node(category("Value"))]
-fn bool_value(_: (), _primary: (), #[name("Bool")] bool_value: bool) -> bool {
+fn bool_value(_: impl Ctx, _primary: (), #[name("Bool")] bool_value: bool) -> bool {
 	bool_value
 }
 
 /// Constructs a number value which may be set to any real number.
 #[node_macro::node(category("Value"))]
-fn number_value(_: (), _primary: (), number: f64) -> f64 {
+fn number_value(_: impl Ctx, _primary: (), number: f64) -> f64 {
 	number
 }
 
 /// Constructs a number value which may be set to any value from 0% to 100% by dragging the slider.
 #[node_macro::node(category("Value"))]
-fn percentage_value(_: (), _primary: (), percentage: Percentage) -> f64 {
+fn percentage_value(_: impl Ctx, _primary: (), percentage: Percentage) -> f64 {
 	percentage
 }
 
 /// Constructs a two-dimensional vector value which may be set to any XY coordinate.
-#[node_macro::node(category("Value"))]
-fn vector2_value(_: (), _primary: (), x: f64, y: f64) -> DVec2 {
+#[node_macro::node(name("Vector2 Value"), category("Value"))]
+fn vector2_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
 	DVec2::new(x, y)
 }
 
 /// Constructs a color value which may be set to any color, or no color.
 #[node_macro::node(category("Value"))]
-fn color_value(_: (), _primary: (), #[default(Color::BLACK)] color: Option<Color>) -> Option<Color> {
+fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Option<Color>) -> Option<Color> {
 	color
 }
 
 /// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
 #[node_macro::node(category("Value"))]
-fn gradient_value(_: (), _primary: (), gradient: GradientStops) -> GradientStops {
+fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> GradientStops {
 	gradient
 }
 
 /// Constructs a blend mode choice value which may be set to any of the available blend modes in order to tell another node which blending operation to use.
 #[node_macro::node(category("Value"))]
-fn blend_mode_value(_: (), _primary: (), blend_mode: BlendMode) -> BlendMode {
+fn blend_mode_value(_: impl Ctx, _primary: (), blend_mode: BlendMode) -> BlendMode {
 	blend_mode
 }
 
 /// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
 #[cfg(feature = "std")]
 #[node_macro::node(category("Debug"))]
-fn size_of(_: (), ty: crate::Type) -> Option<usize> {
+fn size_of(_: impl Ctx, ty: crate::Type) -> Option<usize> {
 	ty.size()
 }
 
 /// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
 #[node_macro::node(category("Debug"))]
-fn some<T>(_: (), #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
+fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String, Color)] input: T) -> Option<T> {
 	Some(input)
 }
 
 /// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
 #[node_macro::node(category("Debug"))]
-fn unwrap<T: Default>(_: (), #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
+fn unwrap<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<f32>, Option<u32>, Option<u64>, Option<String>, Option<Color>)] input: Option<T>) -> T {
 	input.unwrap_or_default()
 }
 
 /// Meant for debugging purposes, not general use. Clones the input value.
 #[node_macro::node(category("Debug"))]
-fn clone<'i, T: Clone + 'i>(_: (), #[implementations(&ImageFrameTable<Color>)] value: &'i T) -> T {
+fn clone<'i, T: Clone + 'i>(_: impl Ctx, #[implementations(&ImageFrameTable<Color>)] value: &'i T) -> T {
 	value.clone()
 }
 
 #[node_macro::node(category("Math: Vector"))]
-fn dot_product(vector_a: DVec2, vector_b: DVec2) -> f64 {
+fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
 	vector_a.dot(vector_b)
 }
 
 // TODO: Rename to "Passthrough"
 /// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
 #[node_macro::node(skip_impl)]
-fn identity<'i, T: 'i>(value: T) -> T {
+fn identity<'i, T: 'i + Send>(value: T) -> T {
 	value
 }
 
@@ -537,13 +538,13 @@ where
 #[cfg(test)]
 mod test {
 	use super::*;
-	use crate::{generic::*, structural::*, value::*};
+	use crate::generic::*;
 
 	#[test]
 	pub fn dot_product_function() {
 		let vector_a = glam::DVec2::new(1., 2.);
 		let vector_b = glam::DVec2::new(3., 4.);
-		assert_eq!(dot_product(vector_a, vector_b), 11.);
+		assert_eq!(dot_product((), vector_a, vector_b), 11.);
 	}
 
 	#[test]
@@ -572,8 +573,7 @@ mod test {
 
 	#[test]
 	pub fn identity_node() {
-		let value = ValueNode(4u32).then(IdentityNode::new());
-		assert_eq!(value.eval(()), &4);
+		assert_eq!(identity(&4), &4);
 	}
 
 	#[test]
diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs
index 2abc76cee4..3e35afbd02 100644
--- a/node-graph/gcore/src/raster.rs
+++ b/node-graph/gcore/src/raster.rs
@@ -1,8 +1,8 @@
 pub use self::color::{Color, Luma, SRGBA8};
 use crate::raster::image::ImageFrameTable;
 use crate::registry::types::Percentage;
-use crate::transform::Footprint;
 use crate::vector::VectorDataTable;
+use crate::Ctx;
 use crate::GraphicGroupTable;
 
 use bytemuck::{Pod, Zeroable};
@@ -182,6 +182,9 @@ pub trait Alpha {
 	}
 	fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
 }
+pub trait AlphaMut: Alpha {
+	fn set_alpha(&mut self, value: Self::AlphaChannel);
+}
 
 pub trait Depth {
 	type DepthChannel: Channel;
@@ -228,6 +231,12 @@ pub trait Bitmap {
 	type Pixel: Pixel;
 	fn width(&self) -> u32;
 	fn height(&self) -> u32;
+	fn dimensions(&self) -> (u32, u32) {
+		(self.width(), self.height())
+	}
+	fn dim(&self) -> (u32, u32) {
+		self.dimensions()
+	}
 	fn get_pixel(&self, x: u32, y: u32) -> Option<Self::Pixel>;
 }
 
@@ -316,51 +325,31 @@ impl SetBlendMode for ImageFrameTable<Color> {
 }
 
 #[node_macro::node(category("Style"))]
-async fn blend_mode<F: 'n + Send, T: SetBlendMode>(
+fn blend_mode<T: SetBlendMode>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		GraphicGroupTable,
+		VectorDataTable,
+		ImageFrameTable<Color>,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> GraphicGroupTable,
-		() -> VectorDataTable,
-		() -> ImageFrameTable<Color>,
-		Footprint -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> ImageFrameTable<Color>,
-	)]
-	value: impl Node<F, Output = T>,
+	mut value: T,
 	blend_mode: BlendMode,
 ) -> T {
-	let mut value = value.eval(footprint).await;
 	value.set_blend_mode(blend_mode);
 	value
 }
 
 #[node_macro::node(category("Style"))]
-async fn opacity<F: 'n + Send, T: MultiplyAlpha>(
-	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+fn opacity<T: MultiplyAlpha>(
+	_: impl Ctx,
 	#[implementations(
-		() -> GraphicGroupTable,
-		() -> VectorDataTable,
-		() -> ImageFrameTable<Color>,
-		Footprint -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> ImageFrameTable<Color>,
+		GraphicGroupTable,
+		VectorDataTable,
+		ImageFrameTable<Color>,
 	)]
-	value: impl Node<F, Output = T>,
+	mut value: T,
 	#[default(100.)] factor: Percentage,
 ) -> T {
-	let mut value = value.eval(footprint).await;
 	let opacity_multiplier = factor / 100.;
 	value.multiply_alpha(opacity_multiplier);
 	value
diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs
index 6ee2f36c7c..9e875a8f5a 100644
--- a/node-graph/gcore/src/raster/adjustments.rs
+++ b/node-graph/gcore/src/raster/adjustments.rs
@@ -6,9 +6,9 @@ use crate::raster::curve::{Curve, CurveManipulatorGroup, ValueMapperNode};
 use crate::raster::image::{ImageFrame, ImageFrameTable};
 use crate::raster::{Channel, Color, Pixel};
 use crate::registry::types::{Angle, Percentage, SignedPercentage};
-use crate::transform::Footprint;
 use crate::vector::style::GradientStops;
 use crate::vector::VectorDataTable;
+use crate::{Ctx, Node};
 use crate::{GraphicElement, GraphicGroupTable};
 
 use dyn_any::DynAny;
@@ -284,26 +284,16 @@ impl From<BlendMode> for vello::peniko::Mix {
 }
 
 #[node_macro::node(category("Raster: Adjustment"))]
-async fn luminance<F: 'n + Send, T: Adjust<Color>>(
+fn luminance<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	input: impl Node<F, Output = T>,
+	mut input: T,
 	luminance_calc: LuminanceCalculation,
 ) -> T {
-	let mut input = input.eval(footprint).await;
 	input.adjust(|color| {
 		let luminance = match luminance_calc {
 			LuminanceCalculation::SRGB => color.luminance_srgb(),
@@ -318,26 +308,16 @@ async fn luminance<F: 'n + Send, T: Adjust<Color>>(
 }
 
 #[node_macro::node(category("Raster"))]
-async fn extract_channel<F: 'n + Send, T: Adjust<Color>>(
-	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+fn extract_channel<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	input: impl Node<F, Output = T>,
+	mut input: T,
 	channel: RedGreenBlueAlpha,
 ) -> T {
-	let mut input = input.eval(footprint).await;
 	input.adjust(|color| {
 		let extracted_value = match channel {
 			RedGreenBlueAlpha::Red => color.r(),
@@ -351,25 +331,15 @@ async fn extract_channel<F: 'n + Send, T: Adjust<Color>>(
 }
 
 #[node_macro::node(category("Raster"))]
-async fn make_opaque<F: 'n + Send, T: Adjust<Color>>(
+fn make_opaque<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	input: impl Node<F, Output = T>,
+	mut input: T,
 ) -> T {
-	let mut input = input.eval(footprint).await;
 	input.adjust(|color| {
 		if color.a() == 0. {
 			return color.with_alpha(1.);
@@ -385,31 +355,21 @@ async fn make_opaque<F: 'n + Send, T: Adjust<Color>>(
 // Algorithm from:
 // https://stackoverflow.com/questions/39510072/algorithm-for-adjustment-of-image-levels
 #[node_macro::node(category("Raster: Adjustment"))]
-async fn levels<F: 'n + Send, T: Adjust<Color>>(
-	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+fn levels<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	image: impl Node<F, Output = T>,
+	mut image: T,
 	#[default(0.)] shadows: Percentage,
 	#[default(50.)] midtones: Percentage,
 	#[default(100.)] highlights: Percentage,
 	#[default(0.)] output_minimums: Percentage,
 	#[default(100.)] output_maximums: Percentage,
 ) -> T {
-	let mut input = image.eval(footprint).await;
-	input.adjust(|color| {
+	image.adjust(|color| {
 		let color = color.to_gamma_srgb();
 
 		// Input Range (Range: 0-1)
@@ -451,7 +411,7 @@ async fn levels<F: 'n + Send, T: Adjust<Color>>(
 
 		color.to_linear_srgb()
 	});
-	input
+	image
 }
 
 // Aims for interoperable compatibility with:
@@ -462,23 +422,14 @@ async fn levels<F: 'n + Send, T: Adjust<Color>>(
 // https://stackoverflow.com/a/55233732/775283
 // Works the same for gamma and linear color
 #[node_macro::node(name("Black & White"), category("Raster: Adjustment"))]
-async fn black_and_white<F: 'n + Send, T: Adjust<Color>>(
+async fn black_and_white<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	image: impl Node<F, Output = T>,
+	mut image: T,
 	#[default(Color::BLACK)] tint: Color,
 	#[default(40.)]
 	#[range((-200., 300.))]
@@ -499,8 +450,7 @@ async fn black_and_white<F: 'n + Send, T: Adjust<Color>>(
 	#[range((-200., 300.))]
 	magentas: Percentage,
 ) -> T {
-	let mut input = image.eval(footprint).await;
-	input.adjust(|color| {
+	image.adjust(|color| {
 		let color = color.to_gamma_srgb();
 
 		let reds = reds as f32 / 100.;
@@ -537,35 +487,25 @@ async fn black_and_white<F: 'n + Send, T: Adjust<Color>>(
 
 		color.to_linear_srgb()
 	});
-	input
+	image
 }
 
 // Aims for interoperable compatibility with:
 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27hue%20%27%20%3D%20Old,saturation%2C%20Photoshop%205.0
 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=0%20%3D%20Use%20other.-,Hue/Saturation,-Hue/Saturation%20settings
 #[node_macro::node(name("Hue/Saturation"), category("Raster: Adjustment"))]
-async fn hue_saturation<F: 'n + Send, T: Adjust<Color>>(
+async fn hue_saturation<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	input: impl Node<F, Output = T>,
+	mut input: T,
 	hue_shift: Angle,
 	saturation_shift: SignedPercentage,
 	lightness_shift: SignedPercentage,
 ) -> T {
-	let mut input = input.eval(footprint).await;
 	input.adjust(|color| {
 		let color = color.to_gamma_srgb();
 
@@ -588,25 +528,15 @@ async fn hue_saturation<F: 'n + Send, T: Adjust<Color>>(
 // Aims for interoperable compatibility with:
 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27%20%3D%20Color%20Lookup-,%27nvrt%27%20%3D%20Invert,-%27post%27%20%3D%20Posterize
 #[node_macro::node(category("Raster: Adjustment"))]
-async fn invert<F: 'n + Send, T: Adjust<Color>>(
-	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+async fn invert<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	input: impl Node<F, Output = T>,
+	mut input: T,
 ) -> T {
-	let mut input = input.eval(footprint).await;
 	input.adjust(|color| {
 		let color = color.to_gamma_srgb();
 
@@ -620,29 +550,19 @@ async fn invert<F: 'n + Send, T: Adjust<Color>>(
 // Aims for interoperable compatibility with:
 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=post%27%20%3D%20Posterize-,%27thrs%27%20%3D%20Threshold,-%27grdm%27%20%3D%20Gradient
 #[node_macro::node(category("Raster: Adjustment"))]
-async fn threshold<F: 'n + Send, T: Adjust<Color>>(
+async fn threshold<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	image: impl Node<F, Output = T>,
+	mut image: T,
 	#[default(50.)] min_luminance: Percentage,
 	#[default(100.)] max_luminance: Percentage,
 	luminance_calc: LuminanceCalculation,
 ) -> T {
-	let mut input = image.eval(footprint).await;
-	input.adjust(|color| {
+	image.adjust(|color| {
 		let min_luminance = Color::srgb_to_linear(min_luminance as f32 / 100.);
 		let max_luminance = Color::srgb_to_linear(max_luminance as f32 / 100.);
 
@@ -660,7 +580,7 @@ async fn threshold<F: 'n + Send, T: Adjust<Color>>(
 			Color::BLACK
 		}
 	});
-	input
+	image
 }
 
 trait Blend<P: Pixel> {
@@ -723,44 +643,35 @@ impl Blend<Color> for GradientStops {
 }
 
 #[node_macro::node(category("Raster"))]
-async fn blend<F: 'n + Send + Copy, T: Blend<Color> + Send>(
-	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+async fn blend<T: Blend<Color> + Send>(
+	_: impl Ctx,
 	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	over: impl Node<F, Output = T>,
+	over: T,
 	#[expose]
 	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	under: impl Node<F, Output = T>,
+	under: T,
 	blend_mode: BlendMode,
 	#[default(100.)] opacity: Percentage,
 ) -> T {
-	let over = over.eval(footprint).await;
-	let under = under.eval(footprint).await;
-
 	over.blend(&under, |a, b| blend_colors(a, b, blend_mode, opacity / 100.))
 }
 
-#[node_macro::node(category(""))]
-fn blend_color_pair(input: (Color, Color), blend_mode: BlendMode, opacity: Percentage) -> Color {
+#[node_macro::node(category(""), skip_impl)]
+fn blend_color_pair<BlendModeNode, OpacityNode>(input: (Color, Color), blend_mode: &'n BlendModeNode, opacity: &'n OpacityNode) -> Color
+where
+	BlendModeNode: Node<'n, (), Output = BlendMode> + 'n,
+	OpacityNode: Node<'n, (), Output = Percentage> + 'n,
+{
+	let blend_mode = blend_mode.eval(());
+	let opacity = opacity.eval(());
 	blend_colors(input.0, input.1, blend_mode, opacity / 100.)
 }
 
@@ -857,35 +768,24 @@ pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode,
 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
 #[node_macro::node(category("Raster: Adjustment"))]
-async fn gradient_map<F: 'n + Send, T: Adjust<Color>>(
-	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+async fn gradient_map<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	image: impl Node<F, Output = T>,
+	mut image: T,
 	gradient: GradientStops,
 	reverse: bool,
 ) -> T {
-	let mut input = image.eval(footprint).await;
-
-	input.adjust(|color| {
+	image.adjust(|color| {
 		let intensity = color.luminance_srgb();
 		let intensity = if reverse { 1. - intensity } else { intensity };
 		gradient.evalute(intensity as f64)
 	});
 
-	input
+	image
 }
 
 // Aims for interoperable compatibility with:
@@ -896,27 +796,17 @@ async fn gradient_map<F: 'n + Send, T: Adjust<Color>>(
 // https://stackoverflow.com/questions/33966121/what-is-the-algorithm-for-vibrance-filters
 // The results of this implementation are very close to correct, but not quite perfect
 #[node_macro::node(category("Raster: Adjustment"))]
-async fn vibrance<F: 'n + Send, T: Adjust<Color>>(
+async fn vibrance<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	image: impl Node<F, Output = T>,
+	mut image: T,
 	vibrance: SignedPercentage,
 ) -> T {
-	let mut input = image.eval(footprint).await;
-	input.adjust(|color| {
+	image.adjust(|color| {
 		let vibrance = vibrance as f32 / 100.;
 		// Slow the effect down by half when it's negative, since artifacts begin appearing past -50%.
 		// So this scales the 0% to -50% range to 0% to -100%.
@@ -963,7 +853,7 @@ async fn vibrance<F: 'n + Send, T: Adjust<Color>>(
 			altered_color.map_rgb(|c| c * (1. - factor) + luminance * factor)
 		}
 	});
-	input
+	image
 }
 
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -1196,23 +1086,14 @@ impl DomainWarpType {
 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27mixr%27%20%3D%20Channel%20Mixer
 // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Lab%20color%20only-,Channel%20Mixer,-Key%20is%20%27mixr
 #[node_macro::node(category("Raster: Adjustment"), properties("channel_mixer_properties"))]
-async fn channel_mixer<F: 'n + Send, T: Adjust<Color>>(
+async fn channel_mixer<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	image: impl Node<F, Output = T>,
+	mut image: T,
 
 	monochrome: bool,
 	#[default(40.)]
@@ -1270,8 +1151,7 @@ async fn channel_mixer<F: 'n + Send, T: Adjust<Color>>(
 	// Display-only properties (not used within the node)
 	_output_channel: RedGreenBlue,
 ) -> T {
-	let mut input = image.eval(footprint).await;
-	input.adjust(|color| {
+	image.adjust(|color| {
 		let color = color.to_gamma_srgb();
 
 		let (r, g, b, a) = color.components();
@@ -1296,7 +1176,7 @@ async fn channel_mixer<F: 'n + Send, T: Adjust<Color>>(
 
 		color.to_linear_srgb()
 	});
-	input
+	image
 }
 
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -1356,24 +1236,15 @@ impl core::fmt::Display for SelectiveColorChoice {
 //
 // Algorithm based on:
 // https://blog.pkh.me/p/22-understanding-selective-coloring-in-adobe-photoshop.html
-#[node_macro::node(category("Raster: Adjustment"))]
-async fn selective_color<F: 'n + Send, T: Adjust<Color>>(
-	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+#[node_macro::node(category("Raster: Adjustment"), properties("selective_color_properties"))]
+async fn selective_color<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	image: impl Node<F, Output = T>,
+	mut image: T,
 	mode: RelativeAbsolute,
 	#[name("(Reds) Cyan")] r_c: f64,
 	#[name("(Reds) Magenta")] r_m: f64,
@@ -1413,8 +1284,7 @@ async fn selective_color<F: 'n + Send, T: Adjust<Color>>(
 	#[name("(Blacks) Black")] k_k: f64,
 	_colors: SelectiveColorChoice,
 ) -> T {
-	let mut input = image.eval(footprint).await;
-	input.adjust(|color| {
+	image.adjust(|color| {
 		let color = color.to_gamma_srgb();
 
 		let (r, g, b, a) = color.components();
@@ -1488,7 +1358,7 @@ async fn selective_color<F: 'n + Send, T: Adjust<Color>>(
 
 		color.to_linear_srgb()
 	});
-	input
+	image
 }
 
 pub(super) trait MultiplyAlpha {
@@ -1534,28 +1404,18 @@ where
 // https://www.axiomx.com/posterize.htm
 // This algorithm produces fully accurate output in relation to the industry standard.
 #[node_macro::node(category("Raster: Adjustment"))]
-async fn posterize<F: 'n + Send, T: Adjust<Color>>(
+async fn posterize<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	input: impl Node<F, Output = T>,
+	mut input: T,
 	#[default(4)]
 	#[min(2.)]
 	levels: u32,
 ) -> T {
-	let mut input = input.eval(footprint).await;
 	input.adjust(|color| {
 		let color = color.to_gamma_srgb();
 
@@ -1577,30 +1437,20 @@ async fn posterize<F: 'n + Send, T: Adjust<Color>>(
 // Algorithm based on:
 // https://geraldbakker.nl/psnumbers/exposure.html
 #[node_macro::node(category("Raster: Adjustment"), properties("exposure_properties"))]
-async fn exposure<F: 'n + Send, T: Adjust<Color>>(
-	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
+async fn exposure<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	input: impl Node<F, Output = T>,
+	mut input: T,
 	exposure: f64,
 	offset: f64,
 	#[default(1.)]
 	#[range((0.01, 10.))]
 	gamma_correction: f64,
 ) -> T {
-	let mut input = input.eval(footprint).await;
 	input.adjust(|color| {
 		let adjusted = color
 		// Exposure
@@ -1619,7 +1469,7 @@ const WINDOW_SIZE: usize = 1024;
 
 #[cfg(feature = "alloc")]
 #[node_macro::node(category(""))]
-fn generate_curves<C: Channel + super::Linear>(_: (), curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
+fn generate_curves<C: Channel + super::Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
 	use bezier_rs::{Bezier, TValue};
 
 	let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle];
@@ -1660,31 +1510,21 @@ fn generate_curves<C: Channel + super::Linear>(_: (), curve: Curve, #[implementa
 
 #[cfg(feature = "alloc")]
 #[node_macro::node(category("Raster: Adjustment"))]
-async fn color_overlay<F: 'n + Send, T: Adjust<Color>>(
+fn color_overlay<T: Adjust<Color>>(
+	_: impl Ctx,
 	#[implementations(
-		(),
-		(),
-		(),
-		Footprint,
+		Color,
+		ImageFrameTable<Color>,
+		GradientStops,
 	)]
-	footprint: F,
-	#[implementations(
-		() -> Color,
-		() -> ImageFrameTable<Color>,
-		() -> GradientStops,
-		Footprint -> Color,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GradientStops,
-	)]
-	image: impl Node<F, Output = T>,
+	mut image: T,
 	#[default(Color::BLACK)] color: Color,
 	blend_mode: BlendMode,
 	#[default(100.)] opacity: Percentage,
 ) -> T {
 	let opacity = (opacity as f32 / 100.).clamp(0., 1.);
 
-	let mut input = image.eval(footprint).await;
-	input.adjust(|pixel| {
+	image.adjust(|pixel| {
 		let image = pixel.map_rgb(|channel| channel * (1. - opacity));
 
 		// The apply blend mode function divides rgb by the alpha channel for the background. This undoes that.
@@ -1693,7 +1533,7 @@ async fn color_overlay<F: 'n + Send, T: Adjust<Color>>(
 
 		Color::from_rgbaf32_unchecked(image.r() + overlay.r(), image.g() + overlay.g(), image.b() + overlay.b(), pixel.a())
 	});
-	input
+	image
 }
 
 // #[cfg(feature = "alloc")]
@@ -1702,10 +1542,11 @@ async fn color_overlay<F: 'n + Send, T: Adjust<Color>>(
 // #[cfg(feature = "alloc")]
 // mod index_node {
 // 	use crate::raster::{Color, ImageFrame};
+// 	use crate::Ctx;
 
 // 	#[node_macro::node(category(""))]
 // 	pub fn index<T: Default + Clone>(
-// 		_: (),
+// 		_: impl Ctx,
 // 		#[implementations(Vec<ImageFrame<Color>>, Vec<Color>)]
 // 		#[widget(ParsedWidgetOverride::Hidden)]
 // 		input: Vec<T>,
@@ -1752,7 +1593,7 @@ mod test {
 		// 100% of the output should come from the multiplied value
 		let opacity = 100_f64;
 
-		let result = super::color_overlay((), &FutureWrapperNode(ImageFrameTable::new(image.clone())), overlay_color, BlendMode::Multiply, opacity).await;
+		let result = super::color_overlay((), ImageFrameTable::new(image.clone()), overlay_color, BlendMode::Multiply, opacity);
 		let result = result.one_item();
 
 		// The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0)
diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs
index 336ce114df..9d4d673cd9 100644
--- a/node-graph/gcore/src/raster/color.rs
+++ b/node-graph/gcore/src/raster/color.rs
@@ -1,5 +1,5 @@
 use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
-use super::{Alpha, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB};
+use super::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB};
 
 use dyn_any::DynAny;
 #[cfg(feature = "serde")]
@@ -257,6 +257,11 @@ impl RGBMut for Color {
 		self.blue = blue;
 	}
 }
+impl AlphaMut for Color {
+	fn set_alpha(&mut self, value: Self::AlphaChannel) {
+		self.alpha = value;
+	}
+}
 
 impl Pixel for Color {
 	#[cfg(not(target_arch = "spirv"))]
diff --git a/node-graph/gcore/src/structural.rs b/node-graph/gcore/src/structural.rs
index be702a5b74..5fc7c5da6a 100644
--- a/node-graph/gcore/src/structural.rs
+++ b/node-graph/gcore/src/structural.rs
@@ -132,14 +132,15 @@ impl<'i, Root: Node<'i, I>, I: 'i + From<()>> ConsNode<I, Root> {
 #[cfg(test)]
 mod test {
 	use super::*;
-	use crate::{ops::IdentityNode, value::ValueNode};
+	use crate::generic::FnNode;
+	use crate::value::ValueNode;
 
 	#[test]
 	fn compose() {
 		let value = ValueNode::new(4u32);
-		let compose = value.then(IdentityNode::new());
+		let compose = value.then(FnNode::new(|x| x));
 		assert_eq!(compose.eval(()), &4u32);
-		let type_erased = &compose as &dyn for<'i> Node<'i, (), Output = &'i u32>;
+		let type_erased = &compose as &dyn Node<'_, (), Output = &'_ u32>;
 		assert_eq!(type_erased.eval(()), &4u32);
 	}
 
@@ -148,7 +149,7 @@ mod test {
 		let value = ValueNode::new(5);
 
 		assert_eq!(value.eval(()), &5);
-		let id = IdentityNode::new();
+		let id = FnNode::new(|x| x);
 
 		let compose = ComposeNode::new(&value, &id);
 
diff --git a/node-graph/gcore/src/transform.rs b/node-graph/gcore/src/transform.rs
index 3aaf1c245e..31ea15387c 100644
--- a/node-graph/gcore/src/transform.rs
+++ b/node-graph/gcore/src/transform.rs
@@ -1,9 +1,9 @@
-use crate::application_io::{TextureFrame, TextureFrameTable};
+use crate::application_io::TextureFrameTable;
 use crate::raster::bbox::AxisAlignedBbox;
 use crate::raster::image::{ImageFrame, ImageFrameTable};
 use crate::raster::Pixel;
 use crate::vector::{VectorData, VectorDataTable};
-use crate::{Artboard, ArtboardGroup, Color, GraphicElement, GraphicGroup, GraphicGroupTable};
+use crate::{Artboard, ArtboardGroup, CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroup, GraphicGroupTable, OwnedContextImpl};
 
 use glam::{DAffine2, DVec2};
 
@@ -244,6 +244,12 @@ pub struct Footprint {
 
 impl Default for Footprint {
 	fn default() -> Self {
+		Self::empty()
+	}
+}
+
+impl Footprint {
+	pub const fn empty() -> Self {
 		Self {
 			transform: DAffine2::IDENTITY,
 			resolution: glam::UVec2::new(1920, 1080),
@@ -251,9 +257,6 @@ impl Default for Footprint {
 			ignore_modifications: false,
 		}
 	}
-}
-
-impl Footprint {
 	pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox {
 		let inverse = self.transform.inverse();
 		let start = inverse.transform_point2((0., 0.).into());
@@ -277,7 +280,7 @@ impl From<()> for Footprint {
 }
 
 #[node_macro::node(category("Debug"))]
-fn cull<T>(_footprint: Footprint, #[implementations(VectorDataTable, GraphicGroupTable, Artboard, ImageFrameTable<Color>, ArtboardGroup)] data: T) -> T {
+fn cull<T>(_: impl Ctx, #[implementations(VectorDataTable, GraphicGroupTable, Artboard, ImageFrameTable<Color>, ArtboardGroup)] data: T) -> T {
 	data
 }
 
@@ -301,26 +304,15 @@ impl ApplyTransform for () {
 }
 
 #[node_macro::node(category(""))]
-async fn transform<I: Into<Footprint> + 'n + ApplyTransform + Clone + Send + Sync, T: 'n + TransformMut>(
+async fn transform<T: 'n + TransformMut + 'static>(
+	ctx: impl Ctx + CloneVarArgs + ExtractAll,
 	#[implementations(
-		(),
-		(),
-		(),
-		(),
-		Footprint,
+		Context -> VectorDataTable,
+		Context -> GraphicGroupTable,
+		Context -> ImageFrameTable<Color>,
+		Context -> TextureFrameTable,
 	)]
-	mut input: I,
-	#[implementations(
-		() -> VectorDataTable,
-		() -> GraphicGroupTable,
-		() -> ImageFrameTable<Color>,
-		() -> TextureFrame,
-		Footprint -> VectorDataTable,
-		Footprint -> GraphicGroupTable,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> TextureFrame,
-	)]
-	transform_target: impl Node<I, Output = T>,
+	transform_target: impl Node<Context<'static>, Output = T>,
 	translate: DVec2,
 	rotate: f64,
 	scale: DVec2,
@@ -328,22 +320,27 @@ async fn transform<I: Into<Footprint> + 'n + ApplyTransform + Clone + Send + Syn
 	_pivot: DVec2,
 ) -> T {
 	let modification = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
-	let footprint = input.clone().into();
-	if !footprint.ignore_modifications {
-		input.apply_transform(&modification);
+	let footprint = ctx.try_footprint().copied();
+
+	let mut ctx = OwnedContextImpl::from(ctx);
+	if let Some(mut footprint) = footprint {
+		if !footprint.ignore_modifications {
+			footprint.apply_transform(&modification);
+		}
+		ctx = ctx.with_footprint(footprint);
 	}
 
-	let mut data = transform_target.eval(input).await;
+	let mut transform_target = transform_target.eval(ctx.into_context()).await;
 
-	let data_transform = data.transform_mut();
+	let data_transform = transform_target.transform_mut();
 	*data_transform = modification * (*data_transform);
 
-	data
+	transform_target
 }
 
 #[node_macro::node(category(""))]
 fn replace_transform<Data: TransformMut, TransformInput: Transform>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(VectorDataTable, ImageFrameTable<Color>, GraphicGroupTable)] mut data: Data,
 	#[implementations(DAffine2)] transform: TransformInput,
 ) -> Data {
diff --git a/node-graph/gcore/src/types.rs b/node-graph/gcore/src/types.rs
index 1868a4f7a4..86a034cf71 100644
--- a/node-graph/gcore/src/types.rs
+++ b/node-graph/gcore/src/types.rs
@@ -53,6 +53,9 @@ macro_rules! future {
 	($type:ty) => {{
 		$crate::Type::Future(Box::new(concrete!($type)))
 	}};
+	($type:ty, $name:ty) => {
+		$crate::Type::Future(Box::new(concrete!($type, $name)))
+	};
 }
 
 #[macro_export]
@@ -67,6 +70,18 @@ macro_rules! fn_type {
 		$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(concrete!($type)))
 	};
 }
+#[macro_export]
+macro_rules! fn_type_fut {
+	($type:ty) => {
+		$crate::Type::Fn(Box::new(concrete!(())), Box::new(future!($type)))
+	};
+	($in_type:ty, $type:ty, alias: $outname:ty) => {
+		$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type, $outname)))
+	};
+	($in_type:ty, $type:ty) => {
+		$crate::Type::Fn(Box::new(concrete!($in_type)), Box::new(future!($type)))
+	};
+}
 
 #[derive(Clone, PartialEq, Eq, Hash, Default)]
 pub struct NodeIOTypes {
@@ -134,6 +149,7 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer:
 	let name = String::deserialize(deserializer)?;
 	let name = match name.as_str() {
 		"f32" => "f64".to_string(),
+		"graphene_core::transform::Footprint" => "core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>".to_string(),
 		"graphene_core::graphic_element::GraphicGroup" => "graphene_core::graphic_element::Instances<graphene_core::graphic_element::GraphicGroup>".to_string(),
 		"graphene_core::vector::vector_data::VectorData" => "graphene_core::graphic_element::Instances<graphene_core::vector::vector_data::VectorData>".to_string(),
 		"graphene_core::raster::image::ImageFrame<Color>" => "graphene_core::graphic_element::Instances<graphene_core::raster::image::ImageFrame<Color>>".to_string(),
@@ -189,7 +205,7 @@ pub enum Type {
 	/// Runtime type information for a function. Given some input, gives some output.
 	/// See the example and explanation in the `ComposeNode` implementation within the node registry for more info.
 	Fn(Box<Type>, Box<Type>),
-	/// Not used at the moment.
+	/// Represents a future which promises to return the inner type.
 	Future(Box<Type>),
 }
 
@@ -280,7 +296,7 @@ impl Type {
 			Self::Generic(_) => self,
 			Self::Concrete(_) => self,
 			Self::Fn(_, output) => output.nested_type(),
-			Self::Future(_) => self,
+			Self::Future(output) => output.nested_type(),
 		}
 	}
 }
diff --git a/node-graph/gcore/src/value.rs b/node-graph/gcore/src/value.rs
index 48389f02fb..e63ad453cb 100644
--- a/node-graph/gcore/src/value.rs
+++ b/node-graph/gcore/src/value.rs
@@ -8,10 +8,10 @@ use core::{
 #[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
 pub struct IntNode<const N: u32>;
 
-impl<'i, const N: u32> Node<'i, ()> for IntNode<N> {
+impl<'i, const N: u32, I> Node<'i, I> for IntNode<N> {
 	type Output = u32;
 	#[inline(always)]
-	fn eval(&'i self, _input: ()) -> Self::Output {
+	fn eval(&'i self, _input: I) -> Self::Output {
 		N
 	}
 }
@@ -19,10 +19,10 @@ impl<'i, const N: u32> Node<'i, ()> for IntNode<N> {
 #[derive(Default, Debug, Clone, Copy)]
 pub struct ValueNode<T>(pub T);
 
-impl<'i, T: 'i> Node<'i, ()> for ValueNode<T> {
+impl<'i, T: 'i, I> Node<'i, I> for ValueNode<T> {
 	type Output = &'i T;
 	#[inline(always)]
-	fn eval(&'i self, _input: ()) -> Self::Output {
+	fn eval(&'i self, _input: I) -> Self::Output {
 		&self.0
 	}
 }
@@ -77,10 +77,10 @@ impl<T> RefCellMutNode<T> {
 #[derive(Default)]
 pub struct OnceCellNode<T>(pub Cell<T>);
 
-impl<'i, T: Default + 'i> Node<'i, ()> for OnceCellNode<T> {
+impl<'i, T: Default + 'i, I> Node<'i, I> for OnceCellNode<T> {
 	type Output = T;
 	#[inline(always)]
-	fn eval(&'i self, _input: ()) -> Self::Output {
+	fn eval(&'i self, _input: I) -> Self::Output {
 		self.0.replace(T::default())
 	}
 }
@@ -94,10 +94,10 @@ impl<T> OnceCellNode<T> {
 #[derive(Clone, Copy)]
 pub struct ClonedNode<T: Clone>(pub T);
 
-impl<'i, T: Clone + 'i> Node<'i, ()> for ClonedNode<T> {
+impl<'i, T: Clone + 'i, I> Node<'i, I> for ClonedNode<T> {
 	type Output = T;
 	#[inline(always)]
-	fn eval(&'i self, _input: ()) -> Self::Output {
+	fn eval(&'i self, _input: I) -> Self::Output {
 		self.0.clone()
 	}
 }
@@ -140,10 +140,10 @@ impl<T: Clone> DebugClonedNode<T> {
 #[derive(Clone, Copy)]
 pub struct CopiedNode<T: Copy>(pub T);
 
-impl<'i, T: Copy + 'i> Node<'i, ()> for CopiedNode<T> {
+impl<'i, T: Copy + 'i, I> Node<'i, I> for CopiedNode<T> {
 	type Output = T;
 	#[inline(always)]
-	fn eval(&'i self, _input: ()) -> Self::Output {
+	fn eval(&'i self, _input: I) -> Self::Output {
 		self.0
 	}
 }
@@ -157,9 +157,9 @@ impl<T: Copy> CopiedNode<T> {
 #[derive(Default)]
 pub struct DefaultNode<T>(PhantomData<T>);
 
-impl<'i, T: Default + 'i> Node<'i, ()> for DefaultNode<T> {
+impl<'i, T: Default + 'i, I> Node<'i, I> for DefaultNode<T> {
 	type Output = T;
-	fn eval(&'i self, _input: ()) -> Self::Output {
+	fn eval(&'i self, _input: I) -> Self::Output {
 		T::default()
 	}
 }
@@ -205,7 +205,7 @@ mod test {
 	#[test]
 	fn test_default_node() {
 		let node = DefaultNode::<u32>::new();
-		assert_eq!(node.eval(()), 0);
+		assert_eq!(node.eval(42), 0);
 	}
 	#[test]
 	#[allow(clippy::unit_cmp)]
diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs
index d7fc44d5d0..bcca892b3d 100644
--- a/node-graph/gcore/src/vector/generator_nodes.rs
+++ b/node-graph/gcore/src/vector/generator_nodes.rs
@@ -1,5 +1,5 @@
-use crate::transform::Footprint;
 use crate::vector::{HandleId, PointId, VectorData, VectorDataTable};
+use crate::Ctx;
 
 use bezier_rs::Subpath;
 use glam::DVec2;
@@ -35,12 +35,12 @@ impl CornerRadius for [f64; 4] {
 }
 
 #[node_macro::node(category("Vector: Shape"))]
-fn circle<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, _primary: (), #[default(50.)] radius: f64) -> VectorDataTable {
+fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataTable {
 	VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
 }
 
 #[node_macro::node(category("Vector: Shape"))]
-fn ellipse<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, _primary: (), #[default(50)] radius_x: f64, #[default(25)] radius_y: f64) -> VectorDataTable {
+fn ellipse(_: impl Ctx, _primary: (), #[default(50)] radius_x: f64, #[default(25)] radius_y: f64) -> VectorDataTable {
 	let radius = DVec2::new(radius_x, radius_y);
 	let corner1 = -radius;
 	let corner2 = radius;
@@ -58,8 +58,8 @@ fn ellipse<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, _prima
 }
 
 #[node_macro::node(category("Vector: Shape"), properties("rectangle_properties"))]
-fn rectangle<F: 'n + Send, T: CornerRadius>(
-	#[implementations((), Footprint)] _footprint: F,
+fn rectangle<T: CornerRadius>(
+	_: impl Ctx,
 	_primary: (),
 	#[default(100)] width: f64,
 	#[default(100)] height: f64,
@@ -71,8 +71,8 @@ fn rectangle<F: 'n + Send, T: CornerRadius>(
 }
 
 #[node_macro::node(category("Vector: Shape"))]
-fn regular_polygon<F: 'n + Send>(
-	#[implementations((), Footprint)] _footprint: F,
+fn regular_polygon(
+	_: impl Ctx,
 	_primary: (),
 	#[default(6)]
 	#[min(3.)]
@@ -85,8 +85,8 @@ fn regular_polygon<F: 'n + Send>(
 }
 
 #[node_macro::node(category("Vector: Shape"))]
-fn star<F: 'n + Send>(
-	#[implementations((), Footprint)] _footprint: F,
+fn star(
+	_: impl Ctx,
 	_primary: (),
 	#[default(5)]
 	#[min(2.)]
@@ -102,15 +102,14 @@ fn star<F: 'n + Send>(
 }
 
 #[node_macro::node(category("Vector: Shape"))]
-fn line<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, _primary: (), #[default((0., -50.))] start: DVec2, #[default((0., 50.))] end: DVec2) -> VectorDataTable {
+fn line(_: impl Ctx, _primary: (), #[default((0., -50.))] start: DVec2, #[default((0., 50.))] end: DVec2) -> VectorDataTable {
 	VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end)))
 }
 
 // TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
 #[node_macro::node(category(""))]
-fn path<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, path_data: Vec<Subpath<PointId>>, colinear_manipulators: Vec<PointId>) -> VectorDataTable {
+fn path(_: impl Ctx, path_data: Vec<Subpath<PointId>>, colinear_manipulators: Vec<PointId>) -> VectorDataTable {
 	let mut vector_data = VectorData::from_subpaths(path_data, false);
-
 	vector_data.colinear_manipulators = colinear_manipulators
 		.iter()
 		.filter_map(|&point| super::ManipulatorPointId::Anchor(point).get_handle_pair(&vector_data))
diff --git a/node-graph/gcore/src/vector/vector_data/modification.rs b/node-graph/gcore/src/vector/vector_data/modification.rs
index a73add8c61..221e41ad35 100644
--- a/node-graph/gcore/src/vector/vector_data/modification.rs
+++ b/node-graph/gcore/src/vector/vector_data/modification.rs
@@ -1,6 +1,6 @@
 use super::*;
-use crate::transform::Footprint;
 use crate::uuid::generate_uuid;
+use crate::Ctx;
 
 use bezier_rs::BezierHandles;
 use dyn_any::DynAny;
@@ -424,20 +424,7 @@ impl core::hash::Hash for VectorModification {
 
 /// A node that applies a procedural modification to some [`VectorData`].
 #[node_macro::node(category(""))]
-async fn path_modify<F: 'n + Send + Sync + Clone>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	input: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
-	modification: Box<VectorModification>,
-) -> VectorDataTable {
-	let mut vector_data = vector_data.eval(input).await;
+async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modification: Box<VectorModification>) -> VectorDataTable {
 	let vector_data = vector_data.one_item_mut();
 
 	modification.apply(vector_data);
diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs
index 389ef9884b..1201e8818a 100644
--- a/node-graph/gcore/src/vector/vector_nodes.rs
+++ b/node-graph/gcore/src/vector/vector_nodes.rs
@@ -6,7 +6,7 @@ use crate::renderer::GraphicElementRendered;
 use crate::transform::{Footprint, Transform, TransformMut};
 use crate::vector::style::LineJoin;
 use crate::vector::PointDomain;
-use crate::{Color, GraphicElement, GraphicGroup, GraphicGroupTable};
+use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroup, GraphicGroupTable, OwnedContextImpl};
 
 use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue};
 use glam::{DAffine2, DVec2};
@@ -43,21 +43,11 @@ impl VectorIterMut for VectorDataTable {
 }
 
 #[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
-async fn assign_colors<F: 'n + Send, T: VectorIterMut>(
-	#[implementations(
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> GraphicGroupTable,
-		() -> VectorDataTable,
-		Footprint -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-	)]
+async fn assign_colors<T: VectorIterMut>(
+	_: impl Ctx,
+	#[implementations(GraphicGroupTable, VectorDataTable)]
 	#[widget(ParsedWidgetOverride::Hidden)]
-	vector_group: impl Node<F, Output = T>,
+	mut vector_group: T,
 	#[default(true)] fill: bool,
 	stroke: bool,
 	#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")] gradient: GradientStops,
@@ -66,8 +56,6 @@ async fn assign_colors<F: 'n + Send, T: VectorIterMut>(
 	#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")] seed: SeedValue,
 	#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] repeat_every: u32,
 ) -> T {
-	let mut vector_group = vector_group.eval(footprint).await;
-
 	let length = vector_group.vector_iter_mut().count();
 	let gradient = if reverse { gradient.reversed() } else { gradient };
 
@@ -99,45 +87,19 @@ async fn assign_colors<F: 'n + Send, T: VectorIterMut>(
 }
 
 #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
-async fn fill<F: 'n + Send, FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
-	#[implementations(
-		(),
-		(),
-		(),
-		(),
-		(),
-		(),
-		(),
-		(),
-		Footprint,
-		Footprint,
-		Footprint,
-		Footprint,
-		Footprint,
-		Footprint,
-		Footprint,
-		Footprint,
-	)]
-	footprint: F,
+async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
+	_: impl Ctx,
 	#[implementations(
-		() -> VectorDataTable,
-		() -> VectorDataTable,
-		() -> VectorDataTable,
-		() -> VectorDataTable,
-		() -> GraphicGroupTable,
-		() -> GraphicGroupTable,
-		() -> GraphicGroupTable,
-		() -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> VectorDataTable,
-		Footprint -> VectorDataTable,
-		Footprint -> VectorDataTable,
-		Footprint -> GraphicGroupTable,
-		Footprint -> GraphicGroupTable,
-		Footprint -> GraphicGroupTable,
-		Footprint -> GraphicGroupTable,
+		VectorDataTable,
+		VectorDataTable,
+		VectorDataTable,
+		VectorDataTable,
+		GraphicGroupTable,
+		GraphicGroupTable,
+		GraphicGroupTable,
+		GraphicGroupTable
 	)]
-	vector_data: impl Node<F, Output = TargetTy>,
+	mut vector_data: TargetTy,
 	#[implementations(
 		Fill,
 		Option<Color>,
@@ -147,62 +109,29 @@ async fn fill<F: 'n + Send, FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIter
 		Option<Color>,
 		Color,
 		Gradient,
-		Fill,
-		Option<Color>,
-		Color,
-		Gradient,
-		Fill,
-		Option<Color>,
-		Color,
-		Gradient,
 	)]
 	#[default(Color::BLACK)]
 	fill: FillTy,
 	_backup_color: Option<Color>,
 	_backup_gradient: Gradient,
 ) -> TargetTy {
-	let mut target = vector_data.eval(footprint).await;
 	let fill: Fill = fill.into();
-	for (target, _transform) in target.vector_iter_mut() {
+	for (target, _transform) in vector_data.vector_iter_mut() {
 		target.style.set_fill(fill.clone());
 	}
 
-	target
+	vector_data
 }
 
 #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
-async fn stroke<F: 'n + Send, ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
-	#[implementations(
-		(),
-		(),
-		(),
-		(),
-		Footprint,
-		Footprint,
-		Footprint,
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		() -> VectorDataTable,
-		() -> GraphicGroupTable,
-		() -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> VectorDataTable,
-		Footprint -> GraphicGroupTable,
-		Footprint -> GraphicGroupTable,
-	)]
-	vector_data: impl Node<F, Output = TargetTy>,
+async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
+	_: impl Ctx,
+	#[implementations(VectorDataTable, VectorDataTable, GraphicGroupTable, GraphicGroupTable)] mut vector_data: TargetTy,
 	#[implementations(
 		Option<Color>,
 		Color,
 		Option<Color>,
 		Color,
-		Option<Color>,
-		Color,
-		Option<Color>,
-		Color,
 	)]
 	#[default(Color::BLACK)]
 	color: ColorTy,
@@ -213,7 +142,6 @@ async fn stroke<F: 'n + Send, ColorTy: Into<Option<Color>> + 'n + Send, TargetTy
 	line_join: LineJoin,
 	#[default(4.)] miter_limit: f64,
 ) -> TargetTy {
-	let mut target = vector_data.eval(footprint).await;
 	let stroke = Stroke {
 		color: color.into(),
 		weight,
@@ -224,37 +152,24 @@ async fn stroke<F: 'n + Send, ColorTy: Into<Option<Color>> + 'n + Send, TargetTy
 		line_join_miter_limit: miter_limit,
 		transform: DAffine2::IDENTITY,
 	};
-	for (target, transform) in target.vector_iter_mut() {
+	for (target, transform) in vector_data.vector_iter_mut() {
 		target.style.set_stroke(Stroke { transform, ..stroke.clone() });
 	}
 
-	target
+	vector_data
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn repeat<F: 'n + Send + Copy, I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
-	#[implementations(
-		(),
-		(),
-		Footprint,
-		Footprint,
-	)]
-	footprint: F,
+async fn repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
+	_: impl Ctx,
 	// TODO: Implement other GraphicElementRendered types.
-	#[implementations(
-		() -> VectorDataTable,
-		() -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> GraphicGroupTable,
-	)]
-	instance: impl Node<F, Output = I>,
+	#[implementations(VectorDataTable, GraphicGroupTable)] instance: I,
 	#[default(100., 100.)]
 	// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
 	direction: DVec2,
 	angle: Angle,
 	#[default(4)] instances: IntegerCount,
 ) -> GraphicGroupTable {
-	let instance = instance.eval(footprint).await;
 	let first_vector_transform = instance.transform();
 
 	let angle = angle.to_radians();
@@ -285,27 +200,14 @@ async fn repeat<F: 'n + Send + Copy, I: 'n + GraphicElementRendered + Transform
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn circular_repeat<F: 'n + Send + Copy, I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
-	#[implementations(
-		(),
-		(),
-		Footprint,
-		Footprint,
-	)]
-	footprint: F,
+async fn circular_repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
+	_: impl Ctx,
 	// TODO: Implement other GraphicElementRendered types.
-	#[implementations(
-		() -> VectorDataTable,
-		() -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> GraphicGroupTable,
-	)]
-	instance: impl Node<F, Output = I>,
+	#[implementations(VectorDataTable, GraphicGroupTable)] instance: I,
 	angle_offset: Angle,
 	#[default(5)] radius: f64,
 	#[default(5)] instances: IntegerCount,
 ) -> GraphicGroupTable {
-	let instance = instance.eval(footprint).await;
 	let first_vector_transform = instance.transform();
 	let instances = instances.max(1);
 
@@ -334,27 +236,12 @@ async fn circular_repeat<F: 'n + Send + Copy, I: 'n + GraphicElementRendered + T
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn copy_to_points<F: 'n + Send + Copy, I: GraphicElementRendered + ConcatElement + TransformMut + Send + 'n>(
-	#[implementations(
-		(),
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	points: impl Node<F, Output = VectorDataTable>,
+async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
+	_: impl Ctx,
+	points: VectorDataTable,
 	#[expose]
-	#[implementations(
-		() -> VectorDataTable,
-		() -> GraphicGroupTable,
-		Footprint -> VectorDataTable,
-		Footprint -> GraphicGroupTable,
-	)]
-	instance: impl Node<F, Output = I>,
+	#[implementations(VectorDataTable, GraphicGroupTable)]
+	instance: I,
 	#[default(1)] random_scale_min: f64,
 	#[default(1)] random_scale_max: f64,
 	random_scale_bias: f64,
@@ -362,11 +249,8 @@ async fn copy_to_points<F: 'n + Send + Copy, I: GraphicElementRendered + ConcatE
 	random_rotation: Angle,
 	random_rotation_seed: SeedValue,
 ) -> GraphicGroupTable {
-	let points = points.eval(footprint).await;
 	let points = points.one_item();
 
-	let instance = instance.eval(footprint).await;
-
 	let instance_transform = instance.transform();
 
 	let random_scale_difference = random_scale_max - random_scale_min;
@@ -421,19 +305,7 @@ async fn copy_to_points<F: 'n + Send + Copy, I: GraphicElementRendered + ConcatE
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn bounding_box<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
-) -> VectorDataTable {
-	let vector_data = vector_data.eval(footprint).await;
+async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
 	let vector_data = vector_data.one_item();
 
 	let bounding_box = vector_data.bounding_box_with_transform(vector_data.transform).unwrap();
@@ -445,30 +317,16 @@ async fn bounding_box<F: 'n + Send>(
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
-async fn offset_path<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
-	distance: f64,
-	line_join: LineJoin,
-	#[default(4.)] miter_limit: f64,
-) -> VectorDataTable {
-	let vector_data = vector_data.eval(footprint).await;
+async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, line_join: LineJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
 	let vector_data = vector_data.one_item();
 
+	let subpaths = vector_data.stroke_bezier_paths();
 	let mut result = VectorData::empty();
 	result.style = vector_data.style.clone();
 	result.style.set_stroke_transform(DAffine2::IDENTITY);
 
 	// Perform operation on all subpaths in this shape.
-	for mut subpath in vector_data.stroke_bezier_paths() {
+	for mut subpath in subpaths {
 		subpath.apply_transform(vector_data.transform);
 
 		// Taking the existing stroke data and passing it to Bezier-rs to generate new paths.
@@ -489,19 +347,7 @@ async fn offset_path<F: 'n + Send>(
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn solidify_stroke<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
-) -> VectorDataTable {
-	let vector_data = vector_data.eval(footprint).await;
+async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
 	let vector_data = vector_data.one_item();
 
 	let transform = &vector_data.transform;
@@ -551,20 +397,8 @@ async fn solidify_stroke<F: 'n + Send>(
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn flatten_vector_elements<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> GraphicGroupTable,
-		Footprint -> GraphicGroupTable,
-	)]
-	graphic_group_input: impl Node<F, Output = GraphicGroupTable>,
-) -> VectorDataTable {
-	let graphic_group = graphic_group_input.eval(footprint).await;
-	let graphic_group = graphic_group.one_item();
+async fn flatten_vector_elements(_: impl Ctx, graphic_group_input: GraphicGroupTable) -> VectorDataTable {
+	let graphic_group_input = graphic_group_input.one_item();
 
 	// A node based solution to support passing through vector data could be a network node with a cache node connected to
 	// a flatten vector elements connected to an if else node, another connection from the cache directly
@@ -587,7 +421,7 @@ async fn flatten_vector_elements<F: 'n + Send>(
 	}
 
 	let mut result = VectorData::empty();
-	concat_group(graphic_group, DAffine2::IDENTITY, &mut result);
+	concat_group(graphic_group_input, DAffine2::IDENTITY, &mut result);
 	// TODO: This leads to incorrect stroke widths when flattening groups with different transforms.
 	result.style.set_stroke_transform(DAffine2::IDENTITY);
 
@@ -614,34 +448,10 @@ impl ConcatElement for GraphicGroupTable {
 }
 
 #[node_macro::node(category(""), path(graphene_core::vector))]
-async fn sample_points<F: 'n + Send + Copy>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
-	spacing: f64,
-	start_offset: f64,
-	stop_offset: f64,
-	adaptive_spacing: bool,
-	#[implementations(
-		() -> Vec<f64>,
-		Footprint -> Vec<f64>,
-	)]
-	subpath_segment_lengths: impl Node<F, Output = Vec<f64>>,
-) -> VectorDataTable {
+async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, subpath_segment_lengths: Vec<f64>) -> VectorDataTable {
 	// Limit the smallest spacing to something sensible to avoid freezing the application.
 	let spacing = spacing.max(0.01);
-
-	// Evaluate vector data and subpath segment lengths asynchronously.
-	let vector_data = vector_data.eval(footprint).await;
 	let vector_data = vector_data.one_item();
-	let subpath_segment_lengths = subpath_segment_lengths.eval(footprint).await;
 
 	// Create an iterator over the bezier segments with enumeration and peeking capability.
 	let mut bezier = vector_data.segment_bezier_iter().enumerate().peekable();
@@ -786,23 +596,14 @@ async fn sample_points<F: 'n + Send + Copy>(
 }
 
 #[node_macro::node(category(""), path(graphene_core::vector))]
-async fn poisson_disk_points<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
+async fn poisson_disk_points(
+	_: impl Ctx,
+	vector_data: VectorDataTable,
 	#[default(10.)]
 	#[min(0.01)]
 	separation_disk_diameter: f64,
 	seed: SeedValue,
 ) -> VectorDataTable {
-	let vector_data = vector_data.eval(footprint).await;
 	let vector_data = vector_data.one_item();
 
 	let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
@@ -846,19 +647,7 @@ async fn poisson_disk_points<F: 'n + Send>(
 }
 
 #[node_macro::node(category(""), path(graphene_core::vector))]
-async fn subpath_segment_lengths<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
-) -> Vec<f64> {
-	let vector_data = vector_data.eval(footprint).await;
+async fn subpath_segment_lengths(_: impl Ctx, vector_data: VectorDataTable) -> Vec<f64> {
 	let vector_data = vector_data.one_item();
 
 	vector_data
@@ -868,20 +657,7 @@ async fn subpath_segment_lengths<F: 'n + Send>(
 }
 
 #[node_macro::node(name("Spline"), category("Vector"), path(graphene_core::vector))]
-async fn spline<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
-) -> VectorDataTable {
-	// Evaluate the vector data within the given footprint.
-	let mut vector_data = vector_data.eval(footprint).await;
+async fn spline(_: impl Ctx, mut vector_data: VectorDataTable) -> VectorDataTable {
 	let vector_data = vector_data.one_item_mut();
 
 	// Exit early if there are no points to generate splines from.
@@ -923,21 +699,7 @@ async fn spline<F: 'n + Send>(
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn jitter_points<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	vector_data: impl Node<F, Output = VectorDataTable>,
-	#[default(5.)] amount: f64,
-	seed: SeedValue,
-) -> VectorDataTable {
-	let vector_data = vector_data.eval(footprint).await;
+async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)] amount: f64, seed: SeedValue) -> VectorDataTable {
 	let mut vector_data = vector_data.one_item().clone();
 
 	let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
@@ -986,31 +748,16 @@ async fn jitter_points<F: 'n + Send>(
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn morph<F: 'n + Send + Copy>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	source: impl Node<F, Output = VectorDataTable>,
-	#[expose]
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	target: impl Node<F, Output = VectorDataTable>,
+async fn morph(
+	_: impl Ctx,
+	source: VectorDataTable,
+	#[expose] target: VectorDataTable,
 	#[range((0., 1.))]
 	#[default(0.5)]
 	time: Fraction,
 	#[min(0.)] start_index: IntegerCount,
 ) -> VectorDataTable {
-	let source = source.eval(footprint).await;
 	let source = source.one_item();
-	let target = target.eval(footprint).await;
 	let target = target.one_item();
 
 	let mut result = VectorData::empty();
@@ -1202,30 +949,16 @@ fn bevel_algorithm(mut vector_data: VectorData, distance: f64) -> VectorData {
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn bevel<F: 'n + Send + Copy>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> VectorDataTable,
-		Footprint -> VectorDataTable,
-	)]
-	source: impl Node<F, Output = VectorDataTable>,
-	#[default(10.)] distance: Length,
-) -> VectorDataTable {
-	let source = source.eval(footprint).await;
+fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) -> VectorDataTable {
 	let source = source.one_item();
 
-	let result = bevel_algorithm(source.clone(), distance);
-
-	VectorDataTable::new(result)
+	VectorDataTable::new(bevel_algorithm(source.clone(), distance))
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn area(_: (), vector_data: impl Node<Footprint, Output = VectorDataTable>) -> f64 {
-	let vector_data = vector_data.eval(Footprint::default()).await;
+async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>) -> f64 {
+	let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
+	let vector_data = vector_data.eval(new_ctx).await;
 	let vector_data = vector_data.one_item();
 
 	let mut area = 0.;
@@ -1237,8 +970,9 @@ async fn area(_: (), vector_data: impl Node<Footprint, Output = VectorDataTable>
 }
 
 #[node_macro::node(category("Vector"), path(graphene_core::vector))]
-async fn centroid(_: (), vector_data: impl Node<Footprint, Output = VectorDataTable>, centroid_type: CentroidType) -> DVec2 {
-	let vector_data = vector_data.eval(Footprint::default()).await;
+async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>, centroid_type: CentroidType) -> DVec2 {
+	let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
+	let vector_data = vector_data.eval(new_ctx).await;
 	let vector_data = vector_data.one_item();
 
 	if centroid_type == CentroidType::Area {
@@ -1303,16 +1037,16 @@ mod test {
 		}
 	}
 
-	fn vector_node(data: Subpath<PointId>) -> FutureWrapperNode<VectorDataTable> {
-		FutureWrapperNode(VectorDataTable::new(VectorData::from_subpath(data)))
+	fn vector_node(data: Subpath<PointId>) -> VectorDataTable {
+		VectorDataTable::new(VectorData::from_subpath(data))
 	}
 
 	#[tokio::test]
 	async fn repeat() {
 		let direction = DVec2::X * 1.5;
 		let instances = 3;
-		let repeated = super::repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
-		let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
+		let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
+		let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
 		let vector_data = vector_data.one_item();
 		assert_eq!(vector_data.region_bezier_paths().count(), 3);
 		for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
@@ -1323,8 +1057,8 @@ mod test {
 	async fn repeat_transform_position() {
 		let direction = DVec2::new(12., 10.);
 		let instances = 8;
-		let repeated = super::repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
-		let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
+		let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
+		let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
 		let vector_data = vector_data.one_item();
 		assert_eq!(vector_data.region_bezier_paths().count(), 8);
 		for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
@@ -1333,8 +1067,8 @@ mod test {
 	}
 	#[tokio::test]
 	async fn circle_repeat() {
-		let repeated = super::circular_repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
-		let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
+		let repeated = super::circular_repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
+		let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
 		let vector_data = vector_data.one_item();
 		assert_eq!(vector_data.region_bezier_paths().count(), 8);
 		for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
@@ -1346,10 +1080,7 @@ mod test {
 	}
 	#[tokio::test]
 	async fn bounding_box() {
-		let bounding_box = BoundingBoxNode {
-			vector_data: vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)),
-		};
-		let bounding_box = bounding_box.eval(Footprint::default()).await;
+		let bounding_box = super::bounding_box((), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))).await;
 		let bounding_box = bounding_box.one_item();
 		assert_eq!(bounding_box.region_bezier_paths().count(), 1);
 		let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
@@ -1375,8 +1106,8 @@ mod test {
 		let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.);
 		let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE);
 		let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec();
-		let copy_to_points = super::copy_to_points(Footprint::default(), &vector_node(points), &vector_node(instance), 1., 1., 0., 0, 0., 0).await;
-		let flattened_copy_to_points = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(copy_to_points)).await;
+		let copy_to_points = super::copy_to_points(Footprint::default(), vector_node(points), vector_node(instance), 1., 1., 0., 0, 0., 0).await;
+		let flattened_copy_to_points = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
 		let flattened_copy_to_points = flattened_copy_to_points.one_item();
 		assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len());
 		for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() {
@@ -1390,7 +1121,7 @@ mod test {
 	#[tokio::test]
 	async fn sample_points() {
 		let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
-		let sample_points = super::sample_points(Footprint::default(), &vector_node(path), 30., 0., 0., false, &FutureWrapperNode(vec![100.])).await;
+		let sample_points = super::sample_points(Footprint::default(), vector_node(path), 30., 0., 0., false, vec![100.]).await;
 		let sample_points = sample_points.one_item();
 		assert_eq!(sample_points.point_domain.positions().len(), 4);
 		for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
@@ -1400,7 +1131,7 @@ mod test {
 	#[tokio::test]
 	async fn adaptive_spacing() {
 		let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
-		let sample_points = super::sample_points(Footprint::default(), &vector_node(path), 18., 45., 10., true, &FutureWrapperNode(vec![100.])).await;
+		let sample_points = super::sample_points(Footprint::default(), vector_node(path), 18., 45., 10., true, vec![100.]).await;
 		let sample_points = sample_points.one_item();
 		assert_eq!(sample_points.point_domain.positions().len(), 4);
 		for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
@@ -1411,7 +1142,7 @@ mod test {
 	async fn poisson() {
 		let sample_points = super::poisson_disk_points(
 			Footprint::default(),
-			&vector_node(Subpath::new_ellipse(DVec2::NEG_ONE * 50., DVec2::ONE * 50.)),
+			vector_node(Subpath::new_ellipse(DVec2::NEG_ONE * 50., DVec2::ONE * 50.)),
 			10. * std::f64::consts::SQRT_2,
 			0,
 		)
@@ -1429,12 +1160,12 @@ mod test {
 	#[tokio::test]
 	async fn lengths() {
 		let subpath = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
-		let lengths = subpath_segment_lengths(Footprint::default(), &vector_node(subpath)).await;
+		let lengths = subpath_segment_lengths(Footprint::default(), vector_node(subpath)).await;
 		assert_eq!(lengths, vec![100.]);
 	}
 	#[tokio::test]
 	async fn spline() {
-		let spline = super::spline(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await;
+		let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await;
 		let spline = spline.one_item();
 		assert_eq!(spline.stroke_bezier_paths().count(), 1);
 		assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]);
@@ -1443,7 +1174,7 @@ mod test {
 	async fn morph() {
 		let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
 		let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO);
-		let sample_points = super::morph(Footprint::default(), &vector_node(source), &vector_node(target), 0.5, 0).await;
+		let sample_points = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5, 0).await;
 		let sample_points = sample_points.one_item();
 		assert_eq!(
 			&sample_points.point_domain.positions()[..4],
@@ -1452,7 +1183,7 @@ mod test {
 	}
 
 	#[track_caller]
-	fn contains_segment(vector: &VectorData, target: bezier_rs::Bezier) {
+	fn contains_segment(vector: VectorData, target: bezier_rs::Bezier) {
 		let segments = vector.segment_bezier_iter().map(|x| x.1);
 		let count = segments.filter(|bezier| bezier.abs_diff_eq(&target, 0.01) || bezier.reversed().abs_diff_eq(&target, 0.01)).count();
 		assert_eq!(count, 1, "Incorrect number of {target:#?} in {:#?}", vector.segment_bezier_iter().collect::<Vec<_>>());
@@ -1461,42 +1192,42 @@ mod test {
 	#[tokio::test]
 	async fn bevel_rect() {
 		let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
-		let beveled = super::bevel(Footprint::default(), &vector_node(source), 5.).await;
+		let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
 		let beveled = beveled.one_item();
 
 		assert_eq!(beveled.point_domain.positions().len(), 8);
 		assert_eq!(beveled.segment_domain.ids().len(), 8);
 
 		// Segments
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(95., 0.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(95., 100.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 5.), DVec2::new(0., 95.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 5.), DVec2::new(100., 95.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(95., 0.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(95., 100.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 5.), DVec2::new(0., 95.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 5.), DVec2::new(100., 95.)));
 
 		// Joins
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(0., 5.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(95., 0.), DVec2::new(100., 5.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 95.), DVec2::new(95., 100.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(0., 95.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 0.), DVec2::new(0., 5.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(95., 0.), DVec2::new(100., 5.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 95.), DVec2::new(95., 100.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(5., 100.), DVec2::new(0., 95.)));
 	}
 
 	#[tokio::test]
 	async fn bevel_open_curve() {
 		let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::X * 100.);
 		let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), curve], false);
-		let beveled = super::bevel(Footprint::default(), &vector_node(source), 5.).await;
+		let beveled = super::bevel((), vector_node(source), 5.);
 		let beveled = beveled.one_item();
 
 		assert_eq!(beveled.point_domain.positions().len(), 4);
 		assert_eq!(beveled.segment_domain.ids().len(), 3);
 
 		// Segments
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-100., 0.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-100., 0.)));
 		let trimmed = curve.trim(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.));
-		contains_segment(&beveled, trimmed);
+		contains_segment(beveled.clone(), trimmed);
 
 		// Join
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
 	}
 
 	#[tokio::test]
@@ -1506,7 +1237,7 @@ mod test {
 		let mut vector_data = VectorData::from_subpath(source);
 		let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
 		vector_data.transform = transform;
-		let beveled = super::bevel(Footprint::default(), &FutureWrapperNode(VectorDataTable::new(vector_data)), 5.).await;
+		let beveled = super::bevel((), VectorDataTable::new(vector_data), 5.);
 		let beveled = beveled.one_item();
 
 		assert_eq!(beveled.point_domain.positions().len(), 4);
@@ -1514,31 +1245,31 @@ mod test {
 		assert_eq!(beveled.transform, transform);
 
 		// Segments
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-0.5, 0.), DVec2::new(-10., 0.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-0.5, 0.), DVec2::new(-10., 0.)));
 		let trimmed = curve.trim(bezier_rs::TValue::Euclidean(0.5 / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.));
-		contains_segment(&beveled, trimmed);
+		contains_segment(beveled.clone(), trimmed);
 
 		// Join
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-0.5, 0.), trimmed.start));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-0.5, 0.), trimmed.start));
 	}
 
 	#[tokio::test]
 	async fn bevel_too_high() {
 		let source = Subpath::from_anchors([DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)], false);
-		let beveled = super::bevel(Footprint::default(), &vector_node(source), 999.).await;
+		let beveled = super::bevel(Footprint::default(), vector_node(source), 999.);
 		let beveled = beveled.one_item();
 
 		assert_eq!(beveled.point_domain.positions().len(), 6);
 		assert_eq!(beveled.segment_domain.ids().len(), 5);
 
 		// Segments
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 0.), DVec2::new(50., 0.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(100., 50.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(0., 0.), DVec2::new(50., 0.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(100., 50.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
 
 		// Joins
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(50., 0.), DVec2::new(100., 50.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(50., 0.), DVec2::new(100., 50.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(100., 50.), DVec2::new(50., 100.)));
 	}
 
 	#[tokio::test]
@@ -1546,18 +1277,18 @@ mod test {
 		let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::X * 100.);
 		let point = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::ZERO, DVec2::ZERO);
 		let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), point, curve], false);
-		let beveled = super::bevel(Footprint::default(), &vector_node(source), 5.).await;
+		let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
 		let beveled = beveled.one_item();
 
 		assert_eq!(beveled.point_domain.positions().len(), 6);
 		assert_eq!(beveled.segment_domain.ids().len(), 5);
 
 		// Segments
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-100., 0.), DVec2::new(-5., 0.)));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(0., 0.)));
-		contains_segment(&beveled, point);
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-100., 0.), DVec2::new(-5., 0.)));
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(0., 0.)));
+		contains_segment(beveled.clone(), point);
 		let [start, end] = curve.split(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))));
-		contains_segment(&beveled, bezier_rs::Bezier::from_linear_dvec2(start.start, start.end));
-		contains_segment(&beveled, end);
+		contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(start.start, start.end));
+		contains_segment(beveled.clone(), end);
 	}
 }
diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml
index 107ef5e413..b876af99b3 100644
--- a/node-graph/graph-craft/Cargo.toml
+++ b/node-graph/graph-craft/Cargo.toml
@@ -54,6 +54,7 @@ winit = { workspace = true }
 [dev-dependencies]
 # Workspace dependencies
 graph-craft = { workspace = true, features = ["loading"] }
+pretty_assertions = { workspace = true }
 
 # Required dependencies
 criterion = { version = "0.5", features = ["html_reports"]}
diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs
index 8e8b2bfd14..65d1af8c5a 100644
--- a/node-graph/graph-craft/src/document.rs
+++ b/node-graph/graph-craft/src/document.rs
@@ -304,7 +304,7 @@ impl DocumentNode {
 			match first {
 				NodeInput::Value { tagged_value, .. } => {
 					assert_eq!(self.inputs.len(), 0, "A value node cannot have any inputs. Current inputs: {:?}", self.inputs);
-					(ProtoNodeInput::None, ConstructionArgs::Value(tagged_value))
+					(ProtoNodeInput::ManualComposition(concrete!(graphene_core::Context<'static>)), ConstructionArgs::Value(tagged_value))
 				}
 				NodeInput::Node { node_id, output_index, lambda } => {
 					assert_eq!(output_index, 0, "Outputs should be flattened before converting to proto node");
@@ -1567,7 +1567,7 @@ mod test {
 					NodeId(14),
 					ProtoNode {
 						identifier: "graphene_core::value::ClonedNode".into(),
-						input: ProtoNodeInput::None,
+						input: ProtoNodeInput::ManualComposition(concrete!(graphene_core::Context)),
 						construction_args: ConstructionArgs::Value(TaggedValue::U32(2).into()),
 						original_location: OriginalLocation {
 							path: Some(vec![NodeId(1), NodeId(4)]),
@@ -1589,7 +1589,7 @@ mod test {
 
 		println!("{:#?}", resolved_network[0]);
 		println!("{construction_network:#?}");
-		assert_eq!(resolved_network[0], construction_network);
+		pretty_assertions::assert_eq!(resolved_network[0], construction_network);
 	}
 
 	fn flat_network() -> NodeNetwork {
diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs
index 81034f915d..d2b83c83de 100644
--- a/node-graph/graph-craft/src/document/value.rs
+++ b/node-graph/graph-craft/src/document/value.rs
@@ -102,8 +102,8 @@ macro_rules! tagged_value {
 						})
 					}
 					Type::Fn(_, output) => TaggedValue::from_type(output),
-					Type::Future(_) => {
-						None
+					Type::Future(output) => {
+						TaggedValue::from_type(output)
 					}
 				}
 			}
@@ -270,7 +270,7 @@ impl TaggedValue {
 				Some(ty)
 			}
 			Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output),
-			Type::Future(_) => None,
+			Type::Future(fut) => TaggedValue::from_primitive_string(string, fut),
 		}
 	}
 
diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs
index 79d780e8fa..cb71106c80 100644
--- a/node-graph/graph-craft/src/proto.rs
+++ b/node-graph/graph-craft/src/proto.rs
@@ -162,7 +162,7 @@ impl Default for ProtoNode {
 #[derive(Debug, PartialEq, Eq, Clone, Hash)]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub enum ProtoNodeInput {
-	/// [`ProtoNode`]s do not require any input, e.g. the value node just takes in [`ConstructionArgs`].
+	/// This input will be converted to `()` as the call argument.
 	None,
 	/// A ManualComposition input represents an input that opts out of being resolved through the `ComposeNode`, which first runs the previous (upstream) node, then passes that evaluated
 	/// result to this node. Instead, ManualComposition lets this node actually consume the provided input instead of passing it to its predecessor.
@@ -203,6 +203,7 @@ impl ProtoNode {
 		if self.skip_deduplication {
 			self.original_location.path.hash(&mut hasher);
 		}
+
 		std::mem::discriminant(&self.input).hash(&mut hasher);
 		match self.input {
 			ProtoNodeInput::None => (),
@@ -212,6 +213,7 @@ impl ProtoNode {
 			ProtoNodeInput::Node(id) => (id, false).hash(&mut hasher),
 			ProtoNodeInput::NodeLambda(id) => (id, true).hash(&mut hasher),
 		};
+
 		Some(NodeId(hasher.finish()))
 	}
 
@@ -224,7 +226,7 @@ impl ProtoNode {
 		Self {
 			identifier: ProtoNodeIdentifier::new("graphene_core::value::ClonedNode"),
 			construction_args: value,
-			input: ProtoNodeInput::None,
+			input: ProtoNodeInput::ManualComposition(concrete!(Context)),
 			original_location: OriginalLocation {
 				path: Some(path),
 				inputs_exposed: vec![false; inputs_exposed],
@@ -552,10 +554,11 @@ impl core::fmt::Debug for GraphErrorType {
 			GraphErrorType::NoImplementations => write!(f, "No implementations found"),
 			GraphErrorType::NoConstructor => write!(f, "No construct found for node"),
 			GraphErrorType::InvalidImplementations { inputs, error_inputs } => {
-				let format_error = |(index, (_found, expected)): &(usize, (Type, Type))| format!("• Input {}: {expected}", index + 1);
-				let format_error_list = |errors: &Vec<(usize, (Type, Type))>| errors.iter().map(format_error).collect::<Vec<_>>().join("\n");
+				let format_error = |(index, (_found, expected)): &(usize, (Type, Type))| format!("• Input {}: {expected}, found: {_found}", index + 1);
+				let format_error_list = |errors: &Vec<(usize, (Type, Type))>| errors.iter().map(format_error).collect::<Vec<_>>().join("\n").replace("Option<Arc<OwnedContextImpl>>", "Context");
 				let mut errors = error_inputs.iter().map(format_error_list).collect::<Vec<_>>();
 				errors.sort();
+				let inputs = inputs.replace("Option<Arc<OwnedContextImpl>>", "Context");
 				write!(
 					f,
 					"This node isn't compatible with the com-\n\
@@ -651,9 +654,9 @@ impl TypingContext {
 		let inputs = match node.construction_args {
 			// If the node has a value input we can infer the return type from it
 			ConstructionArgs::Value(ref v) => {
-				assert!(matches!(node.input, ProtoNodeInput::None));
+				assert!(matches!(node.input, ProtoNodeInput::None) || matches!(node.input, ProtoNodeInput::ManualComposition(ref x) if x == &concrete!(Context)));
 				// TODO: This should return a reference to the value
-				let types = NodeIOTypes::new(concrete!(()), v.ty(), vec![v.ty()]);
+				let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]);
 				self.inferred.insert(node_id, types.clone());
 				return Ok(types);
 			}
@@ -696,6 +699,8 @@ impl TypingContext {
 			match (from, to) {
 				// Direct comparison of two concrete types.
 				(Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2,
+				// Check inner type for futures
+				(Type::Future(type1), Type::Future(type2)) => type1 == type2,
 				// Loose comparison of function types, where loose means that functions are considered on a "greater than or equal to" basis of its function type's generality.
 				// That means we compare their types with a contravariant relationship, which means that a more general type signature may be substituted for a more specific type signature.
 				// For example, we allow `T -> V` to be substituted with `T' -> V` or `() -> V` where T' and () are more specific than T.
diff --git a/node-graph/gstd/src/brush.rs b/node-graph/gstd/src/brush.rs
index 43f5b4c22a..7017b73c99 100644
--- a/node-graph/gstd/src/brush.rs
+++ b/node-graph/gstd/src/brush.rs
@@ -1,21 +1,23 @@
-use crate::raster::{blend_image_closure, BlendImageTupleNode, EmptyImageNode, ExtendImageToBoundsNode};
+use crate::raster::{blend_image_closure, BlendImageTupleNode, ExtendImageToBoundsNode};
 
+use graph_craft::generic::FnNode;
+use graph_craft::proto::FutureWrapperNode;
 use graphene_core::raster::adjustments::blend_colors;
 use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
 use graphene_core::raster::brush_cache::BrushCache;
 use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
 use graphene_core::raster::BlendMode;
-use graphene_core::raster::{Alpha, BlendColorPairNode, Color, Image, Pixel, Sample};
-use graphene_core::transform::{Footprint, Transform, TransformMut};
+use graphene_core::raster::{Alpha, Color, Image, Pixel, Sample};
+use graphene_core::transform::{Transform, TransformMut};
 use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
 use graphene_core::vector::brush_stroke::{BrushStroke, BrushStyle};
 use graphene_core::vector::VectorDataTable;
-use graphene_core::Node;
+use graphene_core::{Ctx, Node};
 
 use glam::{DAffine2, DVec2};
 
 #[node_macro::node(category("Debug"))]
-fn vector_points(_: (), vector_data: VectorDataTable) -> Vec<DVec2> {
+fn vector_points(_: impl Ctx, vector_data: VectorDataTable) -> Vec<DVec2> {
 	let vector_data = vector_data.one_item();
 
 	vector_data.point_domain.positions().to_vec()
@@ -131,14 +133,21 @@ where
 	target
 }
 
-pub fn create_brush_texture(brush_style: &BrushStyle) -> Image<Color> {
-	let stamp = BrushStampGeneratorNode::new(CopiedNode::new(brush_style.color), CopiedNode::new(brush_style.hardness), CopiedNode::new(brush_style.flow));
-	let stamp = stamp.eval(brush_style.diameter);
+pub async fn create_brush_texture(brush_style: &BrushStyle) -> Image<Color> {
+	let stamp = brush_stamp_generator(brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow);
 	let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
-	let blank_texture = EmptyImageNode::new(CopiedNode::new(transform), CopiedNode::new(Color::TRANSPARENT)).eval(());
-	let normal_blend = BlendColorPairNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
-	let blend_executor = BlendImageTupleNode::new(ValueNode::new(normal_blend));
-	blend_executor.eval((blank_texture, stamp)).image
+	use crate::raster::empty_image;
+	let blank_texture = empty_image((), transform, Color::TRANSPARENT);
+	// let normal_blend = BlendColorPairNode::new(
+	// 	FutureWrapperNode::new(ValueNode::new(CopiedNode::new(BlendMode::Normal))),
+	// 	FutureWrapperNode::new(ValueNode::new(CopiedNode::new(100.))),
+	// );
+	// normal_blend.eval((Color::default(), Color::default()));
+	// use crate::raster::blend_image_tuple;
+	// blend_image_tuple((blank_texture, stamp), &normal_blend).await.image;
+	crate::raster::blend_image_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)).image
+	// let blend_executoc = BlendImageTupleNode::new(FutureWrapperNode::new(ValueNode::new(normal_blend)));
+	// blend_executor.eval((blank_texture, stamp)).image
 }
 
 macro_rules! inline_blend_funcs {
@@ -202,7 +211,7 @@ pub fn blend_with_mode(background: ImageFrame<Color>, foreground: ImageFrame<Col
 }
 
 #[node_macro::node(category(""))]
-fn brush(_: Footprint, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Color>, strokes: Vec<BrushStroke>, cache: BrushCache) -> ImageFrameTable<Color> {
+async fn brush(_: impl Ctx, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Color>, strokes: Vec<BrushStroke>, cache: BrushCache) -> ImageFrameTable<Color> {
 	let image = image.one_item().clone();
 
 	let stroke_bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
@@ -225,11 +234,13 @@ fn brush(_: Footprint, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Co
 	for (idx, stroke) in brush_plan.strokes.into_iter().enumerate() {
 		// Create brush texture.
 		// TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes.
-		let brush_texture = cache.get_cached_brush(&stroke.style).unwrap_or_else(|| {
-			let tex = create_brush_texture(&stroke.style);
+		let mut brush_texture = cache.get_cached_brush(&stroke.style);
+		if brush_texture.is_none() {
+			let tex = create_brush_texture(&stroke.style).await;
 			cache.store_brush(stroke.style.clone(), tex.clone());
-			tex
-		});
+			brush_texture = Some(tex);
+		}
+		let brush_texture = brush_texture.unwrap();
 
 		// Compute transformation from stroke texture space into layer space, and create the stroke texture.
 		let skip = if idx == 0 { brush_plan.first_stroke_point_skip } else { 0 };
@@ -246,16 +257,23 @@ fn brush(_: Footprint, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Co
 			let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.);
 			let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size);
 
-			let normal_blend = BlendColorPairNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
-			let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(normal_blend));
+			// let normal_blend = BlendColorPairNode::new(ValueNode::new(CopiedNode::new(BlendMode::Normal)), ValueNode::new(CopiedNode::new(100.)));
+			let normal_blend = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Normal, 1.));
+			let blit_node = BlitNode::new(
+				FutureWrapperNode::new(ClonedNode::new(brush_texture)),
+				FutureWrapperNode::new(ClonedNode::new(positions)),
+				FutureWrapperNode::new(ClonedNode::new(normal_blend)),
+			);
 			let blit_target = if idx == 0 {
 				let target = core::mem::take(&mut brush_plan.first_stroke_texture);
 				ExtendImageToBoundsNode::new(CopiedNode::new(stroke_to_layer)).eval(target)
 			} else {
-				EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
+				use crate::raster::empty_image;
+				empty_image((), stroke_to_layer, Color::TRANSPARENT)
+				// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
 			};
 
-			blit_node.eval(blit_target)
+			blit_node.eval(blit_target).await
 		};
 
 		// Cache image before doing final blend, and store final stroke texture.
@@ -277,34 +295,44 @@ fn brush(_: Footprint, image: ImageFrameTable<Color>, bounds: ImageFrameTable<Co
 		let mut erase_restore_mask = opaque_image;
 
 		for stroke in erase_restore_strokes {
-			let brush_texture = cache.get_cached_brush(&stroke.style).unwrap_or_else(|| {
-				let tex = create_brush_texture(&stroke.style);
+			let mut brush_texture = cache.get_cached_brush(&stroke.style);
+			if brush_texture.is_none() {
+				let tex = create_brush_texture(&stroke.style).await;
 				cache.store_brush(stroke.style.clone(), tex.clone());
-				tex
-			});
+				brush_texture = Some(tex);
+			}
+			let brush_texture = brush_texture.unwrap();
 			let positions: Vec<_> = stroke.compute_blit_points().into_iter().collect();
 
 			match stroke.style.blend_mode {
 				BlendMode::Erase => {
-					let blend_params = BlendColorPairNode::new(CopiedNode::new(BlendMode::Erase), CopiedNode::new(100.));
-					let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params));
-					erase_restore_mask = blit_node.eval(erase_restore_mask);
+					let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Erase, 1.));
+					let blit_node = BlitNode::new(
+						FutureWrapperNode::new(ClonedNode::new(brush_texture)),
+						FutureWrapperNode::new(ClonedNode::new(positions)),
+						FutureWrapperNode::new(ClonedNode::new(blend_params)),
+					);
+					erase_restore_mask = blit_node.eval(erase_restore_mask).await;
 				}
 
 				// Yes, this is essentially the same as the above, but we duplicate to inline the blend mode.
 				BlendMode::Restore => {
-					let blend_params = BlendColorPairNode::new(CopiedNode::new(BlendMode::Restore), CopiedNode::new(100.));
-					let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params));
-					erase_restore_mask = blit_node.eval(erase_restore_mask);
+					let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Restore, 1.));
+					let blit_node = BlitNode::new(
+						FutureWrapperNode::new(ClonedNode::new(brush_texture)),
+						FutureWrapperNode::new(ClonedNode::new(positions)),
+						FutureWrapperNode::new(ClonedNode::new(blend_params)),
+					);
+					erase_restore_mask = blit_node.eval(erase_restore_mask).await;
 				}
 
 				_ => unreachable!(),
 			}
 		}
 
-		let blend_params = BlendColorPairNode::new(CopiedNode::new(BlendMode::MultiplyAlpha), CopiedNode::new(100.));
-		let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params));
-		actual_image = blend_executor.eval((actual_image, erase_restore_mask));
+		let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.));
+		let blend_executor = BlendImageTupleNode::new(FutureWrapperNode::new(ValueNode::new(blend_params)));
+		actual_image = blend_executor.eval((actual_image, erase_restore_mask)).await;
 	}
 
 	ImageFrameTable::new(actual_image)
@@ -315,15 +343,13 @@ mod test {
 	use super::*;
 
 	use graphene_core::transform::Transform;
-	use graphene_core::value::ClonedNode;
 
 	use glam::DAffine2;
 
 	#[test]
 	fn test_brush_texture() {
-		let brush_texture_node = BrushStampGeneratorNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(100.), ClonedNode::new(100.));
 		let size = 20.;
-		let image = brush_texture_node.eval(size);
+		let image = brush_stamp_generator(size, Color::BLACK, 100., 100.);
 		assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.)));
 		// center pixel should be BLACK
 		assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK));
diff --git a/node-graph/gstd/src/dehaze.rs b/node-graph/gstd/src/dehaze.rs
index 647983104e..20d942d2f7 100644
--- a/node-graph/gstd/src/dehaze.rs
+++ b/node-graph/gstd/src/dehaze.rs
@@ -1,28 +1,14 @@
 use graph_craft::proto::types::Percentage;
 use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
 use graphene_core::raster::Image;
-use graphene_core::transform::Footprint;
-use graphene_core::Color;
+use graphene_core::{Color, Ctx};
 
 use image::{DynamicImage, GenericImage, GenericImageView, GrayImage, ImageBuffer, Luma, Rgba, RgbaImage};
 use ndarray::{Array2, ArrayBase, Dim, OwnedRepr};
 use std::cmp::{max, min};
 
-#[node_macro::node(category("Raster: Filter"))]
-async fn dehaze<F: 'n + Send + Sync>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> ImageFrameTable<Color>,
-		Footprint -> ImageFrameTable<Color>,
-	)]
-	image_frame: impl Node<F, Output = ImageFrameTable<Color>>,
-	strength: Percentage,
-) -> ImageFrameTable<Color> {
-	let image_frame = image_frame.eval(footprint).await;
+#[node_macro::node(category("Raster"))]
+async fn dehaze(_: impl Ctx, image_frame: ImageFrameTable<Color>, strength: Percentage) -> ImageFrameTable<Color> {
 	let image_frame = image_frame.one_item();
 
 	// Prepare the image data for processing
diff --git a/node-graph/gstd/src/gpu_nodes.rs b/node-graph/gstd/src/gpu_nodes.rs
index c30ddf015d..4e2233f33d 100644
--- a/node-graph/gstd/src/gpu_nodes.rs
+++ b/node-graph/gstd/src/gpu_nodes.rs
@@ -19,7 +19,7 @@ use crate::wasm_application_io::WasmApplicationIo;
 
 // TODO: Move to graph-craft
 #[node_macro::node(category("Debug: GPU"))]
-async fn compile_gpu<'a: 'n>(_: (), node: &'a DocumentNode, typing_context: TypingContext, io: ShaderIO) -> Result<compilation_client::Shader, String> {
+async fn compile_gpu<'a: 'n>(_: impl Ctx, node: &'a DocumentNode, typing_context: TypingContext, io: ShaderIO) -> Result<compilation_client::Shader, String> {
 	let mut typing_context = typing_context;
 	let compiler = graph_craft::graphene_compiler::Compiler {};
 	let DocumentNodeImplementation::Network(ref network) = node.implementation else { panic!() };
@@ -279,7 +279,7 @@ where
 }
 
 #[node_macro::node(category("Debug: GPU"))]
-async fn blend_gpu_image(_: (), foreground: ImageFrameTable<Color>, background: ImageFrameTable<Color>, blend_mode: BlendMode, opacity: f64) -> ImageFrameTable<Color> {
+async fn blend_gpu_image(_: impl Ctx, foreground: ImageFrameTable<Color>, background: ImageFrameTable<Color>, blend_mode: BlendMode, opacity: f64) -> ImageFrameTable<Color> {
 	let foreground = foreground.one_item();
 	let background = background.one_item();
 
diff --git a/node-graph/gstd/src/http.rs b/node-graph/gstd/src/http.rs
index 26c6fab0f0..a90af0ba97 100644
--- a/node-graph/gstd/src/http.rs
+++ b/node-graph/gstd/src/http.rs
@@ -1,9 +1,11 @@
+use graphene_core::Ctx;
+
 #[node_macro::node(category("Network"))]
-async fn get_request(_: (), url: String) -> reqwest::Response {
+async fn get_request(_: impl Ctx, url: String) -> reqwest::Response {
 	reqwest::get(url).await.unwrap()
 }
 
 #[node_macro::node(category("Network"))]
-async fn post_request(_: (), url: String, body: String) -> reqwest::Response {
+async fn post_request(_: impl Ctx, url: String, body: String) -> reqwest::Response {
 	reqwest::Client::new().post(url).body(body).send().await.unwrap()
 }
diff --git a/node-graph/gstd/src/image_color_palette.rs b/node-graph/gstd/src/image_color_palette.rs
index 03ac5245f6..a554c9ee15 100644
--- a/node-graph/gstd/src/image_color_palette.rs
+++ b/node-graph/gstd/src/image_color_palette.rs
@@ -1,19 +1,10 @@
 use graphene_core::raster::image::ImageFrameTable;
-use graphene_core::transform::Footprint;
-use graphene_core::Color;
+use graphene_core::{Color, Ctx};
 
 #[node_macro::node(category("Raster"))]
-async fn image_color_palette<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> ImageFrameTable<Color>,
-		Footprint -> ImageFrameTable<Color>,
-	)]
-	image: impl Node<F, Output = ImageFrameTable<Color>>,
+async fn image_color_palette(
+	_: impl Ctx,
+	image: ImageFrameTable<Color>,
 	#[min(1.)]
 	#[max(28.)]
 	max_size: u32,
@@ -25,7 +16,6 @@ async fn image_color_palette<F: 'n + Send>(
 	let mut histogram: Vec<usize> = vec![0; (bins + 1.) as usize];
 	let mut colors: Vec<Vec<Color>> = vec![vec![]; (bins + 1.) as usize];
 
-	let image = image.eval(footprint).await;
 	let image = image.one_item();
 
 	for pixel in image.image.data.iter() {
@@ -75,30 +65,24 @@ async fn image_color_palette<F: 'n + Send>(
 mod test {
 	use super::*;
 
-	use graph_craft::generic::FnNode;
 	use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
 	use graphene_core::raster::Image;
-	use graphene_core::value::CopiedNode;
-	use graphene_core::Node;
 
 	#[test]
 	fn test_image_color_palette() {
-		let node = ImageColorPaletteNode {
-			max_size: CopiedNode(1u32),
-			image: FnNode::new(|_| {
-				Box::pin(async move {
-					ImageFrameTable::new(ImageFrame {
-						image: Image {
-							width: 100,
-							height: 100,
-							data: vec![Color::from_rgbaf32(0., 0., 0., 1.).unwrap(); 10000],
-							base64_string: None,
-						},
-						..Default::default()
-					})
-				})
+		let result = image_color_palette(
+			(),
+			ImageFrameTable::new(ImageFrame {
+				image: Image {
+					width: 100,
+					height: 100,
+					data: vec![Color::from_rgbaf32(0., 0., 0., 1.).unwrap(); 10000],
+					base64_string: None,
+				},
+				..Default::default()
 			}),
-		};
-		assert_eq!(futures::executor::block_on(node.eval(())), [Color::from_rgbaf32(0., 0., 0., 1.).unwrap()]);
+			1,
+		);
+		assert_eq!(futures::executor::block_on(result), [Color::from_rgbaf32(0., 0., 0., 1.).unwrap()]);
 	}
 }
diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs
index ed4a9b7447..39cbf95efe 100644
--- a/node-graph/gstd/src/raster.rs
+++ b/node-graph/gstd/src/raster.rs
@@ -2,10 +2,11 @@ use dyn_any::DynAny;
 use graphene_core::raster::bbox::Bbox;
 use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
 use graphene_core::raster::{
-	Alpha, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, Image, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue, Sample,
+	Alpha, AlphaMut, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, Image, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue,
+	Sample,
 };
-use graphene_core::transform::{Footprint, Transform};
-use graphene_core::{AlphaBlending, Color, Node};
+use graphene_core::transform::Transform;
+use graphene_core::{AlphaBlending, Color, Ctx, ExtractFootprint, Node};
 
 use fastnoise_lite;
 use glam::{DAffine2, DVec2, Vec2};
@@ -28,13 +29,14 @@ impl From<std::io::Error> for Error {
 }
 
 #[node_macro::node(category("Debug: Raster"))]
-fn sample_image(footprint: Footprint, image_frame: ImageFrameTable<Color>) -> ImageFrameTable<Color> {
+fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: ImageFrameTable<Color>) -> ImageFrameTable<Color> {
 	let image_frame = image_frame.one_item();
 
 	// Resize the image using the image crate
 	let image = &image_frame.image;
 	let data = bytemuck::cast_vec(image.data.clone());
 
+	let footprint = ctx.footprint();
 	let viewport_bounds = footprint.viewport_bounds_in_local_space();
 	let image_bounds = Bbox::from_transform(image_frame.transform).to_axis_aligned_bbox();
 	let intersection = viewport_bounds.intersect(&image_bounds);
@@ -107,15 +109,7 @@ where
 	image
 }
 
-#[derive(Debug, Clone, Copy)]
-pub struct InsertChannelNode<P, S, Insertion, TargetChannel> {
-	insertion: Insertion,
-	target_channel: TargetChannel,
-	_p: PhantomData<P>,
-	_s: PhantomData<S>,
-}
-
-#[node_macro::old_node_fn(InsertChannelNode<_P, _S>)]
+#[node_macro::node]
 fn insert_channel<
 	// _P is the color of the input image.
 	_P: RGBMut,
@@ -124,8 +118,9 @@ fn insert_channel<
 	Input: BitmapMut<Pixel = _P>,
 	Insertion: Bitmap<Pixel = _S>,
 >(
-	mut image: Input,
-	insertion: Insertion,
+	_: impl Ctx,
+	#[implementations(ImageFrameTable<Color>)] mut image: Input,
+	#[implementations(ImageFrameTable<Color>)] insertion: Insertion,
 	target_channel: RedGreenBlue,
 ) -> Input
 where
@@ -154,15 +149,60 @@ where
 
 	image
 }
+#[node_macro::node]
+fn combine_channels<
+	// _P is the color of the input image.
+	_P: RGBMut + AlphaMut,
+	_S: Pixel + Luminance,
+	// Input image
+	Input: BitmapMut<Pixel = _P>,
+	Red: Bitmap<Pixel = _S>,
+	Green: Bitmap<Pixel = _S>,
+	Blue: Bitmap<Pixel = _S>,
+	Alpha: Bitmap<Pixel = _S>,
+>(
+	_: impl Ctx,
+	#[implementations(ImageFrameTable<Color>)] mut image: Input,
+	#[implementations(ImageFrameTable<Color>)] red: Red,
+	#[implementations(ImageFrameTable<Color>)] green: Green,
+	#[implementations(ImageFrameTable<Color>)] blue: Blue,
+	#[implementations(ImageFrameTable<Color>)] alpha: Alpha,
+) -> Input
+where
+	_P::ColorChannel: Linear,
+{
+	let dimensions = [red.dim(), green.dim(), blue.dim(), alpha.dim()];
+	if dimensions.iter().all(|&(x, _)| x == 0) {
+		return image;
+	}
 
-#[derive(Debug, Clone, Copy)]
-pub struct MaskImageNode<P, S, Stencil> {
-	stencil: Stencil,
-	_p: PhantomData<P>,
-	_s: PhantomData<S>,
+	if dimensions.iter().any(|&(x, y)| x != image.width() || y != image.height()) {
+		log::warn!("Stencil and image have different sizes. This is not supported.");
+		return image;
+	}
+
+	for y in 0..image.height() {
+		for x in 0..image.width() {
+			let image_pixel = image.get_pixel_mut(x, y).unwrap();
+			if let Some(r) = red.get_pixel(x, y) {
+				image_pixel.set_red(r.l().cast_linear_channel());
+			}
+			if let Some(g) = green.get_pixel(x, y) {
+				image_pixel.set_green(g.l().cast_linear_channel());
+			}
+			if let Some(b) = blue.get_pixel(x, y) {
+				image_pixel.set_blue(b.l().cast_linear_channel());
+			}
+			if let Some(a) = alpha.get_pixel(x, y) {
+				image_pixel.set_alpha(a.l().cast_linear_channel());
+			}
+		}
+	}
+
+	image
 }
 
-#[node_macro::old_node_fn(MaskImageNode<_P, _S>)]
+#[node_macro::node()]
 fn mask_image<
 	// _P is the color of the input image. It must have an alpha channel because that is going to
 	// be modified by the mask
@@ -175,8 +215,9 @@ fn mask_image<
 	// Stencil
 	Stencil: Transform + Sample<Pixel = _S>,
 >(
-	mut image: Input,
-	stencil: Stencil,
+	_: impl Ctx,
+	#[implementations(ImageFrameTable<Color>)] mut image: Input,
+	#[implementations(ImageFrameTable<Color>)] stencil: Stencil,
 ) -> Input {
 	let image_size = DVec2::new(image.width() as f64, image.height() as f64);
 	let mask_size = stencil.transform().decompose_scale();
@@ -207,17 +248,17 @@ fn mask_image<
 	image
 }
 
-#[derive(Debug, Clone, Copy)]
-pub struct BlendImageTupleNode<P, Fg, MapFn> {
-	map_fn: MapFn,
-	_p: PhantomData<P>,
-	_fg: PhantomData<Fg>,
-}
+// #[derive(Debug, Clone, Copy)]
+// pub struct BlendImageTupleNode<P, Fg, MapFn> {
+// 	map_fn: MapFn,
+// 	_p: PhantomData<P>,
+// 	_fg: PhantomData<Fg>,
+// }
 
-#[node_macro::old_node_fn(BlendImageTupleNode<_P, _Fg>)]
-fn blend_image_tuple<_P: Alpha + Pixel + Debug, MapFn, _Fg: Sample<Pixel = _P> + Transform>(images: (ImageFrame<_P>, _Fg), map_fn: &'input MapFn) -> ImageFrame<_P>
+#[node_macro::node(skip_impl)]
+async fn blend_image_tuple<_P: Alpha + Pixel + Debug + Send, MapFn, _Fg: Sample<Pixel = _P> + Transform + Clone + Send + 'n>(images: (ImageFrame<_P>, _Fg), map_fn: &'n MapFn) -> ImageFrame<_P>
 where
-	MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'input + Clone,
+	MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'n + Clone,
 {
 	let (background, foreground) = images;
 
@@ -319,7 +360,7 @@ fn extend_image_to_bounds(image: ImageFrame<Color>, bounds: DAffine2) -> ImageFr
 }
 
 #[node_macro::node(category("Debug: Raster"))]
-fn empty_image<P: Pixel>(_: (), transform: DAffine2, #[implementations(Color)] color: P) -> ImageFrame<P> {
+fn empty_image<P: Pixel>(_: impl Ctx, transform: DAffine2, #[implementations(Color)] color: P) -> ImageFrame<P> {
 	let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
 	let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
 
@@ -431,10 +472,10 @@ fn empty_image<P: Pixel>(_: (), transform: DAffine2, #[implementations(Color)] c
 // 	tiling: Tiling: bool,
 // }
 
-#[node_macro::node(category("Raster: Generator"))]
+#[node_macro::node(category("Raster"))]
 #[allow(clippy::too_many_arguments)]
 fn noise_pattern(
-	footprint: Footprint,
+	ctx: impl ExtractFootprint + Ctx,
 	_primary: (),
 	clip: bool,
 	seed: u32,
@@ -452,6 +493,7 @@ fn noise_pattern(
 	cellular_return_type: CellularReturnType,
 	cellular_jitter: f64,
 ) -> ImageFrameTable<Color> {
+	let footprint = ctx.footprint();
 	let viewport_bounds = footprint.viewport_bounds_in_local_space();
 
 	let mut size = viewport_bounds.size();
@@ -585,8 +627,9 @@ fn noise_pattern(
 	ImageFrameTable::new(result)
 }
 
-#[node_macro::node(category("Raster: Generator"))]
-fn mandelbrot(footprint: Footprint) -> ImageFrameTable<Color> {
+#[node_macro::node(category("Raster"))]
+fn mandelbrot(ctx: impl ExtractFootprint + Send) -> ImageFrameTable<Color> {
+	let footprint = ctx.footprint();
 	let viewport_bounds = footprint.viewport_bounds_in_local_space();
 
 	let image_bounds = Bbox::from_transform(DAffine2::IDENTITY).to_axis_aligned_bbox();
diff --git a/node-graph/gstd/src/text.rs b/node-graph/gstd/src/text.rs
index 11a7b49bbb..3139307cca 100644
--- a/node-graph/gstd/src/text.rs
+++ b/node-graph/gstd/src/text.rs
@@ -3,10 +3,11 @@ use crate::vector::{VectorData, VectorDataTable};
 use graph_craft::wasm_application_io::WasmEditorApi;
 use graphene_core::text::TypesettingConfig;
 pub use graphene_core::text::{bounding_box, load_face, to_path, Font, FontCache};
+use graphene_core::Ctx;
 
 #[node_macro::node(category(""))]
 fn text<'i: 'n>(
-	_: (),
+	_: impl Ctx,
 	editor: &'i WasmEditorApi,
 	text: String,
 	font_name: Font,
diff --git a/node-graph/gstd/src/vector.rs b/node-graph/gstd/src/vector.rs
index 852e498b6b..aab0fffa79 100644
--- a/node-graph/gstd/src/vector.rs
+++ b/node-graph/gstd/src/vector.rs
@@ -1,11 +1,9 @@
-use crate::transform::Footprint;
-
 use bezier_rs::{ManipulatorGroup, Subpath};
 use graphene_core::vector::misc::BooleanOperation;
 use graphene_core::vector::style::Fill;
 pub use graphene_core::vector::*;
 use graphene_core::{transform::Transform, GraphicGroup};
-use graphene_core::{Color, GraphicElement, GraphicGroupTable};
+use graphene_core::{Color, Ctx, GraphicElement, GraphicGroupTable};
 pub use path_bool as path_bool_lib;
 use path_bool::{FillRule, PathBooleanOperation};
 
@@ -13,22 +11,7 @@ use glam::{DAffine2, DVec2};
 use std::ops::Mul;
 
 #[node_macro::node(category(""))]
-async fn boolean_operation<F: 'n + Send>(
-	#[implementations(
-		(),
-		Footprint,
-	)]
-	footprint: F,
-	#[implementations(
-		() -> GraphicGroupTable,
-		Footprint -> GraphicGroupTable,
-	)]
-	group_of_paths: impl Node<F, Output = GraphicGroupTable>,
-	operation: BooleanOperation,
-) -> VectorDataTable {
-	let group_of_paths = group_of_paths.eval(footprint).await;
-	let group_of_paths = group_of_paths.one_item();
-
+async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, operation: BooleanOperation) -> VectorDataTable {
 	fn vector_from_image<T: Transform>(image_frame: T) -> VectorData {
 		let corner1 = DVec2::ZERO;
 		let corner2 = DVec2::new(1., 1.);
@@ -193,6 +176,7 @@ async fn boolean_operation<F: 'n + Send>(
 		}
 	}
 
+	let group_of_paths = group_of_paths.one_item();
 	// The first index is the bottom of the stack
 	let mut boolean_operation_result = boolean_operation_on_vector_data(&collect_vector_data(group_of_paths), operation);
 
diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs
index f5e31a713b..c3231ea4a0 100644
--- a/node-graph/gstd/src/wasm_application_io.rs
+++ b/node-graph/gstd/src/wasm_application_io.rs
@@ -12,8 +12,7 @@ use graphene_core::renderer::RenderMetadata;
 use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, RenderSvgSegmentList, SvgRender};
 use graphene_core::transform::Footprint;
 use graphene_core::vector::VectorDataTable;
-use graphene_core::GraphicGroupTable;
-use graphene_core::{Color, WasmNotSend};
+use graphene_core::{Color, Context, Ctx, ExtractFootprint, GraphicGroupTable, OwnedContextImpl, WasmNotSend};
 
 #[cfg(target_arch = "wasm32")]
 use base64::Engine;
@@ -27,7 +26,7 @@ use wasm_bindgen::JsCast;
 use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
 
 #[node_macro::node(category("Debug: GPU"))]
-async fn create_surface<'a: 'n>(_: (), editor: &'a WasmEditorApi) -> Arc<WasmSurfaceHandle> {
+async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc<WasmSurfaceHandle> {
 	Arc::new(editor.application_io.as_ref().unwrap().create_window())
 }
 
@@ -37,7 +36,7 @@ async fn create_surface<'a: 'n>(_: (), editor: &'a WasmEditorApi) -> Arc<WasmSur
 // #[node_macro::node(category("Debug: GPU"))]
 // #[cfg(target_arch = "wasm32")]
 // async fn draw_image_frame(
-// 	_: (),
+// 	_: impl Ctx,
 // 	image: ImageFrameTable<graphene_core::raster::SRGBA8>,
 // 	surface_handle: Arc<WasmSurfaceHandle>,
 // ) -> graphene_core::application_io::SurfaceHandleFrame<HtmlCanvasElement> {
@@ -60,7 +59,7 @@ async fn create_surface<'a: 'n>(_: (), editor: &'a WasmEditorApi) -> Arc<WasmSur
 // }
 
 #[node_macro::node(category("Network"))]
-async fn load_resource<'a: 'n>(_: (), _primary: (), #[scope("editor-api")] editor: &'a WasmEditorApi, url: String) -> Arc<[u8]> {
+async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor: &'a WasmEditorApi, #[name("URL")] url: String) -> Arc<[u8]> {
 	let Some(api) = editor.application_io.as_ref() else {
 		return Arc::from(include_bytes!("../../graph-craft/src/null.png").to_vec());
 	};
@@ -74,8 +73,8 @@ async fn load_resource<'a: 'n>(_: (), _primary: (), #[scope("editor-api")] edito
 	data
 }
 
-#[node_macro::node(category("Raster"))]
-fn decode_image(_: (), data: Arc<[u8]>) -> ImageFrameTable<Color> {
+#[node_macro::node(category("Network"))]
+fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> ImageFrameTable<Color> {
 	let Some(image) = image::load_from_memory(data.as_ref()).ok() else {
 		return ImageFrameTable::default();
 	};
@@ -152,13 +151,13 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen
 #[node_macro::node(category(""))]
 #[cfg(target_arch = "wasm32")]
 async fn rasterize<T: GraphicElementRendered + graphene_core::transform::TransformMut + WasmNotSend + 'n>(
-	_: (),
+	_: impl Ctx,
 	#[implementations(
-		Footprint -> VectorDataTable,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GraphicGroupTable,
+		VectorDataTable,
+		ImageFrameTable<Color>,
+		GraphicGroupTable,
 	)]
-	data: impl Node<Footprint, Output = T>,
+	mut data: T,
 	footprint: Footprint,
 	surface_handle: Arc<SurfaceHandle<HtmlCanvasElement>>,
 ) -> ImageFrameTable<Color> {
@@ -167,7 +166,6 @@ async fn rasterize<T: GraphicElementRendered + graphene_core::transform::Transfo
 		return ImageFrameTable::default();
 	}
 
-	let mut data = data.eval(footprint).await;
 	let mut render = SvgRender::new();
 	let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox();
 	let size = aabb.size();
@@ -214,31 +212,34 @@ async fn rasterize<T: GraphicElementRendered + graphene_core::transform::Transfo
 #[node_macro::node(category(""))]
 async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
 	render_config: RenderConfig,
-	editor_api: &'a WasmEditorApi,
+	editor_api: impl Node<Context<'static>, Output = &'a WasmEditorApi>,
 	#[implementations(
-		Footprint -> VectorDataTable,
-		Footprint -> ImageFrameTable<Color>,
-		Footprint -> GraphicGroupTable,
-		Footprint -> graphene_core::Artboard,
-		Footprint -> graphene_core::ArtboardGroup,
-		Footprint -> Option<Color>,
-		Footprint -> Vec<Color>,
-		Footprint -> bool,
-		Footprint -> f32,
-		Footprint -> f64,
-		Footprint -> String,
+		Context -> VectorDataTable,
+		Context -> ImageFrameTable<Color>,
+		Context -> GraphicGroupTable,
+		Context -> graphene_core::Artboard,
+		Context -> graphene_core::ArtboardGroup,
+		Context -> Option<Color>,
+		Context -> Vec<Color>,
+		Context -> bool,
+		Context -> f32,
+		Context -> f64,
+		Context -> String,
 	)]
-	data: impl Node<Footprint, Output = T>,
-	_surface_handle: impl Node<(), Output = Option<wgpu_executor::WgpuSurface>>,
+	data: impl Node<Context<'static>, Output = T>,
+	_surface_handle: impl Node<Context<'static>, Output = Option<wgpu_executor::WgpuSurface>>,
 ) -> RenderOutput {
 	let footprint = render_config.viewport;
+	let ctx = OwnedContextImpl::default().with_footprint(footprint).into_context();
+	ctx.footprint();
 
 	let RenderConfig { hide_artboards, for_export, .. } = render_config;
 	let render_params = RenderParams::new(render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
 
-	let data = data.eval(footprint).await;
+	let data = data.eval(ctx.clone()).await;
+	let editor_api = editor_api.eval(ctx.clone()).await;
 	#[cfg(all(feature = "vello", target_arch = "wasm32"))]
-	let surface_handle = _surface_handle.eval(()).await;
+	let surface_handle = _surface_handle.eval(ctx.clone()).await;
 	let use_vello = editor_api.editor_preferences.use_vello();
 	#[cfg(all(feature = "vello", target_arch = "wasm32"))]
 	let use_vello = use_vello && surface_handle.is_some();
diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs
index c37c0eb2c1..85ab9d23b0 100644
--- a/node-graph/interpreted-executor/src/node_registry.rs
+++ b/node-graph/interpreted-executor/src/node_registry.rs
@@ -2,74 +2,29 @@ use dyn_any::StaticType;
 use graph_craft::document::value::RenderOutput;
 use graph_craft::proto::{NodeConstructor, TypeErasedBox};
 use graphene_core::fn_type;
-use graphene_core::ops::IdentityNode;
 use graphene_core::raster::color::Color;
-use graphene_core::raster::image::{ImageFrame, ImageFrameTable};
+use graphene_core::raster::image::ImageFrameTable;
 use graphene_core::raster::*;
-use graphene_core::structural::Then;
-use graphene_core::transform::Footprint;
 use graphene_core::value::{ClonedNode, ValueNode};
 use graphene_core::vector::VectorDataTable;
 use graphene_core::{concrete, generic, Artboard, GraphicGroupTable};
+use graphene_core::{fn_type_fut, future};
 use graphene_core::{Cow, ProtoNodeIdentifier, Type};
 use graphene_core::{Node, NodeIO, NodeIOTypes};
 use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode};
 use graphene_std::application_io::TextureFrame;
-use graphene_std::raster::*;
 use graphene_std::wasm_application_io::*;
+use graphene_std::Context;
 use graphene_std::{GraphicElement, GraphicGroup};
 #[cfg(feature = "gpu")]
 use wgpu_executor::{ShaderInputFrame, WgpuExecutor};
 use wgpu_executor::{WgpuSurface, WindowHandle};
 
-use glam::{DAffine2, DVec2, UVec2};
+use glam::{DVec2, UVec2};
 use once_cell::sync::Lazy;
 use std::collections::HashMap;
 use std::sync::Arc;
 
-macro_rules! construct_node {
-	($args: ident, $path:ty, [$($arg:ty => $type:ty),*]) => { async move {
-		let mut args = $args.clone();
-		args.reverse();
-		let node = <$path>::new($(
-				{
-					let node = graphene_std::any::downcast_node::<$arg, $type>(args.pop().expect("Not enough arguments provided to construct node"));
-					let value = node.eval(()).await;
-					graphene_core::value::ClonedNode::new(value)
-				}
-			),*
-		);
-		node
-	}}
-}
-
-macro_rules! register_node {
-	($path:ty, input: $input:ty, params: [ $($type:ty),*]) => {
-		register_node!($path, input: $input, fn_params: [ $(() => $type),*])
-	};
-	($path:ty, input: $input:ty, fn_params: [ $($arg:ty => $type:ty),*]) => {
-		(
-			ProtoNodeIdentifier::new(stringify!($path)),
-			|args| {
-				Box::pin(async move {
-				let node = construct_node!(args, $path, [$($arg => $type),*]).await;
-				let node = graphene_std::any::FutureWrapperNode::new(node);
-				let any: DynAnyNode<$input, _, _> = graphene_std::any::DynAnyNode::new(node);
-				Box::new(any) as TypeErasedBox
-				})
-			},
-			{
-				let node = <$path>::new($(
-					graphene_std::any::PanicNode::<(), $type>::new()
-				),*);
-				let params = vec![$(fn_type!((), $type)),*];
-				let mut node_io = <$path as NodeIO<'_, $input>>::to_node_io(&node, params);
-				node_io.call_argument = concrete!(<$input as StaticType>::Static);
-				node_io
-			},
-		)
-	};
-}
 macro_rules! async_node {
 	// TODO: we currently need to annotate the type here because the compiler would otherwise (correctly)
 	// TODO: assign a Pin<Box<dyn Future<Output=T>>> type to the node, which is not what we want for now.
@@ -95,7 +50,7 @@ macro_rules! async_node {
 				),*);
 				// TODO: Propagate the future type through the node graph
 				// let params = vec![$(Type::Fn(Box::new(concrete!(())), Box::new(Type::Future(Box::new(concrete!($type)))))),*];
-				let params = vec![$(fn_type!($arg, $type)),*];
+				let params = vec![$(fn_type_fut!($arg, $type)),*];
 				let mut node_io = NodeIO::<'_, $input>::to_async_node_io(&node, params);
 				node_io.call_argument = concrete!(<$input as StaticType>::Static);
 				node_io
@@ -107,11 +62,11 @@ macro_rules! async_node {
 // TODO: turn into hashmap
 fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> {
 	let node_types: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
-		(
-			ProtoNodeIdentifier::new("graphene_core::ops::IdentityNode"),
-			|_| Box::pin(async move { FutureWrapperNode::new(IdentityNode::new()).into_type_erased() }),
-			NodeIOTypes::new(generic!(I), generic!(I), vec![]),
-		),
+		// (
+		// 	ProtoNodeIdentifier::new("graphene_core::ops::IdentityNode"),
+		// 	|_| Box::pin(async move { FutureWrapperNode::new(IdentityNode::new()).into_type_erased() }),
+		// 	NodeIOTypes::new(generic!(I), generic!(I), vec![]),
+		// ),
 		// async_node!(graphene_core::ops::IntoNode<ImageFrameTable<SRGBA8>>, input: ImageFrameTable<Color>, params: []),
 		// async_node!(graphene_core::ops::IntoNode<ImageFrameTable<Color>>, input: ImageFrameTable<SRGBA8>, params: []),
 		async_node!(graphene_core::ops::IntoNode<GraphicGroupTable>, input: ImageFrameTable<Color>, params: []),
@@ -123,78 +78,31 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
 		async_node!(graphene_core::ops::IntoNode<GraphicElement>, input: GraphicGroupTable, params: []),
 		async_node!(graphene_core::ops::IntoNode<GraphicGroupTable>, input: VectorDataTable, params: []),
 		async_node!(graphene_core::ops::IntoNode<GraphicGroupTable>, input: ImageFrameTable<Color>, params: []),
-		register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrameTable<Color>, params: [ImageFrameTable<Color>]),
-		register_node!(graphene_std::raster::InsertChannelNode<_, _, _, _>, input: ImageFrameTable<Color>, params: [ImageFrameTable<Color>, RedGreenBlue]),
-		// register_node!(graphene_std::raster::MaskImageNode<_, _, _>, input: ImageFrameTable<Color>, params: [ImageFrameTable<Luma>]),
-		// register_node!(graphene_std::raster::InsertChannelNode<_, _, _, _>, input: ImageFrameTable<Color>, params: [ImageFrameTable<Luma>, RedGreenBlue]),
+		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ImageFrameTable<Color>]),
+		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => TextureFrame]),
+		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => VectorDataTable]),
+		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => GraphicGroupTable]),
+		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => GraphicElement]),
+		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Artboard]),
+		#[cfg(feature = "gpu")]
 		(
-			ProtoNodeIdentifier::new("graphene_std::raster::CombineChannelsNode"),
+			ProtoNodeIdentifier::new(stringify!(wgpu_executor::CreateGpuSurfaceNode<_>)),
 			|args| {
 				Box::pin(async move {
-					use graphene_core::raster::*;
-					use graphene_core::value::*;
-
-					let channel_r: ImageFrameTable<Color> = DowncastBothNode::new(args[0].clone()).eval(()).await;
-					let channel_g: ImageFrameTable<Color> = DowncastBothNode::new(args[1].clone()).eval(()).await;
-					let channel_b: ImageFrameTable<Color> = DowncastBothNode::new(args[2].clone()).eval(()).await;
-					let channel_a: ImageFrameTable<Color> = DowncastBothNode::new(args[3].clone()).eval(()).await;
-
-					let insert_r = InsertChannelNode::new(ClonedNode::new(channel_r.clone()), CopiedNode::new(RedGreenBlue::Red));
-					let insert_g = InsertChannelNode::new(ClonedNode::new(channel_g.clone()), CopiedNode::new(RedGreenBlue::Green));
-					let insert_b = InsertChannelNode::new(ClonedNode::new(channel_b.clone()), CopiedNode::new(RedGreenBlue::Blue));
-					let complete_node = insert_r.then(insert_g).then(insert_b);
-					let complete_node = complete_node.then(MaskImageNode::new(ClonedNode::new(channel_a.clone())));
-
-					let channel_r = channel_r.one_item();
-					let channel_g = channel_g.one_item();
-					let channel_b = channel_b.one_item();
-					let channel_a = channel_a.one_item();
-
-					// TODO: Move to FN Node for better performance
-					let (mut transform, mut bounds) = (DAffine2::ZERO, glam::UVec2::ZERO);
-					for image in [channel_a, channel_r, channel_g, channel_b] {
-						if image.image.width() > bounds.x {
-							bounds = glam::UVec2::new(image.image.width(), image.image.height());
-							transform = image.transform;
-						}
-					}
-					let empty_image = ImageFrame {
-						image: Image::new(bounds.x, bounds.y, Color::BLACK),
-						transform,
-						alpha_blending: Default::default(),
-					};
-					let empty_image = ImageFrameTable::new(empty_image);
-					let final_image = ClonedNode::new(empty_image).then(complete_node);
-					let final_image = FutureWrapperNode::new(final_image);
-
-					let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(final_image);
-					any.into_type_erased()
+					let editor_api: DowncastBothNode<(), &WasmEditorApi> = DowncastBothNode::new(args[0].clone());
+					let node = <wgpu_executor::CreateGpuSurfaceNode<_>>::new(editor_api);
+					let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(node);
+					Box::new(any) as TypeErasedBox
 				})
 			},
-			NodeIOTypes::new(
-				concrete!(()),
-				concrete!(ImageFrameTable<Color>),
-				vec![
-					fn_type!(ImageFrameTable<Color>),
-					fn_type!(ImageFrameTable<Color>),
-					fn_type!(ImageFrameTable<Color>),
-					fn_type!(ImageFrameTable<Color>),
-				],
-			),
+			{
+				let node = <wgpu_executor::CreateGpuSurfaceNode<_>>::new(graphene_std::any::PanicNode::<Context, dyn_any::DynFuture<'static, &WasmEditorApi>>::new());
+				let params = vec![fn_type_fut!(Context, &WasmEditorApi)];
+				let mut node_io = <wgpu_executor::CreateGpuSurfaceNode<_> as NodeIO<'_, Context>>::to_async_node_io(&node, params);
+				node_io.call_argument = concrete!(<Context as StaticType>::Static);
+				node_io
+			},
 		),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, fn_params: [Footprint => ImageFrameTable<Color>]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: (), params: [ImageFrameTable<Color>]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, fn_params: [Footprint => TextureFrame]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: (), params: [TextureFrame]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, fn_params: [Footprint => VectorDataTable]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: (), fn_params: [() => VectorDataTable]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, fn_params: [Footprint => GraphicGroupTable]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: (), fn_params: [() => GraphicGroupTable]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, fn_params: [Footprint => GraphicElement]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: (), fn_params: [() => GraphicElement]),
-		async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, fn_params: [Footprint => Artboard]),
-		#[cfg(feature = "gpu")]
-		register_node!(wgpu_executor::CreateGpuSurfaceNode<_>, input: (), params: [&WasmEditorApi]),
 		#[cfg(feature = "gpu")]
 		(
 			ProtoNodeIdentifier::new("graphene_std::executor::MapGpuSingleImageNode"),
@@ -202,7 +110,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
 				Box::pin(async move {
 					let document_node: DowncastBothNode<(), graph_craft::document::DocumentNode> = DowncastBothNode::new(args[0].clone());
 					let editor_api: DowncastBothNode<(), &WasmEditorApi> = DowncastBothNode::new(args[1].clone());
-					// let document_node = ClonedNode::new(document_node.eval(()));
 					let node = graphene_std::gpu_nodes::MapGpuNode::new(document_node, editor_api);
 					let any: DynAnyNode<ImageFrameTable<Color>, _, _> = graphene_std::any::DynAnyNode::new(node);
 					any.into_type_erased()
@@ -279,27 +186,27 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
 		// 	NodeIOTypes::new(concrete!(ImageFrameTable<Luma>), concrete!(ImageFrameTable<Luma>), vec![fn_type!(graphene_core::raster::curve::Curve)]),
 		// ),
 		// TODO: Use channel split and merge for this instead of using LuminanceMut for the whole color.
-		(
-			ProtoNodeIdentifier::new("graphene_core::raster::CurvesNode"),
-			|args| {
-				use graphene_core::raster::{curve::Curve, GenerateCurvesNode};
-				let curve: DowncastBothNode<(), Curve> = DowncastBothNode::new(args[0].clone());
-				Box::pin(async move {
-					let curve = ClonedNode::new(curve.eval(()).await);
+		// (
+		// 	ProtoNodeIdentifier::new("graphene_core::raster::CurvesNode"),
+		// 	|args| {
+		// 		use graphene_core::raster::{curve::Curve, GenerateCurvesNode};
+		// 		let curve: DowncastBothNode<(), Curve> = DowncastBothNode::new(args[0].clone());
+		// 		Box::pin(async move {
+		// 			let curve = ValueNode::new(ClonedNode::new(curve.eval(()).await));
 
-					let generate_curves_node = GenerateCurvesNode::new(curve, ClonedNode::new(0_f32));
-					let map_image_frame_node = graphene_std::raster::MapImageNode::new(ValueNode::new(generate_curves_node.eval(())));
-					let map_image_frame_node = FutureWrapperNode::new(map_image_frame_node);
-					let any: DynAnyNode<ImageFrameTable<Color>, _, _> = graphene_std::any::DynAnyNode::new(map_image_frame_node);
-					any.into_type_erased()
-				})
-			},
-			NodeIOTypes::new(
-				concrete!(ImageFrameTable<Color>),
-				concrete!(ImageFrameTable<Color>),
-				vec![fn_type!(graphene_core::raster::curve::Curve)],
-			),
-		),
+		// 			let generate_curves_node = GenerateCurvesNode::new(FutureWrapperNode::new(curve), FutureWrapperNode::new(ClonedNode::new(0_f32)));
+		// 			let map_image_frame_node = graphene_std::raster::MapImageNode::new(FutureWrapperNode::new(ValueNode::new(generate_curves_node.eval(()))));
+		// 			let map_image_frame_node = FutureWrapperNode::new(map_image_frame_node);
+		// 			let any: DynAnyNode<ImageFrameTable<Color>, _, _> = graphene_std::any::DynAnyNode::new(map_image_frame_node);
+		// 			any.into_type_erased()
+		// 		})
+		// 	},
+		// 	NodeIOTypes::new(
+		// 		concrete!(ImageFrameTable<Color>),
+		// 		concrete!(ImageFrameTable<Color>),
+		// 		vec![fn_type!(graphene_core::raster::curve::Curve)],
+		// 	),
+		// ),
 		// (
 		// 	ProtoNodeIdentifier::new("graphene_std::raster::ImaginateNode"),
 		// 	|args: Vec<graph_craft::proto::SharedNodeContainer>| {
@@ -337,44 +244,31 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
 		// 		],
 		// 	),
 		// ),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [Image<Color>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [VectorDataTable]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [ImageFrameTable<Color>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [Vec<DVec2>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [Arc<WasmSurfaceHandle>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [WindowHandle]),
-		#[cfg(feature = "gpu")]
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [ShaderInputFrame]),
-		#[cfg(feature = "gpu")]
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [wgpu_executor::WgpuSurface]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [Option<wgpu_executor::WgpuSurface>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [wgpu_executor::WindowHandle]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [graphene_std::SurfaceFrame]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: (), params: [RenderOutput]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => Image<Color>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => VectorDataTable]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => ImageFrameTable<Color>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => Vec<DVec2>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => Arc<WasmSurfaceHandle>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => WindowHandle]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => VectorDataTable]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => ImageFrameTable<Color>]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => GraphicGroupTable]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<DVec2>]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WindowHandle]),
 		#[cfg(feature = "gpu")]
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => ShaderInputFrame]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => ShaderInputFrame]),
 		#[cfg(feature = "gpu")]
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => wgpu_executor::WgpuSurface]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => Option<wgpu_executor::WgpuSurface>]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => wgpu_executor::WindowHandle]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => graphene_std::SurfaceFrame]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuSurface]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<wgpu_executor::WgpuSurface>]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WindowHandle]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::SurfaceFrame]),
 		async_node!(graphene_core::memo::MemoNode<_, _>, input: UVec2, fn_params: [UVec2 => graphene_std::SurfaceFrame]),
-		async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, fn_params: [Footprint => RenderOutput]),
-		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => GraphicElement]),
-		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => GraphicGroup]),
-		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => VectorDataTable]),
+		async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderOutput]),
+		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => GraphicElement]),
+		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => GraphicGroup]),
+		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => VectorDataTable]),
+		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => GraphicGroupTable]),
 		#[cfg(feature = "gpu")]
-		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => ShaderInputFrame]),
-		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => WgpuSurface]),
-		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => Option<WgpuSurface>]),
-		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => TextureFrame]),
-		register_node!(graphene_core::structural::ConsNode<_, _>, input: Image<Color>, params: [&str]),
+		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => ShaderInputFrame]),
+		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => WgpuSurface]),
+		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => Option<WgpuSurface>]),
+		async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Context, fn_params: [Context => TextureFrame]),
 	];
 	let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
 	for (id, entry) in graphene_core::registry::NODE_REGISTRY.lock().unwrap().iter() {
diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs
index dbf080faa0..4e88b41a5e 100644
--- a/node-graph/interpreted-executor/src/util.rs
+++ b/node-graph/interpreted-executor/src/util.rs
@@ -1,13 +1,12 @@
 use std::sync::Arc;
 
-use graph_craft::{
-	concrete,
-	document::{value::TaggedValue, DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork},
-	generic,
-	wasm_application_io::WasmEditorApi,
-	ProtoNodeIdentifier,
-};
-use graphene_std::{transform::Footprint, uuid::NodeId};
+use graph_craft::concrete;
+use graph_craft::document::{value::TaggedValue, DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
+use graph_craft::generic;
+use graph_craft::wasm_application_io::WasmEditorApi;
+use graph_craft::ProtoNodeIdentifier;
+use graphene_std::uuid::NodeId;
+use graphene_std::Context;
 
 // TODO: this is copy pasta from the editor (and does get out of sync)
 pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEditorApi>) -> NodeNetwork {
@@ -27,18 +26,18 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
 
 	let render_node = graph_craft::document::DocumentNode {
 		inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(2), 0)],
-		implementation: graph_craft::document::DocumentNodeImplementation::Network(NodeNetwork {
+		implementation: DocumentNodeImplementation::Network(NodeNetwork {
 			exports: vec![NodeInput::node(NodeId(2), 0)],
 			nodes: [
 				DocumentNode {
 					inputs: vec![NodeInput::scope("editor-api")],
-					manual_composition: Some(concrete!(())),
+					manual_composition: Some(concrete!(Context)),
 					implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("wgpu_executor::CreateGpuSurfaceNode")),
 					skip_deduplication: true,
 					..Default::default()
 				},
 				DocumentNode {
-					manual_composition: Some(concrete!(())),
+					manual_composition: Some(concrete!(Context)),
 					inputs: vec![NodeInput::node(NodeId(0), 0)],
 					implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode")),
 					..Default::default()
@@ -48,7 +47,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
 					manual_composition: Some(concrete!(graphene_std::application_io::RenderConfig)),
 					inputs: vec![
 						NodeInput::scope("editor-api"),
-						NodeInput::network(graphene_core::Type::Fn(Box::new(concrete!(Footprint)), Box::new(generic!(T))), 0),
+						NodeInput::network(graphene_core::Type::Fn(Box::new(concrete!(Context)), Box::new(generic!(T))), 0),
 						NodeInput::node(NodeId(1), 0),
 					],
 					implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::wasm_application_io::RenderNode")),
diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs
index 4bcc9d2d13..7bc6148ce3 100644
--- a/node-graph/node-macro/src/codegen.rs
+++ b/node-graph/node-macro/src/codegen.rs
@@ -37,7 +37,6 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
 
 	let struct_generics: Vec<Ident> = fields.iter().enumerate().map(|(i, _)| format_ident!("Node{}", i)).collect();
 	let input_ident = &input.pat_ident;
-	let input_type = &input.ty;
 
 	let field_idents: Vec<_> = fields
 		.iter()
@@ -78,12 +77,14 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
 		}
 	};
 
+	let mut future_idents = Vec::new();
+
 	let field_types: Vec<_> = fields
 		.iter()
 		.map(|field| match field {
 			ParsedField::Regular { ty, .. } => ty.clone(),
 			ParsedField::Node { output_type, input_type, .. } => match parsed.is_async {
-				true => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output: core::future::Future<Output=#output_type> + #graphene_core::WasmNotSend>),
+				true => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output = impl core::future::Future<Output=#output_type>>),
 				false => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output = #output_type>),
 			},
 		})
@@ -164,7 +165,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
 	let eval_args = fields.iter().map(|field| match field {
 		ParsedField::Regular { pat_ident, .. } => {
 			let name = &pat_ident.ident;
-			quote! { let #name = self.#name.eval(()); }
+			quote! { let #name = self.#name.eval(__input.clone()).await; }
 		}
 		ParsedField::Node { pat_ident, .. } => {
 			let name = &pat_ident.ident;
@@ -181,16 +182,32 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
 	});
 	let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned());
 
+	let input_type = &parsed.input.ty;
 	let mut clauses = Vec::new();
 	for (field, name) in fields.iter().zip(struct_generics.iter()) {
 		clauses.push(match (field, *is_async) {
-			(ParsedField::Regular { ty, .. }, _) => quote!(#name: #graphene_core::Node<'n, (), Output = #ty> ),
-			(ParsedField::Node { input_type, output_type, .. }, false) => {
-				quote!(for<'all_input> #name: #graphene_core::Node<'all_input, #input_type, Output = #output_type> + #graphene_core::WasmNotSync)
+			(ParsedField::Regular { ty, .. }, _) => {
+				let all_lifetime_ty = substitute_lifetimes(ty.clone(), "all");
+				let id = future_idents.len();
+				let fut_ident = format_ident!("F{}", id);
+				future_idents.push(fut_ident.clone());
+				quote!(
+					#fut_ident: core::future::Future<Output = #ty> + #graphene_core::WasmNotSend + 'n,
+					for<'all> #all_lifetime_ty: #graphene_core::WasmNotSend,
+					#name: #graphene_core::Node<'n, #input_type, Output = #fut_ident> + #graphene_core::WasmNotSync
+				)
 			}
 			(ParsedField::Node { input_type, output_type, .. }, true) => {
-				quote!(for<'all_input> #name: #graphene_core::Node<'all_input, #input_type, Output: core::future::Future<Output = #output_type> + #graphene_core::WasmNotSend> + #graphene_core::WasmNotSync)
+				let id = future_idents.len();
+				let fut_ident = format_ident!("F{}", id);
+				future_idents.push(fut_ident.clone());
+
+				quote!(
+					#fut_ident: core::future::Future<Output = #output_type> + #graphene_core::WasmNotSend + 'n,
+					#name: #graphene_core::Node<'n, #input_type, Output = #fut_ident > + #graphene_core::WasmNotSync
+				)
 			}
+			(ParsedField::Node { .. }, false) => unreachable!(),
 		});
 	}
 	let where_clause = where_clause.clone().unwrap_or(WhereClause {
@@ -210,24 +227,16 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
 	});
 
 	let async_keyword = is_async.then(|| quote!(async));
+	let await_keyword = is_async.then(|| quote!(.await));
 
-	let eval_impl = if *is_async {
-		quote! {
-			type Output = #graphene_core::registry::DynFuture<'n, #output_type>;
-			#[inline]
-			fn eval(&'n self, __input: #input_type) -> Self::Output {
-				#(#eval_args)*
-				Box::pin(self::#fn_name(__input #(, #field_names)*))
-			}
-		}
-	} else {
-		quote! {
-			type Output = #output_type;
-			#[inline]
-			fn eval(&'n self, __input: #input_type) -> Self::Output {
+	let eval_impl = quote! {
+		type Output = #graphene_core::registry::DynFuture<'n, #output_type>;
+		#[inline]
+		fn eval(&'n self, __input: #input_type) -> Self::Output {
+			Box::pin(async move {
 				#(#eval_args)*
-				self::#fn_name(__input #(, #field_names)*)
-			}
+				self::#fn_name(__input #(, #field_names)*) #await_keyword
+			})
 		}
 	};
 	let path = match parsed.attributes.path {
@@ -245,10 +254,10 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
 		/// Underlying implementation for [#struct_name]
 		#[inline]
 		#[allow(clippy::too_many_arguments)]
-		#async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
+		pub(crate) #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
 
 		#[automatically_derived]
-		impl<'n, #(#fn_generics,)* #(#struct_generics,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*>
+		impl<'n, #(#fn_generics,)* #(#struct_generics,)* #(#future_idents,)*> #graphene_core::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_generics,)*>
 		#struct_where_clause
 		{
 			#eval_impl
@@ -260,7 +269,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
 		mod #mod_name {
 			use super::*;
 			use #graphene_core as gcore;
-			use gcore::{Node, NodeIOTypes, concrete, fn_type, future, ProtoNodeIdentifier, WasmNotSync, NodeIO};
+			use gcore::{Node, NodeIOTypes, concrete, fn_type, fn_type_fut, future, ProtoNodeIdentifier, WasmNotSync, NodeIO};
 			use gcore::value::ClonedNode;
 			use gcore::ops::TypeNode;
 			use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride};
@@ -323,7 +332,7 @@ fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], st
 	}
 
 	let mut constructors = Vec::new();
-	let unit = parse_quote!(());
+	let unit = parse_quote!(gcore::Context);
 	let parameter_types: Vec<_> = parsed
 		.fields
 		.iter()
@@ -331,9 +340,9 @@ fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], st
 			match field {
 				ParsedField::Regular { implementations, ty, .. } => {
 					if !implementations.is_empty() {
-						implementations.iter().map(|ty| (&unit, ty, false)).collect()
+						implementations.iter().map(|ty| (&unit, ty)).collect()
 					} else {
-						vec![(&unit, ty, false)]
+						vec![(&unit, ty)]
 					}
 				}
 				ParsedField::Node {
@@ -343,20 +352,19 @@ fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], st
 					..
 				} => {
 					if !implementations.is_empty() {
-						implementations.iter().map(|impl_| (&impl_.input, &impl_.output, true)).collect()
+						implementations.iter().map(|impl_| (&impl_.input, &impl_.output)).collect()
 					} else {
-						vec![(input_type, output_type, true)]
+						vec![(input_type, output_type)]
 					}
 				}
 			}
 			.into_iter()
-			.map(|(input, out, node)| (substitute_lifetimes(input.clone()), substitute_lifetimes(out.clone()), node))
+			.map(|(input, out)| (substitute_lifetimes(input.clone(), "_"), substitute_lifetimes(out.clone(), "_")))
 			.collect::<Vec<_>>()
 		})
 		.collect();
 
 	let max_implementations = parameter_types.iter().map(|x| x.len()).chain([parsed.input.implementations.len().max(1)]).max();
-	let future_node = (!parsed.is_async).then(|| quote!(let node = gcore::registry::FutureWrapperNode::new(node);));
 
 	for i in 0..max_implementations.unwrap_or(0) {
 		let mut temp_constructors = Vec::new();
@@ -365,38 +373,24 @@ fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], st
 
 		for (j, types) in parameter_types.iter().enumerate() {
 			let field_name = field_names[j];
-			let (input_type, output_type, impl_node) = &types[i.min(types.len() - 1)];
+			let (input_type, output_type) = &types[i.min(types.len() - 1)];
 
 			let node = matches!(parsed.fields[j], ParsedField::Node { .. });
 
 			let downcast_node = quote!(
-			let #field_name: DowncastBothNode<#input_type, #output_type> = DowncastBothNode::new(args[#j].clone());
-			 );
-			temp_constructors.push(if node {
-				if !parsed.is_async {
-					return Err(Error::new_spanned(&parsed.fn_name, "Node needs to be async if you want to use lambda parameters"));
-				}
-				downcast_node
-			} else {
-				quote!(
-						#downcast_node
-						let #field_name = #field_name.eval(()).await;
-						let #field_name = ClonedNode::new(#field_name);
-						let #field_name: TypeNode<_, #input_type, #output_type> = TypeNode::new(#field_name);
-						// try polling futures
-				)
-			});
-			temp_node_io.push(quote!(fn_type!(#input_type, #output_type, alias: #output_type)));
-			match parsed.is_async && *impl_node {
-				true => panic_node_types.push(quote!(#input_type, DynFuture<'static, #output_type>)),
-				false => panic_node_types.push(quote!(#input_type, #output_type)),
-			};
+				let #field_name: DowncastBothNode<#input_type, #output_type> = DowncastBothNode::new(args[#j].clone());
+			);
+			if node && !parsed.is_async {
+				return Err(Error::new_spanned(&parsed.fn_name, "Node needs to be async if you want to use lambda parameters"));
+			}
+			temp_constructors.push(downcast_node);
+			temp_node_io.push(quote!(fn_type_fut!(#input_type, #output_type, alias: #output_type)));
+			panic_node_types.push(quote!(#input_type, DynFuture<'static, #output_type>));
 		}
 		let input_type = match parsed.input.implementations.is_empty() {
 			true => parsed.input.ty.clone(),
 			false => parsed.input.implementations[i.min(parsed.input.implementations.len() - 1)].clone(),
 		};
-		let node_io = if parsed.is_async { quote!(to_async_node_io) } else { quote!(to_node_io) };
 		constructors.push(quote!(
 			(
 				|args| {
@@ -404,14 +398,13 @@ fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], st
 						#(#temp_constructors;)*
 						let node = #struct_name::new(#(#field_names,)*);
 						// try polling futures
-						#future_node
 						let any: DynAnyNode<#input_type, _, _> = DynAnyNode::new(node);
 						Box::new(any) as TypeErasedBox<'_>
 					})
 				}, {
 					let node = #struct_name::new(#(PanicNode::<#panic_node_types>::new(),)*);
 					let params = vec![#(#temp_node_io,)*];
-					let mut node_io = NodeIO::<'_, #input_type>::#node_io(&node, params);
+					let mut node_io = NodeIO::<'_, #input_type>::to_async_node_io(&node, params);
 					node_io
 
 				}
@@ -443,11 +436,11 @@ fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], st
 
 use syn::{visit_mut::VisitMut, GenericArgument, Lifetime, Type};
 
-struct LifetimeReplacer;
+struct LifetimeReplacer(&'static str);
 
 impl VisitMut for LifetimeReplacer {
 	fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) {
-		lifetime.ident = syn::Ident::new("_", lifetime.ident.span());
+		lifetime.ident = syn::Ident::new(self.0, lifetime.ident.span());
 	}
 
 	fn visit_type_mut(&mut self, ty: &mut Type) {
@@ -472,7 +465,7 @@ impl VisitMut for LifetimeReplacer {
 }
 
 #[must_use]
-fn substitute_lifetimes(mut ty: Type) -> Type {
-	LifetimeReplacer.visit_type_mut(&mut ty);
+fn substitute_lifetimes(mut ty: Type, lifetime: &'static str) -> Type {
+	LifetimeReplacer(lifetime).visit_type_mut(&mut ty);
 	ty
 }
diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs
index 96b010c27e..f5cbc75eab 100644
--- a/node-graph/node-macro/src/parsing.rs
+++ b/node-graph/node-macro/src/parsing.rs
@@ -4,8 +4,11 @@ use proc_macro2::TokenStream as TokenStream2;
 use quote::{format_ident, ToTokens};
 use syn::parse::{Parse, ParseStream, Parser};
 use syn::punctuated::Punctuated;
+use syn::spanned::Spanned;
 use syn::token::{Comma, RArrow};
-use syn::{AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, WhereClause};
+use syn::{
+	parse_quote, AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, WhereClause,
+};
 
 use crate::codegen::generate_node_code;
 
@@ -548,10 +551,12 @@ fn extract_attribute<'a>(attrs: &'a [Attribute], name: &str) -> Option<&'a Attri
 // Modify the new_node_fn function to use the code generation
 pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> TokenStream2 {
 	let parse_result = parse_node_fn(attr, item.clone());
-	let Ok(parsed_node) = parse_result else {
+	let Ok(mut parsed_node) = parse_result else {
 		let e = parse_result.unwrap_err();
 		return Error::new(e.span(), format!("Failed to parse node function: {e}")).to_compile_error();
 	};
+
+	parsed_node.replace_impl_trait_in_input();
 	if let Err(e) = crate::validation::validate_node_fn(&parsed_node) {
 		return Error::new(e.span(), format!("Validation Error:\n{e}")).to_compile_error();
 	}
@@ -564,6 +569,31 @@ pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> TokenStream2 {
 	}
 }
 
+impl ParsedNodeFn {
+	fn replace_impl_trait_in_input(&mut self) {
+		if let Type::ImplTrait(impl_trait) = self.input.ty.clone() {
+			let ident = Ident::new("_Input", impl_trait.span());
+			let mut bounds = impl_trait.bounds;
+			bounds.push(parse_quote!('n));
+			self.fn_generics.push(GenericParam::Type(TypeParam {
+				attrs: Default::default(),
+				ident: ident.clone(),
+				colon_token: Some(Default::default()),
+				bounds,
+				eq_token: None,
+				default: None,
+			}));
+			self.input.ty = parse_quote!(#ident);
+			if self.input.implementations.is_empty() {
+				self.input.implementations.push(parse_quote!(gcore::Context));
+			}
+		}
+		if self.input.pat_ident.ident == "_" {
+			self.input.pat_ident.ident = Ident::new("__ctx", self.input.pat_ident.ident.span());
+		}
+	}
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
@@ -774,7 +804,7 @@ mod tests {
 		let attr = quote!(category("Vector: Shape"));
 		let input = quote!(
 			/// Test
-			fn circle(_: (), #[default(50.)] radius: f64) -> VectorData {
+			fn circle(_: impl Ctx, #[default(50.)] radius: f64) -> VectorData {
 				// Implementation details...
 			}
 		);
@@ -795,7 +825,7 @@ mod tests {
 			where_clause: None,
 			input: Input {
 				pat_ident: pat_ident("_"),
-				ty: parse_quote!(()),
+				ty: parse_quote!(impl Ctx),
 				implementations: Punctuated::new(),
 			},
 			output_type: parse_quote!(VectorData),
diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs
index a11d35cff9..be111736d3 100644
--- a/node-graph/wgpu-executor/src/lib.rs
+++ b/node-graph/wgpu-executor/src/lib.rs
@@ -6,9 +6,11 @@ pub use executor::GpuExecutor;
 
 use dyn_any::{DynAny, StaticType};
 use gpu_executor::{ComputePassDimensions, GPUConstant, StorageBufferOptions, TextureBufferOptions, TextureBufferType, ToStorageBuffer, ToUniformBuffer};
-use graphene_core::application_io::{ApplicationIo, EditorApi, SurfaceHandle};
+use graphene_core::application_io::{ApplicationIo, EditorApi, SurfaceHandle, TextureFrame};
+use graphene_core::raster::image::ImageFrameTable;
+use graphene_core::raster::{Image, SRGBA8};
 use graphene_core::transform::{Footprint, Transform};
-use graphene_core::{Cow, Node, SurfaceFrame, Type};
+use graphene_core::{Color, Cow, Ctx, ExtractFootprint, Node, SurfaceFrame, Type};
 
 use anyhow::{bail, Result};
 use futures::Future;
@@ -822,12 +824,12 @@ impl<T> ShaderInputNode<T> {
 }
 
 #[node_macro::node(category(""))]
-async fn uniform<'a: 'n, T: ToUniformBuffer + Send + 'n>(_: (), #[implementations(f32, DAffine2)] data: T, executor: &'a WgpuExecutor) -> WgpuShaderInput {
+async fn uniform<'a: 'n, T: ToUniformBuffer + Send + 'n>(_: impl Ctx, #[implementations(f32, DAffine2)] data: T, executor: &'a WgpuExecutor) -> WgpuShaderInput {
 	executor.create_uniform_buffer(data).unwrap()
 }
 
 #[node_macro::node(category(""))]
-async fn storage<'a: 'n, T: ToStorageBuffer + Send + 'n>(_: (), #[implementations(Vec<u8>)] data: T, executor: &'a WgpuExecutor) -> WgpuShaderInput {
+async fn storage<'a: 'n, T: ToStorageBuffer + Send + 'n>(_: impl Ctx, #[implementations(Vec<u8>)] data: T, executor: &'a WgpuExecutor) -> WgpuShaderInput {
 	executor
 		.create_storage_buffer(
 			data,
@@ -842,18 +844,18 @@ async fn storage<'a: 'n, T: ToStorageBuffer + Send + 'n>(_: (), #[implementation
 }
 
 #[node_macro::node(category(""))]
-async fn create_output_buffer<'a: 'n>(_: (), size: usize, executor: &'a WgpuExecutor, ty: Type) -> Arc<WgpuShaderInput> {
+async fn create_output_buffer<'a: 'n>(_: impl Ctx + 'a, size: usize, executor: &'a WgpuExecutor, ty: Type) -> Arc<WgpuShaderInput> {
 	Arc::new(executor.create_output_buffer(size, ty, true).unwrap())
 }
 
 #[node_macro::node(skip_impl)]
-async fn create_compute_pass<'a: 'n>(_: (), layout: PipelineLayout, executor: &'a WgpuExecutor, output: WgpuShaderInput, instances: ComputePassDimensions) -> CommandBuffer {
+async fn create_compute_pass<'a: 'n>(_: impl Ctx + 'a, layout: PipelineLayout, executor: &'a WgpuExecutor, output: WgpuShaderInput, instances: ComputePassDimensions) -> CommandBuffer {
 	executor.create_compute_pass(&layout, Some(output.into()), instances).unwrap()
 }
 
 #[node_macro::node(category("Debug: GPU"))]
 async fn create_pipeline_layout(
-	_: (),
+	_: impl Ctx,
 	shader: impl Node<(), Output = ShaderHandle>,
 	entry_point: String,
 	bind_group: impl Node<(), Output = Bindgroup>,
@@ -868,14 +870,14 @@ async fn create_pipeline_layout(
 }
 
 #[node_macro::node(category(""))]
-async fn read_output_buffer<'a: 'n>(_: (), buffer: Arc<WgpuShaderInput>, executor: &'a WgpuExecutor, _compute_pass: ()) -> Vec<u8> {
+async fn read_output_buffer<'a: 'n>(_: impl Ctx + 'a, buffer: Arc<WgpuShaderInput>, executor: &'a WgpuExecutor, _compute_pass: ()) -> Vec<u8> {
 	executor.read_output_buffer(buffer).await.unwrap()
 }
 
 pub type WindowHandle = Arc<SurfaceHandle<Window>>;
 
 #[node_macro::node(skip_impl)]
-fn create_gpu_surface<'a: 'n, Io: ApplicationIo<Executor = WgpuExecutor, Surface = Window> + 'a + Send + Sync>(_: (), editor_api: &'a EditorApi<Io>) -> Option<WgpuSurface> {
+fn create_gpu_surface<'a: 'n, Io: ApplicationIo<Executor = WgpuExecutor, Surface = Window> + 'a + Send + Sync>(_: impl Ctx + 'a, editor_api: &'a EditorApi<Io>) -> Option<WgpuSurface> {
 	let canvas = editor_api.application_io.as_ref()?.window()?;
 	let executor = editor_api.application_io.as_ref()?.gpu_executor()?;
 	Some(Arc::new(executor.create_surface(canvas).ok()?))
@@ -888,7 +890,13 @@ pub struct ShaderInputFrame {
 }
 
 #[node_macro::node(category(""))]
-async fn render_texture<'a: 'n>(_: (), footprint: Footprint, image: impl Node<Footprint, Output = ShaderInputFrame>, surface: Option<WgpuSurface>, executor: &'a WgpuExecutor) -> SurfaceFrame {
+async fn render_texture<'a: 'n>(
+	_: impl Ctx + 'a,
+	footprint: Footprint,
+	image: impl Node<Footprint, Output = ShaderInputFrame>,
+	surface: Option<WgpuSurface>,
+	executor: &'a WgpuExecutor,
+) -> SurfaceFrame {
 	let surface = surface.unwrap();
 	let surface_id = surface.window_id;
 	let image = image.eval(footprint).await;
@@ -903,34 +911,29 @@ async fn render_texture<'a: 'n>(_: (), footprint: Footprint, image: impl Node<Fo
 	}
 }
 
-// #[node_macro::node(category(""))]
-// async fn upload_texture<'a: 'n, F: Copy + Send + Sync + 'n>(
-// 	#[implementations((), Footprint)] footprint: F,
-// 	#[implementations(() -> ImageFrameTable<Color>, Footprint -> ImageFrameTable<Color>)] input: impl Node<F, Output = ImageFrameTable<Color>>,
-// 	executor: &'a WgpuExecutor,
-// ) -> TextureFrame {
-// 	// let new_data: Vec<RGBA16F> = input.image.data.into_iter().map(|c| c.into()).collect();
-// 	let input = input.eval(footprint).await;
-// 	let input = input.one_item();
-
-// 	let new_data: Vec<SRGBA8> = input.image.data.iter().map(|x| (*x).into()).collect();
-// 	let new_image = Image {
-// 		width: input.image.width,
-// 		height: input.image.height,
-// 		data: new_data,
-// 		base64_string: None,
-// 	};
-
-// 	let shader_input = executor.create_texture_buffer(new_image, TextureBufferOptions::Texture).unwrap();
-// 	let texture = match shader_input {
-// 		ShaderInput::TextureBuffer(buffer, _) => buffer,
-// 		ShaderInput::StorageTextureBuffer(buffer, _) => buffer,
-// 		_ => unreachable!("Unsupported ShaderInput type"),
-// 	};
-
-// 	TextureFrame {
-// 		texture: texture.into(),
-// 		transform: input.transform,
-// 		alpha_blend: Default::default(),
-// 	}
-// }
+#[node_macro::node(category(""))]
+async fn upload_texture<'a: 'n>(_: impl ExtractFootprint + Ctx, input: ImageFrameTable<Color>, executor: &'a WgpuExecutor) -> TextureFrame {
+	// let new_data: Vec<RGBA16F> = input.image.data.into_iter().map(|c| c.into()).collect();
+
+	let input = input.one_item();
+	let new_data: Vec<SRGBA8> = input.image.data.iter().map(|x| (*x).into()).collect();
+	let new_image = Image {
+		width: input.image.width,
+		height: input.image.height,
+		data: new_data,
+		base64_string: None,
+	};
+
+	let shader_input = executor.create_texture_buffer(new_image, TextureBufferOptions::Texture).unwrap();
+	let texture = match shader_input {
+		ShaderInput::TextureBuffer(buffer, _) => buffer,
+		ShaderInput::StorageTextureBuffer(buffer, _) => buffer,
+		_ => unreachable!("Unsupported ShaderInput type"),
+	};
+
+	TextureFrame {
+		texture: texture.into(),
+		transform: input.transform,
+		alpha_blend: Default::default(),
+	}
+}
diff --git a/website/content/volunteer/guide/project-setup/_index.md b/website/content/volunteer/guide/project-setup/_index.md
index 9da71b5c1e..a40c6244b6 100644
--- a/website/content/volunteer/guide/project-setup/_index.md
+++ b/website/content/volunteer/guide/project-setup/_index.md
@@ -21,7 +21,7 @@ Next, install the dependencies required for development builds:
 ```sh
 cargo install cargo-watch
 cargo install wasm-pack
-cargo install -f wasm-bindgen-cli@0.2.99
+cargo install -f wasm-bindgen-cli@0.2.100
 ```
 
 Regarding the last one: you'll likely get faster build times if you manually install that specific version of `wasm-bindgen-cli`. It is supposed to be installed automatically but a version mismatch causes it to reinstall every single recompilation. It may need to be manually updated periodically to match the version of the `wasm-bindgen` dependency in [`Cargo.toml`](https://github.com/GraphiteEditor/Graphite/blob/master/Cargo.toml).


Test patch: