feat: Add motion presets for distortion animations

- 왜곡 애니메이션에 사용할 수 있는 다양한 모션 프리셋(horizontal, vertical, rotate-cw 등)을 추가했습니다.
- `DistortionMovement` 인터페이스에 `preset`과 `strength` 옵션을 추가하여 모션 프리셋을 설정할 수 있도록 변경했습니다.
- `presetToVector` 함수와 `isRotationPreset` 함수를 추가하여 모션 프리셋 로직을 구현했습니다.
- `AnimationLoop` 클래스에서 모션 프리셋을 적용하여 `vectorA`를 계산하도록 수정했습니다.
This commit is contained in:
BaekRyang 2025-11-24 13:41:36 +09:00
parent bbbb49aa1d
commit f6ad8b11b0
10 changed files with 275 additions and 44 deletions

26
dist/index.d.mts vendored
View File

@ -12,18 +12,26 @@ interface Point {
* *
*/ */
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad'; type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad';
/**
*
*/
type MotionPreset = 'none' | 'horizontal' | 'vertical' | 'rotate-cw' | 'rotate-ccw' | 'pulse' | 'diagonal-1' | 'diagonal-2';
/** /**
* *
*/ */
interface DistortionMovement { interface DistortionMovement {
/** 왜곡 시작 벡터 */ /** 모션 프리셋 (vectorA, vectorB 대신 사용) */
preset?: MotionPreset;
/** 왜곡 시작 벡터 (preset 없을 때 사용) */
vectorA: Point; vectorA: Point;
/** 왜곡 종료 벡터 */ /** 왜곡 종료 벡터 (preset 없을 때 사용, 현재는 미사용) */
vectorB: Point; vectorB: Point;
/** 애니메이션 지속 시간 (초) */ /** 애니메이션 지속 시간 (초) */
duration: number; duration: number;
/** 적용할 이징 함수 */ /** 적용할 이징 함수 */
easing: EasingFunction; easing: EasingFunction;
/** 모션 강도 (프리셋 적용 시 벡터 크기 조절용, 기본값: 0.1) */
strength?: number;
} }
/** /**
* *
@ -376,6 +384,18 @@ declare const DEFAULT_AREA: {
}; };
}; };
/**
*
* @param preset
* @param strength (기본값: 0.1)
* @returns (vectorA)
*/
declare function presetToVector(preset: MotionPreset, strength?: number): Point;
/**
*
*/
declare function isRotationPreset(preset?: MotionPreset): boolean;
/** /**
* Three.js * Three.js
*/ */
@ -539,4 +559,4 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
reset: () => void; reset: () => void;
}; };
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type MouseInteractionConfig, type MouseState, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity }; export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type MotionPreset, type MouseInteractionConfig, type MouseState, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, isRotationPreset, presetToVector, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity };

26
dist/index.d.ts vendored
View File

@ -12,18 +12,26 @@ interface Point {
* *
*/ */
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad'; type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad';
/**
*
*/
type MotionPreset = 'none' | 'horizontal' | 'vertical' | 'rotate-cw' | 'rotate-ccw' | 'pulse' | 'diagonal-1' | 'diagonal-2';
/** /**
* *
*/ */
interface DistortionMovement { interface DistortionMovement {
/** 왜곡 시작 벡터 */ /** 모션 프리셋 (vectorA, vectorB 대신 사용) */
preset?: MotionPreset;
/** 왜곡 시작 벡터 (preset 없을 때 사용) */
vectorA: Point; vectorA: Point;
/** 왜곡 종료 벡터 */ /** 왜곡 종료 벡터 (preset 없을 때 사용, 현재는 미사용) */
vectorB: Point; vectorB: Point;
/** 애니메이션 지속 시간 (초) */ /** 애니메이션 지속 시간 (초) */
duration: number; duration: number;
/** 적용할 이징 함수 */ /** 적용할 이징 함수 */
easing: EasingFunction; easing: EasingFunction;
/** 모션 강도 (프리셋 적용 시 벡터 크기 조절용, 기본값: 0.1) */
strength?: number;
} }
/** /**
* *
@ -376,6 +384,18 @@ declare const DEFAULT_AREA: {
}; };
}; };
/**
*
* @param preset
* @param strength (기본값: 0.1)
* @returns (vectorA)
*/
declare function presetToVector(preset: MotionPreset, strength?: number): Point;
/**
*
*/
declare function isRotationPreset(preset?: MotionPreset): boolean;
/** /**
* Three.js * Three.js
*/ */
@ -539,4 +559,4 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
reset: () => void; reset: () => void;
}; };
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type MouseInteractionConfig, type MouseState, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity }; export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type MotionPreset, type MouseInteractionConfig, type MouseState, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, isRotationPreset, presetToVector, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity };

