using System;
using System.Collections.Generic;
namespace Cthangover.Live2D.Cubism.Framework
{
///
/// Abstract base for all Live2D Cubism motions (animations and expressions).
/// Порт ACubismMotion из Cubism SDK for Native 5-r.5: владеет временами фейда,
/// весом, цикличностью и колбэками; очередь ()
/// вызывает раз в кадр, тот в свою очередь —
/// (при старте),
/// и виртуальный .
///
/// Завершение выставляется по EndTime: сам моушен ставит IsFinished в
/// DoUpdateParameters (незацикленный), а обёртка
/// после применения параметров закрывает запись, когда текущее время прошло EndTime
/// (например, после запущенного через очередь fade-out). Колбэки при этом не
/// дублируются: OnFinishedMotion вызывается только самим моушеном.
///
public abstract class ACubismMotion
{
///
/// Callback invoked when the motion finishes (non-looping only).
/// The finished motion is passed as the argument.
///
public delegate void FinishedMotionCallback(ACubismMotion motion);
///
/// Callback invoked when the motion begins playback.
/// Called from right after
/// the entry's times are configured.
///
public delegate void BeganMotionCallback(ACubismMotion motion);
protected float _fadeInSeconds = -1f;
protected float _fadeOutSeconds = -1f;
protected float _weight = 1f;
protected float _offsetSeconds;
protected bool _isLoop;
protected bool _isLoopFadeIn = true;
protected bool _previousLoopState;
/// Сработавшие события (UserData); наполняется в .
protected readonly List _firedEventValues = new();
///
/// Duration of the fade-in transition in seconds.
/// A negative value means the default (1 second) will be used
/// by and .
///
public float FadeInSeconds { get => _fadeInSeconds; set => _fadeInSeconds = value; }
///
/// Duration of the fade-out transition in seconds.
/// A negative value means the default (1 second) will be used.
///
public float FadeOutSeconds { get => _fadeOutSeconds; set => _fadeOutSeconds = value; }
///
/// Overall motion weight (0..1). Multiplied with the fade-in/fade-out
/// factors in to produce the final blend weight.
///
public float Weight { get => _weight; set => _weight = value; }
///
/// Time offset in seconds applied to the motion's start time.
/// Allows starting playback from an arbitrary point.
///
public float OffsetSeconds { get => _offsetSeconds; set => _offsetSeconds = value; }
///
/// Whether the motion loops. When enabled,
/// wraps timeOffset back to the beginning after the duration elapses.
///
public bool IsLoop { get => _isLoop; set => _isLoop = value; }
///
/// When true and looping, the motion performs a fade-in at each loop
/// restart. When false, the fade-in is skipped on subsequent loops.
///
public bool IsLoopFadeIn { get => _isLoopFadeIn; set => _isLoopFadeIn = value; }
///
/// Invoked when the motion finishes (non-looping only).
/// Автоматически вызывается самим моушеном в DoUpdateParameters по достижении
/// длительности; очередь этот колбэк не дублирует.
///
public FinishedMotionCallback OnFinishedMotion { get; set; }
///
/// Invoked when the motion begins. Fires at the start of
/// after the entry is initialized.
///
public BeganMotionCallback OnBeganMotion { get; set; }
///
/// Returns the total motion duration in seconds.
/// Override in to return the motion3.json
/// duration, or -1 for looping motions.
///
public virtual float GetDuration() => -1f;
///
/// Returns the loop cycle duration in seconds.
/// Override in to return the full duration
/// even when looping is active.
///
public virtual float GetLoopDuration() => -1f;
///
/// Порт ACubismMotion::UpdateParameters — кадровая обёртка, вызываемая очередью.
/// Проверяет доступность записи, инициализирует её при старте, считает fade-вес,
/// применяет параметры и после этого закрывает запись, если время прошло EndTime.
///
public void UpdateParameters(CubismNativeModel model, CubismMotionQueueEntry entry, float userTimeSeconds)
{
if (!entry.IsAvailable || entry.IsFinished)
{
return;
}
SetupMotionQueueEntry(entry, userTimeSeconds);
float fadeWeight = UpdateFadeWeight(entry, userTimeSeconds);
DoUpdateParameters(model, userTimeSeconds, fadeWeight, entry);
// Пост-проверка SDK: EndTime > 0 и уже пройден — запись завершена.
if ((entry.EndTime > 0f) && (entry.EndTime < userTimeSeconds))
{
entry.IsFinished = true;
}
}
///
/// Порт ACubismMotion::SetupMotionQueueEntry: при первом кадре записывает
/// StartTime (с учётом OffsetSeconds), FadeInStartTime и EndTime
/// (если EndTime ещё не выставлен извне, например фейдаутом от очереди).
///
public void SetupMotionQueueEntry(CubismMotionQueueEntry entry, float userTimeSeconds)
{
if (!entry.IsAvailable || entry.IsFinished)
{
return;
}
if (entry.IsStarted)
{
return;
}
entry.IsStarted = true;
entry.StartTime = userTimeSeconds - _offsetSeconds;
entry.FadeInStartTime = userTimeSeconds;
// EndTime мог быть выставлен заранее (SetFadeout до старта).
if (entry.EndTime < 0f)
{
AdjustEndTime(entry);
}
OnBeganMotion?.Invoke(this);
}
///
/// Порт ACubismMotion::UpdateFadeWeight: вес = Weight x fadeIn x fadeOut
/// с синус-эйзингом. При fadeIn/fadeOut == 0 соответствующий множитель = 1.
///
public float UpdateFadeWeight(CubismMotionQueueEntry entry, float userTimeSeconds)
{
if (entry == null)
{
return -1f;
}
float fadeWeight = _weight;
float fadeIn = _fadeInSeconds == 0f
? 1f
: CubismMath.GetEasingSine((userTimeSeconds - entry.FadeInStartTime) / _fadeInSeconds);
float fadeOut = (_fadeOutSeconds == 0f || entry.EndTime < 0f)
? 1f
: CubismMath.GetEasingSine((entry.EndTime - userTimeSeconds) / _fadeOutSeconds);
fadeWeight = fadeWeight * fadeIn * fadeOut;
entry.SetState(userTimeSeconds, fadeWeight);
return fadeWeight;
}
///
/// Порт ACubismMotion::AdjustEndTime: EndTime = StartTime + GetDuration();
/// для цикличных моушенов (GetDuration() <= 0) EndTime = -1 (бесконечность).
///
protected void AdjustEndTime(CubismMotionQueueEntry entry)
{
float duration = GetDuration();
float endTime = duration <= 0f ? -1f : entry.StartTime + duration;
entry.EndTime = endTime;
}
///
/// Порт ACubismMotion::GetFiredEvent: базовая реализация не содержит событий.
/// переопределяет и наполняет список по UserData.
///
public virtual List GetFiredEvent(float beforeCheckTimeSeconds, float motionTimeSeconds)
{
return _firedEventValues;
}
///
/// Applies motion parameter values to the model for the current frame.
/// Called every frame by .
///
/// The native Cubism model to modify.
/// Elapsed user time in seconds.
/// Blend weight already multiplied by fade factors.
/// The queue entry containing start/end timing.
public abstract void DoUpdateParameters(
CubismNativeModel model,
float userTimeSeconds,
float weight,
CubismMotionQueueEntry entry);
}
}