BaekRyang ecf3e81101 Refactor step easing into independent snapSteps property
- EasingFunction에서 step 옵션을 제거하고 독립적인 snapSteps 속성으로 분리
- AnimationLoop에 snapSteps 기반의 움직임 양자화 로직 구현
- 에디터 파라미터 패널에 움직임 단계 조절 슬라이더 추가
- 기본 설정값에 SNAP_STEPS 추가 및 패키지 버전 업데이트 (1.2.10)
2026-02-25 16:14:46 +09:00

122 lines
4.0 KiB
TypeScript

import { applyEasing } from '../utils/easing';
import { presetToVector, isRotationPreset } from '../utils/motionPresets';
import type {DistortionArea, Point} from "../types";
/**
* 애니메이션 루프 관리 클래스
*/
export class AnimationLoop {
/**
* 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트
* @param areas 왜곡 영역 배열
* @returns 업데이트된 영역 배열
*/
public static updateAreaDragVectors(
areas: DistortionArea[]
): DistortionArea[] {
return areas.map((area) => {
const { progress, movement } = area;
// duration이 0이거나 프리셋이 'none'이면 애니메이션 없음
if (movement.duration <= 0 || movement.preset === 'none') {
return {
...area,
dragVector: { x: 0, y: 0 },
};
}
// 프리셋이 설정되어 있으면 프리셋 기반 벡터 사용
let baseVector: Point;
if (movement.preset) {
const strength = movement.strength ?? 0.1;
baseVector = presetToVector(movement.preset, strength);
} else {
// 프리셋 없으면 기존 vectorA 사용 (하위 호환성)
baseVector = movement.vectorA;
}
// 이징 적용
const easedProgress = applyEasing(progress, movement.easing);
// 벡터 계산
let dragVector: Point;
// 스텝 양자화 (영역 속성에서 가져옴)
const snapSteps = area.snapSteps ?? 0;
// 회전 프리셋인 경우 원운동
if (movement.preset && isRotationPreset(movement.preset)) {
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === 'rotate-cw' ? 1 : -1;
if (snapSteps > 0) {
// 스텝 양자화: 각도를 이산적 단계로 양자화
const totalAngleSteps = snapSteps * 4;
const rawAngle = progress * Math.PI * 2;
const quantizedAngle = Math.round(rawAngle / (Math.PI * 2) * totalAngleSteps) / totalAngleSteps * Math.PI * 2;
dragVector = {
x: Math.cos(quantizedAngle * direction) * radius,
y: Math.sin(quantizedAngle * direction) * radius,
};
} else {
const angle = easedProgress * Math.PI * 2;
dragVector = {
x: Math.cos(angle * direction) * radius,
y: Math.sin(angle * direction) * radius,
};
}
} else {
// 일반 왕복 모션 (sin 기반으로 진짜 좌↔우/상↔하 왕복)
// sin(0)=0 → sin(π/2)=1 → sin(π)=0 → sin(3π/2)=-1 → sin(2π)=0
if (snapSteps > 0) {
// 스텝 양자화: oscillation 출력을 양자화
// Math.round(sin * N) / N → 한 방향 N단계, 전체 2N+1개 위치
const oscillation = Math.sin(progress * Math.PI * 2);
const quantized = Math.round(oscillation * snapSteps) / snapSteps;
dragVector = {
x: baseVector.x * quantized,
y: baseVector.y * quantized,
};
} else {
const oscillation = Math.sin(easedProgress * Math.PI * 2);
dragVector = {
x: baseVector.x * oscillation,
y: baseVector.y * oscillation,
};
}
}
return {
...area,
dragVector,
};
});
}
/**
* 모든 영역의 진행도를 델타 타임만큼 업데이트
* @param areas 왜곡 영역 배열
* @param deltaTime 델타 타임 (초)
* @returns 업데이트된 영역 배열
*/
public static updateProgress(
areas: DistortionArea[],
deltaTime: number
): DistortionArea[] {
return areas.map((area) => {
// duration이 0이면 progress 업데이트 안 함
if (area.movement.duration <= 0) {
return area;
}
let newProgress = area.progress + deltaTime / area.movement.duration;
newProgress %= 1.0; // 루프
return {
...area,
progress: newProgress,
};
});
}
}