mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-13 15:09:03 +00:00
Gap 7: Backgrounds animation presets (#1096)
Problem: the built-in animation preset library had no Backgrounds category (gap 7: 31 presets against After Effects' 621). Fix: 23 original Backgrounds presets in crates/engine/presets/backgrounds.json, built from our own generator, noise, colour and stylize effects: 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. The preset format gains `expr` (an expression per property, so backgrounds keep moving for the whole layer) and point parameters written as fractions of the layer size. Effects & Presets lists them under * Animation Presets > Backgrounds. Tests: catalogue uniqueness and categories, apply/undo/redo of every preset (engine), render of every preset with the expression engine: finite, non-flat, deterministic, moving (host), panel listing and apply (ui-egui). Sidecar, ATTRIBUTION.md, gaps.md row 7 and effects-parity.md updated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
470ea7a3af
commit
51371939ad
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");
|
||||
}
|
||||
}
|
||||
@@ -466,6 +466,31 @@ fn essential_graphics_scale_keeps_constrain_proportions() {
|
||||
assert!(unlinked[0] > linked[0] && unlinked[1] == linked[1], "unlinked, X moves alone: {unlinked:?}");
|
||||
}
|
||||
|
||||
/// 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, #304, #329 slow scrubbing, #554 / #504 / #335 audio late or crackling, #586 puppet lag | Preview is where motion designers spend their day | 25–50 h | G5 · [hardware-parity.md](hardware-parity.md) |
|
||||
| 6 | **Linux and Windows interaction bugs** | #491 multi-layer edits, #488 rename arrows, #493, #492, #467 shortcuts, #268 drag and drop, #320 workspaces not persisting, #302 docking, #613 Direct3D 12 compositor, #596 GPU not detected | 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** | #510, #513, #527 (first Intersect/Darken mask), #509 Roto Brush size, #303 Mesh Warp, #553 pen tools on motion paths | Masks are core compositing | 10–20 h | G2 · [target-app-parity.md](target-app-parity.md) |
|
||||
| 9 | **Effect behaviour bugs** | #580, #526 Fractal reset, #514 Apply Color LUT, #463 Curl Noise, #369 Motion Tile output size, #473 Cryptomatte selection, #474 multi-layer EXR decoded twice, #534–#536 community fixes pending | Each one breaks a look someone built | 10–20 h | G1/G2 · [effects-parity.md](effects-parity.md) |
|
||||
| 10 | **Colour pipeline differences** | #494 32-bpc blend modes clamp and apply opacity differently; #495 Working Gamma 2.4 (in, also for float footage: #750); #340 ProRes 4444 tagged bt709 but sRGB-encoded; ACES 2.0 / OCIO 2.5 configs (26.5) missing; #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, `#include`; external editor (#364); see [scripting-parity.md](scripting-parity.md) |
|
||||
| Shapes, masks and roto | 9% | 93% | 45% | 30–50 | Several open mask bugs (#510, #513, #527); Roto Brush size (#509); Mesh Warp can't be dragged (#303); paste SVG/AI as shape layers (26.3) |
|
||||
| Text and typography | 7% | 94% | 50% | 20–35 | CJK text in comps renders wrongly (#453); paragraph alignment inverted (#316); squeezed text at 4:3 (#483); font search (#583); soft text at Fit (#417); variable-font filter (26.3) |
|
||||
| Effects and animation presets | 14% | 85% | 40% | 70–130 | All 306 effects exist, but rendered output is unmeasured against After Effects; 8 built-in presets against After Effects' 621; open effect bugs (#580 crash, #514, #526, #463, #369, #473); 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; open effect bugs (#580 crash, #514, #526, #463, #369, #473); 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); Z position (#489); reflections (#261); camera aperture (#260); bevel options (#315) |
|
||||
| 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); slow scrubbing (#329); late audio (#554, #504, #335); no benchmark against After Effects |
|
||||
|
||||
Reference in new issue
Block a user