Compare commits
No commits in common. "e08f34caab8684e0b866fc6f63e9efb3d98889e4" and "c18f3fffb59dd8b3c4ce8d07edee4f0c90f909b0" have entirely different histories.
e08f34caab
...
c18f3fffb5
2
.gitignore
vendored
2
.gitignore
vendored
@ -14,4 +14,4 @@ yarn-error.log*
|
||||
nul
|
||||
|
||||
# Demo (템플릿 파일, 실제 데모는 별도 저장소)
|
||||
/demo.npmrc
|
||||
/demo
|
||||
17
dist/distortion.frag.glsl
vendored
17
dist/distortion.frag.glsl
vendored
@ -74,21 +74,12 @@ void main() {
|
||||
// dragVector는 정규화된 좌표(0-1)이므로 바로 사용
|
||||
vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i];
|
||||
texCoord += distortion;
|
||||
// clamp를 루프 내에서 제거: 모든 왜곡을 완전히 누적한 후 마지막에 한 번만 clamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 경계 근처에서 부드럽게 페이드 아웃
|
||||
// 텍스처 좌표가 0~1 범위를 벗어나면 알파값을 줄여서 자연스럽게 처리
|
||||
vec2 edgeDist = min(texCoord, 1.0 - texCoord);
|
||||
float edgeFade = smoothstep(0.0, 0.05, min(edgeDist.x, edgeDist.y));
|
||||
|
||||
// 범위를 벗어난 좌표는 fract로 래핑하여 반복 효과 (더 자연스러움)
|
||||
vec2 wrappedCoord = fract(texCoord);
|
||||
|
||||
vec4 color = texture2D(u_texture, wrappedCoord);
|
||||
// 경계에서 페이드 아웃 적용
|
||||
color.a *= edgeFade;
|
||||
|
||||
gl_FragColor = color;
|
||||
// 모든 왜곡을 누적한 후 최종적으로 한 번만 clamp
|
||||
texCoord = clamp(texCoord, 0.0, 1.0);
|
||||
gl_FragColor = texture2D(u_texture, texCoord);
|
||||
}
|
||||
36
dist/index.d.mts
vendored
36
dist/index.d.mts
vendored
@ -12,26 +12,18 @@ interface Point {
|
||||
* 애니메이션 이징 함수 타입
|
||||
*/
|
||||
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad';
|
||||
/**
|
||||
* 모션 프리셋 타입
|
||||
*/
|
||||
type MotionPreset = 'none' | 'horizontal' | 'vertical' | 'rotate-cw' | 'rotate-ccw' | 'pulse' | 'diagonal-1' | 'diagonal-2';
|
||||
/**
|
||||
* 왜곡 애니메이션 움직임 설정
|
||||
*/
|
||||
interface DistortionMovement {
|
||||
/** 모션 프리셋 (vectorA, vectorB 대신 사용) */
|
||||
preset?: MotionPreset;
|
||||
/** 왜곡 시작 벡터 (preset 없을 때 사용) */
|
||||
/** 왜곡 시작 벡터 */
|
||||
vectorA: Point;
|
||||
/** 왜곡 종료 벡터 (preset 없을 때 사용, 현재는 미사용) */
|
||||
/** 왜곡 종료 벡터 */
|
||||
vectorB: Point;
|
||||
/** 애니메이션 지속 시간 (초) */
|
||||
duration: number;
|
||||
/** 적용할 이징 함수 */
|
||||
easing: EasingFunction;
|
||||
/** 모션 강도 (프리셋 적용 시 벡터 크기 조절용, 기본값: 0.1) */
|
||||
strength?: number;
|
||||
}
|
||||
/**
|
||||
* 사각형 포인트와 애니메이션 설정을 포함하는 왜곡 영역
|
||||
@ -49,14 +41,6 @@ interface DistortionArea {
|
||||
progress: number;
|
||||
/** 현재 드래그 벡터 (progress로부터 계산됨) */
|
||||
dragVector: Point;
|
||||
/** 영역별 물리 설정 (선택사항, 마우스 인터랙션 시 사용) */
|
||||
physics?: {
|
||||
stiffness: number;
|
||||
damping: number;
|
||||
mass: number;
|
||||
influenceRadius: number;
|
||||
maxStrength: number;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 영역 충돌 감지를 위한 경계 상자
|
||||
@ -392,18 +376,6 @@ 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 씬 관리 클래스
|
||||
*/
|
||||
@ -565,8 +537,6 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
|
||||
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];
|
||||
updateConfig: (newConfig: Partial<MouseInteractionConfig>) => void;
|
||||
reset: () => void;
|
||||
isDragging: () => boolean;
|
||||
getInteractingAreaIndices: () => Set<number>;
|
||||
};
|
||||
|
||||
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 };
|
||||
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 };
|
||||
|
||||
36
dist/index.d.ts
vendored
36
dist/index.d.ts
vendored
@ -12,26 +12,18 @@ interface Point {
|
||||
* 애니메이션 이징 함수 타입
|
||||
*/
|
||||
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad';
|
||||
/**
|
||||
* 모션 프리셋 타입
|
||||
*/
|
||||
type MotionPreset = 'none' | 'horizontal' | 'vertical' | 'rotate-cw' | 'rotate-ccw' | 'pulse' | 'diagonal-1' | 'diagonal-2';
|
||||
/**
|
||||
* 왜곡 애니메이션 움직임 설정
|
||||
*/
|
||||
interface DistortionMovement {
|
||||
/** 모션 프리셋 (vectorA, vectorB 대신 사용) */
|
||||
preset?: MotionPreset;
|
||||
/** 왜곡 시작 벡터 (preset 없을 때 사용) */
|
||||
/** 왜곡 시작 벡터 */
|
||||
vectorA: Point;
|
||||
/** 왜곡 종료 벡터 (preset 없을 때 사용, 현재는 미사용) */
|
||||
/** 왜곡 종료 벡터 */
|
||||
vectorB: Point;
|
||||
/** 애니메이션 지속 시간 (초) */
|
||||
duration: number;
|
||||
/** 적용할 이징 함수 */
|
||||
easing: EasingFunction;
|
||||
/** 모션 강도 (프리셋 적용 시 벡터 크기 조절용, 기본값: 0.1) */
|
||||
strength?: number;
|
||||
}
|
||||
/**
|
||||
* 사각형 포인트와 애니메이션 설정을 포함하는 왜곡 영역
|
||||
@ -49,14 +41,6 @@ interface DistortionArea {
|
||||
progress: number;
|
||||
/** 현재 드래그 벡터 (progress로부터 계산됨) */
|
||||
dragVector: Point;
|
||||
/** 영역별 물리 설정 (선택사항, 마우스 인터랙션 시 사용) */
|
||||
physics?: {
|
||||
stiffness: number;
|
||||
damping: number;
|
||||
mass: number;
|
||||
influenceRadius: number;
|
||||
maxStrength: number;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 영역 충돌 감지를 위한 경계 상자
|
||||
@ -392,18 +376,6 @@ 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 씬 관리 클래스
|
||||
*/
|
||||
@ -565,8 +537,6 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
|
||||
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];
|
||||
updateConfig: (newConfig: Partial<MouseInteractionConfig>) => void;
|
||||
reset: () => void;
|
||||
isDragging: () => boolean;
|
||||
getInteractingAreaIndices: () => Set<number>;
|
||||
};
|
||||
|
||||
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 };
|
||||
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 };
|
||||
|
||||
219
dist/index.js
vendored
219
dist/index.js
vendored
@ -40,8 +40,6 @@ __export(index_exports, {
|
||||
SpringPhysics: () => SpringPhysics,
|
||||
ThreeScene: () => ThreeScene,
|
||||
applyEasing: () => applyEasing,
|
||||
isRotationPreset: () => isRotationPreset,
|
||||
presetToVector: () => presetToVector,
|
||||
useAnimationFrame: () => useAnimationFrame,
|
||||
useDistortionEditor: () => useDistortionEditor,
|
||||
useMouseInteraction: () => useMouseInteraction,
|
||||
@ -246,34 +244,6 @@ var applyEasing = (progress, easingType) => {
|
||||
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
|
||||
var AnimationLoop = class {
|
||||
/**
|
||||
@ -284,44 +254,27 @@ var AnimationLoop = class {
|
||||
static updateAreaDragVectors(areas) {
|
||||
return areas.map((area) => {
|
||||
const { progress, movement } = area;
|
||||
if (movement.duration <= 0 || movement.preset === "none") {
|
||||
if (movement.duration <= 0) {
|
||||
return {
|
||||
...area,
|
||||
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);
|
||||
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) {
|
||||
const t = easedProgress * 2;
|
||||
dragVector = {
|
||||
x: baseVector.x * t,
|
||||
y: baseVector.y * t
|
||||
x: movement.vectorA.x * t,
|
||||
y: movement.vectorA.y * t
|
||||
};
|
||||
} else {
|
||||
const t = (easedProgress - 0.5) * 2;
|
||||
dragVector = {
|
||||
x: baseVector.x * (1 - t),
|
||||
y: baseVector.y * (1 - t)
|
||||
x: movement.vectorA.x * (1 - t),
|
||||
y: movement.vectorA.y * (1 - t)
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...area,
|
||||
dragVector
|
||||
@ -633,21 +586,14 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
const { getState } = useMouseVelocity(containerRef);
|
||||
const [interactingAreaIndices, setInteractingAreaIndices] = (0, import_react3.useState)(/* @__PURE__ */ new Set());
|
||||
const springPhysicsMapRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
|
||||
const getSpringPhysics = (0, import_react3.useCallback)((areaIndex, area) => {
|
||||
const getSpringPhysics = (0, import_react3.useCallback)((areaIndex) => {
|
||||
if (!springPhysicsMapRef.current.has(areaIndex)) {
|
||||
const physicsConfig = area?.physics || config.physics;
|
||||
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(physicsConfig));
|
||||
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
|
||||
}
|
||||
return springPhysicsMapRef.current.get(areaIndex);
|
||||
}, [config.physics]);
|
||||
const updateInteraction = (0, import_react3.useCallback)((areas, deltaTime) => {
|
||||
if (!config.enabled) return areas;
|
||||
areas.forEach((area, index) => {
|
||||
if (area.physics && springPhysicsMapRef.current.has(index)) {
|
||||
const spring = springPhysicsMapRef.current.get(index);
|
||||
spring.setConfig(area.physics);
|
||||
}
|
||||
});
|
||||
const mouseState = getState();
|
||||
if (mouseState.isDragging && mouseState.position) {
|
||||
const currentlyInAreas = /* @__PURE__ */ new Set();
|
||||
@ -655,13 +601,13 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
|
||||
currentlyInAreas.add(i);
|
||||
if (!interactingAreaIndices.has(i)) {
|
||||
getSpringPhysics(i, areas[i]).reset();
|
||||
getSpringPhysics(i).reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
interactingAreaIndices.forEach((areaIndex) => {
|
||||
if (!currentlyInAreas.has(areaIndex)) {
|
||||
getSpringPhysics(areaIndex, areas[areaIndex]).returnToEquilibrium();
|
||||
getSpringPhysics(areaIndex).returnToEquilibrium();
|
||||
}
|
||||
});
|
||||
setInteractingAreaIndices(currentlyInAreas);
|
||||
@ -680,7 +626,7 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
};
|
||||
}
|
||||
currentlyInAreas.forEach((areaIndex) => {
|
||||
const spring = getSpringPhysics(areaIndex, areas[areaIndex]);
|
||||
const spring = getSpringPhysics(areaIndex);
|
||||
if (velocityMag >= minVel) {
|
||||
spring.setTarget(clampedVelocity, velocityMult);
|
||||
} else {
|
||||
@ -703,7 +649,7 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
};
|
||||
}
|
||||
interactingAreaIndices.forEach((areaIndex) => {
|
||||
const spring = getSpringPhysics(areaIndex, areas[areaIndex]);
|
||||
const spring = getSpringPhysics(areaIndex);
|
||||
spring.setInitialVelocity(clampedVelocity, velocityMult);
|
||||
});
|
||||
setInteractingAreaIndices(/* @__PURE__ */ new Set());
|
||||
@ -746,19 +692,10 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
});
|
||||
setInteractingAreaIndices(/* @__PURE__ */ new Set());
|
||||
}, []);
|
||||
const isDragging = (0, import_react3.useCallback)(() => {
|
||||
const mouseState = getState();
|
||||
return mouseState.isDragging;
|
||||
}, [getState]);
|
||||
const getInteractingAreaIndices = (0, import_react3.useCallback)(() => {
|
||||
return interactingAreaIndices;
|
||||
}, [interactingAreaIndices]);
|
||||
return {
|
||||
updateInteraction,
|
||||
updateConfig,
|
||||
reset,
|
||||
isDragging,
|
||||
getInteractingAreaIndices
|
||||
reset
|
||||
};
|
||||
};
|
||||
|
||||
@ -933,20 +870,8 @@ var ImageDistortion = ({
|
||||
const animationCallback = (0, import_react4.useCallback)((deltaTime) => {
|
||||
if (!isReady) return;
|
||||
setCurrentAreas((prevAreas) => {
|
||||
const interactingIndices = mouseInteractionHook.getInteractingAreaIndices?.() || /* @__PURE__ */ new Set();
|
||||
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
if (interactingIndices.size > 0) {
|
||||
updatedAreas = updatedAreas.map((area, index) => {
|
||||
if (interactingIndices.has(index)) {
|
||||
return {
|
||||
...area,
|
||||
dragVector: { x: 0, y: 0 }
|
||||
};
|
||||
}
|
||||
return area;
|
||||
});
|
||||
}
|
||||
if (mouseInteraction?.enabled) {
|
||||
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||||
}
|
||||
@ -1065,6 +990,66 @@ var useDistortionEditor = (initialAreas = []) => {
|
||||
|
||||
// src/editor/components/EditorCanvas.tsx
|
||||
var import_react6 = require("react");
|
||||
|
||||
// src/editor/constants.ts
|
||||
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||
// 3단계 원 스타일 (외부 -> 내부)
|
||||
circleLevels: [
|
||||
{
|
||||
radius: 0.5,
|
||||
opacity: 0.3,
|
||||
lineWidth: 2,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
},
|
||||
{
|
||||
radius: 0.33,
|
||||
opacity: 0.6,
|
||||
lineWidth: 2.5,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
},
|
||||
{
|
||||
radius: 0.167,
|
||||
opacity: 0.9,
|
||||
lineWidth: 3,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
}
|
||||
],
|
||||
// 원 내부 채우기
|
||||
circleFillColor: "rgba(255, 200, 0, 0.08)",
|
||||
// 중심점
|
||||
centerPoint: {
|
||||
radius: 5,
|
||||
fillColor: "rgba(255, 200, 0, 1)",
|
||||
strokeColor: "rgba(255, 255, 255, 0.8)",
|
||||
strokeWidth: 2
|
||||
},
|
||||
// 포인트 핸들
|
||||
pointHandle: {
|
||||
size: 16,
|
||||
fillColor: "#00aaff",
|
||||
strokeColor: "white",
|
||||
strokeWidth: 2,
|
||||
labelColor: "#00aaff",
|
||||
labelFontSize: 11
|
||||
},
|
||||
// 영역 외곽선
|
||||
areaOutline: {
|
||||
selectedColor: "#00aaff",
|
||||
unselectedColor: "#888",
|
||||
selectedWidth: 2,
|
||||
unselectedWidth: 1,
|
||||
unselectedDashPattern: [5, 5],
|
||||
selectedFillColor: "rgba(0, 170, 255, 0.08)",
|
||||
// 선택된 영역 배경 (연한 파란색)
|
||||
unselectedFillColor: "rgba(136, 136, 136, 0.03)"
|
||||
// 선택 안된 영역 배경 (연한 회색)
|
||||
}
|
||||
};
|
||||
|
||||
// src/editor/components/EditorCanvas.tsx
|
||||
var import_jsx_runtime2 = require("react/jsx-runtime");
|
||||
var EditorCanvas = ({
|
||||
areas,
|
||||
@ -1688,64 +1673,6 @@ var DistortionEditor = ({
|
||||
] })
|
||||
] });
|
||||
};
|
||||
|
||||
// src/editor/constants.ts
|
||||
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||
// 3단계 원 스타일 (외부 -> 내부)
|
||||
circleLevels: [
|
||||
{
|
||||
radius: 0.5,
|
||||
opacity: 0.3,
|
||||
lineWidth: 2,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
},
|
||||
{
|
||||
radius: 0.33,
|
||||
opacity: 0.6,
|
||||
lineWidth: 2.5,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
},
|
||||
{
|
||||
radius: 0.167,
|
||||
opacity: 0.9,
|
||||
lineWidth: 3,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
}
|
||||
],
|
||||
// 원 내부 채우기
|
||||
circleFillColor: "rgba(255, 200, 0, 0.08)",
|
||||
// 중심점
|
||||
centerPoint: {
|
||||
radius: 5,
|
||||
fillColor: "rgba(255, 200, 0, 1)",
|
||||
strokeColor: "rgba(255, 255, 255, 0.8)",
|
||||
strokeWidth: 2
|
||||
},
|
||||
// 포인트 핸들
|
||||
pointHandle: {
|
||||
size: 16,
|
||||
fillColor: "#00aaff",
|
||||
strokeColor: "white",
|
||||
strokeWidth: 2,
|
||||
labelColor: "#00aaff",
|
||||
labelFontSize: 11
|
||||
},
|
||||
// 영역 외곽선
|
||||
areaOutline: {
|
||||
selectedColor: "#00aaff",
|
||||
unselectedColor: "#888",
|
||||
selectedWidth: 2,
|
||||
unselectedWidth: 1,
|
||||
unselectedDashPattern: [5, 5],
|
||||
selectedFillColor: "rgba(0, 170, 255, 0.08)",
|
||||
// 선택된 영역 배경 (연한 파란색)
|
||||
unselectedFillColor: "rgba(136, 136, 136, 0.03)"
|
||||
// 선택 안된 영역 배경 (연한 회색)
|
||||
}
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
ANIMATION_CONFIG,
|
||||
@ -1758,8 +1685,6 @@ var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||
SpringPhysics,
|
||||
ThreeScene,
|
||||
applyEasing,
|
||||
isRotationPreset,
|
||||
presetToVector,
|
||||
useAnimationFrame,
|
||||
useDistortionEditor,
|
||||
useMouseInteraction,
|
||||
|
||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
217
dist/index.mjs
vendored
217
dist/index.mjs
vendored
@ -195,34 +195,6 @@ var applyEasing = (progress, easingType) => {
|
||||
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
|
||||
var AnimationLoop = class {
|
||||
/**
|
||||
@ -233,44 +205,27 @@ var AnimationLoop = class {
|
||||
static updateAreaDragVectors(areas) {
|
||||
return areas.map((area) => {
|
||||
const { progress, movement } = area;
|
||||
if (movement.duration <= 0 || movement.preset === "none") {
|
||||
if (movement.duration <= 0) {
|
||||
return {
|
||||
...area,
|
||||
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);
|
||||
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) {
|
||||
const t = easedProgress * 2;
|
||||
dragVector = {
|
||||
x: baseVector.x * t,
|
||||
y: baseVector.y * t
|
||||
x: movement.vectorA.x * t,
|
||||
y: movement.vectorA.y * t
|
||||
};
|
||||
} else {
|
||||
const t = (easedProgress - 0.5) * 2;
|
||||
dragVector = {
|
||||
x: baseVector.x * (1 - t),
|
||||
y: baseVector.y * (1 - t)
|
||||
x: movement.vectorA.x * (1 - t),
|
||||
y: movement.vectorA.y * (1 - t)
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...area,
|
||||
dragVector
|
||||
@ -582,21 +537,14 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
const { getState } = useMouseVelocity(containerRef);
|
||||
const [interactingAreaIndices, setInteractingAreaIndices] = useState(/* @__PURE__ */ new Set());
|
||||
const springPhysicsMapRef = useRef3(/* @__PURE__ */ new Map());
|
||||
const getSpringPhysics = useCallback2((areaIndex, area) => {
|
||||
const getSpringPhysics = useCallback2((areaIndex) => {
|
||||
if (!springPhysicsMapRef.current.has(areaIndex)) {
|
||||
const physicsConfig = area?.physics || config.physics;
|
||||
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(physicsConfig));
|
||||
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
|
||||
}
|
||||
return springPhysicsMapRef.current.get(areaIndex);
|
||||
}, [config.physics]);
|
||||
const updateInteraction = useCallback2((areas, deltaTime) => {
|
||||
if (!config.enabled) return areas;
|
||||
areas.forEach((area, index) => {
|
||||
if (area.physics && springPhysicsMapRef.current.has(index)) {
|
||||
const spring = springPhysicsMapRef.current.get(index);
|
||||
spring.setConfig(area.physics);
|
||||
}
|
||||
});
|
||||
const mouseState = getState();
|
||||
if (mouseState.isDragging && mouseState.position) {
|
||||
const currentlyInAreas = /* @__PURE__ */ new Set();
|
||||
@ -604,13 +552,13 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
|
||||
currentlyInAreas.add(i);
|
||||
if (!interactingAreaIndices.has(i)) {
|
||||
getSpringPhysics(i, areas[i]).reset();
|
||||
getSpringPhysics(i).reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
interactingAreaIndices.forEach((areaIndex) => {
|
||||
if (!currentlyInAreas.has(areaIndex)) {
|
||||
getSpringPhysics(areaIndex, areas[areaIndex]).returnToEquilibrium();
|
||||
getSpringPhysics(areaIndex).returnToEquilibrium();
|
||||
}
|
||||
});
|
||||
setInteractingAreaIndices(currentlyInAreas);
|
||||
@ -629,7 +577,7 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
};
|
||||
}
|
||||
currentlyInAreas.forEach((areaIndex) => {
|
||||
const spring = getSpringPhysics(areaIndex, areas[areaIndex]);
|
||||
const spring = getSpringPhysics(areaIndex);
|
||||
if (velocityMag >= minVel) {
|
||||
spring.setTarget(clampedVelocity, velocityMult);
|
||||
} else {
|
||||
@ -652,7 +600,7 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
};
|
||||
}
|
||||
interactingAreaIndices.forEach((areaIndex) => {
|
||||
const spring = getSpringPhysics(areaIndex, areas[areaIndex]);
|
||||
const spring = getSpringPhysics(areaIndex);
|
||||
spring.setInitialVelocity(clampedVelocity, velocityMult);
|
||||
});
|
||||
setInteractingAreaIndices(/* @__PURE__ */ new Set());
|
||||
@ -695,19 +643,10 @@ var useMouseInteraction = (containerRef, config) => {
|
||||
});
|
||||
setInteractingAreaIndices(/* @__PURE__ */ new Set());
|
||||
}, []);
|
||||
const isDragging = useCallback2(() => {
|
||||
const mouseState = getState();
|
||||
return mouseState.isDragging;
|
||||
}, [getState]);
|
||||
const getInteractingAreaIndices = useCallback2(() => {
|
||||
return interactingAreaIndices;
|
||||
}, [interactingAreaIndices]);
|
||||
return {
|
||||
updateInteraction,
|
||||
updateConfig,
|
||||
reset,
|
||||
isDragging,
|
||||
getInteractingAreaIndices
|
||||
reset
|
||||
};
|
||||
};
|
||||
|
||||
@ -882,20 +821,8 @@ var ImageDistortion = ({
|
||||
const animationCallback = useCallback3((deltaTime) => {
|
||||
if (!isReady) return;
|
||||
setCurrentAreas((prevAreas) => {
|
||||
const interactingIndices = mouseInteractionHook.getInteractingAreaIndices?.() || /* @__PURE__ */ new Set();
|
||||
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
if (interactingIndices.size > 0) {
|
||||
updatedAreas = updatedAreas.map((area, index) => {
|
||||
if (interactingIndices.has(index)) {
|
||||
return {
|
||||
...area,
|
||||
dragVector: { x: 0, y: 0 }
|
||||
};
|
||||
}
|
||||
return area;
|
||||
});
|
||||
}
|
||||
if (mouseInteraction?.enabled) {
|
||||
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||||
}
|
||||
@ -1014,6 +941,66 @@ var useDistortionEditor = (initialAreas = []) => {
|
||||
|
||||
// src/editor/components/EditorCanvas.tsx
|
||||
import { useRef as useRef5, useEffect as useEffect4, useState as useState4, useCallback as useCallback5, useMemo } from "react";
|
||||
|
||||
// src/editor/constants.ts
|
||||
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||
// 3단계 원 스타일 (외부 -> 내부)
|
||||
circleLevels: [
|
||||
{
|
||||
radius: 0.5,
|
||||
opacity: 0.3,
|
||||
lineWidth: 2,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
},
|
||||
{
|
||||
radius: 0.33,
|
||||
opacity: 0.6,
|
||||
lineWidth: 2.5,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
},
|
||||
{
|
||||
radius: 0.167,
|
||||
opacity: 0.9,
|
||||
lineWidth: 3,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
}
|
||||
],
|
||||
// 원 내부 채우기
|
||||
circleFillColor: "rgba(255, 200, 0, 0.08)",
|
||||
// 중심점
|
||||
centerPoint: {
|
||||
radius: 5,
|
||||
fillColor: "rgba(255, 200, 0, 1)",
|
||||
strokeColor: "rgba(255, 255, 255, 0.8)",
|
||||
strokeWidth: 2
|
||||
},
|
||||
// 포인트 핸들
|
||||
pointHandle: {
|
||||
size: 16,
|
||||
fillColor: "#00aaff",
|
||||
strokeColor: "white",
|
||||
strokeWidth: 2,
|
||||
labelColor: "#00aaff",
|
||||
labelFontSize: 11
|
||||
},
|
||||
// 영역 외곽선
|
||||
areaOutline: {
|
||||
selectedColor: "#00aaff",
|
||||
unselectedColor: "#888",
|
||||
selectedWidth: 2,
|
||||
unselectedWidth: 1,
|
||||
unselectedDashPattern: [5, 5],
|
||||
selectedFillColor: "rgba(0, 170, 255, 0.08)",
|
||||
// 선택된 영역 배경 (연한 파란색)
|
||||
unselectedFillColor: "rgba(136, 136, 136, 0.03)"
|
||||
// 선택 안된 영역 배경 (연한 회색)
|
||||
}
|
||||
};
|
||||
|
||||
// src/editor/components/EditorCanvas.tsx
|
||||
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
||||
var EditorCanvas = ({
|
||||
areas,
|
||||
@ -1637,64 +1624,6 @@ var DistortionEditor = ({
|
||||
] })
|
||||
] });
|
||||
};
|
||||
|
||||
// src/editor/constants.ts
|
||||
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||
// 3단계 원 스타일 (외부 -> 내부)
|
||||
circleLevels: [
|
||||
{
|
||||
radius: 0.5,
|
||||
opacity: 0.3,
|
||||
lineWidth: 2,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
},
|
||||
{
|
||||
radius: 0.33,
|
||||
opacity: 0.6,
|
||||
lineWidth: 2.5,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
},
|
||||
{
|
||||
radius: 0.167,
|
||||
opacity: 0.9,
|
||||
lineWidth: 3,
|
||||
color: "rgba(255, 200, 0, 1)",
|
||||
dashPattern: [8, 4]
|
||||
}
|
||||
],
|
||||
// 원 내부 채우기
|
||||
circleFillColor: "rgba(255, 200, 0, 0.08)",
|
||||
// 중심점
|
||||
centerPoint: {
|
||||
radius: 5,
|
||||
fillColor: "rgba(255, 200, 0, 1)",
|
||||
strokeColor: "rgba(255, 255, 255, 0.8)",
|
||||
strokeWidth: 2
|
||||
},
|
||||
// 포인트 핸들
|
||||
pointHandle: {
|
||||
size: 16,
|
||||
fillColor: "#00aaff",
|
||||
strokeColor: "white",
|
||||
strokeWidth: 2,
|
||||
labelColor: "#00aaff",
|
||||
labelFontSize: 11
|
||||
},
|
||||
// 영역 외곽선
|
||||
areaOutline: {
|
||||
selectedColor: "#00aaff",
|
||||
unselectedColor: "#888",
|
||||
selectedWidth: 2,
|
||||
unselectedWidth: 1,
|
||||
unselectedDashPattern: [5, 5],
|
||||
selectedFillColor: "rgba(0, 170, 255, 0.08)",
|
||||
// 선택된 영역 배경 (연한 파란색)
|
||||
unselectedFillColor: "rgba(136, 136, 136, 0.03)"
|
||||
// 선택 안된 영역 배경 (연한 회색)
|
||||
}
|
||||
};
|
||||
export {
|
||||
ANIMATION_CONFIG,
|
||||
AnimationLoop,
|
||||
@ -1706,8 +1635,6 @@ export {
|
||||
SpringPhysics,
|
||||
ThreeScene,
|
||||
applyEasing,
|
||||
isRotationPreset,
|
||||
presetToVector,
|
||||
useAnimationFrame,
|
||||
useDistortionEditor,
|
||||
useMouseInteraction,
|
||||
|
||||
2
dist/index.mjs.map
vendored
2
dist/index.mjs.map
vendored
File diff suppressed because one or more lines are too long
@ -1,9 +1,6 @@
|
||||
{
|
||||
"name": "@baekryang/responsive-image-canvas",
|
||||
"version": "1.0.1",
|
||||
"publishConfig": {
|
||||
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
|
||||
},
|
||||
"name": "responsive-image-canvas",
|
||||
"version": "1.0.0",
|
||||
"description": "React component for interactive image distortion with GPU-accelerated shaders",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@ -214,26 +214,10 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
||||
if (!isReady) return;
|
||||
|
||||
setCurrentAreas((prevAreas) => {
|
||||
// 현재 인터랙션 중인 영역 인덱스 가져오기
|
||||
const interactingIndices = mouseInteractionHook.getInteractingAreaIndices?.() || new Set<number>();
|
||||
|
||||
// 1. 자동 애니메이션 업데이트
|
||||
// 1. 기존 영역 애니메이션 업데이트
|
||||
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
|
||||
// 인터랙션 중인 영역만 dragVector를 0으로 설정
|
||||
if (interactingIndices.size > 0) {
|
||||
updatedAreas = updatedAreas.map((area, index) => {
|
||||
if (interactingIndices.has(index)) {
|
||||
return {
|
||||
...area,
|
||||
dragVector: { x: 0, y: 0 }
|
||||
};
|
||||
}
|
||||
return area;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 마우스 인터랙션 적용 (기존 dragVector에 스프링 변위 추가)
|
||||
if (mouseInteraction?.enabled) {
|
||||
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { applyEasing } from '../utils/easing';
|
||||
import { presetToVector, isRotationPreset } from '../utils/motionPresets';
|
||||
import type {DistortionArea, Point} from "../types";
|
||||
|
||||
/**
|
||||
@ -17,57 +16,35 @@ export class AnimationLoop {
|
||||
return areas.map((area) => {
|
||||
const { progress, movement } = area;
|
||||
|
||||
// duration이 0이거나 프리셋이 'none'이면 애니메이션 없음
|
||||
if (movement.duration <= 0 || movement.preset === 'none') {
|
||||
// duration이 0이면 애니메이션 없음 (dragVector를 0으로 유지)
|
||||
if (movement.duration <= 0) {
|
||||
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;
|
||||
|
||||
// 회전 프리셋인 경우 원운동
|
||||
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) {
|
||||
// 0.0 -> 0.5: 0에서 baseVector로 보간
|
||||
// 0.0 -> 0.5: 0에서 vectorA로 보간
|
||||
const t = easedProgress * 2;
|
||||
dragVector = {
|
||||
x: baseVector.x * t,
|
||||
y: baseVector.y * t,
|
||||
x: movement.vectorA.x * t,
|
||||
y: movement.vectorA.y * t,
|
||||
};
|
||||
} else {
|
||||
// 0.5 -> 1.0: baseVector에서 0으로 보간
|
||||
// 0.5 -> 1.0: vectorA에서 0으로 보간
|
||||
const t = (easedProgress - 0.5) * 2;
|
||||
dragVector = {
|
||||
x: baseVector.x * (1 - t),
|
||||
y: baseVector.y * (1 - t),
|
||||
x: movement.vectorA.x * (1 - t),
|
||||
y: movement.vectorA.y * (1 - t),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...area,
|
||||
|
||||
@ -34,11 +34,9 @@ export const useMouseInteraction = (
|
||||
/**
|
||||
* 특정 영역의 스프링 물리 엔진 가져오기 (없으면 생성)
|
||||
*/
|
||||
const getSpringPhysics = useCallback((areaIndex: number, area?: DistortionArea): SpringPhysics => {
|
||||
const getSpringPhysics = useCallback((areaIndex: number): SpringPhysics => {
|
||||
if (!springPhysicsMapRef.current.has(areaIndex)) {
|
||||
// 영역별 물리 설정이 있으면 사용, 없으면 전역 설정 사용
|
||||
const physicsConfig = area?.physics || config.physics;
|
||||
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(physicsConfig));
|
||||
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
|
||||
}
|
||||
return springPhysicsMapRef.current.get(areaIndex)!;
|
||||
}, [config.physics]);
|
||||
@ -49,14 +47,6 @@ export const useMouseInteraction = (
|
||||
const updateInteraction = useCallback((areas: DistortionArea[], deltaTime: number): DistortionArea[] => {
|
||||
if (!config.enabled) return areas;
|
||||
|
||||
// 영역별 물리 설정이 변경되었을 수 있으므로 모든 스프링 업데이트
|
||||
areas.forEach((area, index) => {
|
||||
if (area.physics && springPhysicsMapRef.current.has(index)) {
|
||||
const spring = springPhysicsMapRef.current.get(index)!;
|
||||
spring.setConfig(area.physics);
|
||||
}
|
||||
});
|
||||
|
||||
const mouseState = getState();
|
||||
|
||||
// 마우스 클릭/드래그 중이고 위치가 있으면
|
||||
@ -69,7 +59,7 @@ export const useMouseInteraction = (
|
||||
|
||||
// 새로 진입한 영역이면 스프링 리셋
|
||||
if (!interactingAreaIndices.has(i)) {
|
||||
getSpringPhysics(i, areas[i]).reset();
|
||||
getSpringPhysics(i).reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,7 +67,7 @@ export const useMouseInteraction = (
|
||||
// 이전에 인터랙션하던 영역에서 벗어났으면 평형으로 복귀
|
||||
interactingAreaIndices.forEach((areaIndex) => {
|
||||
if (!currentlyInAreas.has(areaIndex)) {
|
||||
getSpringPhysics(areaIndex, areas[areaIndex]).returnToEquilibrium();
|
||||
getSpringPhysics(areaIndex).returnToEquilibrium();
|
||||
}
|
||||
});
|
||||
|
||||
@ -103,7 +93,7 @@ export const useMouseInteraction = (
|
||||
}
|
||||
|
||||
currentlyInAreas.forEach((areaIndex) => {
|
||||
const spring = getSpringPhysics(areaIndex, areas[areaIndex]);
|
||||
const spring = getSpringPhysics(areaIndex);
|
||||
|
||||
if (velocityMag >= minVel) {
|
||||
// 드래그 중: 마우스 속도를 목표로 설정
|
||||
@ -134,7 +124,7 @@ export const useMouseInteraction = (
|
||||
|
||||
// 모든 인터랙션 영역에 초기 속도 설정
|
||||
interactingAreaIndices.forEach((areaIndex) => {
|
||||
const spring = getSpringPhysics(areaIndex, areas[areaIndex]);
|
||||
const spring = getSpringPhysics(areaIndex);
|
||||
spring.setInitialVelocity(clampedVelocity, velocityMult);
|
||||
});
|
||||
|
||||
@ -204,26 +194,9 @@ export const useMouseInteraction = (
|
||||
setInteractingAreaIndices(new Set());
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 현재 드래그 중인지 확인
|
||||
*/
|
||||
const isDragging = useCallback((): boolean => {
|
||||
const mouseState = getState();
|
||||
return mouseState.isDragging;
|
||||
}, [getState]);
|
||||
|
||||
/**
|
||||
* 현재 인터랙션 중인 영역 인덱스 가져오기
|
||||
*/
|
||||
const getInteractingAreaIndices = useCallback((): Set<number> => {
|
||||
return interactingAreaIndices;
|
||||
}, [interactingAreaIndices]);
|
||||
|
||||
return {
|
||||
updateInteraction,
|
||||
updateConfig,
|
||||
reset,
|
||||
isDragging,
|
||||
getInteractingAreaIndices,
|
||||
};
|
||||
};
|
||||
|
||||
@ -11,7 +11,6 @@ export { useDistortionEditor } from './editor';
|
||||
export type {
|
||||
Point,
|
||||
EasingFunction,
|
||||
MotionPreset,
|
||||
DistortionMovement,
|
||||
DistortionArea,
|
||||
AreaBounds,
|
||||
@ -32,7 +31,6 @@ export type {
|
||||
// 유틸리티 함수
|
||||
export { applyEasing } from './utils/easing';
|
||||
export { SHADER_CONFIG, ANIMATION_CONFIG, DEFAULT_AREA } from './utils/constants';
|
||||
export { presetToVector, isRotationPreset } from './utils/motionPresets';
|
||||
|
||||
// 엔진 클래스 (고급 사용자용)
|
||||
export { ThreeScene } from './engine/ThreeScene';
|
||||
|
||||
@ -74,21 +74,12 @@ void main() {
|
||||
// dragVector는 정규화된 좌표(0-1)이므로 바로 사용
|
||||
vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i];
|
||||
texCoord += distortion;
|
||||
// clamp를 루프 내에서 제거: 모든 왜곡을 완전히 누적한 후 마지막에 한 번만 clamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 경계 근처에서 부드럽게 페이드 아웃
|
||||
// 텍스처 좌표가 0~1 범위를 벗어나면 알파값을 줄여서 자연스럽게 처리
|
||||
vec2 edgeDist = min(texCoord, 1.0 - texCoord);
|
||||
float edgeFade = smoothstep(0.0, 0.05, min(edgeDist.x, edgeDist.y));
|
||||
|
||||
// 범위를 벗어난 좌표는 fract로 래핑하여 반복 효과 (더 자연스러움)
|
||||
vec2 wrappedCoord = fract(texCoord);
|
||||
|
||||
vec4 color = texture2D(u_texture, wrappedCoord);
|
||||
// 경계에서 페이드 아웃 적용
|
||||
color.a *= edgeFade;
|
||||
|
||||
gl_FragColor = color;
|
||||
// 모든 왜곡을 누적한 후 최종적으로 한 번만 clamp
|
||||
texCoord = clamp(texCoord, 0.0, 1.0);
|
||||
gl_FragColor = texture2D(u_texture, texCoord);
|
||||
}
|
||||
@ -17,35 +17,18 @@ export type EasingFunction =
|
||||
| 'easeInQuad'
|
||||
| 'easeOutQuad';
|
||||
|
||||
/**
|
||||
* 모션 프리셋 타입
|
||||
*/
|
||||
export type MotionPreset =
|
||||
| 'none' // 없음 (애니메이션 없음)
|
||||
| 'horizontal' // 좌우 왕복
|
||||
| 'vertical' // 상하 왕복
|
||||
| 'rotate-cw' // 시계방향 회전
|
||||
| 'rotate-ccw' // 반시계방향 회전
|
||||
| 'pulse' // 펄스 (확대/축소)
|
||||
| 'diagonal-1' // 대각선 (좌상→우하)
|
||||
| 'diagonal-2'; // 대각선 (우상→좌하)
|
||||
|
||||
/**
|
||||
* 왜곡 애니메이션 움직임 설정
|
||||
*/
|
||||
export interface DistortionMovement {
|
||||
/** 모션 프리셋 (vectorA, vectorB 대신 사용) */
|
||||
preset?: MotionPreset;
|
||||
/** 왜곡 시작 벡터 (preset 없을 때 사용) */
|
||||
/** 왜곡 시작 벡터 */
|
||||
vectorA: Point;
|
||||
/** 왜곡 종료 벡터 (preset 없을 때 사용, 현재는 미사용) */
|
||||
/** 왜곡 종료 벡터 */
|
||||
vectorB: Point;
|
||||
/** 애니메이션 지속 시간 (초) */
|
||||
duration: number;
|
||||
/** 적용할 이징 함수 */
|
||||
easing: EasingFunction;
|
||||
/** 모션 강도 (프리셋 적용 시 벡터 크기 조절용, 기본값: 0.1) */
|
||||
strength?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,14 +47,6 @@ export interface DistortionArea {
|
||||
progress: number;
|
||||
/** 현재 드래그 벡터 (progress로부터 계산됨) */
|
||||
dragVector: Point;
|
||||
/** 영역별 물리 설정 (선택사항, 마우스 인터랙션 시 사용) */
|
||||
physics?: {
|
||||
stiffness: number;
|
||||
damping: number;
|
||||
mass: number;
|
||||
influenceRadius: number;
|
||||
maxStrength: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
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';
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user