Tests: give the nested knockout draw-budget test two minutes, not twenty seconds, on a loaded runner (#3650)

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
This commit is contained in:
FredAppsandFredApps authored and GitHub committed 2026-11-13 20:27:01 +01:00
1 parent a70eb03fa6
commit 05fc7f2835
27 files changed
+15656 -15654

No files matched your search

+6 -6
View File
@@ -1,6 +1,6 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
reviews:
auto_review:
# Dependabot bumps are checked by CI; skip CodeRabbit reviews on them.
ignore_usernames:
- "dependabot[bot]"
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
reviews:
auto_review:
# Dependabot bumps are checked by CI; skip CodeRabbit reviews on them.
ignore_usernames:
- "dependabot[bot]"
+11 -11
View File
@@ -1,11 +1,11 @@
*~
plan/
target/
/dist
/log
.DS_Store
.claude/worktrees/
__pycache__/
# After Effects test corpus fetched by `cargo xtask aep-corpus`
/corpus/
*~
plan/
target/
/dist
/log
.DS_Store
.claude/worktrees/
__pycache__/
# After Effects test corpus fetched by `cargo xtask aep-corpus`
/corpus/
+12 -12
View File
@@ -1,12 +1,12 @@
{
"mcpServers": {
"effectcraft": {
"command": "cargo",
"args": ["run", "--quiet", "--release", "-p", "effectcraft-cli", "--", "mcp", "--demo"]
},
"effectcraft-app": {
"command": "cargo",
"args": ["run", "--quiet", "--release", "-p", "effectcraft-cli", "--", "mcp", "--bridge", "9877"]
}
}
}
{
"mcpServers": {
"effectcraft": {
"command": "cargo",
"args": ["run", "--quiet", "--release", "-p", "effectcraft-cli", "--", "mcp", "--demo"]
},
"effectcraft-app": {
"command": "cargo",
"args": ["run", "--quiet", "--release", "-p", "effectcraft-cli", "--", "mcp", "--bridge", "9877"]
}
}
}
+210 -210
View File
@@ -1,210 +1,210 @@
<!-- craft-house-rules:start core source=dde09ea31e2e457f5b4653a6e76f7cdf55944bdf sha256=540e6cea75d12d0710c3dca89658328e81e508ecef102446ca5bb95b7c0b92bb -->
## House rules
These apply in every Craft repository. A later rule may be stricter, not weaker. The source is `standards/house-rules-core.md` in `storytold/craft-contrib`. Propose changes there, not in a copy.
1. **A person is accountable.** Whoever runs an agent answers for what it opens and for its review comments.
2. **Check, then claim.** Before writing code, read the issue comments, search pull requests for `#N` and the keywords, and check recent commits on `main`. Comment on the issue first. Release a claim you will not finish.
3. **Verify, don't guess.** Open a definition before using it. Confirm the cause in the code, even when someone names a location.
4. **Keep fixes narrow and tested.** Fix what the issue describes, where the bug is. Add a test that fails without the fix.
5. **Review the diff before opening a pull request.** Identifiers exist as used, the test fails without the fix, nothing unrelated changed, and the description is true.
6. **Look for a better pull request.** If another fixes the same thing, close yours when it is better or equal, and say why.
7. **Report only what ran.** Say what you tested and what you could not run. Without a local build or test, say so and rely on CI before marking ready.
8. **`main` changes through a pull request.** Do not push to it. Branch protection enforces this. There is no exception.
9. **Stop and ask only about** licensing outside the allowlist, publishing, releases, a new remote, secrets, spending money, destructive git, and anything else hard to undo. Otherwise keep working. `plan/` is a gitignored notebook, not rules. Without it, do not ask for it and do not stop.
10. **Keep secrets and personal details out** of the repository, logs and requests.
11. **CI stays shared.** Workflows call `storytold/craft-common-actions`. Do not change CI in a feature or fix pull request. If CI cannot cover a change, open an issue there.
12. **Guards say why.** A check that looks removable says what it prevents. Read that before changing it.
13. **Instructions must be readable by everyone.** No notes about one machine, and no links to files a reader cannot open. `craftrules` is retired. Do not link to it.
14. **Keep pull requests moving.** Concurrent pull requests are fine; unanswered actionable feedback is not. Respond in 7 days, a reminder at 14, closure eligible at 30, from when an actionable request waits on the author. Suggestions and unconfirmed bot findings do not start the clock. A fix, explanation or agreed plan resets it; unrelated activity does not. Waiting on a reviewer or maintainer pauses it and does not count against the author. A maintainer decides closure; it is never automatic. Reuse search results, do not poll, and stop at a rate limit.
15. **No agent attribution.** A pull request does not say an agent wrote it. A repository rule may not require that.
<!-- craft-house-rules:end -->
<!-- craft-house-rules:start rust source=dde09ea31e2e457f5b4653a6e76f7cdf55944bdf sha256=387e0ccb4b49e567f60951d7d6fff7305054c89f43ad812b9993dc7c38528013 -->
## Rust Craft repositories
These apply to native Craft apps and to shared Rust libraries. Select them by repository type. A repository that merely contains some Rust does not take this block. Rules later in a repository's file may be stricter, but not weaker. The source is `standards/house-rules-rust.md` in [storytold/craft-contrib](https://github.com/storytold/craft-contrib).
1. **Never crash.** Non-test code does not panic and has no `unsafe`, except where the repository file names an exception. Return errors, treat input as hostile, and bound recursion and allocations. New production crate roots copy the `deny` attribute from an existing crate: `clippy::unwrap_used`, `clippy::expect_used`, `clippy::panic`, `clippy::unimplemented`, `clippy::todo`, `clippy::unreachable`. Every crash fix comes with a test that panicked before the fix.
2. **Clean room.** Learn from observed behaviour and public specifications. Do not copy another product's code, assets, strings or artwork. Check a dependency's licence before adding it.
3. **Assets and fonts.** Every asset is original or suitably licensed, and is recorded in `ATTRIBUTION.md` in the same change. Font files live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not in the app repository.
4. **Rust only for native Craft apps.** No handwritten JavaScript or TypeScript, and no Tauri, Electron or webview shell, in those apps. Shared tooling follows its documented technical scope and maintainer-approved exceptions. Craft PR Review's existing Tauri and TypeScript UI is one such exception. It does not relax this rule for native apps. Preserve any existing web build.
5. **Time goes through `chrono`.** Do not hand-roll calendar conversion. For dates beyond what `chrono` can represent, follow [standards/time.md](https://github.com/storytold/craft-contrib/blob/main/standards/time.md).
6. **Contributor credits.** Add or edit only your own person's entry in `contributors/people.toml`, and only after asking them. Never guess a name or hand-edit `contributors.json`.
## Licence allowlist
Dependencies and assets use SPDX identifiers. An expression is allowed when every identifier is in this list. A repository may name a shorter list. It may not add an identifier here by editing its own copy of these rules.
- `ISC`
- `MIT`
- `BSD-2-Clause`
- `BSD-3-Clause`
- `Apache-2.0`
- `OFL-1.1`
- `Ubuntu-font-1.0`
- `Bitstream-Vera`
- `CC0-1.0`
- `CC-BY-4.0`
- `CC-BY-SA-4.0`
- `LicenseRef-Public-Domain`
`LicenseRef-Storyteller-Trademark` is not in this list. It covers Storyteller brand marks only, where a repository already says so.
<!-- craft-house-rules:end -->
# EffectCraft — rules for agents and contributors
These rules apply to every human and AI contributor. `CLAUDE.md` holds the working instructions; this
file holds the rules that must never be broken. When the two disagree, this file wins.
## Never crash
People trust EffectCraft with hours of work. A malformed project, PSD, SVG, Lottie or media file, a bad
expression or script, a broken plug-in, a bad control-channel or MCP argument, a corrupt preferences file
or a full disk must produce an error the user (or agent) can act on, never a crash and never lost work.
**This rule outranks feature work:** don't ship a feature by adding a panic path, and fix a crash before
building on top of it.
- **Fail with `Result<T, E>`** through the crate's error type and propagate with `?`; add context
(`map_err`, an error variant) rather than discarding it. Lenient readers fall back and record the repair.
- **No panicking shortcuts in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`,
`todo!` or `unimplemented!`. Use `?`, `ok_or(..)?`, `let … else`, `if let`, or `unwrap_or*` where a
fallback is genuinely correct (never where it would silently corrupt a document). Unfinished features
return an "unsupported" error. The only exception is a provably infallible literal, written as
`#[allow(clippy::expect_used)]` + `.expect("why this cannot fail")`.
- **No `unsafe`** (`unsafe_code = "forbid"` for the workspace).
- **Every input-derived number is hostile.** Use `get()` instead of `[i]`/`[a..b]` when the position
comes from a file, a user, an agent or arithmetic on those; slice strings only at char boundaries; use
checked or saturating arithmetic for lengths, offsets and counts; guard division by zero and NaN/inf
casts; cap allocations sized by input.
- **Bound recursion:** projects, precomps, expressions and SVG/PSD trees can be cyclic or deeply nested;
walk them with seen-sets or depth limits.
- **Don't cascade:** handle lock poisoning (`lock().unwrap_or_else(PoisonError::into_inner)` or an
error), and treat worker-thread join results as `Result`s.
- **Last-resort guard:** the app wraps command execution and file import/export in a panic hook /
`catch_unwind` boundary that turns an escaped panic into an error and keeps the document. It is a
safety net, not a substitute for the rules above; keep `panic = "unwind"`.
- **Prove it:** every crash fix comes with a small synthetic regression test that panicked before the fix.
Tests, benches, fuzz targets and `xtask` may `unwrap()`; `clippy.toml` allows it in tests. Crates that are
clean carry `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented,
clippy::todo, clippy::unreachable)]`; new crates start with it.
## 1. Assets: no Adobe artwork, every asset licensed and attributed
This rule is absolute. Breaking it is the most serious mistake a contributor can make on this project.
**What counts as an asset:** any image, icon, cursor, logo, illustration, screenshot, font, colour LUT,
preset, template, sound, music, video, 3D model, or other non-code media. This covers files in the
repository, bytes embedded in code (`include_bytes!`, base64, data URIs), and data hard-coded to
reproduce an image (for example, point lists traced from someone else's icon).
1. **Never use Adobe iconography, images or other Adobe assets**, in any form:
- no icons, cursors, logos, splash screens, UI artwork or screenshots from any Adobe product;
- no Adobe fonts, LUTs, presets, templates, sound effects, stock media or sample projects;
- no traced, redrawn, recoloured or "inspired-by" copies of Adobe icons. Our icons may follow generic
conventions (a play triangle, a razor blade, a stopwatch), but each must be drawn from scratch
without reference to Adobe's artwork.
2. **Every asset must be open**: open-source licensed (MIT, Apache-2.0, BSD, ISC, zlib, SIL OFL…),
public domain / CC0, or Creative Commons (CC BY or CC BY-SA; no NC/ND licences), **or** created by a
contributor who owns it and licenses it to the project under the project licence (MIT OR Apache-2.0).
If the licence is unknown or unclear, the asset does not go in.
3. **Every asset needs an attribution sidecar.** Next to each asset file `X`, add `X.attribution` with:
```text
asset: <file name>
title: <what it is>
author: <creator / copyright holder>
source: <URL, or "original work" for contributor-made assets>
license: <SPDX id or licence name>
license-file: <path to the licence text, if the licence requires shipping it>
added: <YYYY-MM-DD> by <contributor>
notes: <how it was made or modified; for screenshots, what is shown>
```
Also add one line for the asset to [`ATTRIBUTION.md`](ATTRIBUTION.md). `cargo xtask assets` (part of
`cargo xtask ci`) fails if any asset lacks a sidecar or index entry.
4. **Screenshots** in the repo may show only EffectCraft (or other open projects), with media we generated
or media that is itself openly licensed. Never commit screenshots of Adobe products.
5. **Local reference material stays local.** After Effects reference screenshots and notes live only in
`plan/aftereffects/` (gitignored). They must never be committed, bundled, embedded, traced or shipped.
6. **Code-drawn assets** (icons in `crates/ui-egui/src/icons.rs`, procedural demo content in
`crates/engine/src/demo.rs`, generators in `crates/effects`) are original work under the project licence
and are listed in `ATTRIBUTION.md`.
7. **When in doubt, leave it out** and draw or generate it yourself.
8. **The one exception: first-party ArtCraft brand marks.** The ArtCraft name and logos in `docs/brand/`
are trademarks of the ArtCraft Team, not open source, usable only unmodified and only in the context of
EffectCraft under `docs/brand/LICENSE-brand.txt` (forks and modified versions must remove them). They still
need a sidecar and an `ATTRIBUTION.md` row (licence `LicenseRef-ArtCraft-Trademark`). No other
non-open asset is allowed, and this exception never covers third-party marks (Adobe, Discord, GitHub
and other logos stay out; draw a generic icon instead).
## 2. Clean-room code
- Never read, disassemble or copy anything inside Adobe application bundles; file names and listings only.
Observe behaviour by using the app; never capture its Home screen, recent projects, account info or
file browsers.
- Never copy GPL/LGPL/AGPL code (FFmpeg, Natron, Blender, Glaxnimate, Synfig, Friction, Olive, MLT, frei0r, G'MIC…).
- Never read or copy Adobe expression/scripting implementations, `.ffx` presets or effect plug-ins; the public Expression Language Reference and Scripting Guide describe *behaviour* only.
- Implement codecs and formats from public specifications (ITU-T/ISO/IEC standards, IETF RFCs, the VP9
and AV1 bitstream specs, SMPTE documents, published container specs). Do not read the source of
reference or third-party decoders/encoders while implementing a format, even permissively licensed
ones (libvpx, libopus, libaom, dav1d, openh264…); spec text and conformance vectors only. Record the
spec edition used in the crate README.
- ffmpeg/ffprobe may be used only as external test oracles and fixture generators. They are never
linked, bundled or shipped.
- Dependencies must use permissive licences: MIT/Apache-2.0/BSD/ISC/Zlib/Unicode/CC0/BSL-1.0, or
MPL-2.0 used unmodified.
## 3. Engineering rules
- **Layering** (`cargo xtask layers`): nothing below L5 depends on egui/eframe/winit/rfd/cpal; L0–L4 build for wasm32.
- **Exact time:** `effectcraft_time::Tick` (254 016 000 000/s). Keyframe times are layer time.
- **Property tree:** everything animatable is a `Property` in the layer's `PropGroup` tree, addressed by paths (`transform/position`, `effects/#1/blurriness`, `@uid`).
- **Everything is a command** (`crates/engine`): id, label, menu path, shortcut, params, enabled(), run(). UI, CLI, control channel and MCP dispatch by id.
- **Everything is agent-drivable:** every interactive widget registers an automation id; UI state is serde.
- **Community links** (Help menu, About, header Discord button): https://discord.gg/artcraft, https://getartcraft.com, https://getartcraft.com/apps/effectcraft, https://github.com/storytold/effectcraft.
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, layers, assets, wasm).
- New workspace crates include `[lints] workspace = true` in `Cargo.toml`. Production crate roots declare the crate-level lint attributes before modules; crate-level `//!` documentation may precede them.
- **Commits:** one task id per commit (`M6.3: trim paths`). Only green states.
- `cargo run -p effectcraft -- --control 9877` opens the desktop app with the JSON-lines control server (`docs/control-protocol.md`). For UI work, look at the result, including the headless `snapshot` example.
- `plan/` is an optional gitignored notebook (`plan/aftereffects/` may hold reference notes and captures). Do not ask for it, do not stop when it is absent, and never commit those captures.
- Read [`docs/gaps.md`](docs/gaps.md) and [`docs/target-app-parity.md`](docs/target-app-parity.md) before choosing work. Fidelity, reliability and stability (G1–G3) come before new features.
- Some workflow files still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task.
Shared real-file test corpora (Photoshop-authored PSDs, etc.) live in
[`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus).
Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified,
as PhotoCraft does with `cargo xtask corpus`.
## See also
- [ROADMAP.md](ROADMAP.md): stage, headline numbers and what's next
- [docs/gaps.md](docs/gaps.md): every known shortfall against After Effects, ranked, and the prioritised workstreams (G1–G9); read it before choosing work
- [docs/target-app-parity.md](docs/target-app-parity.md): the parity numbers and how they were measured, with the per-area docs it links (UI, file formats, hardware, localization, effects, scripting)
- [CONTRIBUTING.md](CONTRIBUTING.md): setup, gates, commits, how to add things
- [docs/architecture.md](docs/architecture.md): layers, data model, commands, pipeline
- [docs/testing.md](docs/testing.md): oracle tests, criteria, benchmarks
- [docs/agents.md](docs/agents.md): driving EffectCraft over MCP / the control channel, and the agent work loop
- [docs/control-protocol.md](docs/control-protocol.md): control-channel method reference
- [docs/contributors.md](docs/contributors.md): the About window's contributor and model credits
- [ATTRIBUTION.md](ATTRIBUTION.md): asset index
## Contributor credits (About window)
- About ▸ Contributors/Models are compiled into the binary from `contributors/contributors.json`
(commit stats; generated, never hand-edit) and `contributors/people.toml` (names people chose for
themselves). See `docs/contributors.md`.
- **Agents working for a contributor:** when you prepare a PR, check whether your human's GitHub
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
whether they want to be credited by more than their username: a real name, a display name, and/or
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
(copy the template at the top of the file)
and include it in their PR, committed as them. If no, change nothing: they are credited as
`@username` anyway.
- Never add, edit, guess or copy anyone else's entry or name (not from git config, commit authors or
GitHub profiles). Never hand-edit `contributors.json`.
## CI workflows
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
<!-- craft-house-rules:start core source=dde09ea31e2e457f5b4653a6e76f7cdf55944bdf sha256=540e6cea75d12d0710c3dca89658328e81e508ecef102446ca5bb95b7c0b92bb -->
## House rules
These apply in every Craft repository. A later rule may be stricter, not weaker. The source is `standards/house-rules-core.md` in `storytold/craft-contrib`. Propose changes there, not in a copy.
1. **A person is accountable.** Whoever runs an agent answers for what it opens and for its review comments.
2. **Check, then claim.** Before writing code, read the issue comments, search pull requests for `#N` and the keywords, and check recent commits on `main`. Comment on the issue first. Release a claim you will not finish.
3. **Verify, don't guess.** Open a definition before using it. Confirm the cause in the code, even when someone names a location.
4. **Keep fixes narrow and tested.** Fix what the issue describes, where the bug is. Add a test that fails without the fix.
5. **Review the diff before opening a pull request.** Identifiers exist as used, the test fails without the fix, nothing unrelated changed, and the description is true.
6. **Look for a better pull request.** If another fixes the same thing, close yours when it is better or equal, and say why.
7. **Report only what ran.** Say what you tested and what you could not run. Without a local build or test, say so and rely on CI before marking ready.
8. **`main` changes through a pull request.** Do not push to it. Branch protection enforces this. There is no exception.
9. **Stop and ask only about** licensing outside the allowlist, publishing, releases, a new remote, secrets, spending money, destructive git, and anything else hard to undo. Otherwise keep working. `plan/` is a gitignored notebook, not rules. Without it, do not ask for it and do not stop.
10. **Keep secrets and personal details out** of the repository, logs and requests.
11. **CI stays shared.** Workflows call `storytold/craft-common-actions`. Do not change CI in a feature or fix pull request. If CI cannot cover a change, open an issue there.
12. **Guards say why.** A check that looks removable says what it prevents. Read that before changing it.
13. **Instructions must be readable by everyone.** No notes about one machine, and no links to files a reader cannot open. `craftrules` is retired. Do not link to it.
14. **Keep pull requests moving.** Concurrent pull requests are fine; unanswered actionable feedback is not. Respond in 7 days, a reminder at 14, closure eligible at 30, from when an actionable request waits on the author. Suggestions and unconfirmed bot findings do not start the clock. A fix, explanation or agreed plan resets it; unrelated activity does not. Waiting on a reviewer or maintainer pauses it and does not count against the author. A maintainer decides closure; it is never automatic. Reuse search results, do not poll, and stop at a rate limit.
15. **No agent attribution.** A pull request does not say an agent wrote it. A repository rule may not require that.
<!-- craft-house-rules:end -->
<!-- craft-house-rules:start rust source=dde09ea31e2e457f5b4653a6e76f7cdf55944bdf sha256=387e0ccb4b49e567f60951d7d6fff7305054c89f43ad812b9993dc7c38528013 -->
## Rust Craft repositories
These apply to native Craft apps and to shared Rust libraries. Select them by repository type. A repository that merely contains some Rust does not take this block. Rules later in a repository's file may be stricter, but not weaker. The source is `standards/house-rules-rust.md` in [storytold/craft-contrib](https://github.com/storytold/craft-contrib).
1. **Never crash.** Non-test code does not panic and has no `unsafe`, except where the repository file names an exception. Return errors, treat input as hostile, and bound recursion and allocations. New production crate roots copy the `deny` attribute from an existing crate: `clippy::unwrap_used`, `clippy::expect_used`, `clippy::panic`, `clippy::unimplemented`, `clippy::todo`, `clippy::unreachable`. Every crash fix comes with a test that panicked before the fix.
2. **Clean room.** Learn from observed behaviour and public specifications. Do not copy another product's code, assets, strings or artwork. Check a dependency's licence before adding it.
3. **Assets and fonts.** Every asset is original or suitably licensed, and is recorded in `ATTRIBUTION.md` in the same change. Font files live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not in the app repository.
4. **Rust only for native Craft apps.** No handwritten JavaScript or TypeScript, and no Tauri, Electron or webview shell, in those apps. Shared tooling follows its documented technical scope and maintainer-approved exceptions. Craft PR Review's existing Tauri and TypeScript UI is one such exception. It does not relax this rule for native apps. Preserve any existing web build.
5. **Time goes through `chrono`.** Do not hand-roll calendar conversion. For dates beyond what `chrono` can represent, follow [standards/time.md](https://github.com/storytold/craft-contrib/blob/main/standards/time.md).
6. **Contributor credits.** Add or edit only your own person's entry in `contributors/people.toml`, and only after asking them. Never guess a name or hand-edit `contributors.json`.
## Licence allowlist
Dependencies and assets use SPDX identifiers. An expression is allowed when every identifier is in this list. A repository may name a shorter list. It may not add an identifier here by editing its own copy of these rules.
- `ISC`
- `MIT`
- `BSD-2-Clause`
- `BSD-3-Clause`
- `Apache-2.0`
- `OFL-1.1`
- `Ubuntu-font-1.0`
- `Bitstream-Vera`
- `CC0-1.0`
- `CC-BY-4.0`
- `CC-BY-SA-4.0`
- `LicenseRef-Public-Domain`
`LicenseRef-Storyteller-Trademark` is not in this list. It covers Storyteller brand marks only, where a repository already says so.
<!-- craft-house-rules:end -->
# EffectCraft — rules for agents and contributors
These rules apply to every human and AI contributor. `CLAUDE.md` holds the working instructions; this
file holds the rules that must never be broken. When the two disagree, this file wins.
## Never crash
People trust EffectCraft with hours of work. A malformed project, PSD, SVG, Lottie or media file, a bad
expression or script, a broken plug-in, a bad control-channel or MCP argument, a corrupt preferences file
or a full disk must produce an error the user (or agent) can act on, never a crash and never lost work.
**This rule outranks feature work:** don't ship a feature by adding a panic path, and fix a crash before
building on top of it.
- **Fail with `Result<T, E>`** through the crate's error type and propagate with `?`; add context
(`map_err`, an error variant) rather than discarding it. Lenient readers fall back and record the repair.
- **No panicking shortcuts in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`,
`todo!` or `unimplemented!`. Use `?`, `ok_or(..)?`, `let … else`, `if let`, or `unwrap_or*` where a
fallback is genuinely correct (never where it would silently corrupt a document). Unfinished features
return an "unsupported" error. The only exception is a provably infallible literal, written as
`#[allow(clippy::expect_used)]` + `.expect("why this cannot fail")`.
- **No `unsafe`** (`unsafe_code = "forbid"` for the workspace).
- **Every input-derived number is hostile.** Use `get()` instead of `[i]`/`[a..b]` when the position
comes from a file, a user, an agent or arithmetic on those; slice strings only at char boundaries; use
checked or saturating arithmetic for lengths, offsets and counts; guard division by zero and NaN/inf
casts; cap allocations sized by input.
- **Bound recursion:** projects, precomps, expressions and SVG/PSD trees can be cyclic or deeply nested;
walk them with seen-sets or depth limits.
- **Don't cascade:** handle lock poisoning (`lock().unwrap_or_else(PoisonError::into_inner)` or an
error), and treat worker-thread join results as `Result`s.
- **Last-resort guard:** the app wraps command execution and file import/export in a panic hook /
`catch_unwind` boundary that turns an escaped panic into an error and keeps the document. It is a
safety net, not a substitute for the rules above; keep `panic = "unwind"`.
- **Prove it:** every crash fix comes with a small synthetic regression test that panicked before the fix.
Tests, benches, fuzz targets and `xtask` may `unwrap()`; `clippy.toml` allows it in tests. Crates that are
clean carry `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented,
clippy::todo, clippy::unreachable)]`; new crates start with it.
## 1. Assets: no Adobe artwork, every asset licensed and attributed
This rule is absolute. Breaking it is the most serious mistake a contributor can make on this project.
**What counts as an asset:** any image, icon, cursor, logo, illustration, screenshot, font, colour LUT,
preset, template, sound, music, video, 3D model, or other non-code media. This covers files in the
repository, bytes embedded in code (`include_bytes!`, base64, data URIs), and data hard-coded to
reproduce an image (for example, point lists traced from someone else's icon).
1. **Never use Adobe iconography, images or other Adobe assets**, in any form:
- no icons, cursors, logos, splash screens, UI artwork or screenshots from any Adobe product;
- no Adobe fonts, LUTs, presets, templates, sound effects, stock media or sample projects;
- no traced, redrawn, recoloured or "inspired-by" copies of Adobe icons. Our icons may follow generic
conventions (a play triangle, a razor blade, a stopwatch), but each must be drawn from scratch
without reference to Adobe's artwork.
2. **Every asset must be open**: open-source licensed (MIT, Apache-2.0, BSD, ISC, zlib, SIL OFL…),
public domain / CC0, or Creative Commons (CC BY or CC BY-SA; no NC/ND licences), **or** created by a
contributor who owns it and licenses it to the project under the project licence (MIT OR Apache-2.0).
If the licence is unknown or unclear, the asset does not go in.
3. **Every asset needs an attribution sidecar.** Next to each asset file `X`, add `X.attribution` with:
```text
asset: <file name>
title: <what it is>
author: <creator / copyright holder>
source: <URL, or "original work" for contributor-made assets>
license: <SPDX id or licence name>
license-file: <path to the licence text, if the licence requires shipping it>
added: <YYYY-MM-DD> by <contributor>
notes: <how it was made or modified; for screenshots, what is shown>
```
Also add one line for the asset to [`ATTRIBUTION.md`](ATTRIBUTION.md). `cargo xtask assets` (part of
`cargo xtask ci`) fails if any asset lacks a sidecar or index entry.
4. **Screenshots** in the repo may show only EffectCraft (or other open projects), with media we generated
or media that is itself openly licensed. Never commit screenshots of Adobe products.
5. **Local reference material stays local.** After Effects reference screenshots and notes live only in
`plan/aftereffects/` (gitignored). They must never be committed, bundled, embedded, traced or shipped.
6. **Code-drawn assets** (icons in `crates/ui-egui/src/icons.rs`, procedural demo content in
`crates/engine/src/demo.rs`, generators in `crates/effects`) are original work under the project licence
and are listed in `ATTRIBUTION.md`.
7. **When in doubt, leave it out** and draw or generate it yourself.
8. **The one exception: first-party ArtCraft brand marks.** The ArtCraft name and logos in `docs/brand/`
are trademarks of the ArtCraft Team, not open source, usable only unmodified and only in the context of
EffectCraft under `docs/brand/LICENSE-brand.txt` (forks and modified versions must remove them). They still
need a sidecar and an `ATTRIBUTION.md` row (licence `LicenseRef-ArtCraft-Trademark`). No other
non-open asset is allowed, and this exception never covers third-party marks (Adobe, Discord, GitHub
and other logos stay out; draw a generic icon instead).
## 2. Clean-room code
- Never read, disassemble or copy anything inside Adobe application bundles; file names and listings only.
Observe behaviour by using the app; never capture its Home screen, recent projects, account info or
file browsers.
- Never copy GPL/LGPL/AGPL code (FFmpeg, Natron, Blender, Glaxnimate, Synfig, Friction, Olive, MLT, frei0r, G'MIC…).
- Never read or copy Adobe expression/scripting implementations, `.ffx` presets or effect plug-ins; the public Expression Language Reference and Scripting Guide describe *behaviour* only.
- Implement codecs and formats from public specifications (ITU-T/ISO/IEC standards, IETF RFCs, the VP9
and AV1 bitstream specs, SMPTE documents, published container specs). Do not read the source of
reference or third-party decoders/encoders while implementing a format, even permissively licensed
ones (libvpx, libopus, libaom, dav1d, openh264…); spec text and conformance vectors only. Record the
spec edition used in the crate README.
- ffmpeg/ffprobe may be used only as external test oracles and fixture generators. They are never
linked, bundled or shipped.
- Dependencies must use permissive licences: MIT/Apache-2.0/BSD/ISC/Zlib/Unicode/CC0/BSL-1.0, or
MPL-2.0 used unmodified.
## 3. Engineering rules
- **Layering** (`cargo xtask layers`): nothing below L5 depends on egui/eframe/winit/rfd/cpal; L0–L4 build for wasm32.
- **Exact time:** `effectcraft_time::Tick` (254 016 000 000/s). Keyframe times are layer time.
- **Property tree:** everything animatable is a `Property` in the layer's `PropGroup` tree, addressed by paths (`transform/position`, `effects/#1/blurriness`, `@uid`).
- **Everything is a command** (`crates/engine`): id, label, menu path, shortcut, params, enabled(), run(). UI, CLI, control channel and MCP dispatch by id.
- **Everything is agent-drivable:** every interactive widget registers an automation id; UI state is serde.
- **Community links** (Help menu, About, header Discord button): https://discord.gg/artcraft, https://getartcraft.com, https://getartcraft.com/apps/effectcraft, https://github.com/storytold/effectcraft.
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, layers, assets, wasm).
- New workspace crates include `[lints] workspace = true` in `Cargo.toml`. Production crate roots declare the crate-level lint attributes before modules; crate-level `//!` documentation may precede them.
- **Commits:** one task id per commit (`M6.3: trim paths`). Only green states.
- `cargo run -p effectcraft -- --control 9877` opens the desktop app with the JSON-lines control server (`docs/control-protocol.md`). For UI work, look at the result, including the headless `snapshot` example.
- `plan/` is an optional gitignored notebook (`plan/aftereffects/` may hold reference notes and captures). Do not ask for it, do not stop when it is absent, and never commit those captures.
- Read [`docs/gaps.md`](docs/gaps.md) and [`docs/target-app-parity.md`](docs/target-app-parity.md) before choosing work. Fidelity, reliability and stability (G1–G3) come before new features.
- Some workflow files still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task.
Shared real-file test corpora (Photoshop-authored PSDs, etc.) live in
[`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus).
Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified,
as PhotoCraft does with `cargo xtask corpus`.
## See also
- [ROADMAP.md](ROADMAP.md): stage, headline numbers and what's next
- [docs/gaps.md](docs/gaps.md): every known shortfall against After Effects, ranked, and the prioritised workstreams (G1–G9); read it before choosing work
- [docs/target-app-parity.md](docs/target-app-parity.md): the parity numbers and how they were measured, with the per-area docs it links (UI, file formats, hardware, localization, effects, scripting)
- [CONTRIBUTING.md](CONTRIBUTING.md): setup, gates, commits, how to add things
- [docs/architecture.md](docs/architecture.md): layers, data model, commands, pipeline
- [docs/testing.md](docs/testing.md): oracle tests, criteria, benchmarks
- [docs/agents.md](docs/agents.md): driving EffectCraft over MCP / the control channel, and the agent work loop
- [docs/control-protocol.md](docs/control-protocol.md): control-channel method reference
- [docs/contributors.md](docs/contributors.md): the About window's contributor and model credits
- [ATTRIBUTION.md](ATTRIBUTION.md): asset index
## Contributor credits (About window)
- About ▸ Contributors/Models are compiled into the binary from `contributors/contributors.json`
(commit stats; generated, never hand-edit) and `contributors/people.toml` (names people chose for
themselves). See `docs/contributors.md`.
- **Agents working for a contributor:** when you prepare a PR, check whether your human's GitHub
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
whether they want to be credited by more than their username: a real name, a display name, and/or
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
(copy the template at the top of the file)
and include it in their PR, committed as them. If no, change nothing: they are credited as
`@username` anyway.
- Never add, edit, guess or copy anyone else's entry or name (not from git config, commit authors or
GitHub profiles). Never hand-edit `contributors.json`.
## CI workflows
- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here.
- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time.
- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it.
+158 -158
View File
@@ -1,158 +1,158 @@
# Attribution
Every non-code asset in EffectCraft, with its author, source and licence. Each repository file listed here also has
a `<file>.attribution` sidecar with full details. The rules are in [AGENTS.md](AGENTS.md) §1: no Adobe
iconography, images or other Adobe assets; open licences only; every asset attributed.
`cargo xtask assets` checks that this index and the sidecars cover every asset in the repository.
The complete licence texts and copyright notices for the bundled fonts, including the fonts embedded
by the pinned egui dependency, are reproduced in [`NOTICE`](NOTICE), which every package ships; the
`assets/fonts/OFL-*.txt` files are the upstream copies kept beside the fonts.
## Bundled dependency fonts
These fonts are embedded by the pinned egui dependency (epaint_default_fonts 0.37 at
`6d518fc799681571963842f95b55a6b68431b15a`); no font file is added to this repository.
egui-icons is always embedded, `default_fonts` adds Ubuntu Light and Hack, and
`monochrome_emoji_fonts` adds Noto Emoji and emoji-icon-font. The complete copyright notices
and licence texts are in [`NOTICE`](NOTICE).
| Asset | Author | Source | Licence |
|---|---|---|---|
| `epaint_default_fonts/fonts/egui-icons.ttf` | John Slegers (emoji-icon-font) | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/egui-icons.txt; original https://github.com/jslegers/emoji-icon-font | MIT (complete copyright and permission text in `NOTICE`) |
| `epaint_default_fonts/fonts/emoji-icon-font.ttf` | John Slegers | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/emoji-icon-font-mit-license.txt; original https://github.com/jslegers/emoji-icon-font | MIT (complete copyright and permission text in `NOTICE`) |
| `epaint_default_fonts/fonts/NotoEmoji-Regular.ttf` | Google Inc. (The Noto Project Authors) | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/OFL.txt | OFL-1.1 (complete text in `NOTICE`) |
| `epaint_default_fonts/fonts/Ubuntu-Light.ttf` | Canonical Ltd. and Dalton Maag | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/UFL.txt | Ubuntu Font Licence 1.0 (complete text in `NOTICE`) |
| `epaint_default_fonts/fonts/Hack-Regular.ttf` | Source Foundry Authors; Bitstream Inc. | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/Hack-Regular.txt; original https://github.com/source-foundry/Hack | MIT AND Bitstream-Vera (complete texts in `NOTICE`) |
## Asset files
| Asset | Author | Source | Licence |
|---|---|---|---|
| `assets/fonts/Inter-Regular.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/Inter-Medium.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/Inter-SemiBold.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/Inter-Bold.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `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/NotoSansSymbols2-Regular.ttf` | The Noto Project Authors | https://github.com/notofonts/symbols | OFL-1.1 (`assets/fonts/OFL-NotoSansSymbols2.txt`) |
| `assets/fonts/JetBrainsMono-Regular.ttf` | The JetBrains Mono Project Authors | https://github.com/JetBrains/JetBrainsMono | OFL-1.1 (`assets/fonts/OFL-JetBrainsMono.txt`) |
| `apps/effectcraft-web/web/favicon.svg` | EffectCraft contributors | Original work: web app icon, a copy of `assets/app-icon/effectcraft-small.svg` (the unicorn app icon) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `packaging/macos/dmg/background.svg` | @XusBadia | Original work: macOS DMG Finder window background, derived from the EffectCraft app icon (`assets/app-icon/effectcraft.svg`); rendered to a TIFF by `packaging/macos/package.sh` at package time | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) |
| `assets/app-icon/effectcraft-1024.png` | EffectCraft contributors | Original work: effectCraft app icon, 1024 px (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft-macos-512.png` | EffectCraft contributors | Original work: effectCraft runtime Dock icon (macOS) (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft-small.svg` | EffectCraft contributors | Original work: effectCraft app icon, light vector (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft.icns` | EffectCraft contributors | Original work: effectCraft macOS app icon (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft.ico` | EffectCraft contributors | Original work: effectCraft Windows app icon (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft.svg` | EffectCraft contributors | Original work: effectCraft app icon, master vector (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 128x128 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 16x16 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 24x24 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 256x256 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 32x32 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 48x48 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 512x512 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 64x64 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.effectcraft.svg` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor scalable (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `crates/lottie/tests/fixtures/bouncing-ball.json` | EffectCraft contributors | Original work: hand-written Lottie test fixture (bouncing ball, eased position keys) | MIT OR Apache-2.0 |
| `crates/lottie/tests/fixtures/shapes-gradient.json` | EffectCraft contributors | Original work: hand-written Lottie test fixture (star, rectangle, gradient fill, dashed stroke, trim paths) | MIT OR Apache-2.0 |
| `crates/lottie/tests/fixtures/precomp-matte-text.json` | EffectCraft contributors | Original work: hand-written Lottie test fixture (precomp, track matte, parented text) | MIT OR Apache-2.0 |
| `crates/lottie/tests/fixtures/legacy-masks.json` | EffectCraft contributors | Original work: hand-written Lottie test fixture (legacy keyframes, masks) | MIT OR Apache-2.0 |
| `crates/svg/tests/fixtures/basic-shapes.svg` | EffectCraft contributors | Original work: hand-written SVG test fixture (basic shapes, groups, transforms) | MIT OR Apache-2.0 |
| `crates/svg/tests/fixtures/paths-gradients.svg` | EffectCraft contributors | Original work: hand-written SVG test fixture (path data, gradients, CSS, use) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/quad.gltf` | EffectCraft contributors | Original work: glTF 2.0 test fixture (textured quad, node hierarchy, translation animation; data URIs) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/quad.glb` | EffectCraft contributors | Original work: binary glTF test fixture (the same quad with its texture in the BIN chunk) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/cube.obj` | EffectCraft contributors | Original work: Wavefront OBJ test fixture (unit cube, two materials) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/cube.mtl` | EffectCraft contributors | Original work: MTL test fixture (materials of cube.obj) | MIT OR Apache-2.0 |
Three small After Effects test projects of `crates/aep` are in this repository: `crates/aep/tests/fixtures/synthetic.aep` (original work, made with After Effects 2026 from own data, working space None, the embedded colour profile removed; MIT OR Apache-2.0, with its `.attribution` sidecar), `crates/aep/tests/fixtures/ai-mask-3099.aep` (original work, made with After Effects 2026 from four hand-written one-page PDFs named `.ai`, footage paths and host name neutralised, the embedded colour profiles replaced by stubs made with `crates/aep/examples/stubicc.rs`; MIT OR Apache-2.0, with its `.attribution` sidecar) and `crates/aep/tests/fixtures/text-synthetic.aep` (original work, made with After Effects 2026 by script from own data, working space None, no colour profile embedded, nothing changed after the save; MIT OR Apache-2.0, with its `.attribution` sidecar). The other After Effects test projects of `crates/aep` are not in this repository. They are original work of an EffectCraft contributor, generated with After Effects 2026 as own test data (MIT OR Apache-2.0, each file with its `.attribution` sidecar); they are in [storytold/effectcraft-corpus](https://github.com/storytold/effectcraft-corpus) with their sidecars, fetched by `cargo xtask aep-corpus` (commit-pinned, SHA-256-verified), and the tests read them only from the folder named by `EFFECTCRAFT_AEP_FIXTURES` (opt-in). Their embedded colour profiles are replaced by stubs made with `crates/aep/examples/stubicc.rs` (a header and a description, no colour data), so no vendor profile data is in them.
## Screenshots
Screenshots of EffectCraft itself, rendered headlessly, showing only procedurally generated demo content.
| Asset | Author | Source | Licence |
|---|---|---|---|
| `docs/images/effectcraft-hero.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft main window) | MIT OR Apache-2.0 |
| `docs/images/effectcraft-3d.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft Classic 3D) | MIT OR Apache-2.0 |
| `docs/images/effectcraft-graph-editor.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft Graph Editor) | MIT OR Apache-2.0 |
| `docs/images/effectcraft-effects.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft effects) | MIT OR Apache-2.0 |
| `docs/images/effectcraft-render-queue.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft Render Queue panel with the demo compositions queued for H.264 and ProRes export) | MIT OR Apache-2.0 |
## Code-drawn assets (original work, MIT OR Apache-2.0)
- UI icons drawn in code: `crates/ui-egui/src/icons.rs`, including the original filled favorite star (2026-10-11, EffectCraft contributors).
- Procedural demo content (solids, shapes, gradients, text animations): `crates/engine/src/demo.rs`.
- Built-in project templates (Lower Third, Title Card, Logo Reveal, Kinetic Type, Social Square, Vertical Story,
Slideshow, 3D Text Orbit) built from engine commands, and their thumbnails rendered by our own renderer:
`crates/engine/src/templates.rs`.
- Text animator presets (Typewriter, Fade Up Characters, Bounce In Words, Tracking In, Scramble, Blur In, Jitter,
Drop In Lines): `crates/engine/presets/text_animators.json` (sidecar `.attribution`).
- Transition animation presets (23: wipes, clock and fan reveals, blinds, dissolves, card flips, CC wipes, fades):
`crates/engine/presets/transitions.json` (sidecar `.attribution`).
- Backgrounds animation presets (23: drifting gradients, fog, cloud and smoke, plasma and magma, cells and crystals, scrolling grids
and checks, pulse rings, bokeh, light sweep): `crates/engine/presets/backgrounds.json` (sidecar `.attribution`).
- Behaviors animation presets (22: wiggle, shake, float, orbit, bounce, drift, pendulum, spin, pulse, heartbeat, blink, strobe, flicker):
`crates/engine/presets/behaviors.json` (sidecar `.attribution`).
- Sample scripts (File ▸ Scripts) and ScriptUI panel: `crates/engine/scripts/Rename Layers.jsx`,
`crates/engine/scripts/Sort Layers by In Point.jsx`, `crates/engine/scripts/Create Null at Selected Layers.jsx`,
`crates/engine/scripts/Render Queue Batch.jsx`, `crates/engine/scripts/ScriptUI Panels/Layer Tools.jsx`
(sidecars `.attribution`).
- Bundled extension (ScriptUI panel) with its twelve built-in ease presets:
`extensions/scriptui-panels/Ease Presets.jsx` (sidecar `.attribution`).
- Example effect plug-in: `examples/plugins/posterize-bands` (WebAssembly ABI 1).
## Trained models (optional downloads, not bundled)
The repository and the installers contain no model weights. These models are downloaded only when
a user asks (Settings ▸ Roto Brush, Settings ▸ Face Tracking, or the `roto.model.*` /
`face.model.*` commands), unmodified from their authors' official location, checked against a
pinned SHA-256 and stored in the user's `models` folder with a `<file>.NOTICE.txt` naming their
authors, licence and source. EffectCraft runs them with its own Rust implementation and is not
affiliated with or endorsed by their authors.
| Model | Authors | Source | Licence |
|---|---|---|---|
| MobileSAM (`mobile_sam.pt`) | Chaoning Zhang et al., building on Meta AI's Segment Anything and Microsoft's TinyViT | https://github.com/ChaoningZhang/MobileSAM | Apache-2.0 |
| MediaPipe Face Landmarker (`face_landmarker.task`: BlazeFace short-range detector, Face Mesh V2, blendshapes) | Google LLC | https://developers.google.com/edge/mediapipe/solutions/vision/face_landmarker (model cards: https://storage.googleapis.com/mediapipe-assets/MediaPipe%20BlazeFace%20Model%20Card%20(Short%20Range).pdf, https://storage.googleapis.com/mediapipe-assets/Model%20Card%20MediaPipe%20Face%20Mesh%20V2.pdf) | Apache-2.0 |
## Formats and parameters from other open-source projects
Written in Rust for EffectCraft; no code is copied. They follow published specifications and
configuration from these Apache-2.0 projects:
- `crates/segment/src/tflite.rs` reads TensorFlow Lite files using the field layout of TensorFlow
Lite's `schema.fbs` (The TensorFlow Authors, https://github.com/tensorflow/tensorflow,
Apache-2.0).
- `crates/segment/src/mediapipe.rs` follows MediaPipe's published face landmarker graph: the SSD
anchor layout, detection decoding, crop sizes and the face-mesh point indices it uses (Google
LLC, https://github.com/google-ai-edge/mediapipe, Apache-2.0).
- `crates/segment/src/mobilesam.rs` follows the MobileSAM, TinyViT and Segment Anything
architectures and their Apache-2.0 reference code.
## Frozen matte regression fixture
| Asset | Author | Source | Licence |
|---|---|---|---|
| `crates/engine/src/tests/fixtures/analysed-0.24.ecproj` | arnav-hiwarkar | Original work, synthetic project keyed by EffectCraft 0.24.1 | MIT OR Apache-2.0 |
| `crates/engine/src/tests/fixtures/object-matte-0.22.ecproj` | lisn0 | Original work, synthetic project saved by EffectCraft 0.22.0 | MIT OR Apache-2.0 |
| `crates/engine/src/tests/fixtures/tracked-0.22.ecproj` | arnav-hiwarkar | Original work, synthetic project saved by EffectCraft 0.22.0 | MIT OR Apache-2.0 |
## First-party brand marks
The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team. They are not open source and are not
covered by the project licence; they may be used only unmodified and only as part of this repository and EffectCraft,
under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). Forks and modified versions must remove them
(AGENTS.md §1.8).
| File | Author | Source | Licence |
|---|---|---|---|
| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-logo.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-logo.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-mark.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
- Appearance theme previews, drawn procedurally from original shapes: `crates/ui-egui/src/panels/settings.rs` (original work, MIT OR Apache-2.0).
# Attribution
Every non-code asset in EffectCraft, with its author, source and licence. Each repository file listed here also has
a `<file>.attribution` sidecar with full details. The rules are in [AGENTS.md](AGENTS.md) §1: no Adobe
iconography, images or other Adobe assets; open licences only; every asset attributed.
`cargo xtask assets` checks that this index and the sidecars cover every asset in the repository.
The complete licence texts and copyright notices for the bundled fonts, including the fonts embedded
by the pinned egui dependency, are reproduced in [`NOTICE`](NOTICE), which every package ships; the
`assets/fonts/OFL-*.txt` files are the upstream copies kept beside the fonts.
## Bundled dependency fonts
These fonts are embedded by the pinned egui dependency (epaint_default_fonts 0.37 at
`6d518fc799681571963842f95b55a6b68431b15a`); no font file is added to this repository.
egui-icons is always embedded, `default_fonts` adds Ubuntu Light and Hack, and
`monochrome_emoji_fonts` adds Noto Emoji and emoji-icon-font. The complete copyright notices
and licence texts are in [`NOTICE`](NOTICE).
| Asset | Author | Source | Licence |
|---|---|---|---|
| `epaint_default_fonts/fonts/egui-icons.ttf` | John Slegers (emoji-icon-font) | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/egui-icons.txt; original https://github.com/jslegers/emoji-icon-font | MIT (complete copyright and permission text in `NOTICE`) |
| `epaint_default_fonts/fonts/emoji-icon-font.ttf` | John Slegers | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/emoji-icon-font-mit-license.txt; original https://github.com/jslegers/emoji-icon-font | MIT (complete copyright and permission text in `NOTICE`) |
| `epaint_default_fonts/fonts/NotoEmoji-Regular.ttf` | Google Inc. (The Noto Project Authors) | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/OFL.txt | OFL-1.1 (complete text in `NOTICE`) |
| `epaint_default_fonts/fonts/Ubuntu-Light.ttf` | Canonical Ltd. and Dalton Maag | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/UFL.txt | Ubuntu Font Licence 1.0 (complete text in `NOTICE`) |
| `epaint_default_fonts/fonts/Hack-Regular.ttf` | Source Foundry Authors; Bitstream Inc. | https://github.com/storytold/egui/blob/6d518fc799681571963842f95b55a6b68431b15a/crates/epaint_default_fonts/fonts/Hack-Regular.txt; original https://github.com/source-foundry/Hack | MIT AND Bitstream-Vera (complete texts in `NOTICE`) |
## Asset files
| Asset | Author | Source | Licence |
|---|---|---|---|
| `assets/fonts/Inter-Regular.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/Inter-Medium.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/Inter-SemiBold.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `assets/fonts/Inter-Bold.ttf` | The Inter Project Authors | https://github.com/rsms/inter | OFL-1.1 (`assets/fonts/OFL-Inter.txt`) |
| `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/NotoSansSymbols2-Regular.ttf` | The Noto Project Authors | https://github.com/notofonts/symbols | OFL-1.1 (`assets/fonts/OFL-NotoSansSymbols2.txt`) |
| `assets/fonts/JetBrainsMono-Regular.ttf` | The JetBrains Mono Project Authors | https://github.com/JetBrains/JetBrainsMono | OFL-1.1 (`assets/fonts/OFL-JetBrainsMono.txt`) |
| `apps/effectcraft-web/web/favicon.svg` | EffectCraft contributors | Original work: web app icon, a copy of `assets/app-icon/effectcraft-small.svg` (the unicorn app icon) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `packaging/macos/dmg/background.svg` | @XusBadia | Original work: macOS DMG Finder window background, derived from the EffectCraft app icon (`assets/app-icon/effectcraft.svg`); rendered to a TIFF by `packaging/macos/package.sh` at package time | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) |
| `assets/app-icon/effectcraft-1024.png` | EffectCraft contributors | Original work: effectCraft app icon, 1024 px (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft-macos-512.png` | EffectCraft contributors | Original work: effectCraft runtime Dock icon (macOS) (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft-small.svg` | EffectCraft contributors | Original work: effectCraft app icon, light vector (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft.icns` | EffectCraft contributors | Original work: effectCraft macOS app icon (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft.ico` | EffectCraft contributors | Original work: effectCraft Windows app icon (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/effectcraft.svg` | EffectCraft contributors | Original work: effectCraft app icon, master vector (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 128x128 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 16x16 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 24x24 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 256x256 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 32x32 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 48x48 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 512x512 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor 64x64 (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.effectcraft.svg` | EffectCraft contributors | Original work: effectCraft app icon, Linux hicolor scalable (owner's ArtCraft unicorn drawing, vectorised) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) |
| `crates/lottie/tests/fixtures/bouncing-ball.json` | EffectCraft contributors | Original work: hand-written Lottie test fixture (bouncing ball, eased position keys) | MIT OR Apache-2.0 |
| `crates/lottie/tests/fixtures/shapes-gradient.json` | EffectCraft contributors | Original work: hand-written Lottie test fixture (star, rectangle, gradient fill, dashed stroke, trim paths) | MIT OR Apache-2.0 |
| `crates/lottie/tests/fixtures/precomp-matte-text.json` | EffectCraft contributors | Original work: hand-written Lottie test fixture (precomp, track matte, parented text) | MIT OR Apache-2.0 |
| `crates/lottie/tests/fixtures/legacy-masks.json` | EffectCraft contributors | Original work: hand-written Lottie test fixture (legacy keyframes, masks) | MIT OR Apache-2.0 |
| `crates/svg/tests/fixtures/basic-shapes.svg` | EffectCraft contributors | Original work: hand-written SVG test fixture (basic shapes, groups, transforms) | MIT OR Apache-2.0 |
| `crates/svg/tests/fixtures/paths-gradients.svg` | EffectCraft contributors | Original work: hand-written SVG test fixture (path data, gradients, CSS, use) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/quad.gltf` | EffectCraft contributors | Original work: glTF 2.0 test fixture (textured quad, node hierarchy, translation animation; data URIs) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/quad.glb` | EffectCraft contributors | Original work: binary glTF test fixture (the same quad with its texture in the BIN chunk) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/cube.obj` | EffectCraft contributors | Original work: Wavefront OBJ test fixture (unit cube, two materials) | MIT OR Apache-2.0 |
| `crates/model/tests/fixtures/cube.mtl` | EffectCraft contributors | Original work: MTL test fixture (materials of cube.obj) | MIT OR Apache-2.0 |
Three small After Effects test projects of `crates/aep` are in this repository: `crates/aep/tests/fixtures/synthetic.aep` (original work, made with After Effects 2026 from own data, working space None, the embedded colour profile removed; MIT OR Apache-2.0, with its `.attribution` sidecar), `crates/aep/tests/fixtures/ai-mask-3099.aep` (original work, made with After Effects 2026 from four hand-written one-page PDFs named `.ai`, footage paths and host name neutralised, the embedded colour profiles replaced by stubs made with `crates/aep/examples/stubicc.rs`; MIT OR Apache-2.0, with its `.attribution` sidecar) and `crates/aep/tests/fixtures/text-synthetic.aep` (original work, made with After Effects 2026 by script from own data, working space None, no colour profile embedded, nothing changed after the save; MIT OR Apache-2.0, with its `.attribution` sidecar). The other After Effects test projects of `crates/aep` are not in this repository. They are original work of an EffectCraft contributor, generated with After Effects 2026 as own test data (MIT OR Apache-2.0, each file with its `.attribution` sidecar); they are in [storytold/effectcraft-corpus](https://github.com/storytold/effectcraft-corpus) with their sidecars, fetched by `cargo xtask aep-corpus` (commit-pinned, SHA-256-verified), and the tests read them only from the folder named by `EFFECTCRAFT_AEP_FIXTURES` (opt-in). Their embedded colour profiles are replaced by stubs made with `crates/aep/examples/stubicc.rs` (a header and a description, no colour data), so no vendor profile data is in them.
## Screenshots
Screenshots of EffectCraft itself, rendered headlessly, showing only procedurally generated demo content.
| Asset | Author | Source | Licence |
|---|---|---|---|
| `docs/images/effectcraft-hero.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft main window) | MIT OR Apache-2.0 |
| `docs/images/effectcraft-3d.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft Classic 3D) | MIT OR Apache-2.0 |
| `docs/images/effectcraft-graph-editor.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft Graph Editor) | MIT OR Apache-2.0 |
| `docs/images/effectcraft-effects.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft effects) | MIT OR Apache-2.0 |
| `docs/images/effectcraft-render-queue.png` | EffectCraft contributors | Original work: screenshot of EffectCraft (EffectCraft Render Queue panel with the demo compositions queued for H.264 and ProRes export) | MIT OR Apache-2.0 |
## Code-drawn assets (original work, MIT OR Apache-2.0)
- UI icons drawn in code: `crates/ui-egui/src/icons.rs`, including the original filled favorite star (2026-10-11, EffectCraft contributors).
- Procedural demo content (solids, shapes, gradients, text animations): `crates/engine/src/demo.rs`.
- Built-in project templates (Lower Third, Title Card, Logo Reveal, Kinetic Type, Social Square, Vertical Story,
Slideshow, 3D Text Orbit) built from engine commands, and their thumbnails rendered by our own renderer:
`crates/engine/src/templates.rs`.
- Text animator presets (Typewriter, Fade Up Characters, Bounce In Words, Tracking In, Scramble, Blur In, Jitter,
Drop In Lines): `crates/engine/presets/text_animators.json` (sidecar `.attribution`).
- Transition animation presets (23: wipes, clock and fan reveals, blinds, dissolves, card flips, CC wipes, fades):
`crates/engine/presets/transitions.json` (sidecar `.attribution`).
- Backgrounds animation presets (23: drifting gradients, fog, cloud and smoke, plasma and magma, cells and crystals, scrolling grids
and checks, pulse rings, bokeh, light sweep): `crates/engine/presets/backgrounds.json` (sidecar `.attribution`).
- Behaviors animation presets (22: wiggle, shake, float, orbit, bounce, drift, pendulum, spin, pulse, heartbeat, blink, strobe, flicker):
`crates/engine/presets/behaviors.json` (sidecar `.attribution`).
- Sample scripts (File ▸ Scripts) and ScriptUI panel: `crates/engine/scripts/Rename Layers.jsx`,
`crates/engine/scripts/Sort Layers by In Point.jsx`, `crates/engine/scripts/Create Null at Selected Layers.jsx`,
`crates/engine/scripts/Render Queue Batch.jsx`, `crates/engine/scripts/ScriptUI Panels/Layer Tools.jsx`
(sidecars `.attribution`).
- Bundled extension (ScriptUI panel) with its twelve built-in ease presets:
`extensions/scriptui-panels/Ease Presets.jsx` (sidecar `.attribution`).
- Example effect plug-in: `examples/plugins/posterize-bands` (WebAssembly ABI 1).
## Trained models (optional downloads, not bundled)
The repository and the installers contain no model weights. These models are downloaded only when
a user asks (Settings ▸ Roto Brush, Settings ▸ Face Tracking, or the `roto.model.*` /
`face.model.*` commands), unmodified from their authors' official location, checked against a
pinned SHA-256 and stored in the user's `models` folder with a `<file>.NOTICE.txt` naming their
authors, licence and source. EffectCraft runs them with its own Rust implementation and is not
affiliated with or endorsed by their authors.
| Model | Authors | Source | Licence |
|---|---|---|---|
| MobileSAM (`mobile_sam.pt`) | Chaoning Zhang et al., building on Meta AI's Segment Anything and Microsoft's TinyViT | https://github.com/ChaoningZhang/MobileSAM | Apache-2.0 |
| MediaPipe Face Landmarker (`face_landmarker.task`: BlazeFace short-range detector, Face Mesh V2, blendshapes) | Google LLC | https://developers.google.com/edge/mediapipe/solutions/vision/face_landmarker (model cards: https://storage.googleapis.com/mediapipe-assets/MediaPipe%20BlazeFace%20Model%20Card%20(Short%20Range).pdf, https://storage.googleapis.com/mediapipe-assets/Model%20Card%20MediaPipe%20Face%20Mesh%20V2.pdf) | Apache-2.0 |
## Formats and parameters from other open-source projects
Written in Rust for EffectCraft; no code is copied. They follow published specifications and
configuration from these Apache-2.0 projects:
- `crates/segment/src/tflite.rs` reads TensorFlow Lite files using the field layout of TensorFlow
Lite's `schema.fbs` (The TensorFlow Authors, https://github.com/tensorflow/tensorflow,
Apache-2.0).
- `crates/segment/src/mediapipe.rs` follows MediaPipe's published face landmarker graph: the SSD
anchor layout, detection decoding, crop sizes and the face-mesh point indices it uses (Google
LLC, https://github.com/google-ai-edge/mediapipe, Apache-2.0).
- `crates/segment/src/mobilesam.rs` follows the MobileSAM, TinyViT and Segment Anything
architectures and their Apache-2.0 reference code.
## Frozen matte regression fixture
| Asset | Author | Source | Licence |
|---|---|---|---|
| `crates/engine/src/tests/fixtures/analysed-0.24.ecproj` | arnav-hiwarkar | Original work, synthetic project keyed by EffectCraft 0.24.1 | MIT OR Apache-2.0 |
| `crates/engine/src/tests/fixtures/object-matte-0.22.ecproj` | lisn0 | Original work, synthetic project saved by EffectCraft 0.22.0 | MIT OR Apache-2.0 |
| `crates/engine/src/tests/fixtures/tracked-0.22.ecproj` | arnav-hiwarkar | Original work, synthetic project saved by EffectCraft 0.22.0 | MIT OR Apache-2.0 |
## First-party brand marks
The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team. They are not open source and are not
covered by the project licence; they may be used only unmodified and only as part of this repository and EffectCraft,
under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). Forks and modified versions must remove them
(AGENTS.md §1.8).
| File | Author | Source | Licence |
|---|---|---|---|
| `docs/brand/artcraft-logo-white.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-logo-white.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-logo.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-logo.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-mark.png` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
| `docs/brand/artcraft-mark.svg` | ArtCraft Team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft brand terms, not open source (`docs/brand/LICENSE-brand.txt`) |
- Appearance theme previews, drawn procedurally from original shapes: `crates/ui-egui/src/panels/settings.rs` (original work, MIT OR Apache-2.0).
+1 -1
View File
@@ -1 +1 @@
@AGENTS.md
@AGENTS.md
+109 -109
View File
@@ -1,109 +1,109 @@
# Contributing to EffectCraft
Thanks for helping. Please read [AGENTS.md](AGENTS.md) first: its rules on assets and clean-room
work apply to everyone, people and AI agents alike, and they are not negotiable.
## Setup
You need [Rust](https://rustup.rs/) 1.95 or newer.
On Linux the desktop app plays preview sound through `cpal`, so the build needs a usable ALSA
pkg-config package: an `alsa.pc` that pkg-config can find, naming the linkable ALSA runtime
library (usually `libasound.so.2`). `alsa-sys` runs pkg-config at build time; it does not compile
ALSA's headers. Without a usable package the build fails at link time with an unrelated-looking
error; `cargo xtask ci` probes pkg-config up front and names the package:
```sh
sudo dnf install alsa-lib-devel # Fedora
sudo apt install libasound2-dev # Debian / Ubuntu
sudo pacman -S alsa-lib # Arch
```
If you cannot install the package, point `PKG_CONFIG_PATH` at a directory holding an `alsa.pc`
whose `Libs` line names your system's `libasound` (usually `libasound.so.2`); every CI run needs
that variable set.
```sh
cargo run -p effectcraft -- --demo # the desktop app, with the demo project
cargo test --workspace
cargo xtask ci # what every commit must pass (dev-profile checks and tests)
```
The default `dev` profile keeps debug info to line tables: builds are fast and panics/backtraces
still name the file and line, but a debugger cannot show local variables. For an interactive
debugger session use the `dev-full` profile, which is the same build with full debug info:
```sh
cargo run --profile dev-full -p effectcraft -- --demo
```
### Windows
The MSVC toolchain, rustup's default on Windows, is what the releases use: with the Visual Studio
C++ build tools installed, the commands above work as they are.
The GNU toolchain (`x86_64-pc-windows-gnu`) needs a current mingw-w64 GCC and binutils first on
`PATH`. They provide the linker and the Windows import libraries, such as `libshlwapi.a` for
opening links from the app. With MSYS2:
```powershell
C:\msys64\usr\bin\pacman.exe -S --needed mingw-w64-x86_64-gcc
$env:PATH = "C:\msys64\mingw64\bin;$env:PATH"
cargo +stable-x86_64-pc-windows-gnu run -p effectcraft -- --demo
```
An older MinGW earlier on `PATH` can break the build: CodeBlocks' GCC 8.1 fails to link
`boa_engine` ([#1882](https://github.com/storytold/effectcraft/issues/1882)).
Rust's bundled LLD and self-contained libraries don't include every import library EffectCraft
needs, so on their own they stop with `unable to find library` for `-lshlwapi`, `-lktmw32`,
`-lpropsys` and `-lversion` ([#1886](https://github.com/storytold/effectcraft/issues/1886)). To
link that way, copy those four from mingw-w64 into a folder of its own and add that folder to the
search path in your own Cargo config (`%USERPROFILE%\.cargo\config.toml`). Don't add all of
mingw-w64's `lib` folder: its C runtime clashes with Rust's bundled one
(`undefined symbol: DllEntryPoint`).
```powershell
New-Item -ItemType Directory -Force C:\gnu-import-libs
foreach ($lib in "shlwapi", "ktmw32", "propsys", "version") {
Copy-Item "C:\msys64\mingw64\lib\lib$lib.a" C:\gnu-import-libs\
}
```
```toml
[target.x86_64-pc-windows-gnu]
linker = "rust-lld"
rustflags = ["-C", "link-self-contained=yes", "-L", "native=C:/gnu-import-libs"]
```
## Ground rules
- **Clean room.** Work from After Effects' public documentation and its observable behaviour only.
Never use Adobe artwork, presets or code, and never copy GPL/LGPL/AGPL code.
- **Every asset is attributed.** Add a `.attribution` sidecar and an [ATTRIBUTION.md](ATTRIBUTION.md)
row; `cargo xtask assets` checks it.
- **Layering.** Nothing below the interface layer depends on UI or OS crates; see
[docs/architecture.md](docs/architecture.md).
- **Everything is a command.** New features are engine commands with an id, label, menu path,
shortcut and parameters, so the UI, the CLI and the MCP server get them at once. Interactive
widgets register an automation id.
- **Never crash.** Non-test code returns errors instead of panicking: no `unwrap()`, `expect()`,
`panic!`, `unreachable!`, `todo!`, `unimplemented!` or `unsafe`, checked indexing and arithmetic on
input-derived values, and a regression test with every crash fix. See the "Never crash" section of
[AGENTS.md](AGENTS.md).
- **Tests with every change**, and green `cargo xtask ci` before every commit. One task per commit.
## How to add…
- **An effect:** add an `EffectSpec` in `crates/effects` (AE display name, category, parameters
with AE's names, defaults and ranges) and its render function, with a determinism test and a
behaviour test. If it reads the clock directly, add it to `TIME_DEPENDENT`.
- **A command:** register it with `cmd!` in `crates/engine/src/commands/`, place it in the menu
tree in `crates/engine/src/menus.rs`, and test it including undo.
- **A panel or dialog:** `crates/ui-egui/src/panels/`; look at the result with the headless
snapshot tool described in [docs/testing.md](docs/testing.md).
## Talk to us
Questions, ideas and bug reports are welcome on [Discord](https://discord.gg/artcraft) and in
GitHub issues.
# Contributing to EffectCraft
Thanks for helping. Please read [AGENTS.md](AGENTS.md) first: its rules on assets and clean-room
work apply to everyone, people and AI agents alike, and they are not negotiable.
## Setup
You need [Rust](https://rustup.rs/) 1.95 or newer.
On Linux the desktop app plays preview sound through `cpal`, so the build needs a usable ALSA
pkg-config package: an `alsa.pc` that pkg-config can find, naming the linkable ALSA runtime
library (usually `libasound.so.2`). `alsa-sys` runs pkg-config at build time; it does not compile
ALSA's headers. Without a usable package the build fails at link time with an unrelated-looking
error; `cargo xtask ci` probes pkg-config up front and names the package:
```sh
sudo dnf install alsa-lib-devel # Fedora
sudo apt install libasound2-dev # Debian / Ubuntu
sudo pacman -S alsa-lib # Arch
```
If you cannot install the package, point `PKG_CONFIG_PATH` at a directory holding an `alsa.pc`
whose `Libs` line names your system's `libasound` (usually `libasound.so.2`); every CI run needs
that variable set.
```sh
cargo run -p effectcraft -- --demo # the desktop app, with the demo project
cargo test --workspace
cargo xtask ci # what every commit must pass (dev-profile checks and tests)
```
The default `dev` profile keeps debug info to line tables: builds are fast and panics/backtraces
still name the file and line, but a debugger cannot show local variables. For an interactive
debugger session use the `dev-full` profile, which is the same build with full debug info:
```sh
cargo run --profile dev-full -p effectcraft -- --demo
```
### Windows
The MSVC toolchain, rustup's default on Windows, is what the releases use: with the Visual Studio
C++ build tools installed, the commands above work as they are.
The GNU toolchain (`x86_64-pc-windows-gnu`) needs a current mingw-w64 GCC and binutils first on
`PATH`. They provide the linker and the Windows import libraries, such as `libshlwapi.a` for
opening links from the app. With MSYS2:
```powershell
C:\msys64\usr\bin\pacman.exe -S --needed mingw-w64-x86_64-gcc
$env:PATH = "C:\msys64\mingw64\bin;$env:PATH"
cargo +stable-x86_64-pc-windows-gnu run -p effectcraft -- --demo
```
An older MinGW earlier on `PATH` can break the build: CodeBlocks' GCC 8.1 fails to link
`boa_engine` ([#1882](https://github.com/storytold/effectcraft/issues/1882)).
Rust's bundled LLD and self-contained libraries don't include every import library EffectCraft
needs, so on their own they stop with `unable to find library` for `-lshlwapi`, `-lktmw32`,
`-lpropsys` and `-lversion` ([#1886](https://github.com/storytold/effectcraft/issues/1886)). To
link that way, copy those four from mingw-w64 into a folder of its own and add that folder to the
search path in your own Cargo config (`%USERPROFILE%\.cargo\config.toml`). Don't add all of
mingw-w64's `lib` folder: its C runtime clashes with Rust's bundled one
(`undefined symbol: DllEntryPoint`).
```powershell
New-Item -ItemType Directory -Force C:\gnu-import-libs
foreach ($lib in "shlwapi", "ktmw32", "propsys", "version") {
Copy-Item "C:\msys64\mingw64\lib\lib$lib.a" C:\gnu-import-libs\
}
```
```toml
[target.x86_64-pc-windows-gnu]
linker = "rust-lld"
rustflags = ["-C", "link-self-contained=yes", "-L", "native=C:/gnu-import-libs"]
```
## Ground rules
- **Clean room.** Work from After Effects' public documentation and its observable behaviour only.
Never use Adobe artwork, presets or code, and never copy GPL/LGPL/AGPL code.
- **Every asset is attributed.** Add a `.attribution` sidecar and an [ATTRIBUTION.md](ATTRIBUTION.md)
row; `cargo xtask assets` checks it.
- **Layering.** Nothing below the interface layer depends on UI or OS crates; see
[docs/architecture.md](docs/architecture.md).
- **Everything is a command.** New features are engine commands with an id, label, menu path,
shortcut and parameters, so the UI, the CLI and the MCP server get them at once. Interactive
widgets register an automation id.
- **Never crash.** Non-test code returns errors instead of panicking: no `unwrap()`, `expect()`,
`panic!`, `unreachable!`, `todo!`, `unimplemented!` or `unsafe`, checked indexing and arithmetic on
input-derived values, and a regression test with every crash fix. See the "Never crash" section of
[AGENTS.md](AGENTS.md).
- **Tests with every change**, and green `cargo xtask ci` before every commit. One task per commit.
## How to add…
- **An effect:** add an `EffectSpec` in `crates/effects` (AE display name, category, parameters
with AE's names, defaults and ranges) and its render function, with a determinism test and a
behaviour test. If it reads the clock directly, add it to `TIME_DEPENDENT`.
- **A command:** register it with `cmd!` in `crates/engine/src/commands/`, place it in the menu
tree in `crates/engine/src/menus.rs`, and test it including undo.
- **A panel or dialog:** `crates/ui-egui/src/panels/`; look at the result with the headless
snapshot tool described in [docs/testing.md](docs/testing.md).
## Talk to us
Questions, ideas and bug reports are welcome on [Discord](https://discord.gg/artcraft) and in
GitHub issues.
Generated
+8262 -8262
View File
File diff suppressed because it is too large. Load diff
+249 -249
View File
@@ -1,249 +1,249 @@
[workspace]
resolver = "3"
members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/*"]
[workspace.package]
version = "0.24.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
repository = "https://github.com/storytold/effectcraft"
[workspace.dependencies]
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", version = "=0.1.0", default-features = false, features = ["read"] }
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-quit = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-ecproj-format = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-dpx = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-eps = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-exr = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-gif = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-psd = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-svg = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-webp = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-lottie = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-mesh = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", default-features = false, features = ["stl", "obj", "gltf", "fbx"] }
craft-aep = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
# craft-update: app-specific pretend versions apply to startup and manual checks.
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "47107f660191de2f0b425569506f4daa4faadac9", features = ["egui"] }
# internal
effectcraft-time = { path = "crates/time" }
effectcraft-geom = { path = "crates/geom" }
effectcraft-color = { path = "crates/color" }
effectcraft-raster = { path = "crates/raster" }
effectcraft-path = { path = "crates/path" }
effectcraft-keyframe = { path = "crates/keyframe" }
effectcraft-project = { path = "crates/project" }
effectcraft-text = { path = "crates/text" }
effectcraft-effects = { path = "crates/effects" }
effectcraft-track = { path = "crates/track" }
effectcraft-segment = { path = "crates/segment" }
effectcraft-sam2 = { path = "crates/sam2" }
effectcraft-model = { path = "crates/model" }
effectcraft-render = { path = "crates/render" }
effectcraft-gpu = { path = "crates/gpu" }
effectcraft-media = { path = "crates/media" }
effectcraft-export = { path = "crates/export" }
effectcraft-expr = { path = "crates/expr" }
effectcraft-lottie = { path = "crates/lottie" }
effectcraft-interchange = { path = "crates/interchange" }
effectcraft-pdf = { path = "crates/pdf" }
effectcraft-psd = { path = "crates/psd" }
effectcraft-aep = { path = "crates/aep" }
effectcraft-svg = { path = "crates/svg" }
effectcraft-engine = { path = "crates/engine" }
effectcraft-host = { path = "crates/host" }
effectcraft-ui-egui = { path = "crates/ui-egui" }
effectcraft-automation = { path = "crates/automation" }
effectcraft-opusenc = { path = "crates/opusenc" }
effectcraft-script = { path = "crates/script" }
effectcraft-plugin = { path = "crates/plugin" }
effectcraft-helper-host = { path = "crates/helper-host" }
# foundation
thiserror = "2"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["float_roundtrip"] }
log = "0.4"
# All date/time conversion goes through chrono (no hand-rolled calendar code beyond the 400-year range extension; AGENTS.md).
chrono = { version = "0.4.45", default-features = false, features = ["std"] }
rayon = "1"
# std::time::Instant/SystemTime on native; the browser clock on wasm32 (std's panic there).
web-time = "1"
gif = "0.14"
proptest = "1"
# scripting (expressions)
boa_engine = { version = "0.22", default-features = false }
# WebAssembly effect plug-ins (sandboxed interpreter; MIT/Apache-2.0, pure Rust)
wasmi = { version = "2", default-features = false, features = ["std", "validate", "wat", "deterministic", "portable-dispatch"] }
# geometry / fonts
kurbo = { version = "0.13", features = ["serde"] }
tiny-skia = { version = "0.12", default-features = false, features = ["std", "simd"] }
skrifa = { version = "0.44", default-features = false, features = ["std"] }
harfrust = { version = "0.12", default-features = false, features = ["std"] }
exr = "1.74"
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
same-file = "1.0"
tempfile = "3.27"
# codecs + containers shared by the Craft apps (crates/media, crates/export); keep all three on one rev.
craft-media = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", default-features = false, features = ["isobmff", "matroska"] }
craft-audio = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", default-features = false, features = ["aac-decode", "aac-encode", "opus-decode"] }
craft-video = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", features = ["h264-encode", "prores", "hevc-encode", "vp9-encode", "av1", "threads"] }
# footage decoding: FilmCraft's pure-Rust crates, pinned to a revision (plan/adr/0001).
# Only `crates/media`, `crates/export` and `crates/interchange` may depend on these.
filmcraft-codecs = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-media = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-frame = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-color = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-time = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
# MPEG TS / PS headers read for AAC channel layouts (crates/media); craft-media has no MPEG TS reader
filmcraft-mpegts = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
# decoders used as the round-trip oracles of craft-video's VP9, HEVC and AV1 encoders (crates/export
# dev-dependencies only); craft-video has no decoder for them
filmcraft-vp9 = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-hevc = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-av1 = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
# timeline interchange (EDL, FCP7 XML, FCPXML, OTIO) for Premiere Pro interop (crates/interchange only)
filmcraft-interchange = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-project = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-geom = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
# gpu (the same wgpu eframe / egui-wgpu use)
wgpu = { version = "30.0.1", features = ["fragile-send-sync-non-atomic-wasm"] }
bytemuck = "1"
pollster = "1"
half = "2.7"
# ui
# Shared frontend components; keep this dependency above the engine boundary.
# Shared UI and the complete egui family select reviewed immutable commits below.
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", features = ["serde"] }
# `default_fonts` and `monochrome_emoji_fonts` are explicit: the app's `FontDefinitions::default()`
# and its `✔`/`▣` chrome glyphs need both, and kittest's own dev-dependencies do not propagate.
egui = { version = "0.37", default-features = false, features = ["default_fonts", "monochrome_emoji_fonts"] }
eframe = { version = "0.37", default-features = false }
# Resizable table columns (About ▸ Contributors / Models); no image loaders.
egui_extras = { version = "0.37", default-features = false }
egui_kittest = "0.37"
rfd = "0.17"
cpal = "0.16"
# The operating system's interface language (MIT/Apache-2.0, no dependencies on desktop).
sys-locale = "0.3"
# Local co-development against a FilmCraft checkout next to this repo: uncomment.
# [patch."https://github.com/storytold/filmcraft"]
# filmcraft-codecs = { path = "../filmcraft/crates/codecs" }
# filmcraft-media = { path = "../filmcraft/crates/media" }
# filmcraft-frame = { path = "../filmcraft/crates/frame" }
# filmcraft-color = { path = "../filmcraft/crates/color" }
# filmcraft-time = { path = "../filmcraft/crates/time" }
# filmcraft-mpegts = { path = "../filmcraft/crates/mpegts" }
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
cognitive_complexity = "deny"
too_many_arguments = "deny"
wildcard_imports = "deny"
dbg_macro = "warn"
todo = "warn"
needless_range_loop = "allow"
type_complexity = "allow"
[profile.dev.package."*"]
opt-level = 2
debug = false
[profile.dev]
opt-level = 1
# Panics and backtraces keep file and line; full debug info only made the many test binaries large.
debug = "line-tables-only"
# Same settings as `dev`, but with full debug info for interactive debugger sessions: local
# variables, parameters and column breakpoints work. Use `--profile dev-full` when debugging;
# everything else stays on the fast default. See CONTRIBUTING.md, "Setup".
[profile.dev-full]
inherits = "dev"
debug = true
# Pixel hot loops: keep debug builds interactive.
[profile.dev.package.effectcraft-raster]
opt-level = 3
[profile.dev.package.effectcraft-render]
opt-level = 3
[profile.dev.package.effectcraft-gpu]
opt-level = 3
[profile.dev.package.effectcraft-effects]
opt-level = 3
[profile.dev.package.effectcraft-track]
opt-level = 3
[profile.dev.package.effectcraft-path]
opt-level = 3
[profile.dev.package.effectcraft-media]
opt-level = 3
[profile.dev.package.effectcraft-opusenc]
opt-level = 3
[profile.dev.package.effectcraft-export]
opt-level = 3
# craft-libs codec hot loops.
[profile.dev.package.craft-video]
opt-level = 3
[profile.dev.package.craft-audio]
opt-level = 3
[profile.dev.package.craft-media]
opt-level = 3
# FilmCraft codec hot loops (same settings as FilmCraft's own workspace).
[profile.dev.package.filmcraft-h264]
opt-level = 3
[profile.dev.package.filmcraft-hevc]
opt-level = 3
[profile.dev.package.filmcraft-vp9]
opt-level = 3
[profile.dev.package.filmcraft-dnx]
opt-level = 3
[profile.dev.package.filmcraft-av1]
opt-level = 3
[profile.release]
lto = "thin"
codegen-units = 1
[patch.crates-io]
hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", version = "=0.7.2" }
hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", version = "=0.3.0" }
ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
epaint_default_fonts = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui-winit = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui-wgpu = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui_glow = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui_extras = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui_kittest = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
eframe = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
# Patch a crate only together with the dependency that needs it: an unused patch makes the Nix
# build, which uses vendored sources, fail (`cargo xtask lock-patches`).
winit = { git = "https://github.com/storytold/winit.git", rev = "a517b3846ee8457d84ee4975110bfda6797c44dc" }
[workspace]
resolver = "3"
members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/*"]
[workspace.package]
version = "0.24.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
repository = "https://github.com/storytold/effectcraft"
[workspace.dependencies]
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", version = "=0.1.0", default-features = false, features = ["read"] }
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-quit = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-ecproj-format = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-dpx = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-eps = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-exr = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-gif = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-psd = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-svg = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-webp = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-lottie = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
craft-mesh = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", default-features = false, features = ["stl", "obj", "gltf", "fbx"] }
craft-aep = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9" }
# craft-update: app-specific pretend versions apply to startup and manual checks.
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "47107f660191de2f0b425569506f4daa4faadac9", features = ["egui"] }
# internal
effectcraft-time = { path = "crates/time" }
effectcraft-geom = { path = "crates/geom" }
effectcraft-color = { path = "crates/color" }
effectcraft-raster = { path = "crates/raster" }
effectcraft-path = { path = "crates/path" }
effectcraft-keyframe = { path = "crates/keyframe" }
effectcraft-project = { path = "crates/project" }
effectcraft-text = { path = "crates/text" }
effectcraft-effects = { path = "crates/effects" }
effectcraft-track = { path = "crates/track" }
effectcraft-segment = { path = "crates/segment" }
effectcraft-sam2 = { path = "crates/sam2" }
effectcraft-model = { path = "crates/model" }
effectcraft-render = { path = "crates/render" }
effectcraft-gpu = { path = "crates/gpu" }
effectcraft-media = { path = "crates/media" }
effectcraft-export = { path = "crates/export" }
effectcraft-expr = { path = "crates/expr" }
effectcraft-lottie = { path = "crates/lottie" }
effectcraft-interchange = { path = "crates/interchange" }
effectcraft-pdf = { path = "crates/pdf" }
effectcraft-psd = { path = "crates/psd" }
effectcraft-aep = { path = "crates/aep" }
effectcraft-svg = { path = "crates/svg" }
effectcraft-engine = { path = "crates/engine" }
effectcraft-host = { path = "crates/host" }
effectcraft-ui-egui = { path = "crates/ui-egui" }
effectcraft-automation = { path = "crates/automation" }
effectcraft-opusenc = { path = "crates/opusenc" }
effectcraft-script = { path = "crates/script" }
effectcraft-plugin = { path = "crates/plugin" }
effectcraft-helper-host = { path = "crates/helper-host" }
# foundation
thiserror = "2"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["float_roundtrip"] }
log = "0.4"
# All date/time conversion goes through chrono (no hand-rolled calendar code beyond the 400-year range extension; AGENTS.md).
chrono = { version = "0.4.45", default-features = false, features = ["std"] }
rayon = "1"
# std::time::Instant/SystemTime on native; the browser clock on wasm32 (std's panic there).
web-time = "1"
gif = "0.14"
proptest = "1"
# scripting (expressions)
boa_engine = { version = "0.22", default-features = false }
# WebAssembly effect plug-ins (sandboxed interpreter; MIT/Apache-2.0, pure Rust)
wasmi = { version = "2", default-features = false, features = ["std", "validate", "wat", "deterministic", "portable-dispatch"] }
# geometry / fonts
kurbo = { version = "0.13", features = ["serde"] }
tiny-skia = { version = "0.12", default-features = false, features = ["std", "simd"] }
skrifa = { version = "0.44", default-features = false, features = ["std"] }
harfrust = { version = "0.12", default-features = false, features = ["std"] }
exr = "1.74"
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
same-file = "1.0"
tempfile = "3.27"
# codecs + containers shared by the Craft apps (crates/media, crates/export); keep all three on one rev.
craft-media = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", default-features = false, features = ["isobmff", "matroska"] }
craft-audio = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", default-features = false, features = ["aac-decode", "aac-encode", "opus-decode"] }
craft-video = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", features = ["h264-encode", "prores", "hevc-encode", "vp9-encode", "av1", "threads"] }
# footage decoding: FilmCraft's pure-Rust crates, pinned to a revision (plan/adr/0001).
# Only `crates/media`, `crates/export` and `crates/interchange` may depend on these.
filmcraft-codecs = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-media = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-frame = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-color = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-time = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
# MPEG TS / PS headers read for AAC channel layouts (crates/media); craft-media has no MPEG TS reader
filmcraft-mpegts = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
# decoders used as the round-trip oracles of craft-video's VP9, HEVC and AV1 encoders (crates/export
# dev-dependencies only); craft-video has no decoder for them
filmcraft-vp9 = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-hevc = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-av1 = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
# timeline interchange (EDL, FCP7 XML, FCPXML, OTIO) for Premiere Pro interop (crates/interchange only)
filmcraft-interchange = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-project = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
filmcraft-geom = { git = "https://github.com/storytold/filmcraft", rev = "d5ead4ba1ee2f1cd13cb86a4239aee2730a3d9f5" }
# gpu (the same wgpu eframe / egui-wgpu use)
wgpu = { version = "30.0.1", features = ["fragile-send-sync-non-atomic-wasm"] }
bytemuck = "1"
pollster = "1"
half = "2.7"
# ui
# Shared frontend components; keep this dependency above the engine boundary.
# Shared UI and the complete egui family select reviewed immutable commits below.
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", features = ["serde"] }
# `default_fonts` and `monochrome_emoji_fonts` are explicit: the app's `FontDefinitions::default()`
# and its `✔`/`▣` chrome glyphs need both, and kittest's own dev-dependencies do not propagate.
egui = { version = "0.37", default-features = false, features = ["default_fonts", "monochrome_emoji_fonts"] }
eframe = { version = "0.37", default-features = false }
# Resizable table columns (About ▸ Contributors / Models); no image loaders.
egui_extras = { version = "0.37", default-features = false }
egui_kittest = "0.37"
rfd = "0.17"
cpal = "0.16"
# The operating system's interface language (MIT/Apache-2.0, no dependencies on desktop).
sys-locale = "0.3"
# Local co-development against a FilmCraft checkout next to this repo: uncomment.
# [patch."https://github.com/storytold/filmcraft"]
# filmcraft-codecs = { path = "../filmcraft/crates/codecs" }
# filmcraft-media = { path = "../filmcraft/crates/media" }
# filmcraft-frame = { path = "../filmcraft/crates/frame" }
# filmcraft-color = { path = "../filmcraft/crates/color" }
# filmcraft-time = { path = "../filmcraft/crates/time" }
# filmcraft-mpegts = { path = "../filmcraft/crates/mpegts" }
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
cognitive_complexity = "deny"
too_many_arguments = "deny"
wildcard_imports = "deny"
dbg_macro = "warn"
todo = "warn"
needless_range_loop = "allow"
type_complexity = "allow"
[profile.dev.package."*"]
opt-level = 2
debug = false
[profile.dev]
opt-level = 1
# Panics and backtraces keep file and line; full debug info only made the many test binaries large.
debug = "line-tables-only"
# Same settings as `dev`, but with full debug info for interactive debugger sessions: local
# variables, parameters and column breakpoints work. Use `--profile dev-full` when debugging;
# everything else stays on the fast default. See CONTRIBUTING.md, "Setup".
[profile.dev-full]
inherits = "dev"
debug = true
# Pixel hot loops: keep debug builds interactive.
[profile.dev.package.effectcraft-raster]
opt-level = 3
[profile.dev.package.effectcraft-render]
opt-level = 3
[profile.dev.package.effectcraft-gpu]
opt-level = 3
[profile.dev.package.effectcraft-effects]
opt-level = 3
[profile.dev.package.effectcraft-track]
opt-level = 3
[profile.dev.package.effectcraft-path]
opt-level = 3
[profile.dev.package.effectcraft-media]
opt-level = 3
[profile.dev.package.effectcraft-opusenc]
opt-level = 3
[profile.dev.package.effectcraft-export]
opt-level = 3
# craft-libs codec hot loops.
[profile.dev.package.craft-video]
opt-level = 3
[profile.dev.package.craft-audio]
opt-level = 3
[profile.dev.package.craft-media]
opt-level = 3
# FilmCraft codec hot loops (same settings as FilmCraft's own workspace).
[profile.dev.package.filmcraft-h264]
opt-level = 3
[profile.dev.package.filmcraft-hevc]
opt-level = 3
[profile.dev.package.filmcraft-vp9]
opt-level = 3
[profile.dev.package.filmcraft-dnx]
opt-level = 3
[profile.dev.package.filmcraft-av1]
opt-level = 3
[profile.release]
lto = "thin"
codegen-units = 1
[patch.crates-io]
hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", version = "=0.7.2" }
hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "47107f660191de2f0b425569506f4daa4faadac9", version = "=0.3.0" }
ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
epaint_default_fonts = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui-winit = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui-wgpu = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui_glow = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui_extras = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
egui_kittest = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
eframe = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
# Patch a crate only together with the dependency that needs it: an unused patch makes the Nix
# build, which uses vendored sources, fail (`cargo xtask lock-patches`).
winit = { git = "https://github.com/storytold/winit.git", rev = "a517b3846ee8457d84ee4975110bfda6797c44dc" }
+177 -177
View File
@@ -1,177 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+21 -21
View File
@@ -1,21 +1,21 @@
MIT License
Copyright (c) 2026 ArtCraft Team and the EffectCraft contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License
Copyright (c) 2026 ArtCraft Team and the EffectCraft contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+322 -322
View File
@@ -1,322 +1,322 @@
EffectCraft
Copyright (c) 2026 ArtCraft Team and the EffectCraft contributors.
Licensed under the MIT License or the Apache License, Version 2.0, at your option
(LICENSE-MIT, LICENSE-APACHE).
The ArtCraft name, wordmark and logos in docs/brand/ are trademarks of the ArtCraft Team. They are
not open source and are not covered by the license above; their terms are in
docs/brand/LICENSE-brand.txt.
Third-party material
This product bundles third-party material under its own licenses, listed below. The per-file
list (author, source, license) is in ATTRIBUTION.md.
Bundled fonts
EffectCraft embeds Inter, Noto Serif, JetBrains Mono and Noto Sans Symbols 2 from
assets/fonts/. The pinned egui dependency (epaint_default_fonts, revision
6d518fc799681571963842f95b55a6b68431b15a) additionally embeds Ubuntu Light, Hack,
Noto Emoji, egui-icons and emoji-icon-font through EffectCraft's enabled `default_fonts`
and `monochrome_emoji_fonts` features; no font file from that dependency is added to
this repository. The complete copyright notices and licence texts required for all of
these fonts follow.
The optional PDF document reader uses craft-pdf and Hayro-syntax 0.7.2, MIT OR Apache-2.0.
The shared patched syntax source is centralized in storytold/craft-libs, transferred from
PdfCraft revision 9ed3abe4907a127d36b12fbfeee71d7e26248da1. This parser-only use embeds no
Hayro fonts, CMaps or image codecs; EffectCraft keeps its existing fonts and drawing filters.
Linux Wayland window decorations
-------------------------------
sctk-adwaita 0.10.1 (https://github.com/PolyMeilex/sctk-adwaita,
commit d5f343c45c04d4a026db57f76760819b7325db00) is used by winit on Linux
for vector window controls. Its title-font features are disabled: no Cantarell
font is embedded by this dependency and no font file is added to this repo.
sctk-adwaita's MIT licence:
MIT License
Copyright (c) 2022 Bartłomiej Maryńczak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SIL Open Font License 1.1
Applies to Inter (assets/fonts/Inter-*.ttf), Noto Serif (assets/fonts/NotoSerif-Regular.ttf),
JetBrains Mono (assets/fonts/JetBrainsMono-Regular.ttf), Noto Sans Symbols 2
(assets/fonts/NotoSansSymbols2-Regular.ttf) and Noto Emoji (NotoEmoji-Regular.ttf).
Copyright notices:
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/symbols)
Copyright 2013 Google Inc. All Rights Reserved.
The fonts in assets/fonts/ also keep their upstream licence files at
assets/fonts/OFL-<family>.txt. The complete licence text follows.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Ubuntu Font Licence 1.0
Applies to Ubuntu Light (Ubuntu-Light.ttf). Copyright notice:
Copyright 2011 Canonical Ltd. Licensed under the Ubuntu Font Licence 1.0
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.
Hack: MIT License and Bitstream Vera License
Applies to Hack (Hack-Regular.ttf). Upstream notice and both licence texts:
The work in the Hack project is Copyright 2018 Source Foundry Authors and licensed under the MIT License
The work in the DejaVu project was committed to the public domain.
Bitstream Vera Sans Mono Copyright 2003 Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera"
MIT License
Copyright (c) 2018 Source Foundry Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
BITSTREAM VERA LICENSE
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions:
The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera".
This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names.
The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org.
MIT License (egui-icons and emoji-icon-font)
Applies to egui-icons.ttf, a five-glyph subset of emoji-icon-font, and the parent
emoji-icon-font.ttf, both by John Slegers, https://github.com/jslegers/emoji-icon-font.
MIT License
Copyright (c) 2014 John Slegers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
EffectCraft
Copyright (c) 2026 ArtCraft Team and the EffectCraft contributors.
Licensed under the MIT License or the Apache License, Version 2.0, at your option
(LICENSE-MIT, LICENSE-APACHE).
The ArtCraft name, wordmark and logos in docs/brand/ are trademarks of the ArtCraft Team. They are
not open source and are not covered by the license above; their terms are in
docs/brand/LICENSE-brand.txt.
Third-party material
This product bundles third-party material under its own licenses, listed below. The per-file
list (author, source, license) is in ATTRIBUTION.md.
Bundled fonts
EffectCraft embeds Inter, Noto Serif, JetBrains Mono and Noto Sans Symbols 2 from
assets/fonts/. The pinned egui dependency (epaint_default_fonts, revision
6d518fc799681571963842f95b55a6b68431b15a) additionally embeds Ubuntu Light, Hack,
Noto Emoji, egui-icons and emoji-icon-font through EffectCraft's enabled `default_fonts`
and `monochrome_emoji_fonts` features; no font file from that dependency is added to
this repository. The complete copyright notices and licence texts required for all of
these fonts follow.
The optional PDF document reader uses craft-pdf and Hayro-syntax 0.7.2, MIT OR Apache-2.0.
The shared patched syntax source is centralized in storytold/craft-libs, transferred from
PdfCraft revision 9ed3abe4907a127d36b12fbfeee71d7e26248da1. This parser-only use embeds no
Hayro fonts, CMaps or image codecs; EffectCraft keeps its existing fonts and drawing filters.
Linux Wayland window decorations
-------------------------------
sctk-adwaita 0.10.1 (https://github.com/PolyMeilex/sctk-adwaita,
commit d5f343c45c04d4a026db57f76760819b7325db00) is used by winit on Linux
for vector window controls. Its title-font features are disabled: no Cantarell
font is embedded by this dependency and no font file is added to this repo.
sctk-adwaita's MIT licence:
MIT License
Copyright (c) 2022 Bartłomiej Maryńczak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SIL Open Font License 1.1
Applies to Inter (assets/fonts/Inter-*.ttf), Noto Serif (assets/fonts/NotoSerif-Regular.ttf),
JetBrains Mono (assets/fonts/JetBrainsMono-Regular.ttf), Noto Sans Symbols 2
(assets/fonts/NotoSansSymbols2-Regular.ttf) and Noto Emoji (NotoEmoji-Regular.ttf).
Copyright notices:
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/symbols)
Copyright 2013 Google Inc. All Rights Reserved.
The fonts in assets/fonts/ also keep their upstream licence files at
assets/fonts/OFL-<family>.txt. The complete licence text follows.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Ubuntu Font Licence 1.0
Applies to Ubuntu Light (Ubuntu-Light.ttf). Copyright notice:
Copyright 2011 Canonical Ltd. Licensed under the Ubuntu Font Licence 1.0
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.
Hack: MIT License and Bitstream Vera License
Applies to Hack (Hack-Regular.ttf). Upstream notice and both licence texts:
The work in the Hack project is Copyright 2018 Source Foundry Authors and licensed under the MIT License
The work in the DejaVu project was committed to the public domain.
Bitstream Vera Sans Mono Copyright 2003 Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera"
MIT License
Copyright (c) 2018 Source Foundry Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
BITSTREAM VERA LICENSE
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions:
The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera".
This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names.
The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org.
MIT License (egui-icons and emoji-icon-font)
Applies to egui-icons.ttf, a five-glyph subset of emoji-icon-font, and the parent
emoji-icon-font.ttf, both by John Slegers, https://github.com/jslegers/emoji-icon-font.
MIT License
Copyright (c) 2014 John Slegers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+399 -399
View File
@@ -1,399 +1,399 @@
<p align="center">
<a href="https://getartcraft.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/brand/artcraft-logo-white.svg">
<img alt="ArtCraft" src="docs/brand/artcraft-logo.svg" width="200">
</picture>
</a>
</p>
<p align="center">
<img src="assets/app-icon/hicolor/256x256/apps/ai.storyteller.effectcraft.png" width="128" height="128" alt="EffectCraft icon: an engraved unicorn on a pink tile">
</p>
<h1 align="center">EffectCraft</h1>
<p align="center">
<b>Motion graphics and visual effects; an open-source, professional motion graphics and visual effects studio, built in pure Rust.</b>
</p>
<p align="center">
A free, open-source compositor: compositions, layers,
keyframes, 308 effects, layer styles, expressions, 3D cameras and lights, and a render queue, native on macOS,
Windows and Linux, and in the browser. Young, moving fast, and already usable.
</p>
<p align="center">
<img alt="Status: young and moving fast" src="https://img.shields.io/badge/status-young%20and%20moving%20fast-e0368f?style=flat-square">
<img alt="Written in Rust" src="https://img.shields.io/badge/rust-1.95%2B-b0206c?style=flat-square&logo=rust&logoColor=white">
<img alt="License: MIT OR Apache-2.0" src="https://img.shields.io/badge/license-MIT%20%2F%20Apache--2.0-555?style=flat-square">
<br>
<a href="https://sonarcloud.io/project/overview?id=storytold_effectcraft"><img alt="Scanned by SonarCloud" src="https://img.shields.io/badge/Scanned_by-SonarCloud-4b9fd5?style=flat-square&logo=sonarqubecloud&logoColor=white"></a>
<a href="https://coderabbit.ai"><img alt="Reviewed by CodeRabbit" src="https://img.shields.io/coderabbit/prs/github/storytold/effectcraft?style=flat-square&label=Reviewed%20by%20CodeRabbit&logo=coderabbit"></a>
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Join the ArtCraft community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
</p>
<p align="center">
<a href="https://getartcraft.com/apps/effectcraft"><b>EffectCraft on getartcraft.com</b></a> ·
<a href="https://getartcraft.com/">ArtCraft</a> ·
<a href="https://getartcraft.com/apps">All Crafting Apps</a>
</p>
<br>
<p align="center">
<img src="docs/images/effectcraft-hero.png" alt="EffectCraft's main window: the animated demo composition in the Composition panel, the Project panel, a Timeline with text, shape and solid layers, and the Properties panel showing the selected text layer's transform, font and paragraph settings" width="100%">
</p>
> [!NOTE]
> **ArtCraft is a community of artists from all walks of life.** Digital, generative, music,
> games &mdash; if you make things, you're one of us. **[Come say hi on Discord](https://discord.gg/artcraft).**
<p align="center">
<a href="#what-effectcraft-is">What it is</a> ·
<a href="#animate">Animate</a> ·
<a href="#effects">Effects</a> ·
<a href="#3d">3D</a> ·
<a href="#export">Export</a> ·
<a href="#built-for-agents">Agents</a> ·
<a href="#get-started">Get started</a> ·
<a href="#where-it-stands">Status</a> ·
<a href="#how-its-made">How it's made</a> ·
<a href="#downloads">Downloads</a> ·
<a href="#the-crafting-apps">The Crafting Apps</a> ·
<a href="#license-and-credits">License</a>
</p>
## What EffectCraft is
EffectCraft is for animated titles, motion graphics and compositing work: the kind of thing
motion designers and VFX artists need. You build a composition out of layers (solids, shapes,
text, footage, other compositions), animate their properties with keyframes, stack effects on
them and render the result.
The interface feels natural and responsive, with dedicated panels (Project, Composition, Timeline,
Layer, Footage, Effect Controls, Tools, Preview, Effects & Presets, Audio, Info, Paragraph,
Character, Align, Progress, Render Queue) and standard motion graphics shortcuts, going further in
a few places where it matters to us:
- **Lottie import and export built in**, so animations can go straight to the web and apps
without a plugin.
- **A project file you can read.** Projects are versioned JSON (`.ecproj`), so they diff cleanly
in version control.
- **Everything is scriptable.** Menu actions, timeline edits and property changes are exposed as
commands. Engine commands are available through the CLI, JSON control channel and MCP;
UI-only commands use the frontend dispatcher and can be invoked through the control channel
or an attached MCP bridge.
- **No FFmpeg.** Video and audio decoding and encoding are pure Rust: FilmCraft's codecs and
EffectCraft's own VP9, AV1, HEVC and Opus encoders.
## Animate
The panels, menus and shortcuts follow standard motion design conventions:
Project, Composition, Timeline, Effect Controls, Properties, Effects & Presets, Character,
Paragraph, Align, Info, Preview, Audio and the Render Queue, docked the way you expect. Drag
footage, comps and effects between them: onto the comp viewer, where they land under the pointer,
or into the Timeline, between layers and at the time you point to.
- **Layers of every kind:** solids, shapes, text, footage, nested compositions, nulls,
adjustment layers, cameras and lights; parenting, track mattes, all 38 blend modes, motion blur.
- **Keyframes that behave the same:** linear, Bezier, hold, auto and continuous Bezier, roving
keys, Easy Ease (F9), Keyframe Velocity and Interpolation dialogs, copy and paste at the current
time, and a **Graph Editor** with value and speed graphs and draggable handles.
- **Time:** time remapping, time stretch, time-reverse, freeze frame, work area, markers, exact
frame-accurate timing at every frame rate including 29.97 drop-frame.
- **Shapes and masks:** shape layers with trim paths, repeaters, round corners, offset, zig zag,
twist, wiggle, merge paths and gradient strokes; masks drawn with the pen tool, with modes,
feather, expansion and vertex editing in the viewer.
- **Text:** point and paragraph text with real shaping in any font installed on your machine, the
Character and Paragraph panels, and text animators with range selectors.
- **Expressions:** JavaScript with the standard motion expressions model (`wiggle`, `loopOut`,
`thisComp.layer("…")`, vector maths on arrays), an inline editor and the pick-whip.
- **Timeline like you know it:** twirl layers open to Transform, masks, effects and the rest; drag
rows to reorder layers; rename with Enter or a double-click; the property shortcuts (A, P, S,
R, T, M, F, E, L, U and their double presses AA, PP, SS, RR, TT, MM, FF, EE, LL, UU), Shift to
add properties, Alt+Shift+A/P/S/R/T to key the current time, Ctrl+` to twirl selected layers.
- **Puppet tools:** Position, Advanced, Bend, Starch and Overlap pins on a mesh built from the
layer, real-time pin recording, rotate/scale handles, pins driven by (or carrying) nulls for
rigging, and follow-through for hair and cloth.
<p align="center">
<img src="docs/images/effectcraft-graph-editor.png" alt="The Graph Editor showing an eased value curve for a text animator's Range Start, with the graph editor's button bar for hold, linear, auto Bezier and Easy Ease" width="100%">
</p>
## Effects
308 built-in effects across its categories, each with its parameter names, order and
defaults: blur and sharpen, channel, color correction (Curves, Levels, Hue/Saturation, a full
color-grading panel…), distort (Warp, Bulge, Turbulent Displace, Perspective Pin…), generate (Fractal Noise,
Gradient Ramp, Stroke, Write-on, Audio Spectrum…), keying, matte, noise and grain, perspective,
**simulation** (Particle Emitter 3D, Rainfall, Shatter, Card Dance, Caustics, Wave World…),
stylize (Glow, Glass Relief…), **time** (Echo, Posterize Time, Timewarp, Time Displacement…),
**audio** (Reverb, Parametric EQ, Delay, Stereo Mixer…), text, transitions, utility and expression
controls. All nine **Layer Styles** (Drop Shadow, Inner/Outer Glow, Bevel and Emboss, Satin,
overlays, Stroke) with Global Light. Preview plays audio in sync, with meters and waveforms.
## 3D
Classic 3D compositing: 3D layers with orientation and material options,
one- and two-node **cameras** with depth of field, **lights** (parallel, spot, point, ambient)
with soft ray-traced shadows, layers that intersect correctly, orbit, pan and dolly camera
tools, and Front, Top, Left and Custom views.
<p align="center">
<img src="docs/images/effectcraft-3d.png" alt="The 3D Showcase demo in Custom View 1: intersecting cards lit by a spot light with soft shadows on a gridded floor, with the camera frustum and light drawn as wireframes" width="100%">
</p>
## Export
A dedicated Render Queue, with Render Settings and Output Modules: **H.264** MP4 and
**ProRes** MOV (Proxy to 4444 XQ with alpha) with audio, **HEVC** (Main / Main 10) and **AV1**
MP4, **WebM** (VP9 with inter frames and alpha, or AV1; Opus audio), PNG, JPEG, TIFF, Targa and 32-bit EXR sequences, animated GIF and WAV/AIFF. Field
rendering with 3:2 pulldown, effect/solo/guide/depth overrides, crop, region of interest and
resize, Render Settings and Output Module templates with defaults, post-render actions, storage
overflow and render logs. The same queue runs from the command line. Every encoder is pure
Rust (craft-libs' H.264, ProRes, HEVC, VP9, AV1 and AAC; EffectCraft's own Opus); there is no
FFmpeg inside.
**Lottie** goes both ways: File ▸ Export ▸ Lottie JSON… writes a composition (precomps, shape,
solid, image, text and null layers, eased and spatial keyframes, masks, track mattes, blend
modes, time remapping, optionally expressions) as `.json` or `.lottie`, and lists anything Lottie
cannot express; File ▸ Import ▸ Lottie… opens one as a new composition.
<p align="center">
<img src="docs/images/effectcraft-render-queue.png" alt="The Render Queue panel with two compositions queued" width="100%">
</p>
## Built for agents
Engine commands are available through the CLI, JSON control channel and MCP. UI-only commands
need the running app's frontend dispatcher: complete menu access requires the app's control
channel or an attached MCP bridge. Headless MCP sessions expose supported engine commands,
but not UI-only commands or bridge-only `ui_*` tools.
- **MCP server:** `effectcraft-cli mcp` speaks the Model Context Protocol over stdio, headless
or bridged to the running app (`--bridge 9877`). Tools cover commands, the project and property
tree, keyframes and rendered frames. This repository ships a ready [`.mcp.json`](.mcp.json).
- **Command line:** one-shot calls with JSON output, for example
`effectcraft-cli set Main '#1' transform/position '[100,360]' --time 0 main.ecproj --save`
or `effectcraft-cli render --comp Main --out main.mp4`, or
`effectcraft-cli exec file.exportLottie '{"comp":"Main","path":"main.json"}' main.ecproj`.
- **Control channel:** `effectcraft --control 9877` accepts JSON lines to run commands, inspect
and click any widget by its automation id, and take screenshots.
See [docs/agents.md](docs/agents.md) and [docs/control-protocol.md](docs/control-protocol.md).
## Get started
Installers for each version are on the [Releases](https://github.com/storytold/effectcraft/releases)
page: a universal macOS app; Windows MSIs and portable zips for x64, x86 and ARM64; Linux
AppImage, Flatpak, deb, rpm and tar.gz for x86_64 and aarch64, plus a riscv64 tar.gz; a FreeBSD x86_64 tarball; and the
web build. [Downloads](#downloads) lists every file. The Windows ARM64 build runs natively on
Windows on ARM, without x64 emulation. CI installs that MSI on Windows 11 ARM64 hardware, checks
both programs are ARM64 and runs the command-line tool there, but it doesn't open the app's window
or run the test suite natively on ARM64 yet, so please report anything that behaves differently.
On Gentoo, the community [::snakebyte overlay](https://github.com/switch87/snakebyte-overlay) packages
the Linux release as `media-video/effectcraft-bin` (not maintained by the EffectCraft team):
```sh
eselect repository add snakebyte git https://github.com/switch87/snakebyte-overlay.git
emaint sync -r snakebyte
echo 'media-video/effectcraft-bin ~amd64' >> /etc/portage/package.accept_keywords/effectcraft
emerge --ask media-video/effectcraft-bin
```
To build it yourself you need [Rust](https://rustup.rs/) 1.95 or newer.
```sh
git clone https://github.com/storytold/effectcraft
cd effectcraft
cargo run --release -p effectcraft # the app (add `-- --demo` to open the demo project)
cargo run --release -p effectcraft-cli -- render --out intro.mp4 # render the demo headless
cargo xtask web --serve 8765 # the browser build on http://127.0.0.1:8765/ (docs/web.md)
```
New to it? [docs/footage.md](docs/footage.md) covers importing footage and image sequences and
Interpret Footage.
To work on it: `cargo test --workspace`, and `cargo xtask ci` before every commit (format, lints,
tests, layering, asset attribution and the WebAssembly build). See [CONTRIBUTING.md](CONTRIBUTING.md),
[docs/architecture.md](docs/architecture.md) and [docs/testing.md](docs/testing.md).
## Where it stands
EffectCraft is young: its first commit was on 1 October 2026. A comprehensive feature set
exists, including 308 effects, but breadth is not the same as full maturity across every edge
case. Today it's a good place to try things and to tell us what breaks. It isn't yet recommended
for production client work. In particular:
- **Native project format.** Projects are saved as `.ecproj`, readable versioned JSON. Proprietary
project formats and After Effects SDK plug-ins are not supported.
- **Fidelity is still being refined.** Automated rendering tests currently focus on small
compositions, so please report visual discrepancies or unexpected behaviour.
- **macOS is the most tested platform.** Linux and Windows users have hit basic interaction
bugs, which we're fixing first.
- **The trained models are new.** Roto Brush can use MobileSAM (an optional 40 MB download in
Settings ▸ Roto Brush) and face tracking can use Google's MediaPipe Face Landmarker (an optional
3.8 MB download in Settings ▸ Face Tracking). Both are actively undergoing evaluation and tuning.
EffectCraft is in **alpha**: about 91% of the features tracked in
[docs/target-app-parity.md](docs/target-app-parity.md) exist, and we estimate it is about 45% of the
way to being ready for full professional production work. The [ROADMAP](ROADMAP.md) has the plan,
[docs/target-app-parity.md](docs/target-app-parity.md) the measurement and [docs/gaps.md](docs/gaps.md)
every known shortfall. The web build ([docs/web.md](docs/web.md)) runs the full app in the browser.
[Bug reports](https://github.com/storytold/effectcraft/issues) are the most useful thing you can
send us right now.
## How it's made
- **Engine first.** A stack of small crates with strict layering. The interface (egui) sits on
top and nothing underneath depends on it.
- **Clean room.** We work from public documentation and observable behavior, never
from proprietary files, icons, presets or code. No code is copied from GPL projects. The full rules
are in [AGENTS.md](AGENTS.md).
- **Private by default.** No telemetry, and no network access unless you ask for it.
- **One family.** EffectCraft shares its time model and text engine design with FilmCraft, and
gets its video and audio codecs from it.
## Downloads
**New to EffectCraft?** Download it from the [EffectCraft page on getartcraft.com](https://getartcraft.com/apps/effectcraft). That's the easiest way to install it.
**Want a specific build or format?** On GitHub, the [latest release](https://github.com/storytold/effectcraft/releases/latest) has every build listed below, and [all releases](https://github.com/storytold/effectcraft/releases) has earlier versions and their notes. `<ver>` in the file names is the version number, and `SHA256SUMS.txt` lists a checksum for every file.
### Windows
| Build | Installer | Portable |
|---|---|---|
| x64 (64-bit Intel/AMD) | `effectcraft-<ver>-windows-x64.msi` | `effectcraft-<ver>-windows-x64-portable.zip` |
| arm64 (Snapdragon and other ARM PCs) | `effectcraft-<ver>-windows-arm64.msi` | `effectcraft-<ver>-windows-arm64-portable.zip` |
| x86 (32-bit) | `effectcraft-<ver>-windows-x86.msi` | `effectcraft-<ver>-windows-x86-portable.zip` |
Installers and executables are code-signed. The installer asks who to install EffectCraft for and where. By default it installs for your
account only, into `%LOCALAPPDATA%\Programs\ArtCraft\EffectCraft`, without an administrator prompt;
"Everyone on this computer" installs into `C:\Program Files\ArtCraft\EffectCraft` (`Program Files (x86)` for the 32-bit build). Later versions
install into the same folder. To install silently
into a folder of your choice: `msiexec /i effectcraft-<ver>-windows-x64.msi /qn INSTALLFOLDER="D:\Apps\EffectCraft"`.
A silent install is per-user by default. `INSTALLFOLDER` chooses the folder, not who it is installed for;
add `ALLUSERS=1`, from an elevated terminal or deployment tool, for an all-users install.
### macOS
| Build | File | Notes |
|---|---|---|
| App, universal (Apple silicon + Intel) | `effectcraft-<ver>-macos-universal.dmg` | Signed and notarized |
| Command-line tool, universal | `effectcraft-cli-<ver>-macos-universal.zip` | Signed and notarized |
### Linux
| Format | x86_64 | aarch64 (ARM64) | Notes |
|---|---|---|---|
| AppImage | `effectcraft-<ver>-linux-x86_64.AppImage` | `effectcraft-<ver>-linux-aarch64.AppImage` | Runs anywhere; updates itself with [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) (`.zsync` files) |
| Flatpak | `effectcraft-<ver>-linux-x86_64.flatpak` | `effectcraft-<ver>-linux-aarch64.flatpak` | Sandboxed; `flatpak install --user <file>` |
| Debian/Ubuntu | `effectcraft-<ver>-linux-x86_64.deb` | `effectcraft-<ver>-linux-aarch64.deb` | |
| Fedora/RHEL/openSUSE | `effectcraft-<ver>-linux-x86_64.rpm` | `effectcraft-<ver>-linux-aarch64.rpm` | |
| Tarball | `effectcraft-<ver>-linux-x86_64.tar.gz` | `effectcraft-<ver>-linux-aarch64.tar.gz` | Unpack anywhere |
RISC-V 64-bit: `effectcraft-<ver>-linux-riscv64.tar.gz` (tarball only; needs glibc 2.39+, e.g. Ubuntu 24.04+ or Debian 13+).
**File drag-and-drop on Linux:** The current eframe/winit 0.30 native Wayland backend does not
deliver file-hover or file-drop events. On Hyprland and other native Wayland sessions, use
**File ▸ Import ▸ File…** (which accepts multiple files), or start EffectCraft via XWayland if
XWayland is installed and `DISPLAY` is set:
```sh
env -u WAYLAND_DISPLAY effectcraft
```
When the backend delivers events (X11/XWayland, Windows and macOS), dropping media anywhere
in the window imports it, and a drop-target hint appears while hovering. On native Wayland the
app cannot detect an attempt that generates no event; it shows a startup notice with the
File ▸ Import fallback instead.
### FreeBSD
| Build | File |
|---|---|
| x86_64 | `effectcraft-<ver>-freebsd-x86_64.tar.gz` |
### Web (WebAssembly)
| Build | File | Notes |
|---|---|---|
| Static site | `effectcraft-web-<ver>.zip` | Runs in a modern browser; host it on any static server |
## The Crafting Apps
EffectCraft is one of the **Crafting Apps**: free, open-source creative tools from the
[ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to
stand on its own.
| | App | What it's for | Code | Learn more |
|:-:|---|---|---|---|
| <img src="https://raw.githubusercontent.com/storytold/photocraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.photocraft.png" alt="" width="32" height="32"> | **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) | [Website](https://getartcraft.com/apps/photocraft) |
| <img src="https://raw.githubusercontent.com/storytold/vectorcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.vectorcraft.png" alt="" width="32" height="32"> | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
| <img src="https://raw.githubusercontent.com/storytold/filmcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png" alt="" width="32" height="32"> | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
| <img src="https://raw.githubusercontent.com/storytold/lightcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png" alt="" width="32" height="32"> | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
| <img src="https://raw.githubusercontent.com/storytold/pdfcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" alt="" width="32" height="32"> | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
| <img src="https://raw.githubusercontent.com/storytold/figcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.figcraft.png" alt="" width="32" height="32"> | **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
| <img src="https://raw.githubusercontent.com/storytold/wordcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.wordcraft.png" alt="" width="32" height="32"> | **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
| <img src="https://raw.githubusercontent.com/storytold/soundcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png" alt="" width="32" height="32"> | **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
| <img src="https://raw.githubusercontent.com/storytold/deckcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png" alt="" width="32" height="32"> | **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
| <img src="https://raw.githubusercontent.com/storytold/cadcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.cadcraft.png" alt="" width="32" height="32"> | **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
| <img src="https://raw.githubusercontent.com/storytold/gridcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png" alt="" width="32" height="32"> | **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
| <img src="https://raw.githubusercontent.com/storytold/texturecraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.texturecraft.png" alt="" width="32" height="32"> | **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
| <img src="https://raw.githubusercontent.com/storytold/craft-anything/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png" alt="" width="32" height="32"> | **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
| <img src="https://raw.githubusercontent.com/tangency-labs/solidcraft/main/assets/icons/app/linux/exports/hicolor/64x64/apps/io.tangencylabs.solidcraft.png" alt="" width="32" height="32"> | **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control.
<br>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Join the ArtCraft community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
</p>
<h3 align="center">Come make things with us</h3>
<p align="center">
Our Discord is where artists of every kind hang out: people who paint, shoot, draw, cut film,
set type, and people still figuring out what they like to make. Share what you're working on,
ask for help, tell us what's broken, or tell us what you wish these tools could do.
Whatever your medium and however long you've been at it, you're welcome here.
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><b>discord.gg/artcraft</b></a> ·
<a href="https://getartcraft.com/">getartcraft.com</a> ·
<a href="https://getartcraft.com/apps">The Crafting Apps</a> ·
<a href="https://getartcraft.com/apps/effectcraft">EffectCraft</a>
</p>
## License and credits
EffectCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
Copyright (c) 2026 ArtCraft Team and the EffectCraft contributors. Required notices are in [NOTICE](NOTICE).
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md).
The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the
ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
part of this repository and EffectCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
Forks and modified versions must remove them.
<p align="center">
<a href="https://getartcraft.com/"><img alt="ArtCraft" src="docs/brand/artcraft-mark.svg" width="28"></a><br>
<sub>Made by the <a href="https://getartcraft.com/">ArtCraft</a> team and community.</sub>
</p>
## Star history
[![Star History Chart](https://api.star-history.com/svg?repos=storytold/effectcraft&type=Date&legend=top-left)](https://www.star-history.com/?repos=storytold%2Feffectcraft&type=date&legend=top-left)
<p align="center">
<a href="https://getartcraft.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/brand/artcraft-logo-white.svg">
<img alt="ArtCraft" src="docs/brand/artcraft-logo.svg" width="200">
</picture>
</a>
</p>
<p align="center">
<img src="assets/app-icon/hicolor/256x256/apps/ai.storyteller.effectcraft.png" width="128" height="128" alt="EffectCraft icon: an engraved unicorn on a pink tile">
</p>
<h1 align="center">EffectCraft</h1>
<p align="center">
<b>Motion graphics and visual effects; an open-source, professional motion graphics and visual effects studio, built in pure Rust.</b>
</p>
<p align="center">
A free, open-source compositor: compositions, layers,
keyframes, 308 effects, layer styles, expressions, 3D cameras and lights, and a render queue, native on macOS,
Windows and Linux, and in the browser. Young, moving fast, and already usable.
</p>
<p align="center">
<img alt="Status: young and moving fast" src="https://img.shields.io/badge/status-young%20and%20moving%20fast-e0368f?style=flat-square">
<img alt="Written in Rust" src="https://img.shields.io/badge/rust-1.95%2B-b0206c?style=flat-square&logo=rust&logoColor=white">
<img alt="License: MIT OR Apache-2.0" src="https://img.shields.io/badge/license-MIT%20%2F%20Apache--2.0-555?style=flat-square">
<br>
<a href="https://sonarcloud.io/project/overview?id=storytold_effectcraft"><img alt="Scanned by SonarCloud" src="https://img.shields.io/badge/Scanned_by-SonarCloud-4b9fd5?style=flat-square&logo=sonarqubecloud&logoColor=white"></a>
<a href="https://coderabbit.ai"><img alt="Reviewed by CodeRabbit" src="https://img.shields.io/coderabbit/prs/github/storytold/effectcraft?style=flat-square&label=Reviewed%20by%20CodeRabbit&logo=coderabbit"></a>
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Join the ArtCraft community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
</p>
<p align="center">
<a href="https://getartcraft.com/apps/effectcraft"><b>EffectCraft on getartcraft.com</b></a> ·
<a href="https://getartcraft.com/">ArtCraft</a> ·
<a href="https://getartcraft.com/apps">All Crafting Apps</a>
</p>
<br>
<p align="center">
<img src="docs/images/effectcraft-hero.png" alt="EffectCraft's main window: the animated demo composition in the Composition panel, the Project panel, a Timeline with text, shape and solid layers, and the Properties panel showing the selected text layer's transform, font and paragraph settings" width="100%">
</p>
> [!NOTE]
> **ArtCraft is a community of artists from all walks of life.** Digital, generative, music,
> games &mdash; if you make things, you're one of us. **[Come say hi on Discord](https://discord.gg/artcraft).**
<p align="center">
<a href="#what-effectcraft-is">What it is</a> ·
<a href="#animate">Animate</a> ·
<a href="#effects">Effects</a> ·
<a href="#3d">3D</a> ·
<a href="#export">Export</a> ·
<a href="#built-for-agents">Agents</a> ·
<a href="#get-started">Get started</a> ·
<a href="#where-it-stands">Status</a> ·
<a href="#how-its-made">How it's made</a> ·
<a href="#downloads">Downloads</a> ·
<a href="#the-crafting-apps">The Crafting Apps</a> ·
<a href="#license-and-credits">License</a>
</p>
## What EffectCraft is
EffectCraft is for animated titles, motion graphics and compositing work: the kind of thing
motion designers and VFX artists need. You build a composition out of layers (solids, shapes,
text, footage, other compositions), animate their properties with keyframes, stack effects on
them and render the result.
The interface feels natural and responsive, with dedicated panels (Project, Composition, Timeline,
Layer, Footage, Effect Controls, Tools, Preview, Effects & Presets, Audio, Info, Paragraph,
Character, Align, Progress, Render Queue) and standard motion graphics shortcuts, going further in
a few places where it matters to us:
- **Lottie import and export built in**, so animations can go straight to the web and apps
without a plugin.
- **A project file you can read.** Projects are versioned JSON (`.ecproj`), so they diff cleanly
in version control.
- **Everything is scriptable.** Menu actions, timeline edits and property changes are exposed as
commands. Engine commands are available through the CLI, JSON control channel and MCP;
UI-only commands use the frontend dispatcher and can be invoked through the control channel
or an attached MCP bridge.
- **No FFmpeg.** Video and audio decoding and encoding are pure Rust: FilmCraft's codecs and
EffectCraft's own VP9, AV1, HEVC and Opus encoders.
## Animate
The panels, menus and shortcuts follow standard motion design conventions:
Project, Composition, Timeline, Effect Controls, Properties, Effects & Presets, Character,
Paragraph, Align, Info, Preview, Audio and the Render Queue, docked the way you expect. Drag
footage, comps and effects between them: onto the comp viewer, where they land under the pointer,
or into the Timeline, between layers and at the time you point to.
- **Layers of every kind:** solids, shapes, text, footage, nested compositions, nulls,
adjustment layers, cameras and lights; parenting, track mattes, all 38 blend modes, motion blur.
- **Keyframes that behave the same:** linear, Bezier, hold, auto and continuous Bezier, roving
keys, Easy Ease (F9), Keyframe Velocity and Interpolation dialogs, copy and paste at the current
time, and a **Graph Editor** with value and speed graphs and draggable handles.
- **Time:** time remapping, time stretch, time-reverse, freeze frame, work area, markers, exact
frame-accurate timing at every frame rate including 29.97 drop-frame.
- **Shapes and masks:** shape layers with trim paths, repeaters, round corners, offset, zig zag,
twist, wiggle, merge paths and gradient strokes; masks drawn with the pen tool, with modes,
feather, expansion and vertex editing in the viewer.
- **Text:** point and paragraph text with real shaping in any font installed on your machine, the
Character and Paragraph panels, and text animators with range selectors.
- **Expressions:** JavaScript with the standard motion expressions model (`wiggle`, `loopOut`,
`thisComp.layer("…")`, vector maths on arrays), an inline editor and the pick-whip.
- **Timeline like you know it:** twirl layers open to Transform, masks, effects and the rest; drag
rows to reorder layers; rename with Enter or a double-click; the property shortcuts (A, P, S,
R, T, M, F, E, L, U and their double presses AA, PP, SS, RR, TT, MM, FF, EE, LL, UU), Shift to
add properties, Alt+Shift+A/P/S/R/T to key the current time, Ctrl+` to twirl selected layers.
- **Puppet tools:** Position, Advanced, Bend, Starch and Overlap pins on a mesh built from the
layer, real-time pin recording, rotate/scale handles, pins driven by (or carrying) nulls for
rigging, and follow-through for hair and cloth.
<p align="center">
<img src="docs/images/effectcraft-graph-editor.png" alt="The Graph Editor showing an eased value curve for a text animator's Range Start, with the graph editor's button bar for hold, linear, auto Bezier and Easy Ease" width="100%">
</p>
## Effects
308 built-in effects across its categories, each with its parameter names, order and
defaults: blur and sharpen, channel, color correction (Curves, Levels, Hue/Saturation, a full
color-grading panel…), distort (Warp, Bulge, Turbulent Displace, Perspective Pin…), generate (Fractal Noise,
Gradient Ramp, Stroke, Write-on, Audio Spectrum…), keying, matte, noise and grain, perspective,
**simulation** (Particle Emitter 3D, Rainfall, Shatter, Card Dance, Caustics, Wave World…),
stylize (Glow, Glass Relief…), **time** (Echo, Posterize Time, Timewarp, Time Displacement…),
**audio** (Reverb, Parametric EQ, Delay, Stereo Mixer…), text, transitions, utility and expression
controls. All nine **Layer Styles** (Drop Shadow, Inner/Outer Glow, Bevel and Emboss, Satin,
overlays, Stroke) with Global Light. Preview plays audio in sync, with meters and waveforms.
## 3D
Classic 3D compositing: 3D layers with orientation and material options,
one- and two-node **cameras** with depth of field, **lights** (parallel, spot, point, ambient)
with soft ray-traced shadows, layers that intersect correctly, orbit, pan and dolly camera
tools, and Front, Top, Left and Custom views.
<p align="center">
<img src="docs/images/effectcraft-3d.png" alt="The 3D Showcase demo in Custom View 1: intersecting cards lit by a spot light with soft shadows on a gridded floor, with the camera frustum and light drawn as wireframes" width="100%">
</p>
## Export
A dedicated Render Queue, with Render Settings and Output Modules: **H.264** MP4 and
**ProRes** MOV (Proxy to 4444 XQ with alpha) with audio, **HEVC** (Main / Main 10) and **AV1**
MP4, **WebM** (VP9 with inter frames and alpha, or AV1; Opus audio), PNG, JPEG, TIFF, Targa and 32-bit EXR sequences, animated GIF and WAV/AIFF. Field
rendering with 3:2 pulldown, effect/solo/guide/depth overrides, crop, region of interest and
resize, Render Settings and Output Module templates with defaults, post-render actions, storage
overflow and render logs. The same queue runs from the command line. Every encoder is pure
Rust (craft-libs' H.264, ProRes, HEVC, VP9, AV1 and AAC; EffectCraft's own Opus); there is no
FFmpeg inside.
**Lottie** goes both ways: File ▸ Export ▸ Lottie JSON… writes a composition (precomps, shape,
solid, image, text and null layers, eased and spatial keyframes, masks, track mattes, blend
modes, time remapping, optionally expressions) as `.json` or `.lottie`, and lists anything Lottie
cannot express; File ▸ Import ▸ Lottie… opens one as a new composition.
<p align="center">
<img src="docs/images/effectcraft-render-queue.png" alt="The Render Queue panel with two compositions queued" width="100%">
</p>
## Built for agents
Engine commands are available through the CLI, JSON control channel and MCP. UI-only commands
need the running app's frontend dispatcher: complete menu access requires the app's control
channel or an attached MCP bridge. Headless MCP sessions expose supported engine commands,
but not UI-only commands or bridge-only `ui_*` tools.
- **MCP server:** `effectcraft-cli mcp` speaks the Model Context Protocol over stdio, headless
or bridged to the running app (`--bridge 9877`). Tools cover commands, the project and property
tree, keyframes and rendered frames. This repository ships a ready [`.mcp.json`](.mcp.json).
- **Command line:** one-shot calls with JSON output, for example
`effectcraft-cli set Main '#1' transform/position '[100,360]' --time 0 main.ecproj --save`
or `effectcraft-cli render --comp Main --out main.mp4`, or
`effectcraft-cli exec file.exportLottie '{"comp":"Main","path":"main.json"}' main.ecproj`.
- **Control channel:** `effectcraft --control 9877` accepts JSON lines to run commands, inspect
and click any widget by its automation id, and take screenshots.
See [docs/agents.md](docs/agents.md) and [docs/control-protocol.md](docs/control-protocol.md).
## Get started
Installers for each version are on the [Releases](https://github.com/storytold/effectcraft/releases)
page: a universal macOS app; Windows MSIs and portable zips for x64, x86 and ARM64; Linux
AppImage, Flatpak, deb, rpm and tar.gz for x86_64 and aarch64, plus a riscv64 tar.gz; a FreeBSD x86_64 tarball; and the
web build. [Downloads](#downloads) lists every file. The Windows ARM64 build runs natively on
Windows on ARM, without x64 emulation. CI installs that MSI on Windows 11 ARM64 hardware, checks
both programs are ARM64 and runs the command-line tool there, but it doesn't open the app's window
or run the test suite natively on ARM64 yet, so please report anything that behaves differently.
On Gentoo, the community [::snakebyte overlay](https://github.com/switch87/snakebyte-overlay) packages
the Linux release as `media-video/effectcraft-bin` (not maintained by the EffectCraft team):
```sh
eselect repository add snakebyte git https://github.com/switch87/snakebyte-overlay.git
emaint sync -r snakebyte
echo 'media-video/effectcraft-bin ~amd64' >> /etc/portage/package.accept_keywords/effectcraft
emerge --ask media-video/effectcraft-bin
```
To build it yourself you need [Rust](https://rustup.rs/) 1.95 or newer.
```sh
git clone https://github.com/storytold/effectcraft
cd effectcraft
cargo run --release -p effectcraft # the app (add `-- --demo` to open the demo project)
cargo run --release -p effectcraft-cli -- render --out intro.mp4 # render the demo headless
cargo xtask web --serve 8765 # the browser build on http://127.0.0.1:8765/ (docs/web.md)
```
New to it? [docs/footage.md](docs/footage.md) covers importing footage and image sequences and
Interpret Footage.
To work on it: `cargo test --workspace`, and `cargo xtask ci` before every commit (format, lints,
tests, layering, asset attribution and the WebAssembly build). See [CONTRIBUTING.md](CONTRIBUTING.md),
[docs/architecture.md](docs/architecture.md) and [docs/testing.md](docs/testing.md).
## Where it stands
EffectCraft is young: its first commit was on 1 October 2026. A comprehensive feature set
exists, including 308 effects, but breadth is not the same as full maturity across every edge
case. Today it's a good place to try things and to tell us what breaks. It isn't yet recommended
for production client work. In particular:
- **Native project format.** Projects are saved as `.ecproj`, readable versioned JSON. Proprietary
project formats and After Effects SDK plug-ins are not supported.
- **Fidelity is still being refined.** Automated rendering tests currently focus on small
compositions, so please report visual discrepancies or unexpected behaviour.
- **macOS is the most tested platform.** Linux and Windows users have hit basic interaction
bugs, which we're fixing first.
- **The trained models are new.** Roto Brush can use MobileSAM (an optional 40 MB download in
Settings ▸ Roto Brush) and face tracking can use Google's MediaPipe Face Landmarker (an optional
3.8 MB download in Settings ▸ Face Tracking). Both are actively undergoing evaluation and tuning.
EffectCraft is in **alpha**: about 91% of the features tracked in
[docs/target-app-parity.md](docs/target-app-parity.md) exist, and we estimate it is about 45% of the
way to being ready for full professional production work. The [ROADMAP](ROADMAP.md) has the plan,
[docs/target-app-parity.md](docs/target-app-parity.md) the measurement and [docs/gaps.md](docs/gaps.md)
every known shortfall. The web build ([docs/web.md](docs/web.md)) runs the full app in the browser.
[Bug reports](https://github.com/storytold/effectcraft/issues) are the most useful thing you can
send us right now.
## How it's made
- **Engine first.** A stack of small crates with strict layering. The interface (egui) sits on
top and nothing underneath depends on it.
- **Clean room.** We work from public documentation and observable behavior, never
from proprietary files, icons, presets or code. No code is copied from GPL projects. The full rules
are in [AGENTS.md](AGENTS.md).
- **Private by default.** No telemetry, and no network access unless you ask for it.
- **One family.** EffectCraft shares its time model and text engine design with FilmCraft, and
gets its video and audio codecs from it.
## Downloads
**New to EffectCraft?** Download it from the [EffectCraft page on getartcraft.com](https://getartcraft.com/apps/effectcraft). That's the easiest way to install it.
**Want a specific build or format?** On GitHub, the [latest release](https://github.com/storytold/effectcraft/releases/latest) has every build listed below, and [all releases](https://github.com/storytold/effectcraft/releases) has earlier versions and their notes. `<ver>` in the file names is the version number, and `SHA256SUMS.txt` lists a checksum for every file.
### Windows
| Build | Installer | Portable |
|---|---|---|
| x64 (64-bit Intel/AMD) | `effectcraft-<ver>-windows-x64.msi` | `effectcraft-<ver>-windows-x64-portable.zip` |
| arm64 (Snapdragon and other ARM PCs) | `effectcraft-<ver>-windows-arm64.msi` | `effectcraft-<ver>-windows-arm64-portable.zip` |
| x86 (32-bit) | `effectcraft-<ver>-windows-x86.msi` | `effectcraft-<ver>-windows-x86-portable.zip` |
Installers and executables are code-signed. The installer asks who to install EffectCraft for and where. By default it installs for your
account only, into `%LOCALAPPDATA%\Programs\ArtCraft\EffectCraft`, without an administrator prompt;
"Everyone on this computer" installs into `C:\Program Files\ArtCraft\EffectCraft` (`Program Files (x86)` for the 32-bit build). Later versions
install into the same folder. To install silently
into a folder of your choice: `msiexec /i effectcraft-<ver>-windows-x64.msi /qn INSTALLFOLDER="D:\Apps\EffectCraft"`.
A silent install is per-user by default. `INSTALLFOLDER` chooses the folder, not who it is installed for;
add `ALLUSERS=1`, from an elevated terminal or deployment tool, for an all-users install.
### macOS
| Build | File | Notes |
|---|---|---|
| App, universal (Apple silicon + Intel) | `effectcraft-<ver>-macos-universal.dmg` | Signed and notarized |
| Command-line tool, universal | `effectcraft-cli-<ver>-macos-universal.zip` | Signed and notarized |
### Linux
| Format | x86_64 | aarch64 (ARM64) | Notes |
|---|---|---|---|
| AppImage | `effectcraft-<ver>-linux-x86_64.AppImage` | `effectcraft-<ver>-linux-aarch64.AppImage` | Runs anywhere; updates itself with [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) (`.zsync` files) |
| Flatpak | `effectcraft-<ver>-linux-x86_64.flatpak` | `effectcraft-<ver>-linux-aarch64.flatpak` | Sandboxed; `flatpak install --user <file>` |
| Debian/Ubuntu | `effectcraft-<ver>-linux-x86_64.deb` | `effectcraft-<ver>-linux-aarch64.deb` | |
| Fedora/RHEL/openSUSE | `effectcraft-<ver>-linux-x86_64.rpm` | `effectcraft-<ver>-linux-aarch64.rpm` | |
| Tarball | `effectcraft-<ver>-linux-x86_64.tar.gz` | `effectcraft-<ver>-linux-aarch64.tar.gz` | Unpack anywhere |
RISC-V 64-bit: `effectcraft-<ver>-linux-riscv64.tar.gz` (tarball only; needs glibc 2.39+, e.g. Ubuntu 24.04+ or Debian 13+).
**File drag-and-drop on Linux:** The current eframe/winit 0.30 native Wayland backend does not
deliver file-hover or file-drop events. On Hyprland and other native Wayland sessions, use
**File ▸ Import ▸ File…** (which accepts multiple files), or start EffectCraft via XWayland if
XWayland is installed and `DISPLAY` is set:
```sh
env -u WAYLAND_DISPLAY effectcraft
```
When the backend delivers events (X11/XWayland, Windows and macOS), dropping media anywhere
in the window imports it, and a drop-target hint appears while hovering. On native Wayland the
app cannot detect an attempt that generates no event; it shows a startup notice with the
File ▸ Import fallback instead.
### FreeBSD
| Build | File |
|---|---|
| x86_64 | `effectcraft-<ver>-freebsd-x86_64.tar.gz` |
### Web (WebAssembly)
| Build | File | Notes |
|---|---|---|
| Static site | `effectcraft-web-<ver>.zip` | Runs in a modern browser; host it on any static server |
## The Crafting Apps
EffectCraft is one of the **Crafting Apps**: free, open-source creative tools from the
[ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to
stand on its own.
| | App | What it's for | Code | Learn more |
|:-:|---|---|---|---|
| <img src="https://raw.githubusercontent.com/storytold/photocraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.photocraft.png" alt="" width="32" height="32"> | **PhotoCraft** | Image editing: layers, masks, type and real PSD files | [GitHub](https://github.com/storytold/photocraft) | [Website](https://getartcraft.com/apps/photocraft) |
| <img src="https://raw.githubusercontent.com/storytold/vectorcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.vectorcraft.png" alt="" width="32" height="32"> | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
| <img src="https://raw.githubusercontent.com/storytold/filmcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png" alt="" width="32" height="32"> | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
| <img src="https://raw.githubusercontent.com/storytold/lightcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png" alt="" width="32" height="32"> | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
| <img src="https://raw.githubusercontent.com/storytold/pdfcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" alt="" width="32" height="32"> | **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
| <img src="https://raw.githubusercontent.com/storytold/figcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.figcraft.png" alt="" width="32" height="32"> | **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
| <img src="https://raw.githubusercontent.com/storytold/wordcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.wordcraft.png" alt="" width="32" height="32"> | **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
| <img src="https://raw.githubusercontent.com/storytold/soundcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png" alt="" width="32" height="32"> | **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
| <img src="https://raw.githubusercontent.com/storytold/deckcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.deckcraft.png" alt="" width="32" height="32"> | **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
| <img src="https://raw.githubusercontent.com/storytold/cadcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.cadcraft.png" alt="" width="32" height="32"> | **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
| <img src="https://raw.githubusercontent.com/storytold/gridcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png" alt="" width="32" height="32"> | **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
| <img src="https://raw.githubusercontent.com/storytold/texturecraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.texturecraft.png" alt="" width="32" height="32"> | **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
| <img src="https://raw.githubusercontent.com/storytold/craft-anything/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.craft-anything.png" alt="" width="32" height="32"> | **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
| <img src="https://raw.githubusercontent.com/tangency-labs/solidcraft/main/assets/icons/app/linux/exports/hicolor/64x64/apps/io.tangencylabs.solidcraft.png" alt="" width="32" height="32"> | **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control.
<br>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Join the ArtCraft community on Discord" src="https://img.shields.io/badge/Join%20us%20on%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" height="40"></a>
</p>
<h3 align="center">Come make things with us</h3>
<p align="center">
Our Discord is where artists of every kind hang out: people who paint, shoot, draw, cut film,
set type, and people still figuring out what they like to make. Share what you're working on,
ask for help, tell us what's broken, or tell us what you wish these tools could do.
Whatever your medium and however long you've been at it, you're welcome here.
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><b>discord.gg/artcraft</b></a> ·
<a href="https://getartcraft.com/">getartcraft.com</a> ·
<a href="https://getartcraft.com/apps">The Crafting Apps</a> ·
<a href="https://getartcraft.com/apps/effectcraft">EffectCraft</a>
</p>
## License and credits
EffectCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
Copyright (c) 2026 ArtCraft Team and the EffectCraft contributors. Required notices are in [NOTICE](NOTICE).
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md).
The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the
ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
part of this repository and EffectCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
Forks and modified versions must remove them.
<p align="center">
<a href="https://getartcraft.com/"><img alt="ArtCraft" src="docs/brand/artcraft-mark.svg" width="28"></a><br>
<sub>Made by the <a href="https://getartcraft.com/">ArtCraft</a> team and community.</sub>
</p>
## Star history
[![Star History Chart](https://api.star-history.com/svg?repos=storytold/effectcraft&type=Date&legend=top-left)](https://www.star-history.com/?repos=storytold%2Feffectcraft&type=date&legend=top-left)
+193 -193
View File
@@ -1,193 +1,193 @@
# EffectCraft Roadmap
**Stage: alpha** · next: beta, ~30 points of "ready for real work" and ~350–620 h away
> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-11-05 · **Change:** minor (ProRes alpha final-run conformance; previously: features of 30 October in the progress log: text Line Join, Timeline drag-reorder and multi-select of effects, viewer edge pan, ScriptUI drag capture and dock size; previously: localization recount after #1774) · **Target:** Adobe After Effects 2026 (26.5.0.89)
EffectCraft aims to do what After Effects does, with the same panels, menus and behaviour, written
from scratch in Rust. This page is the one-page summary; the evidence is in
[docs/target-app-parity.md](docs/target-app-parity.md), the work list in
[docs/gaps.md](docs/gaps.md), and the plan in [docs/roadmap.md](docs/roadmap.md).
## Headline numbers
| | Value | Kind |
|---|---|---|
| **Feature breadth** | **≈ 91%** | Estimated, weighted by area; the self-graded 92-item checklist says ≈ 99% |
| **Ready for real work** | **≈ 45%** (40–50%) | Estimated; one live fidelity measurement so far |
| **Mainstream practitioner** | **≈ 47%** | Estimated: the typical motion designer's weekly work ([how](docs/target-app-parity.md#mainstream-practitioner-and-essentials-user)) |
| **Essentials user** | **≈ 52%** | Estimated: core features only ([how](docs/target-app-parity.md#essentials-user)) |
| Remaining to **beta** | **≈ 350–620 Opus 5.5 agent-hours** | Estimated |
| Remaining to **full parity** | **≈ 850–1,500 Opus 5.5 agent-hours** | Estimated; ≈ 70% parallelises |
| Audience | Ready % | Hours to ~95% | Work that dominates |
|---|---:|---:|---|
| Full target | ≈ 45% | ≈ 850–1,500 h | `.aep`, fidelity corpus, bug backlog, performance, formats, hardware, localization |
| Mainstream practitioner | ≈ 47% | ≈ 280–530 h | `.aep` exchange, stability, everyday fidelity and bugs, preview speed |
| Essentials user | ≈ 52% | ≈ 150–300 h | Opening tutorial `.aep` files, start-up stability, core bugs, preview with sound |
About 60–70% of these hours parallelise ([detail](docs/target-app-parity.md#readiness-by-audience)).
Hours are one Opus 5.5 agent working sequentially, calibrated on this repository's history
([how](docs/target-app-parity.md#calibration-of-the-hours)).
### Stage
**Alpha**, because all six core workflows of the [alpha gate](docs/roadmap.md#alpha-gate)
(set up, animate, text and shapes, composite, preview, save and deliver) work end to end on macOS
and the work saves and reopens, and ready for real work (≈ 45%) is above the ≈ 40% bar. But: After Effects projects (`.aep`) can't be opened, which
alone rules out beta; fidelity against After Effects is measured for one narrow case (scalar ease,
168 samples); 216 issues are open on 12 October, six of them crash reports (#580, #501, #426, #341, #234, #879). Beta needs ≈ 75% ready and `.aep`
opening reliably: the `.aep` owner decision and importer (80–160 h), the fidelity harness and
P0/P1 corpus (60–100 h), the crash and bug backlog (40–80 h), performance (40–70 h),
Linux/Windows reliability (30–50 h) and depth in animation, masks, effects and text (100–160 h).
## By dimension
| Dimension | % | Hours | Doc |
|---|---:|---:|---|
| Features: breadth | 91% | — | [target-app-parity.md](docs/target-app-parity.md#by-feature-area) |
| Features: ready for real work | 50% | 360–660 | [target-app-parity.md](docs/target-app-parity.md#by-feature-area) |
| UI/UX fidelity | 65% | 60–120 | [ui-parity.md](docs/ui-parity.md) |
| File formats | 50% | 130–230 | [file-format-parity.md](docs/file-format-parity.md) |
| Hardware | 40% | 60–120 | [hardware-parity.md](docs/hardware-parity.md) |
| Localization | 24% | 60–110 + review | [localization-parity.md](docs/localization-parity.md) |
| Performance | 35% | 50–100 | [gaps.md](docs/gaps.md#g5-performance-at-real-world-scale) |
| Stability | 45% | 40–80 | [gaps.md](docs/gaps.md#g3-stability-and-a-green-main) |
| Platforms | 65% | 30–60 | [gaps.md](docs/gaps.md#g2-real-user-reliability-on-every-platform) |
| Ecosystem and plug-ins | 20% | 40–80 | [plugins.md](docs/plugins.md), [scripting-parity.md](docs/scripting-parity.md) |
| AI features | 35% | 40–80 | [gaps.md](docs/gaps.md) |
| Agent control (beyond After Effects) | ahead | — | [agents.md](docs/agents.md) |
## Features
| Area | Breadth | Ready | Hours |
|---|---:|---:|---:|
| Project, footage and import | 92% | 55% | 25–45 |
| Compositions, layers and compositing | 98% | 60% | 30–55 |
| Animation: keyframes, graph editor, motion paths, puppet | 97% | 55% | 35–60 |
| Expressions and scripting ([detail](docs/scripting-parity.md)) | 93% | 65% | 20–40 |
| Shapes, masks and roto | 93% | 45% | 30–50 |
| Text and typography | 94% | 50% | 20–35 |
| Effects and animation presets ([detail](docs/effects-parity.md)) | 85% | 40% | 70–130 |
| 3D (Classic and Advanced) | 82% | 35% | 35–65 |
| Tracking and AI-assisted tools | 80% | 35% | 35–70 |
| Preview and cache | 92% | 40% | 25–50 |
| Render queue and export | 85% | 55% | 25–45 |
| Audio | 85% | 45% | 6–12 |
Weights and evidence per row: [target-app-parity.md](docs/target-app-parity.md#by-feature-area).
## Languages
UI strings translated out of ≈ 4,200 (detail: [localization-parity.md](docs/localization-parity.md)).
| Language | Status | % |
|---|---|---:|
| English | full | 100% |
| Simplified Chinese | partial (menus, most of the panels) | 46% |
| Spanish | menus only | 29% |
| Hindi | none | 0% |
| Arabic | none | 0% |
| French | menus only | 16% |
| Portuguese (Brazil) | partial (menus, part of the panels) | 34% |
| Indonesian | none | 0% |
| Japanese | partial (menus, most of the panels) | 46% |
| German | menus only | 16% |
| Korean | none | 0% |
| Vietnamese | none | 0% |
Also shipped: Traditional Chinese (partial, 38%), Ukrainian (partial, 29%), Czech (partial, 29%), Polish (partial, 34%), Georgian (partial, 39%; native-speaker reviewed), Russian (menus only, 15%) and Swedish (menus, panels and effect names; not yet measured). No other language has had
native-speaker review.
## Upcoming
Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gaps.md).
| | Next | Hours |
|---|---|---:|
| 1 | Fix the open crash reports; run `cargo xtask ci` on every pull request (G3) | 15–30 |
| 2 | Fidelity harness against After Effects: rendered frames, a corpus case per P0/P1 feature (G1) | 60–100 |
| 3 | Real-user reliability on Linux and Windows; the open mask, effect, text and 3D bugs (G2) | 100–190 |
| 4 | `.aep` / `.aepx` import, after the owner's clean-room decision (G4) | 80–160 |
| 5 | Performance: benchmarks at 1080p / 4K, real-time cached playback, hardware video (G5) | 50–100 |
| 6 | Localization depth: effect names, all panels, Korean / Italian (G9) | 60–110 |
Come tell us what matters most to you on [Discord](https://discord.gg/artcraft).
## Progress log
- 2026-11-05: ProRes 4444 and XQ exports write the required run after the final alpha
difference. The FilmCraft encoder pin fixes alpha movies rejected by native macOS
decoding; an export regression checks the stored bytes for both profiles, alpha modes,
and motion-blur settings (#293). Resolve itself still needs confirmation.
- 2026-10-30: Text strokes take a Line Join (Miter, Round or Bevel) in the Character panel,
Miter for new text as in After Effects (#2077). In the Timeline, effects, masks and shape items
drag to a new place in their stack (#2210) and Shift-click adds them to the selection, so one
animation preset can hold several (#2552). The Composition panel pans while a mask vertex is
held at its edge (#999). Effect buffers that pad a small layer stay within a memory budget
(#2261); an MP4 lasts as long as its edit list shows (#2592); the Render Queue's times tick
between frames (#2593). ScriptUI drags keep their control past the window edge with
`MouseEvent.buttons` (#2584), and docked panels get their dock's size and `onResize` (#2585).
- 2026-10-28: A shape layer's Rectangle and Ellipse Size get the Constrain Proportions chain
that Scale has, so a circle stays a circle while its size is typed, scrubbed or keyed; it
starts off, so existing shapes behave as before (#2489).
- 2026-10-24: Shape layers have the Wiggle Transform path operation (Contents ▸ Add, or
`layer.addShapeItem {kind: "wiggler"}`): it wiggles the anchor point, position, scale and
rotation of the paths above it by random amounts that change over time, up to the Transform
values it holds. The copies of a Repeater above it wiggle independently as far as its
Correlation allows; with the Repeater below it, they wiggle together.
- 2026-10-22: Localization: the panel/dialog catalogs gain 303 rows in Japanese, Simplified
Chinese and Traditional Chinese — panel tabs, form and dialog labels, panel context menus,
tracker/graph/text-panel controls, File-panel footers and credits. Recounted after #1774:
Japanese is 1,923 of ≈ 4,200 interface strings (46%, with 297 effect names); the Chinese
panel catalogs hold 998 (Simplified) and 951 (Traditional) rows, and Simplified Chinese is
46% with its 307 effect names; the overall share is ≈ 24%.
- 2026-10-16: Convert To Bezier Path turns a shape layer's Rectangle, Ellipse or Polystar
path into an editable Path 1 with the same geometry, as After Effects does; it used to
apply to mask RotoBezier only, so a parametric shape path could not be converted at all
and its vertices could not be edited or animated (#932).
- 2026-10-14: Georgian (`ka`): every menu row, every panel-catalog row the Chinese catalogs have plus 195 Georgian-only ones, and the effect names and categories; Match System for `ka` / `ka-GE` / `ka_GE.UTF-8` locales. Reviewed by a native speaker.
- 2026-10-12: Roto Brush span edits report strokes outside a trimmed layer without panicking;
strokes and undo history are preserved so restoring the timing allows the span edit again.
- 2026-10-11: Ukrainian panels caught up with the Chinese catalogs. The 95 rows the panel
catalog gained since 9 October are translated: the Home screen (recent files, their Kind and
Opened columns, the empty and no-match states), the viewer's toolbar tooltips, magnification,
resolution, grid, channel and Fast Previews popups, the Appearance settings, the solid dialog
and the project, import, export and colour-management forms. `uk` now carries 495 of the 496
panel rows — `RGB` reads the same in Ukrainian, so it falls back to the English source by
design — alongside its 618/618 menus.
- 2026-10-10: Full parity re-measure against After Effects 2026 26.5 (installed bundle, 26.2–26.5
release notes, repository counts): breadth ≈ 91%, ready ≈ 45%, stage alpha. Progress docs
restructured: `docs/parity.md` became `docs/target-app-parity.md`; new
`docs/roadmap.md`, `localization-parity.md`, `file-format-parity.md`, `hardware-parity.md`,
`ui-parity.md`, `effects-parity.md` and `scripting-parity.md`.
- 2026-10-10: Spanish (#460) and Brazilian Portuguese (#217) menu translations.
- 2026-10-10: Japanese now translates the interface below the menu bar too: `crates/ui-egui/src/i18n/ui.rs` has a Japanese table with the same 396 rows as the Chinese ones (panels, dialogs, buttons and tooltips; the shape test keeps the three catalogs listing the same source strings). Menus were already Japanese.
- 2026-10-09: Ukrainian now covers all 396 current panel-catalog strings, including dialogs, buttons and tooltips, alongside its menu translation. The catalog keeps the same source keys and placeholder counts as the Chinese catalogs; missing or engine-generated text still falls back to English.
- UI.6: Viewer breadcrumbs use theme colors for readable active text. The bottom control row scrolls horizontally at compact widths; keyboard focus reveals the selected control while dropdowns, exposure adjustments and snapshot holds retain their behavior.
- UI.3: Shared docking transactions and pointer targets now handle panel moves, splits, tab ordering and floating groups, preserving accordion metadata and the saved workspace schema. Dock dividers use bounded shared geometry and keyboard-accessible resize handles. Tiny layouts no longer reach an invalid floating-point clamp; workspace trees and saved layouts are retained.
- 2026-10-09: Settings ▸ General ▸ Language now translates the interface below the menu bar as well: panels, dialogs, buttons and tooltips read their text through `crates/ui-egui/src/i18n/ui.rs`, keyed by the English source string. Simplified and Traditional Chinese are complete (384 strings); Japanese rows can follow in the same file. A test fails when a converted panel gains a hard-coded string, and another keeps the catalog free of unused rows.
- 2026-10-08: `effectcraft-cli render --out` resolves a relative path against the working directory, like `--project` (it used the project's folder, which doubled a path that already named it). Render Queue outputs set in a project are unchanged.
- 2026-10-08: Long-press a grouped toolbar button to choose its tools, including Horizontal/Vertical Type; releasing the hold keeps the menu open. Existing point/paragraph text creation is unchanged.
- 2026-10-06: Settings ▸ General ▸ Language persists English/Japanese menu labels (`general.language`); native UI reuses installed Japanese font fallback, with no bundled CJK font. Dialog and panel contents remain English.
- 2026-10-05: Honest assessment ([docs/gaps.md](docs/gaps.md)): breadth ≈ 99% by our own checklist, real use ≈ 30–50%; workstreams G1–G9.
- 2026-10-04: Checklist audit at `d39c0e8`: 89 done / 3 partial / 0 missing of 92 features.
- 2026-10-01: First commit.
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Progress log: text Line Join, Timeline drag-reorder and Shift multi-select of effects, viewer edge pan, effect padding budget, MP4 edit-list duration, Render Queue clock, ScriptUI drag capture and docked-panel size |
| 2026-10-22 | minor | Localization: 300 more panel/dialog rows in Japanese and the Chinese catalogs, plus the review's three missing rows (Basic, Advanced, Play/Stop) — panel tabs, form and dialog labels, context menus, tracker/graph/text-panel controls, File-panel footers, credits; language shares recounted after #1774 (Japanese 27% → 46% with 297 effect names, Simplified Chinese 29% → 46% with 307 effect names, Traditional Chinese 29% → 38%, overall 18% → 24%) |
| 2026-10-12 | minor | Open-issue count (134 to 216), crash list and localization share (12% to 18%) updated to match docs/gaps.md |
| 2026-10-12 | patch | Roto Brush span validation after trimming; command regressions for both layer edges |
| 2026-10-12 | minor | Shared PDF object/xref parsing with bounded app-owned conversion and preserved stream budgets; drawing, EPS and image/font policy remain unchanged |
| 2026-10-10 | minor | Hours to ≈ 95% for each audience |
| 2026-10-10 | minor | Headline adds mainstream practitioner (≈ 47%) and essentials user (≈ 52%) |
| 2026-10-10 | major | Progress docs restructured: stage, two numbers, dimensions, features, languages, upcoming, progress log; milestones moved to docs/roadmap.md |
| 2026-10-10 | minor | Stage checked against the new core-workflow gate (all six pass; still alpha) |
| 2026-10-05 | major | Two numbers (breadth vs real use) and workstreams G1–G9 |
# EffectCraft Roadmap
**Stage: alpha** · next: beta, ~30 points of "ready for real work" and ~350–620 h away
> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-11-05 · **Change:** minor (ProRes alpha final-run conformance; previously: features of 30 October in the progress log: text Line Join, Timeline drag-reorder and multi-select of effects, viewer edge pan, ScriptUI drag capture and dock size; previously: localization recount after #1774) · **Target:** Adobe After Effects 2026 (26.5.0.89)
EffectCraft aims to do what After Effects does, with the same panels, menus and behaviour, written
from scratch in Rust. This page is the one-page summary; the evidence is in
[docs/target-app-parity.md](docs/target-app-parity.md), the work list in
[docs/gaps.md](docs/gaps.md), and the plan in [docs/roadmap.md](docs/roadmap.md).
## Headline numbers
| | Value | Kind |
|---|---|---|
| **Feature breadth** | **≈ 91%** | Estimated, weighted by area; the self-graded 92-item checklist says ≈ 99% |
| **Ready for real work** | **≈ 45%** (40–50%) | Estimated; one live fidelity measurement so far |
| **Mainstream practitioner** | **≈ 47%** | Estimated: the typical motion designer's weekly work ([how](docs/target-app-parity.md#mainstream-practitioner-and-essentials-user)) |
| **Essentials user** | **≈ 52%** | Estimated: core features only ([how](docs/target-app-parity.md#essentials-user)) |
| Remaining to **beta** | **≈ 350–620 Opus 5.5 agent-hours** | Estimated |
| Remaining to **full parity** | **≈ 850–1,500 Opus 5.5 agent-hours** | Estimated; ≈ 70% parallelises |
| Audience | Ready % | Hours to ~95% | Work that dominates |
|---|---:|---:|---|
| Full target | ≈ 45% | ≈ 850–1,500 h | `.aep`, fidelity corpus, bug backlog, performance, formats, hardware, localization |
| Mainstream practitioner | ≈ 47% | ≈ 280–530 h | `.aep` exchange, stability, everyday fidelity and bugs, preview speed |
| Essentials user | ≈ 52% | ≈ 150–300 h | Opening tutorial `.aep` files, start-up stability, core bugs, preview with sound |
About 60–70% of these hours parallelise ([detail](docs/target-app-parity.md#readiness-by-audience)).
Hours are one Opus 5.5 agent working sequentially, calibrated on this repository's history
([how](docs/target-app-parity.md#calibration-of-the-hours)).
### Stage
**Alpha**, because all six core workflows of the [alpha gate](docs/roadmap.md#alpha-gate)
(set up, animate, text and shapes, composite, preview, save and deliver) work end to end on macOS
and the work saves and reopens, and ready for real work (≈ 45%) is above the ≈ 40% bar. But: After Effects projects (`.aep`) can't be opened, which
alone rules out beta; fidelity against After Effects is measured for one narrow case (scalar ease,
168 samples); 216 issues are open on 12 October, six of them crash reports (#580, #501, #426, #341, #234, #879). Beta needs ≈ 75% ready and `.aep`
opening reliably: the `.aep` owner decision and importer (80–160 h), the fidelity harness and
P0/P1 corpus (60–100 h), the crash and bug backlog (40–80 h), performance (40–70 h),
Linux/Windows reliability (30–50 h) and depth in animation, masks, effects and text (100–160 h).
## By dimension
| Dimension | % | Hours | Doc |
|---|---:|---:|---|
| Features: breadth | 91% | — | [target-app-parity.md](docs/target-app-parity.md#by-feature-area) |
| Features: ready for real work | 50% | 360–660 | [target-app-parity.md](docs/target-app-parity.md#by-feature-area) |
| UI/UX fidelity | 65% | 60–120 | [ui-parity.md](docs/ui-parity.md) |
| File formats | 50% | 130–230 | [file-format-parity.md](docs/file-format-parity.md) |
| Hardware | 40% | 60–120 | [hardware-parity.md](docs/hardware-parity.md) |
| Localization | 24% | 60–110 + review | [localization-parity.md](docs/localization-parity.md) |
| Performance | 35% | 50–100 | [gaps.md](docs/gaps.md#g5-performance-at-real-world-scale) |
| Stability | 45% | 40–80 | [gaps.md](docs/gaps.md#g3-stability-and-a-green-main) |
| Platforms | 65% | 30–60 | [gaps.md](docs/gaps.md#g2-real-user-reliability-on-every-platform) |
| Ecosystem and plug-ins | 20% | 40–80 | [plugins.md](docs/plugins.md), [scripting-parity.md](docs/scripting-parity.md) |
| AI features | 35% | 40–80 | [gaps.md](docs/gaps.md) |
| Agent control (beyond After Effects) | ahead | — | [agents.md](docs/agents.md) |
## Features
| Area | Breadth | Ready | Hours |
|---|---:|---:|---:|
| Project, footage and import | 92% | 55% | 25–45 |
| Compositions, layers and compositing | 98% | 60% | 30–55 |
| Animation: keyframes, graph editor, motion paths, puppet | 97% | 55% | 35–60 |
| Expressions and scripting ([detail](docs/scripting-parity.md)) | 93% | 65% | 20–40 |
| Shapes, masks and roto | 93% | 45% | 30–50 |
| Text and typography | 94% | 50% | 20–35 |
| Effects and animation presets ([detail](docs/effects-parity.md)) | 85% | 40% | 70–130 |
| 3D (Classic and Advanced) | 82% | 35% | 35–65 |
| Tracking and AI-assisted tools | 80% | 35% | 35–70 |
| Preview and cache | 92% | 40% | 25–50 |
| Render queue and export | 85% | 55% | 25–45 |
| Audio | 85% | 45% | 6–12 |
Weights and evidence per row: [target-app-parity.md](docs/target-app-parity.md#by-feature-area).
## Languages
UI strings translated out of ≈ 4,200 (detail: [localization-parity.md](docs/localization-parity.md)).
| Language | Status | % |
|---|---|---:|
| English | full | 100% |
| Simplified Chinese | partial (menus, most of the panels) | 46% |
| Spanish | menus only | 29% |
| Hindi | none | 0% |
| Arabic | none | 0% |
| French | menus only | 16% |
| Portuguese (Brazil) | partial (menus, part of the panels) | 34% |
| Indonesian | none | 0% |
| Japanese | partial (menus, most of the panels) | 46% |
| German | menus only | 16% |
| Korean | none | 0% |
| Vietnamese | none | 0% |
Also shipped: Traditional Chinese (partial, 38%), Ukrainian (partial, 29%), Czech (partial, 29%), Polish (partial, 34%), Georgian (partial, 39%; native-speaker reviewed), Russian (menus only, 15%) and Swedish (menus, panels and effect names; not yet measured). No other language has had
native-speaker review.
## Upcoming
Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gaps.md).
| | Next | Hours |
|---|---|---:|
| 1 | Fix the open crash reports; run `cargo xtask ci` on every pull request (G3) | 15–30 |
| 2 | Fidelity harness against After Effects: rendered frames, a corpus case per P0/P1 feature (G1) | 60–100 |
| 3 | Real-user reliability on Linux and Windows; the open mask, effect, text and 3D bugs (G2) | 100–190 |
| 4 | `.aep` / `.aepx` import, after the owner's clean-room decision (G4) | 80–160 |
| 5 | Performance: benchmarks at 1080p / 4K, real-time cached playback, hardware video (G5) | 50–100 |
| 6 | Localization depth: effect names, all panels, Korean / Italian (G9) | 60–110 |
Come tell us what matters most to you on [Discord](https://discord.gg/artcraft).
## Progress log
- 2026-11-05: ProRes 4444 and XQ exports write the required run after the final alpha
difference. The FilmCraft encoder pin fixes alpha movies rejected by native macOS
decoding; an export regression checks the stored bytes for both profiles, alpha modes,
and motion-blur settings (#293). Resolve itself still needs confirmation.
- 2026-10-30: Text strokes take a Line Join (Miter, Round or Bevel) in the Character panel,
Miter for new text as in After Effects (#2077). In the Timeline, effects, masks and shape items
drag to a new place in their stack (#2210) and Shift-click adds them to the selection, so one
animation preset can hold several (#2552). The Composition panel pans while a mask vertex is
held at its edge (#999). Effect buffers that pad a small layer stay within a memory budget
(#2261); an MP4 lasts as long as its edit list shows (#2592); the Render Queue's times tick
between frames (#2593). ScriptUI drags keep their control past the window edge with
`MouseEvent.buttons` (#2584), and docked panels get their dock's size and `onResize` (#2585).
- 2026-10-28: A shape layer's Rectangle and Ellipse Size get the Constrain Proportions chain
that Scale has, so a circle stays a circle while its size is typed, scrubbed or keyed; it
starts off, so existing shapes behave as before (#2489).
- 2026-10-24: Shape layers have the Wiggle Transform path operation (Contents ▸ Add, or
`layer.addShapeItem {kind: "wiggler"}`): it wiggles the anchor point, position, scale and
rotation of the paths above it by random amounts that change over time, up to the Transform
values it holds. The copies of a Repeater above it wiggle independently as far as its
Correlation allows; with the Repeater below it, they wiggle together.
- 2026-10-22: Localization: the panel/dialog catalogs gain 303 rows in Japanese, Simplified
Chinese and Traditional Chinese — panel tabs, form and dialog labels, panel context menus,
tracker/graph/text-panel controls, File-panel footers and credits. Recounted after #1774:
Japanese is 1,923 of ≈ 4,200 interface strings (46%, with 297 effect names); the Chinese
panel catalogs hold 998 (Simplified) and 951 (Traditional) rows, and Simplified Chinese is
46% with its 307 effect names; the overall share is ≈ 24%.
- 2026-10-16: Convert To Bezier Path turns a shape layer's Rectangle, Ellipse or Polystar
path into an editable Path 1 with the same geometry, as After Effects does; it used to
apply to mask RotoBezier only, so a parametric shape path could not be converted at all
and its vertices could not be edited or animated (#932).
- 2026-10-14: Georgian (`ka`): every menu row, every panel-catalog row the Chinese catalogs have plus 195 Georgian-only ones, and the effect names and categories; Match System for `ka` / `ka-GE` / `ka_GE.UTF-8` locales. Reviewed by a native speaker.
- 2026-10-12: Roto Brush span edits report strokes outside a trimmed layer without panicking;
strokes and undo history are preserved so restoring the timing allows the span edit again.
- 2026-10-11: Ukrainian panels caught up with the Chinese catalogs. The 95 rows the panel
catalog gained since 9 October are translated: the Home screen (recent files, their Kind and
Opened columns, the empty and no-match states), the viewer's toolbar tooltips, magnification,
resolution, grid, channel and Fast Previews popups, the Appearance settings, the solid dialog
and the project, import, export and colour-management forms. `uk` now carries 495 of the 496
panel rows — `RGB` reads the same in Ukrainian, so it falls back to the English source by
design — alongside its 618/618 menus.
- 2026-10-10: Full parity re-measure against After Effects 2026 26.5 (installed bundle, 26.2–26.5
release notes, repository counts): breadth ≈ 91%, ready ≈ 45%, stage alpha. Progress docs
restructured: `docs/parity.md` became `docs/target-app-parity.md`; new
`docs/roadmap.md`, `localization-parity.md`, `file-format-parity.md`, `hardware-parity.md`,
`ui-parity.md`, `effects-parity.md` and `scripting-parity.md`.
- 2026-10-10: Spanish (#460) and Brazilian Portuguese (#217) menu translations.
- 2026-10-10: Japanese now translates the interface below the menu bar too: `crates/ui-egui/src/i18n/ui.rs` has a Japanese table with the same 396 rows as the Chinese ones (panels, dialogs, buttons and tooltips; the shape test keeps the three catalogs listing the same source strings). Menus were already Japanese.
- 2026-10-09: Ukrainian now covers all 396 current panel-catalog strings, including dialogs, buttons and tooltips, alongside its menu translation. The catalog keeps the same source keys and placeholder counts as the Chinese catalogs; missing or engine-generated text still falls back to English.
- UI.6: Viewer breadcrumbs use theme colors for readable active text. The bottom control row scrolls horizontally at compact widths; keyboard focus reveals the selected control while dropdowns, exposure adjustments and snapshot holds retain their behavior.
- UI.3: Shared docking transactions and pointer targets now handle panel moves, splits, tab ordering and floating groups, preserving accordion metadata and the saved workspace schema. Dock dividers use bounded shared geometry and keyboard-accessible resize handles. Tiny layouts no longer reach an invalid floating-point clamp; workspace trees and saved layouts are retained.
- 2026-10-09: Settings ▸ General ▸ Language now translates the interface below the menu bar as well: panels, dialogs, buttons and tooltips read their text through `crates/ui-egui/src/i18n/ui.rs`, keyed by the English source string. Simplified and Traditional Chinese are complete (384 strings); Japanese rows can follow in the same file. A test fails when a converted panel gains a hard-coded string, and another keeps the catalog free of unused rows.
- 2026-10-08: `effectcraft-cli render --out` resolves a relative path against the working directory, like `--project` (it used the project's folder, which doubled a path that already named it). Render Queue outputs set in a project are unchanged.
- 2026-10-08: Long-press a grouped toolbar button to choose its tools, including Horizontal/Vertical Type; releasing the hold keeps the menu open. Existing point/paragraph text creation is unchanged.
- 2026-10-06: Settings ▸ General ▸ Language persists English/Japanese menu labels (`general.language`); native UI reuses installed Japanese font fallback, with no bundled CJK font. Dialog and panel contents remain English.
- 2026-10-05: Honest assessment ([docs/gaps.md](docs/gaps.md)): breadth ≈ 99% by our own checklist, real use ≈ 30–50%; workstreams G1–G9.
- 2026-10-04: Checklist audit at `d39c0e8`: 89 done / 3 partial / 0 missing of 92 features.
- 2026-10-01: First commit.
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Progress log: text Line Join, Timeline drag-reorder and Shift multi-select of effects, viewer edge pan, effect padding budget, MP4 edit-list duration, Render Queue clock, ScriptUI drag capture and docked-panel size |
| 2026-10-22 | minor | Localization: 300 more panel/dialog rows in Japanese and the Chinese catalogs, plus the review's three missing rows (Basic, Advanced, Play/Stop) — panel tabs, form and dialog labels, context menus, tracker/graph/text-panel controls, File-panel footers, credits; language shares recounted after #1774 (Japanese 27% → 46% with 297 effect names, Simplified Chinese 29% → 46% with 307 effect names, Traditional Chinese 29% → 38%, overall 18% → 24%) |
| 2026-10-12 | minor | Open-issue count (134 to 216), crash list and localization share (12% to 18%) updated to match docs/gaps.md |
| 2026-10-12 | patch | Roto Brush span validation after trimming; command regressions for both layer edges |
| 2026-10-12 | minor | Shared PDF object/xref parsing with bounded app-owned conversion and preserved stream budgets; drawing, EPS and image/font policy remain unchanged |
| 2026-10-10 | minor | Hours to ≈ 95% for each audience |
| 2026-10-10 | minor | Headline adds mainstream practitioner (≈ 47%) and essentials user (≈ 52%) |
| 2026-10-10 | major | Progress docs restructured: stage, two numbers, dimensions, features, languages, upcoming, progress log; milestones moved to docs/roadmap.md |
| 2026-10-10 | minor | Stage checked against the new core-workflow gate (all six pass; still alpha) |
| 2026-10-05 | major | Two numbers (breadth vs real use) and workstreams G1–G9 |
+8 -8
View File
@@ -1,8 +1,8 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-panic-in-tests = true
allow-indexing-slicing-in-tests = true
disallowed-types = [
{ path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
]
cognitive-complexity-threshold = 30
allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-panic-in-tests = true
allow-indexing-slicing-in-tests = true
disallowed-types = [
{ path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
]
cognitive-complexity-threshold = 30
+20 -20
View File
@@ -1,20 +1,20 @@
[package]
name = "effectcraft-svg"
description = "SVG import: parses SVG 1.1 / SVG 2 static vector content (paths, basic shapes, groups, transforms, use, CSS styles, solid, gradient and pattern fills/strokes, opacity) and rasterises it at any scale"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
craft-svg = { workspace = true }
effectcraft-geom = { workspace = true }
effectcraft-raster = { workspace = true }
effectcraft-path = { workspace = true }
kurbo = { workspace = true }
roxmltree = "0.21"
rayon = { workspace = true }
log = { workspace = true }
[package]
name = "effectcraft-svg"
description = "SVG import: parses SVG 1.1 / SVG 2 static vector content (paths, basic shapes, groups, transforms, use, CSS styles, solid, gradient and pattern fills/strokes, opacity) and rasterises it at any scale"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dependencies]
craft-svg = { workspace = true }
effectcraft-geom = { workspace = true }
effectcraft-raster = { workspace = true }
effectcraft-path = { workspace = true }
kurbo = { workspace = true }
roxmltree = "0.21"
rayon = { workspace = true }
log = { workspace = true }
+48 -48
View File
@@ -1,48 +1,48 @@
# effectcraft-svg
SVG import for EffectCraft: `.svg` files import as footage (rasterised at any scale, so
Continuously Rasterize stays sharp) and Layer ▸ Create ▸ Create Shapes from Vector Layer turns
them into editable shape layers.
Implemented from the W3C **Scalable Vector Graphics (SVG) 1.1 (Second Edition)** recommendation
(16 August 2011) and the **SVG 2** candidate recommendation (4 October 2018) for the parts listed
here, plus **CSS Color Module Level 3** for colour keywords and functions:
- document: `svg` (width/height, `viewBox`, `preserveAspectRatio`), nested `svg`, `g`, `defs`,
`use` (x/y, `href` / `xlink:href`), `symbol` (through `use`), `title`/`desc`/`metadata` ignored;
- shapes: `path` (all path data commands incl. elliptical arcs), `rect` (rx/ry), `circle`,
`ellipse`, `line`, `polyline`, `polygon`;
- `transform` lists (matrix, translate, scale, rotate with centre, skewX, skewY);
- painting: `fill`, `fill-rule`, `fill-opacity`, `stroke`, `stroke-width`, `stroke-linecap`,
`stroke-linejoin`, `stroke-miterlimit`, `stroke-dasharray`, `stroke-dashoffset`,
`stroke-opacity`, `opacity`, `display`, `visibility`, `color`/`currentColor`;
- `linearGradient` and `radialGradient` (with `fx`/`fy` focal point), `gradientUnits`,
`gradientTransform`, `spreadMethod`, `href` inheritance of stops and attributes;
- static (non-animated) `<pattern>` fill/stroke with `patternUnits`, `patternContentUnits`, `patternTransform`,
tile `x`/`y`/`width`/`height`, local `href`/`xlink:href` inheritance and solid paints on
supported shape/group children;
- styling through presentation attributes, the `style` attribute and `<style>` sheets with
type, class, id and universal selectors (simple selectors and selector lists).
Pattern `viewBox`, external pattern references, nested paint servers and non-shape pattern content
are unsupported; invalid patterns use a `url(...)` fallback when present. Local pattern `href`
chains are depth-limited and cycles are rejected. Tiles render once and are sampled across the
painted area; both live surfaces and aggregate tile work are bounded. Pattern paints are
rasterised in SVG footage; editable shape layers retain geometry and convert only a supplied
`url(...)` fallback to solid fill/stroke.
Path expansion and gradient resolution have per-import work limits. The rasterizer also caps live
surface memory and cumulative pattern, clip-coverage, and source-image work. Over-budget imports
return an error; over-budget raster operations are skipped, using a paint fallback where available.
`rasterize`/`rasterize_with` drop content refused by a budget and return the image drawn so far
(an empty image if the canvas itself is refused). `try_rasterize`/`try_rasterize_with` return
`Err(RasterError)` instead (`SurfaceLimit` for memory, `WorkLimit` for work). Budgets per render:
512 MiB of live offscreen surfaces, 512 MiB of pattern tiles, 256 Mi units each of pattern
render work, clip-path rasterisation work and image sampling work, and 2^31 pixels touched across
all passes.
For SVG input, text, images, filters, clip paths, masks, markers and animation (including pattern
animation) are unsupported and ignored. The PDF importers' documents do use clip, mask, blend and
image nodes, which the rasterizer renders under the same budgets.
XML parsing uses `roxmltree` (MIT/Apache-2.0). Test fixtures are original work (see
`tests/fixtures/*.attribution`).
# effectcraft-svg
SVG import for EffectCraft: `.svg` files import as footage (rasterised at any scale, so
Continuously Rasterize stays sharp) and Layer ▸ Create ▸ Create Shapes from Vector Layer turns
them into editable shape layers.
Implemented from the W3C **Scalable Vector Graphics (SVG) 1.1 (Second Edition)** recommendation
(16 August 2011) and the **SVG 2** candidate recommendation (4 October 2018) for the parts listed
here, plus **CSS Color Module Level 3** for colour keywords and functions:
- document: `svg` (width/height, `viewBox`, `preserveAspectRatio`), nested `svg`, `g`, `defs`,
`use` (x/y, `href` / `xlink:href`), `symbol` (through `use`), `title`/`desc`/`metadata` ignored;
- shapes: `path` (all path data commands incl. elliptical arcs), `rect` (rx/ry), `circle`,
`ellipse`, `line`, `polyline`, `polygon`;
- `transform` lists (matrix, translate, scale, rotate with centre, skewX, skewY);
- painting: `fill`, `fill-rule`, `fill-opacity`, `stroke`, `stroke-width`, `stroke-linecap`,
`stroke-linejoin`, `stroke-miterlimit`, `stroke-dasharray`, `stroke-dashoffset`,
`stroke-opacity`, `opacity`, `display`, `visibility`, `color`/`currentColor`;
- `linearGradient` and `radialGradient` (with `fx`/`fy` focal point), `gradientUnits`,
`gradientTransform`, `spreadMethod`, `href` inheritance of stops and attributes;
- static (non-animated) `<pattern>` fill/stroke with `patternUnits`, `patternContentUnits`, `patternTransform`,
tile `x`/`y`/`width`/`height`, local `href`/`xlink:href` inheritance and solid paints on
supported shape/group children;
- styling through presentation attributes, the `style` attribute and `<style>` sheets with
type, class, id and universal selectors (simple selectors and selector lists).
Pattern `viewBox`, external pattern references, nested paint servers and non-shape pattern content
are unsupported; invalid patterns use a `url(...)` fallback when present. Local pattern `href`
chains are depth-limited and cycles are rejected. Tiles render once and are sampled across the
painted area; both live surfaces and aggregate tile work are bounded. Pattern paints are
rasterised in SVG footage; editable shape layers retain geometry and convert only a supplied
`url(...)` fallback to solid fill/stroke.
Path expansion and gradient resolution have per-import work limits. The rasterizer also caps live
surface memory and cumulative pattern, clip-coverage, and source-image work. Over-budget imports
return an error; over-budget raster operations are skipped, using a paint fallback where available.
`rasterize`/`rasterize_with` drop content refused by a budget and return the image drawn so far
(an empty image if the canvas itself is refused). `try_rasterize`/`try_rasterize_with` return
`Err(RasterError)` instead (`SurfaceLimit` for memory, `WorkLimit` for work). Budgets per render:
512 MiB of live offscreen surfaces, 512 MiB of pattern tiles, 256 Mi units each of pattern
render work, clip-path rasterisation work and image sampling work, and 2^31 pixels touched across
all passes.
For SVG input, text, images, filters, clip paths, masks, markers and animation (including pattern
animation) are unsupported and ignored. The PDF importers' documents do use clip, mask, blend and
image nodes, which the rasterizer renders under the same budgets.
XML parsing uses `roxmltree` (MIT/Apache-2.0). Test fixtures are original work (see
`tests/fixtures/*.attribution`).
+64 -64
View File
@@ -1,64 +1,64 @@
use kurbo::{Affine, BezPath, Rect, Shape as _};
use crate::{Doc, Group, Node};
impl Doc {
/// Geometric artwork bounds in document pixels, including strokes and clipping.
/// Opacity and soft masks do not change the artwork's dimensions.
pub fn artwork_bounds(&self) -> Option<Rect> {
let page = Rect::new(0.0, 0.0, self.width, self.height);
group_bounds(&self.root, Affine::IDENTITY, 0, page)
}
}
fn path_bounds(path: BezPath) -> Option<Rect> {
if path.elements().is_empty() || !path.is_finite() {
return None;
}
let r = path.bounding_box();
(r.is_finite() && r.width() > 0.0 && r.height() > 0.0).then_some(r)
}
fn group_bounds(group: &Group, transform: Affine, depth: usize, page: Rect) -> Option<Rect> {
if depth > 256 {
return Some(page);
}
let transform = transform * group.transform;
if !transform.as_coeffs().iter().all(|v| v.is_finite()) {
return None;
}
let mut bounds: Option<Rect> = None;
for node in &group.children {
let rect = match node {
Node::Group(g) => group_bounds(g, transform, depth + 1, page),
Node::Image(i) => {
let rect = Rect::new(0.0, 0.0, f64::from(i.width), f64::from(i.height));
path_bounds(transform * i.transform * rect.to_path(0.01))
}
Node::Shape(s) => {
let m = transform * s.transform;
let path = s.geom.to_path();
let mut rect = s.fill.as_ref().and_then(|_| path_bounds(m * &path));
if let Some(stroke) = &s.stroke {
let scale = m.determinant().abs().sqrt();
if scale.is_finite()
&& path.is_finite()
&& let Some(outline) = effectcraft_path::stroke_outline(std::slice::from_ref(&path), &stroke.style(), scale)
&& let Some(r) = path_bounds(m * outline)
{
rect = Some(rect.map_or(r, |b| b.union(r)));
}
}
rect
}
};
if let Some(r) = rect {
bounds = Some(bounds.map_or(r, |b| b.union(r)));
}
}
let mut bounds = bounds?;
for (clip, _) in &group.clip {
bounds = bounds.intersect(path_bounds(transform * clip)?);
}
(bounds.width() > 0.0 && bounds.height() > 0.0).then_some(bounds)
}
use kurbo::{Affine, BezPath, Rect, Shape as _};
use crate::{Doc, Group, Node};
impl Doc {
/// Geometric artwork bounds in document pixels, including strokes and clipping.
/// Opacity and soft masks do not change the artwork's dimensions.
pub fn artwork_bounds(&self) -> Option<Rect> {
let page = Rect::new(0.0, 0.0, self.width, self.height);
group_bounds(&self.root, Affine::IDENTITY, 0, page)
}
}
fn path_bounds(path: BezPath) -> Option<Rect> {
if path.elements().is_empty() || !path.is_finite() {
return None;
}
let r = path.bounding_box();
(r.is_finite() && r.width() > 0.0 && r.height() > 0.0).then_some(r)
}
fn group_bounds(group: &Group, transform: Affine, depth: usize, page: Rect) -> Option<Rect> {
if depth > 256 {
return Some(page);
}
let transform = transform * group.transform;
if !transform.as_coeffs().iter().all(|v| v.is_finite()) {
return None;
}
let mut bounds: Option<Rect> = None;
for node in &group.children {
let rect = match node {
Node::Group(g) => group_bounds(g, transform, depth + 1, page),
Node::Image(i) => {
let rect = Rect::new(0.0, 0.0, f64::from(i.width), f64::from(i.height));
path_bounds(transform * i.transform * rect.to_path(0.01))
}
Node::Shape(s) => {
let m = transform * s.transform;
let path = s.geom.to_path();
let mut rect = s.fill.as_ref().and_then(|_| path_bounds(m * &path));
if let Some(stroke) = &s.stroke {
let scale = m.determinant().abs().sqrt();
if scale.is_finite()
&& path.is_finite()
&& let Some(outline) = effectcraft_path::stroke_outline(std::slice::from_ref(&path), &stroke.style(), scale)
&& let Some(r) = path_bounds(m * outline)
{
rect = Some(rect.map_or(r, |b| b.union(r)));
}
}
rect
}
};
if let Some(r) = rect {
bounds = Some(bounds.map_or(r, |b| b.union(r)));
}
}
let mut bounds = bounds?;
for (clip, _) in &group.clip {
bounds = bounds.intersect(path_bounds(transform * clip)?);
}
(bounds.width() > 0.0 && bounds.height() > 0.0).then_some(bounds)
}
+282 -282
View File
@@ -1,282 +1,282 @@
//! CSS colour values (CSS Color Module Level 3): keywords, `#rgb[a]`, `#rrggbb[aa]`, `rgb[a]()`,
//! `hsl[a]()`.
/// The CSS colour keywords (CSS Color Module Level 3, §4.3 extended colour keywords).
const NAMED: &[(&str, u32)] = &[
("aliceblue", 0xF0F8FF),
("antiquewhite", 0xFAEBD7),
("aqua", 0x00FFFF),
("aquamarine", 0x7FFFD4),
("azure", 0xF0FFFF),
("beige", 0xF5F5DC),
("bisque", 0xFFE4C4),
("black", 0x000000),
("blanchedalmond", 0xFFEBCD),
("blue", 0x0000FF),
("blueviolet", 0x8A2BE2),
("brown", 0xA52A2A),
("burlywood", 0xDEB887),
("cadetblue", 0x5F9EA0),
("chartreuse", 0x7FFF00),
("chocolate", 0xD2691E),
("coral", 0xFF7F50),
("cornflowerblue", 0x6495ED),
("cornsilk", 0xFFF8DC),
("crimson", 0xDC143C),
("cyan", 0x00FFFF),
("darkblue", 0x00008B),
("darkcyan", 0x008B8B),
("darkgoldenrod", 0xB8860B),
("darkgray", 0xA9A9A9),
("darkgreen", 0x006400),
("darkgrey", 0xA9A9A9),
("darkkhaki", 0xBDB76B),
("darkmagenta", 0x8B008B),
("darkolivegreen", 0x556B2F),
("darkorange", 0xFF8C00),
("darkorchid", 0x9932CC),
("darkred", 0x8B0000),
("darksalmon", 0xE9967A),
("darkseagreen", 0x8FBC8F),
("darkslateblue", 0x483D8B),
("darkslategray", 0x2F4F4F),
("darkslategrey", 0x2F4F4F),
("darkturquoise", 0x00CED1),
("darkviolet", 0x9400D3),
("deeppink", 0xFF1493),
("deepskyblue", 0x00BFFF),
("dimgray", 0x696969),
("dimgrey", 0x696969),
("dodgerblue", 0x1E90FF),
("firebrick", 0xB22222),
("floralwhite", 0xFFFAF0),
("forestgreen", 0x228B22),
("fuchsia", 0xFF00FF),
("gainsboro", 0xDCDCDC),
("ghostwhite", 0xF8F8FF),
("gold", 0xFFD700),
("goldenrod", 0xDAA520),
("gray", 0x808080),
("grey", 0x808080),
("green", 0x008000),
("greenyellow", 0xADFF2F),
("honeydew", 0xF0FFF0),
("hotpink", 0xFF69B4),
("indianred", 0xCD5C5C),
("indigo", 0x4B0082),
("ivory", 0xFFFFF0),
("khaki", 0xF0E68C),
("lavender", 0xE6E6FA),
("lavenderblush", 0xFFF0F5),
("lawngreen", 0x7CFC00),
("lemonchiffon", 0xFFFACD),
("lightblue", 0xADD8E6),
("lightcoral", 0xF08080),
("lightcyan", 0xE0FFFF),
("lightgoldenrodyellow", 0xFAFAD2),
("lightgray", 0xD3D3D3),
("lightgreen", 0x90EE90),
("lightgrey", 0xD3D3D3),
("lightpink", 0xFFB6C1),
("lightsalmon", 0xFFA07A),
("lightseagreen", 0x20B2AA),
("lightskyblue", 0x87CEFA),
("lightslategray", 0x778899),
("lightslategrey", 0x778899),
("lightsteelblue", 0xB0C4DE),
("lightyellow", 0xFFFFE0),
("lime", 0x00FF00),
("limegreen", 0x32CD32),
("linen", 0xFAF0E6),
("magenta", 0xFF00FF),
("maroon", 0x800000),
("mediumaquamarine", 0x66CDAA),
("mediumblue", 0x0000CD),
("mediumorchid", 0xBA55D3),
("mediumpurple", 0x9370DB),
("mediumseagreen", 0x3CB371),
("mediumslateblue", 0x7B68EE),
("mediumspringgreen", 0x00FA9A),
("mediumturquoise", 0x48D1CC),
("mediumvioletred", 0xC71585),
("midnightblue", 0x191970),
("mintcream", 0xF5FFFA),
("mistyrose", 0xFFE4E1),
("moccasin", 0xFFE4B5),
("navajowhite", 0xFFDEAD),
("navy", 0x000080),
("oldlace", 0xFDF5E6),
("olive", 0x808000),
("olivedrab", 0x6B8E23),
("orange", 0xFFA500),
("orangered", 0xFF4500),
("orchid", 0xDA70D6),
("palegoldenrod", 0xEEE8AA),
("palegreen", 0x98FB98),
("paleturquoise", 0xAFEEEE),
("palevioletred", 0xDB7093),
("papayawhip", 0xFFEFD5),
("peachpuff", 0xFFDAB9),
("peru", 0xCD853F),
("pink", 0xFFC0CB),
("plum", 0xDDA0DD),
("powderblue", 0xB0E0E6),
("purple", 0x800080),
("rebeccapurple", 0x663399),
("red", 0xFF0000),
("rosybrown", 0xBC8F8F),
("royalblue", 0x4169E1),
("saddlebrown", 0x8B4513),
("salmon", 0xFA8072),
("sandybrown", 0xF4A460),
("seagreen", 0x2E8B57),
("seashell", 0xFFF5EE),
("sienna", 0xA0522D),
("silver", 0xC0C0C0),
("skyblue", 0x87CEEB),
("slateblue", 0x6A5ACD),
("slategray", 0x708090),
("slategrey", 0x708090),
("snow", 0xFFFAFA),
("springgreen", 0x00FF7F),
("steelblue", 0x4682B4),
("tan", 0xD2B48C),
("teal", 0x008080),
("thistle", 0xD8BFD8),
("tomato", 0xFF6347),
("turquoise", 0x40E0D0),
("violet", 0xEE82EE),
("wheat", 0xF5DEB3),
("white", 0xFFFFFF),
("whitesmoke", 0xF5F5F5),
("yellow", 0xFFFF00),
("yellowgreen", 0x9ACD32),
];
/// Parse a CSS colour into RGBA 0..1 (`transparent` is RGBA 0). `None` for anything else
/// (`none`, `currentColor` and `url()` are handled by the caller).
pub fn parse_color(s: &str) -> Option<[f64; 4]> {
let s = s.trim();
let lower = s.to_ascii_lowercase();
if lower == "transparent" {
return Some([0.0; 4]);
}
if let Some(h) = lower.strip_prefix('#') {
let d: Vec<u32> = h.chars().map(|c| c.to_digit(16)).collect::<Option<_>>()?;
let v = |i: usize| d[i] as f64 / 15.0;
let w = |i: usize| (d[i] * 16 + d[i + 1]) as f64 / 255.0;
return Some(match d.len() {
3 => [v(0), v(1), v(2), 1.0],
4 => [v(0), v(1), v(2), v(3)],
6 => [w(0), w(2), w(4), 1.0],
8 => [w(0), w(2), w(4), w(6)],
_ => return None,
});
}
if let Some(args) = func(&lower, "rgba").or_else(|| func(&lower, "rgb")) {
let a: Vec<&str> = split_args(args);
if a.len() < 3 {
return None;
}
let comp = |t: &str| -> Option<f64> {
Some(if let Some(p) = t.strip_suffix('%') {
p.trim().parse::<f64>().ok().filter(|v| v.is_finite())? / 100.0
} else {
t.parse::<f64>().ok().filter(|v| v.is_finite())? / 255.0
})
};
let alpha = a.get(3).map_or(Some(1.0), |t| alpha(t))?;
return Some([comp(a[0])?.clamp(0.0, 1.0), comp(a[1])?.clamp(0.0, 1.0), comp(a[2])?.clamp(0.0, 1.0), alpha]);
}
if let Some(args) = func(&lower, "hsla").or_else(|| func(&lower, "hsl")) {
let a: Vec<&str> = split_args(args);
if a.len() < 3 {
return None;
}
let h = a[0].trim_end_matches("deg").parse::<f64>().ok().filter(|v| v.is_finite())?.rem_euclid(360.0) / 360.0;
let s = a[1].trim_end_matches('%').parse::<f64>().ok().filter(|v| v.is_finite())?.clamp(0.0, 100.0) / 100.0;
let l = a[2].trim_end_matches('%').parse::<f64>().ok().filter(|v| v.is_finite())?.clamp(0.0, 100.0) / 100.0;
let al = a.get(3).map_or(Some(1.0), |t| alpha(t))?;
let [r, g, b] = hsl(h, s, l);
return Some([r, g, b, al]);
}
NAMED.iter().find(|(n, _)| *n == lower).map(|(_, v)| [((v >> 16) & 255) as f64 / 255.0, ((v >> 8) & 255) as f64 / 255.0, (v & 255) as f64 / 255.0, 1.0])
}
fn func<'a>(s: &'a str, name: &str) -> Option<&'a str> {
s.strip_prefix(name)?.trim_start().strip_prefix('(')?.strip_suffix(')')
}
fn split_args(s: &str) -> Vec<&str> {
s.split(|c: char| c == ',' || c == '/' || c.is_whitespace()).filter(|t| !t.is_empty()).collect()
}
fn alpha(t: &str) -> Option<f64> {
Some(
if let Some(p) = t.strip_suffix('%') {
p.parse::<f64>().ok().filter(|v| v.is_finite())? / 100.0
} else {
t.parse::<f64>().ok().filter(|v| v.is_finite())?
}
.clamp(0.0, 1.0),
)
}
/// CSS HSL → RGB (CSS Color 3 §4.2.4).
fn hsl(h: f64, s: f64, l: f64) -> [f64; 3] {
let m2 = if l <= 0.5 { l * (s + 1.0) } else { l + s - l * s };
let m1 = l * 2.0 - m2;
let hue = |mut h: f64| {
if h < 0.0 {
h += 1.0;
}
if h > 1.0 {
h -= 1.0;
}
if h * 6.0 < 1.0 {
m1 + (m2 - m1) * h * 6.0
} else if h * 2.0 < 1.0 {
m2
} else if h * 3.0 < 2.0 {
m1 + (m2 - m1) * (2.0 / 3.0 - h) * 6.0
} else {
m1
}
};
[hue(h + 1.0 / 3.0), hue(h), hue(h - 1.0 / 3.0)]
}
#[cfg(test)]
mod tests {
use super::parse_color;
#[test]
/// Non-finite channel values are rejected.
fn non_finite_rejected() {
for s in [
"rgb(nan,0,0)",
"rgb(inf,0,0)",
"rgba(0,0,0,nan)",
"rgba(0,0,0,inf)",
"hsl(nan,100%,50%)",
"hsl(120,nan%,50%)",
"hsl(120,100%,inf%)",
"hsl(120,100%,50%,nan)",
] {
assert_eq!(parse_color(s), None, "{s}");
}
}
#[test]
fn colors() {
assert_eq!(parse_color("#f00"), Some([1.0, 0.0, 0.0, 1.0]));
assert_eq!(parse_color("#00ff0080").map(|c| (c[1], (c[3] * 255.0).round())), Some((1.0, 128.0)));
assert_eq!(parse_color("rgb(255, 0, 0)"), Some([1.0, 0.0, 0.0, 1.0]));
assert_eq!(parse_color("rgba(0,0,255,0.5)"), Some([0.0, 0.0, 1.0, 0.5]));
assert_eq!(parse_color("rgb(100%, 50%, 0%)"), Some([1.0, 0.5, 0.0, 1.0]));
assert_eq!(parse_color("hsl(120, 100%, 50%)"), Some([0.0, 1.0, 0.0, 1.0]));
assert_eq!(parse_color("CornflowerBlue"), Some([100.0 / 255.0, 149.0 / 255.0, 237.0 / 255.0, 1.0]));
assert_eq!(parse_color("transparent"), Some([0.0; 4]));
assert_eq!(parse_color("nonsense"), None);
}
}
//! CSS colour values (CSS Color Module Level 3): keywords, `#rgb[a]`, `#rrggbb[aa]`, `rgb[a]()`,
//! `hsl[a]()`.
/// The CSS colour keywords (CSS Color Module Level 3, §4.3 extended colour keywords).
const NAMED: &[(&str, u32)] = &[
("aliceblue", 0xF0F8FF),
("antiquewhite", 0xFAEBD7),
("aqua", 0x00FFFF),
("aquamarine", 0x7FFFD4),
("azure", 0xF0FFFF),
("beige", 0xF5F5DC),
("bisque", 0xFFE4C4),
("black", 0x000000),
("blanchedalmond", 0xFFEBCD),
("blue", 0x0000FF),
("blueviolet", 0x8A2BE2),
("brown", 0xA52A2A),
("burlywood", 0xDEB887),
("cadetblue", 0x5F9EA0),
("chartreuse", 0x7FFF00),
("chocolate", 0xD2691E),
("coral", 0xFF7F50),
("cornflowerblue", 0x6495ED),
("cornsilk", 0xFFF8DC),
("crimson", 0xDC143C),
("cyan", 0x00FFFF),
("darkblue", 0x00008B),
("darkcyan", 0x008B8B),
("darkgoldenrod", 0xB8860B),
("darkgray", 0xA9A9A9),
("darkgreen", 0x006400),
("darkgrey", 0xA9A9A9),
("darkkhaki", 0xBDB76B),
("darkmagenta", 0x8B008B),
("darkolivegreen", 0x556B2F),
("darkorange", 0xFF8C00),
("darkorchid", 0x9932CC),
("darkred", 0x8B0000),
("darksalmon", 0xE9967A),
("darkseagreen", 0x8FBC8F),
("darkslateblue", 0x483D8B),
("darkslategray", 0x2F4F4F),
("darkslategrey", 0x2F4F4F),
("darkturquoise", 0x00CED1),
("darkviolet", 0x9400D3),
("deeppink", 0xFF1493),
("deepskyblue", 0x00BFFF),
("dimgray", 0x696969),
("dimgrey", 0x696969),
("dodgerblue", 0x1E90FF),
("firebrick", 0xB22222),
("floralwhite", 0xFFFAF0),
("forestgreen", 0x228B22),
("fuchsia", 0xFF00FF),
("gainsboro", 0xDCDCDC),
("ghostwhite", 0xF8F8FF),
("gold", 0xFFD700),
("goldenrod", 0xDAA520),
("gray", 0x808080),
("grey", 0x808080),
("green", 0x008000),
("greenyellow", 0xADFF2F),
("honeydew", 0xF0FFF0),
("hotpink", 0xFF69B4),
("indianred", 0xCD5C5C),
("indigo", 0x4B0082),
("ivory", 0xFFFFF0),
("khaki", 0xF0E68C),
("lavender", 0xE6E6FA),
("lavenderblush", 0xFFF0F5),
("lawngreen", 0x7CFC00),
("lemonchiffon", 0xFFFACD),
("lightblue", 0xADD8E6),
("lightcoral", 0xF08080),
("lightcyan", 0xE0FFFF),
("lightgoldenrodyellow", 0xFAFAD2),
("lightgray", 0xD3D3D3),
("lightgreen", 0x90EE90),
("lightgrey", 0xD3D3D3),
("lightpink", 0xFFB6C1),
("lightsalmon", 0xFFA07A),
("lightseagreen", 0x20B2AA),
("lightskyblue", 0x87CEFA),
("lightslategray", 0x778899),
("lightslategrey", 0x778899),
("lightsteelblue", 0xB0C4DE),
("lightyellow", 0xFFFFE0),
("lime", 0x00FF00),
("limegreen", 0x32CD32),
("linen", 0xFAF0E6),
("magenta", 0xFF00FF),
("maroon", 0x800000),
("mediumaquamarine", 0x66CDAA),
("mediumblue", 0x0000CD),
("mediumorchid", 0xBA55D3),
("mediumpurple", 0x9370DB),
("mediumseagreen", 0x3CB371),
("mediumslateblue", 0x7B68EE),
("mediumspringgreen", 0x00FA9A),
("mediumturquoise", 0x48D1CC),
("mediumvioletred", 0xC71585),
("midnightblue", 0x191970),
("mintcream", 0xF5FFFA),
("mistyrose", 0xFFE4E1),
("moccasin", 0xFFE4B5),
("navajowhite", 0xFFDEAD),
("navy", 0x000080),
("oldlace", 0xFDF5E6),
("olive", 0x808000),
("olivedrab", 0x6B8E23),
("orange", 0xFFA500),
("orangered", 0xFF4500),
("orchid", 0xDA70D6),
("palegoldenrod", 0xEEE8AA),
("palegreen", 0x98FB98),
("paleturquoise", 0xAFEEEE),
("palevioletred", 0xDB7093),
("papayawhip", 0xFFEFD5),
("peachpuff", 0xFFDAB9),
("peru", 0xCD853F),
("pink", 0xFFC0CB),
("plum", 0xDDA0DD),
("powderblue", 0xB0E0E6),
("purple", 0x800080),
("rebeccapurple", 0x663399),
("red", 0xFF0000),
("rosybrown", 0xBC8F8F),
("royalblue", 0x4169E1),
("saddlebrown", 0x8B4513),
("salmon", 0xFA8072),
("sandybrown", 0xF4A460),
("seagreen", 0x2E8B57),
("seashell", 0xFFF5EE),
("sienna", 0xA0522D),
("silver", 0xC0C0C0),
("skyblue", 0x87CEEB),
("slateblue", 0x6A5ACD),
("slategray", 0x708090),
("slategrey", 0x708090),
("snow", 0xFFFAFA),
("springgreen", 0x00FF7F),
("steelblue", 0x4682B4),
("tan", 0xD2B48C),
("teal", 0x008080),
("thistle", 0xD8BFD8),
("tomato", 0xFF6347),
("turquoise", 0x40E0D0),
("violet", 0xEE82EE),
("wheat", 0xF5DEB3),
("white", 0xFFFFFF),
("whitesmoke", 0xF5F5F5),
("yellow", 0xFFFF00),
("yellowgreen", 0x9ACD32),
];
/// Parse a CSS colour into RGBA 0..1 (`transparent` is RGBA 0). `None` for anything else
/// (`none`, `currentColor` and `url()` are handled by the caller).
pub fn parse_color(s: &str) -> Option<[f64; 4]> {
let s = s.trim();
let lower = s.to_ascii_lowercase();
if lower == "transparent" {
return Some([0.0; 4]);
}
if let Some(h) = lower.strip_prefix('#') {
let d: Vec<u32> = h.chars().map(|c| c.to_digit(16)).collect::<Option<_>>()?;
let v = |i: usize| d[i] as f64 / 15.0;
let w = |i: usize| (d[i] * 16 + d[i + 1]) as f64 / 255.0;
return Some(match d.len() {
3 => [v(0), v(1), v(2), 1.0],
4 => [v(0), v(1), v(2), v(3)],
6 => [w(0), w(2), w(4), 1.0],
8 => [w(0), w(2), w(4), w(6)],
_ => return None,
});
}
if let Some(args) = func(&lower, "rgba").or_else(|| func(&lower, "rgb")) {
let a: Vec<&str> = split_args(args);
if a.len() < 3 {
return None;
}
let comp = |t: &str| -> Option<f64> {
Some(if let Some(p) = t.strip_suffix('%') {
p.trim().parse::<f64>().ok().filter(|v| v.is_finite())? / 100.0
} else {
t.parse::<f64>().ok().filter(|v| v.is_finite())? / 255.0
})
};
let alpha = a.get(3).map_or(Some(1.0), |t| alpha(t))?;
return Some([comp(a[0])?.clamp(0.0, 1.0), comp(a[1])?.clamp(0.0, 1.0), comp(a[2])?.clamp(0.0, 1.0), alpha]);
}
if let Some(args) = func(&lower, "hsla").or_else(|| func(&lower, "hsl")) {
let a: Vec<&str> = split_args(args);
if a.len() < 3 {
return None;
}
let h = a[0].trim_end_matches("deg").parse::<f64>().ok().filter(|v| v.is_finite())?.rem_euclid(360.0) / 360.0;
let s = a[1].trim_end_matches('%').parse::<f64>().ok().filter(|v| v.is_finite())?.clamp(0.0, 100.0) / 100.0;
let l = a[2].trim_end_matches('%').parse::<f64>().ok().filter(|v| v.is_finite())?.clamp(0.0, 100.0) / 100.0;
let al = a.get(3).map_or(Some(1.0), |t| alpha(t))?;
let [r, g, b] = hsl(h, s, l);
return Some([r, g, b, al]);
}
NAMED.iter().find(|(n, _)| *n == lower).map(|(_, v)| [((v >> 16) & 255) as f64 / 255.0, ((v >> 8) & 255) as f64 / 255.0, (v & 255) as f64 / 255.0, 1.0])
}
fn func<'a>(s: &'a str, name: &str) -> Option<&'a str> {
s.strip_prefix(name)?.trim_start().strip_prefix('(')?.strip_suffix(')')
}
fn split_args(s: &str) -> Vec<&str> {
s.split(|c: char| c == ',' || c == '/' || c.is_whitespace()).filter(|t| !t.is_empty()).collect()
}
fn alpha(t: &str) -> Option<f64> {
Some(
if let Some(p) = t.strip_suffix('%') {
p.parse::<f64>().ok().filter(|v| v.is_finite())? / 100.0
} else {
t.parse::<f64>().ok().filter(|v| v.is_finite())?
}
.clamp(0.0, 1.0),
)
}
/// CSS HSL → RGB (CSS Color 3 §4.2.4).
fn hsl(h: f64, s: f64, l: f64) -> [f64; 3] {
let m2 = if l <= 0.5 { l * (s + 1.0) } else { l + s - l * s };
let m1 = l * 2.0 - m2;
let hue = |mut h: f64| {
if h < 0.0 {
h += 1.0;
}
if h > 1.0 {
h -= 1.0;
}
if h * 6.0 < 1.0 {
m1 + (m2 - m1) * h * 6.0
} else if h * 2.0 < 1.0 {
m2
} else if h * 3.0 < 2.0 {
m1 + (m2 - m1) * (2.0 / 3.0 - h) * 6.0
} else {
m1
}
};
[hue(h + 1.0 / 3.0), hue(h), hue(h - 1.0 / 3.0)]
}
#[cfg(test)]
mod tests {
use super::parse_color;
#[test]
/// Non-finite channel values are rejected.
fn non_finite_rejected() {
for s in [
"rgb(nan,0,0)",
"rgb(inf,0,0)",
"rgba(0,0,0,nan)",
"rgba(0,0,0,inf)",
"hsl(nan,100%,50%)",
"hsl(120,nan%,50%)",
"hsl(120,100%,inf%)",
"hsl(120,100%,50%,nan)",
] {
assert_eq!(parse_color(s), None, "{s}");
}
}
#[test]
fn colors() {
assert_eq!(parse_color("#f00"), Some([1.0, 0.0, 0.0, 1.0]));
assert_eq!(parse_color("#00ff0080").map(|c| (c[1], (c[3] * 255.0).round())), Some((1.0, 128.0)));
assert_eq!(parse_color("rgb(255, 0, 0)"), Some([1.0, 0.0, 0.0, 1.0]));
assert_eq!(parse_color("rgba(0,0,255,0.5)"), Some([0.0, 0.0, 1.0, 0.5]));
assert_eq!(parse_color("rgb(100%, 50%, 0%)"), Some([1.0, 0.5, 0.0, 1.0]));
assert_eq!(parse_color("hsl(120, 100%, 50%)"), Some([0.0, 1.0, 0.0, 1.0]));
assert_eq!(parse_color("CornflowerBlue"), Some([100.0 / 255.0, 149.0 / 255.0, 237.0 / 255.0, 1.0]));
assert_eq!(parse_color("transparent"), Some([0.0; 4]));
assert_eq!(parse_color("nonsense"), None);
}
}
+1852 -1852
View File
File diff suppressed because it is too large. Load diff
+435 -435
View File
@@ -1,435 +1,435 @@
//! SVG path data (SVG 1.1 §8.3 / SVG 2 §9.3): every command, implicit repeats, relative
//! coordinates, smooth curves and elliptical arcs (converted to cubics through kurbo's `SvgArc`).
//! Parsing stops at the first error, keeping what was read (as the specification asks).
use kurbo::{BezPath, PathEl, Point, SvgArc, Vec2};
/// Maximum path elements retained from one SVG path or point list.
pub(crate) const MAX_PATH_ELEMENTS: usize = 100_000;
/// Tokenizer over numbers and command letters.
struct Lex<'a> {
b: &'a [u8],
i: usize,
}
impl Lex<'_> {
fn skip_sep(&mut self) {
while self.i < self.b.len() && (self.b[self.i].is_ascii_whitespace() || self.b[self.i] == b',') {
self.i += 1;
}
}
fn command(&mut self) -> Option<u8> {
self.skip_sep();
let c = *self.b.get(self.i)?;
if c.is_ascii_alphabetic() && c != b'e' && c != b'E' {
self.i += 1;
Some(c)
} else {
None
}
}
fn at_number(&mut self) -> bool {
self.skip_sep();
self.b.get(self.i).is_some_and(|c| c.is_ascii_digit() || matches!(c, b'-' | b'+' | b'.'))
}
fn number(&mut self) -> Option<f64> {
self.skip_sep();
let start = self.i;
let b = self.b;
let mut i = self.i;
if i < b.len() && matches!(b[i], b'-' | b'+') {
i += 1;
}
let mut digits = false;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
digits = true;
}
if i < b.len() && b[i] == b'.' {
i += 1;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
digits = true;
}
}
if !digits {
return None;
}
if i < b.len() && matches!(b[i], b'e' | b'E') {
let mut j = i + 1;
if j < b.len() && matches!(b[j], b'-' | b'+') {
j += 1;
}
if j < b.len() && b[j].is_ascii_digit() {
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
i = j;
}
}
self.i = i;
let number = std::str::from_utf8(&b[start..i]).ok()?.parse::<f64>().ok()?;
number.is_finite().then_some(number)
}
/// Arc flags may be written without separators ("a1 1 0 00 1 1").
fn flag(&mut self) -> Option<bool> {
self.skip_sep();
let c = *self.b.get(self.i)?;
self.i += 1;
match c {
b'0' => Some(false),
b'1' => Some(true),
_ => None,
}
}
}
/// Parse no more than `max_values`; the flag reports whether another valid value was present.
pub(crate) fn numbers_bounded(s: &str, max_values: usize) -> (Vec<f64>, bool) {
let mut l = Lex { b: s.as_bytes(), i: 0 };
let mut v = vec![];
while l.at_number() {
if v.len() >= max_values {
return (v, true);
}
match l.number() {
Some(x) => v.push(x),
None => break,
}
}
(v, false)
}
/// Parse path data into a kurbo path, retaining at most [`MAX_PATH_ELEMENTS`] elements.
/// Malformed or overlong input returns the prefix parsed before the problem.
pub fn parse_path_data(d: &str) -> BezPath {
parse_path_data_bounded(d, MAX_PATH_ELEMENTS).0
}
/// Pen position and reflection state while parsing path commands.
struct PathState {
/// Current point.
current: Point,
/// Start of the current subpath.
start: Point,
/// Last cubic control point, for smooth curves.
last_cubic: Option<Point>,
/// Last quadratic control point, for smooth curves.
last_quad: Option<Point>,
/// Whether a subpath is open.
open: bool,
}
/// Cap on path elements and whether it has been hit.
struct PathLimit {
/// Maximum number of path elements.
max_elements: usize,
/// Set when the cap or a non-finite coordinate stopped output.
exceeded: bool,
}
/// Parse at most `max_elements`, reporting when more path geometry was present.
pub(crate) fn parse_path_data_bounded(d: &str, max_elements: usize) -> (BezPath, bool) {
let mut l = Lex { b: d.as_bytes(), i: 0 };
let mut p = BezPath::new();
let mut state = PathState { current: Point::ZERO, start: Point::ZERO, last_cubic: None, last_quad: None, open: false };
let mut limit = PathLimit { max_elements, exceeded: false };
let mut cmd: Option<u8> = None;
loop {
if p.elements().len() >= limit.max_elements {
l.skip_sep();
limit.exceeded = l.i < l.b.len();
break;
}
let c = match l.command() {
Some(c) => c,
None => match cmd {
// Implicit repeat (a moveto repeats as lineto). Z takes no numbers, so a number
// after it is an error: repeating Z would loop forever without consuming input.
Some(c) if l.at_number() && !c.eq_ignore_ascii_case(&b'z') => match c {
b'M' => b'L',
b'm' => b'l',
other => other,
},
_ => break,
},
};
let rel = c.is_ascii_lowercase();
let base = if rel { state.current.to_vec2() } else { Vec2::ZERO };
let pt = |l: &mut Lex| -> Option<Point> { Some(Point::new(l.number()?, l.number()?) + base) };
let ok = (|| -> Option<()> {
match c.to_ascii_uppercase() {
b'M' => {
state.current = pt(&mut l)?;
if !push_element(&mut p, PathEl::MoveTo(state.current), &mut limit) {
return None;
}
state.start = state.current;
state.open = true;
state.last_cubic = None;
state.last_quad = None;
}
b'Z' => {
if state.open && !push_element(&mut p, PathEl::ClosePath, &mut limit) {
return None;
}
state.current = state.start;
state.last_cubic = None;
state.last_quad = None;
}
_ if !state.open => {
// A segment without a preceding moveto starts at the current point.
if !push_element(&mut p, PathEl::MoveTo(state.current), &mut limit) {
return None;
}
state.open = true;
return segment(&mut l, c, base, &mut p, &mut state, &mut limit);
}
_ => return segment(&mut l, c, base, &mut p, &mut state, &mut limit),
}
Some(())
})();
if ok.is_none() {
break;
}
if limit.exceeded {
break;
}
cmd = Some(c);
if c.eq_ignore_ascii_case(&b'z') {
// After Z a new segment starts from the subpath start (an unused move is dropped
// below).
if !push_element(&mut p, PathEl::MoveTo(state.start), &mut limit) {
break;
}
state.open = true;
}
}
(strip_trailing_moves(p), limit.exceeded)
}
/// Push an element if finite and under the cap; otherwise mark the limit exceeded and return false.
fn push_element(p: &mut BezPath, element: PathEl, limit: &mut PathLimit) -> bool {
let finite = match element {
PathEl::MoveTo(point) | PathEl::LineTo(point) => point.x.is_finite() && point.y.is_finite(),
PathEl::QuadTo(control, point) => [control.x, control.y, point.x, point.y].iter().all(|value| value.is_finite()),
PathEl::CurveTo(first, second, point) => [first.x, first.y, second.x, second.y, point.x, point.y].iter().all(|value| value.is_finite()),
PathEl::ClosePath => true,
};
if p.elements().len() >= limit.max_elements || !finite {
limit.exceeded = true;
return false;
}
p.push(element);
true
}
/// Parse one path command's arguments into `p`, returning `None` on malformed or over-limit input.
fn segment(l: &mut Lex, c: u8, base: Vec2, p: &mut BezPath, state: &mut PathState, limit: &mut PathLimit) -> Option<()> {
let num = |l: &mut Lex| l.number();
let pt = |l: &mut Lex| -> Option<Point> { Some(Point::new(l.number()?, l.number()?) + base) };
match c.to_ascii_uppercase() {
b'L' => {
let current = pt(l)?;
if !push_element(p, PathEl::LineTo(current), limit) {
return None;
}
state.current = current;
state.last_cubic = None;
state.last_quad = None;
}
b'H' => {
let x = num(l)? + base.x;
let current = Point::new(x, state.current.y);
if !push_element(p, PathEl::LineTo(current), limit) {
return None;
}
state.current = current;
state.last_cubic = None;
state.last_quad = None;
}
b'V' => {
let y = num(l)? + base.y;
let current = Point::new(state.current.x, y);
if !push_element(p, PathEl::LineTo(current), limit) {
return None;
}
state.current = current;
state.last_cubic = None;
state.last_quad = None;
}
b'C' => {
let (c1, c2, e) = (pt(l)?, pt(l)?, pt(l)?);
if !push_element(p, PathEl::CurveTo(c1, c2, e), limit) {
return None;
}
state.last_cubic = Some(c2);
state.last_quad = None;
state.current = e;
}
b'S' => {
let c1 = state.last_cubic.map_or(state.current, |c| state.current + (state.current - c));
let (c2, e) = (pt(l)?, pt(l)?);
if !push_element(p, PathEl::CurveTo(c1, c2, e), limit) {
return None;
}
state.last_cubic = Some(c2);
state.last_quad = None;
state.current = e;
}
b'Q' => {
let (q, e) = (pt(l)?, pt(l)?);
if !push_element(p, PathEl::QuadTo(q, e), limit) {
return None;
}
state.last_quad = Some(q);
state.last_cubic = None;
state.current = e;
}
b'T' => {
let q = state.last_quad.map_or(state.current, |c| state.current + (state.current - c));
let e = pt(l)?;
if !push_element(p, PathEl::QuadTo(q, e), limit) {
return None;
}
state.last_quad = Some(q);
state.last_cubic = None;
state.current = e;
}
b'A' => {
let rx = num(l)?.abs();
let ry = num(l)?.abs();
let rot = num(l)?;
let large = l.flag()?;
let sweep = l.flag()?;
let e = pt(l)?;
let arc = SvgArc { from: state.current, to: e, radii: Vec2::new(rx, ry), x_rotation: rot.to_radians(), large_arc: large, sweep };
let mut last_end = None;
if let Some(a) = kurbo::Arc::from_svg_arc(&arc) {
// Scale tolerance with the radius so the segment count stays bounded.
let tol = 0.1_f64.max(a.radii.x.max(a.radii.y) * 1e-4);
a.to_cubic_beziers(tol, |c1, c2, end| {
if limit.exceeded {
return;
}
if push_element(p, PathEl::CurveTo(c1, c2, end), limit) {
last_end = Some(end);
}
});
}
if limit.exceeded {
return None;
}
// Degenerate arcs (e.g. huge radii) may emit nothing or stop short of the end point.
if last_end.is_none_or(|q| (q - e).hypot() > 1e-3 * (1.0 + e.to_vec2().hypot())) && !push_element(p, PathEl::LineTo(e), limit) {
return None;
}
state.current = e;
state.last_cubic = None;
state.last_quad = None;
}
_ => return None,
}
Some(())
}
/// Drop move-tos that start no segment (from closepath bookkeeping).
fn strip_trailing_moves(p: BezPath) -> BezPath {
use kurbo::PathEl;
let els = p.elements();
let mut out = BezPath::new();
for (i, e) in els.iter().enumerate() {
if let PathEl::MoveTo(_) = e {
let next_is_seg = els.get(i + 1).is_some_and(|n| !matches!(n, PathEl::MoveTo(_)));
if !next_is_seg {
continue;
}
}
out.push(*e);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use kurbo::PathEl;
#[test]
/// Huge arc radii stay bounded.
fn huge_arc_radius_stays_bounded() {
let p = parse_path_data("M0 0A1e36 1e36 0 1 1 1e30 0");
assert!(p.elements().len() < 1000, "got {} elements", p.elements().len());
}
#[test]
fn commands() {
let p = parse_path_data("M10,10 L20,10 h10 v10 H10 z m5 5 l1-1.5.5.5e1 C 0 0 1 1 2 2 s3 3 4 4 Q1 1 2 2 t3 3 A5 5 0 0 1 20 20 a5 5 0 1010 0");
let els = p.elements();
assert_eq!(els[0], PathEl::MoveTo(Point::new(10.0, 10.0)));
assert_eq!(els[2], PathEl::LineTo(Point::new(30.0, 10.0)));
assert_eq!(els[3], PathEl::LineTo(Point::new(30.0, 20.0)));
assert!(els.iter().any(|e| matches!(e, PathEl::ClosePath)));
// Relative move after Z is relative to the subpath start (10,10).
assert!(els.contains(&PathEl::MoveTo(Point::new(15.0, 15.0))));
// "l1-1.5.5.5e1": two implicit linetos (1,-1.5) then (.5,5).
assert!(els.contains(&PathEl::LineTo(Point::new(16.0, 13.5))));
assert!(els.contains(&PathEl::LineTo(Point::new(16.5, 18.5))));
assert!(els.iter().filter(|e| matches!(e, PathEl::CurveTo(..))).count() >= 4);
}
#[test]
fn errors_keep_prefix() {
let p = parse_path_data("M0 0 L10 0 L10 x10 L0 10");
assert_eq!(p.elements().len(), 2);
assert!(parse_path_data("garbage").elements().is_empty());
assert!(parse_path_data("").elements().is_empty());
}
#[test]
fn number_after_close_stops_instead_of_looping() {
// "Z9": Z was repeated implicitly without consuming the number, appending close-paths
// until memory ran out.
let p = parse_path_data("M0 0 L10 0 L10 10 Z9 C1 2 3 4 5 6");
assert_eq!(p.elements().len(), 4);
assert!(parse_path_data("M0 0 L10 0 z 9 9").elements().len() <= 4);
}
#[test]
/// Huge-radius arcs still reach their end point.
fn huge_radius_arc_still_reaches_end_point() {
for d in ["M0 0 A1e20 1e20 0 0 1 10 0 L10 10", "M0 0 A1e20 1e20 0 1 1 10 0 L10 10", "M0 0 a1e20 1e20 0 0 1 10 0 L10 10"] {
let p = parse_path_data(d);
let reaches = p.elements().iter().any(|e| match e {
PathEl::LineTo(q) | PathEl::CurveTo(_, _, q) => (q.x - 10.0).abs() < 1e-6 && q.y.abs() < 1e-6,
_ => false,
});
assert!(reaches, "{d}: {:?}", p.elements());
}
}
#[test]
fn number_lists() {
assert_eq!(numbers_bounded("0 0,100 50", 8).0, vec![0.0, 0.0, 100.0, 50.0]);
assert_eq!(numbers_bounded("-1-2.5.5", 8).0, vec![-1.0, -2.5, 0.5]);
assert_eq!(numbers_bounded("1e2,2E-1", 8).0, vec![100.0, 0.2]);
}
#[test]
/// The element cap stops expansion and reports truncation.
fn path_element_limit_stops_expansion_and_reports_truncation() {
let data = format!("M0 0{}", "h1".repeat(MAX_PATH_ELEMENTS + 1));
let (path, exceeded) = parse_path_data_bounded(&data, 32);
assert!(exceeded);
assert!(path.elements().len() <= 32);
}
#[test]
/// The number list cap stops retaining values.
fn number_list_limit_stops_retaining_values() {
let (values, exceeded) = numbers_bounded("1 2 3 4", 3);
assert_eq!(values, [1.0, 2.0, 3.0]);
assert!(exceeded);
}
}
//! SVG path data (SVG 1.1 §8.3 / SVG 2 §9.3): every command, implicit repeats, relative
//! coordinates, smooth curves and elliptical arcs (converted to cubics through kurbo's `SvgArc`).
//! Parsing stops at the first error, keeping what was read (as the specification asks).
use kurbo::{BezPath, PathEl, Point, SvgArc, Vec2};
/// Maximum path elements retained from one SVG path or point list.
pub(crate) const MAX_PATH_ELEMENTS: usize = 100_000;
/// Tokenizer over numbers and command letters.
struct Lex<'a> {
b: &'a [u8],
i: usize,
}
impl Lex<'_> {
fn skip_sep(&mut self) {
while self.i < self.b.len() && (self.b[self.i].is_ascii_whitespace() || self.b[self.i] == b',') {
self.i += 1;
}
}
fn command(&mut self) -> Option<u8> {
self.skip_sep();
let c = *self.b.get(self.i)?;
if c.is_ascii_alphabetic() && c != b'e' && c != b'E' {
self.i += 1;
Some(c)
} else {
None
}
}
fn at_number(&mut self) -> bool {
self.skip_sep();
self.b.get(self.i).is_some_and(|c| c.is_ascii_digit() || matches!(c, b'-' | b'+' | b'.'))
}
fn number(&mut self) -> Option<f64> {
self.skip_sep();
let start = self.i;
let b = self.b;
let mut i = self.i;
if i < b.len() && matches!(b[i], b'-' | b'+') {
i += 1;
}
let mut digits = false;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
digits = true;
}
if i < b.len() && b[i] == b'.' {
i += 1;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
digits = true;
}
}
if !digits {
return None;
}
if i < b.len() && matches!(b[i], b'e' | b'E') {
let mut j = i + 1;
if j < b.len() && matches!(b[j], b'-' | b'+') {
j += 1;
}
if j < b.len() && b[j].is_ascii_digit() {
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
i = j;
}
}
self.i = i;
let number = std::str::from_utf8(&b[start..i]).ok()?.parse::<f64>().ok()?;
number.is_finite().then_some(number)
}
/// Arc flags may be written without separators ("a1 1 0 00 1 1").
fn flag(&mut self) -> Option<bool> {
self.skip_sep();
let c = *self.b.get(self.i)?;
self.i += 1;
match c {
b'0' => Some(false),
b'1' => Some(true),
_ => None,
}
}
}
/// Parse no more than `max_values`; the flag reports whether another valid value was present.
pub(crate) fn numbers_bounded(s: &str, max_values: usize) -> (Vec<f64>, bool) {
let mut l = Lex { b: s.as_bytes(), i: 0 };
let mut v = vec![];
while l.at_number() {
if v.len() >= max_values {
return (v, true);
}
match l.number() {
Some(x) => v.push(x),
None => break,
}
}
(v, false)
}
/// Parse path data into a kurbo path, retaining at most [`MAX_PATH_ELEMENTS`] elements.
/// Malformed or overlong input returns the prefix parsed before the problem.
pub fn parse_path_data(d: &str) -> BezPath {
parse_path_data_bounded(d, MAX_PATH_ELEMENTS).0
}
/// Pen position and reflection state while parsing path commands.
struct PathState {
/// Current point.
current: Point,
/// Start of the current subpath.
start: Point,
/// Last cubic control point, for smooth curves.
last_cubic: Option<Point>,
/// Last quadratic control point, for smooth curves.
last_quad: Option<Point>,
/// Whether a subpath is open.
open: bool,
}
/// Cap on path elements and whether it has been hit.
struct PathLimit {
/// Maximum number of path elements.
max_elements: usize,
/// Set when the cap or a non-finite coordinate stopped output.
exceeded: bool,
}
/// Parse at most `max_elements`, reporting when more path geometry was present.
pub(crate) fn parse_path_data_bounded(d: &str, max_elements: usize) -> (BezPath, bool) {
let mut l = Lex { b: d.as_bytes(), i: 0 };
let mut p = BezPath::new();
let mut state = PathState { current: Point::ZERO, start: Point::ZERO, last_cubic: None, last_quad: None, open: false };
let mut limit = PathLimit { max_elements, exceeded: false };
let mut cmd: Option<u8> = None;
loop {
if p.elements().len() >= limit.max_elements {
l.skip_sep();
limit.exceeded = l.i < l.b.len();
break;
}
let c = match l.command() {
Some(c) => c,
None => match cmd {
// Implicit repeat (a moveto repeats as lineto). Z takes no numbers, so a number
// after it is an error: repeating Z would loop forever without consuming input.
Some(c) if l.at_number() && !c.eq_ignore_ascii_case(&b'z') => match c {
b'M' => b'L',
b'm' => b'l',
other => other,
},
_ => break,
},
};
let rel = c.is_ascii_lowercase();
let base = if rel { state.current.to_vec2() } else { Vec2::ZERO };
let pt = |l: &mut Lex| -> Option<Point> { Some(Point::new(l.number()?, l.number()?) + base) };
let ok = (|| -> Option<()> {
match c.to_ascii_uppercase() {
b'M' => {
state.current = pt(&mut l)?;
if !push_element(&mut p, PathEl::MoveTo(state.current), &mut limit) {
return None;
}
state.start = state.current;
state.open = true;
state.last_cubic = None;
state.last_quad = None;
}
b'Z' => {
if state.open && !push_element(&mut p, PathEl::ClosePath, &mut limit) {
return None;
}
state.current = state.start;
state.last_cubic = None;
state.last_quad = None;
}
_ if !state.open => {
// A segment without a preceding moveto starts at the current point.
if !push_element(&mut p, PathEl::MoveTo(state.current), &mut limit) {
return None;
}
state.open = true;
return segment(&mut l, c, base, &mut p, &mut state, &mut limit);
}
_ => return segment(&mut l, c, base, &mut p, &mut state, &mut limit),
}
Some(())
})();
if ok.is_none() {
break;
}
if limit.exceeded {
break;
}
cmd = Some(c);
if c.eq_ignore_ascii_case(&b'z') {
// After Z a new segment starts from the subpath start (an unused move is dropped
// below).
if !push_element(&mut p, PathEl::MoveTo(state.start), &mut limit) {
break;
}
state.open = true;
}
}
(strip_trailing_moves(p), limit.exceeded)
}
/// Push an element if finite and under the cap; otherwise mark the limit exceeded and return false.
fn push_element(p: &mut BezPath, element: PathEl, limit: &mut PathLimit) -> bool {
let finite = match element {
PathEl::MoveTo(point) | PathEl::LineTo(point) => point.x.is_finite() && point.y.is_finite(),
PathEl::QuadTo(control, point) => [control.x, control.y, point.x, point.y].iter().all(|value| value.is_finite()),
PathEl::CurveTo(first, second, point) => [first.x, first.y, second.x, second.y, point.x, point.y].iter().all(|value| value.is_finite()),
PathEl::ClosePath => true,
};
if p.elements().len() >= limit.max_elements || !finite {
limit.exceeded = true;
return false;
}
p.push(element);
true
}
/// Parse one path command's arguments into `p`, returning `None` on malformed or over-limit input.
fn segment(l: &mut Lex, c: u8, base: Vec2, p: &mut BezPath, state: &mut PathState, limit: &mut PathLimit) -> Option<()> {
let num = |l: &mut Lex| l.number();
let pt = |l: &mut Lex| -> Option<Point> { Some(Point::new(l.number()?, l.number()?) + base) };
match c.to_ascii_uppercase() {
b'L' => {
let current = pt(l)?;
if !push_element(p, PathEl::LineTo(current), limit) {
return None;
}
state.current = current;
state.last_cubic = None;
state.last_quad = None;
}
b'H' => {
let x = num(l)? + base.x;
let current = Point::new(x, state.current.y);
if !push_element(p, PathEl::LineTo(current), limit) {
return None;
}
state.current = current;
state.last_cubic = None;
state.last_quad = None;
}
b'V' => {
let y = num(l)? + base.y;
let current = Point::new(state.current.x, y);
if !push_element(p, PathEl::LineTo(current), limit) {
return None;
}
state.current = current;
state.last_cubic = None;
state.last_quad = None;
}
b'C' => {
let (c1, c2, e) = (pt(l)?, pt(l)?, pt(l)?);
if !push_element(p, PathEl::CurveTo(c1, c2, e), limit) {
return None;
}
state.last_cubic = Some(c2);
state.last_quad = None;
state.current = e;
}
b'S' => {
let c1 = state.last_cubic.map_or(state.current, |c| state.current + (state.current - c));
let (c2, e) = (pt(l)?, pt(l)?);
if !push_element(p, PathEl::CurveTo(c1, c2, e), limit) {
return None;
}
state.last_cubic = Some(c2);
state.last_quad = None;
state.current = e;
}
b'Q' => {
let (q, e) = (pt(l)?, pt(l)?);
if !push_element(p, PathEl::QuadTo(q, e), limit) {
return None;
}
state.last_quad = Some(q);
state.last_cubic = None;
state.current = e;
}
b'T' => {
let q = state.last_quad.map_or(state.current, |c| state.current + (state.current - c));
let e = pt(l)?;
if !push_element(p, PathEl::QuadTo(q, e), limit) {
return None;
}
state.last_quad = Some(q);
state.last_cubic = None;
state.current = e;
}
b'A' => {
let rx = num(l)?.abs();
let ry = num(l)?.abs();
let rot = num(l)?;
let large = l.flag()?;
let sweep = l.flag()?;
let e = pt(l)?;
let arc = SvgArc { from: state.current, to: e, radii: Vec2::new(rx, ry), x_rotation: rot.to_radians(), large_arc: large, sweep };
let mut last_end = None;
if let Some(a) = kurbo::Arc::from_svg_arc(&arc) {
// Scale tolerance with the radius so the segment count stays bounded.
let tol = 0.1_f64.max(a.radii.x.max(a.radii.y) * 1e-4);
a.to_cubic_beziers(tol, |c1, c2, end| {
if limit.exceeded {
return;
}
if push_element(p, PathEl::CurveTo(c1, c2, end), limit) {
last_end = Some(end);
}
});
}
if limit.exceeded {
return None;
}
// Degenerate arcs (e.g. huge radii) may emit nothing or stop short of the end point.
if last_end.is_none_or(|q| (q - e).hypot() > 1e-3 * (1.0 + e.to_vec2().hypot())) && !push_element(p, PathEl::LineTo(e), limit) {
return None;
}
state.current = e;
state.last_cubic = None;
state.last_quad = None;
}
_ => return None,
}
Some(())
}
/// Drop move-tos that start no segment (from closepath bookkeeping).
fn strip_trailing_moves(p: BezPath) -> BezPath {
use kurbo::PathEl;
let els = p.elements();
let mut out = BezPath::new();
for (i, e) in els.iter().enumerate() {
if let PathEl::MoveTo(_) = e {
let next_is_seg = els.get(i + 1).is_some_and(|n| !matches!(n, PathEl::MoveTo(_)));
if !next_is_seg {
continue;
}
}
out.push(*e);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use kurbo::PathEl;
#[test]
/// Huge arc radii stay bounded.
fn huge_arc_radius_stays_bounded() {
let p = parse_path_data("M0 0A1e36 1e36 0 1 1 1e30 0");
assert!(p.elements().len() < 1000, "got {} elements", p.elements().len());
}
#[test]
fn commands() {
let p = parse_path_data("M10,10 L20,10 h10 v10 H10 z m5 5 l1-1.5.5.5e1 C 0 0 1 1 2 2 s3 3 4 4 Q1 1 2 2 t3 3 A5 5 0 0 1 20 20 a5 5 0 1010 0");
let els = p.elements();
assert_eq!(els[0], PathEl::MoveTo(Point::new(10.0, 10.0)));
assert_eq!(els[2], PathEl::LineTo(Point::new(30.0, 10.0)));
assert_eq!(els[3], PathEl::LineTo(Point::new(30.0, 20.0)));
assert!(els.iter().any(|e| matches!(e, PathEl::ClosePath)));
// Relative move after Z is relative to the subpath start (10,10).
assert!(els.contains(&PathEl::MoveTo(Point::new(15.0, 15.0))));
// "l1-1.5.5.5e1": two implicit linetos (1,-1.5) then (.5,5).
assert!(els.contains(&PathEl::LineTo(Point::new(16.0, 13.5))));
assert!(els.contains(&PathEl::LineTo(Point::new(16.5, 18.5))));
assert!(els.iter().filter(|e| matches!(e, PathEl::CurveTo(..))).count() >= 4);
}
#[test]
fn errors_keep_prefix() {
let p = parse_path_data("M0 0 L10 0 L10 x10 L0 10");
assert_eq!(p.elements().len(), 2);
assert!(parse_path_data("garbage").elements().is_empty());
assert!(parse_path_data("").elements().is_empty());
}
#[test]
fn number_after_close_stops_instead_of_looping() {
// "Z9": Z was repeated implicitly without consuming the number, appending close-paths
// until memory ran out.
let p = parse_path_data("M0 0 L10 0 L10 10 Z9 C1 2 3 4 5 6");
assert_eq!(p.elements().len(), 4);
assert!(parse_path_data("M0 0 L10 0 z 9 9").elements().len() <= 4);
}
#[test]
/// Huge-radius arcs still reach their end point.
fn huge_radius_arc_still_reaches_end_point() {
for d in ["M0 0 A1e20 1e20 0 0 1 10 0 L10 10", "M0 0 A1e20 1e20 0 1 1 10 0 L10 10", "M0 0 a1e20 1e20 0 0 1 10 0 L10 10"] {
let p = parse_path_data(d);
let reaches = p.elements().iter().any(|e| match e {
PathEl::LineTo(q) | PathEl::CurveTo(_, _, q) => (q.x - 10.0).abs() < 1e-6 && q.y.abs() < 1e-6,
_ => false,
});
assert!(reaches, "{d}: {:?}", p.elements());
}
}
#[test]
fn number_lists() {
assert_eq!(numbers_bounded("0 0,100 50", 8).0, vec![0.0, 0.0, 100.0, 50.0]);
assert_eq!(numbers_bounded("-1-2.5.5", 8).0, vec![-1.0, -2.5, 0.5]);
assert_eq!(numbers_bounded("1e2,2E-1", 8).0, vec![100.0, 0.2]);
}
#[test]
/// The element cap stops expansion and reports truncation.
fn path_element_limit_stops_expansion_and_reports_truncation() {
let data = format!("M0 0{}", "h1".repeat(MAX_PATH_ELEMENTS + 1));
let (path, exceeded) = parse_path_data_bounded(&data, 32);
assert!(exceeded);
assert!(path.elements().len() <= 32);
}
#[test]
/// The number list cap stops retaining values.
fn number_list_limit_stops_retaining_values() {
let (values, exceeded) = numbers_bounded("1 2 3 4", 3);
assert_eq!(values, [1.0, 2.0, 3.0]);
assert!(exceeded);
}
}
+1505 -1505
View File
File diff suppressed because it is too large. Load diff
+1154 -1152
View File
File diff suppressed because it is too large. Load diff
Generated
+27 -27
View File
@@ -1,27 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1791594108,
"narHash": "sha256-XK4KceonkwD8LuMAt/vNrz982mXJM5jeHAxMwsUXXAQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6510408d147d8c5e6d76043e899e9251756f6a75",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1791594108,
"narHash": "sha256-XK4KceonkwD8LuMAt/vNrz982mXJM5jeHAxMwsUXXAQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6510408d147d8c5e6d76043e899e9251756f6a75",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+106 -106
View File
@@ -1,106 +1,106 @@
{
description = "EffectCraft — motion graphics and visual effects, a free alternative to After Effects, in pure Rust";
inputs = {
# EffectCraft needs edition 2024 and rustc ≥ 1.95, so the flake tracks unstable. Consumers on a
# release channel can retarget it: `inputs.effectcraft.inputs.nixpkgs.follows = "nixpkgs";`
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
{
self,
nixpkgs,
}:
let
inherit (nixpkgs) lib;
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
];
# The AppStream metadata carries a release date. The flake source's own commit date is
# reproducible, unlike "now"; a tarball or dirty tree without one falls back to the epoch.
buildDate =
let
d = self.lastModifiedDate or "";
in
if builtins.stringLength d >= 8 then
"${builtins.substring 0 4 d}-${builtins.substring 4 2 d}-${builtins.substring 6 2 d}"
else
"1970-01-01";
packageArgs = {
inherit buildDate;
};
forAllSystems = f: lib.genAttrs systems (system: f (import nixpkgs { inherit system; }) system);
in
{
packages = forAllSystems (
pkgs: _:
let
effectcraft = pkgs.callPackage ./nix/package.nix packageArgs;
in
{
inherit effectcraft;
default = effectcraft;
}
);
# `nixpkgs.overlays = [ inputs.effectcraft.overlays.default ]` → `pkgs.effectcraft` everywhere.
overlays.default = final: _prev: {
effectcraft = final.callPackage ./nix/package.nix packageArgs;
};
apps = forAllSystems (
_: system: {
default = {
type = "app";
program = lib.getExe self.packages.${system}.effectcraft;
};
}
);
devShells = forAllSystems (
pkgs: system:
let
effectcraft = self.packages.${system}.effectcraft;
in
{
default = pkgs.mkShell ({
# The package's own native inputs, so `cargo build` in the shell needs no extra setup.
inputsFrom = [ effectcraft ];
packages = with pkgs; [
cargo
rustc
clippy
rustfmt
rust-analyzer
];
# `cargo run` dlopens the Vulkan loader, libwayland and libxkbcommon like the packaged binary.
LD_LIBRARY_PATH = lib.makeLibraryPath effectcraft.passthru.runtimeLibs;
RUST_BACKTRACE = "1";
});
}
);
checks = forAllSystems (
pkgs: system: {
effectcraft = self.packages.${system}.effectcraft;
# `nix fmt` formats the flake; this keeps it formatted.
formatting =
pkgs.runCommand "effectcraft-nix-formatting" { nativeBuildInputs = [ pkgs.nixfmt ]; }
''
nixfmt --check ${./flake.nix} ${./nix}
touch $out
'';
}
);
formatter = forAllSystems (pkgs: _: pkgs.nixfmt);
};
}
{
description = "EffectCraft — motion graphics and visual effects, a free alternative to After Effects, in pure Rust";
inputs = {
# EffectCraft needs edition 2024 and rustc ≥ 1.95, so the flake tracks unstable. Consumers on a
# release channel can retarget it: `inputs.effectcraft.inputs.nixpkgs.follows = "nixpkgs";`
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs =
{
self,
nixpkgs,
}:
let
inherit (nixpkgs) lib;
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
];
# The AppStream metadata carries a release date. The flake source's own commit date is
# reproducible, unlike "now"; a tarball or dirty tree without one falls back to the epoch.
buildDate =
let
d = self.lastModifiedDate or "";
in
if builtins.stringLength d >= 8 then
"${builtins.substring 0 4 d}-${builtins.substring 4 2 d}-${builtins.substring 6 2 d}"
else
"1970-01-01";
packageArgs = {
inherit buildDate;
};
forAllSystems = f: lib.genAttrs systems (system: f (import nixpkgs { inherit system; }) system);
in
{
packages = forAllSystems (
pkgs: _:
let
effectcraft = pkgs.callPackage ./nix/package.nix packageArgs;
in
{
inherit effectcraft;
default = effectcraft;
}
);
# `nixpkgs.overlays = [ inputs.effectcraft.overlays.default ]` → `pkgs.effectcraft` everywhere.
overlays.default = final: _prev: {
effectcraft = final.callPackage ./nix/package.nix packageArgs;
};
apps = forAllSystems (
_: system: {
default = {
type = "app";
program = lib.getExe self.packages.${system}.effectcraft;
};
}
);
devShells = forAllSystems (
pkgs: system:
let
effectcraft = self.packages.${system}.effectcraft;
in
{
default = pkgs.mkShell ({
# The package's own native inputs, so `cargo build` in the shell needs no extra setup.
inputsFrom = [ effectcraft ];
packages = with pkgs; [
cargo
rustc
clippy
rustfmt
rust-analyzer
];
# `cargo run` dlopens the Vulkan loader, libwayland and libxkbcommon like the packaged binary.
LD_LIBRARY_PATH = lib.makeLibraryPath effectcraft.passthru.runtimeLibs;
RUST_BACKTRACE = "1";
});
}
);
checks = forAllSystems (
pkgs: system: {
effectcraft = self.packages.${system}.effectcraft;
# `nix fmt` formats the flake; this keeps it formatted.
formatting =
pkgs.runCommand "effectcraft-nix-formatting" { nativeBuildInputs = [ pkgs.nixfmt ]; }
''
nixfmt --check ${./flake.nix} ${./nix}
touch $out
'';
}
);
formatter = forAllSystems (pkgs: _: pkgs.nixfmt);
};
}
+2 -2
View File
@@ -1,2 +1,2 @@
max_width = 160
use_small_heuristics = "Max"
max_width = 160
use_small_heuristics = "Max"
+23 -23
View File
@@ -1,23 +1,23 @@
sonar.projectKey=storytold_effectcraft
sonar.organization=storytold
# crates/aep/tests/fixtures/tools/ holds the contributor's offline generators of the AEP test
# corpus (Node and ExtendScript run by hand against a local folder, see its README). They are not
# shipped or run by the app or CI, so they are not analysed.
sonar.exclusions=crates/aep/tests/fixtures/tools/**
# S1523 / S2137 (eval, Function): the expression runtime in crates/expr/src/prelude.js replaces
# `eval` and `Function` on purpose, with wrappers that check the code they are given before it
# runs (After Effects expressions may call them). The rules flag exactly that wrapping.
#
# S7773 / S8786 (Number.isFinite and friends, String.trim): the .jsx scripts are After Effects
# ExtendScript, which is ES3. They also run in After Effects, which has neither.
sonar.issue.ignore.multicriteria=prelude_eval,prelude_redefine,jsx_es3_globals,jsx_es3_trim
sonar.issue.ignore.multicriteria.prelude_eval.ruleKey=javascript:S1523
sonar.issue.ignore.multicriteria.prelude_eval.resourceKey=crates/expr/src/prelude.js
sonar.issue.ignore.multicriteria.prelude_redefine.ruleKey=javascript:S2137
sonar.issue.ignore.multicriteria.prelude_redefine.resourceKey=crates/expr/src/prelude.js
sonar.issue.ignore.multicriteria.jsx_es3_globals.ruleKey=javascript:S7773
sonar.issue.ignore.multicriteria.jsx_es3_globals.resourceKey=**/*.jsx
sonar.issue.ignore.multicriteria.jsx_es3_trim.ruleKey=javascript:S8786
sonar.issue.ignore.multicriteria.jsx_es3_trim.resourceKey=**/*.jsx
sonar.projectKey=storytold_effectcraft
sonar.organization=storytold
# crates/aep/tests/fixtures/tools/ holds the contributor's offline generators of the AEP test
# corpus (Node and ExtendScript run by hand against a local folder, see its README). They are not
# shipped or run by the app or CI, so they are not analysed.
sonar.exclusions=crates/aep/tests/fixtures/tools/**
# S1523 / S2137 (eval, Function): the expression runtime in crates/expr/src/prelude.js replaces
# `eval` and `Function` on purpose, with wrappers that check the code they are given before it
# runs (After Effects expressions may call them). The rules flag exactly that wrapping.
#
# S7773 / S8786 (Number.isFinite and friends, String.trim): the .jsx scripts are After Effects
# ExtendScript, which is ES3. They also run in After Effects, which has neither.
sonar.issue.ignore.multicriteria=prelude_eval,prelude_redefine,jsx_es3_globals,jsx_es3_trim
sonar.issue.ignore.multicriteria.prelude_eval.ruleKey=javascript:S1523
sonar.issue.ignore.multicriteria.prelude_eval.resourceKey=crates/expr/src/prelude.js
sonar.issue.ignore.multicriteria.prelude_redefine.ruleKey=javascript:S2137
sonar.issue.ignore.multicriteria.prelude_redefine.resourceKey=crates/expr/src/prelude.js
sonar.issue.ignore.multicriteria.jsx_es3_globals.ruleKey=javascript:S7773
sonar.issue.ignore.multicriteria.jsx_es3_globals.resourceKey=**/*.jsx
sonar.issue.ignore.multicriteria.jsx_es3_trim.ruleKey=javascript:S8786
sonar.issue.ignore.multicriteria.jsx_es3_trim.resourceKey=**/*.jsx