Compare commits

..

No commits in common. "e08f34caab8684e0b866fc6f63e9efb3d98889e4" and "c18f3fffb59dd8b3c4ce8d07edee4f0c90f909b0" have entirely different histories.

16 changed files with 196 additions and 571 deletions

2
.gitignore vendored
View File

@ -14,4 +14,4 @@ yarn-error.log*
nul nul
# Demo (템플릿 파일, 실제 데모는 별도 저장소) # Demo (템플릿 파일, 실제 데모는 별도 저장소)
/demo.npmrc /demo

View File

@ -74,21 +74,12 @@ void main() {
// dragVector는 정규화된 좌표(0-1)이므로 바로 사용 // dragVector는 정규화된 좌표(0-1)이므로 바로 사용
vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i]; vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i];
texCoord += distortion; texCoord += distortion;
// clamp를 루프 내에서 제거: 모든 왜곡을 완전히 누적한 후 마지막에 한 번만 clamp
} }
} }
} }
// 경계 근처에서 부드럽게 페이드 아웃 // 모든 왜곡을 누적한 후 최종적으로 한 번만 clamp
// 텍스처 좌표가 0~1 범위를 벗어나면 알파값을 줄여서 자연스럽게 처리 texCoord = clamp(texCoord, 0.0, 1.0);
vec2 edgeDist = min(texCoord, 1.0 - texCoord); gl_FragColor = texture2D(u_texture, 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;
} }

36
dist/index.d.mts vendored
View File

@ -12,26 +12,18 @@ 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;
} }
/** /**
* *
@ -49,14 +41,6 @@ interface DistortionArea {
progress: number; progress: number;
/** 현재 드래그 벡터 (progress로부터 계산됨) */ /** 현재 드래그 벡터 (progress로부터 계산됨) */
dragVector: Point; 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 * Three.js
*/ */
@ -565,8 +537,6 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[]; updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];
updateConfig: (newConfig: Partial<MouseInteractionConfig>) => void; updateConfig: (newConfig: Partial<MouseInteractionConfig>) => void;
reset: () => 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
View File

@ -12,26 +12,18 @@ 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;
} }
/** /**
* *
@ -49,14 +41,6 @@ interface DistortionArea {
progress: number; progress: number;
/** 현재 드래그 벡터 (progress로부터 계산됨) */ /** 현재 드래그 벡터 (progress로부터 계산됨) */
dragVector: Point; 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 * Three.js
*/ */
@ -565,8 +537,6 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[]; updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];
updateConfig: (newConfig: Partial<MouseInteractionConfig>) => void; updateConfig: (newConfig: Partial<MouseInteractionConfig>) => void;
reset: () => 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 };

229
dist/index.js vendored
View File

