feat: Improve mouse interaction to affect multiple areas

- 마우스가 닿는 모든 영역에 왜곡 효과가 적용되도록 수정했습니다.
- 기존에는 마우스가 처음 닿는 단일 영역에만 효과가 적용되었으나, 이제는 마우스 커서가 영역을 벗어나도 해당 영역에 대한 스프링 물리 효과가 유지되도록 변경되었습니다.
- `useMouseInteraction` 훅에서 `interactingAreaIndex` 대신 `interactingAreaIndices` (Set)를 사용하여 여러 영역을 동시에 추적합니다.
- 영역 진입 시 스프링이 리셋되고, 영역 이탈 시 평형 상태로 복귀하는 로직이 추가되었습니다.
This commit is contained in:
BaekRyang 2025-11-05 15:31:11 +09:00
parent 7f6a72c058
commit ddcf8b463a
7 changed files with 134 additions and 106 deletions

2
dist/index.d.mts vendored
View File

@ -531,7 +531,7 @@ declare const useMouseVelocity: (containerRef: React.RefObject<HTMLElement | nul
/** /**
* *
* *
*/ */
declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement | null>, config: MouseInteractionConfig) => { declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement | null>, config: MouseInteractionConfig) => {
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[]; updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];

2
dist/index.d.ts vendored
View File

@ -531,7 +531,7 @@ declare const useMouseVelocity: (containerRef: React.RefObject<HTMLElement | nul
/** /**
* *
* *
*/ */
declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement | null>, config: MouseInteractionConfig) => { declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement | null>, config: MouseInteractionConfig) => {
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[]; updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];

38
dist/index.js vendored
View File

