using System;
using System.IO;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Tools.Animation
{
internal static class ClipPresets
{
private static readonly string[] ValidPresets = { "bounce", "rotate", "pulse", "fade", "shake", "hover", "spin", "sway", "bob", "wiggle", "blink", "slide_in", "elastic", "grow", "shrink" };
public static object CreatePreset(JObject @params)
{
string clipPath = @params["clipPath"]?.ToString();
if (string.IsNullOrEmpty(clipPath))
return new { success = false, message = "'clipPath' is required (e.g. 'Assets/Animations/Bounce.anim')" };
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
if (clipPath == null)
return new { success = false, message = "Invalid asset path" };
if (!clipPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase))
clipPath += ".anim";
string preset = @params["preset"]?.ToString()?.ToLowerInvariant();
if (string.IsNullOrEmpty(preset))
return new { success = false, message = $"'preset' is required. Valid: {string.Join(", ", ValidPresets)}" };
float duration = @params["duration"]?.ToObject<float>() ?? 1f;
float amplitude = @params["amplitude"]?.ToObject<float>() ?? 1f;
bool loop = @params["loop"]?.ToObject<bool>() ?? true;
// Resolve position offset from target GameObject or explicit offset parameter.
// localPosition rather than absolute origin, preventing objects from jumping to (0,0,0).
Vector3 offset = Vector3.zero;
var targetToken = @params["target"];
if (targetToken != null && targetToken.Type != JTokenType.Null)
{
string searchMethod = @params["searchMethod"]?.ToString();
var go = ObjectResolver.ResolveGameObject(targetToken, searchMethod);
if (go != null)
offset = go.transform.localPosition;
}
var offsetToken = @params["offset"];
if (offsetToken is JArray offsetArray && offsetArray.Count >= 3)
{
offset = new Vector3(
offsetArray[0].ToObject<float>(),
offsetArray[1].ToObject<float>(),
offsetArray[2].ToObject<float>()
);
}
string dir = Path.GetDirectoryName(clipPath)?.Replace('\\', '/');
if (!string.IsNullOrEmpty(dir) && !AssetDatabase.IsValidFolder(dir))
CreateFoldersRecursive(dir);
var existing = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
if (existing != null)
return new { success = false, message = $"AnimationClip already exists at '{clipPath}'. Delete it first or use a different path." };
var clip = new AnimationClip();
clip.name = Path.GetFileNameWithoutExtension(clipPath);
clip.frameRate = 60f;
var settings = AnimationUtility.GetAnimationClipSettings(clip);
settings.loopTime = loop;
settings.stopTime = duration;
AnimationUtility.SetAnimationClipSettings(clip, settings);
switch (preset)
{
case "bounce":
ApplyBounce(clip, duration, amplitude, offset);
break;
case "rotate":
ApplyRotate(clip, duration, amplitude);
break;
case "pulse":
ApplyPulse(clip, duration, amplitude);
break;
case "fade":
ApplyFade(clip, duration);
break;
case "shake":
ApplyShake(clip, duration, amplitude, offset);
break;
case "hover":
ApplyHover(clip, duration, amplitude, offset);
break;
case "spin":
ApplySpin(clip, duration, amplitude);
break;
case "sway":
ApplySway(clip, duration, amplitude);
break;
case "bob":
ApplyBob(clip, duration, amplitude, offset);
break;
case "wiggle":
ApplyWiggle(clip, duration, amplitude);
break;
case "blink":
ApplyBlink(clip, duration);
break;
case "slide_in":
ApplySlideIn(clip, duration, amplitude, offset);
break;
case "elastic":
ApplyElastic(clip, duration, amplitude);
break;
case "grow":
ApplyGrow(clip, duration, amplitude);
break;
case "shrink":
ApplyShrink(clip, duration, amplitude);
break;
default:
return new { success = false, message = $"Unknown preset '{preset}'. Valid: {string.Join(", ", ValidPresets)}" };
}
AssetDatabase.CreateAsset(clip, clipPath);
AssetDatabase.SaveAssets();
return new
{
success = true,
message = $"Created '{preset}' preset clip at '{clipPath}'" + (offset != Vector3.zero ? $" (offset: {offset})" : ""),
data = new
{
path = clipPath,
name = clip.name,
preset,
duration,
amplitude,
isLooping = loop,
offset = new { x = offset.x, y = offset.y, z = offset.z },
curveCount = AnimationUtility.GetCurveBindings(clip).Length
}
};
}
private static void ApplyBounce(AnimationClip clip, float duration, float amplitude, Vector3 offset)
{
// localPosition.y sine wave oscillation, offset by target's current position
float half = duration * 0.5f;
var curve = new AnimationCurve(
new Keyframe(0f, offset.y),
new Keyframe(half * 0.5f, offset.y + amplitude),
new Keyframe(half, offset.y),
new Keyframe(half + half * 0.5f, offset.y + amplitude),
new Keyframe(duration, offset.y)
);
SetTransformCurve(clip, "localPosition.y", curve);
}
private static void ApplyRotate(AnimationClip clip, float duration, float amplitude)
{
// localEulerAngles.y full 360 rotation (amplitude acts as multiplier)
var curve = new AnimationCurve(
new Keyframe(0f, 0f),
new Keyframe(duration, 360f * amplitude)
);
// Linear tangents for smooth rotation
var keys = curve.keys;
keys[0].outTangent = 360f * amplitude / duration;
keys[1].inTangent = 360f * amplitude / duration;
curve.keys = keys;
SetTransformCurve(clip, "localEulerAngles.y", curve);
}
private static void ApplyPulse(AnimationClip clip, float duration, float amplitude)
{
// localScale uniform scale up/down
float peak = 1f + amplitude * 0.5f;
float half = duration * 0.5f;
var curve = new AnimationCurve(
new Keyframe(0f, 1f),
new Keyframe(half, peak),
new Keyframe(duration, 1f)
);
SetTransformCurve(clip, "localScale.x", curve);
SetTransformCurve(clip, "localScale.y", curve);
SetTransformCurve(clip, "localScale.z", curve);
}
private static void ApplyFade(AnimationClip clip, float duration)
{
// CanvasGroup alpha 1 -> 0
var curve = new AnimationCurve(
new Keyframe(0f, 1f),
new Keyframe(duration, 0f)
);
var binding = EditorCurveBinding.FloatCurve("", typeof(CanvasGroup), "m_Alpha");
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
private static void ApplyShake(AnimationClip clip, float duration, float amplitude, Vector3 offset)
{
// localPosition.x/z oscillation simulating shake, centered on target's current position
int steps = 8;
float stepTime = duration / steps;
var xKeys = new Keyframe[steps + 1];
var zKeys = new Keyframe[steps + 1];
for (int i = 0; i <= steps; i++)
{
float t = i * stepTime;
float decay = 1f - (float)i / steps;
// Alternating direction with decay
float sign = (i % 2 == 0) ? 1f : -1f;
xKeys[i] = new Keyframe(t, offset.x + sign * amplitude * decay);
zKeys[i] = new Keyframe(t, offset.z - sign * amplitude * 0.5f * decay);
}
// End at offset position
xKeys[steps] = new Keyframe(duration, offset.x);
zKeys[steps] = new Keyframe(duration, offset.z);
SetTransformCurve(clip, "localPosition.x", new AnimationCurve(xKeys));
SetTransformCurve(clip, "localPosition.z", new AnimationCurve(zKeys));
}
private static void ApplyHover(AnimationClip clip, float duration, float amplitude, Vector3 offset)
{
// localPosition.y gentle sine wave, offset by target's current position
float q = duration * 0.25f;
var curve = new AnimationCurve(
new Keyframe(0f, offset.y),
new Keyframe(q, offset.y + amplitude * 0.5f),
new Keyframe(q * 2f, offset.y),
new Keyframe(q * 3f, offset.y - amplitude * 0.5f),
new Keyframe(duration, offset.y)
);
SetTransformCurve(clip, "localPosition.y", curve);
}
private static void ApplySpin(AnimationClip clip, float duration, float amplitude)
{
// localEulerAngles.z continuous rotation
var curve = new AnimationCurve(
new Keyframe(0f, 0f),
new Keyframe(duration, 360f * amplitude)
);
var keys = curve.keys;
keys[0].outTangent = 360f * amplitude / duration;
keys[1].inTangent = 360f * amplitude / duration;
curve.keys = keys;
SetTransformCurve(clip, "localEulerAngles.z", curve);
}
private static void ApplySway(AnimationClip clip, float duration, float amplitude)
{
// localEulerAngles.z gentle side-to-side rotation (sine wave)
float q = duration * 0.25f;
var curve = new AnimationCurve(
new Keyframe(0f, 0f),
new Keyframe(q, amplitude),
new Keyframe(q * 2f, 0f),
new Keyframe(q * 3f, -amplitude),
new Keyframe(duration, 0f)
);
SetTransformCurve(clip, "localEulerAngles.z", curve);
}
private static void ApplyBob(AnimationClip clip, float duration, float amplitude, Vector3 offset)
{
// localPosition.z gentle forward/back movement, offset by target's current position
float q = duration * 0.25f;
var curve = new AnimationCurve(
new Keyframe(0f, offset.z),
new Keyframe(q, offset.z + amplitude * 0.5f),
new Keyframe(q * 2f, offset.z),
new Keyframe(q * 3f, offset.z - amplitude * 0.5f),
new Keyframe(duration, offset.z)
);
SetTransformCurve(clip, "localPosition.z", curve);
}
private static void ApplyWiggle(AnimationClip clip, float duration, float amplitude)
{
// localEulerAngles.z rapid oscillation (similar to shake but rotation)
int steps = 8;
float stepTime = duration / steps;
var keys = new Keyframe[steps + 1];
for (int i = 0; i <= steps; i++)
{
float t = i * stepTime;
float decay = 1f - (float)i / steps;
float sign = (i % 2 == 0) ? 1f : -1f;
keys[i] = new Keyframe(t, sign * amplitude * decay);
}
keys[steps] = new Keyframe(duration, 0f);
SetTransformCurve(clip, "localEulerAngles.z", new AnimationCurve(keys));
}
private static void ApplyBlink(AnimationClip clip, float duration)
{
// localScale uniform scale to near-zero and back
float mid = duration * 0.5f;
var curve = new AnimationCurve(
new Keyframe(0f, 1f),
new Keyframe(mid, 0.05f),
new Keyframe(duration, 1f)
);
SetTransformCurve(clip, "localScale.x", curve);
SetTransformCurve(clip, "localScale.y", curve);
SetTransformCurve(clip, "localScale.z", curve);
}
private static void ApplySlideIn(AnimationClip clip, float duration, float amplitude, Vector3 offset)
{
// localPosition.x slide from offset-amplitude to offset (linear)
var curve = new AnimationCurve(
new Keyframe(0f, offset.x - amplitude),
new Keyframe(duration, offset.x)
);
// Set linear tangents for smooth slide
var keys = curve.keys;
keys[0].outTangent = amplitude / duration;
keys[1].inTangent = amplitude / duration;
curve.keys = keys;
SetTransformCurve(clip, "localPosition.x", curve);
}
private static void ApplyElastic(AnimationClip clip, float duration, float amplitude)
{
// localScale uniform with overshoot effect
float third = duration / 3f;
float peak = 1f + amplitude * 1.2f;
float settle = 1f + amplitude * 0.8f;
var curve = new AnimationCurve(
new Keyframe(0f, 1f),
new Keyframe(third, peak),
new Keyframe(third * 2f, settle),
new Keyframe(duration, 1f)
);
SetTransformCurve(clip, "localScale.x", curve);
SetTransformCurve(clip, "localScale.y", curve);
SetTransformCurve(clip, "localScale.z", curve);
}
private static void ApplyGrow(AnimationClip clip, float duration, float amplitude)
{
// localScale uniform from a reduced value up to 1.0
float clamped = Mathf.Max(0f, amplitude);
float start = Mathf.Clamp01(1f - clamped);
var curve = new AnimationCurve(
new Keyframe(0f, start),
new Keyframe(duration, 1f)
);
SetTransformCurve(clip, "localScale.x", curve);
SetTransformCurve(clip, "localScale.y", curve);
SetTransformCurve(clip, "localScale.z", curve);
}
private static void ApplyShrink(AnimationClip clip, float duration, float amplitude)
{
// localScale uniform from 1.0 down to a reduced value
float clamped = Mathf.Max(0f, amplitude);
float end = Mathf.Clamp01(1f - clamped);
var curve = new AnimationCurve(
new Keyframe(0f, 1f),
new Keyframe(duration, end)
);
SetTransformCurve(clip, "localScale.x", curve);
SetTransformCurve(clip, "localScale.y", curve);
SetTransformCurve(clip, "localScale.z", curve);
}
/// <summary>
/// Sets an animation curve on a Transform property using AnimationUtility.SetEditorCurve
/// instead of clip.SetCurve to avoid marking the clip as legacy. Legacy clips cannot be
/// used with Mecanim AnimatorControllers, and legacy Animation components take control of
/// the entire Vector3 property (zeroing non-animated axes).
/// </summary>
private static void SetTransformCurve(AnimationClip clip, string propertyName, AnimationCurve curve)
{
var binding = EditorCurveBinding.FloatCurve("", typeof(Transform), propertyName);
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
private static void CreateFoldersRecursive(string folderPath)
{
if (AssetDatabase.IsValidFolder(folderPath))
return;
string parent = Path.GetDirectoryName(folderPath)?.Replace('\\', '/');
if (!string.IsNullOrEmpty(parent) && parent != "Assets" && !AssetDatabase.IsValidFolder(parent))
CreateFoldersRecursive(parent);
string folderName = Path.GetFileName(folderPath);
if (!string.IsNullOrEmpty(parent) && !string.IsNullOrEmpty(folderName))
AssetDatabase.CreateFolder(parent, folderName);
}
}
}