Compare commits
2 Commits
15144240b7
...
09cb2f09af
| Author | SHA1 | Date | |
|---|---|---|---|
| 09cb2f09af | |||
|
|
769934adff |
24
dist/index.d.mts
vendored
24
dist/index.d.mts
vendored
@ -151,10 +151,10 @@ type SpriteBlendMode = 'normal' | 'additive';
|
|||||||
* 수명 기반 파티클 속성 보간 설정
|
* 수명 기반 파티클 속성 보간 설정
|
||||||
*/
|
*/
|
||||||
interface SpriteParticleOverLifetime {
|
interface SpriteParticleOverLifetime {
|
||||||
/** [시작, 끝] 스케일 */
|
/** 스케일 보간: [시작, 끝] 또는 [시작, 중간, 끝] */
|
||||||
scale?: [number, number];
|
scale?: number[];
|
||||||
/** [시작, 끝] 투명도 */
|
/** 투명도 보간: [시작, 끝] 또는 [시작, 중간, 끝] */
|
||||||
opacity?: [number, number];
|
opacity?: number[];
|
||||||
/** 회전 속도 (라디안/초) */
|
/** 회전 속도 (라디안/초) */
|
||||||
rotationSpeed?: number;
|
rotationSpeed?: number;
|
||||||
/** 속도 감쇠 (0-1, 매 프레임 속도에 곱해짐) */
|
/** 속도 감쇠 (0-1, 매 프레임 속도에 곱해짐) */
|
||||||
@ -203,7 +203,10 @@ interface SpriteEffectAreaData {
|
|||||||
effects: Array<{
|
effects: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
trigger: SpriteEffectTrigger;
|
trigger: SpriteEffectTrigger;
|
||||||
spriteUrl: string;
|
/** spriteUrl 또는 emoji 중 하나 필수 */
|
||||||
|
spriteUrl?: string;
|
||||||
|
/** 이모지 파티클 (spriteUrl 대신 사용 가능) */
|
||||||
|
emoji?: string;
|
||||||
blendMode?: SpriteBlendMode;
|
blendMode?: SpriteBlendMode;
|
||||||
maxParticles: number;
|
maxParticles: number;
|
||||||
emitRate?: number;
|
emitRate?: number;
|
||||||
@ -229,8 +232,10 @@ interface SpriteEffectConfig {
|
|||||||
id: string;
|
id: string;
|
||||||
/** 트리거 타입 */
|
/** 트리거 타입 */
|
||||||
trigger: SpriteEffectTrigger;
|
trigger: SpriteEffectTrigger;
|
||||||
/** 스프라이트 이미지 URL */
|
/** 스프라이트 이미지 URL (emoji와 택1) */
|
||||||
spriteUrl: string;
|
spriteUrl?: string;
|
||||||
|
/** 이모지 파티클 (spriteUrl 대신 사용 가능) */
|
||||||
|
emoji?: string;
|
||||||
/** 블렌드 모드 (기본: 'normal') */
|
/** 블렌드 모드 (기본: 'normal') */
|
||||||
blendMode?: SpriteBlendMode;
|
blendMode?: SpriteBlendMode;
|
||||||
/** 최대 파티클 수 */
|
/** 최대 파티클 수 */
|
||||||
@ -802,7 +807,10 @@ declare class SpriteEffectManager {
|
|||||||
* @param deltaTime 초 단위 프레임 시간
|
* @param deltaTime 초 단위 프레임 시간
|
||||||
* @param touchState 마우스/터치 상태
|
* @param touchState 마우스/터치 상태
|
||||||
*/
|
*/
|
||||||
update(effectAreas: SpriteEffectArea[], deltaTime: number, touchState: SpriteEffectTouchState): void;
|
update(effectAreas: SpriteEffectArea[], deltaTime: number, touchState: SpriteEffectTouchState, resolution?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}): void;
|
||||||
/**
|
/**
|
||||||
* 리소스 정리
|
* 리소스 정리
|
||||||
*/
|
*/
|
||||||
|
|||||||
24
dist/index.d.ts
vendored
24
dist/index.d.ts
vendored
@ -151,10 +151,10 @@ type SpriteBlendMode = 'normal' | 'additive';
|
|||||||
* 수명 기반 파티클 속성 보간 설정
|
* 수명 기반 파티클 속성 보간 설정
|
||||||
*/
|
*/
|
||||||
interface SpriteParticleOverLifetime {
|
interface SpriteParticleOverLifetime {
|
||||||
/** [시작, 끝] 스케일 */
|
/** 스케일 보간: [시작, 끝] 또는 [시작, 중간, 끝] */
|
||||||
scale?: [number, number];
|
scale?: number[];
|
||||||
/** [시작, 끝] 투명도 */
|
/** 투명도 보간: [시작, 끝] 또는 [시작, 중간, 끝] */
|
||||||
opacity?: [number, number];
|
opacity?: number[];
|
||||||
/** 회전 속도 (라디안/초) */
|
/** 회전 속도 (라디안/초) */
|
||||||
rotationSpeed?: number;
|
rotationSpeed?: number;
|
||||||
/** 속도 감쇠 (0-1, 매 프레임 속도에 곱해짐) */
|
/** 속도 감쇠 (0-1, 매 프레임 속도에 곱해짐) */
|
||||||
@ -203,7 +203,10 @@ interface SpriteEffectAreaData {
|
|||||||
effects: Array<{
|
effects: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
trigger: SpriteEffectTrigger;
|
trigger: SpriteEffectTrigger;
|
||||||
spriteUrl: string;
|
/** spriteUrl 또는 emoji 중 하나 필수 */
|
||||||
|
spriteUrl?: string;
|
||||||
|
/** 이모지 파티클 (spriteUrl 대신 사용 가능) */
|
||||||
|
emoji?: string;
|
||||||
blendMode?: SpriteBlendMode;
|
blendMode?: SpriteBlendMode;
|
||||||
maxParticles: number;
|
maxParticles: number;
|
||||||
emitRate?: number;
|
emitRate?: number;
|
||||||
@ -229,8 +232,10 @@ interface SpriteEffectConfig {
|
|||||||
id: string;
|
id: string;
|
||||||
/** 트리거 타입 */
|
/** 트리거 타입 */
|
||||||
trigger: SpriteEffectTrigger;
|
trigger: SpriteEffectTrigger;
|
||||||
/** 스프라이트 이미지 URL */
|
/** 스프라이트 이미지 URL (emoji와 택1) */
|
||||||
spriteUrl: string;
|
spriteUrl?: string;
|
||||||
|
/** 이모지 파티클 (spriteUrl 대신 사용 가능) */
|
||||||
|
emoji?: string;
|
||||||
/** 블렌드 모드 (기본: 'normal') */
|
/** 블렌드 모드 (기본: 'normal') */
|
||||||
blendMode?: SpriteBlendMode;
|
blendMode?: SpriteBlendMode;
|
||||||
/** 최대 파티클 수 */
|
/** 최대 파티클 수 */
|
||||||
@ -802,7 +807,10 @@ declare class SpriteEffectManager {
|
|||||||
* @param deltaTime 초 단위 프레임 시간
|
* @param deltaTime 초 단위 프레임 시간
|
||||||
* @param touchState 마우스/터치 상태
|
* @param touchState 마우스/터치 상태
|
||||||
*/
|
*/
|
||||||
update(effectAreas: SpriteEffectArea[], deltaTime: number, touchState: SpriteEffectTouchState): void;
|
update(effectAreas: SpriteEffectArea[], deltaTime: number, touchState: SpriteEffectTouchState, resolution?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}): void;
|
||||||
/**
|
/**
|
||||||
* 리소스 정리
|
* 리소스 정리
|
||||||
*/
|
*/
|
||||||
|
|||||||
105
dist/index.js
vendored
105
dist/index.js
vendored
@ -434,6 +434,7 @@ var SpriteParticlePool = class {
|
|||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
velocity: { x: 0, y: 0 },
|
velocity: { x: 0, y: 0 },
|
||||||
scale: 1,
|
scale: 1,
|
||||||
|
initialScale: 1,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
age: 0,
|
age: 0,
|
||||||
@ -483,6 +484,16 @@ var SpriteParticlePool = class {
|
|||||||
// src/engine/SpriteEffectInstance.ts
|
// src/engine/SpriteEffectInstance.ts
|
||||||
var randomRange = (min, max) => min + Math.random() * (max - min);
|
var randomRange = (min, max) => min + Math.random() * (max - min);
|
||||||
var lerp = (a, b, t) => a + (b - a) * t;
|
var lerp = (a, b, t) => a + (b - a) * t;
|
||||||
|
var lerpKeyframes = (values, t) => {
|
||||||
|
if (values.length === 2) {
|
||||||
|
return lerp(values[0], values[1], t);
|
||||||
|
}
|
||||||
|
const segments = values.length - 1;
|
||||||
|
const segT = t * segments;
|
||||||
|
const idx = Math.min(Math.floor(segT), segments - 1);
|
||||||
|
const localT = segT - idx;
|
||||||
|
return lerp(values[idx], values[idx + 1], localT);
|
||||||
|
};
|
||||||
var SpriteEffectInstance = class {
|
var SpriteEffectInstance = class {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.texture = null;
|
this.texture = null;
|
||||||
@ -513,7 +524,17 @@ var SpriteEffectInstance = class {
|
|||||||
this.group.add(mesh);
|
this.group.add(mesh);
|
||||||
return mesh;
|
return mesh;
|
||||||
});
|
});
|
||||||
|
if (config.emoji) {
|
||||||
|
this.createEmojiTexture(config.emoji);
|
||||||
|
} else if (config.spriteUrl) {
|
||||||
this.loadTexture(config.spriteUrl);
|
this.loadTexture(config.spriteUrl);
|
||||||
|
} else {
|
||||||
|
console.error("[SpriteEffectInstance] spriteUrl \uB610\uB294 emoji \uC911 \uD558\uB098\uB294 \uD544\uC218\uC785\uB2C8\uB2E4.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 런타임 설정 업데이트 (maxParticles 제외) */
|
||||||
|
updateConfig(config) {
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
/** 텍스처 로드 */
|
/** 텍스처 로드 */
|
||||||
loadTexture(url) {
|
loadTexture(url) {
|
||||||
@ -548,6 +569,31 @@ var SpriteEffectInstance = class {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/** 이모지를 Canvas에 렌더링하여 텍스처 생성 */
|
||||||
|
createEmojiTexture(emoji) {
|
||||||
|
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 \uCEE8\uD14D\uC2A4\uD2B8 \uC0DD\uC131 \uC2E4\uD328");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.font = "100px serif";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(emoji, size / 2, size / 2);
|
||||||
|
const texture = new THREE2.CanvasTexture(canvas);
|
||||||
|
this.texture = texture;
|
||||||
|
for (const mesh of this.meshes) {
|
||||||
|
const mat = mesh.material;
|
||||||
|
mat.map = texture;
|
||||||
|
mat.needsUpdate = true;
|
||||||
|
}
|
||||||
|
this.ready = true;
|
||||||
|
console.log(`[SpriteEffectInstance] \uC774\uBAA8\uC9C0 \uD14D\uC2A4\uCC98 \uC0DD\uC131 \uC644\uB8CC: ${emoji}`);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 파티클 1개 방출
|
* 파티클 1개 방출
|
||||||
* @param center 방출 중심 (정규화 좌표 0-1)
|
* @param center 방출 중심 (정규화 좌표 0-1)
|
||||||
@ -572,7 +618,8 @@ var SpriteEffectInstance = class {
|
|||||||
const speed = randomRange(config.initialSpeed[0], config.initialSpeed[1]);
|
const speed = randomRange(config.initialSpeed[0], config.initialSpeed[1]);
|
||||||
particle.velocity.x = Math.cos(angleRad) * speed;
|
particle.velocity.x = Math.cos(angleRad) * speed;
|
||||||
particle.velocity.y = Math.sin(angleRad) * speed;
|
particle.velocity.y = Math.sin(angleRad) * speed;
|
||||||
particle.scale = randomRange(config.initialScale[0], config.initialScale[1]);
|
particle.initialScale = randomRange(config.initialScale[0], config.initialScale[1]);
|
||||||
|
particle.scale = particle.initialScale;
|
||||||
particle.rotation = 0;
|
particle.rotation = 0;
|
||||||
particle.opacity = 1;
|
particle.opacity = 1;
|
||||||
particle.lifetime = randomRange(config.lifetime[0], config.lifetime[1]);
|
particle.lifetime = randomRange(config.lifetime[0], config.lifetime[1]);
|
||||||
@ -602,7 +649,7 @@ var SpriteEffectInstance = class {
|
|||||||
this.emitOne(center);
|
this.emitOne(center);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update(deltaTime, emitCenter) {
|
update(deltaTime, emitCenter, resolution) {
|
||||||
if (!this.ready) return;
|
if (!this.ready) return;
|
||||||
if (this.config.trigger === "ambient") {
|
if (this.config.trigger === "ambient") {
|
||||||
this.updateAmbientEmit(deltaTime, emitCenter);
|
this.updateAmbientEmit(deltaTime, emitCenter);
|
||||||
@ -623,10 +670,10 @@ var SpriteEffectInstance = class {
|
|||||||
const lifeRatio = particle.age / particle.lifetime;
|
const lifeRatio = particle.age / particle.lifetime;
|
||||||
if (overLifetime) {
|
if (overLifetime) {
|
||||||
if (overLifetime.scale) {
|
if (overLifetime.scale) {
|
||||||
particle.scale = lerp(overLifetime.scale[0], overLifetime.scale[1], lifeRatio);
|
particle.scale = particle.initialScale * lerpKeyframes(overLifetime.scale, lifeRatio);
|
||||||
}
|
}
|
||||||
if (overLifetime.opacity) {
|
if (overLifetime.opacity) {
|
||||||
particle.opacity = lerp(overLifetime.opacity[0], overLifetime.opacity[1], lifeRatio);
|
particle.opacity = lerpKeyframes(overLifetime.opacity, lifeRatio);
|
||||||
}
|
}
|
||||||
if (overLifetime.rotationSpeed) {
|
if (overLifetime.rotationSpeed) {
|
||||||
particle.rotation += overLifetime.rotationSpeed * deltaTime;
|
particle.rotation += overLifetime.rotationSpeed * deltaTime;
|
||||||
@ -642,7 +689,7 @@ var SpriteEffectInstance = class {
|
|||||||
}
|
}
|
||||||
particle.position.x += particle.velocity.x * deltaTime;
|
particle.position.x += particle.velocity.x * deltaTime;
|
||||||
particle.position.y += particle.velocity.y * deltaTime;
|
particle.position.y += particle.velocity.y * deltaTime;
|
||||||
this.syncMesh(particle);
|
this.syncMesh(particle, resolution);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -667,7 +714,7 @@ var SpriteEffectInstance = class {
|
|||||||
* 파티클 상태를 Three.js 메쉬에 동기화
|
* 파티클 상태를 Three.js 메쉬에 동기화
|
||||||
* 정규화 좌표(0-1) → NDC(-1~1) 변환, y축 반전
|
* 정규화 좌표(0-1) → NDC(-1~1) 변환, y축 반전
|
||||||
*/
|
*/
|
||||||
syncMesh(particle) {
|
syncMesh(particle, resolution) {
|
||||||
const mesh = this.meshes[particle.index];
|
const mesh = this.meshes[particle.index];
|
||||||
if (!mesh) return;
|
if (!mesh) return;
|
||||||
if (!particle.active) {
|
if (!particle.active) {
|
||||||
@ -678,7 +725,8 @@ var SpriteEffectInstance = class {
|
|||||||
mesh.position.x = particle.position.x * 2 - 1;
|
mesh.position.x = particle.position.x * 2 - 1;
|
||||||
mesh.position.y = -(particle.position.y * 2 - 1);
|
mesh.position.y = -(particle.position.y * 2 - 1);
|
||||||
mesh.position.z = -0.01;
|
mesh.position.z = -0.01;
|
||||||
mesh.scale.set(particle.scale, particle.scale, 1);
|
const aspect = resolution ? resolution.x / resolution.y : 1;
|
||||||
|
mesh.scale.set(particle.scale / aspect, particle.scale, 1);
|
||||||
mesh.rotation.z = particle.rotation;
|
mesh.rotation.z = particle.rotation;
|
||||||
const mat = mesh.material;
|
const mat = mesh.material;
|
||||||
mat.opacity = particle.opacity;
|
mat.opacity = particle.opacity;
|
||||||
@ -718,9 +766,10 @@ var SpriteEffectInstance = class {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src/engine/SpriteEffectManager.ts
|
// src/engine/SpriteEffectManager.ts
|
||||||
var isPointInCircle = (point, center, radius) => {
|
var isPointInCircle = (point, center, radius, resolution) => {
|
||||||
|
const aspect = resolution ? resolution.x / resolution.y : 1;
|
||||||
const dx = point.x - center.x;
|
const dx = point.x - center.x;
|
||||||
const dy = point.y - center.y;
|
const dy = (point.y - center.y) / aspect;
|
||||||
return dx * dx + dy * dy <= radius * radius;
|
return dx * dx + dy * dy <= radius * radius;
|
||||||
};
|
};
|
||||||
var SpriteEffectManager = class {
|
var SpriteEffectManager = class {
|
||||||
@ -748,8 +797,12 @@ var SpriteEffectManager = class {
|
|||||||
for (const effectConfig of area.effects) {
|
for (const effectConfig of area.effects) {
|
||||||
const key = `${area.id}::${effectConfig.id}`;
|
const key = `${area.id}::${effectConfig.id}`;
|
||||||
activeKeys.add(key);
|
activeKeys.add(key);
|
||||||
if (this.instances.has(key)) continue;
|
const existing = this.instances.get(key);
|
||||||
console.log("[SpriteEffectManager] \uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131:", key, effectConfig.spriteUrl);
|
if (existing) {
|
||||||
|
existing.updateConfig(effectConfig);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log("[SpriteEffectManager] \uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131:", key, effectConfig.emoji ?? effectConfig.spriteUrl);
|
||||||
const instance = new SpriteEffectInstance(effectConfig);
|
const instance = new SpriteEffectInstance(effectConfig);
|
||||||
this.instances.set(key, instance);
|
this.instances.set(key, instance);
|
||||||
this.effectGroup.add(instance.group);
|
this.effectGroup.add(instance.group);
|
||||||
@ -769,12 +822,12 @@ var SpriteEffectManager = class {
|
|||||||
* @param deltaTime 초 단위 프레임 시간
|
* @param deltaTime 초 단위 프레임 시간
|
||||||
* @param touchState 마우스/터치 상태
|
* @param touchState 마우스/터치 상태
|
||||||
*/
|
*/
|
||||||
update(effectAreas, deltaTime, touchState) {
|
update(effectAreas, deltaTime, touchState, resolution) {
|
||||||
const currentTouchingAreas = /* @__PURE__ */ new Set();
|
const currentTouchingAreas = /* @__PURE__ */ new Set();
|
||||||
if (touchState.isDragging && touchState.position) {
|
if (touchState.isDragging && 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)) {
|
if (isPointInCircle(touchState.position, area.position, radius, resolution)) {
|
||||||
currentTouchingAreas.add(area.id);
|
currentTouchingAreas.add(area.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -790,7 +843,7 @@ var SpriteEffectManager = class {
|
|||||||
instance.triggerBurst(touchState.position ?? area.position);
|
instance.triggerBurst(touchState.position ?? area.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
instance.update(deltaTime, area.position);
|
instance.update(deltaTime, area.position, resolution);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.previousTouchingAreas = currentTouchingAreas;
|
this.previousTouchingAreas = currentTouchingAreas;
|
||||||
@ -1260,6 +1313,14 @@ var DEFAULT_AREA = {
|
|||||||
SNAP_STEPS: 0
|
SNAP_STEPS: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// src/utils/describeSrc.ts
|
||||||
|
var MAX_LOGGED_LENGTH = 1e3;
|
||||||
|
function describeSrc(src) {
|
||||||
|
if (!src) return String(src);
|
||||||
|
if (src.length <= MAX_LOGGED_LENGTH) return src;
|
||||||
|
return `[\uC0DD\uB7B5: ${src.length.toLocaleString()}\uC790]`;
|
||||||
|
}
|
||||||
|
|
||||||
// src/components/ImageDistortion.tsx
|
// src/components/ImageDistortion.tsx
|
||||||
var import_jsx_runtime = require("react/jsx-runtime");
|
var import_jsx_runtime = require("react/jsx-runtime");
|
||||||
var ImageDistortion = ({
|
var ImageDistortion = ({
|
||||||
@ -1324,7 +1385,7 @@ var ImageDistortion = ({
|
|||||||
console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("[ImageDistortion] \uCD08\uAE30\uD654 \uC2DC\uC791");
|
console.log("[ImageDistortion] v1.5.1 \uCD08\uAE30\uD654 \uC2DC\uC791");
|
||||||
const scene = new ThreeScene(containerRef.current);
|
const scene = new ThreeScene(containerRef.current);
|
||||||
sceneRef.current = scene;
|
sceneRef.current = scene;
|
||||||
const vertPath = vertexShaderPath || "/shaders/distortion.vert.glsl";
|
const vertPath = vertexShaderPath || "/shaders/distortion.vert.glsl";
|
||||||
@ -1346,10 +1407,13 @@ var ImageDistortion = ({
|
|||||||
}, [vertexShaderPath, fragmentShaderPath]);
|
}, [vertexShaderPath, fragmentShaderPath]);
|
||||||
(0, import_react4.useEffect)(() => {
|
(0, import_react4.useEffect)(() => {
|
||||||
if (!imageSrc || !isReady) {
|
if (!imageSrc || !isReady) {
|
||||||
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady });
|
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", {
|
||||||
|
imageSrc: describeSrc(imageSrc),
|
||||||
|
isReady
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2DC\uC791:", imageSrc);
|
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2DC\uC791:", describeSrc(imageSrc));
|
||||||
setImageLoaded(false);
|
setImageLoaded(false);
|
||||||
const loader = new THREE4.TextureLoader();
|
const loader = new THREE4.TextureLoader();
|
||||||
loader.load(
|
loader.load(
|
||||||
@ -1445,13 +1509,15 @@ var ImageDistortion = ({
|
|||||||
});
|
});
|
||||||
if (spriteManagerRef.current) {
|
if (spriteManagerRef.current) {
|
||||||
const mouseState = mouseInteractionHook.getMouseState();
|
const mouseState = mouseInteractionHook.getMouseState();
|
||||||
|
const resolution = sceneRef.current?.getResolution() ?? { x: 1, y: 1 };
|
||||||
spriteManagerRef.current.update(
|
spriteManagerRef.current.update(
|
||||||
spriteEffectAreas,
|
spriteEffectAreas,
|
||||||
deltaTime,
|
deltaTime,
|
||||||
{
|
{
|
||||||
position: mouseState.position ?? null,
|
position: mouseState.position ?? null,
|
||||||
isDragging: mouseState.isDragging
|
isDragging: mouseState.isDragging
|
||||||
}
|
},
|
||||||
|
resolution
|
||||||
);
|
);
|
||||||
sceneRef.current?.render();
|
sceneRef.current?.render();
|
||||||
}
|
}
|
||||||
@ -1900,7 +1966,8 @@ var EditorCanvas = ({
|
|||||||
for (let i = spriteEffectAreas.length - 1; i >= 0; i--) {
|
for (let i = spriteEffectAreas.length - 1; i >= 0; i--) {
|
||||||
const sa = spriteEffectAreas[i];
|
const sa = spriteEffectAreas[i];
|
||||||
const dx = clickPoint.x - sa.position.x;
|
const dx = clickPoint.x - sa.position.x;
|
||||||
const dy = clickPoint.y - sa.position.y;
|
const aspect = rect.width / rect.height;
|
||||||
|
const dy = (clickPoint.y - sa.position.y) / aspect;
|
||||||
const radius = sa.radius ?? 0.1;
|
const radius = sa.radius ?? 0.1;
|
||||||
if (dx * dx + dy * dy <= radius * radius) {
|
if (dx * dx + dy * dy <= radius * radius) {
|
||||||
setDraggingSpriteAreaId(sa.id);
|
setDraggingSpriteAreaId(sa.id);
|
||||||
|
|||||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
105
dist/index.mjs
vendored
105
dist/index.mjs
vendored
@ -373,6 +373,7 @@ var SpriteParticlePool = class {
|
|||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
velocity: { x: 0, y: 0 },
|
velocity: { x: 0, y: 0 },
|
||||||
scale: 1,
|
scale: 1,
|
||||||
|
initialScale: 1,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
age: 0,
|
age: 0,
|
||||||
@ -422,6 +423,16 @@ var SpriteParticlePool = class {
|
|||||||
// src/engine/SpriteEffectInstance.ts
|
// src/engine/SpriteEffectInstance.ts
|
||||||
var randomRange = (min, max) => min + Math.random() * (max - min);
|
var randomRange = (min, max) => min + Math.random() * (max - min);
|
||||||
var lerp = (a, b, t) => a + (b - a) * t;
|
var lerp = (a, b, t) => a + (b - a) * t;
|
||||||
|
var lerpKeyframes = (values, t) => {
|
||||||
|
if (values.length === 2) {
|
||||||
|
return lerp(values[0], values[1], t);
|
||||||
|
}
|
||||||
|
const segments = values.length - 1;
|
||||||
|
const segT = t * segments;
|
||||||
|
const idx = Math.min(Math.floor(segT), segments - 1);
|
||||||
|
const localT = segT - idx;
|
||||||
|
return lerp(values[idx], values[idx + 1], localT);
|
||||||
|
};
|
||||||
var SpriteEffectInstance = class {
|
var SpriteEffectInstance = class {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.texture = null;
|
this.texture = null;
|
||||||
@ -452,7 +463,17 @@ var SpriteEffectInstance = class {
|
|||||||
this.group.add(mesh);
|
this.group.add(mesh);
|
||||||
return mesh;
|
return mesh;
|
||||||
});
|
});
|
||||||
|
if (config.emoji) {
|
||||||
|
this.createEmojiTexture(config.emoji);
|
||||||
|
} else if (config.spriteUrl) {
|
||||||
this.loadTexture(config.spriteUrl);
|
this.loadTexture(config.spriteUrl);
|
||||||
|
} else {
|
||||||
|
console.error("[SpriteEffectInstance] spriteUrl \uB610\uB294 emoji \uC911 \uD558\uB098\uB294 \uD544\uC218\uC785\uB2C8\uB2E4.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 런타임 설정 업데이트 (maxParticles 제외) */
|
||||||
|
updateConfig(config) {
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
/** 텍스처 로드 */
|
/** 텍스처 로드 */
|
||||||
loadTexture(url) {
|
loadTexture(url) {
|
||||||
@ -487,6 +508,31 @@ var SpriteEffectInstance = class {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/** 이모지를 Canvas에 렌더링하여 텍스처 생성 */
|
||||||
|
createEmojiTexture(emoji) {
|
||||||
|
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 \uCEE8\uD14D\uC2A4\uD2B8 \uC0DD\uC131 \uC2E4\uD328");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.font = "100px serif";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(emoji, size / 2, size / 2);
|
||||||
|
const texture = new THREE2.CanvasTexture(canvas);
|
||||||
|
this.texture = texture;
|
||||||
|
for (const mesh of this.meshes) {
|
||||||
|
const mat = mesh.material;
|
||||||
|
mat.map = texture;
|
||||||
|
mat.needsUpdate = true;
|
||||||
|
}
|
||||||
|
this.ready = true;
|
||||||
|
console.log(`[SpriteEffectInstance] \uC774\uBAA8\uC9C0 \uD14D\uC2A4\uCC98 \uC0DD\uC131 \uC644\uB8CC: ${emoji}`);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 파티클 1개 방출
|
* 파티클 1개 방출
|
||||||
* @param center 방출 중심 (정규화 좌표 0-1)
|
* @param center 방출 중심 (정규화 좌표 0-1)
|
||||||
@ -511,7 +557,8 @@ var SpriteEffectInstance = class {
|
|||||||
const speed = randomRange(config.initialSpeed[0], config.initialSpeed[1]);
|
const speed = randomRange(config.initialSpeed[0], config.initialSpeed[1]);
|
||||||
particle.velocity.x = Math.cos(angleRad) * speed;
|
particle.velocity.x = Math.cos(angleRad) * speed;
|
||||||
particle.velocity.y = Math.sin(angleRad) * speed;
|
particle.velocity.y = Math.sin(angleRad) * speed;
|
||||||
particle.scale = randomRange(config.initialScale[0], config.initialScale[1]);
|
particle.initialScale = randomRange(config.initialScale[0], config.initialScale[1]);
|
||||||
|
particle.scale = particle.initialScale;
|
||||||
particle.rotation = 0;
|
particle.rotation = 0;
|
||||||
particle.opacity = 1;
|
particle.opacity = 1;
|
||||||
particle.lifetime = randomRange(config.lifetime[0], config.lifetime[1]);
|
particle.lifetime = randomRange(config.lifetime[0], config.lifetime[1]);
|
||||||
@ -541,7 +588,7 @@ var SpriteEffectInstance = class {
|
|||||||
this.emitOne(center);
|
this.emitOne(center);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update(deltaTime, emitCenter) {
|
update(deltaTime, emitCenter, resolution) {
|
||||||
if (!this.ready) return;
|
if (!this.ready) return;
|
||||||
if (this.config.trigger === "ambient") {
|
if (this.config.trigger === "ambient") {
|
||||||
this.updateAmbientEmit(deltaTime, emitCenter);
|
this.updateAmbientEmit(deltaTime, emitCenter);
|
||||||
@ -562,10 +609,10 @@ var SpriteEffectInstance = class {
|
|||||||
const lifeRatio = particle.age / particle.lifetime;
|
const lifeRatio = particle.age / particle.lifetime;
|
||||||
if (overLifetime) {
|
if (overLifetime) {
|
||||||
if (overLifetime.scale) {
|
if (overLifetime.scale) {
|
||||||
particle.scale = lerp(overLifetime.scale[0], overLifetime.scale[1], lifeRatio);
|
particle.scale = particle.initialScale * lerpKeyframes(overLifetime.scale, lifeRatio);
|
||||||
}
|
}
|
||||||
if (overLifetime.opacity) {
|
if (overLifetime.opacity) {
|
||||||
particle.opacity = lerp(overLifetime.opacity[0], overLifetime.opacity[1], lifeRatio);
|
particle.opacity = lerpKeyframes(overLifetime.opacity, lifeRatio);
|
||||||
}
|
}
|
||||||
if (overLifetime.rotationSpeed) {
|
if (overLifetime.rotationSpeed) {
|
||||||
particle.rotation += overLifetime.rotationSpeed * deltaTime;
|
particle.rotation += overLifetime.rotationSpeed * deltaTime;
|
||||||
@ -581,7 +628,7 @@ var SpriteEffectInstance = class {
|
|||||||
}
|
}
|
||||||
particle.position.x += particle.velocity.x * deltaTime;
|
particle.position.x += particle.velocity.x * deltaTime;
|
||||||
particle.position.y += particle.velocity.y * deltaTime;
|
particle.position.y += particle.velocity.y * deltaTime;
|
||||||
this.syncMesh(particle);
|
this.syncMesh(particle, resolution);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -606,7 +653,7 @@ var SpriteEffectInstance = class {
|
|||||||
* 파티클 상태를 Three.js 메쉬에 동기화
|
* 파티클 상태를 Three.js 메쉬에 동기화
|
||||||
* 정규화 좌표(0-1) → NDC(-1~1) 변환, y축 반전
|
* 정규화 좌표(0-1) → NDC(-1~1) 변환, y축 반전
|
||||||
*/
|
*/
|
||||||
syncMesh(particle) {
|
syncMesh(particle, resolution) {
|
||||||
const mesh = this.meshes[particle.index];
|
const mesh = this.meshes[particle.index];
|
||||||
if (!mesh) return;
|
if (!mesh) return;
|
||||||
if (!particle.active) {
|
if (!particle.active) {
|
||||||
@ -617,7 +664,8 @@ var SpriteEffectInstance = class {
|
|||||||
mesh.position.x = particle.position.x * 2 - 1;
|
mesh.position.x = particle.position.x * 2 - 1;
|
||||||
mesh.position.y = -(particle.position.y * 2 - 1);
|
mesh.position.y = -(particle.position.y * 2 - 1);
|
||||||
mesh.position.z = -0.01;
|
mesh.position.z = -0.01;
|
||||||
mesh.scale.set(particle.scale, particle.scale, 1);
|
const aspect = resolution ? resolution.x / resolution.y : 1;
|
||||||
|
mesh.scale.set(particle.scale / aspect, particle.scale, 1);
|
||||||
mesh.rotation.z = particle.rotation;
|
mesh.rotation.z = particle.rotation;
|
||||||
const mat = mesh.material;
|
const mat = mesh.material;
|
||||||
mat.opacity = particle.opacity;
|
mat.opacity = particle.opacity;
|
||||||
@ -657,9 +705,10 @@ var SpriteEffectInstance = class {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src/engine/SpriteEffectManager.ts
|
// src/engine/SpriteEffectManager.ts
|
||||||
var isPointInCircle = (point, center, radius) => {
|
var isPointInCircle = (point, center, radius, resolution) => {
|
||||||
|
const aspect = resolution ? resolution.x / resolution.y : 1;
|
||||||
const dx = point.x - center.x;
|
const dx = point.x - center.x;
|
||||||
const dy = point.y - center.y;
|
const dy = (point.y - center.y) / aspect;
|
||||||
return dx * dx + dy * dy <= radius * radius;
|
return dx * dx + dy * dy <= radius * radius;
|
||||||
};
|
};
|
||||||
var SpriteEffectManager = class {
|
var SpriteEffectManager = class {
|
||||||
@ -687,8 +736,12 @@ var SpriteEffectManager = class {
|
|||||||
for (const effectConfig of area.effects) {
|
for (const effectConfig of area.effects) {
|
||||||
const key = `${area.id}::${effectConfig.id}`;
|
const key = `${area.id}::${effectConfig.id}`;
|
||||||
activeKeys.add(key);
|
activeKeys.add(key);
|
||||||
if (this.instances.has(key)) continue;
|
const existing = this.instances.get(key);
|
||||||
console.log("[SpriteEffectManager] \uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131:", key, effectConfig.spriteUrl);
|
if (existing) {
|
||||||
|
existing.updateConfig(effectConfig);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log("[SpriteEffectManager] \uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131:", key, effectConfig.emoji ?? effectConfig.spriteUrl);
|
||||||
const instance = new SpriteEffectInstance(effectConfig);
|
const instance = new SpriteEffectInstance(effectConfig);
|
||||||
this.instances.set(key, instance);
|
this.instances.set(key, instance);
|
||||||
this.effectGroup.add(instance.group);
|
this.effectGroup.add(instance.group);
|
||||||
@ -708,12 +761,12 @@ var SpriteEffectManager = class {
|
|||||||
* @param deltaTime 초 단위 프레임 시간
|
* @param deltaTime 초 단위 프레임 시간
|
||||||
* @param touchState 마우스/터치 상태
|
* @param touchState 마우스/터치 상태
|
||||||
*/
|
*/
|
||||||
update(effectAreas, deltaTime, touchState) {
|
update(effectAreas, deltaTime, touchState, resolution) {
|
||||||
const currentTouchingAreas = /* @__PURE__ */ new Set();
|
const currentTouchingAreas = /* @__PURE__ */ new Set();
|
||||||
if (touchState.isDragging && touchState.position) {
|
if (touchState.isDragging && 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)) {
|
if (isPointInCircle(touchState.position, area.position, radius, resolution)) {
|
||||||
currentTouchingAreas.add(area.id);
|
currentTouchingAreas.add(area.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -729,7 +782,7 @@ var SpriteEffectManager = class {
|
|||||||
instance.triggerBurst(touchState.position ?? area.position);
|
instance.triggerBurst(touchState.position ?? area.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
instance.update(deltaTime, area.position);
|
instance.update(deltaTime, area.position, resolution);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.previousTouchingAreas = currentTouchingAreas;
|
this.previousTouchingAreas = currentTouchingAreas;
|
||||||
@ -1199,6 +1252,14 @@ var DEFAULT_AREA = {
|
|||||||
SNAP_STEPS: 0
|
SNAP_STEPS: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// src/utils/describeSrc.ts
|
||||||
|
var MAX_LOGGED_LENGTH = 1e3;
|
||||||
|
function describeSrc(src) {
|
||||||
|
if (!src) return String(src);
|
||||||
|
if (src.length <= MAX_LOGGED_LENGTH) return src;
|
||||||
|
return `[\uC0DD\uB7B5: ${src.length.toLocaleString()}\uC790]`;
|
||||||
|
}
|
||||||
|
|
||||||
// src/components/ImageDistortion.tsx
|
// src/components/ImageDistortion.tsx
|
||||||
import { jsx } from "react/jsx-runtime";
|
import { jsx } from "react/jsx-runtime";
|
||||||
var ImageDistortion = ({
|
var ImageDistortion = ({
|
||||||
@ -1263,7 +1324,7 @@ var ImageDistortion = ({
|
|||||||
console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("[ImageDistortion] \uCD08\uAE30\uD654 \uC2DC\uC791");
|
console.log("[ImageDistortion] v1.5.1 \uCD08\uAE30\uD654 \uC2DC\uC791");
|
||||||
const scene = new ThreeScene(containerRef.current);
|
const scene = new ThreeScene(containerRef.current);
|
||||||
sceneRef.current = scene;
|
sceneRef.current = scene;
|
||||||
const vertPath = vertexShaderPath || "/shaders/distortion.vert.glsl";
|
const vertPath = vertexShaderPath || "/shaders/distortion.vert.glsl";
|
||||||
@ -1285,10 +1346,13 @@ var ImageDistortion = ({
|
|||||||
}, [vertexShaderPath, fragmentShaderPath]);
|
}, [vertexShaderPath, fragmentShaderPath]);
|
||||||
useEffect3(() => {
|
useEffect3(() => {
|
||||||
if (!imageSrc || !isReady) {
|
if (!imageSrc || !isReady) {
|
||||||
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady });
|
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", {
|
||||||
|
imageSrc: describeSrc(imageSrc),
|
||||||
|
isReady
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2DC\uC791:", imageSrc);
|
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2DC\uC791:", describeSrc(imageSrc));
|
||||||
setImageLoaded(false);
|
setImageLoaded(false);
|
||||||
const loader = new THREE4.TextureLoader();
|
const loader = new THREE4.TextureLoader();
|
||||||
loader.load(
|
loader.load(
|
||||||
@ -1384,13 +1448,15 @@ var ImageDistortion = ({
|
|||||||
});
|
});
|
||||||
if (spriteManagerRef.current) {
|
if (spriteManagerRef.current) {
|
||||||
const mouseState = mouseInteractionHook.getMouseState();
|
const mouseState = mouseInteractionHook.getMouseState();
|
||||||
|
const resolution = sceneRef.current?.getResolution() ?? { x: 1, y: 1 };
|
||||||
spriteManagerRef.current.update(
|
spriteManagerRef.current.update(
|
||||||
spriteEffectAreas,
|
spriteEffectAreas,
|
||||||
deltaTime,
|
deltaTime,
|
||||||
{
|
{
|
||||||
position: mouseState.position ?? null,
|
position: mouseState.position ?? null,
|
||||||
isDragging: mouseState.isDragging
|
isDragging: mouseState.isDragging
|
||||||
}
|
},
|
||||||
|
resolution
|
||||||
);
|
);
|
||||||
sceneRef.current?.render();
|
sceneRef.current?.render();
|
||||||
}
|
}
|
||||||
@ -1839,7 +1905,8 @@ var EditorCanvas = ({
|
|||||||
for (let i = spriteEffectAreas.length - 1; i >= 0; i--) {
|
for (let i = spriteEffectAreas.length - 1; i >= 0; i--) {
|
||||||
const sa = spriteEffectAreas[i];
|
const sa = spriteEffectAreas[i];
|
||||||
const dx = clickPoint.x - sa.position.x;
|
const dx = clickPoint.x - sa.position.x;
|
||||||
const dy = clickPoint.y - sa.position.y;
|
const aspect = rect.width / rect.height;
|
||||||
|
const dy = (clickPoint.y - sa.position.y) / aspect;
|
||||||
const radius = sa.radius ?? 0.1;
|
const radius = sa.radius ?? 0.1;
|
||||||
if (dx * dx + dy * dy <= radius * radius) {
|
if (dx * dx + dy * dy <= radius * radius) {
|
||||||
setDraggingSpriteAreaId(sa.id);
|
setDraggingSpriteAreaId(sa.id);
|
||||||
|
|||||||
2
dist/index.mjs.map
vendored
2
dist/index.mjs.map
vendored
File diff suppressed because one or more lines are too long
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@baekryang/responsive-image-canvas",
|
"name": "@baekryang/responsive-image-canvas",
|
||||||
"version": "1.3.0",
|
"version": "1.5.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@baekryang/responsive-image-canvas",
|
"name": "@baekryang/responsive-image-canvas",
|
||||||
"version": "1.3.0",
|
"version": "1.5.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@baekryang/responsive-image-canvas",
|
"name": "@baekryang/responsive-image-canvas",
|
||||||
"version": "1.5.2",
|
"version": "1.5.5",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
|
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { SpriteEffectManager } from '@/engine/SpriteEffectManager';
|
|||||||
import { useAnimationFrame } from '@/hooks/useAnimationFrame';
|
import { useAnimationFrame } from '@/hooks/useAnimationFrame';
|
||||||
import { useMouseInteraction } from '@/hooks/useMouseInteraction';
|
import { useMouseInteraction } from '@/hooks/useMouseInteraction';
|
||||||
import { SHADER_CONFIG } from '@/utils/constants';
|
import { SHADER_CONFIG } from '@/utils/constants';
|
||||||
|
import { describeSrc } from '@/utils/describeSrc';
|
||||||
import { MouseInteractionConfig } from '@/types/interaction';
|
import { MouseInteractionConfig } from '@/types/interaction';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,11 +148,16 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
|||||||
// 이미지 텍스처 로드
|
// 이미지 텍스처 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageSrc || !isReady) {
|
if (!imageSrc || !isReady) {
|
||||||
console.log('[ImageDistortion] 이미지 로드 스킵:', { imageSrc, isReady });
|
console.log('[ImageDistortion] 이미지 로드 스킵:', {
|
||||||
|
imageSrc: describeSrc(imageSrc),
|
||||||
|
isReady,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[ImageDistortion] 이미지 로드 시작:', imageSrc);
|
// describeSrc: data: URL을 그대로 찍으면 DevTools가 그 한 줄을 렌더하느라
|
||||||
|
// 메인 스레드가 수 초간 멈춘다. 너무 길면 길이만 남기고 본문은 버린다.
|
||||||
|
console.log('[ImageDistortion] 이미지 로드 시작:', describeSrc(imageSrc));
|
||||||
setImageLoaded(false);
|
setImageLoaded(false);
|
||||||
|
|
||||||
const loader = new THREE.TextureLoader();
|
const loader = new THREE.TextureLoader();
|
||||||
|
|||||||
@ -147,7 +147,8 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
|
|||||||
for (let i = spriteEffectAreas.length - 1; i >= 0; i--) {
|
for (let i = spriteEffectAreas.length - 1; i >= 0; i--) {
|
||||||
const sa = spriteEffectAreas[i];
|
const sa = spriteEffectAreas[i];
|
||||||
const dx = clickPoint.x - sa.position.x;
|
const dx = clickPoint.x - sa.position.x;
|
||||||
const dy = clickPoint.y - sa.position.y;
|
const aspect = rect.width / rect.height;
|
||||||
|
const dy = (clickPoint.y - sa.position.y) / aspect;
|
||||||
const radius = sa.radius ?? 0.1;
|
const radius = sa.radius ?? 0.1;
|
||||||
if (dx * dx + dy * dy <= radius * radius) {
|
if (dx * dx + dy * dy <= radius * radius) {
|
||||||
setDraggingSpriteAreaId(sa.id);
|
setDraggingSpriteAreaId(sa.id);
|
||||||
|
|||||||
@ -16,9 +16,10 @@ export interface SpriteEffectTouchState {
|
|||||||
/**
|
/**
|
||||||
* 점이 원 안에 있는지 확인
|
* 점이 원 안에 있는지 확인
|
||||||
*/
|
*/
|
||||||
const isPointInCircle = (point: Point, center: Point, radius: number): 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 dx = point.x - center.x;
|
||||||
const dy = point.y - center.y;
|
const dy = (point.y - center.y) / aspect;
|
||||||
return dx * dx + dy * dy <= radius * radius;
|
return dx * dx + dy * dy <= radius * radius;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,7 +98,7 @@ export class SpriteEffectManager {
|
|||||||
if (touchState.isDragging && touchState.position) {
|
if (touchState.isDragging && 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)) {
|
if (isPointInCircle(touchState.position, area.position, radius, resolution)) {
|
||||||
currentTouchingAreas.add(area.id);
|
currentTouchingAreas.add(area.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/utils/describeSrc.ts
Normal file
27
src/utils/describeSrc.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 이 길이를 넘으면 로그에 찍지 않는다.
|
||||||
|
*
|
||||||
|
* 실제 주소는 여기 한참 못 미친다 — Firebase Storage의 토큰 붙은 URL도 300자 안쪽이고,
|
||||||
|
* 브라우저가 다루는 URL 상한도 보통 2,000자대다. 반면 사진을 담은 `data:` URL은
|
||||||
|
* 수십만 자다. 그 사이 어디를 잘라도 되지만 넉넉하게 잡는다.
|
||||||
|
*/
|
||||||
|
const MAX_LOGGED_LENGTH = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지 소스를 **로그에 안전한 한 줄**로 줄인다.
|
||||||
|
*
|
||||||
|
* `data:` URL은 사진을 통째로 base64로 안고 있어 수십만 자에 이른다. 그대로
|
||||||
|
* `console.log`에 넘기면 DevTools가 그 문자열을 저장·포맷·렌더하느라 **메인 스레드가
|
||||||
|
* 수 초간 멈춘다** — 실측으로 0.41MB짜리 한 줄에 10초 넘게 굳은 사례가 있었다.
|
||||||
|
* 화면이 통째로 얼어붙는데 원인이 로그 한 줄이라 추적도 어렵다.
|
||||||
|
*
|
||||||
|
* 거르는 기준은 **스킴이 아니라 길이**다. 위험한 것은 `data:`라는 형식이 아니라
|
||||||
|
* 크기이고, 스킴으로 거르면 상대 경로처럼 멀쩡하고 짧은 주소까지 가려져 정작
|
||||||
|
* 디버깅할 때 아무것도 안 보인다.
|
||||||
|
*/
|
||||||
|
export function describeSrc(src: string | null | undefined): string {
|
||||||
|
if (!src) return String(src);
|
||||||
|
if (src.length <= MAX_LOGGED_LENGTH) return src;
|
||||||
|
|
||||||
|
return `[생략: ${src.length.toLocaleString()}자]`;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user