use crate::scene::{Point, Transform};
use crate::tex::{self, Cat, Token};
use crate::Error;
use super::{tikra_tokens, Engine};
fn seconds(s: &str) -> Option<f64> {
let s = s.trim();
if let Some(ms) = s.strip_suffix("ms") {
return ms.trim().parse::<f64>().ok().map(|v| v / 1000.0);
}
if let Some(min) = s.strip_suffix("min") {
return min.trim().parse::<f64>().ok().map(|v| v * 60.0);
}
s.strip_suffix('s').unwrap_or(s).trim().parse().ok()
}
fn number_unit(s: &str) -> Option<(f64, &str)> {
let s = s.trim();
let end = s.find(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-' || c == '+')).unwrap_or(s.len());
Some((s[..end].parse().ok()?, s[end..].trim()))
}
fn between(a: &str, b: &str, f: f64) -> String {
match (number_unit(a), number_unit(b)) {
(Some((x, u)), Some((y, w))) if u == w || w.is_empty() => format!("{}{u}", super::pgf_number(x + (y - x) * f)),
_ if f <= 0.0 => a.to_string(),
_ if f >= 1.0 => b.to_string(),
_ => format!("{b}!{}!{a}", super::pgf_number(f * 100.0)),
}
}
fn option_name(attribute: &str) -> &str {
match attribute {
"position" | "translate" => "shift",
"rotate" | "xshift" | "yshift" | "shift" | "scale" | "xscale" | "yscale" | "xslant" | "yslant" => attribute,
"fill" | "draw" | "color" | "text" | "line width" | "opacity" | "fill opacity" | "draw opacity" | "text opacity" | "dash pattern" | "dash phase" => attribute,
other => other,
}
}
impl Engine {
pub fn set_snapshot(&mut self, v: &str) {
self.st.snapshot = seconds(v);
}
pub fn animation_option(&mut self, attribute: &str, spec: Vec<Token>) -> Result<Option<Vec<Token>>, Error> {
let attribute = attribute.rsplit(':').next().unwrap_or(attribute).trim().to_string();
let Some(t) = self.st.snapshot else {
self.animation_room(&attribute, &spec);
return Ok(None);
};
let mut times: Vec<(f64, String)> = Vec::new();
let mut begin = 0.0;
let mut freeze = false;
for item in crate::keys::split_list(&spec) {
let value = item.value.map(|v| tex::tokens_to_string(&v)).unwrap_or_default();
let value = value.trim().trim_matches('"').to_string();
match item.key.as_str() {
"begin" => begin = seconds(&value).unwrap_or(0.0),
"begin on" => return Ok(None),
"begin snapshot" => begin = seconds(&value).unwrap_or(0.0),
"freeze" => freeze = true,
"end" | "repeats" | "forever" | "restart" | "remove" | "whom" | "entry control" | "exit control" | "stay" | "jump" => {}
key => {
if let Some(s) = seconds(key) {
times.push((s, value));
}
}
}
}
if times.is_empty() {
return Ok(None);
}
times.sort_by(|a, b| a.0.total_cmp(&b.0));
let at = t - begin;
if at > times[times.len() - 1].0 && !freeze {
return Ok(None);
}
let value = match times.iter().position(|(s, _)| *s > at) {
Some(0) => times[0].1.clone(),
None => times[times.len() - 1].1.clone(),
Some(i) => {
let ((s0, a), (s1, b)) = (×[i - 1], ×[i]);
between(a, b, if s1 > s0 { (at - s0) / (s1 - s0) } else { 1.0 })
}
};
let mut v = tikra_tokens(&format!("{}=", option_name(&attribute)));
v.push(Token::Char('{', Cat::Begin));
v.extend(tikra_tokens(&value));
v.push(Token::Char('}', Cat::End));
Ok(Some(v))
}
fn animation_room(&mut self, attribute: &str, spec: &[Token]) {
if !matches!(attribute, "xshift" | "yshift" | "shift" | "position" | "translate") {
return;
}
let linear = Transform { e: 0.0, f: 0.0, ..self.st.transform };
for item in crate::keys::split_list(spec) {
if seconds(&item.key).is_none() {
continue;
}
let value = item.value.map(|v| tex::tokens_to_string(&v)).unwrap_or_default();
let value = value.trim().trim_matches('"').trim().to_string();
let d = match attribute {
"xshift" => self.length(&value).map(|x| Point::new(x, 0.0)),
"yshift" => self.length(&value).map(|y| Point::new(0.0, y)),
_ => {
let origin = self.st.transform.apply(Point::default());
self.coordinate(value.trim_start_matches('(').trim_end_matches(')')).map(|c| c.p.minus(origin))
}
};
if let Ok(d) = d {
self.anim_shifts.push(if attribute == "xshift" || attribute == "yshift" { linear.apply(d) } else { d });
}
}
}
pub fn read_animation(&mut self) -> Result<Option<Vec<Token>>, Error> {
let mut name = String::new();
loop {
match self.tex.get_token() {
Some(t) if t.is_char('=') => break,
Some(Token::Char(c, _)) => name.push(c),
Some(t) => name.push_str(&tex::show_token(&t)),
None => return Ok(None),
}
}
let spec = match self.tex.next_nonspace() {
Some(Token::Char(_, Cat::Begin)) => self.tex.read_group_body()?,
Some(t) => vec![t],
None => Vec::new(),
};
self.animation_option(name.trim(), spec)
}
pub fn animate_key(&mut self, v: Vec<Token>) -> Result<(), Error> {
for item in crate::keys::split_list(&v) {
match item.value {
Some(value) if item.key.contains(':') => {
if let Some(o) = self.animation_option(&item.key, value)? {
self.process_keys(o, Some("/tikz"))?;
}
}
value => {
let mut keys = tikra_tokens(&format!("/tikz/animate/{}", item.key));
if let Some(value) = value {
keys.push(Token::other('='));
keys.push(Token::Char('{', Cat::Begin));
keys.extend(value);
keys.push(Token::Char('}', Cat::End));
}
self.process_keys(keys, Some("/tikz"))?;
}
}
}
Ok(())
}
}