Add a hold trigger that trails the pointer

- SpriteEffectTrigger에 hold 추가 - 포인터가 영역 안에 머무는 동안 그 자리에서 계속 방출
- 매니저가 hover 영역을 따로 잡아 방출 중심을 포인터 위치로 넘김 (마우스는 올려두기만 해도, 터치는 누르는 동안)
- 버전 1.7.0으로 올림
This commit is contained in:
윤정민 2026-08-18 13:51:56 +09:00
parent 8188d8d79e
commit 692f9b1260
10 changed files with 72 additions and 35 deletions

11
dist/index.d.mts vendored
View File

@ -143,8 +143,13 @@ interface AnimationTicker {
resume: () => void; resume: () => void;
} }
/** 이펙트 트리거 타입 */ /**
type SpriteEffectTrigger = 'ambient' | 'touch'; *
* - ambient: 영역
* - touch: 영역에
* - hold: 포인터가 ()
*/
type SpriteEffectTrigger = 'ambient' | 'touch' | 'hold';
/** 블렌드 모드 */ /** 블렌드 모드 */
type SpriteBlendMode = 'normal' | 'additive'; type SpriteBlendMode = 'normal' | 'additive';
/** /**
@ -242,7 +247,7 @@ interface SpriteEffectConfig {
blendMode?: SpriteBlendMode; blendMode?: SpriteBlendMode;
/** 최대 파티클 수 */ /** 최대 파티클 수 */
maxParticles: number; maxParticles: number;
/** ambient: 초당 방출 수 */ /** ambient·hold: 초당 방출 수 */
emitRate?: number; emitRate?: number;
/** touch: 터치 시 방출 수 */ /** touch: 터치 시 방출 수 */
burstCount?: number; burstCount?: number;

11
dist/index.d.ts vendored
View File

@ -143,8 +143,13 @@ interface AnimationTicker {
resume: () => void; resume: () => void;
} }
/** 이펙트 트리거 타입 */ /**
type SpriteEffectTrigger = 'ambient' | 'touch'; *
* - ambient: 영역
* - touch: 영역에
* - hold: 포인터가 ()
*/
type SpriteEffectTrigger = 'ambient' | 'touch' | 'hold';
/** 블렌드 모드 */ /** 블렌드 모드 */
type SpriteBlendMode = 'normal' | 'additive'; type SpriteBlendMode = 'normal' | 'additive';
/** /**
@ -242,7 +247,7 @@ interface SpriteEffectConfig {
blendMode?: SpriteBlendMode; blendMode?: SpriteBlendMode;
/** 최대 파티클 수 */ /** 최대 파티클 수 */
maxParticles: number; maxParticles: number;
/** ambient: 초당 방출 수 */ /** ambient·hold: 초당 방출 수 */
emitRate?: number; emitRate?: number;
/** touch: 터치 시 방출 수 */ /** touch: 터치 시 방출 수 */
burstCount?: number; burstCount?: number;

19
dist/index.js vendored
View File