@ -582,7 +582,7 @@ var isPointInPolygon = (point, polygon) => {
}; };
var useMouseInteraction = (containerRef, config) => { var useMouseInteraction = (containerRef, config) => {
const { getState } = useMouseVelocity(containerRef); const { getState } = useMouseVelocity(containerRef);
const [interactingAreaIndex, setInteractingAreaIndex] = (0, import_react3.useState)(null); 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) => { const getSpringPhysics = (0, import_react3.useCallback)((areaIndex) => {
if (!springPhysicsMapRef.current.has(areaIndex)) { if (!springPhysicsMapRef.current.has(areaIndex)) {
@ -594,24 +594,27 @@ var useMouseInteraction = (containerRef, config) => {
if (!config.enabled) return areas; if (!config.enabled) return areas;
const mouseState = getState(); const mouseState = getState();
if (mouseState.isDragging && mouseState.position) { if (mouseState.isDragging && mouseState.position) {
if (interactingAreaIndex === null) { const currentlyInAreas = /* @__PURE__ */ new Set();
for (let i = areas.length - 1; i >= 0; i--) { for (let i = 0; i < areas.length; i++) {
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) { if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
setInteractingAreaIndex(i); currentlyInAreas.add(i);
if (!interactingAreaIndices.has(i)) {
getSpringPhysics(i).reset(); getSpringPhysics(i).reset();
break;
} }
} }
} }
if (interactingAreaIndex !== null) { interactingAreaIndices.forEach((areaIndex) => {
if (!currentlyInAreas.has(areaIndex)) {
getSpringPhysics(areaIndex).returnToEquilibrium();
}
});
setInteractingAreaIndices(currentlyInAreas);
const velocityMult = config.velocityMultiplier || 1; const velocityMult = config.velocityMultiplier || 1;
const spring = getSpringPhysics(interactingAreaIndex);
const velocityMag = Math.sqrt( const velocityMag = Math.sqrt(
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
); );
const minVel = config.minVelocity || 0.05; const minVel = config.minVelocity || 0.05;
const maxVel = config.maxVelocity || 5; const maxVel = config.maxVelocity || 5;
if (velocityMag >= minVel) {
let clampedVelocity = mouseState.velocity; let clampedVelocity = mouseState.velocity;
if (velocityMag > maxVel) { if (velocityMag > maxVel) {
const scale = maxVel / velocityMag; const scale = maxVel / velocityMag;
@ -620,15 +623,17 @@ var useMouseInteraction = (containerRef, config) => {
y: mouseState.velocity.y * scale y: mouseState.velocity.y * scale
}; };
} }
currentlyInAreas.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex);
if (velocityMag >= minVel) {
spring.setTarget(clampedVelocity, velocityMult); spring.setTarget(clampedVelocity, velocityMult);
} else { } else {
spring.returnToEquilibrium(); spring.returnToEquilibrium();
} }
} });
} else { } else {
if (interactingAreaIndex !== null) { if (interactingAreaIndices.size > 0) {
const velocityMult = config.velocityMultiplier || 1; const velocityMult = config.velocityMultiplier || 1;
const spring = getSpringPhysics(interactingAreaIndex);
const maxVel = config.maxVelocity || 5; const maxVel = config.maxVelocity || 5;
const velocityMag = Math.sqrt( const velocityMag = Math.sqrt(
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
@ -641,8 +646,11 @@ var useMouseInteraction = (containerRef, config) => {
y: mouseState.velocity.y * scale y: mouseState.velocity.y * scale
}; };
} }
interactingAreaIndices.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex);
spring.setInitialVelocity(clampedVelocity, velocityMult); spring.setInitialVelocity(clampedVelocity, velocityMult);
setInteractingAreaIndex(null); });
setInteractingAreaIndices(/* @__PURE__ */ new Set());
} }
} }
return areas.map((area, index) => { return areas.map((area, index) => {
@ -651,7 +659,7 @@ var useMouseInteraction = (containerRef, config) => {
const springVelocity = spring.getVelocity(); const springVelocity = spring.getVelocity();
const springDisplacement = spring.getDisplacement(); const springDisplacement = spring.getDisplacement();
const isSpringActive = Math.sqrt(springVelocity.x ** 2 + springVelocity.y ** 2) > 1e-3 || Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 1e-3; const isSpringActive = Math.sqrt(springVelocity.x ** 2 + springVelocity.y ** 2) > 1e-3 || Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 1e-3;
if (index !== interactingAreaIndex && !isSpringActive) { if (!interactingAreaIndices.has(index) && !isSpringActive) {
return area; return area;
} }
const displacement = spring.update(deltaTime); const displacement = spring.update(deltaTime);
@ -667,7 +675,7 @@ var useMouseInteraction = (containerRef, config) => {
} }
}; };
}); });
}, [config, getState, interactingAreaIndex, getSpringPhysics]); }, [config, getState, interactingAreaIndices, getSpringPhysics]);
const updateConfig = (0, import_react3.useCallback)((newConfig) => { const updateConfig = (0, import_react3.useCallback)((newConfig) => {
const physicsConfig = newConfig.physics; const physicsConfig = newConfig.physics;
if (physicsConfig) { if (physicsConfig) {
@ -680,7 +688,7 @@ var useMouseInteraction = (containerRef, config) => {
springPhysicsMapRef.current.forEach((spring) => { springPhysicsMapRef.current.forEach((spring) => {
spring.reset(); spring.reset();
}); });
setInteractingAreaIndex(null); setInteractingAreaIndices(/* @__PURE__ */ new Set());
}, []); }, []);
return { return {
updateInteraction, updateInteraction,

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

38
dist/index.mjs vendored
View File

@ -533,7 +533,7 @@ var isPointInPolygon = (point, polygon) => {
}; };
var useMouseInteraction = (containerRef, config) => { var useMouseInteraction = (containerRef, config) => {
const { getState } = useMouseVelocity(containerRef); const { getState } = useMouseVelocity(containerRef);
const [interactingAreaIndex, setInteractingAreaIndex] = useState(null); const [interactingAreaIndices, setInteractingAreaIndices] = useState(/* @__PURE__ */ new Set());
const springPhysicsMapRef = useRef3(/* @__PURE__ */ new Map()); const springPhysicsMapRef = useRef3(/* @__PURE__ */ new Map());
const getSpringPhysics = useCallback2((areaIndex) => { const getSpringPhysics = useCallback2((areaIndex) => {
if (!springPhysicsMapRef.current.has(areaIndex)) { if (!springPhysicsMapRef.current.has(areaIndex)) {
@ -545,24 +545,27 @@ var useMouseInteraction = (containerRef, config) => {
if (!config.enabled) return areas; if (!config.enabled) return areas;
const mouseState = getState(); const mouseState = getState();
if (mouseState.isDragging && mouseState.position) { if (mouseState.isDragging && mouseState.position) {
if (interactingAreaIndex === null) { const currentlyInAreas = /* @__PURE__ */ new Set();
for (let i = areas.length - 1; i >= 0; i--) { for (let i = 0; i < areas.length; i++) {
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) { if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
setInteractingAreaIndex(i); currentlyInAreas.add(i);
if (!interactingAreaIndices.has(i)) {
getSpringPhysics(i).reset(); getSpringPhysics(i).reset();
break;
} }
} }
} }
if (interactingAreaIndex !== null) { interactingAreaIndices.forEach((areaIndex) => {
if (!currentlyInAreas.has(areaIndex)) {
getSpringPhysics(areaIndex).returnToEquilibrium();
}
});
setInteractingAreaIndices(currentlyInAreas);
const velocityMult = config.velocityMultiplier || 1; const velocityMult = config.velocityMultiplier || 1;
const spring = getSpringPhysics(interactingAreaIndex);
const velocityMag = Math.sqrt( const velocityMag = Math.sqrt(
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
); );
const minVel = config.minVelocity || 0.05; const minVel = config.minVelocity || 0.05;
const maxVel = config.maxVelocity || 5; const maxVel = config.maxVelocity || 5;
if (velocityMag >= minVel) {
let clampedVelocity = mouseState.velocity; let clampedVelocity = mouseState.velocity;
if (velocityMag > maxVel) { if (velocityMag > maxVel) {
const scale = maxVel / velocityMag; const scale = maxVel / velocityMag;
@ -571,15 +574,17 @@ var useMouseInteraction = (containerRef, config) => {
y: mouseState.velocity.y * scale y: mouseState.velocity.y * scale
}; };
} }
currentlyInAreas.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex);
if (velocityMag >= minVel) {
spring.setTarget(clampedVelocity, velocityMult); spring.setTarget(clampedVelocity, velocityMult);
} else { } else {
spring.returnToEquilibrium(); spring.returnToEquilibrium();
} }
} });
} else { } else {
if (interactingAreaIndex !== null) { if (interactingAreaIndices.size > 0) {
const velocityMult = config.velocityMultiplier || 1; const velocityMult = config.velocityMultiplier || 1;
const spring = getSpringPhysics(interactingAreaIndex);
const maxVel = config.maxVelocity || 5; const maxVel = config.maxVelocity || 5;
const velocityMag = Math.sqrt( const velocityMag = Math.sqrt(
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
@ -592,8 +597,11 @@ var useMouseInteraction = (containerRef, config) => {
y: mouseState.velocity.y * scale y: mouseState.velocity.y * scale
}; };
} }
interactingAreaIndices.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex);
spring.setInitialVelocity(clampedVelocity, velocityMult); spring.setInitialVelocity(clampedVelocity, velocityMult);
setInteractingAreaIndex(null); });
setInteractingAreaIndices(/* @__PURE__ */ new Set());
} }
} }
return areas.map((area, index) => { return areas.map((area, index) => {
@ -602,7 +610,7 @@ var useMouseInteraction = (containerRef, config) => {
const springVelocity = spring.getVelocity(); const springVelocity = spring.getVelocity();
const springDisplacement = spring.getDisplacement(); const springDisplacement = spring.getDisplacement();
const isSpringActive = Math.sqrt(springVelocity.x ** 2 + springVelocity.y ** 2) > 1e-3 || Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 1e-3; const isSpringActive = Math.sqrt(springVelocity.x ** 2 + springVelocity.y ** 2) > 1e-3 || Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 1e-3;
if (index !== interactingAreaIndex && !isSpringActive) { if (!interactingAreaIndices.has(index) && !isSpringActive) {
return area; return area;
} }
const displacement = spring.update(deltaTime); const displacement = spring.update(deltaTime);
@ -618,7 +626,7 @@ var useMouseInteraction = (containerRef, config) => {
} }
}; };
}); });
}, [config, getState, interactingAreaIndex, getSpringPhysics]); }, [config, getState, interactingAreaIndices, getSpringPhysics]);
const updateConfig = useCallback2((newConfig) => { const updateConfig = useCallback2((newConfig) => {
const physicsConfig = newConfig.physics; const physicsConfig = newConfig.physics;
if (physicsConfig) { if (physicsConfig) {
@ -631,7 +639,7 @@ var useMouseInteraction = (containerRef, config) => {
springPhysicsMapRef.current.forEach((spring) => { springPhysicsMapRef.current.forEach((spring) => {
spring.reset(); spring.reset();
}); });
setInteractingAreaIndex(null); setInteractingAreaIndices(/* @__PURE__ */ new Set());
}, []); }, []);
return { return {
updateInteraction, updateInteraction,

2
dist/index.mjs.map vendored

File diff suppressed because one or more lines are too long

View File

@ -21,14 +21,14 @@ const isPointInPolygon = (point: Point, polygon: Point[]): boolean => {
/** /**
* *
* *
*/ */
export const useMouseInteraction = ( export const useMouseInteraction = (
containerRef: React.RefObject<HTMLElement | null>, containerRef: React.RefObject<HTMLElement | null>,
config: MouseInteractionConfig config: MouseInteractionConfig
) => { ) => {
const { getState } = useMouseVelocity(containerRef); const { getState } = useMouseVelocity(containerRef);
const [interactingAreaIndex, setInteractingAreaIndex] = useState<number | null>(null); const [interactingAreaIndices, setInteractingAreaIndices] = useState<Set<number>>(new Set());
const springPhysicsMapRef = useRef<Map<number, SpringPhysics>>(new Map()); const springPhysicsMapRef = useRef<Map<number, SpringPhysics>>(new Map());
/** /**
@ -51,33 +51,38 @@ export const useMouseInteraction = (
// 마우스 클릭/드래그 중이고 위치가 있으면 // 마우스 클릭/드래그 중이고 위치가 있으면
if (mouseState.isDragging && mouseState.position) { if (mouseState.isDragging && mouseState.position) {
// 아직 영역을 선택하지 않았으면 찾기 // 현재 마우스 위치가 포함된 모든 영역 찾기
if (interactingAreaIndex === null) { const currentlyInAreas = new Set<number>();
// 마우스 위치가 포함된 영역 찾기 (마지막 영역부터 - 위에 있는 영역 우선) for (let i = 0; i < areas.length; i++) {
for (let i = areas.length - 1; i >= 0; i--) {
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) { if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
setInteractingAreaIndex(i); currentlyInAreas.add(i);
// 해당 영역의 스프링 리셋
// 새로 진입한 영역이면 스프링 리셋
if (!interactingAreaIndices.has(i)) {
getSpringPhysics(i).reset(); getSpringPhysics(i).reset();
break;
} }
} }
} }
// 드래그 중인 영역이 있으면 마우스 방향으로 실시간 늘어남 // 이전에 인터랙션하던 영역에서 벗어났으면 평형으로 복귀
if (interactingAreaIndex !== null) { interactingAreaIndices.forEach((areaIndex) => {
if (!currentlyInAreas.has(areaIndex)) {
getSpringPhysics(areaIndex).returnToEquilibrium();
}
});
// 인터랙션 영역 업데이트
setInteractingAreaIndices(currentlyInAreas);
// 현재 위치의 모든 영역에 속도 적용
const velocityMult = config.velocityMultiplier || 1.0; const velocityMult = config.velocityMultiplier || 1.0;
const spring = getSpringPhysics(interactingAreaIndex);
// 속도 크기 확인
const velocityMag = Math.sqrt( const velocityMag = Math.sqrt(
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
); );
const minVel = config.minVelocity || 0.05; const minVel = config.minVelocity || 0.05;
const maxVel = config.maxVelocity || 5.0; const maxVel = config.maxVelocity || 5.0;
if (velocityMag >= minVel) { // 속도 클램핑
// 속도 클램핑 (너무 빠른 움직임 제한)
let clampedVelocity = mouseState.velocity; let clampedVelocity = mouseState.velocity;
if (velocityMag > maxVel) { if (velocityMag > maxVel) {
const scale = maxVel / velocityMag; const scale = maxVel / velocityMag;
@ -87,21 +92,24 @@ export const useMouseInteraction = (
}; };
} }
// 드래그 중: 클램핑된 마우스 속도를 목표로 설정 currentlyInAreas.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex);
if (velocityMag >= minVel) {
// 드래그 중: 마우스 속도를 목표로 설정
spring.setTarget(clampedVelocity, velocityMult); spring.setTarget(clampedVelocity, velocityMult);
} else { } else {
// 드래그 중이지만 마우스가 멈춰있으면 평형으로 복귀 // 드래그 중이지만 마우스가 멈춰있으면 평형으로 복귀
spring.returnToEquilibrium(); spring.returnToEquilibrium();
} }
} });
} else { } else {
// 마우스를 놓았으면 마지막 속도를 초기 속도로 설정하여 튕김 // 마우스를 놓았으면 인터랙션 중이던 모든 영역에 튕김 효과
if (interactingAreaIndex !== null) { if (interactingAreaIndices.size > 0) {
const velocityMult = config.velocityMultiplier || 1.0; const velocityMult = config.velocityMultiplier || 1.0;
const spring = getSpringPhysics(interactingAreaIndex);
const maxVel = config.maxVelocity || 5.0; const maxVel = config.maxVelocity || 5.0;
// 속도 클램핑 (놓을 때도 제한) // 속도 클램핑
const velocityMag = Math.sqrt( const velocityMag = Math.sqrt(
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
); );
@ -114,9 +122,13 @@ export const useMouseInteraction = (
}; };
} }
// 클램핑된 속도를 초기 속도로 설정하여 튕김 // 모든 인터랙션 영역에 초기 속도 설정
interactingAreaIndices.forEach((areaIndex) => {
const spring = getSpringPhysics(areaIndex);
spring.setInitialVelocity(clampedVelocity, velocityMult); spring.setInitialVelocity(clampedVelocity, velocityMult);
setInteractingAreaIndex(null); });
setInteractingAreaIndices(new Set());
} }
} }
@ -132,7 +144,7 @@ export const useMouseInteraction = (
Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 0.001; Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 0.001;
// 드래그 중이 아니고 스프링도 비활성이면 업데이트 안 함 // 드래그 중이 아니고 스프링도 비활성이면 업데이트 안 함
if (index !== interactingAreaIndex && !isSpringActive) { if (!interactingAreaIndices.has(index) && !isSpringActive) {
return area; return area;
} }
@ -157,7 +169,7 @@ export const useMouseInteraction = (
}, },
}; };
}); });
}, [config, getState, interactingAreaIndex, getSpringPhysics]); }, [config, getState, interactingAreaIndices, getSpringPhysics]);
/** /**
* *
@ -179,7 +191,7 @@ export const useMouseInteraction = (
springPhysicsMapRef.current.forEach((spring) => { springPhysicsMapRef.current.forEach((spring) => {
spring.reset(); spring.reset();
}); });
setInteractingAreaIndex(null); setInteractingAreaIndices(new Set());
}, []); }, []);
return { return {