responsive-image-canvas/src/engine/SpriteEffectManager.ts
BaekRyang 769934adff Fix aspect ratio in circle hit detection logic
- 캔버스 종횡비에 따른 원형 영역 클릭 감지 로직 수정
- SpriteEffectManager의 원형 판정 함수에 aspect ratio 계산 추가
- 패키지 버전을 1.5.4로 업데이트
2026-03-16 16:53:13 +09:00

148 lines
4.5 KiB
TypeScript

import * as THREE from 'three';
import type { Point } from '@/types';
import type { SpriteEffectArea } from '@/types/spriteEffect';
import { SpriteEffectInstance } from './SpriteEffectInstance';
/**
* 터치/마우스 상태 (스프라이트 이펙트 전용)
*/
export interface SpriteEffectTouchState {
/** 마우스/터치 위치 (정규화 좌표, null이면 미접촉) */
position: Point | null;
/** 드래그 중 여부 */
isDragging: boolean;
}
/**
* 점이 원 안에 있는지 확인
*/
const isPointInCircle = (point: Point, center: Point, radius: number, resolution?: { x: number; y: number }): boolean => {
const aspect = resolution ? resolution.x / resolution.y : 1;
const dx = point.x - center.x;
const dy = (point.y - center.y) / aspect;
return dx * dx + dy * dy <= radius * radius;
};
/**
* 스프라이트 이펙트 전체 관리자
* ImageDistortion 컴포넌트에서 생성하여 사용하는 최상위 진입점
* 왜곡 영역(DistortionArea)과 독립적으로 이펙트 영역을 관리
*/
export class SpriteEffectManager {
/** 모든 이펙트 메쉬를 담는 그룹 */
private effectGroup: THREE.Group;
/** 영역ID+이펙트ID → 인스턴스 맵 */
private instances: Map<string, SpriteEffectInstance> = new Map();
/** 이전 프레임에서 터치 중이던 영역 ID 세트 (버스트 감지용) */
private previousTouchingAreas: Set<string> = new Set();
constructor() {
this.effectGroup = new THREE.Group();
this.effectGroup.renderOrder = 1;
}
/**
* Three.js 씬에 이펙트 그룹 추가
*/
attachToScene(scene: THREE.Scene): void {
scene.add(this.effectGroup);
}
/**
* 이펙트 영역 설정 변경을 감지하여 인스턴스 생성/제거
*/
syncEffects(effectAreas: SpriteEffectArea[]): void {
console.log('[SpriteEffectManager] syncEffects 호출:', effectAreas.length, '개 영역');
const activeKeys = new Set<string>();
for (const area of effectAreas) {
for (const effectConfig of area.effects) {
const key = `${area.id}::${effectConfig.id}`;
activeKeys.add(key);
// 이미 존재하면 설정만 업데이트
const existing = this.instances.get(key);
if (existing) {
existing.updateConfig(effectConfig);
continue;
}
// 새 인스턴스 생성
console.log('[SpriteEffectManager] 인스턴스 생성:', key, effectConfig.emoji ?? effectConfig.spriteUrl);
const instance = new SpriteEffectInstance(effectConfig);
this.instances.set(key, instance);
this.effectGroup.add(instance.group);
}
}
// 더 이상 사용되지 않는 인스턴스 제거
for (const [key, instance] of this.instances) {
if (!activeKeys.has(key)) {
instance.dispose();
this.effectGroup.remove(instance.group);
this.instances.delete(key);
}
}
}
/**
* 매 프레임 업데이트
* @param effectAreas 이펙트 영역 배열
* @param deltaTime 초 단위 프레임 시간
* @param touchState 마우스/터치 상태
*/
update(effectAreas: SpriteEffectArea[], deltaTime: number, touchState: SpriteEffectTouchState, resolution?: { x: number; y: number }): void {
// 현재 터치 중인 영역 감지
const currentTouchingAreas = new Set<string>();
if (touchState.isDragging && touchState.position) {
for (const area of effectAreas) {
const radius = area.radius ?? 0.1;
if (isPointInCircle(touchState.position, area.position, radius, resolution)) {
currentTouchingAreas.add(area.id);
}
}
}
// 각 영역의 이펙트 업데이트
for (const area of effectAreas) {
for (const effectConfig of area.effects) {
const key = `${area.id}::${effectConfig.id}`;
const instance = this.instances.get(key);
if (!instance) continue;
// touch 이펙트: 새로 터치된 영역이면 버스트
if (effectConfig.trigger === 'touch') {
const isNewTouch = currentTouchingAreas.has(area.id)
&& !this.previousTouchingAreas.has(area.id);
if (isNewTouch) {
instance.triggerBurst(touchState.position ?? area.position);
}
}
// 매 프레임 업데이트 (ambient 방출 + 파티클 물리)
instance.update(deltaTime, area.position, resolution);
}
}
// 터치 상태 갱신
this.previousTouchingAreas = currentTouchingAreas;
}
/**
* 리소스 정리
*/
dispose(): void {
for (const [, instance] of this.instances) {
instance.dispose();
}
this.instances.clear();
this.previousTouchingAreas.clear();
// effectGroup을 부모에서 제거
if (this.effectGroup.parent) {
this.effectGroup.parent.remove(this.effectGroup);
}
}
}