Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes are listed here. The format follows
The first release of the Rust rewrite. An early alpha: see docs/releases/v0.1.0.md for what it does not do yet.

### Added
- The Bone tool, as in Animate: a drag from one symbol instance to another links them with a bone, and both move to a new armature layer ("Armature_1") whose keyframes are poses; a drag from an instance already in an armature adds a bone below it. The Selection tool poses an armature: a joint, the instance it carries or a bone (by its tip) is pulled toward the pointer by inverse kinematics, turning the joints above it, and a root moves the whole armature. A pose made between two keyframes is a keyframe of its own that the one before tweens into, and the instances keep playing their symbols through the poses. Properties edits the joint last touched: its turn, its rotation limits, Remove bone and Release armature (back to plain instances, posed). Exports, the preview, SWF and HTML5 show the poses; XFL export writes each pose as its instances, with a warning. `flare bone` and `flare bones`, and the MCP tools `add_bone`, `pose_bone`, `limit_joint`, `detach_bone`, `release_armature` and `list_bones`, do the same.
- Paste puts what was copied into the active layer's keyframe at the playhead, as Animate does, instead of making a layer for each thing; a locked, hidden or guide layer says why it cannot take it. A tween on a keyframe of several elements moves the first and holds the others, in the editor and in imported XFL.
- `.fla` and `.xfl` are first-class save formats, as in Animate, so an animator can work without ever making a `.flare` and send the file on to somebody who uses Animate. File > Save As offers Flash Document (`.fla`, the default), uncompressed Flash Document (`.xfl`, a folder with the `.xfl` inside) and Flare project; the ending of the name decides, and a name with none gets `.fla`. File > Save writes a document back to the file it was opened from, so opening `x.fla`, editing and saving rewrites `x.fla` (the original bytes if nothing changed, otherwise only what was edited rebuilt and every other file of the original kept) and makes no `.flare`. Revert, the Recent list, the tab and window title, the close prompt's Save and autosave all follow the document's file. The file is replaced only by a whole new one, a failed save (read-only file, full disk) leaves the old file and the unsaved work, and a save says what it wrote in the status line and the Output panel: unchanged original, how many files were rebuilt, and what the format cannot hold. In the browser Save and Save As download a `.fla` (or a `.flare` by name). `flare convert` turns any of the three formats into any other, every `flare` project command takes a `.fla` or `.xfl` and edits it in place, `flare new x.fla` makes one, and the MCP server opens them, `save` writes back and `save_as` writes a new file in any format.
- The timeline's frames are small fixed cells, as Animate's are, instead of sharing the panel's width (a short scene's frames filled the whole timeline): empty cells go on to the edge, the zoom changes the cell width, and the Fit button sizes the scene to the panel.
Expand Down
2 changes: 1 addition & 1 deletion PORTING_TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ into the new crate architecture.
- [ ] OpenToonz-style xsheet view
- [ ] Onion skin
- [ ] Camera tool / camera layer
- [ ] Bone/rigging tools
- [ ] Bone/rigging tools (the Bone tool, armatures of symbol instances and IK posing are built; bones inside shapes and the Bind tool are not)
- [ ] Paint bucket / ink bottle (advanced)
- [ ] Gadget tools
- [ ] Tracker
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The next things, most important first. Most of it is shown dimmed in the interfa
- SWF that keeps more of the document: buttons with their states, shape tweens as morphs, masks of several shapes and sounds inside symbols
- A Motion Editor: per-property graphs and custom ease curves for tweens
- Importing artwork from SVG and Photoshop files
- Bones and inverse kinematics (the Bone and Bind tools)
- Bones inside shapes and the Bind tool (bones that link symbol instances, posed by inverse kinematics with the Bone tool, are built)
- Editable keyboard shortcuts

