use std::fmt;
use std::path::PathBuf;
use std::rc::Rc;
use oxframe_2d::{SceneGraph, View};
use oxframe_core::{Color, Executor, Recorded};
use oxframe_protocol::{MarkerInfo, Meta, MetaError, SceneInfo};
use oxframe_render::{Image, RenderOptions, Renderer};
use crate::{Project, SceneDef};
#[derive(Debug)]
pub enum EngineError {
Meta {
path: PathBuf,
error: MetaError,
},
Io {
path: PathBuf,
error: std::io::Error,
},
NoScenes,
}
impl fmt::Display for EngineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Meta { path, error } => write!(f, "{}: {error}", path.display()),
Self::Io { path, error } => write!(f, "{}: {error}", path.display()),
Self::NoScenes => write!(f, "the project has no scenes"),
}
}
}
impl std::error::Error for EngineError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Meta { error, .. } => Some(error),
Self::Io { error, .. } => Some(error),
Self::NoScenes => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetaSection {
Markers,
Eases,
Tunables,
}
impl MetaSection {
fn singular(self) -> &'static str {
match self {
Self::Markers => "marker",
Self::Eases => "ease",
Self::Tunables => "tunable",
}
}
}
struct SceneRun {
graph: Rc<SceneGraph>,
exec: Executor,
theme: oxframe_2d::Theme,
has_previous: bool,
}
impl SceneRun {
fn new(def: &SceneDef, project: &Project, meta: Meta, has_previous: bool) -> Self {
let graph = SceneGraph::new(project.size.0, project.size.1);
let fut = (def.factory)(View::current());
Self {
graph,
exec: Executor::new(
f64::from(project.fps),
meta,
scene_seed(project.seed, def.name),
fut,
),
theme: project.theme.clone(),
has_previous,
}
}
fn step(&mut self) -> bool {
self.exec.set_has_previous(self.has_previous);
let theme = self.theme.clone();
let done = oxframe_2d::enter_theme(&theme, || {
let Self { graph, exec, .. } = self;
graph.enter(|| exec.step())
});
let frame = self.exec.frames_done().saturating_sub(1);
self.graph.set_time(f64::from(frame) / self.exec.fps());
done
}
fn last_frame(&self) -> Option<u32> {
self.exec.frames_done().checked_sub(1)
}
}
struct SceneState {
def: SceneDef,
meta: Meta,
meta_path: PathBuf,
start: u32,
frames: u32,
incoming: Option<Recorded>,
held: Option<(u32, u32, Image)>,
run: Option<SceneRun>,
}
pub struct Engine {
project: Project,
scenes: Vec<SceneState>,
skip_transitions: bool,
}
impl Engine {
pub fn new(project: Project) -> Result<Self, EngineError> {
if project.scenes.is_empty() {
return Err(EngineError::NoScenes);
}
let mut scenes = Vec::with_capacity(project.scenes.len());
for def in &project.scenes {
let meta_path = match &project.meta_dir {
Some(dir) => dir.join(format!("{}.meta.json", def.name)),
None => def.meta_path(),
};
let meta = match &project.inline_meta {
Some(m) => m.get(def.name).cloned().unwrap_or_default(),
None => load_meta(&meta_path)?,
};
scenes.push(SceneState {
def: *def,
meta,
meta_path,
start: 0,
frames: 0,
incoming: None,
held: None,
run: None,
});
}
if let Some(file) = &project.audio
&& let Some(first) = scenes.first_mut()
&& first.meta.audio.as_ref().is_none_or(|a| &a.file != file)
{
let offset = first.meta.audio.as_ref().map_or(0.0, |a| a.offset);
first.meta.audio = Some(oxframe_protocol::AudioMeta {
file: file.clone(),
offset,
extra: Default::default(),
});
if project.write_meta && project.inline_meta.is_none() {
save_meta(&first.meta_path, &first.meta)?;
}
}
let mut engine = Self {
project,
scenes,
skip_transitions: false,
};
for i in 0..engine.scenes.len() {
engine.measure(i)?;
}
engine.layout();
Ok(engine)
}
pub fn project(&self) -> &Project {
&self.project
}
fn measure(&mut self, i: usize) -> Result<(), EngineError> {
let max = (self.project.max_scene_seconds * f64::from(self.project.fps)).ceil() as u32;
let scene = &mut self.scenes[i];
scene.held = None;
let mut run = SceneRun::new(&scene.def, &self.project, scene.meta.clone(), i > 0);
while !run.step() && run.exec.frames_done() < max.max(1) {}
scene.frames = run.exec.frames_done();
scene.incoming = run.exec.transition();
if run.exec.take_meta_changed() {
scene.meta = run.exec.meta();
if self.project.write_meta && self.project.inline_meta.is_none() {
save_meta(&scene.meta_path, &scene.meta)?;
}
}
scene.run = Some(run);
Ok(())
}
fn layout(&mut self) {
let mut start = 0;
for s in &mut self.scenes {
s.start = start;
start += s.frames;
}
}
pub fn fps(&self) -> u32 {
self.project.fps
}
pub fn size(&self) -> (u32, u32) {
self.project.size
}
pub fn total_frames(&self) -> u32 {
self.scenes.last().map_or(0, |s| s.start + s.frames)
}
pub fn scenes(&self) -> Vec<SceneInfo> {
self.scenes
.iter()
.map(|s| {
let (at, frames) = blend_span(s, self.project.fps);
SceneInfo {
name: s.def.name.to_owned(),
start: s.start,
frames: s.frames,
transition: frames,
transition_at: at,
}
})
.collect()
}
pub fn markers(&self) -> Vec<MarkerInfo> {
self.scenes
.iter()
.flat_map(|s| {
s.meta.markers.iter().map(|(name, &time)| MarkerInfo {
scene: s.def.name.to_owned(),
name: name.clone(),
time,
})
})
.collect()
}
pub fn audio(&self) -> Option<(String, f64)> {
let a = self.scenes.first()?.meta.audio.as_ref()?;
Some((a.file.clone(), a.offset))
}
pub fn metas(&self) -> Vec<(String, Meta)> {
self.scenes
.iter()
.map(|s| (s.def.name.to_owned(), s.meta.clone()))
.collect()
}
pub fn meta(&self, scene: &str) -> Option<&Meta> {
self.scenes
.iter()
.find(|s| s.def.name == scene)
.map(|s| &s.meta)
}
pub fn locate(&self, frame: u32) -> (usize, u32) {
let total = self.total_frames();
let frame = frame.min(total.saturating_sub(1));
let i = self
.scenes
.iter()
.rposition(|s| s.start <= frame)
.unwrap_or(0);
(i, frame - self.scenes[i].start)
}
pub fn seek(&mut self, frame: u32) -> &SceneGraph {
let (i, local) = self.locate(frame);
let project = &self.project;
let scene = &mut self.scenes[i];
let reusable = scene
.run
.as_ref()
.is_some_and(|r| r.last_frame().is_some_and(|last| last <= local));
if !reusable {
scene.run = Some(SceneRun::new(
&scene.def,
project,
scene.meta.clone(),
i > 0,
));
}
let run = scene.run.as_mut().expect("run was just ensured");
while run.last_frame().is_none_or(|last| last < local) {
if run.step() {
break;
}
}
for (j, s) in self.scenes.iter_mut().enumerate() {
if j != i {
s.run = None;
}
}
&self.scenes[i].run.as_ref().expect("run exists").graph
}
pub fn edit_meta(
&mut self,
section: MetaSection,
name: &str,
scene: Option<&str>,
apply: impl FnOnce(&mut Meta),
) -> Result<(String, Meta), String> {
let i = match scene {
Some(scene) => self
.scenes
.iter()
.position(|s| s.def.name == scene)
.ok_or_else(|| format!("no scene named `{scene}`"))?,
None => self
.scenes
.iter()
.position(|s| match section {
MetaSection::Markers => s.meta.markers.contains_key(name),
MetaSection::Eases => s.meta.eases.contains_key(name),
MetaSection::Tunables => s.meta.tunables.contains_key(name),
})
.ok_or_else(|| format!("no {} named `{name}` in any scene", section.singular()))?,
};
let mut meta = self.scenes[i].meta.clone();
apply(&mut meta);
self.write_scene_meta(i, meta)
}
pub fn pixels_to_scene(&self, x: f64, y: f64) -> oxframe_core::Point {
let (w, h) = self.project.size;
oxframe_core::Point::new(x - f64::from(w) / 2.0, y - f64::from(h) / 2.0)
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn skip_transitions(&mut self, skip: bool) {
self.skip_transitions = skip;
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn transition_at(&self, frame: u32) -> Option<TransitionAt> {
let (_i, rec, now) = self.blend_at(frame)?;
Some(TransitionAt {
kind: rec.kind_name(),
dir: rec.dir_name(),
progress: rec.progress(now),
overlap: rec.overlap(),
})
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn transition_spans(&self) -> Vec<TransitionSpan> {
self.scenes
.iter()
.filter_map(|s| {
let (at, frames) = blend_span(s, self.project.fps);
let rec = s.incoming?;
(frames > 0).then(|| TransitionSpan {
scene: s.def.name.to_owned(),
start: s.start + at,
frames,
kind: rec.kind_name(),
})
})
.collect()
}
pub(crate) fn set_transition_seconds(
&mut self,
scene: &str,
seconds: f64,
) -> Result<(String, Meta), String> {
let i = self
.scenes
.iter()
.position(|s| s.def.name == scene)
.ok_or_else(|| format!("no scene named `{scene}`"))?;
let seconds = if seconds.is_finite() {
seconds.max(0.0)
} else {
0.0
};
let mut meta = self.scenes[i].meta.clone();
meta.transition = Some(oxframe_protocol::TransitionMeta { seconds });
self.write_scene_meta(i, meta)
}
fn blend_at(&self, frame: u32) -> Option<(usize, Recorded, f64)> {
if self.skip_transitions {
return None;
}
let (i, local) = self.locate(frame);
if i == 0 {
return None;
}
let rec = self.scenes[i].incoming?;
let now = f64::from(local) / f64::from(self.project.fps);
rec.active(now).then_some((i, rec, now))
}
fn outgoing_frame(&self, i: usize, rec: Recorded, now: f64) -> u32 {
let prev = &self.scenes[i - 1];
let last_local = prev.frames.saturating_sub(1);
if !rec.overlap() {
return prev.start + last_local;
}
let fps = f64::from(self.project.fps);
let elapsed = (now - rec.start()).max(0.0);
let prev_end = f64::from(last_local) / fps;
let prev_t = (prev_end - rec.seconds() + elapsed).max(0.0);
let local = ((prev_t * fps).round() as u32).min(last_local);
prev.start + local
}
pub fn render(&mut self, frame: u32, renderer: &mut dyn Renderer, size: (u32, u32)) -> Image {
let background = self.project.background;
if let Some((i, rec, now)) = self.blend_at(frame) {
let hold = !rec.overlap();
let from_frame = self.outgoing_frame(i, rec, now);
let blend = rec.blend_at(now);
let t = rec.progress(now);
let prev = i - 1;
let from = if hold {
self.held_layer(prev, renderer, size)
} else {
self.layer(from_frame, renderer, size)
};
let to = self.layer(frame, renderer, size);
let image = crate::transition::composite(&from, &to, &blend, t, background);
renderer.present_image(&image);
return image;
}
self.layer_on(frame, renderer, size, background)
}
fn held_layer(&mut self, scene: usize, renderer: &mut dyn Renderer, size: (u32, u32)) -> Image {
if let Some((w, h, img)) = &self.scenes[scene].held
&& *w == size.0
&& *h == size.1
{
return img.clone();
}
let last = self.scenes[scene].start + self.scenes[scene].frames.saturating_sub(1);
let img = self.layer(last, renderer, size);
self.scenes[scene].held = Some((size.0, size.1, img.clone()));
img
}
fn layer(&mut self, frame: u32, renderer: &mut dyn Renderer, size: (u32, u32)) -> Image {
self.draw(frame, renderer, size, Color::TRANSPARENT, false)
}
fn layer_on(
&mut self,
frame: u32,
renderer: &mut dyn Renderer,
size: (u32, u32),
background: Color,
) -> Image {
self.draw(frame, renderer, size, background, true)
}
fn draw(
&mut self,
frame: u32,
renderer: &mut dyn Renderer,
size: (u32, u32),
background: Color,
present: bool,
) -> Image {
let theme = self.project.theme.clone();
let graph = self.seek(frame);
let options = RenderOptions {
width: size.0,
height: size.1,
background,
};
oxframe_2d::enter_theme(&theme, || {
if present {
renderer.render(graph, &options)
} else {
renderer.render_pixels(graph, &options)
}
})
}
fn write_scene_meta(&mut self, i: usize, meta: Meta) -> Result<(String, Meta), String> {
meta.validate().map_err(|e| e.to_string())?;
let scene = &mut self.scenes[i];
scene.meta = meta;
scene.held = None;
if self.project.write_meta && self.project.inline_meta.is_none() {
save_meta(&scene.meta_path, &scene.meta).map_err(|e| e.to_string())?;
}
scene.run = None;
self.measure(i).map_err(|e| e.to_string())?;
self.layout();
let s = &self.scenes[i];
Ok((s.def.name.to_owned(), s.meta.clone()))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) struct TransitionAt {
pub kind: &'static str,
pub dir: Option<&'static str>,
pub progress: f64,
pub overlap: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(not(target_arch = "wasm32"))]
pub(crate) struct TransitionSpan {
pub scene: String,
pub start: u32,
pub frames: u32,
pub kind: &'static str,
}
fn blend_span(scene: &SceneState, fps: u32) -> (u32, u32) {
let Some(rec) = scene.incoming else {
return (0, 0);
};
let fps = f64::from(fps.max(1));
let mut at = 0;
let mut frames = 0;
let mut seen = false;
for local in 0..scene.frames {
if rec.active(f64::from(local) / fps) {
if !seen {
at = local;
seen = true;
}
frames += 1;
}
}
(at, frames)
}
fn scene_seed(seed: u64, name: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in name.bytes() {
h = (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
}
seed ^ h
}
fn load_meta(path: &PathBuf) -> Result<Meta, EngineError> {
match std::fs::read_to_string(path) {
Ok(text) => Meta::parse(&text).map_err(|error| EngineError::Meta {
path: path.clone(),
error,
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Meta::default()),
Err(error) => Err(EngineError::Io {
path: path.clone(),
error,
}),
}
}
fn save_meta(path: &PathBuf, meta: &Meta) -> Result<(), EngineError> {
let text = meta.to_pretty_string();
if std::fs::read_to_string(path).is_ok_and(|old| old == text) {
return Ok(());
}
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, &text)
.and_then(|()| std::fs::rename(&tmp, path))
.map_err(|error| EngineError::Io {
path: path.clone(),
error,
})
}