From 672dd80b9dc236687a9a41e8e206fca126c34743 Mon Sep 17 00:00:00 2001 From: BaekRyang Date: Fri, 13 Mar 2026 13:24:55 +0900 Subject: [PATCH] Add support for emoji particles in SpriteEffect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - spriteEffect 타입에 emoji 필드 추가 및 spriteUrl을 선택 사항으로 변경 - SpriteEffectInstance 내 이모지 기반 캔버스 텍스처 생성 기능 구현 - SpriteEffectManager 인스턴스 생성 로그에 이모지 정보 출력 대응 - 패키지 버전을 1.4.1으로 업데이트 --- package.json | 2 +- src/engine/SpriteEffectInstance.ts | 42 ++++++++++++++++++++++++++++-- src/engine/SpriteEffectManager.ts | 2 +- src/types/spriteEffect.ts | 11 +++++--- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 6e1455c..ce6eb28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@baekryang/responsive-image-canvas", - "version": "1.3.0", + "version": "1.4.1", "publishConfig": { "registry": "https://git.bnovalab.com/api/packages/baekryang/npm/" }, diff --git a/src/engine/SpriteEffectInstance.ts b/src/engine/SpriteEffectInstance.ts index 0fbf1fa..534387d 100644 --- a/src/engine/SpriteEffectInstance.ts +++ b/src/engine/SpriteEffectInstance.ts @@ -78,8 +78,14 @@ export class SpriteEffectInstance { return mesh; }); - // 텍스처 비동기 로드 - this.loadTexture(config.spriteUrl); + // 이모지 또는 URL 텍스처 로드 + if (config.emoji) { + this.createEmojiTexture(config.emoji); + } else if (config.spriteUrl) { + this.loadTexture(config.spriteUrl); + } else { + console.error('[SpriteEffectInstance] spriteUrl 또는 emoji 중 하나는 필수입니다.'); + } } /** 런타임 설정 업데이트 (maxParticles 제외) */ @@ -125,6 +131,38 @@ export class SpriteEffectInstance { ); } + /** 이모지를 Canvas에 렌더링하여 텍스처 생성 */ + private createEmojiTexture(emoji: string): void { + const size = 128; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + console.error('[SpriteEffectInstance] Canvas 2D 컨텍스트 생성 실패'); + return; + } + + ctx.font = '100px serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(emoji, size / 2, size / 2); + + const texture = new THREE.CanvasTexture(canvas); + this.texture = texture; + + // 각 메쉬에 텍스처 적용 (이모지는 스프라이트 시트 불필요) + for (const mesh of this.meshes) { + const mat = mesh.material as THREE.MeshBasicMaterial; + mat.map = texture; + mat.needsUpdate = true; + } + + this.ready = true; + console.log(`[SpriteEffectInstance] 이모지 텍스처 생성 완료: ${emoji}`); + } + /** * 파티클 1개 방출 * @param center 방출 중심 (정규화 좌표 0-1) diff --git a/src/engine/SpriteEffectManager.ts b/src/engine/SpriteEffectManager.ts index 17469bc..7f20a10 100644 --- a/src/engine/SpriteEffectManager.ts +++ b/src/engine/SpriteEffectManager.ts @@ -67,7 +67,7 @@ export class SpriteEffectManager { } // 새 인스턴스 생성 - console.log('[SpriteEffectManager] 인스턴스 생성:', key, effectConfig.spriteUrl); + console.log('[SpriteEffectManager] 인스턴스 생성:', key, effectConfig.emoji ?? effectConfig.spriteUrl); const instance = new SpriteEffectInstance(effectConfig); this.instances.set(key, instance); this.effectGroup.add(instance.group); diff --git a/src/types/spriteEffect.ts b/src/types/spriteEffect.ts index cf27afa..20ec1ce 100644 --- a/src/types/spriteEffect.ts +++ b/src/types/spriteEffect.ts @@ -62,7 +62,10 @@ export interface SpriteEffectAreaData { effects: Array<{ id: string; trigger: SpriteEffectTrigger; - spriteUrl: string; + /** spriteUrl 또는 emoji 중 하나 필수 */ + spriteUrl?: string; + /** 이모지 파티클 (spriteUrl 대신 사용 가능) */ + emoji?: string; blendMode?: SpriteBlendMode; maxParticles: number; emitRate?: number; @@ -86,8 +89,10 @@ export interface SpriteEffectConfig { id: string; /** 트리거 타입 */ trigger: SpriteEffectTrigger; - /** 스프라이트 이미지 URL */ - spriteUrl: string; + /** 스프라이트 이미지 URL (emoji와 택1) */ + spriteUrl?: string; + /** 이모지 파티클 (spriteUrl 대신 사용 가능) */ + emoji?: string; /** 블렌드 모드 (기본: 'normal') */ blendMode?: SpriteBlendMode; /** 최대 파티클 수 */