From d077ff0095d53aab7463868503f6a5e2a1ff939f Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Thu, 1 Oct 2026 09:59:04 -0400
Subject: [PATCH 1/8] =?UTF-8?q?fix:=20project.moveToBin=20and=20file.impor?=
=?UTF-8?q?t=20{bin}=20=E2=80=94=20items=20could=20not=20be=20put=20into?=
=?UTF-8?q?=20bins?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bins created with file.newBin stayed empty: there was no command (or drag) to move a
project item into a bin, and import always went to the root. Adds Project::move_to_bin,
the undoable project.moveToBin {items, bin|null} command, and a bin param on file.import.
Co-Authored-By: Claude Opus 5.5
---
crates/engine/src/commands.rs | 17 +++++++++++++++--
crates/engine/src/tests.rs | 28 ++++++++++++++++++++++++++++
crates/project/src/lib.rs | 16 ++++++++++++++++
3 files changed, 59 insertions(+), 2 deletions(-)
diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs
index 6dc4fa6a..06790f32 100644
--- a/crates/engine/src/commands.rs
+++ b/crates/engine/src/commands.rs
@@ -500,7 +500,8 @@ fn build() -> Vec {
Ok(json!({"items": ids.iter().map(|i| i.0).collect::>()}))
}
),
- cmd!("file.import", "Import…", ["File"], Some("Cmd+I"), r#"{"paths":[str]}"#, always, |s, p| {
+ cmd!("file.import", "Import…", ["File"], Some("Cmd+I"), r#"{"paths":[str],"bin":binId?}"#, always, |s, p| {
+ let bin = u64_p(p, "bin").map(filmcraft_project::BinId);
let paths: Vec = match p.get("paths").and_then(Value::as_array) {
Some(a) => a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect(),
None => str_p(p, "path").map(|x| vec![x.to_string()]).unwrap_or_default(),
@@ -531,7 +532,7 @@ fn build() -> Vec {
Err(e) => errors.push(format!("{path}: {e}")),
}
}
- Ok(b) => match import_bytes(s, &path, b.into(), None) {
+ Ok(b) => match import_bytes(s, &path, b.into(), bin) {
Ok(id) => ids.push(id.0),
Err(e) => errors.push(format!("{path}: {e}")),
},
@@ -1810,6 +1811,18 @@ fn build() -> Vec {
s.fix_state();
Ok(Value::Null)
}),
+ cmd!("project.moveToBin", "Move to Bin", [], None, r#"{"items":[id]?,"bin":binId|null}"#, always, |s, p| {
+ let items: Vec = match p.get("items").and_then(Value::as_array) {
+ Some(a) => a.iter().filter_map(|v| v.as_u64().map(ItemId)).collect(),
+ None => s.state.project_selection.clone(),
+ };
+ if items.is_empty() {
+ return Err(bad("project.moveToBin", "need `items` (or a Project panel selection)"));
+ }
+ let bin = u64_p(p, "bin").map(filmcraft_project::BinId);
+ let moved = s.edit("Move to Bin", |pr, _| pr.move_to_bin(&items, bin).ok_or_else(|| bad("project.moveToBin", "no such bin")))?;
+ Ok(json!({"moved": moved}))
+ }),
cmd!("project.setMarks", "Set Source In/Out", [], None, r#"{"item":id,"in":ticks?|null,"out":ticks?|null}"#, always, |s, p| {
let id = item_p(p, "item").or(s.state.source_item).ok_or_else(|| bad("project.setMarks", "need `item`"))?;
let p = p.clone();
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
index 09bea8dd..72c97350 100644
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -226,3 +226,31 @@ fn interchange_roundtrip_through_files() {
}
let _ = std::fs::remove_dir_all(&dir);
}
+
+#[test]
+fn move_items_to_bin_and_back_with_undo() {
+ let mut s = demo();
+ let bin = s.execute("file.newBin", json!({"name": "Selects"})).unwrap()["bin"].as_u64().unwrap();
+ let mut items = Vec::new();
+ s.project.root.all_items(&mut items);
+ let item = items[0];
+ let home = s.project.root.parent_of(item);
+ let r = s.execute("project.moveToBin", json!({"items": [item.0], "bin": bin})).unwrap();
+ assert_eq!(r["moved"], 1);
+ assert_eq!(s.project.root.parent_of(item), Some(filmcraft_project::BinId(bin)));
+ // the item is in the tree exactly once
+ let mut all = Vec::new();
+ s.project.root.all_items(&mut all);
+ assert_eq!(all.iter().filter(|i| **i == item).count(), 1);
+ s.execute("edit.undo", json!({})).unwrap();
+ assert_eq!(s.project.root.parent_of(item), home);
+ s.execute("edit.redo", json!({})).unwrap();
+ assert_eq!(s.project.root.parent_of(item), Some(filmcraft_project::BinId(bin)));
+ // back to the root with `bin: null`
+ s.execute("project.moveToBin", json!({"items": [item.0], "bin": null})).unwrap();
+ assert_eq!(s.project.root.parent_of(item), Some(s.project.root.id));
+ // an unknown bin is an error, and so is no items and no selection
+ assert!(s.execute("project.moveToBin", json!({"items": [item.0], "bin": 999_999})).is_err());
+ s.state.project_selection.clear();
+ assert!(s.execute("project.moveToBin", json!({})).is_err());
+}
diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs
index 5a1f1192..d9e09ec9 100644
--- a/crates/project/src/lib.rs
+++ b/crates/project/src/lib.rs
@@ -862,6 +862,22 @@ impl Project {
id
}
+ /// Move items into a bin (the root when `bin` is None). Returns how many moved; unknown items
+ /// are skipped. Fails (returns None) when the bin does not exist.
+ pub fn move_to_bin(&mut self, items: &[ItemId], bin: Option) -> Option {
+ let target = bin.unwrap_or(self.root.id);
+ self.root.find_bin(target)?;
+ let mut moved = 0;
+ for &id in items {
+ if !self.items.contains_key(&id) || !self.root.remove_item(id) {
+ continue;
+ }
+ self.root.find_bin_mut(target).expect("checked above").children.push(BinEntry::Item(id));
+ moved += 1;
+ }
+ Some(moved)
+ }
+
pub fn item(&self, id: ItemId) -> Option<&ProjectItem> {
self.items.get(&id)
}
From a426bdfb2d3bd64cd0571b4eb5abc266f7251044 Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Thu, 1 Oct 2026 10:05:53 -0400
Subject: [PATCH 2/8] fix: Save As names the project after its file
After file.saveAs to 'Apollo 11 - Tranquility.fcproj' the title bar, Project panel and
recovery list still said 'Untitled'. Save As now renames the project (and its root bin)
to the file stem, without an undo step or dirtying the project; Save a Copy keeps the name.
Co-Authored-By: Claude Opus 5.5
---
crates/engine/src/autosave_tests.rs | 2 +-
crates/engine/src/commands.rs | 18 ++++++++++++++++++
crates/engine/src/file_tests.rs | 18 ++++++++++++++++++
3 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/crates/engine/src/autosave_tests.rs b/crates/engine/src/autosave_tests.rs
index 51f27cd2..0e113e41 100644
--- a/crates/engine/src/autosave_tests.rs
+++ b/crates/engine/src/autosave_tests.rs
@@ -86,7 +86,7 @@ fn crash_recovery_end_to_end() {
start(&mut c, &d);
assert_eq!(c.recovery_candidates().len(), 1);
let list = c.execute("file.recoveryList", json!({})).unwrap();
- assert_eq!(list[0]["projectName"], "FilmCraft Demo");
+ assert_eq!(list[0]["projectName"], "film", "named after the file it was saved as");
assert_eq!(list[0]["projectPath"], proj.as_str());
assert_eq!(list[0]["cleanExit"], false);
assert_eq!(list[0]["savedAt"].as_str().unwrap().len(), 19);
diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs
index 06790f32..2260dfd2 100644
--- a/crates/engine/src/commands.rs
+++ b/crates/engine/src/commands.rs
@@ -2299,8 +2299,26 @@ pub fn secs(s: f64) -> Tick {
/// Save / Save As (the file becomes the project's path and the project is clean); otherwise Save a
/// Copy. The first save over a file upgraded from an older schema keeps the original as
/// ` (schema vN backup).fcproj`.
+/// A project is named after its file (as in Premiere): `/a/b/Trailer v2.fcproj` → `Trailer v2`.
+fn project_name_for(path: &str) -> Option {
+ let stem = std::path::Path::new(path).file_stem()?.to_string_lossy().to_string();
+ (!stem.is_empty()).then_some(stem)
+}
+
+fn name_project(p: &mut filmcraft_project::Project, name: String) {
+ p.root.name.clone_from(&name);
+ p.name = name;
+}
+
fn write_project(s: &mut Session, path: &str, adopt: bool) -> Result {
let t0 = std::time::Instant::now();
+ // Save As renames the project after its new file (not an edit: no undo step, not dirty).
+ if adopt
+ && let Some(name) = project_name_for(path)
+ && s.project.name != name
+ {
+ name_project(std::sync::Arc::make_mut(&mut s.project), name);
+ }
let bytes = filmcraft_format::encode(&s.project, false);
let mut backup = None;
if adopt && s.path.as_deref() == Some(path) && s.loaded_schema < filmcraft_format::SCHEMA_VERSION {
diff --git a/crates/engine/src/file_tests.rs b/crates/engine/src/file_tests.rs
index 8466183c..00f2ea07 100644
--- a/crates/engine/src/file_tests.rs
+++ b/crates/engine/src/file_tests.rs
@@ -75,3 +75,21 @@ fn legacy_project_opens_and_first_save_keeps_a_backup() {
assert!(e.contains("newer version of FilmCraft"), "{e}");
let _ = std::fs::remove_dir_all(&d);
}
+
+#[test]
+fn project_is_named_after_its_file() {
+ let d = temp_dir("name");
+ let mut s = demo();
+ let a = d.join("Apollo 11 - Tranquility.fcproj").to_string_lossy().to_string();
+ s.execute("file.saveAs", json!({"path": a})).unwrap();
+ assert_eq!(s.project.name, "Apollo 11 - Tranquility");
+ assert_eq!(s.project.root.name, "Apollo 11 - Tranquility");
+ assert!(!s.is_dirty(), "renaming on Save As is not an edit");
+ // Save a Copy keeps the current name.
+ let c = d.join("Backup copy.fcproj").to_string_lossy().to_string();
+ s.execute("file.saveCopy", json!({"path": c})).unwrap();
+ assert_eq!(s.project.name, "Apollo 11 - Tranquility");
+ s.execute("file.open", json!({"path": c})).unwrap();
+ assert_eq!(s.project.name, "Apollo 11 - Tranquility");
+ let _ = std::fs::remove_dir_all(&d);
+}
From 54e2ca0994392db642d615284d286abb9ce2736b Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Thu, 1 Oct 2026 10:14:55 -0400
Subject: [PATCH 3/8] fix: Effect Controls shows LUT, text and curve params
readably
Lumetri's Input LUT and Look LUT rows printed the Rust debug value (Text("")), and
curve params would print Curve([[0.0, 0.0], ...]). They now read None / the LUT's name,
the text itself, and Default / Custom (n points).
Co-Authored-By: Claude Opus 5.5
---
crates/ui-egui/src/panels/effect_controls.rs | 31 +++++++++++++++++++-
1 file changed, 30 insertions(+), 1 deletion(-)
diff --git a/crates/ui-egui/src/panels/effect_controls.rs b/crates/ui-egui/src/panels/effect_controls.rs
index 0597566c..be13d847 100644
--- a/crates/ui-egui/src/panels/effect_controls.rs
+++ b/crates/ui-egui/src/panels/effect_controls.rs
@@ -260,7 +260,7 @@ fn param_row(
}
}
_ => {
- vui.label(format!("{value:?}"));
+ vui.label(param_text(&app.session.project, pd.id, &value));
}
}
if let Some(v) = set {
@@ -775,3 +775,32 @@ fn graph_rows(
}
}
}
+
+/// Read-only text for parameters without an inline editor (LUT references, free text, curves).
+fn param_text(project: &filmcraft_project::Project, id: &str, value: &ParamValue) -> String {
+ match value {
+ ParamValue::Text(s) if id.ends_with("_lut") => filmcraft_render::luts::label(Some(project), s),
+ ParamValue::Text(s) if s.is_empty() => "None".into(),
+ ParamValue::Text(s) => s.clone(),
+ ParamValue::Curve(pts) if pts.is_empty() || *pts == [[0.0, 0.0], [1.0, 1.0]] => "Default".into(),
+ ParamValue::Curve(pts) => format!("Custom ({} points)", pts.len()),
+ other => format!("{other:?}"),
+ }
+}
+
+#[cfg(test)]
+mod param_text_tests {
+ use super::*;
+
+ #[test]
+ fn readable_values_for_text_lut_and_curve_params() {
+ let p = filmcraft_project::Project::default();
+ assert_eq!(param_text(&p, "input_lut", &ParamValue::Text(String::new())), "None");
+ assert_eq!(param_text(&p, "look_lut", &ParamValue::Text("builtin:look-teal-orange".into())), "Teal & Orange");
+ assert_eq!(param_text(&p, "label", &ParamValue::Text(String::new())), "None");
+ assert_eq!(param_text(&p, "label", &ParamValue::Text("Reel 3".into())), "Reel 3");
+ assert_eq!(param_text(&p, "curve_luma", &ParamValue::Curve(vec![[0.0, 0.0], [1.0, 1.0]])), "Default");
+ assert_eq!(param_text(&p, "hue_vs_sat", &ParamValue::Curve(vec![])), "Default");
+ assert_eq!(param_text(&p, "curve_luma", &ParamValue::Curve(vec![[0.0, 0.0], [0.5, 0.6], [1.0, 1.0]])), "Custom (3 points)");
+ }
+}
From c14fc8e1b709fc7f3d72d8f3547c61216a31c901 Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Thu, 1 Oct 2026 10:56:50 -0400
Subject: [PATCH 4/8] fix: timeline waveforms from the previous project after
File > Open
Waveform peaks were cached by item id and never cleared, and item ids repeat across
projects: after opening a project the timeline drew the previous project's waveforms
(a 7-minute mission-audio clip showed as silent). The cache is now reset with the media
pool, and an item whose source is not available is no longer left pending forever.
Co-Authored-By: Claude Opus 5.5
---
crates/ui-egui/src/lib.rs | 1 +
crates/ui-egui/src/panels/timeline.rs | 13 ++++++++++++-
crates/ui-egui/tests/scripted.rs | 27 +++++++++++++++++++++++++++
3 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
index 9142652a..16a598a5 100644
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -250,6 +250,7 @@ impl FilmcraftApp {
FrameServer::default_workers(),
));
self.textures.clear();
+ self.tl.reset_media_caches();
}
}
diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs
index da05c234..30c70735 100644
--- a/crates/ui-egui/src/panels/timeline.rs
+++ b/crates/ui-egui/src/panels/timeline.rs
@@ -42,6 +42,16 @@ pub struct TlState {
zoom_anchor: Option<(f64, f32)>,
}
+impl TlState {
+ /// Forget cached waveform peaks: they are keyed by item id, and ids repeat across projects
+ /// (opening another project would otherwise show the old project's waveforms). Peak jobs
+ /// still running finish into the old maps and are dropped.
+ pub fn reset_media_caches(&mut self) {
+ self.peaks = Default::default();
+ self.peaks_pending = Default::default();
+ }
+}
+
#[derive(Clone, Debug)]
pub enum Drag {
Scrub,
@@ -544,6 +554,8 @@ fn request_peaks(app: &mut FilmcraftApp, item: ItemId) -> Option Option
Date: Thu, 1 Oct 2026 11:05:30 -0400
Subject: [PATCH 5/8] fix: timeline waveforms of near-silent clips no longer
drawn full scale
Waveforms are normalised to the source's peak so quiet recordings stay readable, but the
boost was unbounded: an empty audio stream at -52 dBFS (silent archive film) was drawn as
loud as dialogue. The normalisation is now capped at +12 dB.
Co-Authored-By: Claude Opus 5.5
---
crates/ui-egui/src/panels/timeline.rs | 36 +++++++++++++++++++++++++--
1 file changed, 34 insertions(+), 2 deletions(-)
diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs
index 30c70735..f3f08143 100644
--- a/crates/ui-egui/src/panels/timeline.rs
+++ b/crates/ui-egui/src/panels/timeline.rs
@@ -509,8 +509,8 @@ fn draw_waveform(app: &mut FilmcraftApp, p: &egui::Painter, body: Rect, it: &Tra
if clip.width() <= 0.0 {
return;
}
- let peak = peaks.iter().fold(0f32, |m, (a, b)| m.max(a.abs()).max(b.abs())).max(1e-4);
- let gain = filmcraft_render::audio::db_to_gain(it.gain_db) / peak;
+ let peak = peaks.iter().fold(0f32, |m, (a, b)| m.max(a.abs()).max(b.abs()));
+ let gain = waveform_display_gain(peak, it.gain_db);
let mut mesh = egui::Mesh::default();
let dur_px = body.width().max(1.0);
for x in (clip.min.x.floor() as i32)..(clip.max.x.ceil() as i32) {
@@ -550,6 +550,15 @@ fn draw_waveform(app: &mut FilmcraftApp, p: &egui::Painter, body: Rect, it: &Tra
}
}
+/// Display gain for a clip's waveform: clip gain, plus normalisation of the source to full scale
+/// so quiet recordings stay readable, but by at most +12 dB, so a near-silent track (room tone at
+/// −50 dBFS, a silent film's empty audio stream) does not look like a loud one.
+fn waveform_display_gain(source_peak: f32, clip_gain_db: f64) -> f32 {
+ const MAX_BOOST: f32 = 3.981_072; // +12 dB
+ let boost = (1.0 / source_peak.max(1e-9)).min(MAX_BOOST);
+ filmcraft_render::audio::db_to_gain(clip_gain_db) * boost
+}
+
fn request_peaks(app: &mut FilmcraftApp, item: ItemId) -> Option>> {
if let Some(p) = app.tl.peaks.lock().unwrap_or_else(|e| e.into_inner()).get(&item) {
return Some(p.clone());
@@ -1611,3 +1620,26 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: &
}
app.auto.add("timeline.tracks", layout.content, "tracks");
}
+
+#[cfg(test)]
+mod waveform_tests {
+ use super::waveform_display_gain;
+
+ fn db(g: f32) -> f32 {
+ 20.0 * g.log10()
+ }
+
+ #[test]
+ fn normalisation_is_capped_for_near_silent_sources() {
+ // a −14 dBFS recording is normalised to full scale (+12 dB cap reached: −2 dBFS)
+ assert!((db(0.2 * waveform_display_gain(0.2, 0.0)) - (-2.0)).abs() < 0.1);
+ // a −3 dBFS recording is normalised exactly
+ assert!((db(0.708 * waveform_display_gain(0.708, 0.0))).abs() < 0.1);
+ // a −52 dBFS "silent" stream stays near the floor of the −48 dB display range
+ assert!(db(0.0025 * waveform_display_gain(0.0025, 0.0)) < -39.0);
+ // digital silence does not blow up
+ assert!(waveform_display_gain(0.0, 0.0).is_finite());
+ // clip gain still applies
+ assert!((db(waveform_display_gain(1.0, -6.0)) - (-6.0)).abs() < 0.1);
+ }
+}
From a2517bff082584bbd5e62fb6cb6015a4c66bedfc Mon Sep 17 00:00:00 2001
From: Brandon Thomas
Date: Thu, 1 Oct 2026 11:17:08 -0400
Subject: [PATCH 6/8] =?UTF-8?q?README:=20new=20hero=20=E2=80=94=20Apollo?=
=?UTF-8?q?=2011=20documentary=20edit=20(public=20domain)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replaces the Night of the Living Dead trailer screenshot with a real job: a 3:10
documentary cut of NASA's Apollo 11 film (images.nasa.gov, US Government works) built
through the control channel — 49 picture cuts with dissolves, B-roll, lower-third titles
set by the text engine, subtitles from the NASA air-to-ground transcript, mission audio,
a CC0 Chopin bed ducked by Essential Sound, a Lumetri grade with keyframes, markers and a
rendered section. Sidecar lists every source and the reproduction steps; NASA does not
endorse this project.
Co-Authored-By: Claude Opus 5.5
---
ATTRIBUTION.md | 2 +-
README.md | 6 +--
docs/images/filmcraft-hero.png | Bin 1029560 -> 1281948 bytes
docs/images/filmcraft-hero.png.attribution | 60 +++++++++++++++++----
4 files changed, 53 insertions(+), 15 deletions(-)
diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md
index 06967d51..54ea5276 100644
--- a/ATTRIBUTION.md
+++ b/ATTRIBUTION.md
@@ -16,7 +16,7 @@ iconography, images or other Adobe assets; open licences only; every asset attri
| `assets/fonts/Inter-Italic.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/NotoSerif-Regular.ttf` | The Noto Project Authors | https://github.com/notofonts/latin-greek-cyrillic | OFL-1.1 (`assets/fonts/OFL-NotoSerif.txt`) |
| `assets/fonts/JetBrainsMono-Regular.ttf` | The JetBrains Mono Project Authors | https://github.com/JetBrains/JetBrainsMono | OFL-1.1 (`assets/fonts/OFL-JetBrainsMono.txt`) |
-| `docs/images/filmcraft-hero.png` | FilmCraft contributors | Original work: FilmCraft screenshot; frames from public-domain films (Night of the Living Dead 1968, Carnival of Souls 1962), CC0 Chopin (Musopen) | MIT OR Apache-2.0; film frames public domain |
+| `docs/images/filmcraft-hero.png` | FilmCraft contributors | Original work: FilmCraft screenshot; NASA Apollo 11 film and TV footage (images.nasa.gov; US Government work, public domain; NASA does not endorse this project), subtitles from the NASA Apollo 11 air-to-ground transcript (public domain), CC0 Chopin (Musopen) | MIT OR Apache-2.0; footage and transcript public domain |
| `docs/images/filmcraft-color.png` | FilmCraft contributors | Original work: FilmCraft screenshot; frames from Charade (1963, public domain) | MIT OR Apache-2.0; film frames public domain |
| `docs/images/filmcraft-keyframes.png` | FilmCraft contributors | Original work: FilmCraft screenshot; frames from Night of the Living Dead (1968, public domain) | MIT OR Apache-2.0; film frames public domain |
| `docs/images/filmcraft-export.png` | FilmCraft contributors | Original work: FilmCraft screenshot; frame from Night of the Living Dead (1968, public domain) | MIT OR Apache-2.0; film frames public domain |
diff --git a/README.md b/README.md
index 6a707158..4e668c0d 100644
--- a/README.md
+++ b/README.md
@@ -19,10 +19,10 @@
-
+
-Cutting a trailer for Night of the Living Dead (1968). Every frame in these screenshots comes from public-domain footage, decoded, composited and graded by FilmCraft's own code.
+Apollo 11 — Tranquility: a three-minute documentary edit of NASA's 1969 launch, landing and moonwalk film, with subtitles from the mission transcript. Every frame in these screenshots comes from public-domain footage, decoded, composited and graded by FilmCraft's own code.
@@ -259,7 +259,7 @@ Open-source, clean-room, pure-Rust creative tools. Each runs natively on macOS,
## Credits and clean room
-**Footage and music in the screenshots:** *Night of the Living Dead* (1968), *Carnival of Souls* (1962) and *Charade* (1963), all in the US public domain; *Earth Views from the ISS* by NASA (a US Government work; NASA does not endorse this project); Chopin's Nocturne Op. 48 No. 1 and Ballade No. 1, performed for Musopen and released under CC0. The media itself is not in this repository. Sources and details for every asset are in [ATTRIBUTION.md](ATTRIBUTION.md).
+**Footage and music in the screenshots:** NASA's Apollo 11 film and television footage from [images.nasa.gov](https://images.nasa.gov) (launch, Launch Control Center, lunar surface and recovery) and the Apollo 11 air-to-ground voice transcript, all US Government works in the public domain (NASA does not endorse this project); *Night of the Living Dead* (1968), *Carnival of Souls* (1962) and *Charade* (1963), all in the US public domain; *Earth Views from the ISS* by NASA; Chopin's Nocturne Op. 48 No. 1 and Ballade No. 1, performed for Musopen and released under CC0. The media itself is not in this repository. Sources and details for every asset are in [ATTRIBUTION.md](ATTRIBUTION.md).
FilmCraft is an independent implementation. It contains no Adobe code, icons, images, presets or LUTs, and no GPL or LGPL code; every icon is drawn in code and every asset is openly licensed and attributed ([AGENTS.md](AGENTS.md)). ffmpeg is used only as an external test oracle. Adobe and Premiere Pro are trademarks of Adobe Inc.; FilmCraft is not affiliated with Adobe.
diff --git a/docs/images/filmcraft-hero.png b/docs/images/filmcraft-hero.png
index 7584cedb725f261c4dbc34bdb8effa0cb15f5ad1..1b19d1dffffb8a23a19191f5f533b38ee5f4e8ab 100644
GIT binary patch
literal 1281948
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