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() ?? 1f; float amplitude = @params["amplitude"]?.ToObject() ?? 1f; bool loop = @params["loop"]?.ToObject() ?? 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(), offsetArray[1].ToObject(), offsetArray[2].ToObject() ); } string dir = Path.GetDirectoryName(clipPath)?.Replace('\\', '/'); if (!string.IsNullOrEmpty(dir) && !AssetDatabase.IsValidFolder(dir)) CreateFoldersRecursive(dir); var existing = AssetDatabase.LoadAssetAtPath(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); } /// /// 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). /// 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); } } }