Skip to main content

csp_format/
animation.rs

1//! Animation (R17): timelines, tracks, keyframes, cels, the 2D camera and
2//! onion skin settings, how a frame is evaluated, and edits.
3//!
4//! Tables (all optional; CSP adds them when a file first uses animation):
5//! `AnimationCutBank` (which timeline is current), `TimeLine` (one row per
6//! timeline, linked by `NextTimeLine`), `Track` (one row per animated layer
7//! and timeline, linked by `TrackNextIndex` from `TimeLine.FirstTrack`) and
8//! `TimeLineLabel`. A track's keys are in two "cmt " documents
9//! ([`crate::cmt`]) in external chunks: `TrackActionMixer` (version 0100,
10//! single precision) and `TrackActionMixer2` (0110, double precision), with
11//! the same content. `TrackValueMap` holds the values at the current frame.
12//!
13//! Time: a timeline frame `f` (0-based) is at `t = f · 60 / FrameRate`
14//! ticks. Each track has one clip mapping timeline ticks to motion ticks,
15//! `m = MS + (t − TS) · (ME − MS) / (TE − TS)` (`TimeClip` = TS..TE,
16//! `MotionClip` = MS..ME), and its curves are keyed in motion ticks.
17//!
18//! Measured against CSP's image-sequence export (`docs/validation/V6c-animation.md`):
19//! interpolation `Constant` holds a key until the next one, `Linear` moves
20//! at a constant speed, `Default` (CSP's smooth interpolation, also used
21//! when a curve has no `Interp` list) eases with a cubic Hermite through
22//! the slopes (0 in every key seen so far), a keyframed layer is not shown
23//! before its clip starts,
24//! layer keyframes move `TimeLineOriginalMipmap` by
25//! `q = Position + R(rotation) · Scale · (p − Center)` (rotation in degrees,
26//! clockwise on screen), and opacity keys are percentages applied as
27//! `round(v · 256 / 100) / 256`. See `docs/spec/08-animation.md`.
28
29use std::collections::{HashMap, HashSet};
30
31use rusqlite::types::Value as SqlValue;
32
33use crate::binary::{Reader, Writer};
34use crate::clip::ClipFile;
35use crate::cmt::{self, Document, Node, Value};
36use crate::database::{Row, select_all, table_exists, value_as_text};
37use crate::error::{Error, Result, bail};
38use crate::model::LayerNode;
39
40/// `LayerType` of light-table reference layers. They are hidden in the
41/// layer palette, only shown on the light table, and never rendered.
42pub const LAYER_TYPE_LIGHT_TABLE: i64 = 10016;
43/// `LayerType` of a 2D camera folder.
44pub const LAYER_TYPE_CAMERA: i64 = 512;
45/// Timeline ticks per second.
46pub const TICKS_PER_SECOND: f64 = 60.0;
47
48/// What a track animates (`Track.TrackKind`).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
50#[non_exhaustive]
51pub enum TrackKind {
52    /// 2000: an animation folder's cel assignment.
53    Cels,
54    /// 2001: a layer's keyframes (position, scale, rotation, opacity).
55    Keyframes,
56    /// 2003: the paper.
57    Paper,
58    /// 2005: a 2D camera folder.
59    Camera,
60    /// Another `TrackKind` value.
61    Other(i64),
62}
63
64impl TrackKind {
65    /// The kind of a stored `TrackKind` value.
66    pub fn from_code(code: i64) -> Self {
67        match code {
68            2000 => TrackKind::Cels,
69            2001 => TrackKind::Keyframes,
70            2003 => TrackKind::Paper,
71            2005 => TrackKind::Camera,
72            other => TrackKind::Other(other),
73        }
74    }
75}
76
77/// How a key moves to the next one (`FCurve/Interp`).
78#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
79#[non_exhaustive]
80pub enum Interp {
81    /// `Constant`: hold until the next key.
82    Constant,
83    /// `Default`: CSP's smooth interpolation (cubic Hermite through the slopes).
84    Smooth,
85    /// `Linear`: CSP's 等速補間.
86    Linear,
87    /// Any other name; evaluated as linear and reported.
88    Other(String),
89}
90
91impl Interp {
92    /// The interpolation named `name` in a mixer.
93    pub fn from_name(name: &str) -> Self {
94        match name {
95            "Constant" => Interp::Constant,
96            "Default" => Interp::Smooth,
97            "Linear" => Interp::Linear,
98            other => Interp::Other(other.to_owned()),
99        }
100    }
101
102    /// The name stored in a mixer.
103    pub fn name(&self) -> &str {
104        match self {
105            Interp::Constant => "Constant",
106            Interp::Smooth => "Default",
107            Interp::Linear => "Linear",
108            Interp::Other(name) => name,
109        }
110    }
111}
112
113/// One key of a curve.
114#[derive(Debug, Clone, PartialEq, serde::Serialize)]
115pub struct Key {
116    /// Motion ticks.
117    pub frame: f64,
118    /// The value at the key.
119    pub value: f64,
120    /// Slope on the left of the key (0 in every file seen).
121    pub left_slope: f64,
122    /// Slope on the right of the key (0 in every file seen).
123    pub right_slope: f64,
124    /// How the value moves to the next key.
125    pub interp: Interp,
126    /// Cel curves: the cel's layer name.
127    pub tag: Option<String>,
128}
129
130/// One animated value (`FCurve`): `Type` and, for pairs, `Axis` X or Y.
131#[derive(Debug, Clone, PartialEq, serde::Serialize)]
132pub struct Curve {
133    /// `Type` of the curve (`Position`, `Scale`, `Rotation`, `Opacity`, …).
134    pub kind: String,
135    /// `Axis` (`X` or `Y`) of a pair's curve.
136    pub axis: Option<String>,
137    /// The keys, in time order.
138    pub keys: Vec<Key>,
139}
140
141impl Curve {
142    fn from_node(node: &Node) -> Result<Self> {
143        let kind = node.attribute("Type").unwrap_or_default().to_owned();
144        let axis = node.attribute("Axis").map(str::to_owned);
145        let numbers = |name: &str| node.child(name).and_then(|n| n.value.as_numbers());
146        let frames = numbers("Frame").ok_or_else(|| Error::format(format!("FCurve {kind} has no Frame")))?;
147        let values = numbers("Value").ok_or_else(|| Error::format(format!("FCurve {kind} has no Value")))?;
148        if values.len() != frames.len() {
149            bail!("FCurve {kind}: {} frames, {} values", frames.len(), values.len());
150        }
151        let left = numbers("LeftSlope");
152        let right = numbers("RightSlope");
153        let interp = node.child("Interp").and_then(|n| n.value.as_strings());
154        let tags = node.child("Tag").and_then(|n| n.value.as_strings());
155        let keys = frames
156            .iter()
157            .zip(&values)
158            .enumerate()
159            .map(|(i, (frame, value))| Key {
160                frame: *frame,
161                value: *value,
162                left_slope: left.as_ref().and_then(|v| v.get(i)).copied().unwrap_or(0.0),
163                right_slope: right.as_ref().and_then(|v| v.get(i)).copied().unwrap_or(0.0),
164                interp: interp.and_then(|v| v.get(i)).map_or(Interp::Smooth, |name| Interp::from_name(name)),
165                tag: tags.and_then(|v| v.get(i)).cloned(),
166            })
167            .collect();
168        Ok(Curve { kind, axis, keys })
169    }
170
171    /// The value at motion time `m` and the index of the key in effect
172    /// (None before the first key). Before the first key the first value
173    /// holds, after the last the last.
174    pub fn evaluate(&self, m: f64) -> Option<(f64, Option<usize>)> {
175        let first = self.keys.first()?;
176        if m < first.frame {
177            return Some((first.value, None));
178        }
179        let index = self.keys.iter().rposition(|key| key.frame <= m).unwrap_or(0);
180        let key = &self.keys[index];
181        let Some(next) = self.keys.get(index + 1) else { return Some((key.value, Some(index))) };
182        let span = next.frame - key.frame;
183        if span <= 0.0 {
184            return Some((key.value, Some(index)));
185        }
186        let u = (m - key.frame) / span;
187        let value = match key.interp {
188            Interp::Constant => key.value,
189            Interp::Smooth => {
190                let (u2, u3) = (u * u, u * u * u);
191                let h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
192                let h10 = u3 - 2.0 * u2 + u;
193                let h01 = -2.0 * u3 + 3.0 * u2;
194                let h11 = u3 - u2;
195                h00 * key.value + h10 * span * key.right_slope + h01 * next.value + h11 * span * next.left_slope
196            }
197            Interp::Linear | Interp::Other(_) => key.value + (next.value - key.value) * u,
198        };
199        Some((value, Some(index)))
200    }
201}
202
203/// A `Param` node: the value a track shows without keys, and its name.
204#[derive(Debug, Clone, PartialEq, serde::Serialize)]
205pub struct Param {
206    /// `Name` attribute (the value kind, as the curves' `kind`).
207    pub name: String,
208    /// `Tag` attribute, if any.
209    pub tag: Option<String>,
210    /// The value.
211    pub value: Value,
212}
213
214/// The typed content of a track mixer.
215#[derive(Debug, Clone, PartialEq, serde::Serialize)]
216pub struct Clip {
217    /// `TimeClip` Start..End in timeline ticks.
218    pub time: (f64, f64),
219    /// `MotionClip` Start..End in motion ticks.
220    pub motion: (f64, f64),
221    /// `Param` nodes of `AnimInfo`: values without keys.
222    pub params: Vec<Param>,
223    /// `FCurve` nodes of `AnimInfo`.
224    pub curves: Vec<Curve>,
225}
226
227impl Clip {
228    /// Reads the clip of a track mixer document.
229    pub fn from_document(document: &Document) -> Result<Self> {
230        let clip = document.action_clip().ok_or_else(|| Error::format("Track mixer has no ActionNodeClip"))?;
231        let number = |path: &[&str]| clip.find(path).and_then(|n| n.value.as_f64()).unwrap_or(0.0);
232        let mut params = Vec::new();
233        let mut curves = Vec::new();
234        for node in clip.child("AnimInfo").map(|n| n.children.as_slice()).unwrap_or_default() {
235            match node.name.as_str() {
236                "Param" => params.push(Param {
237                    name: node.attribute("Name").unwrap_or_default().to_owned(),
238                    tag: node.attribute("Tag").map(str::to_owned),
239                    value: node.value.clone(),
240                }),
241                "FCurve" => curves.push(Curve::from_node(node)?),
242                _ => {}
243            }
244        }
245        Ok(Clip {
246            time: (number(&["TimeClip", "Start"]), number(&["TimeClip", "End"])),
247            motion: (number(&["MotionClip", "Start"]), number(&["MotionClip", "End"])),
248            params,
249            curves,
250        })
251    }
252
253    /// Motion ticks at timeline ticks `t`.
254    pub fn motion_at(&self, t: f64) -> f64 {
255        let (ts, te) = self.time;
256        let (ms, me) = self.motion;
257        if te == ts { ms } else { ms + (t - ts) * (me - ms) / (te - ts) }
258    }
259
260    /// Timeline ticks at motion ticks `m` (inverse of [`Clip::motion_at`]).
261    pub fn time_at(&self, m: f64) -> f64 {
262        let (ts, te) = self.time;
263        let (ms, me) = self.motion;
264        if me == ms { ts } else { ts + (m - ms) * (te - ts) / (me - ms) }
265    }
266
267    /// The curve of this kind and axis.
268    pub fn curve(&self, kind: &str, axis: Option<&str>) -> Option<&Curve> {
269        self.curves.iter().find(|c| c.kind == kind && c.axis.as_deref() == axis)
270    }
271
272    /// The `Param` of this name.
273    pub fn param(&self, name: &str) -> Option<&Param> {
274        self.params.iter().find(|p| p.name == name)
275    }
276
277    /// A value at timeline ticks `t`: the curve if there is one, else the
278    /// `Param` (`axis` picks X or Y of a pair).
279    pub fn value(&self, kind: &str, axis: Option<&str>, t: f64) -> Option<f64> {
280        if let Some((value, _)) = self.curve(kind, axis).and_then(|c| c.evaluate(self.motion_at(t))) {
281            return Some(value);
282        }
283        let value = &self.param(kind)?.value;
284        match axis {
285            Some("X") => value.as_pair().map(|p| p[0]),
286            Some("Y") => value.as_pair().map(|p| p[1]),
287            _ => value.as_f64(),
288        }
289    }
290
291    fn pair(&self, kind: &str, t: f64) -> Option<[f64; 2]> {
292        Some([self.value(kind, Some("X"), t)?, self.value(kind, Some("Y"), t)?])
293    }
294}
295
296/// One entry of `Track.TrackValueMap` (big endian: u32 size, UTF-16 name,
297/// UTF-16 tag, u32 type, value).
298#[derive(Debug, Clone, PartialEq, serde::Serialize)]
299pub struct MapEntry {
300    /// Entry name.
301    pub name: String,
302    /// Entry tag.
303    pub tag: String,
304    /// The value.
305    pub value: MapValue,
306}
307
308/// Value of a [`MapEntry`], by type code.
309#[derive(Debug, Clone, PartialEq, serde::Serialize)]
310pub enum MapValue {
311    /// Type 0.
312    Double(f64),
313    /// Type 2.
314    Int(i32),
315    /// Type 3.
316    Pair([f64; 2]),
317    /// Another type: its code and raw bytes.
318    Raw(u32, Vec<u8>),
319}
320
321/// Decode `Track.TrackValueMap`.
322pub fn parse_value_map(data: &[u8]) -> Result<Vec<MapEntry>> {
323    let mut reader = Reader::new(data);
324    let header = reader.be_u32()?;
325    if header != 8 {
326        bail!("TrackValueMap header {header}");
327    }
328    let count = reader.be_u32()? as usize;
329    let mut entries = Vec::with_capacity(count.min(64));
330    for _ in 0..count {
331        let start = reader.position();
332        let size = reader.be_u32()? as usize;
333        let end = start + size;
334        let name = utf16(&mut reader)?;
335        let tag = utf16(&mut reader)?;
336        let kind = reader.be_u32()?;
337        let value = match kind {
338            0 => MapValue::Double(reader.be_f64()?),
339            2 => MapValue::Int(reader.be_i32()?),
340            3 => MapValue::Pair([reader.be_f64()?, reader.be_f64()?]),
341            other => {
342                let rest = end.checked_sub(reader.position()).ok_or_else(|| Error::format("TrackValueMap entry"))?;
343                MapValue::Raw(other, reader.bytes(rest)?.to_vec())
344            }
345        };
346        if reader.position() != end {
347            bail!("TrackValueMap entry {name:?} is {} bytes, says {size}", reader.position() - start);
348        }
349        entries.push(MapEntry { name, tag, value });
350    }
351    Ok(entries)
352}
353
354/// Encode `Track.TrackValueMap`.
355pub fn value_map_bytes(entries: &[MapEntry]) -> Vec<u8> {
356    let mut out = Writer::new();
357    out.be_u32(8).be_u32(entries.len() as u32);
358    for entry in entries {
359        let mut body = Writer::new();
360        put_utf16(&mut body, &entry.name);
361        put_utf16(&mut body, &entry.tag);
362        match &entry.value {
363            MapValue::Double(v) => {
364                body.be_u32(0).be_f64(*v);
365            }
366            MapValue::Int(v) => {
367                body.be_u32(2).be_i32(*v);
368            }
369            MapValue::Pair([x, y]) => {
370                body.be_u32(3).be_f64(*x).be_f64(*y);
371            }
372            MapValue::Raw(kind, bytes) => {
373                body.be_u32(*kind).bytes(bytes);
374            }
375        }
376        let body = body.into_inner();
377        out.be_u32(body.len() as u32 + 4).bytes(&body);
378    }
379    out.into_inner()
380}
381
382fn utf16(reader: &mut Reader) -> Result<String> {
383    let units = reader.be_u32()? as usize;
384    let bytes = reader.bytes(units.checked_mul(2).ok_or_else(|| Error::format("UTF-16 length"))?)?;
385    let units: Vec<u16> = bytes.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes(*c)).collect();
386    Ok(String::from_utf16_lossy(&units))
387}
388
389fn put_utf16(out: &mut Writer, text: &str) {
390    let units: Vec<u16> = text.encode_utf16().collect();
391    out.be_u32(units.len() as u32);
392    for unit in units {
393        out.be_u16(unit);
394    }
395}
396
397/// A `Track` row with its mixers.
398#[derive(Debug, Clone, serde::Serialize)]
399pub struct Track {
400    /// `MainId` of the row.
401    pub main_id: i64,
402    /// `ItemId`: the timeline's `MainId`.
403    pub timeline: i64,
404    /// What the track animates (`TrackKind`).
405    pub kind: TrackKind,
406    /// `LayerUuidWithTrack` as 32 lower-case hex digits.
407    pub layer_uuid: String,
408    /// The layer it animates (by uuid), if found.
409    pub layer_id: Option<i64>,
410    /// `TrackOptionFlag` as stored.
411    pub option_flag: i64,
412    /// Labels on the track (`TrackLabelFirstIndex` list).
413    pub labels: Vec<Label>,
414    /// From `TrackActionMixer2` (or `TrackActionMixer` when that is absent).
415    pub clip: Option<Clip>,
416    /// The values at the current frame (`TrackValueMap`).
417    pub value_map: Vec<MapEntry>,
418    /// The decoded mixer documents: `TrackActionMixer` and `TrackActionMixer2` (external chunk id and document).
419    #[serde(skip)]
420    pub mixers: [Option<(String, Document)>; 2],
421}
422
423/// A `TimeLineLabel`: a timeline label (type 0), an in-between mark ○ on a
424/// cel track (type 1) or a named track label with a length (type 3).
425#[derive(Debug, Clone, PartialEq, serde::Serialize)]
426pub struct Label {
427    /// `MainId` of the row.
428    pub main_id: i64,
429    /// Label text.
430    pub name: String,
431    /// 0-based frame.
432    pub frame: f64,
433    /// Type: 0 timeline label, 1 in-between mark, 3 named track label.
434    pub kind: i64,
435    /// Length in frames (type 3).
436    pub length: Option<f64>,
437}
438
439/// A `TimeLine` row.
440#[derive(Debug, Clone, serde::Serialize)]
441pub struct Timeline {
442    /// `MainId` of the row.
443    pub main_id: i64,
444    /// Name shown in the timeline palette.
445    pub name: String,
446    /// Frames per second.
447    pub frame_rate: f64,
448    /// 0-based first frame.
449    pub start_frame: f64,
450    /// 0-based frame after the last.
451    pub end_frame: f64,
452    /// 0-based frame shown in CSP.
453    pub current_frame: f64,
454    /// タイムラインの区切り線 (`GuidelineFrameRate`).
455    pub guideline: i64,
456    /// シーン number.
457    pub scene: i64,
458    /// カット number.
459    pub cut: i64,
460    /// `TimeLineUuid` as 32 hex digits.
461    pub uuid: Option<String>,
462    /// Timeline labels.
463    pub labels: Vec<Label>,
464    /// Tracks, in their linked order.
465    pub tracks: Vec<Track>,
466}
467
468impl Timeline {
469    /// Timeline ticks of 0-based frame `frame`.
470    pub fn ticks(&self, frame: f64) -> f64 {
471        frame * TICKS_PER_SECOND / self.frame_rate.max(1e-9)
472    }
473
474    /// The track animating this layer.
475    pub fn track_for_layer(&self, layer_id: i64) -> Option<&Track> {
476        self.tracks.iter().find(|t| t.layer_id == Some(layer_id))
477    }
478}
479
480/// オニオンスキン settings (`Canvas.OnionSkin*`).
481#[derive(Debug, Clone, PartialEq, serde::Serialize)]
482pub struct OnionSkin {
483    /// Onion skin is on (`OnionSkinStatus`).
484    pub enabled: bool,
485    /// Previous frames shown.
486    pub previous: i64,
487    /// Next frames shown.
488    pub next: i64,
489    /// Fade the frames (`OnionSkinUseOpacity`).
490    pub use_opacity: bool,
491    /// Percent.
492    pub start_opacity: i64,
493    /// Opacity step in percent (`OnionSkinStepOpaciy`, misspelled in CSP).
494    pub step_opacity: i64,
495    /// Tint the frames with the colours below.
496    pub use_color: bool,
497    /// `OnionSkinHalfColor`.
498    pub half_color: bool,
499    /// Previous frames' colour.
500    pub front_color: [u8; 3],
501    /// Next frames' colour.
502    pub behind_color: [u8; 3],
503}
504
505/// The 基準フレーム (reference frame): the area an image-sequence export
506/// shows, in canvas pixels.
507#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
508pub struct Frame {
509    /// Left edge.
510    pub x: f64,
511    /// Top edge.
512    pub y: f64,
513    /// Width.
514    pub width: f64,
515    /// Height.
516    pub height: f64,
517}
518
519/// All animation data of a file.
520#[derive(Debug, Clone, serde::Serialize)]
521pub struct Animation {
522    /// Timelines in their linked order.
523    pub timelines: Vec<Timeline>,
524    /// Index (0-based, in [`Animation::timelines`]) of the current timeline.
525    pub current: usize,
526    /// オニオンスキン settings, when the canvas has them.
527    pub onion_skin: Option<OnionSkin>,
528    /// The 基準フレーム.
529    pub frame: Frame,
530}
531
532impl Animation {
533    /// None when the file has no timeline.
534    pub fn read(file: &ClipFile) -> Result<Option<Self>> {
535        let db = file.database();
536        if !table_exists(db, "TimeLine")? {
537            return Ok(None);
538        }
539        let timeline_rows: HashMap<i64, Row> =
540            select_all(db, "TimeLine")?.into_iter().map(|row| (row.int("MainId"), row)).collect();
541        if timeline_rows.is_empty() {
542            return Ok(None);
543        }
544        let track_rows: HashMap<i64, Row> = if table_exists(db, "Track")? {
545            select_all(db, "Track")?.into_iter().map(|row| (row.int("MainId"), row)).collect()
546        } else {
547            HashMap::new()
548        };
549        let label_rows: HashMap<i64, Row> = if table_exists(db, "TimeLineLabel")? {
550            select_all(db, "TimeLineLabel")?.into_iter().map(|row| (row.int("MainId"), row)).collect()
551        } else {
552            HashMap::new()
553        };
554        let bank = if table_exists(db, "AnimationCutBank")? {
555            select_all(db, "AnimationCutBank")?.into_iter().next()
556        } else {
557            None
558        };
559        let mut layer_by_uuid = HashMap::new();
560        for layer in &file.layers.layers {
561            if let Some(uuid) = &layer.uuid {
562                layer_by_uuid.insert(uuid.replace('-', "").to_ascii_lowercase(), layer.main_id);
563            }
564        }
565        let labels = |first: i64| -> Result<Vec<Label>> {
566            let mut out = Vec::new();
567            let mut next = first;
568            while next != 0 {
569                if out.len() > label_rows.len() {
570                    bail!("TimeLineLabel list loops");
571                }
572                let row = label_rows.get(&next).ok_or_else(|| Error::format(format!("No TimeLineLabel {next}")))?;
573                out.push(Label {
574                    main_id: next,
575                    name: row.text("LabelName").unwrap_or_default(),
576                    frame: row.real("LabelFrame"),
577                    kind: row.int("LabelType"),
578                    length: row.opt_real("LabelLength"),
579                });
580                next = row.int("LabelNextIndex");
581            }
582            Ok(out)
583        };
584        let first = bank.as_ref().map(|b| b.int("FirstTimeLine")).filter(|id| *id != 0).unwrap_or_else(|| {
585            // Without a bank: the timeline no other one points to.
586            let pointed: HashSet<i64> = timeline_rows.values().map(|r| r.int("NextTimeLine")).collect();
587            timeline_rows.keys().copied().filter(|id| !pointed.contains(id)).min().unwrap_or(0)
588        });
589        let mut timelines = Vec::new();
590        let mut next = first;
591        while next != 0 {
592            if timelines.len() > timeline_rows.len() {
593                bail!("TimeLine list loops");
594            }
595            let row = timeline_rows.get(&next).ok_or_else(|| Error::format(format!("No TimeLine {next}")))?;
596            let mut tracks = Vec::new();
597            let mut track_id = row.int("FirstTrack");
598            while track_id != 0 {
599                if tracks.len() > track_rows.len() {
600                    bail!("Track list loops");
601                }
602                let track = track_rows.get(&track_id).ok_or_else(|| Error::format(format!("No Track {track_id}")))?;
603                tracks.push(read_track(file, track, &layer_by_uuid, &labels)?);
604                track_id = track.int("TrackNextIndex");
605            }
606            timelines.push(Timeline {
607                main_id: next,
608                name: row.text("TimeLineName").unwrap_or_default(),
609                frame_rate: row.real("FrameRate"),
610                start_frame: row.real("StartFrame"),
611                end_frame: row.real("EndFrame"),
612                current_frame: row.real("CurrentFrame"),
613                guideline: row.int("GuidelineFrameRate"),
614                scene: row.int("SceneIndexForName"),
615                cut: row.int("CutIndexForName"),
616                uuid: row.get("TimeLineUuid").map(uuid_text),
617                labels: labels(row.int("LabelFirstIndex"))?,
618                tracks,
619            });
620            next = row.int("NextTimeLine");
621        }
622        let current = bank.map(|b| b.int("CurrentIndex")).unwrap_or(0).clamp(0, timelines.len() as i64 - 1) as usize;
623        Ok(Some(Animation { timelines, current, onion_skin: onion_skin(&file.canvas.row), frame: frame(file) }))
624    }
625
626    /// The timeline at [`Animation::current`].
627    pub fn current_timeline(&self) -> Option<&Timeline> {
628        self.timelines.get(self.current)
629    }
630
631    /// Find a timeline by `MainId`.
632    pub fn timeline(&self, main_id: i64) -> Option<&Timeline> {
633        self.timelines.iter().find(|t| t.main_id == main_id)
634    }
635}
636
637fn read_track(
638    file: &ClipFile,
639    row: &Row,
640    layer_by_uuid: &HashMap<String, i64>,
641    labels: &dyn Fn(i64) -> Result<Vec<Label>>,
642) -> Result<Track> {
643    let layer_uuid = row.get("LayerUuidWithTrack").map(uuid_text).unwrap_or_default();
644    let mut mixers: [Option<(String, Document)>; 2] = [None, None];
645    for (slot, column) in ["TrackActionMixer", "TrackActionMixer2"].iter().enumerate() {
646        let Some(id) = row.get(column).and_then(value_as_text) else { continue };
647        let Some(payload) = file.external(&id) else { continue };
648        let document = Document::parse(&cmt::unpack(payload)?)?;
649        mixers[slot] = Some((id, document));
650    }
651    let clip = match mixers[1].as_ref().or(mixers[0].as_ref()) {
652        Some((_, document)) => Some(Clip::from_document(document)?),
653        None => None,
654    };
655    let value_map = match row.blob("TrackValueMap") {
656        Some(data) if !data.is_empty() => parse_value_map(data)?,
657        _ => Vec::new(),
658    };
659    Ok(Track {
660        main_id: row.int("MainId"),
661        timeline: row.int("ItemId"),
662        kind: TrackKind::from_code(row.int("TrackKind")),
663        layer_id: layer_by_uuid.get(&layer_uuid).copied(),
664        layer_uuid,
665        option_flag: row.int("TrackOptionFlag"),
666        labels: labels(row.int("TrackLabelFirstIndex"))?,
667        clip,
668        value_map,
669        mixers,
670    })
671}
672
673/// A uuid column (16 bytes, or text) as 32 lower-case hex digits.
674fn uuid_text(value: &SqlValue) -> String {
675    match value {
676        SqlValue::Blob(bytes) => bytes.iter().map(|b| format!("{b:02x}")).collect(),
677        other => value_as_text(other).unwrap_or_default().replace('-', "").to_ascii_lowercase(),
678    }
679}
680
681/// CSP stores the onion skin colours as `c · 0x01010101`.
682fn channel(row: &Row, column: &str) -> u8 {
683    (row.int(column) as u32 & 0xff) as u8
684}
685
686fn onion_skin(row: &Row) -> Option<OnionSkin> {
687    if !row.has("OnionSkinStatus") {
688        return None;
689    }
690    Some(OnionSkin {
691        enabled: row.truthy("OnionSkinStatus"),
692        previous: row.int("OnionSkinPrevDisplayCount"),
693        next: row.int("OnionSkinNextDisplayCount"),
694        use_opacity: row.truthy("OnionSkinUseOpacity"),
695        start_opacity: row.int("OnionSkinStartOpacity"),
696        step_opacity: row.int("OnionSkinStepOpaciy"),
697        use_color: row.truthy("OnionSkinUseColor"),
698        half_color: row.truthy("OnionSkinHalfColor"),
699        front_color: [
700            channel(row, "OnionSkinFrontColorRed"),
701            channel(row, "OnionSkinFrontColorGreen"),
702            channel(row, "OnionSkinFrontColorBlue"),
703        ],
704        behind_color: [
705            channel(row, "OnionSkinBehindColorRed"),
706            channel(row, "OnionSkinBehindColorGreen"),
707            channel(row, "OnionSkinBehindColorBlue"),
708        ],
709    })
710}
711
712/// The reference frame: `CropFrameInner*` centred in the crop frame
713/// (`CropFrameWidth/Height`, centred on the canvas and moved by
714/// `CropFrameCropOffset`), moved by `CropFrameInnerOffset`, all in
715/// `CropFrameUnitKind` units. Without a crop frame it is the whole canvas.
716fn frame(file: &ClipFile) -> Frame {
717    let row = &file.canvas.row;
718    let unit = crate::model::Unit::from_code(row.int("CropFrameUnitKind"));
719    let px = |column: &str| unit.to_pixels(row.real(column), file.canvas.resolution);
720    let (cw, ch) = (file.canvas.width_px as f64, file.canvas.height_px as f64);
721    let crop = (px("CropFrameWidth"), px("CropFrameHeight"));
722    let inner = (px("CropFrameInnerWidth"), px("CropFrameInnerHeight"));
723    if crop.0 <= 0.0 || crop.1 <= 0.0 {
724        return Frame { x: 0.0, y: 0.0, width: cw, height: ch };
725    }
726    let crop_x = (cw - crop.0) / 2.0 + px("CropFrameCropOffsetX");
727    let crop_y = (ch - crop.1) / 2.0 + px("CropFrameCropOffsetY");
728    let (width, height) = if inner.0 > 0.0 && inner.1 > 0.0 { inner } else { crop };
729    Frame {
730        x: crop_x + (crop.0 - width) / 2.0 + px("CropFrameInnerOffsetX"),
731        y: crop_y + (crop.1 - height) / 2.0 + px("CropFrameInnerOffsetY"),
732        width,
733        height,
734    }
735}
736
737/// A 2D affine map `p ↦ (a·x + b·y + c, d·x + e·y + f)`.
738#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
739pub struct Affine(pub [f64; 6]);
740
741impl Affine {
742    /// The identity map.
743    pub const IDENTITY: Affine = Affine([1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
744
745    /// Maps a point.
746    pub fn apply(&self, (x, y): (f64, f64)) -> (f64, f64) {
747        let [a, b, c, d, e, f] = self.0;
748        (a * x + b * y + c, d * x + e * y + f)
749    }
750
751    /// The inverse map; None when it is singular.
752    pub fn inverse(&self) -> Option<Affine> {
753        let [a, b, c, d, e, f] = self.0;
754        let det = a * e - b * d;
755        if det.abs() < 1e-12 {
756            return None;
757        }
758        let (ia, ib, id, ie) = (e / det, -b / det, -d / det, a / det);
759        Some(Affine([ia, ib, -(ia * c + ib * f), id, ie, -(id * c + ie * f)]))
760    }
761
762    /// `self ∘ other` (apply `other` first).
763    pub fn then_after(&self, other: &Affine) -> Affine {
764        let [a, b, c, d, e, f] = self.0;
765        let [p, q, r, s, t, u] = other.0;
766        Affine([a * p + b * s, a * q + b * t, a * r + b * u + c, d * p + e * s, d * q + e * t, d * r + e * u + f])
767    }
768
769    /// `p ↦ position + R(rotation) · diag(scale) · (p − center)`; rotation
770    /// in degrees, clockwise on screen (y points down).
771    pub fn keyframe(center: [f64; 2], position: [f64; 2], scale: [f64; 2], rotation: f64) -> Affine {
772        let (sin, cos) = rotation.to_radians().sin_cos();
773        let (a, b, d, e) = (cos * scale[0], -sin * scale[1], sin * scale[0], cos * scale[1]);
774        Affine([a, b, position[0] - a * center[0] - b * center[1], d, e, position[1] - d * center[0] - e * center[1]])
775    }
776}
777
778/// A keyframed layer at one frame.
779#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
780pub struct LayerTransform {
781    /// Canvas position of an original pixel position.
782    pub map: Affine,
783    /// 0–256, as `LayerOpacity`.
784    pub opacity: Option<i64>,
785}
786
787/// What a frame changes in the layer tree: which layers are not drawn
788/// (cels not shown, light-table layers), keyframed layers' transforms and
789/// cameras' maps (frame position → canvas position of the folder's content).
790#[derive(Debug, Clone, Default, serde::Serialize)]
791pub struct FrameState {
792    /// Layers not drawn at this frame.
793    pub hidden: HashSet<i64>,
794    /// Keyframed layers' transforms, by layer id.
795    pub transforms: HashMap<i64, LayerTransform>,
796    /// 2D cameras' maps, by camera folder id.
797    pub cameras: HashMap<i64, Affine>,
798    /// The cel shown by each animation folder (folder id → cel layer id).
799    pub cels: HashMap<i64, Option<i64>>,
800    /// Interpolations evaluated as linear because they are not known.
801    pub unknown_interp: HashSet<String>,
802}
803
804/// Opacity percent → `LayerOpacity` (0–256).
805fn opacity_256(percent: f64) -> i64 {
806    (percent * 256.0 / 100.0).round().clamp(0.0, 256.0) as i64
807}
808
809impl Animation {
810    /// The state of 0-based frame `frame` of the timeline at `index`.
811    pub fn frame_state(&self, file: &ClipFile, index: usize, frame: f64) -> Result<FrameState> {
812        let timeline =
813            self.timelines.get(index).ok_or_else(|| Error::format(format!("No timeline at index {index}")))?;
814        let t = timeline.ticks(frame);
815        let mut state = FrameState::default();
816        let mut tracks: HashMap<i64, &Track> = HashMap::new();
817        for track in &timeline.tracks {
818            if let Some(id) = track.layer_id {
819                tracks.insert(id, track);
820            }
821        }
822        let mut stack: Vec<&LayerNode> = vec![&file.layers.root];
823        while let Some(node) = stack.pop() {
824            stack.extend(node.children.iter());
825            let layer = &node.layer;
826            if layer.layer_type == LAYER_TYPE_LIGHT_TABLE {
827                state.hidden.insert(layer.main_id);
828                continue;
829            }
830            let Some(track) = tracks.get(&layer.main_id) else {
831                if layer.row.truthy("AnimationFolder") {
832                    // A folder without a track in this timeline shows no cel.
833                    for child in &node.children {
834                        state.hidden.insert(child.layer.main_id);
835                    }
836                    state.cels.insert(layer.main_id, None);
837                }
838                continue;
839            };
840            let Some(clip) = &track.clip else { continue };
841            for curve in &clip.curves {
842                for key in &curve.keys {
843                    if let Interp::Other(name) = &key.interp {
844                        state.unknown_interp.insert(name.clone());
845                    }
846                }
847            }
848            match track.kind {
849                TrackKind::Cels => {
850                    let cel = clip
851                        .curve("ImageCelName", None)
852                        .and_then(|curve| {
853                            let (_, index) = curve.evaluate(clip.motion_at(t))?;
854                            curve.keys.get(index?)
855                        })
856                        .and_then(|key| key.tag.clone());
857                    let shown = cel.as_ref().and_then(|name| {
858                        node.children.iter().rev().find(|c| &c.layer.name == name).map(|c| c.layer.main_id)
859                    });
860                    for child in &node.children {
861                        if Some(child.layer.main_id) != shown {
862                            state.hidden.insert(child.layer.main_id);
863                        }
864                    }
865                    state.cels.insert(layer.main_id, shown);
866                }
867                TrackKind::Keyframes => {
868                    if !layer.row.truthy("TimeLineLayerKeyFrameEnabled") {
869                        continue;
870                    }
871                    // Outside its clip the layer is not shown (measured
872                    // before the clip; the end is taken as exclusive).
873                    if t < clip.time.0 || t >= clip.time.1 {
874                        state.hidden.insert(layer.main_id);
875                        continue;
876                    }
877                    let center = clip.pair("ImageCenter", t).unwrap_or([0.0, 0.0]);
878                    let position = clip.pair("ImagePosition", t).unwrap_or(center);
879                    let scale = clip.pair("ImageAspectScale", t).unwrap_or([100.0, 100.0]);
880                    let rotation = clip.value("ImageRotation", None, t).unwrap_or(0.0);
881                    state.transforms.insert(
882                        layer.main_id,
883                        LayerTransform {
884                            map: Affine::keyframe(center, position, [scale[0] / 100.0, scale[1] / 100.0], rotation),
885                            opacity: clip.value("Opacity", None, t).map(opacity_256),
886                        },
887                    );
888                }
889                TrackKind::Camera => {
890                    let center =
891                        clip.pair("ImageCenter", t).unwrap_or([self.frame.width / 2.0, self.frame.height / 2.0]);
892                    let position =
893                        clip.pair("ImagePosition", t).unwrap_or([self.frame.x + center[0], self.frame.y + center[1]]);
894                    let scale = clip.value("ImageScale", None, t).unwrap_or(100.0) / 100.0;
895                    let rotation = clip.value("ImageRotation", None, t).unwrap_or(0.0);
896                    // Frame coordinates → canvas, then the reference frame's
897                    // own placement undone: canvas → canvas.
898                    let camera = Affine::keyframe(center, position, [scale, scale], rotation);
899                    let reference = Affine([1.0, 0.0, -self.frame.x, 0.0, 1.0, -self.frame.y]);
900                    state.cameras.insert(layer.main_id, camera.then_after(&reference));
901                    if let Some(opacity) = clip.value("Opacity", None, t) {
902                        state.transforms.insert(
903                            layer.main_id,
904                            LayerTransform { map: Affine::IDENTITY, opacity: Some(opacity_256(opacity)) },
905                        );
906                    }
907                }
908                TrackKind::Paper | TrackKind::Other(_) => {}
909            }
910        }
911        Ok(state)
912    }
913}
914
915// -- edits ----------------------------------------------------------------
916
917impl ClipFile {
918    /// The animation data, None when the file has no timeline.
919    pub fn animation(&self) -> Result<Option<Animation>> {
920        Animation::read(self)
921    }
922
923    fn timeline_row(&self, timeline_id: i64) -> Result<Row> {
924        self.row("TimeLine", timeline_id)?.ok_or_else(|| Error::format(format!("No TimeLine {timeline_id}")))
925    }
926
927    /// Change a timeline's frame rate. Keys stay on their frames: CSP
928    /// stretches each track's `TimeClip` by old / new rate (measured).
929    pub fn set_frame_rate(&mut self, timeline_id: i64, frame_rate: f64) -> Result<()> {
930        if !(frame_rate > 0.0 && frame_rate <= 240.0) {
931            bail!("Frame rate {frame_rate} out of range");
932        }
933        let old = self.timeline_row(timeline_id)?.real("FrameRate");
934        if old <= 0.0 {
935            bail!("TimeLine {timeline_id} has no frame rate");
936        }
937        let factor = old / frame_rate;
938        let animation = self.animation()?.ok_or_else(|| Error::format("No animation"))?;
939        let timeline = animation.timeline(timeline_id).ok_or_else(|| Error::format("No timeline"))?;
940        for track in &timeline.tracks {
941            self.edit_mixers(track, |clip| {
942                for name in ["Start", "End"] {
943                    if let Some(node) = clip.find_mut(&["TimeClip", name])
944                        && let Value::Double(v) = &mut node.value
945                    {
946                        *v *= factor;
947                    }
948                }
949                Ok(())
950            })?;
951        }
952        self.database().execute("UPDATE TimeLine SET FrameRate = ?1 WHERE MainId = ?2", (frame_rate, timeline_id))?;
953        Ok(())
954    }
955
956    /// Change a timeline's playback range (0-based first frame, frame after
957    /// the last), as CSP's 再生フレーム settings store it.
958    pub fn set_frame_range(&mut self, timeline_id: i64, start: i64, end: i64) -> Result<()> {
959        if start < 0 || end <= start {
960            bail!("Frame range {start}..{end} is empty");
961        }
962        let row = self.timeline_row(timeline_id)?;
963        let biggest = row.real("BiggestEndFrame").max(end as f64);
964        let current = row.real("CurrentFrame").clamp(start as f64, (end - 1) as f64);
965        self.database().execute(
966            "UPDATE TimeLine SET StartFrame = ?1, EndFrame = ?2, BiggestEndFrame = ?3, CurrentFrame = ?4 WHERE MainId = ?5",
967            (start as f64, end as f64, biggest, current, timeline_id),
968        )?;
969        Ok(())
970    }
971
972    /// Replace the cel keys of an animation folder in a timeline: each
973    /// `(0-based frame, cel layer name)` shows that cel from that frame on.
974    /// The folder must already have a cel track in the timeline.
975    pub fn set_cel_keys(&mut self, timeline_id: i64, folder_id: i64, keys: &[(i64, String)]) -> Result<()> {
976        let animation = self.animation()?.ok_or_else(|| Error::format("No animation"))?;
977        let timeline = animation.timeline(timeline_id).ok_or_else(|| Error::format("No timeline"))?;
978        let track =
979            timeline.tracks.iter().find(|t| t.layer_id == Some(folder_id) && t.kind == TrackKind::Cels).ok_or_else(
980                || Error::format(format!("Layer {folder_id} has no cel track in timeline {timeline_id}")),
981            )?;
982        let clip = track.clip.as_ref().ok_or_else(|| Error::format("Cel track has no mixer"))?;
983        let folder = self.layers.root.find(folder_id).ok_or_else(|| Error::format("No such folder"))?;
984        let cels: Vec<String> = folder.children.iter().map(|c| c.layer.name.clone()).collect();
985        let mut sorted = keys.to_vec();
986        sorted.sort_by_key(|(frame, _)| *frame);
987        sorted.dedup_by_key(|(frame, _)| *frame);
988        let mut frames = Vec::new();
989        let mut values = Vec::new();
990        let mut tags = Vec::new();
991        for (frame, name) in &sorted {
992            let index = cels
993                .iter()
994                .position(|c| c == name)
995                .ok_or_else(|| Error::format(format!("No cel {name:?} in folder {folder_id}")))?;
996            frames.push(clip.motion_at(timeline.ticks(*frame as f64)));
997            values.push(index as f64);
998            tags.push(name.clone());
999        }
1000        let current = sorted.iter().rev().find(|(frame, _)| *frame as f64 <= timeline.current_frame).or(sorted.first());
1001        let n = frames.len();
1002        self.edit_mixers(track, |clip| {
1003            let double = clip_is_double(clip);
1004            let numbers = |v: &[f64]| {
1005                if double {
1006                    Value::DoubleArray(v.to_vec())
1007                } else {
1008                    Value::SingleArray(v.iter().map(|x| *x as f32).collect())
1009                }
1010            };
1011            let anim = clip.child_mut("AnimInfo").ok_or_else(|| Error::format("No AnimInfo"))?;
1012            let mut curve = Node::new("FCurve", Value::Null);
1013            curve.attributes.push(("Type".into(), "ImageCelName".into()));
1014            curve.children = vec![
1015                Node::new("Frame", numbers(&frames)),
1016                Node::new("Value", numbers(&values)),
1017                Node::new("LeftSlope", numbers(&vec![0.0; n])),
1018                Node::new("RightSlope", numbers(&vec![0.0; n])),
1019                Node::new("Interp", Value::StringArray(vec!["Constant".into(); n])),
1020                Node::new("ReviseConstant", Value::ByteArray(vec![1; n])),
1021                Node::new("Tag", Value::StringArray(tags.clone())),
1022            ];
1023            match anim.children.iter_mut().find(|c| c.name == "FCurve" && c.attribute("Type") == Some("ImageCelName")) {
1024                Some(existing) => *existing = curve,
1025                None => anim.children.push(curve),
1026            }
1027            if let Some((_, name)) = current
1028                && let Some(param) =
1029                    anim.children.iter_mut().find(|c| c.name == "Param" && c.attribute("Name") == Some("ImageCelName"))
1030            {
1031                param.value = Value::Int32(cels.iter().position(|c| c == name).unwrap_or(0) as i32);
1032                match param.attributes.iter_mut().find(|(k, _)| k == "Tag") {
1033                    Some(tag) => tag.1 = name.clone(),
1034                    None => param.attributes.push(("Tag".into(), name.clone())),
1035                }
1036            }
1037            Ok(())
1038        })?;
1039        if let Some((_, name)) = current {
1040            let entries = vec![MapEntry {
1041                name: "ImageCelName".into(),
1042                tag: name.clone(),
1043                value: MapValue::Int(cels.iter().position(|c| c == name).unwrap_or(0) as i32),
1044            }];
1045            self.database().execute(
1046                "UPDATE Track SET TrackValueMap = ?1 WHERE MainId = ?2",
1047                (value_map_bytes(&entries), track.main_id),
1048            )?;
1049        }
1050        Ok(())
1051    }
1052
1053    /// Edit a track's `ActionNodeClip` in both mixers and store them.
1054    fn edit_mixers(&mut self, track: &Track, edit: impl Fn(&mut Node) -> Result<()>) -> Result<()> {
1055        for (slot, column) in ["TrackActionMixer", "TrackActionMixer2"].iter().enumerate() {
1056            let Some((id, document)) = &track.mixers[slot] else { continue };
1057            let mut document = document.clone();
1058            edit(document.action_clip_mut().ok_or_else(|| Error::format("No ActionNodeClip"))?)?;
1059            let bytes = document.to_bytes()?;
1060            self.set_external(id, cmt::pack(&bytes)?);
1061            let size_column = format!("{column}Size");
1062            self.database().execute(
1063                &format!("UPDATE Track SET {size_column} = ?1 WHERE MainId = ?2"),
1064                (bytes.len() as i64, track.main_id),
1065            )?;
1066        }
1067        Ok(())
1068    }
1069}
1070
1071/// Whether a clip stores double precision arrays (mixer 2, version 0110).
1072fn clip_is_double(clip: &Node) -> bool {
1073    fn any_double(node: &Node) -> Option<bool> {
1074        match node.value {
1075            Value::DoubleArray(_) | Value::Double2(_) | Value::Double2Array(_) => return Some(true),
1076            Value::SingleArray(_) | Value::Float2(_) | Value::Float2Array(_) => return Some(false),
1077            _ => {}
1078        }
1079        node.children.iter().find_map(any_double)
1080    }
1081    any_double(clip).unwrap_or(true)
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086    use super::*;
1087
1088    fn curve(keys: &[(f64, f64, &str)]) -> Curve {
1089        Curve {
1090            kind: "Opacity".into(),
1091            axis: None,
1092            keys: keys
1093                .iter()
1094                .map(|(frame, value, interp)| Key {
1095                    frame: *frame,
1096                    value: *value,
1097                    left_slope: 0.0,
1098                    right_slope: 0.0,
1099                    interp: Interp::from_name(interp),
1100                    tag: None,
1101                })
1102                .collect(),
1103        }
1104    }
1105
1106    #[test]
1107    fn constant_holds_and_default_eases() {
1108        let c = curve(&[(50.0, 322.0, "Constant"), (110.0, 409.0, "Default"), (130.0, 300.0, "Default")]);
1109        assert_eq!(c.evaluate(0.0), Some((322.0, None)));
1110        assert_eq!(c.evaluate(109.0).unwrap().0, 322.0);
1111        assert_eq!(c.evaluate(110.0).unwrap().0, 409.0);
1112        // Smoothstep: a quarter of the way in is 15.625 % of the change.
1113        let v = c.evaluate(115.0).unwrap().0;
1114        assert!((v - (409.0 - 109.0 * 0.15625)).abs() < 1e-9, "{v}");
1115        assert_eq!(c.evaluate(120.0).unwrap().0, 354.5);
1116        assert_eq!(c.evaluate(500.0), Some((300.0, Some(2))));
1117    }
1118
1119    #[test]
1120    fn clip_maps_time_to_motion() {
1121        let clip = Clip { time: (0.0, 180.0), motion: (0.0, 90.0), params: vec![], curves: vec![] };
1122        assert_eq!(clip.motion_at(20.0), 10.0);
1123        assert_eq!(clip.time_at(10.0), 20.0);
1124    }
1125
1126    #[test]
1127    fn keyframe_transform_rotates_clockwise_about_the_center() {
1128        let map = Affine::keyframe([10.0, 10.0], [100.0, 50.0], [2.0, 1.0], 90.0);
1129        let (x, y) = map.apply((11.0, 10.0));
1130        // One pixel right of the centre, doubled, turned clockwise: down.
1131        assert!((x - 100.0).abs() < 1e-9 && (y - 52.0).abs() < 1e-9, "{x} {y}");
1132        let back = map.inverse().unwrap().apply((x, y));
1133        assert!((back.0 - 11.0).abs() < 1e-9 && (back.1 - 10.0).abs() < 1e-9);
1134    }
1135
1136    #[test]
1137    fn value_map_round_trips() {
1138        let entries = vec![
1139            MapEntry { name: "ImageCelName".into(), tag: "2".into(), value: MapValue::Int(1) },
1140            MapEntry { name: "ImageCenter".into(), tag: String::new(), value: MapValue::Pair([190.0, 130.0]) },
1141            MapEntry { name: "Opacity".into(), tag: String::new(), value: MapValue::Double(89.84375) },
1142        ];
1143        let bytes = value_map_bytes(&entries);
1144        // The cel entry as CSP writes it: 46 bytes.
1145        assert_eq!(&bytes[8..12], &46u32.to_be_bytes());
1146        assert_eq!(parse_value_map(&bytes).unwrap(), entries);
1147    }
1148
1149    /// A cel track mixer: `TimeClip` and `MotionClip` 0..90, keys at the
1150    /// given motion ticks showing the given cels.
1151    fn cel_mixer(version: &str, keys: &[(f64, &str, i32)]) -> Document {
1152        let double = version == "0110";
1153        let numbers = |v: Vec<f64>| {
1154            if double { Value::DoubleArray(v) } else { Value::SingleArray(v.iter().map(|x| *x as f32).collect()) }
1155        };
1156        let range = |name: &str| {
1157            let mut node = Node::new(name, Value::Null);
1158            node.children = vec![
1159                Node::new("Start", Value::Double(0.0)),
1160                Node::new("End", Value::Double(90.0)),
1161                Node::new("Rate", Value::Double(60.0)),
1162            ];
1163            node
1164        };
1165        let mut curve = Node::new("FCurve", Value::Null);
1166        curve.attributes.push(("Type".into(), "ImageCelName".into()));
1167        curve.children = vec![
1168            Node::new("Frame", numbers(keys.iter().map(|k| k.0).collect())),
1169            Node::new("Value", numbers(keys.iter().map(|k| k.2 as f64).collect())),
1170            Node::new("Interp", Value::StringArray(vec!["Constant".into(); keys.len()])),
1171            Node::new("Tag", Value::StringArray(keys.iter().map(|k| k.1.to_owned()).collect())),
1172        ];
1173        let mut param = Node::new("Param", Value::Int32(0));
1174        param.attributes = vec![("Name".into(), "ImageCelName".into()), ("Tag".into(), keys[0].1.into())];
1175        let mut anim = Node::new("AnimInfo", Value::Null);
1176        anim.children = vec![param, curve];
1177        let mut clip = Node::new("ActionNodeClip", Value::Null);
1178        clip.attributes.push(("Name".into(), String::new()));
1179        clip.children = vec![range("TimeClip"), range("MotionClip"), anim];
1180        let mut general = Node::new("General", Value::Null);
1181        general.children = vec![clip];
1182        let mut root = Node::new("celsysdocument", Value::Null);
1183        root.children = vec![general];
1184        Document { version: version.into(), root }
1185    }
1186
1187    /// A new file with an animation folder holding cels "A" (red) and "B"
1188    /// (blue), one 24 fps timeline of 6 frames, and a cel track showing A
1189    /// from frame 0 and B from frame 3 (motion ticks 0 and 7.5).
1190    fn fixture() -> (ClipFile, i64) {
1191        use crate::edit::{NewLayer, new_external_id};
1192        let mut file = ClipFile::create(&crate::NewCanvas { width: 8, height: 8, ..Default::default() }).unwrap();
1193        let folder = file.add_layer(file.canvas.root_folder, NewLayer::Folder, "anim").unwrap();
1194        for (name, color) in [("A", [255, 0, 0, 255]), ("B", [0, 0, 255, 255])] {
1195            let cel = file.add_layer(folder, NewLayer::Raster, name).unwrap();
1196            let mut image = crate::RgbaImage::new(8, 8);
1197            image.fill(color);
1198            file.set_layer_pixels(cel, &image, (0, 0)).unwrap();
1199        }
1200        let uuid = file.layers.get(folder).unwrap().uuid.clone().unwrap().replace('-', "");
1201        let uuid: Vec<u8> = (0..16).map(|i| u8::from_str_radix(&uuid[2 * i..2 * i + 2], 16).unwrap()).collect();
1202        let ids = [new_external_id(), new_external_id()];
1203        let keys = [(0.0, "A", 0), (7.5, "B", 1)];
1204        let mut sizes = Vec::new();
1205        for (id, version) in ids.iter().zip(["0100", "0110"]) {
1206            let bytes = cel_mixer(version, &keys).to_bytes().unwrap();
1207            sizes.push(bytes.len() as i64);
1208            file.set_external(id, cmt::pack(&bytes).unwrap());
1209        }
1210        let db = file.database();
1211        db.execute_batch(
1212            "ALTER TABLE Layer ADD COLUMN AnimationFolder INTEGER DEFAULT NULL;
1213             UPDATE Layer SET AnimationFolder = 1 WHERE MainId = (SELECT MAX(MainId) FROM Layer WHERE LayerFolder = 1);
1214             UPDATE AnimationCutBank SET FirstTimeLine = 1, CurrentIndex = 0, Enable = 1;
1215             CREATE TABLE TimeLine(_PW_ID INTEGER PRIMARY KEY AUTOINCREMENT, MainId INTEGER, NextTimeLine INTEGER,
1216                 FirstTrack INTEGER, TimeLineName TEXT, FrameRate REAL, StartFrame REAL, EndFrame REAL,
1217                 CurrentFrame REAL, BiggestEndFrame REAL, LabelFirstIndex INTEGER);
1218             INSERT INTO TimeLine(MainId, NextTimeLine, FirstTrack, TimeLineName, FrameRate, StartFrame, EndFrame,
1219                 CurrentFrame, BiggestEndFrame, LabelFirstIndex) VALUES (1, 0, 1, 't', 24, 0, 6, 0, 6, 0);
1220             CREATE TABLE Track(_PW_ID INTEGER PRIMARY KEY AUTOINCREMENT, MainId INTEGER, ItemId INTEGER,
1221                 TrackNextIndex INTEGER, TrackKind INTEGER, LayerUuidWithTrack BLOB, TrackActionMixer BLOB,
1222                 TrackActionMixerSize INTEGER, TrackActionMixer2 BLOB, TrackActionMixer2Size INTEGER,
1223                 TrackValueMap BLOB, TrackLabelFirstIndex INTEGER);",
1224        )
1225        .unwrap();
1226        db.execute(
1227            "INSERT INTO Track(MainId, ItemId, TrackNextIndex, TrackKind, LayerUuidWithTrack, TrackActionMixer,
1228                 TrackActionMixerSize, TrackActionMixer2, TrackActionMixer2Size, TrackLabelFirstIndex)
1229             VALUES (1, 1, 0, 2000, ?1, ?2, ?3, ?4, ?5, 0)",
1230            (uuid, ids[0].as_bytes(), sizes[0], ids[1].as_bytes(), sizes[1]),
1231        )
1232        .unwrap();
1233        file.reload().unwrap();
1234        (file, folder)
1235    }
1236
1237    fn shown(file: &ClipFile, frame: f64) -> [u8; 4] {
1238        let animation = Animation::read(file).unwrap().unwrap();
1239        let state = animation.frame_state(file, 0, frame).unwrap();
1240        let (image, _) = crate::render::render_frame(file, &state, Default::default()).unwrap();
1241        image.pixel(4, 4)
1242    }
1243
1244    #[test]
1245    fn cels_switch_by_frame_and_can_be_rekeyed() {
1246        let (mut file, folder) = fixture();
1247        let animation = Animation::read(&file).unwrap().unwrap();
1248        let track = &animation.timelines[0].tracks[0];
1249        assert_eq!((track.kind, track.layer_id), (TrackKind::Cels, Some(folder)));
1250        assert_eq!(shown(&file, 0.0), [255, 0, 0, 255]);
1251        assert_eq!(shown(&file, 2.0), [255, 0, 0, 255]);
1252        assert_eq!(shown(&file, 3.0), [0, 0, 255, 255]);
1253
1254        file.set_cel_keys(1, folder, &[(0, "B".into()), (4, "A".into())]).unwrap();
1255        file.set_frame_rate(1, 12.0).unwrap();
1256        file.set_frame_range(1, 1, 5).unwrap();
1257        let file = ClipFile::from_bytes(file.to_bytes().unwrap()).unwrap();
1258        let animation = Animation::read(&file).unwrap().unwrap();
1259        let timeline = &animation.timelines[0];
1260        assert_eq!((timeline.frame_rate, timeline.start_frame, timeline.end_frame), (12.0, 1.0, 5.0));
1261        let clip = timeline.tracks[0].clip.as_ref().unwrap();
1262        // 24 → 12 fps stretches the clip; the keys stay on frames 0 and 4.
1263        assert_eq!(clip.time, (0.0, 180.0));
1264        let keys: Vec<(f64, Option<&str>)> = clip.curves[0].keys.iter().map(|k| (k.frame, k.tag.as_deref())).collect();
1265        assert_eq!(keys, [(0.0, Some("B")), (10.0, Some("A"))]);
1266        // Both mixers carry the edit.
1267        for (_, document) in timeline.tracks[0].mixers.iter().flatten() {
1268            assert_eq!(Clip::from_document(document).unwrap().curves[0].keys.len(), 2);
1269        }
1270        assert_eq!(timeline.tracks[0].value_map[0].tag, "B");
1271        assert_eq!(shown(&file, 3.0), [0, 0, 255, 255]);
1272        assert_eq!(shown(&file, 4.0), [255, 0, 0, 255]);
1273    }
1274
1275    /// Writes the Python reader's fixture `python/tests/data/anim.clip`:
1276    /// `cargo test -p csp-format -- --ignored write_python_fixture`.
1277    #[test]
1278    #[ignore]
1279    fn write_python_fixture() {
1280        let (mut file, _) = fixture();
1281        file.update_preview().unwrap();
1282        file.save(concat!(env!("CARGO_MANIFEST_DIR"), "/../../python/tests/data/anim.clip")).unwrap();
1283    }
1284
1285    #[test]
1286    fn opacity_is_quantized_to_256ths() {
1287        assert_eq!(opacity_256(89.84375), 230);
1288        assert_eq!(opacity_256(100.0), 256);
1289    }
1290}