@ -503,6 +503,7 @@ var SpriteEffectInstance = class {
* 프레임 업데이트 * 프레임 업데이트
* @param deltaTime 단위 프레임 시간 * @param deltaTime 단위 프레임 시간
* @param emitCenter 방출 중심 (정규화 좌표 0-1) * @param emitCenter 방출 중심 (정규화 좌표 0-1)
* @param holdActive hold 이펙트에서 포인터가 영역 안에 있는지 - 있는 동안만 방출한다
*/ */
this._logCounter = 0; this._logCounter = 0;
this.config = config; this.config = config;
@ -650,9 +651,10 @@ var SpriteEffectInstance = class {
this.emitOne(center); this.emitOne(center);
} }
} }
update(deltaTime, emitCenter, resolution) { update(deltaTime, emitCenter, resolution, holdActive = false) {
if (!this.ready) return; if (!this.ready) return;
if (this.config.trigger === "ambient") { const emitting = this.config.trigger === "ambient" || this.config.trigger === "hold" && holdActive;
if (emitting) {
this.updateAmbientEmit(deltaTime, emitCenter); this.updateAmbientEmit(deltaTime, emitCenter);
} }
const overLifetime = this.config.overLifetime; const overLifetime = this.config.overLifetime;
@ -825,12 +827,13 @@ var SpriteEffectManager = class {
*/ */
update(effectAreas, deltaTime, touchState, resolution) { update(effectAreas, deltaTime, touchState, resolution) {
const currentTouchingAreas = /* @__PURE__ */ new Set(); const currentTouchingAreas = /* @__PURE__ */ new Set();
if (touchState.isDragging && touchState.position) { const currentHoveringAreas = /* @__PURE__ */ new Set();
if (touchState.position) {
for (const area of effectAreas) { for (const area of effectAreas) {
const radius = area.radius ?? 0.1; const radius = area.radius ?? 0.1;
if (isPointInCircle(touchState.position, area.position, radius, resolution)) { if (!isPointInCircle(touchState.position, area.position, radius, resolution)) continue;
currentTouchingAreas.add(area.id); currentHoveringAreas.add(area.id);
} if (touchState.isDragging) currentTouchingAreas.add(area.id);
} }
} }
for (const area of effectAreas) { for (const area of effectAreas) {
@ -844,7 +847,9 @@ var SpriteEffectManager = class {
instance.triggerBurst(touchState.position ?? area.position); instance.triggerBurst(touchState.position ?? area.position);
} }
} }
instance.update(deltaTime, area.position, resolution); const isHolding = effectConfig.trigger === "hold" && currentHoveringAreas.has(area.id);
const emitCenter = isHolding && touchState.position ? touchState.position : area.position;
instance.update(deltaTime, emitCenter, resolution, isHolding);
} }
} }
this.previousTouchingAreas = currentTouchingAreas; this.previousTouchingAreas = currentTouchingAreas;

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

19
dist/index.mjs vendored
View File

@ -442,6 +442,7 @@ var SpriteEffectInstance = class {
* 프레임 업데이트 * 프레임 업데이트
* @param deltaTime 단위 프레임 시간 * @param deltaTime 단위 프레임 시간
* @param emitCenter 방출 중심 (정규화 좌표 0-1) * @param emitCenter 방출 중심 (정규화 좌표 0-1)
* @param holdActive hold 이펙트에서 포인터가 영역 안에 있는지 - 있는 동안만 방출한다
*/ */
this._logCounter = 0; this._logCounter = 0;
this.config = config; this.config = config;
@ -589,9 +590,10 @@ var SpriteEffectInstance = class {
this.emitOne(center); this.emitOne(center);
} }
} }
update(deltaTime, emitCenter, resolution) { update(deltaTime, emitCenter, resolution, holdActive = false) {
if (!this.ready) return; if (!this.ready) return;
if (this.config.trigger === "ambient") { const emitting = this.config.trigger === "ambient" || this.config.trigger === "hold" && holdActive;
if (emitting) {
this.updateAmbientEmit(deltaTime, emitCenter); this.updateAmbientEmit(deltaTime, emitCenter);
} }
const overLifetime = this.config.overLifetime; const overLifetime = this.config.overLifetime;
@ -764,12 +766,13 @@ var SpriteEffectManager = class {
*/ */
update(effectAreas, deltaTime, touchState, resolution) { update(effectAreas, deltaTime, touchState, resolution) {
const currentTouchingAreas = /* @__PURE__ */ new Set(); const currentTouchingAreas = /* @__PURE__ */ new Set();
if (touchState.isDragging && touchState.position) { const currentHoveringAreas = /* @__PURE__ */ new Set();
if (touchState.position) {
for (const area of effectAreas) { for (const area of effectAreas) {
const radius = area.radius ?? 0.1; const radius = area.radius ?? 0.1;
if (isPointInCircle(touchState.position, area.position, radius, resolution)) { if (!isPointInCircle(touchState.position, area.position, radius, resolution)) continue;
currentTouchingAreas.add(area.id); currentHoveringAreas.add(area.id);
} if (touchState.isDragging) currentTouchingAreas.add(area.id);
} }
} }
for (const area of effectAreas) { for (const area of effectAreas) {
@ -783,7 +786,9 @@ var SpriteEffectManager = class {
instance.triggerBurst(touchState.position ?? area.position); instance.triggerBurst(touchState.position ?? area.position);
} }
} }
instance.update(deltaTime, area.position, resolution); const isHolding = effectConfig.trigger === "hold" && currentHoveringAreas.has(area.id);
const emitCenter = isHolding && touchState.position ? touchState.position : area.position;
instance.update(deltaTime, emitCenter, resolution, isHolding);
} }
} }
this.previousTouchingAreas = currentTouchingAreas; this.previousTouchingAreas = currentTouchingAreas;

