mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #1102 from vangel76/feat-gap7-presets-backgrounds
# Conflicts: # crates/ui-egui/tests/ui_panels.rs # docs/gaps.md # docs/target-app-parity.md
This commit is contained in:
commit
7da7358504
10 files changed
+437
-19
No files matched your search
@@ -67,6 +67,8 @@ Screenshots of EffectCraft itself, rendered headlessly, showing only procedurall
|
||||
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`).
|
||||
- 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`
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
{
|
||||
"format": "effectcraft.animationPresets/1",
|
||||
"notes": "Original EffectCraft background presets (MIT OR Apache-2.0), written from scratch on EffectCraft's own generator, noise, colour and stylize effects. Apply to a solid or any layer with effects. Key times are seconds from the current time; most motion comes from expressions (time-based, so the background keeps moving for the layer's whole duration). Point values are fractions of the layer size; effect paths are the effect's parameter ids.",
|
||||
"presets": [
|
||||
{
|
||||
"id": "softGradientDrift",
|
||||
"name": "Soft Gradient Drift",
|
||||
"category": "Backgrounds",
|
||||
"description": "Four soft colour corners slowly drift around the frame.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.fourcolor", "set": {"positionsColors/color1": [0.1137, 0.1686, 0.3922, 1], "positionsColors/color2": [0.1647, 0.6157, 0.5608, 1], "positionsColors/color3": [0.4824, 0.1765, 0.5608, 1], "positionsColors/color4": [0.9569, 0.6353, 0.3804, 1], "blend": 100}, "expr": {"positionsColors/point1": "[thisComp.width*(0.15+0.1*Math.sin(time*0.7)), thisComp.height*(0.2+0.1*Math.cos(time*0.5))]", "positionsColors/point2": "[thisComp.width*(0.85+0.08*Math.cos(time*0.6)), thisComp.height*(0.15+0.1*Math.sin(time*0.8))]", "positionsColors/point3": "[thisComp.width*(0.2+0.1*Math.sin(time*0.9+1)), thisComp.height*(0.85+0.08*Math.cos(time*0.4))]", "positionsColors/point4": "[thisComp.width*(0.8+0.1*Math.cos(time*0.5+2)), thisComp.height*(0.8+0.1*Math.sin(time*0.7+1))]"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "auroraVeil",
|
||||
"name": "Aurora Veil",
|
||||
"category": "Backgrounds",
|
||||
"description": "Tall green curtains of light ripple slowly across a night sky.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 170, "brightness": -40, "complexity": 6, "fractalType": 1, "transform/uniformScaling": false, "transform/scaleWidth": 500, "transform/scaleHeight": 70}, "expr": {"evolution": "time*18", "transform/offset": "[thisComp.width/2 + Math.sin(time*0.3)*thisComp.width*0.2, thisComp.height/2]"}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.0078, 0.0392, 0.0784, 1], "white": [0.302, 1.0, 0.7529, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "slowFog",
|
||||
"name": "Slow Fog",
|
||||
"category": "Backgrounds",
|
||||
"description": "Heavy grey-blue fog banks roll gently through the frame.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 75, "brightness": -5, "complexity": 6, "fractalType": 1, "transform/scale": 320}, "expr": {"evolution": "time*14", "transform/offset": "[thisComp.width/2 + time*25, thisComp.height/2]"}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.0588, 0.0941, 0.1333, 1], "white": [0.6235, 0.7059, 0.7765, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cloudBank",
|
||||
"name": "Cloud Bank",
|
||||
"category": "Backgrounds",
|
||||
"description": "Soft cumulus clouds drift across a blue sky.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 130, "brightness": 8, "complexity": 7, "transform/scale": 240}, "expr": {"evolution": "time*10", "transform/offset": "[thisComp.width/2 + time*40, thisComp.height/2]"}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.1843, 0.4353, 0.702, 1], "white": [1.0, 1.0, 1.0, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "darkSmoke",
|
||||
"name": "Dark Smoke",
|
||||
"category": "Backgrounds",
|
||||
"description": "Thick dark smoke rises slowly and curls as it goes.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.turbulent", "set": {"contrast": 140, "brightness": -15, "complexity": 6, "transform/scale": 200}, "expr": {"evolution": "time*45", "transform/offset": "[thisComp.width/2, thisComp.height/2 + time*70]"}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.0, 0.0, 0.0, 1], "white": [0.4157, 0.4157, 0.4549, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "emberHaze",
|
||||
"name": "Ember Haze",
|
||||
"category": "Backgrounds",
|
||||
"description": "A warm glowing haze of embers, black to deep red to amber.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 120, "brightness": 0, "complexity": 6, "fractalType": 1, "transform/scale": 180}, "expr": {"evolution": "time*30", "transform/offset": "[thisComp.width/2, thisComp.height/2 + time*40]"}},
|
||||
{"effect": "ec.color.tritone", "set": {"shadows": [0.051, 0.0078, 0.0, 1], "midtones": [0.6275, 0.1569, 0.0, 1], "highlights": [1.0, 0.702, 0.2784, 1]}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "magmaFlow",
|
||||
"name": "Magma Flow",
|
||||
"category": "Backgrounds",
|
||||
"description": "Fire-coloured plasma that slowly cycles through its palette.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 110, "brightness": 0, "complexity": 6, "transform/scale": 260}, "expr": {"evolution": "time*20"}},
|
||||
{"effect": "ec.color.colorama", "set": {"outputCycle/usePresetPalette": 2}, "expr": {"inputPhase/phaseShift": "time*30"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "iceFlow",
|
||||
"name": "Ice Flow",
|
||||
"category": "Backgrounds",
|
||||
"description": "Cold blue and white veins drift through a frozen field.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 60, "brightness": -5, "complexity": 6, "fractalType": 1, "transform/scale": 300}, "expr": {"evolution": "time*12"}},
|
||||
{"effect": "ec.color.colorama", "set": {"outputCycle/usePresetPalette": 3, "outputCycle/cycles": 1}, "expr": {"inputPhase/phaseShift": "Math.sin(time*0.4)*40"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pastelPlasma",
|
||||
"name": "Pastel Plasma",
|
||||
"category": "Backgrounds",
|
||||
"description": "Soft pastel plasma with colours that endlessly cycle.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 170, "brightness": 0, "complexity": 6, "transform/scale": 300}, "expr": {"evolution": "time*16"}},
|
||||
{"effect": "ec.color.colorama", "set": {"outputCycle/usePresetPalette": 7}, "expr": {"inputPhase/phaseShift": "time*25"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cellDrift",
|
||||
"name": "Cell Drift",
|
||||
"category": "Backgrounds",
|
||||
"description": "Bubble-like cells slowly swell and merge in teal tones.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.cellpattern", "set": {"cellPattern": 0, "size": 90, "contrast": 140, "disperse": 1.2}, "expr": {"evolution": "time*40"}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.0157, 0.1255, 0.1686, 1], "white": [0.3725, 0.8902, 0.7843, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "crystalFacets",
|
||||
"name": "Crystal Facets",
|
||||
"category": "Backgrounds",
|
||||
"description": "Sharp crystal facets shimmer in warm violet light.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.cellpattern", "set": {"cellPattern": 1, "size": 110, "contrast": 120}, "expr": {"evolution": "time*25"}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.102, 0.0431, 0.1804, 1], "white": [0.8784, 0.6392, 1.0, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hexMosaic",
|
||||
"name": "Hex Mosaic",
|
||||
"category": "Backgrounds",
|
||||
"description": "A hexagon mosaic that glows as fog passes beneath it.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 110, "brightness": 0, "complexity": 6, "transform/scale": 300}, "expr": {"evolution": "time*20", "transform/offset": "[thisComp.width/2 + time*20, thisComp.height/2]"}},
|
||||
{"effect": "ec.stylize.cchextile", "set": {"radius": 45}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.0392, 0.0588, 0.2392, 1], "white": [0.3216, 0.8196, 1.0, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mirroredSilk",
|
||||
"name": "Mirrored Silk",
|
||||
"category": "Backgrounds",
|
||||
"description": "Flowing silk ribbons mirrored down the centre.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.fractal", "set": {"contrast": 130, "brightness": 0, "complexity": 6, "fractalType": 1, "transform/scale": 260}, "expr": {"evolution": "time*15", "transform/offset": "[thisComp.width/2 + Math.sin(time*0.4)*thisComp.width*0.15, thisComp.height/2]"}},
|
||||
{"effect": "ec.distort.mirror", "set": {"center": [0.5, 0.5], "angle": 0}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.1686, 0.0392, 0.1176, 1], "white": [1.0, 0.7608, 0.851, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "waterCaustics",
|
||||
"name": "Water Caustics",
|
||||
"category": "Backgrounds",
|
||||
"description": "Bright caustic filaments shimmer across a turquoise pool floor.",
|
||||
"effects": [
|
||||
{"effect": "ec.noise.turbulent", "set": {"contrast": 120, "brightness": -25, "complexity": 6, "transform/scale": 110, "fractalType": 2}, "expr": {"evolution": "time*60"}},
|
||||
{"effect": "ec.color.tint", "set": {"black": [0.0, 0.2275, 0.3216, 1], "white": [0.5608, 0.9529, 1.0, 1], "amount": 100}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "blueprintGrid",
|
||||
"name": "Blueprint Grid",
|
||||
"category": "Backgrounds",
|
||||
"description": "A technical grid slides across a deep blue plate.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.fill", "set": {"allMasks": true, "color": [0.0431, 0.2392, 0.5686, 1]}},
|
||||
{"effect": "ec.generate.grid", "set": {"width": 16, "height": 16, "border": 1, "color": [0.6235, 0.8314, 1.0, 1], "opacity": 18, "blendingMode": 1}, "expr": {"anchor": "[time*8, time*8]"}},
|
||||
{"effect": "ec.generate.grid", "set": {"width": 80, "height": 80, "border": 1.5, "color": [0.8471, 0.9373, 1.0, 1], "opacity": 45, "blendingMode": 1}, "expr": {"anchor": "[time*16, time*16]"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "checkerScroll",
|
||||
"name": "Checker Scroll",
|
||||
"category": "Backgrounds",
|
||||
"description": "A two-tone checkerboard scrolls diagonally.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.fill", "set": {"allMasks": true, "color": [0.1098, 0.1216, 0.2275, 1]}},
|
||||
{"effect": "ec.generate.checkerboard", "set": {"sizeFrom": 1, "width": 90, "height": 90, "color": [0.1765, 0.2, 0.3882, 1], "opacity": 100, "blendingMode": 1}, "expr": {"anchor": "[time*45, time*45]"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scrollingStripes",
|
||||
"name": "Scrolling Stripes",
|
||||
"category": "Backgrounds",
|
||||
"description": "Wide vertical stripes slide steadily sideways.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.fill", "set": {"allMasks": true, "color": [0.949, 0.9098, 0.8118, 1]}},
|
||||
{"effect": "ec.generate.checkerboard", "set": {"sizeFrom": 0, "corner": [0.11, 20], "color": [0.2196, 0.4, 0.2549, 1], "opacity": 100, "blendingMode": 1}, "expr": {"anchor": "[time*60, 0]"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pulseRings",
|
||||
"name": "Pulse Rings",
|
||||
"category": "Backgrounds",
|
||||
"description": "Soft rings expand from the centre in a steady rhythm.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.fill", "set": {"allMasks": true, "color": [0.0235, 0.0784, 0.1686, 1]}},
|
||||
{"effect": "ec.generate.circle", "set": {"edge": 2, "thickness": 4, "feather/feather": 3, "color": [0.498, 0.9059, 1.0, 1], "blendingMode": 1}, "expr": {"center": "[thisComp.width/2, thisComp.height/2]", "radius": "((time+0)%3)/3*Math.max(thisComp.width,thisComp.height)*0.7", "opacity": "100*Math.pow(1-((time+0)%3)/3, 1.5)"}},
|
||||
{"effect": "ec.generate.circle", "set": {"edge": 2, "thickness": 4, "feather/feather": 3, "color": [0.498, 0.9059, 1.0, 1], "blendingMode": 1}, "expr": {"center": "[thisComp.width/2, thisComp.height/2]", "radius": "((time+1)%3)/3*Math.max(thisComp.width,thisComp.height)*0.7", "opacity": "100*Math.pow(1-((time+1)%3)/3, 1.5)"}},
|
||||
{"effect": "ec.generate.circle", "set": {"edge": 2, "thickness": 4, "feather/feather": 3, "color": [0.498, 0.9059, 1.0, 1], "blendingMode": 1}, "expr": {"center": "[thisComp.width/2, thisComp.height/2]", "radius": "((time+2)%3)/3*Math.max(thisComp.width,thisComp.height)*0.7", "opacity": "100*Math.pow(1-((time+2)%3)/3, 1.5)"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bokehDrift",
|
||||
"name": "Bokeh Drift",
|
||||
"category": "Backgrounds",
|
||||
"description": "Out-of-focus lights float lazily over a dark violet backdrop.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.fill", "set": {"allMasks": true, "color": [0.0784, 0.0627, 0.1647, 1]}},
|
||||
{"effect": "ec.generate.circle", "set": {"radius": 40, "feather/feather": 70, "color": [1.0, 0.6196, 0.7961, 1], "opacity": 32, "blendingMode": 1}, "expr": {"center": "[thisComp.width*(0.12+0.05*Math.sin(time*0.3+0)), thisComp.height*(0.25+0.07*Math.cos(time*0.25+0))]"}},
|
||||
{"effect": "ec.generate.circle", "set": {"radius": 49, "feather/feather": 70, "color": [0.5608, 0.8275, 1.0, 1], "opacity": 32, "blendingMode": 1}, "expr": {"center": "[thisComp.width*(0.27+0.05*Math.sin(time*0.41+1)), thisComp.height*(0.6+0.07*Math.cos(time*0.32+2))]"}},
|
||||
{"effect": "ec.generate.circle", "set": {"radius": 58, "feather/feather": 70, "color": [1.0, 0.8471, 0.5412, 1], "opacity": 32, "blendingMode": 1}, "expr": {"center": "[thisComp.width*(0.42+0.05*Math.sin(time*0.52+2)), thisComp.height*(0.45+0.07*Math.cos(time*0.39+4))]"}},
|
||||
{"effect": "ec.generate.circle", "set": {"radius": 67, "feather/feather": 70, "color": [0.7137, 0.6118, 1.0, 1], "opacity": 32, "blendingMode": 1}, "expr": {"center": "[thisComp.width*(0.57+0.05*Math.sin(time*0.63+3)), thisComp.height*(0.3+0.07*Math.cos(time*0.46+6))]"}},
|
||||
{"effect": "ec.generate.circle", "set": {"radius": 76, "feather/feather": 70, "color": [0.5412, 1.0, 0.7569, 1], "opacity": 32, "blendingMode": 1}, "expr": {"center": "[thisComp.width*(0.72+0.05*Math.sin(time*0.74+4)), thisComp.height*(0.65+0.07*Math.cos(time*0.53+8))]"}},
|
||||
{"effect": "ec.generate.circle", "set": {"radius": 85, "feather/feather": 70, "color": [1.0, 0.6196, 0.4784, 1], "opacity": 32, "blendingMode": 1}, "expr": {"center": "[thisComp.width*(0.87+0.05*Math.sin(time*0.8500000000000001+5)), thisComp.height*(0.5+0.07*Math.cos(time*0.6000000000000001+10))]"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "spotlightFloor",
|
||||
"name": "Spotlight Floor",
|
||||
"category": "Backgrounds",
|
||||
"description": "A soft pool of light on a dark floor that gently breathes.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.gradientramp", "set": {"shape": 1, "startColor": [0.2902, 0.3608, 0.5216, 1], "endColor": [0.0196, 0.0275, 0.051, 1]}, "expr": {"start": "[thisComp.width/2, thisComp.height*0.45]", "end": "[thisComp.width/2 + thisComp.width*(0.55+0.04*Math.sin(time*1.3)), thisComp.height*0.45]"}},
|
||||
{"effect": "ec.stylize.ccvignette", "set": {"amount": 35}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dawnSky",
|
||||
"name": "Dawn Sky",
|
||||
"category": "Backgrounds",
|
||||
"description": "A night sky warms into sunrise over six seconds.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.gradientramp", "set": {"start": [0.5, 0.0], "end": [0.5, 1.0]}, "keys": {"startColor": [[0, [0.1216, 0.2275, 0.5412, 1]], [6, [0.3569, 0.5255, 0.8392, 1]]], "endColor": [[0, [0.0118, 0.0157, 0.0471, 1]], [6, [1.0, 0.702, 0.4196, 1]]]}, "ease": true}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sweepingLight",
|
||||
"name": "Sweeping Light",
|
||||
"category": "Backgrounds",
|
||||
"description": "A band of light sweeps repeatedly across a dark slate backdrop.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.gradientramp", "set": {"startColor": [0.0706, 0.0863, 0.1216, 1], "endColor": [0.1451, 0.1686, 0.2275, 1]}},
|
||||
{"effect": "ec.generate.cclightsweep", "set": {"direction": -30, "width": 60, "sweepIntensity": 70}, "expr": {"center": "[(time%3)/3*thisComp.width*1.6 - thisComp.width*0.3, thisComp.height/2]"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "grainyWash",
|
||||
"name": "Grainy Wash",
|
||||
"category": "Backgrounds",
|
||||
"description": "A muted diagonal wash with live film grain.",
|
||||
"effects": [
|
||||
{"effect": "ec.generate.gradientramp", "set": {"start": [0.1, 0.1], "end": [0.9, 0.9], "startColor": [0.9098, 0.8353, 0.7686, 1], "endColor": [0.5451, 0.4353, 0.5608, 1]}},
|
||||
{"effect": "ec.noise.addgrain", "set": {"tweaking/intensity": 2.2, "tweaking/size": 1.4}},
|
||||
{"effect": "ec.stylize.ccvignette", "set": {"amount": 25}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
asset: backgrounds.json
|
||||
title: EffectCraft Backgrounds animation presets (Soft Gradient Drift, Aurora Veil, Slow Fog, Cloud Bank, Dark Smoke, Ember Haze, Magma Flow, Ice Flow, Pastel Plasma, Cell Drift, Crystal Facets, Hex Mosaic, Mirrored Silk, Water Caustics, Blueprint Grid, Checker Scroll, Scrolling Stripes, Pulse Rings, Bokeh Drift, Spotlight Floor, Dawn Sky, Sweeping Light, Grainy Wash)
|
||||
author: EffectCraft contributors
|
||||
source: original work
|
||||
license: MIT OR Apache-2.0
|
||||
added: 2026-10-12 by vangel76 (Claude Code agent, gap 7, #1096)
|
||||
notes: Effect choices, colours, values and expressions written from scratch for EffectCraft on its own generator, noise, colour and stylize effects; not derived from any Adobe preset (.ffx) or its values.
|
||||
@@ -1,14 +1,18 @@
|
||||
//! EffectCraft's own animation preset library (Effects & Presets ▸ * Animation Presets): the
|
||||
//! Transitions category, original presets built on our Transition effects and layer opacity.
|
||||
//! Transitions category (original presets built on our Transition effects and layer opacity) and
|
||||
//! the Backgrounds category (generators, noise and colour effects driven by expressions).
|
||||
//!
|
||||
//! Each preset adds effects (with values and keyframes) and/or keys layer properties, starting
|
||||
//! at the current time, on every selected layer. The library is data
|
||||
//! (`presets/transitions.json`), written from scratch; nothing comes from After Effects' `.ffx`
|
||||
//! presets.
|
||||
//! (`presets/*.json`), written from scratch; nothing comes from After Effects' `.ffx` presets.
|
||||
//!
|
||||
//! A preset effect or layer block has `set` (static values), `keys` (`[[seconds, value], …]`
|
||||
//! from the current time), `expr` (an expression text per property) and `ease`. Values of
|
||||
//! point parameters are fractions of the layer size, like the effects' own defaults.
|
||||
|
||||
use effectcraft_keyframe::Keyframe;
|
||||
use effectcraft_project::build::Ids;
|
||||
use effectcraft_project::{Node, Property};
|
||||
use effectcraft_project::{Expression, Node, ParamUi, Property, Value as KV};
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
@@ -19,11 +23,19 @@ use crate::{EngineError, Result, Session, cmd, query};
|
||||
/// The original transition presets (`presets/transitions.json`).
|
||||
pub const TRANSITIONS_JSON: &str = include_str!("../../presets/transitions.json");
|
||||
|
||||
/// The original background presets (`presets/backgrounds.json`).
|
||||
pub const BACKGROUNDS_JSON: &str = include_str!("../../presets/backgrounds.json");
|
||||
|
||||
/// Keys per animated property, so a hostile inline preset can't allocate without bound.
|
||||
const MAX_KEYS: usize = 256;
|
||||
/// Longest expression text an inline preset may carry.
|
||||
const MAX_EXPR: usize = 4096;
|
||||
|
||||
fn presets() -> Vec<Value> {
|
||||
serde_json::from_str::<Value>(TRANSITIONS_JSON).ok().and_then(|v| v.get("presets").and_then(Value::as_array).cloned()).unwrap_or_default()
|
||||
[TRANSITIONS_JSON, BACKGROUNDS_JSON]
|
||||
.iter()
|
||||
.flat_map(|src| serde_json::from_str::<Value>(src).ok().and_then(|v| v.get("presets").and_then(Value::as_array).cloned()).unwrap_or_default())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn text<'a>(v: &'a Value, key: &str) -> Option<&'a str> {
|
||||
@@ -44,11 +56,16 @@ fn find_preset(want: &str) -> Option<Value> {
|
||||
|
||||
/// Set `pr` to `v` (when given) and replace its keys with `keys` (`[[seconds, value], …]`,
|
||||
/// seconds from `at`). Returns false when nothing in the request fits the property.
|
||||
fn set_prop(pr: &mut Property, v: Option<&Value>, keys: Option<&Value>, at: Tick, ease: bool) -> bool {
|
||||
fn set_prop(pr: &mut Property, v: Option<&Value>, keys: Option<&Value>, at: Tick, ease: bool, size: [f64; 2]) -> bool {
|
||||
// Point parameters are written as fractions of the layer size.
|
||||
let scale = |val: KV| match val {
|
||||
KV::Vec2(f) if matches!(pr.ui, ParamUi::Point) => KV::Vec2([f[0] * size[0], f[1] * size[1]]),
|
||||
other => other,
|
||||
};
|
||||
let mut ok = true;
|
||||
if let Some(v) = v {
|
||||
match json_value(v, &pr.value) {
|
||||
Some(val) => pr.value = val,
|
||||
Some(val) => pr.value = scale(val),
|
||||
None => ok = false,
|
||||
}
|
||||
}
|
||||
@@ -58,7 +75,7 @@ fn set_prop(pr: &mut Property, v: Option<&Value>, keys: Option<&Value>, at: Tick
|
||||
for k in list.iter().take(MAX_KEYS) {
|
||||
let t = k.get(0).and_then(Value::as_f64).filter(|t| t.is_finite() && t.abs() <= 86_400.0);
|
||||
let (Some(t), Some(val)) = (t, k.get(1).and_then(|x| json_value(x, &pr.value))) else { return false };
|
||||
let mut key = Keyframe::new(at + Tick::from_seconds_f64(t), val);
|
||||
let mut key = Keyframe::new(at + Tick::from_seconds_f64(t), scale(val));
|
||||
if pr.hold_only {
|
||||
key = key.hold();
|
||||
} else if ease {
|
||||
@@ -76,22 +93,30 @@ fn set_prop(pr: &mut Property, v: Option<&Value>, keys: Option<&Value>, at: Tick
|
||||
|
||||
/// Apply `set` / `keys` maps to the properties under `root`; returns the paths that don't exist
|
||||
/// or don't take the given values.
|
||||
fn apply_maps(root: &mut effectcraft_project::PropGroup, spec: &Value, at: Tick) -> Vec<String> {
|
||||
fn apply_maps(root: &mut effectcraft_project::PropGroup, spec: &Value, at: Tick, size: [f64; 2]) -> Vec<String> {
|
||||
let ease = spec.get("ease").and_then(Value::as_bool).unwrap_or(false);
|
||||
let empty = Map::new();
|
||||
let set = spec.get("set").and_then(Value::as_object).unwrap_or(&empty);
|
||||
let keys = spec.get("keys").and_then(Value::as_object).unwrap_or(&empty);
|
||||
let exprs = spec.get("expr").and_then(Value::as_object).unwrap_or(&empty);
|
||||
let mut skipped = Vec::new();
|
||||
for (path, v) in set {
|
||||
if !root.prop_mut(path).is_some_and(|pr| set_prop(pr, Some(v), None, at, false)) {
|
||||
if !root.prop_mut(path).is_some_and(|pr| set_prop(pr, Some(v), None, at, false, size)) {
|
||||
skipped.push(path.clone());
|
||||
}
|
||||
}
|
||||
for (path, list) in keys {
|
||||
if !root.prop_mut(path).is_some_and(|pr| !pr.static_only && set_prop(pr, None, Some(list), at, ease)) {
|
||||
if !root.prop_mut(path).is_some_and(|pr| !pr.static_only && set_prop(pr, None, Some(list), at, ease, size)) {
|
||||
skipped.push(path.clone());
|
||||
}
|
||||
}
|
||||
for (path, text) in exprs {
|
||||
let body = text.as_str().filter(|t| !t.trim().is_empty() && t.len() <= MAX_EXPR);
|
||||
match (root.prop_mut(path), body) {
|
||||
(Some(pr), Some(body)) if !pr.static_only => pr.expr = Some(Expression { text: body.to_string(), enabled: true }),
|
||||
_ => skipped.push(path.clone()),
|
||||
}
|
||||
}
|
||||
skipped
|
||||
}
|
||||
|
||||
@@ -134,13 +159,13 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let same = fx.groups().filter(|g| g.match_id == spec.id).count();
|
||||
let inst = if same == 0 { spec.name.to_string() } else { format!("{} {}", spec.name, same.saturating_add(1)) };
|
||||
let mut g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), &inst, *size);
|
||||
skipped.extend(apply_maps(&mut g, ej, at).into_iter().map(|path| format!("{}/{path}", spec.id)));
|
||||
skipped.extend(apply_maps(&mut g, ej, at, *size).into_iter().map(|path| format!("{}/{path}", spec.id)));
|
||||
added.push(g.uid);
|
||||
fx.children.push(Node::Group(g));
|
||||
}
|
||||
}
|
||||
if let Some(ls) = &layer_spec {
|
||||
skipped.extend(apply_maps(&mut l.props, ls, at));
|
||||
skipped.extend(apply_maps(&mut l.props, ls, at, *size));
|
||||
}
|
||||
proj.next_id = next;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ fn every_transition_preset_applies_and_transitions() {
|
||||
let list = s.execute("anim.builtinPresets", json!({"category": "Transitions"})).unwrap();
|
||||
let list = list.as_array().unwrap().clone();
|
||||
assert!(list.len() >= 20, "{}", list.len());
|
||||
assert_eq!(crate::animation_presets().len(), list.len());
|
||||
assert_eq!(crate::animation_presets().iter().filter(|(_, _, c)| c == "Transitions").count(), list.len());
|
||||
let start = 0.5;
|
||||
for p in &list {
|
||||
let id = p["id"].as_str().unwrap();
|
||||
@@ -72,3 +72,57 @@ fn transition_preset_by_name_and_bad_input() {
|
||||
let cam = s.execute("layer.newCamera", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
assert!(s.execute("anim.applyBuiltinPreset", json!({"layers": [cam], "preset": "clockReveal"})).is_err());
|
||||
}
|
||||
|
||||
/// Every preset's id and name are unique across the whole library (ids key the panel's
|
||||
/// automation ids), each sits in a known category, and Text ids don't clash with them.
|
||||
#[test]
|
||||
fn preset_catalogue_is_unique_and_categorised() {
|
||||
let all = crate::animation_presets();
|
||||
let known = ["Transitions", "Backgrounds"];
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
let mut names = std::collections::HashSet::new();
|
||||
for (id, name, cat) in &all {
|
||||
assert!(known.contains(&cat.as_str()), "{id}: unknown category {cat}");
|
||||
assert!(ids.insert(id.to_lowercase()), "duplicate id {id}");
|
||||
assert!(names.insert(name.to_lowercase()), "duplicate name {name}");
|
||||
}
|
||||
for (id, name) in crate::text_presets() {
|
||||
assert!(!ids.contains(&id.to_lowercase()) && !names.contains(&name.to_lowercase()), "text preset {id} clashes");
|
||||
}
|
||||
let n = |c: &str| all.iter().filter(|(_, _, k)| k == c).count();
|
||||
assert!(n("Backgrounds") >= 15 && n("Transitions") >= 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_background_preset_applies_and_undoes() {
|
||||
let (mut s, solid) = session();
|
||||
let list = s.execute("anim.builtinPresets", json!({"category": "Backgrounds"})).unwrap();
|
||||
let list = list.as_array().unwrap().clone();
|
||||
assert!(list.len() >= 15, "{}", list.len());
|
||||
for p in &list {
|
||||
let id = p["id"].as_str().unwrap();
|
||||
let r = s.execute("anim.applyBuiltinPreset", json!({"layers": [solid], "preset": id})).unwrap();
|
||||
assert_eq!(r["skipped"], json!([]), "{id}: every path and expression in the preset applies");
|
||||
let n = r["effects"].as_array().unwrap().len();
|
||||
assert!(n >= 1, "{id}");
|
||||
assert_eq!(s.active_comp().unwrap().layers[0].effects().unwrap().groups().count(), n, "{id}");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
let l = &s.active_comp().unwrap().layers[0];
|
||||
assert_eq!(l.effects().map_or(0, |fx| fx.groups().count()), 0, "{id}: undo removes the effects");
|
||||
s.execute("edit.redo", json!({})).unwrap();
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_expressions_and_point_fractions() {
|
||||
let (mut s, solid) = session();
|
||||
s.execute("anim.applyBuiltinPreset", json!({"layers": [solid], "preset": "Spotlight Floor"})).unwrap();
|
||||
let fx = s.active_comp().unwrap().layers[0].effects().unwrap().groups().next().unwrap().clone();
|
||||
assert!(fx.prop("start").unwrap().has_expression());
|
||||
// A point written as a fraction of the layer: Sunrise-style presets keep their defaults at 160x90.
|
||||
s.execute("anim.applyBuiltinPreset", json!({"layers": [solid], "preset": "dawnSky"})).unwrap();
|
||||
let ramp = s.active_comp().unwrap().layers[0].effects().unwrap().groups().nth(2).unwrap().clone();
|
||||
assert_eq!(ramp.prop("end").unwrap().value, effectcraft_keyframe::Value::Vec2([80.0, 90.0]));
|
||||
assert_eq!(ramp.prop("startColor").unwrap().keys.len(), 2);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! The Backgrounds animation presets through the wired session (with the expression engine):
|
||||
//! each one renders a non-flat, finite, deterministic frame that keeps moving, and undoes.
|
||||
|
||||
use effectcraft_engine::render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::json;
|
||||
|
||||
type Frame = Vec<[f32; 4]>;
|
||||
|
||||
/// The frame at `secs`, its mean luma, luma standard deviation and whether any channel is not finite.
|
||||
fn frame(s: &effectcraft_engine::Session, secs: f64) -> (Frame, f64, f64, bool) {
|
||||
let Some(cid) = s.active_comp_id() else { panic!("no active comp") };
|
||||
let img = s.render(cid, Tick::from_seconds_f64(secs), RenderOpts { scale: 1.0, ..Default::default() });
|
||||
let bad = img.data.iter().any(|p| p.iter().any(|c| !c.is_finite()));
|
||||
let l: Vec<f64> = img.data.iter().map(|p| 0.2126 * p[0] as f64 + 0.7152 * p[1] as f64 + 0.0722 * p[2] as f64).collect();
|
||||
let n = l.len().max(1) as f64;
|
||||
let mean = l.iter().sum::<f64>() / n;
|
||||
let sd = (l.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n).sqrt();
|
||||
(img.data.clone(), mean, sd, bad)
|
||||
}
|
||||
|
||||
fn diff(a: &[[f32; 4]], b: &[[f32; 4]]) -> f64 {
|
||||
a.iter().zip(b).map(|(p, q)| (0..3).map(|i| (p[i] - q[i]).abs() as f64).sum::<f64>()).sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_background_preset_renders_moves_and_undoes() {
|
||||
let mut s = effectcraft_host::session();
|
||||
s.execute("comp.new", json!({"name": "BG", "width": 640, "height": 360, "frameRate": 30, "duration": 8})).unwrap();
|
||||
let solid = s.execute("layer.newSolid", json!({"name": "S", "color": "#ffffff"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let list = s.execute("anim.builtinPresets", json!({"category": "Backgrounds"})).unwrap();
|
||||
let list = list.as_array().unwrap().clone();
|
||||
assert!(list.len() >= 15, "{}", list.len());
|
||||
for p in &list {
|
||||
let id = p["id"].as_str().unwrap();
|
||||
s.execute("time.set", json!({"time": 0.0})).unwrap();
|
||||
let r = s.execute("anim.applyBuiltinPreset", json!({"layers": [solid], "preset": id})).unwrap();
|
||||
assert_eq!(r["skipped"], json!([]), "{id}");
|
||||
let mut frames = Vec::new();
|
||||
for t in [0.0, 1.3, 3.7] {
|
||||
let (px, mean, sd, nan) = frame(&s, t);
|
||||
if let Some(dir) = std::env::var_os("EC_PRESET_DUMP") {
|
||||
// Contact-sheet helper for reviewing the looks by eye (not part of the check).
|
||||
let mut ppm = b"P6\n640 360\n255\n".to_vec();
|
||||
ppm.extend(px.iter().flat_map(|p| (0..3).map(|i| (p[i].clamp(0.0, 1.0) * 255.0) as u8)));
|
||||
let _ = std::fs::write(std::path::Path::new(&dir).join(format!("{id}_{t}.ppm")), ppm);
|
||||
}
|
||||
assert!(!nan, "{id} @ {t}: NaN or infinite pixel");
|
||||
assert!(sd > 0.015, "{id} @ {t}: flat frame (mean {mean:.3}, sd {sd:.4})");
|
||||
let (again, ..) = frame(&s, t);
|
||||
assert!(px == again, "{id} @ {t}: render is not deterministic");
|
||||
frames.push(px);
|
||||
}
|
||||
assert!(diff(&frames[0], &frames[1]) > 100.0, "{id}: frame does not move between 0 s and 1.3 s");
|
||||
assert!(diff(&frames[1], &frames[2]) > 100.0, "{id}: frame does not move between 1.3 s and 3.7 s");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
let fx = s.active_comp().map_or(1, |c| c.layers[0].effects().map_or(0, |fx| fx.groups().count()));
|
||||
assert_eq!(fx, 0, "{id}: undo removes the effects");
|
||||
}
|
||||
}
|
||||
@@ -504,6 +504,31 @@ fn properties_scale_link_shares_the_timeline_state() {
|
||||
assert!(!h.state().ui.timeline.unlinked.contains(&uid), "re-linking clears the shared state");
|
||||
}
|
||||
|
||||
/// Gap 7: the Backgrounds category is listed under * Animation Presets and a preset applies on double-click.
|
||||
#[test]
|
||||
fn background_presets_are_listed_and_apply() {
|
||||
let mut h = harness();
|
||||
let layer = h.state().session.active_comp().unwrap().layers[0].id.0;
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap();
|
||||
open(&mut h, "effectsPresets");
|
||||
h.state_mut().ui.effects_search = "slow fog".into();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().auto.find("effects.category.presets/Backgrounds").is_some(), "the Backgrounds folder is listed");
|
||||
let e = h.state().auto.find("effects.preset.builtin.slowFog").expect("Slow Fog is listed").clone();
|
||||
let p: Pos2 = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.step();
|
||||
for _ in 0..2 {
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() });
|
||||
}
|
||||
}
|
||||
h.run_steps(3);
|
||||
let comp = h.state().session.active_comp().unwrap();
|
||||
let names: Vec<String> = comp.layers[0].effects().unwrap().groups().map(|g| g.name.clone()).collect();
|
||||
assert_eq!(names, ["Fractal Noise", "Tint"]);
|
||||
}
|
||||
|
||||
/// #757: Effects & Presets lists the built-in Transitions presets under * Animation Presets;
|
||||
/// double-clicking one applies it to the selected layer (effect plus keyframes).
|
||||
#[test]
|
||||
|
||||
@@ -16,7 +16,7 @@ After Effects', and does the preset library exist. The per-effect catalogue is t
|
||||
| On the GPU | **280** (92%) | Measured: registry flags; GPU vs CPU agreement tested per effect |
|
||||
| 32-bit float, no clamping | 306 | Measured: registry flags |
|
||||
| **Rendered output matches After Effects** | **unmeasured** | No effect is compared with an After Effects render yet. `effectcraft_effects::fidelity::compare_frames` exists for the corpus ([fidelity/README.md](fidelity/README.md)); simulation effects are pinned by golden hashes of our own output (M13.27) |
|
||||
| Animation presets | **31** original presets (8 text-animator, 23 Transitions) against After Effects' **621** `.ffx` presets | Measured: `crates/engine/presets/text_animators.json`, `crates/engine/presets/transitions.json`; the After Effects `Presets` folder |
|
||||
| Animation presets | **54** original presets (8 text-animator, 23 Transitions, 23 Backgrounds) against After Effects' **621** `.ffx` presets | Measured: `crates/engine/presets/text_animators.json`, `crates/engine/presets/transitions.json`, `crates/engine/presets/backgrounds.json`; the After Effects `Presets` folder |
|
||||
| 26.x additions | Curl Noise (26.3) present (#463 open); effect colour labels and the rebuilt Effect Controls (26.5) missing | Release notes |
|
||||
|
||||
**Breadth ≈ 85%** (presets weigh in; the effects themselves are ≈ 100%). **Ready ≈ 40%**: until a
|
||||
@@ -56,8 +56,8 @@ implemented under a generic name ("Key Light").
|
||||
**≈ 70–130 h**: a fidelity corpus case per effect at default and one non-default setting, compared
|
||||
with After Effects renders, and the fixes they turn up (40–80 h, most of it parallel, but every
|
||||
reference render needs After Effects on the owner's Mac); an original preset library covering
|
||||
After Effects' preset categories (Behaviors, Backgrounds, Shapes, Synthetics, Text, Transitions,
|
||||
Image effects) 20–40 h; the open effect bugs 10–20 h. Moving Fractal and the analysis effects to
|
||||
After Effects' preset categories (Behaviors, Shapes, Synthetics, Text, Image effects remain; Backgrounds and
|
||||
Transitions exist) 15–35 h; the open effect bugs 10–20 h. Moving Fractal and the analysis effects to
|
||||
the GPU is performance, not parity.
|
||||
|
||||
## Revision history
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ are grouped by area; the issue tracker has the rest.
|
||||
| 4 | **No CI gate on pull requests** | `.github/workflows` has release, packaging-lint, FreeBSD and Windows ARM64 jobs only; `cargo xtask ci` runs locally | Regressions reach main unseen; 49 community PRs landed in one batch (#598) | 4–8 h | G3 · this file |
|
||||
| 5 | **Playback and scrubbing feel** | #595 stutter on cached frames (partly addressed: nearest-first RAM preview prefetch, cache bars at 1/3 resolution), #304, #504 / #335 audio late or crackling, #586 puppet lag (maintainers asked for details); #329 slow scrubbing (#784) and #554 closed | Preview is where motion designers spend their day | 25–50 h | G5 · [hardware-parity.md](hardware-parity.md) |
|
||||
| 6 | **Linux and Windows interaction bugs** | Open: #488 rename arrows (hardening landed, reporter asked for details), #492 anchor drag, #268 file drops on Wayland (winit), #613 Direct3D 12 compositor. Now closed: #491, #493, #467, #320, #302, #596 | Many reports come from Linux; some Windows GPUs fail to start the compositor | 25–45 h | G2 · [ui-parity.md](ui-parity.md) |
|
||||
| 7 | **Animation presets: 31 against 621** | After Effects ships 621 `.ffx` presets (Behaviors, Backgrounds, Shapes, Synthetics, Text, Transitions…); we ship 31 original presets (8 text-animator, 23 Transitions: wipes, clock and fan reveals, blinds, dissolves, card flips, CC wipes, fades; #757) and read only our `.ecpreset`. Missing: Behaviors, Backgrounds, Shapes, Synthetics, Image and more Text | Presets are how many designers start; tutorials assume them | 15–35 h (original presets; `.ffx` reading is out of clean-room scope) | G8 · [effects-parity.md](effects-parity.md) |
|
||||
| 7 | **Animation presets: 54 against 621** | After Effects ships 621 `.ffx` presets (Behaviors, Backgrounds, Shapes, Synthetics, Text, Transitions…); we ship 54 original presets (8 text-animator; 23 Transitions: wipes, clock and fan reveals, blinds, dissolves, card flips, CC wipes, fades, #757; 23 Backgrounds: drifting gradients, fog, cloud and smoke, plasma and magma, cells, hexagons, scrolling grids and checks, pulse rings, bokeh, light sweep, #1096, driven by expressions so they keep moving, not seamless loops) and read only our `.ecpreset`. Missing: Behaviors, Shapes, Synthetics, Image and more Text | Presets are how many designers start; tutorials assume them | 15–35 h (original presets; `.ffx` reading is out of clean-room scope) | G8 · [effects-parity.md](effects-parity.md) |
|
||||
| 8 | **Mask and shape bugs** | Open: #1004 mask vertex multi-select, #999 auto-pan, #1042 / #1033 vertex nodes under the bounding box, #932 Convert to Bezier Path, #959 / #960 shape group toggles and operators. Now closed: #510, #513, #527, #509, #303, #553 | 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) |
|
||||
|
||||
@@ -120,7 +120,7 @@ compositing and effects dominate; audio is light). Hours are to full parity for
|
||||
| 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) |
|
||||
| Effects and animation presets | 14% | 85% | 40% | 70–130 | All 306 effects exist, but rendered output is unmeasured against After Effects; 31 built-in presets 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) |
|
||||
| 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 | No Object Matte (26.2); Roto Brush (MobileSAM) and face tracking (MediaPipe) unmeasured against After Effects; camera-tracker view (#505) |
|
||||
| Preview and cache | 7% | 92% | 40% | 25–50 | Playback stutters on cached frames (#595, #304); late audio (#504, #335); #329 and #554 are closed; no benchmark against After Effects |
|
||||
|
||||
Reference in new issue
Block a user