@ -40,8 +40,6 @@ __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,
@ -246,34 +244,6 @@ 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 {
/** /**
@ -284,43 +254,26 @@ 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 || movement.preset === "none") { if (movement.duration <= 0) {
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)) { if (easedProgress < 0.5) {
const angle = easedProgress * Math.PI * 2; const t = easedProgress * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === "rotate-cw" ? 1 : -1;
dragVector = { dragVector = {
x: Math.cos(angle * direction) * radius, x: movement.vectorA.x * t,
y: Math.sin(angle * direction) * radius y: movement.vectorA.y * t
}; };
} else { } else {
if (easedProgress < 0.5) { const t = (easedProgress - 0.5) * 2;
const t = easedProgress * 2; dragVector = {
dragVector = { x: movement.vectorA.x * (1 - t),
x: baseVector.x * t, y: movement.vectorA.y * (1 - t)
y: baseVector.y * t };
};
} else {
const t = (easedProgress - 0.5) * 2;
dragVector = {
x: baseVector.x * (1 - t),
y: baseVector.y * (1 - t)
};
}
} }
return { return {
...area, ...area,
@ -633,21 +586,14 @@ var useMouseInteraction = (containerRef, config) => {
const { getState } = useMouseVelocity(containerRef); const { getState } = useMouseVelocity(containerRef);
const [interactingAreaIndices, setInteractingAreaIndices] = (0, import_react3.useState)(/* @__PURE__ */ new Set()); const [interactingAreaIndices, setInteractingAreaIndices] = (0, import_react3.useState)(/* @__PURE__ */ new Set());
const springPhysicsMapRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map()); 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)) { if (!springPhysicsMapRef.current.has(areaIndex)) {
const physicsConfig = area?.physics || config.physics; springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(physicsConfig));
} }
return springPhysicsMapRef.current.get(areaIndex); return springPhysicsMapRef.current.get(areaIndex);
}, [config.physics]); }, [config.physics]);
const updateInteraction = (0, import_react3.useCallback)((areas, deltaTime) => { const updateInteraction = (0, import_react3.useCallback)((areas, deltaTime) => {
if (!config.enabled) return areas; 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(); const mouseState = getState();
if (mouseState.isDragging && mouseState.position) { if (mouseState.isDragging && mouseState.position) {
const currentlyInAreas = /* @__PURE__ */ new Set(); const currentlyInAreas = /* @__PURE__ */ new Set();
@ -655,13 +601,13 @@ var useMouseInteraction = (containerRef, config) => {
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) { if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
currentlyInAreas.add(i); currentlyInAreas.add(i);
if (!interactingAreaIndices.has(i)) { if (!interactingAreaIndices.has(i)) {
getSpringPhysics(i, areas[i]).reset(); getSpringPhysics(i).reset();
} }
} }
} }
interactingAreaIndices.forEach((areaIndex) => { interactingAreaIndices.forEach((areaIndex) => {
if (!currentlyInAreas.has(areaIndex)) { if (!currentlyInAreas.has(areaIndex)) {
getSpringPhysics(areaIndex, areas[areaIndex]).returnToEquilibrium(); getSpringPhysics(areaIndex).returnToEquilibrium();
} }
}); });
setInteractingAreaIndices(currentlyInAreas); setInteractingAreaIndices(currentlyInAreas);
@ -680,7 +626,7 @@ var useMouseInteraction = (containerRef, config) => {
}; };
} }
currentlyInAreas.forEach((areaIndex) => { currentlyInAreas.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex, areas[areaIndex]); const spring = getSpringPhysics(areaIndex);
if (velocityMag >= minVel) { if (velocityMag >= minVel) {
spring.setTarget(clampedVelocity, velocityMult); spring.setTarget(clampedVelocity, velocityMult);
} else { } else {
@ -703,7 +649,7 @@ var useMouseInteraction = (containerRef, config) => {
}; };
} }
interactingAreaIndices.forEach((areaIndex) => { interactingAreaIndices.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex, areas[areaIndex]); const spring = getSpringPhysics(areaIndex);
spring.setInitialVelocity(clampedVelocity, velocityMult); spring.setInitialVelocity(clampedVelocity, velocityMult);
}); });
setInteractingAreaIndices(/* @__PURE__ */ new Set()); setInteractingAreaIndices(/* @__PURE__ */ new Set());
@ -746,19 +692,10 @@ var useMouseInteraction = (containerRef, config) => {
}); });
setInteractingAreaIndices(/* @__PURE__ */ new Set()); 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 { return {
updateInteraction, updateInteraction,
updateConfig, updateConfig,
reset, reset
isDragging,
getInteractingAreaIndices
}; };
}; };
@ -933,20 +870,8 @@ var ImageDistortion = ({
const animationCallback = (0, import_react4.useCallback)((deltaTime) => { const animationCallback = (0, import_react4.useCallback)((deltaTime) => {
if (!isReady) return; if (!isReady) return;
setCurrentAreas((prevAreas) => { setCurrentAreas((prevAreas) => {
const interactingIndices = mouseInteractionHook.getInteractingAreaIndices?.() || /* @__PURE__ */ new Set();
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime); let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas); 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) { if (mouseInteraction?.enabled) {
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime); updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
} }
@ -1065,6 +990,66 @@ var useDistortionEditor = (initialAreas = []) => {
// src/editor/components/EditorCanvas.tsx // src/editor/components/EditorCanvas.tsx
var import_react6 = require("react"); 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 import_jsx_runtime2 = require("react/jsx-runtime");
var EditorCanvas = ({ var EditorCanvas = ({
areas, 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: // Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = { 0 && (module.exports = {
ANIMATION_CONFIG, ANIMATION_CONFIG,
@ -1758,8 +1685,6 @@ 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

227
dist/index.mjs vendored
View File

@ -195,34 +195,6 @@ 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 {
/** /**
@ -233,43 +205,26 @@ 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 || movement.preset === "none") { if (movement.duration <= 0) {
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)) { if (easedProgress < 0.5) {
const angle = easedProgress * Math.PI * 2; const t = easedProgress * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === "rotate-cw" ? 1 : -1;
dragVector = { dragVector = {
x: Math.cos(angle * direction) * radius, x: movement.vectorA.x * t,
y: Math.sin(angle * direction) * radius y: movement.vectorA.y * t
}; };
} else { } else {
if (easedProgress < 0.5) { const t = (easedProgress - 0.5) * 2;
const t = easedProgress * 2; dragVector = {
dragVector = { x: movement.vectorA.x * (1 - t),
x: baseVector.x * t, y: movement.vectorA.y * (1 - t)
y: baseVector.y * t };
};
} else {
const t = (easedProgress - 0.5) * 2;
dragVector = {
x: baseVector.x * (1 - t),
y: baseVector.y * (1 - t)
};
}
} }
return { return {
...area, ...area,
@ -582,21 +537,14 @@ var useMouseInteraction = (containerRef, config) => {
const { getState } = useMouseVelocity(containerRef); const { getState } = useMouseVelocity(containerRef);
const [interactingAreaIndices, setInteractingAreaIndices] = useState(/* @__PURE__ */ new Set()); const [interactingAreaIndices, setInteractingAreaIndices] = useState(/* @__PURE__ */ new Set());
const springPhysicsMapRef = useRef3(/* @__PURE__ */ new Map()); const springPhysicsMapRef = useRef3(/* @__PURE__ */ new Map());
const getSpringPhysics = useCallback2((areaIndex, area) => { const getSpringPhysics = useCallback2((areaIndex) => {
if (!springPhysicsMapRef.current.has(areaIndex)) { if (!springPhysicsMapRef.current.has(areaIndex)) {
const physicsConfig = area?.physics || config.physics; springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(physicsConfig));
} }
return springPhysicsMapRef.current.get(areaIndex); return springPhysicsMapRef.current.get(areaIndex);
}, [config.physics]); }, [config.physics]);
const updateInteraction = useCallback2((areas, deltaTime) => { const updateInteraction = useCallback2((areas, deltaTime) => {
if (!config.enabled) return areas; 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(); const mouseState = getState();
if (mouseState.isDragging && mouseState.position) { if (mouseState.isDragging && mouseState.position) {
const currentlyInAreas = /* @__PURE__ */ new Set(); const currentlyInAreas = /* @__PURE__ */ new Set();
@ -604,13 +552,13 @@ var useMouseInteraction = (containerRef, config) => {
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) { if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
currentlyInAreas.add(i); currentlyInAreas.add(i);
if (!interactingAreaIndices.has(i)) { if (!interactingAreaIndices.has(i)) {
getSpringPhysics(i, areas[i]).reset(); getSpringPhysics(i).reset();
} }
} }
} }
interactingAreaIndices.forEach((areaIndex) => { interactingAreaIndices.forEach((areaIndex) => {
if (!currentlyInAreas.has(areaIndex)) { if (!currentlyInAreas.has(areaIndex)) {
getSpringPhysics(areaIndex, areas[areaIndex]).returnToEquilibrium(); getSpringPhysics(areaIndex).returnToEquilibrium();
} }
}); });
setInteractingAreaIndices(currentlyInAreas); setInteractingAreaIndices(currentlyInAreas);
@ -629,7 +577,7 @@ var useMouseInteraction = (containerRef, config) => {
}; };
} }
currentlyInAreas.forEach((areaIndex) => { currentlyInAreas.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex, areas[areaIndex]); const spring = getSpringPhysics(areaIndex);
if (velocityMag >= minVel) { if (velocityMag >= minVel) {
spring.setTarget(clampedVelocity, velocityMult); spring.setTarget(clampedVelocity, velocityMult);
} else { } else {
@ -652,7 +600,7 @@ var useMouseInteraction = (containerRef, config) => {
}; };
} }
interactingAreaIndices.forEach((areaIndex) => { interactingAreaIndices.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex, areas[areaIndex]); const spring = getSpringPhysics(areaIndex);
spring.setInitialVelocity(clampedVelocity, velocityMult); spring.setInitialVelocity(clampedVelocity, velocityMult);
}); });
setInteractingAreaIndices(/* @__PURE__ */ new Set()); setInteractingAreaIndices(/* @__PURE__ */ new Set());
@ -695,19 +643,10 @@ var useMouseInteraction = (containerRef, config) => {
}); });
setInteractingAreaIndices(/* @__PURE__ */ new Set()); setInteractingAreaIndices(/* @__PURE__ */ new Set());
}, []); }, []);
const isDragging = useCallback2(() => {
const mouseState = getState();
return mouseState.isDragging;
}, [getState]);
const getInteractingAreaIndices = useCallback2(() => {
return interactingAreaIndices;
}, [interactingAreaIndices]);
return { return {
updateInteraction, updateInteraction,
updateConfig, updateConfig,
reset, reset
isDragging,
getInteractingAreaIndices
}; };
}; };
@ -882,20 +821,8 @@ var ImageDistortion = ({
const animationCallback = useCallback3((deltaTime) => { const animationCallback = useCallback3((deltaTime) => {
if (!isReady) return; if (!isReady) return;
setCurrentAreas((prevAreas) => { setCurrentAreas((prevAreas) => {
const interactingIndices = mouseInteractionHook.getInteractingAreaIndices?.() || /* @__PURE__ */ new Set();
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime); let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas); 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) { if (mouseInteraction?.enabled) {
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime); updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
} }
@ -1014,6 +941,66 @@ var useDistortionEditor = (initialAreas = []) => {
// src/editor/components/EditorCanvas.tsx // src/editor/components/EditorCanvas.tsx
import { useRef as useRef5, useEffect as useEffect4, useState as useState4, useCallback as useCallback5, useMemo } from "react"; 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"; import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var EditorCanvas = ({ var EditorCanvas = ({
areas, 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 { export {
ANIMATION_CONFIG, ANIMATION_CONFIG,
AnimationLoop, AnimationLoop,
@ -1706,8 +1635,6 @@ 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,9 +1,6 @@
{ {
"name": "@baekryang/responsive-image-canvas", "name": "responsive-image-canvas",
"version": "1.0.1", "version": "1.0.0",
"publishConfig": {
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
},
"description": "React component for interactive image distortion with GPU-accelerated shaders", "description": "React component for interactive image distortion with GPU-accelerated shaders",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",

View File

@ -214,26 +214,10 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
if (!isReady) return; if (!isReady) return;
setCurrentAreas((prevAreas) => { setCurrentAreas((prevAreas) => {
// 현재 인터랙션 중인 영역 인덱스 가져오기 // 1. 기존 영역 애니메이션 업데이트
const interactingIndices = mouseInteractionHook.getInteractingAreaIndices?.() || new Set<number>();
// 1. 자동 애니메이션 업데이트
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime); let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas); 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에 스프링 변위 추가) // 2. 마우스 인터랙션 적용 (기존 dragVector에 스프링 변위 추가)
if (mouseInteraction?.enabled) { if (mouseInteraction?.enabled) {
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime); updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);

View File

@ -1,5 +1,4 @@
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";
/** /**
@ -17,56 +16,34 @@ export class AnimationLoop {
return areas.map((area) => { return areas.map((area) => {
const { progress, movement } = area; const { progress, movement } = area;
// duration이 0이거나 프리셋이 'none'이면 애니메이션 없음 // duration이 0이면 애니메이션 없음 (dragVector를 0으로 유지)
if (movement.duration <= 0 || movement.preset === 'none') { if (movement.duration <= 0) {
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) {
if (movement.preset && isRotationPreset(movement.preset)) { // 0.0 -> 0.5: 0에서 vectorA로 보간
const angle = easedProgress * Math.PI * 2; const t = easedProgress * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === 'rotate-cw' ? 1 : -1;
dragVector = { dragVector = {
x: Math.cos(angle * direction) * radius, x: movement.vectorA.x * t,
y: Math.sin(angle * direction) * radius, y: movement.vectorA.y * t,
}; };
} else { } else {
// 일반 왕복 모션 // 0.5 -> 1.0: vectorA에서 0으로 보간
if (easedProgress < 0.5) { const t = (easedProgress - 0.5) * 2;
// 0.0 -> 0.5: 0에서 baseVector로 보간 dragVector = {
const t = easedProgress * 2; x: movement.vectorA.x * (1 - t),
dragVector = { y: movement.vectorA.y * (1 - t),
x: baseVector.x * t, };
y: baseVector.y * t,
};
} else {
// 0.5 -> 1.0: baseVector에서 0으로 보간
const t = (easedProgress - 0.5) * 2;
dragVector = {
x: baseVector.x * (1 - t),
y: baseVector.y * (1 - t),
};
}
} }
return { return {

View File

@ -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)) { if (!springPhysicsMapRef.current.has(areaIndex)) {
// 영역별 물리 설정이 있으면 사용, 없으면 전역 설정 사용 springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
const physicsConfig = area?.physics || config.physics;
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(physicsConfig));
} }
return springPhysicsMapRef.current.get(areaIndex)!; return springPhysicsMapRef.current.get(areaIndex)!;
}, [config.physics]); }, [config.physics]);
@ -49,14 +47,6 @@ export const useMouseInteraction = (
const updateInteraction = useCallback((areas: DistortionArea[], deltaTime: number): DistortionArea[] => { const updateInteraction = useCallback((areas: DistortionArea[], deltaTime: number): DistortionArea[] => {
if (!config.enabled) return areas; 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(); const mouseState = getState();
// 마우스 클릭/드래그 중이고 위치가 있으면 // 마우스 클릭/드래그 중이고 위치가 있으면
@ -69,7 +59,7 @@ export const useMouseInteraction = (
// 새로 진입한 영역이면 스프링 리셋 // 새로 진입한 영역이면 스프링 리셋
if (!interactingAreaIndices.has(i)) { if (!interactingAreaIndices.has(i)) {
getSpringPhysics(i, areas[i]).reset(); getSpringPhysics(i).reset();
} }
} }
} }
@ -77,7 +67,7 @@ export const useMouseInteraction = (
// 이전에 인터랙션하던 영역에서 벗어났으면 평형으로 복귀 // 이전에 인터랙션하던 영역에서 벗어났으면 평형으로 복귀
interactingAreaIndices.forEach((areaIndex) => { interactingAreaIndices.forEach((areaIndex) => {
if (!currentlyInAreas.has(areaIndex)) { if (!currentlyInAreas.has(areaIndex)) {
getSpringPhysics(areaIndex, areas[areaIndex]).returnToEquilibrium(); getSpringPhysics(areaIndex).returnToEquilibrium();
} }
}); });
@ -103,7 +93,7 @@ export const useMouseInteraction = (
} }
currentlyInAreas.forEach((areaIndex) => { currentlyInAreas.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex, areas[areaIndex]); const spring = getSpringPhysics(areaIndex);
if (velocityMag >= minVel) { if (velocityMag >= minVel) {
// 드래그 중: 마우스 속도를 목표로 설정 // 드래그 중: 마우스 속도를 목표로 설정
@ -134,7 +124,7 @@ export const useMouseInteraction = (
// 모든 인터랙션 영역에 초기 속도 설정 // 모든 인터랙션 영역에 초기 속도 설정
interactingAreaIndices.forEach((areaIndex) => { interactingAreaIndices.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex, areas[areaIndex]); const spring = getSpringPhysics(areaIndex);
spring.setInitialVelocity(clampedVelocity, velocityMult); spring.setInitialVelocity(clampedVelocity, velocityMult);
}); });
@ -204,26 +194,9 @@ export const useMouseInteraction = (
setInteractingAreaIndices(new Set()); setInteractingAreaIndices(new Set());
}, []); }, []);
/**
*
*/
const isDragging = useCallback((): boolean => {
const mouseState = getState();
return mouseState.isDragging;
}, [getState]);
/**
*
*/
const getInteractingAreaIndices = useCallback((): Set<number> => {
return interactingAreaIndices;
}, [interactingAreaIndices]);
return { return {
updateInteraction, updateInteraction,
updateConfig, updateConfig,
reset, reset,
isDragging,
getInteractingAreaIndices,
}; };
}; };

View File

@ -11,7 +11,6 @@ export { useDistortionEditor } from './editor';
export type { export type {
Point, Point,
EasingFunction, EasingFunction,
MotionPreset,
DistortionMovement, DistortionMovement,
DistortionArea, DistortionArea,
AreaBounds, AreaBounds,
@ -32,7 +31,6 @@ 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

@ -74,21 +74,12 @@ void main() {
// dragVector는 정규화된 좌표(0-1)이므로 바로 사용 // dragVector는 정규화된 좌표(0-1)이므로 바로 사용
vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i]; vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i];
texCoord += distortion; texCoord += distortion;
// clamp를 루프 내에서 제거: 모든 왜곡을 완전히 누적한 후 마지막에 한 번만 clamp
} }
} }
} }
// 경계 근처에서 부드럽게 페이드 아웃 // 모든 왜곡을 누적한 후 최종적으로 한 번만 clamp
// 텍스처 좌표가 0~1 범위를 벗어나면 알파값을 줄여서 자연스럽게 처리 texCoord = clamp(texCoord, 0.0, 1.0);
vec2 edgeDist = min(texCoord, 1.0 - texCoord); gl_FragColor = texture2D(u_texture, 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;
} }

View File

@ -17,35 +17,18 @@ 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;
} }
/** /**
@ -64,14 +47,6 @@ export interface DistortionArea {
progress: number; progress: number;
/** 현재 드래그 벡터 (progress로부터 계산됨) */ /** 현재 드래그 벡터 (progress로부터 계산됨) */
dragVector: Point; dragVector: Point;
/** 영역별 물리 설정 (선택사항, 마우스 인터랙션 시 사용) */
physics?: {
stiffness: number;
damping: number;
mass: number;
influenceRadius: number;
maxStrength: number;
};
} }
/** /**

View File

@ -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';
}