From 434258050ac1642eb19051857be23e86a56d00f6 Mon Sep 17 00:00:00 2001 From: midasdf <205497066+midasdf@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:00:20 +0900 Subject: [PATCH 1/4] Text: Chinese Han text uses the installed Chinese face (#453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS does not install the on-demand PingFang SC (macOS 27 on an English install ships Hiragino Sans GB, Heiti SC, Songti SC and STSong instead), and the Simplified Chinese fallback list named only Windows and Linux families. A Chinese character therefore fell through to the exhaustive search, which picks the first installed face that covers it — Hiragino Sans, the Japanese face — so Chinese text in a composition was drawn with Japanese glyph forms (#453). Japanese kana and kanji were already handled, and a Chinese-only character the Japanese face lacks (e.g. 页) still found a Chinese face, which is why the symptom looked selective. Add the macOS Chinese faces to the fallback lists: Hiragino Sans GB, Heiti SC, Songti SC and STSong for Simplified Chinese, Songti TC for Traditional (Heiti TC was already named). The interface language still chooses between the scripts and an English interface keeps the Japanese-first order for shared Han ideographs, as designed in #312; what changes is that a Chinese-locale request has an installed Chinese family to answer it on macOS. Tests: a text-layer render test draws Japanese and Chinese text and checks glyph coverage (skipped where no CJK face is installed); a layout test checks every CJK glyph comes from a face that has the character, so a missing-glyph box cannot creep back; and the fonts test fails without the list change ("Hiragino Sans GB missing from the Han list"), passes with it, and walks the Han branch's list to prove the first installed Simplified family is a Chinese face, not a Japanese one. Verified with CARGO_BUILD_JOBS=2 RUST_TEST_THREADS=2: effectcraft-text 64 passed; effectcraft-render 233 passed, 1 ignored; cargo clippy -p effectcraft-text -p effectcraft-render --all-targets -- -D warnings clean; cargo fmt --all -- --check clean. A 900x240 render of "骨直真具画 中文测试" with the zh-hans request now uses Hiragino Sans GB and differs from the Japanese render, which still uses Hiragino Sans. --- ROADMAP.md | 8 ++++++- crates/render/src/tests.rs | 21 ++++++++++++++++ crates/text/src/fonts.rs | 48 ++++++++++++++++++++++++++++++++++--- crates/text/src/layout.rs | 22 +++++++++++++++++ docs/gaps.md | 5 ++-- docs/localization-parity.md | 5 ++-- docs/roadmap.md | 5 ++-- docs/target-app-parity.md | 7 +++--- 8 files changed, 108 insertions(+), 13 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 1887d7b82..4b2fbfe00 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~30 points of "ready for real work" and ~350–620 h away -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (open-issue count, crash list and localization share re-audited against `upstream/main` 470ea7a3) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (Simplified Chinese Han text in compositions reaches the installed macOS Chinese faces instead of the Japanese face, with regression tests, #453; previous: minor, open-issue count, crash list and localization share re-audited against `upstream/main` 470ea7a3) · **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 @@ -117,6 +117,11 @@ Come tell us what matters most to you on [Discord](https://discord.gg/artcraft). ## Progress log +- 2026-10-22: Text: Chinese Han text in compositions now draws with the installed Chinese face + instead of the Japanese one. macOS does not install the on-demand PingFang SC, so the Simplified + Han list names Hiragino Sans GB, Heiti SC, Songti SC and STSong (Heiti TC and Songti TC for + Traditional) before the fallback search can reach a Japanese face (#453). Japanese kana and kanji + are unchanged; a text-layer render test and a macOS fallback test cover both languages. - 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 @@ -154,6 +159,7 @@ Come tell us what matters most to you on [Discord](https://discord.gg/artcraft). | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Chinese Han text in compositions uses the installed macOS Chinese faces instead of the Japanese face; CJK text-layer and macOS fallback regression tests (#453) | | 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-10 | minor | Hours to ≈ 95% for each audience | diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 72dd26507..7f09339fd 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -219,6 +219,27 @@ fn text_layer_renders_glyphs() { assert!(covered > 300, "{covered}"); } +/// #453: a composition's Japanese (and Chinese) text layer draws its glyphs, not missing-glyph +/// boxes. Skipped where no CJK face is installed. +#[test] +fn text_layer_renders_cjk_glyphs() { + let cjk = effectcraft_text::fonts::resolve("Inter", "Regular").face; + if !effectcraft_text::fonts::face(effectcraft_text::fonts::fallback_for('日', cjk)).has_char('日') { + eprintln!("no Japanese face installed; skipping"); + return; + } + for text in ["日本語", "中文"] { + let (mut p, cid, comp) = setup(); + let mut l = build::layer(&mut p, &comp, "Text", LayerSource::Text, (200, 100), None); + l.props.prop_mut("text/sourceText").unwrap().value = Value::Text(Box::new(TextDoc { text: text.into(), size: 46.0, ..Default::default() })); + l.props.prop_mut("transform/position").unwrap().value = Value::Vec3([100.0, 70.0, 0.0]); + p.comp_mut(cid).unwrap().layers.push(l); + let img = render_frame(&p, cid, Tick::ZERO, 1.0); + let covered = img.data.iter().filter(|px| px[3] > 0.5).count(); + assert!(covered > 200, "{text}: {covered} covered pixels"); + } +} + #[test] fn precomp_and_3d_render() { let (mut p, cid, comp) = setup(); diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index aeabf1872..5e057811c 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -685,14 +685,28 @@ const JAPANESE_FAMILIES: &[&str] = &[ ]; /// Han ideograph families for Simplified Chinese, in the order the platforms' own fallback tables /// prefer them (Windows, macOS, then the Linux/Noto names). Missing ones cost one name lookup each. -const SIMPLIFIED_CHINESE_FAMILIES: &[&str] = - &["Microsoft YaHei", "SimSun", "PingFang SC", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", "WenQuanYi Micro Hei"]; +/// `Hiragino Sans GB`, `Heiti SC` and `Songti SC` are the Simplified faces a macOS install without +/// the on-demand PingFang has (macOS 27 on an English install ships them, not PingFang): without +/// them Chinese Han text fell through to the Japanese face and drew Japanese glyph forms (#453). +const SIMPLIFIED_CHINESE_FAMILIES: &[&str] = &[ + "Microsoft YaHei", + "SimSun", + "PingFang SC", + "Hiragino Sans GB", + "Heiti SC", + "Songti SC", + "STSong", + "Noto Sans CJK SC", + "Noto Sans SC", + "Source Han Sans SC", + "WenQuanYi Micro Hei", +]; /// The same for Traditional Chinese. The two scripts share almost all of their code points but not /// the glyph forms (`东` against `東`'s neighbours, `直`, `骨`), so one list for both drew a /// Traditional interface with Simplified forms - the faces do not differ in *coverage*, which is /// what the fallback search checks, so the script has to pick the list. const TRADITIONAL_CHINESE_FAMILIES: &[&str] = - &["Microsoft JhengHei", "MingLiU", "PingFang TC", "Heiti TC", "Hiragino Sans CNS", "Noto Sans CJK TC", "Noto Sans TC", "Source Han Sans TC"]; + &["Microsoft JhengHei", "MingLiU", "PingFang TC", "Heiti TC", "Hiragino Sans CNS", "Songti TC", "Noto Sans CJK TC", "Noto Sans TC", "Source Han Sans TC"]; const KOREAN_FAMILIES: &[&str] = &["Malgun Gothic", "Gulim", "Apple SD Gothic Neo", "Noto Sans CJK KR", "Noto Sans KR", "Source Han Sans KR", "NanumGothic"]; /// The platforms' plain text families for the other scripts the bundled fonts lack, in the same /// order (Windows, macOS, then the Linux/Noto names). @@ -1307,6 +1321,34 @@ mod tests { assert_eq!(han_order_for("ja_JP.UTF-8")[0], FallbackScript::Japanese); } + /// macOS ships no Simplified Chinese PingFang on an English install (macOS 27 has the + /// on-demand PingFang instead): Hiragino Sans GB, Heiti SC and Songti SC are the installed + /// Chinese faces, so the Han list has to name them or Han text falls through to the Japanese + /// face and draws Japanese glyph forms (#453). The system check is skipped where none of them + /// is installed; the list itself is checked everywhere. + #[test] + fn macos_chinese_faces_are_tried_for_han_text() { + for face in ["Hiragino Sans GB", "Heiti SC", "Songti SC"] { + assert!(SIMPLIFIED_CHINESE_FAMILIES.iter().any(|l| l.eq_ignore_ascii_case(face)), "{face} missing from the Han list"); + } + assert!(TRADITIONAL_CHINESE_FAMILIES.iter().any(|l| l.eq_ignore_ascii_case("Songti TC")), "Songti TC missing from the Traditional list"); + scan_system(); + let installed: Vec<&str> = ["Hiragino Sans GB", "Heiti SC", "Songti SC"].into_iter().filter(|f| resolve_in(f, "Regular").is_some()).collect(); + if installed.is_empty() { + eprintln!("no macOS Simplified Chinese face installed; skipping the system check"); + return; + } + // Walking the Han branch's own list finds a Chinese face before the Japanese face the + // exhaustive search would have found (Hiragino Sans), and it covers the shared ideographs. + let (family, resolved) = + SIMPLIFIED_CHINESE_FAMILIES.iter().find_map(|f| resolve_in(f, "Regular").map(|r| (*f, r))).expect("an installed Simplified Chinese family"); + assert!(installed.iter().any(|i| i.eq_ignore_ascii_case(family)), "{family} is not a macOS Chinese face"); + assert!(!JAPANESE_FAMILIES.iter().any(|j| j.eq_ignore_ascii_case(family)), "{family} is a Japanese family"); + for c in ['骨', '直', '日'] { + assert!(face(resolved.face).has_char(c), "{family} cannot draw {c}"); + } + } + #[test] fn bundled_ui_fonts_cover_the_ukrainian_alphabet() { const ALPHABET: &str = "АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯабвгґдеєжзиіїйклмнопрстуфхцчшщьюя’«»"; diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 2e863d1ae..770afb965 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -1284,6 +1284,28 @@ mod tests { assert!(l.glyphs.iter().all(|g| g.id != 0)); } + /// CJK text drawn with a bundled Latin family falls back to an installed face that has the + /// glyph, so a composition's Japanese or Chinese text is not drawn as missing-glyph boxes + /// (#453). Skipped where no CJK face is installed, like the font-fallback tests. + #[test] + fn cjk_text_falls_back_to_a_face_with_the_glyph() { + let primary = fonts::resolve("Inter", "Regular").face; + if !fonts::face(fonts::fallback_for('日', primary)).has_char('日') { + eprintln!("no Japanese face installed; skipping"); + return; + } + for text in ["日本語", "中文漢字"] { + let l = layout(text, &st(40.0), &ParagraphStyle::default()); + assert_eq!(l.glyphs.len(), text.chars().count(), "{text}"); + for g in &l.glyphs { + let ch = text.get(g.cluster..).and_then(|rest| rest.chars().next()).unwrap_or('?'); + assert_ne!(g.id, 0, "{ch} has a glyph"); + let f = fonts::face(g.face); + assert!(f.has_char(ch), "{ch} drawn by {} {} which lacks it", f.info.family, f.info.style); + } + } + } + #[test] fn empty_text_has_a_caret() { let l = layout("", &st(40.0), &ParagraphStyle::default()); diff --git a/docs/gaps.md b/docs/gaps.md index fd92c22a4..7c01c96c9 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where EffectCraft falls short -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (gap 11: the Chinese Han fallback reaches the installed macOS Chinese faces, #453; previous: minor, path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) An honest assessment of how far EffectCraft is from being a real replacement for After Effects, and the work that closes the gap. This document is meant for contributors and agents choosing @@ -67,7 +67,7 @@ are grouped by area; the issue tracker has the rest. | 8 | **Mask and shape bugs** | Open: #1004 mask vertex multi-select, #999 auto-pan, #1042 / #1033 vertex nodes under the bounding box, #959 / #960 shape group toggles and operators. Now closed: #510, #513, #527, #509, #303, #553, #932 (Convert To Bezier Path on a shape layer's parametric path) | Masks are core compositing | 10–20 h | G2 · [target-app-parity.md](target-app-parity.md) | | 9 | **Effect behaviour bugs** | #580, #463 Curl Noise, #369 Motion Tile output size, #943 Turbulent Displace Evolution, #473 Cryptomatte selection (quoted names with commas fixed, 3b7f1ace; UI and GPU time open), #474 multi-layer EXR (decode shared, faffad3a; f16 storage and cache sizing open). Closed: #526, #514, #533–#536 | Each one breaks a look someone built | 10–20 h | G1/G2 · [effects-parity.md](effects-parity.md) | | 10 | **Colour pipeline differences** | #340 ProRes 4444 tagged bt709 but sRGB-encoded (open); ACES 2.0 / OCIO 2.5 configs (26.5) missing; now closed: #494 32-bpc blend modes, #495 and #750 Working Gamma 2.4, #546 / #547 viewer colour | Wrong colour in delivery is a client-visible failure | 15–30 h | G1 · [file-format-parity.md](file-format-parity.md) | -| 11 | **Text rendering and layout** | Open: #453 CJK text in comps, #316 paragraph alignment inverted (maintainers' tests show Left/Right correct; asked for a project), #417 soft at Fit (mostly fixed by #444), #550 toolbar font options. Closed: #483, #583 | Text is in almost every motion graphic | 10–20 h | G2 · [ui-parity.md](ui-parity.md) | +| 11 | **Text rendering and layout** | #453 CJK text in comps: the Han fallback now reaches the installed Chinese faces; open: #316 paragraph alignment inverted (maintainers' tests show Left/Right correct; asked for a project), #417 soft at Fit (mostly fixed by #444), #550 toolbar font options. Closed: #483, #583 | Text is in almost every motion graphic | 10–20 h | G2 · [ui-parity.md](ui-parity.md) | | 12 | **3D interaction and depth** | No 3D transform gizmos (#392, #347); camera aperture (#260); reflections (#261); extruded surface materials beyond Front / Bevel / Side / Back Color (fb5a5e9a; #315 closed); #307 reopened (the viewer's renderer button now shows and switches the renderer, #645); Z position (#489) closed | Advanced 3D is After Effects' growth area since 2023 | 20–35 h | G2 · [ui-parity.md](ui-parity.md) | | 13 | **3D model formats and materials** | After Effects 26 imports FBX, USD/USDZ, STL besides glTF and OBJ (`usd_plugins`), and Substance 3D materials with mesh displacement (26.0–26.2); we read glTF, OBJ, STL (binary and ASCII, with facet colours) and FBX 7.x (binary and ASCII: meshes, hierarchy, Lambert/Phong materials and textures; no FBX animation or skinning yet) | Product shots and 3D scenes come as FBX/USDZ | 25–45 h | — · [file-format-parity.md](file-format-parity.md) | | 14 | **Object Matte** (26.2, disk cache in 26.5) | First slice (#783): the Object Matte effect (Effect ▸ Matte) and `objectMatte.apply` turn a point (and optional box) on a reference frame into a matte and follow the object up to 600 frames each way, or segment every frame on its own; Roto Brush's engine, cache and model (MobileSAM from Settings ▸ Roto Brush, classical graph cut without it). Since then: the Object Matte tool in the viewer (click / drag a box, hover crosshair), the box as the model's box prompt, background tracking with progress and cancel, Freeze, and segmentations kept in the disk cache (26.5). Missing: a comparison with After Effects ([plan](fidelity/object-matte.md)) | The new one-click roto | 10–20 h | G7 · [target-app-parity.md](target-app-parity.md) | @@ -335,6 +335,7 @@ Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the vie | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Gap 11: the Chinese Han fallback reaches the installed macOS Chinese faces, so Chinese text in compositions no longer draws Japanese glyph forms (#453) | | 2026-10-16 | minor | Gap 8: #932 closed (Convert To Bezier Path converts a shape layer's parametric path, not only mask RotoBezier) | | 2026-10-13 | patch | G2: layer bounds no longer compete with editable shape/mask path points (#1220) | | 2026-10-12 | minor | Audited against `upstream/main` 470ea7a3: open-issue count and crash list, closed issues removed from rows 1, 3, 5, 6, 8–12, 15–17, 22–24, G8 and G9 (#759 `#include`), localization catalogs recounted (≈ 12% to ≈ 18%); rows 7, 14 and 21 left to open PRs #1102, #792 and #1098 | diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 5606a857d..b99ad5884 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (catalogs recounted at `upstream/main` 470ea7a3: menus in 10 languages, panels in 6, Hangul probe (#599)) · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (the Chinese Han fallback reaches the installed Chinese faces, so Chinese text in compositions draws Chinese glyph forms, #453; previous: minor, catalogs recounted at `upstream/main` 470ea7a3: menus in 10 languages, panels in 6, Hangul probe (#599)) · **Target:** Adobe After Effects 2026 How much of EffectCraft's interface is translated, language by language, compared with After Effects. [gaps.md](gaps.md) gap 16 and workstream G9 hold the work; @@ -64,7 +64,7 @@ the true percentages are slightly lower. | Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | To `full` | |---|---|---|---|---|---|---|---| | English | `en` | ≈ 3,900 (100%, source) | All; docs in English | — | yes | full | — | -| Simplified Chinese | `zh-hans` | 1,130 (≈ 29%): menus 624 rows, panels 506 | Panels and dialogs partly; no help, effect names translated (306) and their parameter names not; Home screen literals unwrapped (#574, closed) | System CJK fallback; Han follows the UI language; CJK text in comps renders wrongly (#453); IME in text layers | no | partial | 6–9 h + review | +| Simplified Chinese | `zh-hans` | 1,130 (≈ 29%): menus 624 rows, panels 506 | Panels and dialogs partly; no help, effect names translated (306) and their parameter names not; Home screen literals unwrapped (#574, closed) | System CJK fallback; Han follows the UI language and now reaches the installed Chinese faces (#453); IME in text layers | no | partial | 6–9 h + review | | Spanish | `es` | 624 (≈ 16%) | Menus only (panel catalog in open PR #996) | Latin, fine | no | menus only | 7–10 h + review | | Hindi | `hi` | 0 | — | UI: Devanagari fallback font but no shaping; text layers shape Devanagari | no | none (After Effects: none) | 9–13 h + shaping in the UI | | Arabic | `ar` | 0 | — | UI: Arabic fallback font but no shaping and no RTL layout; text layers: bidi + shaping | no | none (After Effects: none) | 9–13 h + 15–25 h RTL UI | @@ -103,6 +103,7 @@ can give; none has had it. | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Chinese text in compositions follows the fixed Han fallback: installed Chinese faces are tried before the Japanese face (#453) | | 2026-10-12 | minor | Recounted: Russian, French, German and Czech menus (#873, #987), Brazilian Portuguese and Czech panels (#830, #987), Hangul probe (#599); overall ≈ 12% to ≈ 18%; Korean and Italian still absent | | 2026-10-10 | major | First localization parity document: catalog rows counted for all six languages, the twelve-language table, After Effects' 11 shipped languages from its bundle | | 2026-10-11 | minor | Ukrainian panel catalog brought level with the Chinese ones (495 of 496 rows, counted at this commit); its share rises to ≈ 29%. The other languages' panel counts are the 10 October measurement | diff --git a/docs/roadmap.md b/docs/roadmap.md index 73fdfd235..e2692fd8b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap detail -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (new file: milestones moved from ROADMAP.md, current focus and next steps with Opus 5.5 estimates) · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (the Chinese Han fallback reaches the installed Chinese faces, #453; previous: major, new file: milestones moved from ROADMAP.md, current focus and next steps with Opus 5.5 estimates) · **Target:** Adobe After Effects 2026 Forward-looking detail behind the [ROADMAP](../ROADMAP.md). The work list itself, one gap at a time, is [gaps.md](gaps.md); the numbers are in [target-app-parity.md](target-app-parity.md). @@ -31,7 +31,7 @@ none blocks the workflow. |---|---|---|---| | 1. Set up: new comp, import footage, stills, PSD/AI and audio, arrange and trim layers | yes | `file.import` and image-sequence tests, `ui_drag_drop`, `ui_project_*`; rough: a still imports as a sequence (#431), an MP3 layer can go silent (#324), multi-file drops (#552) | 2–4 | | 2. Animate: keyframes, easing in the Graph Editor, motion paths, parenting | yes | 168 live samples match After Effects 26.3 ([fidelity/](fidelity/README.md)); `ui_keyframes`; rough: Graph Editor jitter and clipped buttons (#456, #457), negative rotation drag (#569) | 3–6 | -| 3. Motion graphics: text layers with animators, shape layers with operators | yes | `ui_text_edit`, `ui_timeline_shapes`, text and shape engine tests; rough: paragraph alignment inverted (#316), CJK text in comps (#453) | 3–6 | +| 3. Motion graphics: text layers with animators, shape layers with operators | yes | `ui_text_edit`, `ui_timeline_shapes`, text and shape engine tests; rough: paragraph alignment inverted (#316), Chinese text in comps follows the Han fallback (#453 fixed) | 3–6 | | 4. Composite: masks, track mattes, blend modes, keying and effects | yes | Compositor and mask tests, 306 effects with CPU/GPU agreement; rough: mask bugs (#510, #513, #527), 32-bpc blend differences (#494), effect bugs (#580 crash, #514) | 6–12 | | 5. Preview with audio (RAM preview, cache) | yes | `ui_ram_preview`, `ui_live_preview`; rough: stutter on cached frames (#595, #304), late audio (#554, #504) | 10–20 | | 6. Save, reopen and deliver: `.ecproj`, auto-save, Render Queue to H.264 / ProRes / sequences | yes | Versioned-schema reopen tests, crash recovery, render-queue and encoder tests; rough: the macOS default output folder is `/`, so a render fails until an output path is chosen (#446), ProRes 4444 colour tag (#340), projects don't relink when moved | 1–3 | @@ -83,5 +83,6 @@ To beta (≈ 75% ready and `.aep` opening): **≈ 350–620 h**. To full parity: | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | The Chinese Han fallback reaches the installed Chinese faces (#453) | | 2026-10-10 | minor | Alpha gate: six core workflows checked; all pass, stage stays alpha | | 2026-10-10 | major | New file: milestones from ROADMAP.md, current focus, next steps with estimates | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 3ebaa4524..a3ddc3bfc 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with After Effects -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (issue, catalog and export-format references re-audited against `upstream/main` 470ea7a3; the 10 October re-measure is otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (the Chinese Han fallback reaches the installed macOS Chinese faces, so Chinese text in compositions no longer draws Japanese glyph forms, #453; previous: minor, issue, catalog and export-format references re-audited against `upstream/main` 470ea7a3; the 10 October re-measure is otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) The authoritative assessment of how close EffectCraft is to After Effects, and how much work is left. [gaps.md](gaps.md) lists every known shortfall one at a time; the @@ -119,7 +119,7 @@ compositing and effects dominate; audio is light). Hours are to full parity for | Animation: keyframes, graph editor, motion paths, puppet | 15% | 97% | 55% | 35–60 | Only scalar ease is measured against After Effects (168 samples); puppet lag (#586); #456, #457, #355, #553 and #318 are closed; Proportional Scrubbing (26.2) | | Expressions and scripting | 7% | 93% | 65% | 20–40 | `.jsxbin`, Startup/Shutdown folders, script shortcuts (`#include` landed, #759); external editor (#364); see [scripting-parity.md](scripting-parity.md) | | Shapes, masks and roto | 9% | 93% | 45% | 30–50 | Open mask and shape issues (#1004, #999, #1042, #932); #510, #513, #527, #509 and #303 are closed; paste SVG/AI as shape layers (26.3; SVG markup pastes since #738) | -| Text and typography | 7% | 94% | 50% | 20–35 | CJK text in comps renders wrongly (#453); paragraph alignment inverted (#316); soft text at Fit (#417, mostly fixed by #444); toolbar font options (#550); #483 and #583 are closed; variable-font filter landed (#741) | +| Text and typography | 7% | 94% | 50% | 20–35 | Chinese text in comps now reaches the installed Chinese faces in the Han fallback (#453); paragraph alignment inverted (#316); soft text at Fit (#417, mostly fixed by #444); toolbar font options (#550); #483 and #583 are closed; variable-font filter landed (#741) | | Effects and animation presets | 14% | 85% | 40% | 70–130 | All 306 effects exist, but rendered output is unmeasured against After Effects; 54 built-in presets (text, Transitions, Backgrounds) against After Effects' 621 ([effects-parity.md](effects-parity.md)); open effect bugs (#580 crash, #463, #369, #473, #474; #514 and #526 are closed); effect colour labels (26.5); see [effects-parity.md](effects-parity.md) | | 3D (Classic and Advanced) | 6% | 82% | 35% | 35–65 | No FBX/USD/STL models, no Substance materials, mesh displacement unverified; no 3D transform gizmos (#392, #347); reflections (#261); camera aperture (#260); #489 and #315 are closed | | Tracking and AI-assisted tools | 5% | 80% | 35% | 35–70 | Object Matte (26.2) has the effect, the viewer tool (click / box), background tracking, Freeze and the 26.5 disk cache, not yet compared with After Effects; Roto Brush (MobileSAM) and face tracking (MediaPipe) unmeasured against After Effects; camera-tracker view (#505) | @@ -193,7 +193,7 @@ generative/cloud AI (Object Matte, Firefly), Adobe services and team features, s | Keyframes, Graph Editor, easing, motion paths | 20% | 75% | 168 live samples match After Effects; #456/#457, #553 and #569 were counted here and are now closed | | Expressions: `wiggle`, `loopOut`, linking, sliders | 6% | 80% | `crates/expr` tests; no live expression oracle yet | | Shape layers and masks | 12% | 65% | #510, #513, #527 mask bugs and #303 Mesh Warp were counted here and are now closed; open: #1004, #999, #1042 | -| Text and text animators | 12% | 70% | #316 alignment inverted, #453 CJK (open); #583 font search (closed) | +| Text and text animators | 12% | 70% | #316 alignment inverted; #453 CJK fallback fixed; #583 font search (closed) | | Everyday effects: blurs, glow, fill/tint, curves/levels, keying, drop shadow, displacement | 10% | 70% | All present with GPU paths; unmeasured against After Effects; #514 Apply Color LUT (closed) | | Preview (RAM preview with audio) | 8% | 55% | #595, #304 stutter; #504 late audio (#554 closed) | | Render to H.264 / ProRes / PNG sequences | 6% | 75% | Encoder tests; #340 ProRes 4444 tag (open); #446 default macOS output folder (closed) | @@ -1166,6 +1166,7 @@ top of this page and in items 16–18 above. | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Chinese Han text in compositions uses the installed macOS Chinese faces rather than the Japanese face (#453) | | 2026-10-12 | minor | Audited against `upstream/main` 470ea7a3: open crash list, closed-issue references in the area tables, export formats 12 to 13, localization ≈ 12% to ≈ 18%, `#include`, built-in preset count, 12 October repository counts | | 2026-10-10 | minor | Readiness-by-audience table: hours to ≈ 95% for each of the three numbers; the full number stays an additive weighted sum (unchanged) | | 2026-10-10 | minor | Added mainstream practitioner (≈ 47%) and essentials user (≈ 52%) numbers with weights, discounts and GitHub user evidence; wrote down how the full ready number is built (unchanged at ≈ 45%) | From 2d360d9a83d5ca01bd655ed26d36945b49816a8a Mon Sep 17 00:00:00 2001 From: midasdf Date: Thu, 22 Oct 2026 23:08:39 +0900 Subject: [PATCH 2/4] Text: the Han fallback test accepts an earlier Chinese family (#2039) The macOS fallback test selected the first installed family from the Simplified list, so a Mac that also has PingFang SC (or Microsoft YaHei / Noto Sans CJK SC) picked that one and then failed an assertion that only accepted the three macOS faces. Accept any family from the list -- every one is Chinese -- and check that each installed macOS face covers the shared ideographs instead. Also exercise fallback_for itself under a zh-CN request, which the direct resolve_in lookup bypassed, and serialise the process-global locale with the layout CJK test so a cached family order cannot leak between them. Tests: effectcraft-text 75 lib + 3 integration passed; effectcraft-render 233 passed, 1 ignored; cargo fmt --all -- --check and cargo clippy -p effectcraft-text --all-targets -- -D warnings clean. --- crates/text/src/fonts.rs | 57 ++++++++++++++++++++++++++++++++++++++- crates/text/src/layout.rs | 2 ++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index 5e057811c..c5c5cd58d 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -744,6 +744,12 @@ const THAI_FAMILIES: &[&str] = &["Leelawadee UI", "Tahoma", "Thonburi", "Noto Sa /// The CJK locale the host asked fallbacks to prefer, when it has one (see [`set_cjk_locale`]). static REQUESTED_CJK_LOCALE: RwLock> = RwLock::new(None); +/// Serialises the tests that change the process-global CJK locale with the tests that read it +/// through [`fallback_for`] (the layout tests), which would otherwise cache the other order's +/// families. +#[cfg(test)] +pub(crate) static TEST_CJK_LOCALE: std::sync::Mutex<()> = std::sync::Mutex::new(()); + /// Ask CJK font fallbacks to prefer `tag`'s language for Han ideographs instead of the locale /// environment, so a Chinese interface is drawn with a Chinese face on a machine whose locale is /// Japanese (and the other way round). The interface sets this from its language setting; an empty @@ -989,6 +995,15 @@ fn system_fallback(c: char, style: &str) -> Option { mod tests { use super::*; + /// Clears the requested CJK locale when a test that set it ends, panic or not. + struct LocaleReset; + + impl Drop for LocaleReset { + fn drop(&mut self) { + set_cjk_locale(""); + } + } + #[test] fn extra_font_dirs_come_from_a_path_list() { assert!(extra_dirs(None).is_empty()); @@ -1164,6 +1179,7 @@ mod tests { /// loaded a CJK font first. Skipped where no family from the list is installed. #[test] fn cjk_falls_back_to_a_system_family() { + let _locale = TEST_CJK_LOCALE.lock().unwrap_or_else(|e| e.into_inner()); scan_system(); let inter = resolve("Inter", "Bold").face; let installed = JAPANESE_FAMILIES.iter().any(|f| resolve_in(f, "Regular").is_some()); @@ -1284,6 +1300,7 @@ mod tests { #[test] fn the_han_order_follows_the_cjk_locale() { + let _locale = TEST_CJK_LOCALE.lock().unwrap_or_else(|e| e.into_inner()); // Chinese first for any Chinese tag, whether or not it names a script. for tag in ["zh", "zh-CN", "zh_Hans", "ZH-Hant-TW.UTF-8", "zh_MO"] { assert_eq!(han_order_for(tag)[0], FallbackScript::Han, "{tag}"); @@ -1301,6 +1318,7 @@ mod tests { #[test] fn the_chinese_family_list_follows_the_script_the_locale_asks_for() { + let _locale = TEST_CJK_LOCALE.lock().unwrap_or_else(|e| e.into_inner()); for tag in ["zh", "zh-CN", "zh_Hans", "zh_CN.UTF-8", "zh-SG"] { assert_eq!(chinese_families_for(tag), SIMPLIFIED_CHINESE_FAMILIES, "{tag}"); } @@ -1342,11 +1360,48 @@ mod tests { // exhaustive search would have found (Hiragino Sans), and it covers the shared ideographs. let (family, resolved) = SIMPLIFIED_CHINESE_FAMILIES.iter().find_map(|f| resolve_in(f, "Regular").map(|r| (*f, r))).expect("an installed Simplified Chinese family"); - assert!(installed.iter().any(|i| i.eq_ignore_ascii_case(family)), "{family} is not a macOS Chinese face"); + // An earlier name can win the walk when this Mac has it as well (`PingFang SC` on an older + // install, or Microsoft YaHei / Noto Sans CJK SC through Office or Homebrew). That is just + // as valid a Chinese fallback as the macOS trio; only the Japanese face would be a + // regression. Each installed macOS face is on the list (checked above) and covers the + // shared ideographs. assert!(!JAPANESE_FAMILIES.iter().any(|j| j.eq_ignore_ascii_case(family)), "{family} is a Japanese family"); for c in ['骨', '直', '日'] { assert!(face(resolved.face).has_char(c), "{family} cannot draw {c}"); } + for name in installed { + let resolved = resolve_in(name, "Regular").expect("installed above"); + for c in ['骨', '直', '日'] { + assert!(face(resolved.face).has_char(c), "{name} cannot draw {c}"); + } + } + } + + /// #453 under a Chinese interface: Han text in a Latin-only family falls back through the Han + /// list, in the locale's order, to an installed Simplified Chinese family; it must not reach the + /// Japanese face the exhaustive search would pick. The direct family lookup above would not + /// catch a `fallback_for` that skipped `han_order`. Skipped where no Simplified family is + /// installed. + #[test] + fn chinese_han_text_falls_back_to_a_simplified_chinese_family() { + let _locale = TEST_CJK_LOCALE.lock().unwrap_or_else(|e| e.into_inner()); + scan_system(); + if !SIMPLIFIED_CHINESE_FAMILIES.iter().any(|f| resolve_in(f, "Regular").is_some()) { + eprintln!("no Simplified Chinese family installed; skipping"); + return; + } + set_cjk_locale("zh-CN"); + let _reset = LocaleReset; + let inter = resolve("Inter", "Regular").face; + for c in ['骨', '直', '日', '真'] { + let f = face(fallback_for(c, inter)); + assert!(f.has_char(c), "{c}: {} {} lacks it", f.info.family, f.info.style); + assert!( + SIMPLIFIED_CHINESE_FAMILIES.iter().any(|n| n.eq_ignore_ascii_case(&f.info.family)), + "{c} → {} is not a Simplified Chinese family", + f.info.family + ); + } } #[test] diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 770afb965..47d786241 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -1289,6 +1289,8 @@ mod tests { /// (#453). Skipped where no CJK face is installed, like the font-fallback tests. #[test] fn cjk_text_falls_back_to_a_face_with_the_glyph() { + // The CJK locale is process-global; keep a font test from changing it while this lays out. + let _locale = crate::fonts::TEST_CJK_LOCALE.lock().unwrap_or_else(|e| e.into_inner()); let primary = fonts::resolve("Inter", "Regular").face; if !fonts::face(fonts::fallback_for('日', primary)).has_char('日') { eprintln!("no Japanese face installed; skipping"); From 636f1cf73858c7005ad4eff4404e85931e993193 Mon Sep 17 00:00:00 2001 From: midasdf Date: Fri, 23 Oct 2026 01:37:13 +0900 Subject: [PATCH 3/4] Text: the Japanese fallback test pins ja-JP (#2039) The test checked that Japanese text falls back to an installed Japanese family, but left the process-global CJK locale at the host's: on a zh-CN host with a Chinese face installed, han_order puts the Chinese families first for the shared ideographs and the Japanese-family assertion fails. Set ja-JP under the TEST_CJK_LOCALE lock and clear the request when the test ends, as the zh-CN test beside it does. Tests: effectcraft-text 75 lib + 3 integration passed; cargo fmt --all -- --check passed. --- crates/text/src/fonts.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index c5c5cd58d..e41d28098 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -1179,7 +1179,11 @@ mod tests { /// loaded a CJK font first. Skipped where no family from the list is installed. #[test] fn cjk_falls_back_to_a_system_family() { + // Japanese text must reach the Japanese list even on a Chinese-language host, where the + // ambient locale puts the Chinese families first for the shared ideographs. let _locale = TEST_CJK_LOCALE.lock().unwrap_or_else(|e| e.into_inner()); + set_cjk_locale("ja-JP"); + let _reset = LocaleReset; scan_system(); let inter = resolve("Inter", "Bold").face; let installed = JAPANESE_FAMILIES.iter().any(|f| resolve_in(f, "Regular").is_some()); From cbd67baf36eceb0b6818adceff3ea1d30ec78770 Mon Sep 17 00:00:00 2001 From: midasdf Date: Thu, 29 Oct 2026 14:47:22 +0900 Subject: [PATCH 4/4] Text: the zh-hans Han order test replaces the skipping render test (#2039) The CJK render test skipped wherever no Japanese face was installed, which is every CI runner, and on a Mac it drew glyphs with or without the fix, so it caught nothing. Replace it with a fallback-order test: fixture faces for `Hiragino Sans` and `Hiragino Sans GB` show that a zh-hans Han lookup tries the Chinese family first, that ja tries the Japanese one, and that a layout gets Hiragino Sans GB. Synthetic fonts in an isolated child process keep it independent of the host's fonts; with the Simplified list reverted it fails on `[7]` against `[8, 7]`. Delete `TEST_CJK_LOCALE`: the locale-mutating tests moved to an isolated child process, so nothing used the lock and the test-only static failed clippy's `-D warnings` gate. Trim the documentation edits to the single docs/gaps.md gap-11 row: revert the header and changelog edits in ROADMAP.md, docs/roadmap.md, docs/localization-parity.md and docs/target-app-parity.md, and the gaps.md header and changelog row, which conflict with other PRs. Tests: cargo clippy -p effectcraft-text -p effectcraft-render --all-targets -- -D warnings, cargo clippy --workspace --all-targets -- -D warnings, cargo test -p effectcraft-text (79 lib), cargo test -p effectcraft-render (264 passed, 1 ignored perf test), cargo fmt --all -- --check. --- ROADMAP.md | 8 +---- crates/render/src/tests.rs | 21 ------------ crates/text/src/fonts.rs | 6 ---- crates/text/src/fonts/tests.rs | 62 ++++++++++++++++++++++++++++++---- docs/gaps.md | 3 +- docs/localization-parity.md | 5 ++- docs/roadmap.md | 5 ++- docs/target-app-parity.md | 7 ++-- 8 files changed, 65 insertions(+), 52 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8e058e43f..f4c99d61f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~30 points of "ready for real work" and ~350–620 h away -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (Simplified Chinese Han text in compositions reaches the installed macOS Chinese faces instead of the Japanese face, with regression tests, #453; previous: minor, open-issue count, crash list and localization share re-audited against `upstream/main` 470ea7a3) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (open-issue count, crash list and localization share re-audited against `upstream/main` 470ea7a3) · **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 @@ -117,11 +117,6 @@ Come tell us what matters most to you on [Discord](https://discord.gg/artcraft). ## Progress log -- 2026-10-22: Text: Chinese Han text in compositions now draws with the installed Chinese face - instead of the Japanese one. macOS does not install the on-demand PingFang SC, so the Simplified - Han list names Hiragino Sans GB, Heiti SC, Songti SC and STSong (Heiti TC and Songti TC for - Traditional) before the fallback search can reach a Japanese face (#453). Japanese kana and kanji - are unchanged; a text-layer render test and a macOS fallback test cover both languages. - 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 @@ -160,7 +155,6 @@ Come tell us what matters most to you on [Discord](https://discord.gg/artcraft). | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | Chinese Han text in compositions uses the installed macOS Chinese faces instead of the Japanese face; CJK text-layer and macOS fallback regression tests (#453) | | 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-10 | minor | Hours to ≈ 95% for each audience | diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 726c39d39..079274499 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -237,27 +237,6 @@ fn text_layer_renders_glyphs() { assert!(covered > 300, "{covered}"); } -/// #453: a composition's Japanese (and Chinese) text layer draws its glyphs, not missing-glyph -/// boxes. Skipped where no CJK face is installed. -#[test] -fn text_layer_renders_cjk_glyphs() { - let cjk = effectcraft_text::fonts::resolve("Inter", "Regular").face; - if !effectcraft_text::fonts::face(effectcraft_text::fonts::fallback_for('日', cjk)).has_char('日') { - eprintln!("no Japanese face installed; skipping"); - return; - } - for text in ["日本語", "中文"] { - let (mut p, cid, comp) = setup(); - let mut l = build::layer(&mut p, &comp, "Text", LayerSource::Text, (200, 100), None); - l.props.prop_mut("text/sourceText").unwrap().value = Value::Text(Box::new(TextDoc { text: text.into(), size: 46.0, ..Default::default() })); - l.props.prop_mut("transform/position").unwrap().value = Value::Vec3([100.0, 70.0, 0.0]); - p.comp_mut(cid).unwrap().layers.push(l); - let img = render_frame(&p, cid, Tick::ZERO, 1.0); - let covered = img.data.iter().filter(|px| px[3] > 0.5).count(); - assert!(covered > 200, "{text}: {covered} covered pixels"); - } -} - #[test] fn precomp_and_3d_render() { let (mut p, cid, comp) = setup(); diff --git a/crates/text/src/fonts.rs b/crates/text/src/fonts.rs index b9c4a9084..a31a17124 100644 --- a/crates/text/src/fonts.rs +++ b/crates/text/src/fonts.rs @@ -767,12 +767,6 @@ const GEORGIAN_FAMILIES: &[&str] = &["Sylfaen", ".SF Georgian", "SF Georgian", " /// The CJK locale the host asked fallbacks to prefer, when it has one (see [`set_cjk_locale`]). static REQUESTED_CJK_LOCALE: RwLock> = RwLock::new(None); -/// Serialises the tests that change the process-global CJK locale with the tests that read it -/// through [`fallback_for`] (the layout tests), which would otherwise cache the other order's -/// families. -#[cfg(test)] -pub(crate) static TEST_CJK_LOCALE: std::sync::Mutex<()> = std::sync::Mutex::new(()); - /// Ask CJK font fallbacks to prefer `tag`'s language for Han ideographs instead of the locale /// environment, so a Chinese interface is drawn with a Chinese face on a machine whose locale is /// Japanese (and the other way round). The interface sets this from its language setting; an empty diff --git a/crates/text/src/fonts/tests.rs b/crates/text/src/fonts/tests.rs index 079a001e0..e94cf7e63 100644 --- a/crates/text/src/fonts/tests.rs +++ b/crates/text/src/fonts/tests.rs @@ -1,5 +1,14 @@ use super::*; +/// Run `test` again in a child process with `variable` set. The fixture faces below go into the +/// process-global font database and its fallback caches, so only a process that runs one such test +/// on its own can use them. +#[cfg(not(target_arch = "wasm32"))] +fn run_in_child(variable: &str, test: &str) { + let output = std::process::Command::new(std::env::current_exe().unwrap()).args(["--exact", test, "--nocapture"]).env(variable, "1").output().unwrap(); + assert!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); +} + /// Concurrent language requests must neither choose another context's font nor reuse its /// cached family order. Synthetic fonts in a child process keep this independent of the host /// font installation and of other tests that exercise the process-global database/locale. @@ -8,12 +17,7 @@ use super::*; fn cjk_requests_keep_their_language_and_cache_entries() { const CHILD: &str = "EFFECTCRAFT_TEST_ISOLATED_CJK"; if std::env::var_os(CHILD).is_none() { - let output = std::process::Command::new(std::env::current_exe().unwrap()) - .args(["--exact", "fonts::tests::cjk_requests_keep_their_language_and_cache_entries", "--nocapture"]) - .env(CHILD, "1") - .output() - .unwrap(); - assert!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + run_in_child(CHILD, "fonts::tests::cjk_requests_keep_their_language_and_cache_entries"); return; } let families = [("ja", "Yu Gothic"), ("zh-hans", "Microsoft YaHei"), ("zh-hant", "Microsoft JhengHei"), ("ko", "Malgun Gothic")]; @@ -71,6 +75,52 @@ fn cjk_requests_keep_their_language_and_cache_entries() { } } +/// #453: with a Chinese interface, a Han ideograph that both a Chinese and a Japanese face cover +/// must reach the installed Simplified Chinese family first, or Chinese text in a composition is +/// drawn with Japanese glyph forms. Fixture faces named after the macOS families keep this +/// independent of the host's fonts and of rendering. Before the fix the Simplified list named none +/// of the macOS faces a stock install has, so the walk below reached the Japanese face - the +/// regression this guards. +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn zh_hans_han_fallback_puts_the_chinese_family_first() { + const CHILD: &str = "EFFECTCRAFT_TEST_ZH_HANS_ORDER"; + if std::env::var_os(CHILD).is_none() { + run_in_child(CHILD, "fonts::tests::zh_hans_han_fallback_puts_the_chinese_family_first"); + return; + } + let mut ids = Vec::new(); + { + let mut d = db().write().unwrap(); + d.scanned = true; // This test process uses only bundled and synthetic fonts. + // The Japanese face is added first, so scan order cannot be what picks the Chinese one. + for family in ["Hiragino Sans", "Hiragino Sans GB"] { + let mut data = test_font(family); + let table = data[12..12 + 8 * 16].as_chunks::<16>().0.iter().find(|t| &t[..4] == b"cmap").unwrap(); + let cmap = u32::from_be_bytes(table[8..12].try_into().unwrap()) as usize + 12; + // Map 页 to the fixture's triangle glyph instead of its private-use range. + for (offset, value) in [(16, '页' as u16), (24, '页' as u16), (30, 2u16.wrapping_sub('页' as u16))] { + data[cmap + offset..cmap + offset + 2].copy_from_slice(&value.to_be_bytes()); + } + let data = Arc::new(data); + let names = read_faces_bytes(&data).into_iter().next().unwrap(); + ids.push(push(&mut d, info_from(names, FaceData::Shared(data), "test"))); + } + } + let &[japanese, chinese] = ids.as_slice() else { panic!("two fixture faces expected: {ids:?}") }; + let base = resolve("Inter", "Regular").face; + assert!(face(japanese).has_char('页') && face(chinese).has_char('页')); + // The fallback list itself, before any coverage check: the Chinese family comes first for + // zh-hans and second for ja, whether or not either face can draw the probe. + let zh_hans: Vec = family_faces(FallbackScript::Han, &face(base), CjkLocale::Simplified).to_vec(); + assert_eq!(zh_hans, [chinese, japanese], "zh-hans tries the Chinese family first"); + let ja: Vec = family_faces(FallbackScript::Han, &face(base), CjkLocale::Japanese).to_vec(); + assert_eq!(ja, [japanese, chinese], "ja keeps the Japanese family first"); + // ...and the face a layout actually gets for the shared ideograph. + assert_eq!(face(fallback_for_locale('页', base, "zh-hans")).info.family, "Hiragino Sans GB"); + assert_eq!(face(fallback_for_locale('页', base, "ja")).info.family, "Hiragino Sans"); +} + #[test] fn extra_font_dirs_come_from_a_path_list() { assert!(extra_dirs(None).is_empty()); diff --git a/docs/gaps.md b/docs/gaps.md index b0a6b6b1f..beb9c2880 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where EffectCraft falls short -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (gap 11: the Chinese Han fallback reaches the installed macOS Chinese faces, #453; previous: minor, path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (path-editing layer bounds, #1220; issue, PR and catalog references gap 21: percentage guides and effect colour labels in; earlier change: re-audited against `upstream/main` 470ea7a3; ranked gap list from the 10 October re-measure otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) An honest assessment of how far EffectCraft is from being a real replacement for After Effects, and the work that closes the gap. This document is meant for contributors and agents choosing @@ -335,7 +335,6 @@ Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the vie | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | Gap 11: the Chinese Han fallback reaches the installed macOS Chinese faces, so Chinese text in compositions no longer draws Japanese glyph forms (#453) | | 2026-10-16 | minor | Gap 8: #932 closed (Convert To Bezier Path converts a shape layer's parametric path, not only mask RotoBezier) | | 2026-10-13 | patch | G2: layer bounds no longer compete with editable shape/mask path points (#1220) | | 2026-10-12 | minor | Audited against `upstream/main` 470ea7a3: open-issue count and crash list, closed issues removed from rows 1, 3, 5, 6, 8–12, 15–17, 22–24, G8 and G9 (#759 `#include`), localization catalogs recounted (≈ 12% to ≈ 18%); rows 7, 14 and 21 left to open PRs #1102, #792 and #1098 | diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 651e311bb..d642af43c 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-23 · **Change:** minor (Georgian: menus, panels and effect names; missing strings show in English; `missing_translations_report` lists them; previous: minor, the Chinese Han fallback reaches the installed Chinese faces, so Chinese text in compositions draws Chinese glyph forms, #453; previous: minor, catalogs recounted at `upstream/main` 470ea7a3: menus in 10 languages, panels in 6, Hangul probe (#599)) · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-23 · **Change:** minor (Georgian: menus, panels and effect names; missing strings show in English; `missing_translations_report` lists them) · **Target:** Adobe After Effects 2026 How much of EffectCraft's interface is translated, language by language, compared with After Effects. [gaps.md](gaps.md) gap 16 and workstream G9 hold the work; @@ -71,7 +71,7 @@ the true percentages are slightly lower. | Language | Code | UI strings translated | Dialogs / tooltips / help | Script support | Native review | Status | To `full` | |---|---|---|---|---|---|---|---| | English | `en` | ≈ 3,900 (100%, source) | All; docs in English | — | yes | full | — | -| Simplified Chinese | `zh-hans` | 1,130 (≈ 29%): menus 624 rows, panels 506 | Panels and dialogs partly; no help, effect names translated (306) and their parameter names not; Home screen literals unwrapped (#574, closed) | System CJK fallback; Han follows the UI language and now reaches the installed Chinese faces (#453); IME in text layers | no | partial | 6–9 h + review | +| Simplified Chinese | `zh-hans` | 1,130 (≈ 29%): menus 624 rows, panels 506 | Panels and dialogs partly; no help, effect names translated (306) and their parameter names not; Home screen literals unwrapped (#574, closed) | System CJK fallback; Han follows the UI language; CJK text in comps renders wrongly (#453); IME in text layers | no | partial | 6–9 h + review | | Spanish | `es` | 624 (≈ 16%) | Menus only (panel catalog in open PR #996) | Latin, fine | no | menus only | 7–10 h + review | | Hindi | `hi` | 0 | — | UI: Devanagari fallback font but no shaping; text layers shape Devanagari | no | none (After Effects: none) | 9–13 h + shaping in the UI | | Arabic | `ar` | 0 | — | UI: Arabic fallback font but no shaping and no RTL layout; text layers: bidi + shaping | no | none (After Effects: none) | 9–13 h + 15–25 h RTL UI | @@ -111,7 +111,6 @@ can give; only Georgian, contributed by a native speaker (#1843), has had it. | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | Chinese text in compositions follows the fixed Han fallback: installed Chinese faces are tried before the Japanese face (#453) | | 2026-10-12 | minor | Recounted: Russian, French, German and Czech menus (#873, #987), Brazilian Portuguese and Czech panels (#830, #987), Hangul probe (#599); overall ≈ 12% to ≈ 18%; Korean and Italian still absent | | 2026-10-14 | minor | Georgian (`ka`): menus 628, panels 886 (191 Georgian-only, in `GEORGIAN_PANELS`), effect names and categories in `effects.rs` | | 2026-10-10 | major | First localization parity document: catalog rows counted for all six languages, the twelve-language table, After Effects' 11 shipped languages from its bundle | diff --git a/docs/roadmap.md b/docs/roadmap.md index e2692fd8b..73fdfd235 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap detail -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (the Chinese Han fallback reaches the installed Chinese faces, #453; previous: major, new file: milestones moved from ROADMAP.md, current focus and next steps with Opus 5.5 estimates) · **Target:** Adobe After Effects 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (new file: milestones moved from ROADMAP.md, current focus and next steps with Opus 5.5 estimates) · **Target:** Adobe After Effects 2026 Forward-looking detail behind the [ROADMAP](../ROADMAP.md). The work list itself, one gap at a time, is [gaps.md](gaps.md); the numbers are in [target-app-parity.md](target-app-parity.md). @@ -31,7 +31,7 @@ none blocks the workflow. |---|---|---|---| | 1. Set up: new comp, import footage, stills, PSD/AI and audio, arrange and trim layers | yes | `file.import` and image-sequence tests, `ui_drag_drop`, `ui_project_*`; rough: a still imports as a sequence (#431), an MP3 layer can go silent (#324), multi-file drops (#552) | 2–4 | | 2. Animate: keyframes, easing in the Graph Editor, motion paths, parenting | yes | 168 live samples match After Effects 26.3 ([fidelity/](fidelity/README.md)); `ui_keyframes`; rough: Graph Editor jitter and clipped buttons (#456, #457), negative rotation drag (#569) | 3–6 | -| 3. Motion graphics: text layers with animators, shape layers with operators | yes | `ui_text_edit`, `ui_timeline_shapes`, text and shape engine tests; rough: paragraph alignment inverted (#316), Chinese text in comps follows the Han fallback (#453 fixed) | 3–6 | +| 3. Motion graphics: text layers with animators, shape layers with operators | yes | `ui_text_edit`, `ui_timeline_shapes`, text and shape engine tests; rough: paragraph alignment inverted (#316), CJK text in comps (#453) | 3–6 | | 4. Composite: masks, track mattes, blend modes, keying and effects | yes | Compositor and mask tests, 306 effects with CPU/GPU agreement; rough: mask bugs (#510, #513, #527), 32-bpc blend differences (#494), effect bugs (#580 crash, #514) | 6–12 | | 5. Preview with audio (RAM preview, cache) | yes | `ui_ram_preview`, `ui_live_preview`; rough: stutter on cached frames (#595, #304), late audio (#554, #504) | 10–20 | | 6. Save, reopen and deliver: `.ecproj`, auto-save, Render Queue to H.264 / ProRes / sequences | yes | Versioned-schema reopen tests, crash recovery, render-queue and encoder tests; rough: the macOS default output folder is `/`, so a render fails until an output path is chosen (#446), ProRes 4444 colour tag (#340), projects don't relink when moved | 1–3 | @@ -83,6 +83,5 @@ To beta (≈ 75% ready and `.aep` opening): **≈ 350–620 h**. To full parity: | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | The Chinese Han fallback reaches the installed Chinese faces (#453) | | 2026-10-10 | minor | Alpha gate: six core workflows checked; all pass, stage stays alpha | | 2026-10-10 | major | New file: milestones from ROADMAP.md, current focus, next steps with estimates | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 24030f9fd..ff7cc3ef9 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with After Effects -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (the Chinese Han fallback reaches the installed macOS Chinese faces, so Chinese text in compositions no longer draws Japanese glyph forms, #453; previous: minor, issue, catalog and export-format references re-audited against `upstream/main` 470ea7a3; the 10 October re-measure is otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (issue, catalog and export-format references re-audited against `upstream/main` 470ea7a3; the 10 October re-measure is otherwise unchanged) · **Target:** Adobe After Effects 2026 (26.5.0.89) The authoritative assessment of how close EffectCraft is to After Effects, and how much work is left. [gaps.md](gaps.md) lists every known shortfall one at a time; the @@ -119,7 +119,7 @@ compositing and effects dominate; audio is light). Hours are to full parity for | Animation: keyframes, graph editor, motion paths, puppet | 15% | 97% | 55% | 35–60 | Only scalar ease is measured against After Effects (168 samples); puppet lag (#586); #456, #457, #355, #553 and #318 are closed; Proportional Scrubbing (26.2) | | Expressions and scripting | 7% | 93% | 65% | 20–40 | `.jsxbin`, Startup/Shutdown folders, script shortcuts (`#include` landed, #759); external editor (#364); see [scripting-parity.md](scripting-parity.md) | | Shapes, masks and roto | 9% | 93% | 45% | 30–50 | Open mask and shape issues (#1004, #999, #1042, #932); #510, #513, #527, #509 and #303 are closed; paste SVG/AI as shape layers (26.3; SVG markup pastes since #738) | -| Text and typography | 7% | 94% | 50% | 20–35 | Chinese text in comps now reaches the installed Chinese faces in the Han fallback (#453); paragraph alignment inverted (#316); soft text at Fit (#417, mostly fixed by #444); toolbar font options (#550); #483 and #583 are closed; variable-font filter landed (#741) | +| Text and typography | 7% | 94% | 50% | 20–35 | CJK text in comps renders wrongly (#453); paragraph alignment inverted (#316); soft text at Fit (#417, mostly fixed by #444); toolbar font options (#550); #483 and #583 are closed; variable-font filter landed (#741) | | Effects and animation presets | 14% | 85% | 40% | 70–130 | All 306 effects exist, but rendered output is unmeasured against After Effects; 54 built-in presets (text, Transitions, Backgrounds) against After Effects' 621 ([effects-parity.md](effects-parity.md)); open effect bugs (#580 crash, #463, #369, #473, #474; #514 and #526 are closed); effect colour labels (26.5); see [effects-parity.md](effects-parity.md) | | 3D (Classic and Advanced) | 6% | 82% | 35% | 35–65 | No FBX/USD/STL models, no Substance materials, mesh displacement unverified; no 3D transform gizmos (#392, #347); reflections (#261); camera aperture (#260); #489 and #315 are closed | | Tracking and AI-assisted tools | 5% | 80% | 35% | 35–70 | Object Matte (26.2) has the effect, the viewer tool (click / box), background tracking, Freeze and the 26.5 disk cache, not yet compared with After Effects; Roto Brush (MobileSAM) and face tracking (MediaPipe) unmeasured against After Effects; camera-tracker view (#505) | @@ -193,7 +193,7 @@ generative/cloud AI (Object Matte, Firefly), Adobe services and team features, s | Keyframes, Graph Editor, easing, motion paths | 20% | 75% | 168 live samples match After Effects; #456/#457, #553 and #569 were counted here and are now closed | | Expressions: `wiggle`, `loopOut`, linking, sliders | 6% | 80% | `crates/expr` tests; no live expression oracle yet | | Shape layers and masks | 12% | 65% | #510, #513, #527 mask bugs and #303 Mesh Warp were counted here and are now closed; open: #1004, #999, #1042 | -| Text and text animators | 12% | 70% | #316 alignment inverted; #453 CJK fallback fixed; #583 font search (closed) | +| Text and text animators | 12% | 70% | #316 alignment inverted, #453 CJK (open); #583 font search (closed) | | Everyday effects: blurs, glow, fill/tint, curves/levels, keying, drop shadow, displacement | 10% | 70% | All present with GPU paths; unmeasured against After Effects; #514 Apply Color LUT (closed) | | Preview (RAM preview with audio) | 8% | 55% | #595, #304 stutter; #504 late audio (#554 closed) | | Render to H.264 / ProRes / PNG sequences | 6% | 75% | Encoder tests; #340 ProRes 4444 tag (open); #446 default macOS output folder (closed) | @@ -1168,7 +1168,6 @@ top of this page and in items 16–18 above. | Date | Change | Summary | |---|---|---| -| 2026-10-22 | minor | Chinese Han text in compositions uses the installed macOS Chinese faces rather than the Japanese face (#453) | | 2026-10-12 | minor | Audited against `upstream/main` 470ea7a3: open crash list, closed-issue references in the area tables, export formats 12 to 13, localization ≈ 12% to ≈ 18%, `#include`, built-in preset count, 12 October repository counts | | 2026-10-10 | minor | Readiness-by-audience table: hours to ≈ 95% for each of the three numbers; the full number stays an additive weighted sum (unchanged) | | 2026-10-10 | minor | Added mainstream practitioner (≈ 47%) and essentials user (≈ 52%) numbers with weights, discounts and GitHub user evidence; wrote down how the full ready number is built (unchanged at ≈ 45%) |