Also planned, at lower priority: ActionScript and a script editor (ActionScript in an imported file is kept as data and never run), shape hints and more than one scene, the rest of Animate's tools, custom themes, a fully rearrangeable toolbar and a released web build.
Expand Down
12 changes: 12 additions & 0 deletions apps/flare-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ pub(crate) enum Command {
#[arg(long, allow_hyphen_values = true)]
ease: Option<i8>,
},
/// Link instances with bones and pose the armature, as Animate's Bone tool and posing do.
Bone {
#[command(subcommand)]
action: project::BoneAction,
},
/// List the armatures of the first scene at a frame: each joint, where it is posed, how far it
/// is turned and the instance it carries.
Bones {
path: PathBuf,
#[arg(long, default_value_t = 0)]
frame: u32,
},
/// Add an empty layer below the existing ones in the first scene.
AddLayer { path: PathBuf, name: String },
/// Insert or remove frames and keyframes on a layer (default: the first layer).
Expand Down
2 changes: 2 additions & 0 deletions apps/flare-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ fn main() -> Result<()> {
motion,
ease,
} => project::camera(&path, frame, (x, y), zoom, rotation, motion, ease)?,
Command::Bone { action } => project::bone(action)?,
Command::Bones { path, frame } => project::bones_list(&path, frame)?,
Command::AddLayer { path, name } => project::add_layer(&path, &name)?,
Command::Frame {
op,
Expand Down
239 changes: 239 additions & 0 deletions apps/flare-cli/src/project/bones.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
//! Bones: linking instances into an armature and posing it, as Animate's Bone tool does.

use anyhow::{Context, Result, bail};
use flare_core::{DocumentPoint, InstanceId, LayerId, SceneId};
use flare_document::Document;
use flare_tools::bones::{self, Bond};
use glamx::DVec2;
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use uuid::Uuid;

use super::*;

/// What to do with bones.
#[derive(Debug, clap::Subcommand)]
pub enum BoneAction {
/// Link two instances with a bone, as a drag of the Bone tool from one to the other does. Two
/// instances on plain layers make a new armature layer; from an instance in an armature, the
/// other joins it below that joint. Instance ids are in `flare eval`.
Add {
path: PathBuf,
#[arg(long)]
from: Uuid,
#[arg(long)]
to: Uuid,
#[arg(long, default_value_t = 0)]
frame: u32,
/// Where on `from` the joint goes (default: its registration point).
#[arg(long, num_args = 2, value_names = ["X", "Y"], allow_hyphen_values = true)]
from_at: Option<Vec<f64>>,
/// Where on `to` the joint goes (default: its registration point).
#[arg(long, num_args = 2, value_names = ["X", "Y"], allow_hyphen_values = true)]
to_at: Option<Vec<f64>>,
},
/// Pose the armature at a frame: pull a joint to a point (inverse kinematics; a root moves
/// the whole armature), turn it, or move a root by an offset. Between two poses this makes a
/// pose of its own, which the one before tweens into.
Pose {
path: PathBuf,
/// The armature layer (default: the scene's only one).
#[arg(long)]
layer: Option<Uuid>,
#[arg(long)]
joint: usize,
#[arg(long, default_value_t = 0)]
frame: u32,
/// Pull the joint toward this point.
#[arg(long, num_args = 2, value_names = ["X", "Y"], allow_hyphen_values = true,
conflicts_with_all = ["turn", "by"])]
to: Option<Vec<f64>>,
/// Turn the joint to this many degrees from rest, clockwise.
#[arg(long, allow_hyphen_values = true, conflicts_with = "by")]
turn: Option<f64>,
/// Move a root joint, and the armature with it, by this much.
#[arg(long, num_args = 2, value_names = ["DX", "DY"], allow_hyphen_values = true)]
by: Option<Vec<f64>>,
},
/// Let a joint turn only between two angles from rest, in degrees, in every pose; with
/// neither it turns freely.
Limit {
path: PathBuf,
#[arg(long)]
layer: Option<Uuid>,
#[arg(long)]
joint: usize,
#[arg(long, allow_hyphen_values = true, requires = "max")]
min: Option<f64>,
#[arg(long, allow_hyphen_values = true, requires = "min")]
max: Option<f64>,
},
/// Take away the bone into a joint, and every bone below it; the instances stay where they are.
Detach {
path: PathBuf,
#[arg(long)]
layer: Option<Uuid>,
#[arg(long)]
joint: usize,
},
/// Turn the armature back into plain keyframes of its instances, posed as they are.
Release {
path: PathBuf,
#[arg(long)]
layer: Option<Uuid>,
},
}

fn first_scene(document: &Document) -> Result<SceneId> {
Ok(document
.scenes()
.first()
.context("the document has no scene")?
.id())
}

fn is_armature(layer: &flare_document::Layer) -> bool {
bones::is_armature_layer(layer)
}

/// The armature layer named, or the first scene's only one.
fn armature_layer(document: &Document, layer: Option<Uuid>) -> Result<LayerId> {
let layers = document.scenes()[0].timeline().layers();
match layer {
Some(id) => layers
.iter()
.find(|l| *l.id().as_uuid() == id)
.filter(|l| is_armature(l))
.map(|l| l.id())
.with_context(|| format!("bones: no armature layer {id} in the first scene")),
None => {
let mut found = layers.iter().filter(|l| is_armature(l));
let first = found
.next()
.context("bones: the first scene has no armature; make one with `bone add`")?;
if found.next().is_some() {
bail!("bones: the first scene has several armatures; name one with --layer");
}
Ok(first.id())
}
}
}

