using System;
namespace Cthangover.Live2D.Cubism.Framework
{
///
/// Smooth target-point tracking for face/eye movement parameters.
/// Drives the ParamAngleX/Y and ParamEyeBallX/Y model parameters in
/// .
///
/// Uses a velocity-based smoothing approach: each update computes a
/// desired velocity toward the target, applies acceleration limits,
/// enforces a maximum safe approach speed to prevent overshoot near
/// the target, and integrates velocity into position.
///
/// The tracking is frame-rate independent — delta time is scaled by
/// a factor of 30 (matching Cubism's internal 30fps reference).
/// Acceleration and max velocity are clamped per Cubism SDK conventions
/// (FaceParamMaxV = 40, FrameToMaxSpeed = 4.5).
///
public class CubismTargetPoint
{
private const float FaceParamMaxV = 40f;
private const float FrameToMaxSpeed = 4.5f;
private float _faceTargetX, _faceTargetY;
private float _faceX, _faceY;
private float _faceVX, _faceVY;
private float _lastTimeSeconds;
private float _userTimeSeconds;
/// Current smoothed X position.
public float X => _faceX;
/// Current smoothed Y position.
public float Y => _faceY;
///
/// Sets the target point immediately. The actual position
/// will smoothly approach this target over subsequent calls to .
///
public void Set(float x, float y)
{
_faceTargetX = x;
_faceTargetY = y;
}
///
/// Advances the smoothing simulation by one frame.
/// Computes delta-time-weighted velocity toward target, applies
/// acceleration clamping and safe approach speed limiting, then
/// integrates position.
///
/// Called each frame from
/// before injecting face-tracking parameters.
///
public void Update(float deltaTimeSeconds)
{
_userTimeSeconds += deltaTimeSeconds;
var deltaTimeWeight = (_userTimeSeconds - _lastTimeSeconds) * 30f;
_lastTimeSeconds = _userTimeSeconds;
if (deltaTimeWeight <= 0f) return;
var maxV = FaceParamMaxV / 30f;
var maxA = deltaTimeWeight * maxV / FrameToMaxSpeed;
var dx = _faceTargetX - _faceX;
var dy = _faceTargetY - _faceY;
var distance = MathF.Sqrt(dx * dx + dy * dy);
if (distance < 0.01f) return;
var vx = maxV * dx / distance;
var vy = maxV * dy / distance;
var ax = vx - _faceVX;
var ay = vy - _faceVY;
var aLen = MathF.Sqrt(ax * ax + ay * ay);
if (aLen > 0f)
{
var clampedA = MathF.Min(aLen, maxA);
ax = ax * clampedA / aLen;
ay = ay * clampedA / aLen;
}
var maxSafeV = 0.5f * (MathF.Sqrt(maxA * maxA + 8f * maxA * distance) - maxA);
var vLen = MathF.Sqrt(_faceVX * _faceVX + _faceVY * _faceVY);
if (vLen > maxSafeV && vLen > 0f)
{
var scale = maxSafeV / vLen;
_faceVX *= scale;
_faceVY *= scale;
}
_faceVX += ax;
_faceVY += ay;
_faceX += _faceVX;
_faceY += _faceVY;
}
}
}