59
dist/index.js vendored
View File

@ -40,6 +40,8 @@ __export(index_exports, {
SpringPhysics: () => SpringPhysics, SpringPhysics: () => SpringPhysics,
ThreeScene: () => ThreeScene, ThreeScene: () => ThreeScene,
applyEasing: () => applyEasing, applyEasing: () => applyEasing,
isRotationPreset: () => isRotationPreset,
presetToVector: () => presetToVector,
useAnimationFrame: () => useAnimationFrame, useAnimationFrame: () => useAnimationFrame,
useDistortionEditor: () => useDistortionEditor, useDistortionEditor: () => useDistortionEditor,
useMouseInteraction: () => useMouseInteraction, useMouseInteraction: () => useMouseInteraction,
@ -244,6 +246,34 @@ var applyEasing = (progress, easingType) => {
return easingFunctions[easingType](clampedProgress); return easingFunctions[easingType](clampedProgress);
}; };
// src/utils/motionPresets.ts
function presetToVector(preset, strength = 0.1) {
switch (preset) {
case "none":
return { x: 0, y: 0 };
case "horizontal":
return { x: strength, y: 0 };
case "vertical":
return { x: 0, y: strength };
case "rotate-cw":
return { x: strength, y: 0 };
case "rotate-ccw":
return { x: -strength, y: 0 };
case "pulse":
return { x: strength, y: strength };
case "diagonal-1":
return { x: strength * 0.707, y: strength * 0.707 };
// √2/2 ≈ 0.707
case "diagonal-2":
return { x: strength * 0.707, y: -strength * 0.707 };
default:
return { x: 0, y: 0 };
}
}
function isRotationPreset(preset) {
return preset === "rotate-cw" || preset === "rotate-ccw";
}
// src/engine/AnimationLoop.ts // src/engine/AnimationLoop.ts
var AnimationLoop = class { var AnimationLoop = class {
/** /**
@ -254,27 +284,44 @@ var AnimationLoop = class {
static updateAreaDragVectors(areas) { static updateAreaDragVectors(areas) {
return areas.map((area) => { return areas.map((area) => {
const { progress, movement } = area; const { progress, movement } = area;
if (movement.duration <= 0) { if (movement.duration <= 0 || movement.preset === "none") {
return { return {
...area, ...area,
dragVector: { x: 0, y: 0 } dragVector: { x: 0, y: 0 }
}; };
} }
let baseVector;
if (movement.preset) {
const strength = movement.strength ?? 0.1;
baseVector = presetToVector(movement.preset, strength);
} else {
baseVector = movement.vectorA;
}
const easedProgress = applyEasing(progress, movement.easing); const easedProgress = applyEasing(progress, movement.easing);
let dragVector; let dragVector;
if (movement.preset && isRotationPreset(movement.preset)) {
const angle = easedProgress * Math.PI * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === "rotate-cw" ? 1 : -1;
dragVector = {
x: Math.cos(angle * direction) * radius,
y: Math.sin(angle * direction) * radius
};
} else {
if (easedProgress < 0.5) { if (easedProgress < 0.5) {
const t = easedProgress * 2; const t = easedProgress * 2;
dragVector = { dragVector = {
x: movement.vectorA.x * t, x: baseVector.x * t,
y: movement.vectorA.y * t y: baseVector.y * t
}; };
} else { } else {
const t = (easedProgress - 0.5) * 2; const t = (easedProgress - 0.5) * 2;
dragVector = { dragVector = {
x: movement.vectorA.x * (1 - t), x: baseVector.x * (1 - t),
y: movement.vectorA.y * (1 - t) y: baseVector.y * (1 - t)
}; };
} }
}
return { return {
...area, ...area,
dragVector dragVector
@ -1683,6 +1730,8 @@ var DEFAULT_EDITOR_CANVAS_STYLE = {
SpringPhysics, SpringPhysics,
ThreeScene, ThreeScene,
applyEasing, applyEasing,
isRotationPreset,
presetToVector,
useAnimationFrame, useAnimationFrame,
useDistortionEditor, useDistortionEditor,
useMouseInteraction, useMouseInteraction,

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

57
dist/index.mjs vendored
View File

@ -195,6 +195,34 @@ var applyEasing = (progress, easingType) => {
return easingFunctions[easingType](clampedProgress); return easingFunctions[easingType](clampedProgress);
}; };
// src/utils/motionPresets.ts
function presetToVector(preset, strength = 0.1) {
switch (preset) {
case "none":
return { x: 0, y: 0 };
case "horizontal":
return { x: strength, y: 0 };
case "vertical":
return { x: 0, y: strength };
case "rotate-cw":
return { x: strength, y: 0 };
case "rotate-ccw":
return { x: -strength, y: 0 };
case "pulse":
return { x: strength, y: strength };
case "diagonal-1":
return { x: strength * 0.707, y: strength * 0.707 };
// √2/2 ≈ 0.707
case "diagonal-2":
return { x: strength * 0.707, y: -strength * 0.707 };
default:
return { x: 0, y: 0 };
}
}
function isRotationPreset(preset) {
return preset === "rotate-cw" || preset === "rotate-ccw";
}
// src/engine/AnimationLoop.ts // src/engine/AnimationLoop.ts
var AnimationLoop = class { var AnimationLoop = class {
/** /**
@ -205,27 +233,44 @@ var AnimationLoop = class {
static updateAreaDragVectors(areas) { static updateAreaDragVectors(areas) {
return areas.map((area) => { return areas.map((area) => {
const { progress, movement } = area; const { progress, movement } = area;
if (movement.duration <= 0) { if (movement.duration <= 0 || movement.preset === "none") {
return { return {
...area, ...area,
dragVector: { x: 0, y: 0 } dragVector: { x: 0, y: 0 }
}; };
} }
let baseVector;
if (movement.preset) {
const strength = movement.strength ?? 0.1;
baseVector = presetToVector(movement.preset, strength);
} else {
baseVector = movement.vectorA;
}
const easedProgress = applyEasing(progress, movement.easing); const easedProgress = applyEasing(progress, movement.easing);
let dragVector; let dragVector;
if (movement.preset && isRotationPreset(movement.preset)) {
const angle = easedProgress * Math.PI * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === "rotate-cw" ? 1 : -1;
dragVector = {
x: Math.cos(angle * direction) * radius,
y: Math.sin(angle * direction) * radius
};
} else {
if (easedProgress < 0.5) { if (easedProgress < 0.5) {
const t = easedProgress * 2; const t = easedProgress * 2;
dragVector = { dragVector = {
x: movement.vectorA.x * t, x: baseVector.x * t,
y: movement.vectorA.y * t y: baseVector.y * t
}; };
} else { } else {
const t = (easedProgress - 0.5) * 2; const t = (easedProgress - 0.5) * 2;
dragVector = { dragVector = {
x: movement.vectorA.x * (1 - t), x: baseVector.x * (1 - t),
y: movement.vectorA.y * (1 - t) y: baseVector.y * (1 - t)
}; };
} }
}
return { return {
...area, ...area,
dragVector dragVector
@ -1633,6 +1678,8 @@ export {
SpringPhysics, SpringPhysics,
ThreeScene, ThreeScene,
applyEasing, applyEasing,
isRotationPreset,
presetToVector,
useAnimationFrame, useAnimationFrame,
useDistortionEditor, useDistortionEditor,
useMouseInteraction, useMouseInteraction,

2
dist/index.mjs.map vendored

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import { applyEasing } from '../utils/easing'; import { applyEasing } from '../utils/easing';
import { presetToVector, isRotationPreset } from '../utils/motionPresets';
import type {DistortionArea, Point} from "../types"; import type {DistortionArea, Point} from "../types";
/** /**
@ -16,35 +17,57 @@ export class AnimationLoop {
return areas.map((area) => { return areas.map((area) => {
const { progress, movement } = area; const { progress, movement } = area;
// duration이 0이면 애니메이션 없음 (dragVector를 0으로 유지) // duration이 0이거나 프리셋이 'none'이면 애니메이션 없음
if (movement.duration <= 0) { if (movement.duration <= 0 || movement.preset === 'none') {
return { return {
...area, ...area,
dragVector: { x: 0, y: 0 }, 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); const easedProgress = applyEasing(progress, movement.easing);
// 벡터 간 보간 // 벡터 계산
let dragVector: Point; let dragVector: Point;
if (easedProgress < 0.5) { // 회전 프리셋인 경우 원운동
// 0.0 -> 0.5: 0에서 vectorA로 보간 if (movement.preset && isRotationPreset(movement.preset)) {
const t = easedProgress * 2; const angle = easedProgress * Math.PI * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === 'rotate-cw' ? 1 : -1;
dragVector = { dragVector = {
x: movement.vectorA.x * t, x: Math.cos(angle * direction) * radius,
y: movement.vectorA.y * t, y: Math.sin(angle * direction) * radius,
}; };
} else { } else {
// 0.5 -> 1.0: vectorA에서 0으로 보간 // 일반 왕복 모션
if (easedProgress < 0.5) {
// 0.0 -> 0.5: 0에서 baseVector로 보간
const t = easedProgress * 2;
dragVector = {
x: baseVector.x * t,
y: baseVector.y * t,
};
} else {
// 0.5 -> 1.0: baseVector에서 0으로 보간
const t = (easedProgress - 0.5) * 2; const t = (easedProgress - 0.5) * 2;
dragVector = { dragVector = {
x: movement.vectorA.x * (1 - t), x: baseVector.x * (1 - t),
y: movement.vectorA.y * (1 - t), y: baseVector.y * (1 - t),
}; };
} }
}
return { return {
...area, ...area,

View File

@ -11,6 +11,7 @@ export { useDistortionEditor } from './editor';
export type { export type {
Point, Point,
EasingFunction, EasingFunction,
MotionPreset,
DistortionMovement, DistortionMovement,
DistortionArea, DistortionArea,
AreaBounds, AreaBounds,
@ -31,6 +32,7 @@ export type {
// 유틸리티 함수 // 유틸리티 함수
export { applyEasing } from './utils/easing'; export { applyEasing } from './utils/easing';
export { SHADER_CONFIG, ANIMATION_CONFIG, DEFAULT_AREA } from './utils/constants'; export { SHADER_CONFIG, ANIMATION_CONFIG, DEFAULT_AREA } from './utils/constants';
export { presetToVector, isRotationPreset } from './utils/motionPresets';
// 엔진 클래스 (고급 사용자용) // 엔진 클래스 (고급 사용자용)
export { ThreeScene } from './engine/ThreeScene'; export { ThreeScene } from './engine/ThreeScene';

View File

@ -17,18 +17,35 @@ export type EasingFunction =
| 'easeInQuad' | 'easeInQuad'
| 'easeOutQuad'; | 'easeOutQuad';
/**
*
*/
export type MotionPreset =
| 'none' // 없음 (애니메이션 없음)
| 'horizontal' // 좌우 왕복
| 'vertical' // 상하 왕복
| 'rotate-cw' // 시계방향 회전
| 'rotate-ccw' // 반시계방향 회전
| 'pulse' // 펄스 (확대/축소)
| 'diagonal-1' // 대각선 (좌상→우하)
| 'diagonal-2'; // 대각선 (우상→좌하)
/** /**
* *
*/ */
export interface DistortionMovement { export interface DistortionMovement {
/** 왜곡 시작 벡터 */ /** 모션 프리셋 (vectorA, vectorB 대신 사용) */
preset?: MotionPreset;
/** 왜곡 시작 벡터 (preset 없을 때 사용) */
vectorA: Point; vectorA: Point;
/** 왜곡 종료 벡터 */ /** 왜곡 종료 벡터 (preset 없을 때 사용, 현재는 미사용) */
vectorB: Point; vectorB: Point;
/** 애니메이션 지속 시간 (초) */ /** 애니메이션 지속 시간 (초) */
duration: number; duration: number;
/** 적용할 이징 함수 */ /** 적용할 이징 함수 */
easing: EasingFunction; easing: EasingFunction;
/** 모션 강도 (프리셋 적용 시 벡터 크기 조절용, 기본값: 0.1) */
strength?: number;
} }
/** /**

View File

@ -0,0 +1,53 @@
import type {MotionPreset, Point} from '../types';
/**
*
* @param preset
* @param strength (기본값: 0.1)
* @returns (vectorA)
*/
export function presetToVector(preset: MotionPreset, strength: number = 0.1): Point {
switch (preset) {
case 'none':
// 애니메이션 없음
return {x: 0, y: 0};
case 'horizontal':
// 좌우 왕복
return {x: strength, y: 0};
case 'vertical':
// 상하 왕복
return {x: 0, y: strength};
case 'rotate-cw':
// 시계방향 회전 (원운동의 시작점)
return {x: strength, y: 0};
case 'rotate-ccw':
// 반시계방향 회전 (원운동의 시작점)
return {x: -strength, y: 0};
case 'pulse':
// 펄스 (중심에서 바깥으로)
return {x: strength, y: strength};
case 'diagonal-1':
// 대각선 (좌상→우하)
return {x: strength * 0.707, y: strength * 0.707}; // √2/2 ≈ 0.707
case 'diagonal-2':
// 대각선 (우상→좌하)
return {x: strength * 0.707, y: -strength * 0.707};
default:
return {x: 0, y: 0};
}
}
/**
*
*/
export function isRotationPreset(preset?: MotionPreset): boolean {
return preset === 'rotate-cw' || preset === 'rotate-ccw';
}