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) => {
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) => {
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];

68
dist/index.js vendored
View File

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

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

68
dist/index.mjs vendored
View File

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