2
dist/index.mjs.map vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"name": "@baekryang/responsive-image-canvas", "name": "@baekryang/responsive-image-canvas",
"version": "1.6.0", "version": "1.7.0",
"publishConfig": { "publishConfig": {
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/" "registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
}, },

View File

@ -240,14 +240,17 @@ export class SpriteEffectInstance {
* *
* @param deltaTime * @param deltaTime
* @param emitCenter ( 0-1) * @param emitCenter ( 0-1)
* @param holdActive hold -
*/ */
private _logCounter = 0; private _logCounter = 0;
update(deltaTime: number, emitCenter: Point, resolution?: { x: number; y: number }): void { update(deltaTime: number, emitCenter: Point, resolution?: { x: number; y: number }, holdActive = false): void {
if (!this.ready) return; if (!this.ready) return;
// ambient 방출 // 지속 방출 - ambient는 항상, hold는 포인터가 머무는 동안만
if (this.config.trigger === 'ambient') { const emitting = this.config.trigger === 'ambient'
|| (this.config.trigger === 'hold' && holdActive);
if (emitting) {
this.updateAmbientEmit(deltaTime, emitCenter); this.updateAmbientEmit(deltaTime, emitCenter);
} }

View File

@ -94,13 +94,15 @@ export class SpriteEffectManager {
update(effectAreas: SpriteEffectArea[], deltaTime: number, touchState: SpriteEffectTouchState, resolution?: { x: number; y: number }): void { update(effectAreas: SpriteEffectArea[], deltaTime: number, touchState: SpriteEffectTouchState, resolution?: { x: number; y: number }): void {
// 현재 터치 중인 영역 감지 // 현재 터치 중인 영역 감지
const currentTouchingAreas = new Set<string>(); const currentTouchingAreas = new Set<string>();
// 포인터가 머물고 있는 영역 - 마우스는 올려두기만 해도, 터치는 누르고 있는 동안 들어온다
const currentHoveringAreas = new Set<string>();
if (touchState.isDragging && touchState.position) { if (touchState.position) {
for (const area of effectAreas) { for (const area of effectAreas) {
const radius = area.radius ?? 0.1; const radius = area.radius ?? 0.1;
if (isPointInCircle(touchState.position, area.position, radius, resolution)) { if (!isPointInCircle(touchState.position, area.position, radius, resolution)) continue;
currentTouchingAreas.add(area.id); currentHoveringAreas.add(area.id);
} if (touchState.isDragging) currentTouchingAreas.add(area.id);
} }
} }
@ -120,8 +122,15 @@ export class SpriteEffectManager {
} }
} }
// 매 프레임 업데이트 (ambient 방출 + 파티클 물리) // hold 이펙트: 포인터가 머무는 동안 그 자리에서 계속 방출 - 트레일처럼 따라온다
instance.update(deltaTime, area.position, resolution); const isHolding = effectConfig.trigger === 'hold'
&& currentHoveringAreas.has(area.id);
const emitCenter = isHolding && touchState.position
? touchState.position
: area.position;
// 매 프레임 업데이트 (지속 방출 + 파티클 물리)
instance.update(deltaTime, emitCenter, resolution, isHolding);
} }
} }

View File

@ -1,7 +1,12 @@
import type { Point } from './area'; import type { Point } from './area';
/** 이펙트 트리거 타입 */ /**
export type SpriteEffectTrigger = 'ambient' | 'touch'; *
* - ambient: 영역
* - touch: 영역에
* - hold: 포인터가 ()
*/
export type SpriteEffectTrigger = 'ambient' | 'touch' | 'hold';
/** 블렌드 모드 */ /** 블렌드 모드 */
export type SpriteBlendMode = 'normal' | 'additive'; export type SpriteBlendMode = 'normal' | 'additive';
@ -99,7 +104,7 @@ export interface SpriteEffectConfig {
blendMode?: SpriteBlendMode; blendMode?: SpriteBlendMode;
/** 최대 파티클 수 */ /** 최대 파티클 수 */
maxParticles: number; maxParticles: number;
/** ambient: 초당 방출 수 */ /** ambient·hold: 초당 방출 수 */
emitRate?: number; emitRate?: number;
/** touch: 터치 시 방출 수 */ /** touch: 터치 시 방출 수 */
burstCount?: number; burstCount?: number;