using System;
namespace EyeTracking
{
///
/// Handles smooth blink transitions with configurable lerp duration
/// to create natural-looking eye opening and closing animations
///
public class BlinkLerp
{
private float _blinkLerpDuration;
private float _blinkStartTime;
private float _blinkEndTime;
private bool _isBlinkLerping;
private bool _isBlinkOpening; // true when opening, false when closing
private bool _wasBlinking;
// Per-eye state for winking (each eye lerps independently)
private bool _wasLeftBlinking;
private bool _wasRightBlinking;
private float _leftBlinkStartTime;
private float _rightBlinkStartTime;
private bool _leftBlinkLerping;
private bool _rightBlinkLerping;
private bool _leftBlinkOpening;
private bool _rightBlinkOpening;
///
/// Initializes a new instance of the BlinkLerp class
///
/// Duration in seconds for blink lerping (default: 0.05f)
public BlinkLerp(float blinkLerpDuration = 0.05f)
{
_blinkLerpDuration = blinkLerpDuration;
_wasBlinking = false;
_isBlinkLerping = false;
_isBlinkOpening = false;
_wasLeftBlinking = false;
_wasRightBlinking = false;
_leftBlinkLerping = false;
_rightBlinkLerping = false;
_leftBlinkOpening = false;
_rightBlinkOpening = false;
}
///
/// Updates the blink lerp state and returns the current eye closed amount
///
/// Whether the current frame is detected as blinking
/// Current time in seconds
/// Base closed amount from eye tracking data (0-1)
/// Lerped eye closed amount (0-1)
public float UpdateBlinkLerp(bool isCurrentFrameBlinking, float currentTime, float baseClosedAmount = 0.0f)
{
return UpdateOneEye(
isCurrentFrameBlinking,
currentTime,
baseClosedAmount,
ref _wasBlinking,
ref _blinkStartTime,
ref _isBlinkLerping,
ref _isBlinkOpening);
}
///
/// Updates the blink lerp state and returns separate left and right eye closed amounts.
/// Use this overload when both eyes share the same blink state (e.g. full blink only).
///
public (float leftClosedAmount, float rightClosedAmount) UpdateBlinkLerp(
bool isCurrentFrameBlinking,
float currentTime,
float leftBaseClosedAmount,
float rightBaseClosedAmount)
{
float closedAmount = UpdateBlinkLerp(
isCurrentFrameBlinking,
currentTime,
(leftBaseClosedAmount + rightBaseClosedAmount) * 0.5f);
return (closedAmount, closedAmount);
}
///
/// Updates the blink lerp state per eye and returns separate left and right eye closed amounts.
/// Use this overload when eyes can blink or wink independently (e.g. VRCFT with winking).
/// When an eye is animating from closed to open, the other eye (if not fully open) is made to match it so full blinks stay in sync.
///
public (float leftClosedAmount, float rightClosedAmount) UpdateBlinkLerpPerEye(
bool isLeftBlinking,
bool isRightBlinking,
float currentTime,
float leftBaseClosedAmount = 0.0f,
float rightBaseClosedAmount = 0.0f)
{
float leftClosedAmount = UpdateOneEye(
isLeftBlinking,
currentTime,
leftBaseClosedAmount,
ref _wasLeftBlinking,
ref _leftBlinkStartTime,
ref _leftBlinkLerping,
ref _leftBlinkOpening);
float rightClosedAmount = UpdateOneEye(
isRightBlinking,
currentTime,
rightBaseClosedAmount,
ref _wasRightBlinking,
ref _rightBlinkStartTime,
ref _rightBlinkLerping,
ref _rightBlinkOpening);
// When an eye is animating from closed to open, keep the other eye in sync if it's not fully open (fixes full-blink desync)
const float fullyOpenEpsilon = 0.0001f;
bool leftOpening = _leftBlinkOpening && _leftBlinkLerping;
bool rightOpening = _rightBlinkOpening && _rightBlinkLerping;
if (leftOpening && rightOpening)
{
// Both opening: use the more-open value so both match and we don't slow the faster one
float syncAmount = Math.Min(leftClosedAmount, rightClosedAmount);
leftClosedAmount = rightClosedAmount = syncAmount;
}
else if (leftOpening && rightClosedAmount > fullyOpenEpsilon)
{
rightClosedAmount = leftClosedAmount;
}
else if (rightOpening && leftClosedAmount > fullyOpenEpsilon)
{
leftClosedAmount = rightClosedAmount;
}
return (leftClosedAmount, rightClosedAmount);
}
private float UpdateOneEye(
bool isCurrentFrameBlinking,
float currentTime,
float baseClosedAmount,
ref bool wasBlinking,
ref float blinkStartTime,
ref bool isLerping,
ref bool isOpening)
{
bool transitioningOutOfBlink = wasBlinking && !isCurrentFrameBlinking;
bool transitioningIntoBlink = !wasBlinking && isCurrentFrameBlinking;
if (transitioningIntoBlink)
{
blinkStartTime = currentTime;
isLerping = true;
isOpening = false;
}
else if (transitioningOutOfBlink)
{
blinkStartTime = currentTime;
isLerping = true;
isOpening = true;
}
float closedAmount = baseClosedAmount;
if (isLerping)
{
float elapsedTime = currentTime - blinkStartTime;
float lerpProgress = Math.Min(elapsedTime / _blinkLerpDuration, 1.0f);
if (isOpening)
closedAmount = 1.0f - lerpProgress;
else
closedAmount = lerpProgress;
if (lerpProgress >= 1.0f)
isLerping = false;
}
else if (isCurrentFrameBlinking)
{
closedAmount = 1.0f;
}
wasBlinking = isCurrentFrameBlinking;
return Math.Clamp(closedAmount, 0.0f, 1.0f);
}
///
/// Resets the blink lerp state
///
public void Reset()
{
_wasBlinking = false;
_isBlinkLerping = false;
_isBlinkOpening = false;
_wasLeftBlinking = false;
_wasRightBlinking = false;
_leftBlinkLerping = false;
_rightBlinkLerping = false;
_leftBlinkOpening = false;
_rightBlinkOpening = false;
}
///
/// Gets or sets the blink lerp duration in seconds
///
public float BlinkLerpDuration
{
get => _blinkLerpDuration;
set => _blinkLerpDuration = Math.Max(0.001f, value); // Ensure positive duration
}
///
/// Gets whether the blink lerp is currently active (either combined or per-eye)
///
public bool IsLerping => _isBlinkLerping || _leftBlinkLerping || _rightBlinkLerping;
///
/// Gets whether the eyes are currently opening (true) or closing (false).
/// True if either combined or per-eye opening is in progress.
///
public bool IsOpening => _isBlinkOpening || _leftBlinkOpening || _rightBlinkOpening;
}
}