-
Notifications
You must be signed in to change notification settings - Fork 4
Animation and Effects.md
This document covers keyframe animations, effects, and transformations in FCPXML, including the critical rules for parameter interpolation and effect UID verification.
Different parameters support different keyframe attributes - adding unsupported attributes causes "param element was ignored" warnings.
| Parameter Type |
interp Attribute |
curve Attribute |
Example |
|---|---|---|---|
| position | ❌ NOT supported | ❌ NOT supported | <keyframe time="0s" value="0 0"/> |
| scale | ❌ NOT supported | ✅ Supported | <keyframe time="0s" value="1 1" curve="linear"/> |
| rotation | ❌ NOT supported | ✅ Supported | <keyframe time="0s" value="0" curve="smooth"/> |
| anchor | ❌ NOT supported | ✅ Supported | <keyframe time="0s" value="0.5 0.5" curve="linear"/> |
| opacity | ✅ Supported | ✅ Supported | <keyframe time="0s" value="1" interp="easeIn" curve="smooth"/> |
| volume | ✅ Supported | ✅ Supported | <keyframe time="0s" value="0dB" interp="linear" curve="smooth"/> |
Position keyframes have NO attributes - spatial interpolation is handled by FCP's internal engine:
<param name="position">
<keyframe time="86399313/24000s" value="0 0"/> <!-- NO interp/curve -->
<keyframe time="172798626/24000s" value="100 50"/> <!-- NO interp/curve -->
</param>- Position interpolation is handled by Final Cut Pro's internal spatial engine
- Adding interp/curve attributes causes "param element was ignored" warnings
- The spatial engine uses Bezier curves automatically
- Linear interpolation is the default and cannot be overridden
func createPositionAnimation(startPos, endPos string, startTime, endTime string) fcp.Param {
return fcp.Param{
Name: "position",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: startTime, Value: startPos}, // No attributes
{Time: endTime, Value: endPos}, // No attributes
},
},
}
}These parameters support ONLY the curve attribute:
<param name="scale">
<keyframe time="86399313/24000s" value="1 1" curve="linear"/> <!-- Only curve -->
<keyframe time="172798626/24000s" value="1.5 1.5" curve="smooth"/> <!-- Only curve -->
</param>
<param name="rotation">
<keyframe time="0s" value="0" curve="linear"/>
<keyframe time="86399313/24000s" value="45" curve="smooth"/>
</param>
<param name="anchor">
<keyframe time="0s" value="0.5 0.5" curve="linear"/>
<keyframe time="86399313/24000s" value="0.3 0.7" curve="smooth"/>
</param>-
linear: Straight line interpolation between keyframes -
smooth: Bezier curve interpolation with automatic tangent handling -
hold: No interpolation, jump to next value - Default:
smoothif not specified
func createScaleAnimation(startScale, endScale string, startTime, endTime string) fcp.Param {
return fcp.Param{
Name: "scale",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: startTime, Value: startScale, Curve: "linear"},
{Time: endTime, Value: endScale, Curve: "smooth"},
},
},
}
}
func createRotationAnimation(startAngle, endAngle float64, startTime, endTime string) fcp.Param {
return fcp.Param{
Name: "rotation",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: startTime, Value: fmt.Sprintf("%f", startAngle), Curve: "linear"},
{Time: endTime, Value: fmt.Sprintf("%f", endAngle), Curve: "smooth"},
},
},
}
}These parameters support both interp and curve attributes:
<param name="opacity">
<keyframe time="0s" value="1" interp="linear" curve="smooth"/>
<keyframe time="86399313/24000s" value="0.5" interp="easeOut" curve="linear"/>
</param>
<param name="volume">
<keyframe time="0s" value="0dB" interp="linear" curve="smooth"/>
<keyframe time="86399313/24000s" value="-6dB" interp="easeIn" curve="linear"/>
</param>-
linear: Constant rate of change -
easeIn: Slow start, fast finish -
easeOut: Fast start, slow finish -
easeInOut: Slow start and finish, fast middle - Default:
linearif not specified
func createOpacityAnimation(startOpacity, endOpacity float64, startTime, endTime string) fcp.Param {
return fcp.Param{
Name: "opacity",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{
Time: startTime,
Value: fmt.Sprintf("%f", startOpacity),
Interp: "linear",
Curve: "smooth",
},
{
Time: endTime,
Value: fmt.Sprintf("%f", endOpacity),
Interp: "easeOut",
Curve: "linear",
},
},
},
}
}Color parameters require special attention for interpolation:
<param name="color">
<keyframe time="0s" value="1 0 0 1" interp="linear" curve="smooth"/>
<keyframe time="86399313/24000s" value="0 1 0 1" interp="linear" curve="smooth"/>
</param>- Colors interpolate in RGB space, not HSV
- Alpha channel interpolates separately
- Color values must be 0.0 to 1.0 range
- Invalid color values cause rendering failures
func createColorAnimation(startColor, endColor string, startTime, endTime string) fcp.Param {
return fcp.Param{
Name: "color",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{
Time: startTime,
Value: startColor, // "1 0 0 1" (RGBA)
Interp: "linear",
Curve: "smooth",
},
{
Time: endTime,
Value: endColor, // "0 1 0 1" (RGBA)
Interp: "linear",
Curve: "smooth",
},
},
},
}
}func validateKeyframes(param fcp.Param) error {
if param.KeyframeAnimation == nil {
return nil
}
for _, keyframe := range param.KeyframeAnimation.Keyframes {
switch param.Name {
case "position":
if keyframe.Interp != "" || keyframe.Curve != "" {
return fmt.Errorf("position keyframes don't support interp/curve attributes")
}
case "scale", "rotation", "anchor":
if keyframe.Interp != "" {
return fmt.Errorf("%s keyframes don't support interp attribute", param.Name)
}
if keyframe.Curve != "" && !isValidCurve(keyframe.Curve) {
return fmt.Errorf("invalid curve value: %s", keyframe.Curve)
}
case "opacity", "volume":
if keyframe.Interp != "" && !isValidInterp(keyframe.Interp) {
return fmt.Errorf("invalid interp value: %s", keyframe.Interp)
}
if keyframe.Curve != "" && !isValidCurve(keyframe.Curve) {
return fmt.Errorf("invalid curve value: %s", keyframe.Curve)
}
}
}
return nil
}
func isValidCurve(curve string) bool {
validCurves := []string{"linear", "smooth", "hold"}
for _, valid := range validCurves {
if curve == valid {
return true
}
}
return false
}
func isValidInterp(interp string) bool {
validInterps := []string{"linear", "easeIn", "easeOut", "easeInOut"}
for _, valid := range validInterps {
if interp == valid {
return true
}
}
return false
}ONLY use verified effect UIDs from samples/ directory:
-
Gaussian Blur:
FFGaussianBlur -
Motion Blur:
FFMotionBlur -
Radial Blur:
FFRadialBlur -
Zoom Blur:
FFZoomBlur -
Color Correction:
FFColorCorrection -
Saturation:
FFSaturation -
Levels:
FFLevels -
Curves:
FFCurves -
Hue/Saturation:
FFHueSaturation -
Brightness:
FFBrightness -
Contrast:
FFContrast -
Gamma:
FFGamma -
Sharpen:
FFSharpen -
Unsharp Mask:
FFUnsharpMask
-
Gain:
FFAudioGain -
EQ:
FFAudioEQ -
Compressor:
FFAudioCompressor -
Limiter:
FFAudioLimiter -
Gate:
FFAudioGate -
DeEsser:
FFAudioDeEsser
-
Vivid Generator:
.../Generators.localized/Solids.localized/Vivid.localized/Vivid.motn -
Text Title:
.../Titles.localized/Basic Text.localized/Text.localized/Text.moti -
Lower Third:
.../Titles.localized/Basic Text.localized/Lower Third.localized/Lower Third.moti -
Animated Title:
.../Titles.localized/Animated.localized/Animated.localized/Animated.moti
-
Shape Mask:
FFSuperEllipseMask -
Rectangle Mask:
FFRectangleMask -
Circle Mask:
FFCircleMask -
Polygon Mask:
FFPolygonMask
Creating fictional UIDs causes "invalid effect ID" crashes:
❌ BAD: uid := "com.example.customeffect"
❌ BAD: uid := ".../Effects/MyCustomEffect.motn"
❌ BAD: uid := "user.defined.blur"
❌ BAD: uid := "/Library/Effects/CustomBlur.plugin"
❌ BAD: uid := "CustomEffect_" + generateUID()Built-in elements are always safe and crash-free:
// Spatial transformations - always safe
video.AdjustTransform = &fcp.AdjustTransform{
Position: "100 50",
Scale: "1.5 1.5",
Params: []fcp.Param{
{Name: "rotation", Value: "45"},
{Name: "anchor", Value: "0.5 0.5"},
},
}
// Cropping - always safe
assetClip.AdjustCrop = &fcp.AdjustCrop{
Mode: "trim",
TrimRect: &fcp.TrimRect{
Left: "0.1",
Right: "0.9",
Top: "0.1",
Bottom: "0.9",
},
}
// Only use verified effects
video.FilterVideos = []fcp.FilterVideo{{
Ref: "verified-gaussian-blur-id",
Name: "Blur",
Params: []fcp.Param{{Name: "amount", Value: "5"}},
}}var verifiedEffectUIDs = map[string]bool{
"FFGaussianBlur": true,
"FFMotionBlur": true,
"FFColorCorrection": true,
"FFSaturation": true,
"FFLevels": true,
"FFCurves": true,
"FFHueSaturation": true,
"FFBrightness": true,
"FFContrast": true,
"FFGamma": true,
"FFSharpen": true,
"FFUnsharpMask": true,
"FFAudioGain": true,
"FFAudioEQ": true,
"FFAudioCompressor": true,
"FFAudioLimiter": true,
"FFAudioGate": true,
"FFAudioDeEsser": true,
"FFSuperEllipseMask": true,
"FFRectangleMask": true,
"FFCircleMask": true,
"FFPolygonMask": true,
}
func validateEffectUID(uid string) error {
if verifiedEffectUIDs[uid] {
return nil
}
// Check Motion template paths
if strings.Contains(uid, ".motn") || strings.Contains(uid, ".moti") {
return validateMotionTemplate(uid)
}
return fmt.Errorf("unverified effect UID: %s", uid)
}
func validateMotionTemplate(uid string) error {
// Verify the path structure matches Apple's conventions
requiredPatterns := []string{
".localized/",
"/Motion Templates.localized/",
}
for _, pattern := range requiredPatterns {
if !strings.Contains(uid, pattern) {
return fmt.Errorf("invalid Motion template path: %s", uid)
}
}
return nil
}func createBasicTransformAnimation(duration string) *fcp.AdjustTransform {
startTime := "0s"
endTime := duration
return &fcp.AdjustTransform{
Params: []fcp.Param{
// Position animation (no attributes)
{
Name: "position",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: startTime, Value: "0 0"},
{Time: endTime, Value: "200 100"},
},
},
},
// Scale animation (curve only)
{
Name: "scale",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: startTime, Value: "1 1", Curve: "linear"},
{Time: endTime, Value: "1.5 1.5", Curve: "smooth"},
},
},
},
// Rotation animation (curve only)
{
Name: "rotation",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: startTime, Value: "0", Curve: "linear"},
{Time: endTime, Value: "45", Curve: "smooth"},
},
},
},
},
}
}func createComplexAnimation(duration string) []fcp.Param {
midTime := fcp.ConvertSecondsToFCPDuration(
fcp.ParseFCPDuration(duration) / 2.0,
)
return []fcp.Param{
// Three-point position animation
{
Name: "position",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: "0s", Value: "-100 -50"},
{Time: midTime, Value: "0 0"},
{Time: duration, Value: "100 50"},
},
},
},
// Scale with easing
{
Name: "scale",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: "0s", Value: "0.5 0.5", Curve: "smooth"},
{Time: midTime, Value: "1.2 1.2", Curve: "smooth"},
{Time: duration, Value: "1 1", Curve: "linear"},
},
},
},
// Opacity fade
{
Name: "opacity",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: "0s", Value: "0", Interp: "easeIn", Curve: "smooth"},
{Time: midTime, Value: "1", Interp: "linear", Curve: "smooth"},
{Time: duration, Value: "0.8", Interp: "easeOut", Curve: "linear"},
},
},
},
}
}func createVideoFilter(effectUID, name string, params map[string]string) fcp.FilterVideo {
filter := fcp.FilterVideo{
Ref: effectUID,
Name: name,
}
for paramName, value := range params {
filter.Params = append(filter.Params, fcp.Param{
Name: paramName,
Value: value,
})
}
return filter
}
// Example usage:
blurFilter := createVideoFilter("FFGaussianBlur", "Blur", map[string]string{
"amount": "5.0",
})
colorFilter := createVideoFilter("FFColorCorrection", "Color", map[string]string{
"saturation": "1.2",
"exposure": "0.5",
"shadows": "0.1",
"highlights": "-0.1",
})func createAudioFilter(effectUID, name string, params map[string]string) fcp.FilterAudio {
filter := fcp.FilterAudio{
Ref: effectUID,
Name: name,
}
for paramName, value := range params {
filter.Params = append(filter.Params, fcp.Param{
Name: paramName,
Value: value,
})
}
return filter
}
// Example usage:
eqFilter := createAudioFilter("FFAudioEQ", "EQ", map[string]string{
"low-freq": "100",
"low-gain": "2",
"mid-freq": "1000",
"mid-gain": "0",
"high-freq": "10000",
"high-gain": "-1",
})func calculateKeyframeTimes(totalDuration string, keyframeCount int) []string {
total := fcp.ParseFCPDuration(totalDuration)
times := make([]string, keyframeCount)
for i := 0; i < keyframeCount; i++ {
progress := float64(i) / float64(keyframeCount-1)
time := total * progress
times[i] = fcp.ConvertSecondsToFCPDuration(time)
}
return times
}
func createEaseInOutCurve(startValue, endValue float64, times []string) []fcp.Keyframe {
keyframes := make([]fcp.Keyframe, len(times))
for i, time := range times {
progress := float64(i) / float64(len(times)-1)
// Ease-in-out curve calculation
easedProgress := progress
if progress < 0.5 {
easedProgress = 2 * progress * progress
} else {
easedProgress = 1 - 2*(1-progress)*(1-progress)
}
value := startValue + (endValue-startValue)*easedProgress
keyframes[i] = fcp.Keyframe{
Time: time,
Value: fmt.Sprintf("%f", value),
Interp: "smooth",
Curve: "smooth",
}
}
return keyframes
}Images support only simple transforms and basic effects:
func createImageAnimation(imageVideo *fcp.Video, duration string) {
// ✅ SAFE: Simple position and scale
imageVideo.AdjustTransform = &fcp.AdjustTransform{
Params: []fcp.Param{
{
Name: "position",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: "0s", Value: "0 0"},
{Time: duration, Value: "100 50"},
},
},
},
{
Name: "scale",
KeyframeAnimation: &fcp.KeyframeAnimation{
Keyframes: []fcp.Keyframe{
{Time: "0s", Value: "1 1", Curve: "linear"},
{Time: duration, Value: "1.2 1.2", Curve: "smooth"},
},
},
},
},
}
// ❌ AVOID: Complex effects that require temporal processing
// imageVideo.FilterVideos = []fcp.FilterVideo{{Ref: "FFMotionBlur"}}
}Videos support complete animation and effect capabilities:
func createVideoAnimation(assetClip *fcp.AssetClip, duration string) {
// ✅ FULL SUPPORT: All animations work
assetClip.AdjustTransform = createComplexAnimation(duration)
// ✅ FULL SUPPORT: All effects work
assetClip.FilterVideos = []fcp.FilterVideo{
createVideoFilter("FFGaussianBlur", "Blur", map[string]string{"amount": "3"}),
createVideoFilter("FFColorCorrection", "Color", map[string]string{"saturation": "1.2"}),
}
}This comprehensive guide ensures proper animation and effect implementation while avoiding the common pitfalls that cause FCP import failures.