fn point(values: Option<Vec<f64>>, what: &str) -> Result<Option<DVec2>> {
match values.as_deref() {
None => Ok(None),
Some([x, y]) if x.is_finite() && y.is_finite() => Ok(Some(DVec2::new(*x, *y))),
Some(_) => bail!("bones: {what} must be two finite numbers"),
}
}

/// Every armature of the first scene at `frame`, for printing.
pub fn bones_report(document: &Document, frame: u32) -> Result<Value> {
let scene = first_scene(document)?;
Ok(json!({ "frame": frame, "armatures": bones::report(document, scene, frame) }))
}

/// `flare bones`: the armatures at a frame.
pub fn bones_list(path: &Path, frame: u32) -> Result<Value> {
let session = open(path)?;
bones_report(session.document(), frame)
}

/// `flare bone ...`.
pub fn bone(action: BoneAction) -> Result<Value> {
let (path, frame) = match &action {
BoneAction::Add { path, frame, .. } | BoneAction::Pose { path, frame, .. } => {
(path.clone(), *frame)
}
BoneAction::Limit { path, .. }
| BoneAction::Detach { path, .. }
| BoneAction::Release { path, .. } => (path.clone(), 0),
};
let mut session = open(&path)?;
let document = session.document();
let scene = first_scene(document)?;
let commands = match action {
BoneAction::Add {
from,
to,
frame,
from_at,
to_at,
..
} => {
let at = |p: Option<DVec2>| p.and_then(|p| DocumentPoint::new(p.x, p.y).ok());
let from = Bond {
instance: InstanceId::from_uuid(from),
at: at(point(from_at, "--from-at")?),
};
let to = Bond {
instance: InstanceId::from_uuid(to),
at: at(point(to_at, "--to-at")?),
};
let name = format!("Armature_{}", armature_count(document) + 1);
bones::connect(document, scene, frame, from, to, &name)
.context("bones: cannot add the bone")?
}
BoneAction::Pose {
layer,
joint,
frame,
to,
turn,
by,
..
} => {
let layer = armature_layer(document, layer)?;
let made = match (point(to, "--to")?, turn, point(by, "--by")?) {
(Some(target), None, None) => {
bones::pose_joint(document, scene, layer, frame, joint, target)
}
(None, Some(degrees), None) => {
if !degrees.is_finite() {
bail!("bones: --turn must be a finite number");
}
bones::turn_joint(document, scene, layer, frame, joint, degrees.to_radians())
}
(None, None, Some(by)) => {
bones::shift_root(document, scene, layer, frame, joint, by)
}
_ => bail!("bones: say one of --to, --turn or --by"),
};
made.context("bones: cannot pose the armature")?
}
BoneAction::Limit {
layer,
joint,
min,
max,
..
} => {
let layer = armature_layer(document, layer)?;
let limit = min.zip(max).map(|(a, b)| (a.to_radians(), b.to_radians()));
bones::set_limit(document, scene, layer, joint, limit)
.context("bones: cannot limit the joint")?
}
BoneAction::Detach { layer, joint, .. } => {
let layer = armature_layer(document, layer)?;
bones::detach(document, scene, layer, joint).context("bones: cannot detach the bone")?
}
BoneAction::Release { layer, .. } => {
let layer = armature_layer(document, layer)?;
bones::release(document, scene, layer).context("bones: cannot release the armature")?
}
};
session.apply(commands)?;
commit(&session, &path)?;
let mut report = bones_report(session.document(), frame)?;
report["revision"] = json!(session.revision());
Ok(report)
}

fn armature_count(document: &Document) -> usize {
document.scenes()[0]
.timeline()
.layers()
.iter()
.filter(|l| is_armature(l))
.count()
}
6 changes: 5 additions & 1 deletion apps/flare-cli/src/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ use serde_json::{Value, json};
use std::num::NonZeroU32;
use std::path::Path;

mod bones;
mod camera;
mod draw;
mod interchange;
mod render;
mod text;
mod timeline;

pub use bones::{BoneAction, bone, bones_list};
#[allow(unused_imports)]
use camera::*;
pub use camera::{CameraMotion, camera};
Expand Down Expand Up @@ -95,9 +97,11 @@ pub fn info(path: &Path) -> Result<Value> {
.iter()
.map(|layer| {
let elements: usize = layer.spans().iter().map(|s| s.element_count()).sum();
let armature = flare_tools::bones::is_armature_layer(layer);
json!({ "id": layer.id().as_uuid(), "name": layer.name(),
"visible": layer.is_visible(), "locked": layer.is_locked(),
"spans": layer.spans().len(), "elements": elements })
"spans": layer.spans().len(), "elements": elements,
"armature": armature })
})
.collect();
json!({ "id": scene.id().as_uuid(), "name": scene.name(), "layers": layers })
Expand Down
Loading
Loading