Truncate oversized image sources in logs and bump version to 1.5.5
- data: URL을 그대로 console.log에 넘기면 DevTools가 그 한 줄을 저장·포맷· 렌더하느라 메인 스레드가 멈춘다. 실측으로 0.41MB짜리 한 줄에 10초 넘게 굳었다 - 화면이 통째로 얼어붙는데 원인이 로그 한 줄이라 추적이 매우 어렵다 - describeSrc가 1,000자를 넘는 소스를 길이 표시로 대체한다 - 거르는 기준을 스킴이 아니라 길이로 잡았다: 위험한 것은 data:라는 형식이 아니라 크기이고, 스킴으로 거르면 상대 경로처럼 짧고 멀쩡한 주소까지 가려진다
This commit is contained in:
parent
769934adff
commit
09cb2f09af
24
dist/index.d.mts
vendored
24
dist/index.d.mts
vendored
@ -151,10 +151,10 @@ type SpriteBlendMode = 'normal' | 'additive';
|
||||
* 수명 기반 파티클 속성 보간 설정
|
||||
*/
|
||||
interface SpriteParticleOverLifetime {
|
||||
/** [시작, 끝] 스케일 */
|
||||
scale?: [number, number];
|
||||
/** [시작, 끝] 투명도 */
|
||||
opacity?: [number, number];
|
||||
/** 스케일 보간: [시작, 끝] 또는 [시작, 중간, 끝] */
|
||||
scale?: number[];
|
||||
/** 투명도 보간: [시작, 끝] 또는 [시작, 중간, 끝] */
|
||||
opacity?: number[];
|
||||
/** 회전 속도 (라디안/초) */
|
||||
rotationSpeed?: number;
|
||||
/** 속도 감쇠 (0-1, 매 프레임 속도에 곱해짐) */
|
||||
@ -203,7 +203,10 @@ interface SpriteEffectAreaData {
|
||||
effects: Array<{
|
||||
id: string;
|
||||
trigger: SpriteEffectTrigger;
|
||||
spriteUrl: string;
|
||||
/** spriteUrl 또는 emoji 중 하나 필수 */
|
||||
spriteUrl?: string;
|
||||
/** 이모지 파티클 (spriteUrl 대신 사용 가능) */
|
||||
emoji?: string;
|
||||
blendMode?: SpriteBlendMode;
|
||||
maxParticles: number;
|
||||
emitRate?: number;
|
||||
@ -229,8 +232,10 @@ interface SpriteEffectConfig {
|
||||
id: string;
|
||||
/** 트리거 타입 */
|
||||
trigger: SpriteEffectTrigger;
|
||||
/** 스프라이트 이미지 URL */
|
||||
spriteUrl: string;
|
||||
/** 스프라이트 이미지 URL (emoji와 택1) */
|
||||
spriteUrl?: string;
|
||||
/** 이모지 파티클 (spriteUrl 대신 사용 가능) */
|
||||
emoji?: string;
|
||||
/** 블렌드 모드 (기본: 'normal') */
|
||||
blendMode?: SpriteBlendMode;
|
||||
/** 최대 파티클 수 */
|
||||
@ -802,7 +807,10 @@ declare class SpriteEffectManager {
|
||||
* @param deltaTime 초 단위 프레임 시간
|
||||
* @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 {
|
||||
/** [시작, 끝] 스케일 */
|
||||
scale?: [number, number];
|
||||
/** [시작, 끝] 투명도 */
|
||||
opacity?: [number, number];
|
||||
/** 스케일 보간: [시작, 끝] 또는 [시작, 중간, 끝] */
|
||||
scale?: number[];
|
||||
/** 투명도 보간: [시작, 끝] 또는 [시작, 중간, 끝] */
|
||||
opacity?: number[];
|
||||
/** 회전 속도 (라디안/초) */
|
||||
rotationSpeed?: number;
|
||||
/** 속도 감쇠 (0-1, 매 프레임 속도에 곱해짐) */
|
||||
@ -203,7 +203,10 @@ interface SpriteEffectAreaData {
|
||||
effects: Array<{
|
||||
id: string;
|
||||
trigger: SpriteEffectTrigger;
|
||||
spriteUrl: string;
|
||||
/** spriteUrl 또는 emoji 중 하나 필수 */
|
||||
spriteUrl?: string;
|
||||
/** 이모지 파티클 (spriteUrl 대신 사용 가능) */
|
||||
emoji?: string;
|
||||
blendMode?: SpriteBlendMode;
|
||||
maxParticles: number;
|
||||
emitRate?: number;
|
||||
@ -229,8 +232,10 @@ interface SpriteEffectConfig {
|
||||
id: string;
|
||||
/** 트리거 타입 */
|
||||
trigger: SpriteEffectTrigger;
|
||||
/** 스프라이트 이미지 URL */
|
||||
spriteUrl: string;
|
||||
/** 스프라이트 이미지 URL (emoji와 택1) */
|
||||
spriteUrl?: string;
|
||||
/** 이모지 파티클 (spriteUrl 대신 사용 가능) */
|
||||
emoji?: string;
|
||||
/** 블렌드 모드 (기본: 'normal') */
|
||||
blendMode?: SpriteBlendMode;
|
||||
/** 최대 파티클 수 */
|
||||
@ -802,7 +807,10 @@ declare class SpriteEffectManager {
|
||||
* @param deltaTime 초 단위 프레임 시간
|
||||
* @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 },
|
||||
velocity: { x: 0, y: 0 },
|
||||
scale: 1,
|
||||
initialScale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
age: 0,
|
||||
@ -483,6 +484,16 @@ var SpriteParticlePool = class {
|
||||
// src/engine/SpriteEffectInstance.ts
|
||||
var randomRange = (min, max) => min + Math.random() * (max - min);
|
||||
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 {
|
||||
constructor(config) {
|
||||
this.texture = null;
|
||||
@ -513,7 +524,17 @@ var SpriteEffectInstance = class {
|
||||
this.group.add(mesh);
|
||||
return mesh;
|
||||
});
|
||||
if (config.emoji) {
|
||||
this.createEmojiTexture(config.emoji);
|
||||
} else if (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) {
|
||||
@ -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개 방출
|
||||
* @param center 방출 중심 (정규화 좌표 0-1)
|
||||
@ -572,7 +618,8 @@ var SpriteEffectInstance = class {
|
||||
const speed = randomRange(config.initialSpeed[0], config.initialSpeed[1]);
|
||||
particle.velocity.x = Math.cos(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.opacity = 1;
|
||||
particle.lifetime = randomRange(config.lifetime[0], config.lifetime[1]);
|
||||
@ -602,7 +649,7 @@ var SpriteEffectInstance = class {
|
||||
this.emitOne(center);
|
||||
}
|
||||
}
|
||||
update(deltaTime, emitCenter) {
|
||||
update(deltaTime, emitCenter, resolution) {
|
||||
if (!this.ready) return;
|
||||
if (this.config.trigger === "ambient") {
|
||||
this.updateAmbientEmit(deltaTime, emitCenter);
|
||||
@ -623,10 +670,10 @@ var SpriteEffectInstance = class {
|
||||
const lifeRatio = particle.age / particle.lifetime;
|
||||
if (overLifetime) {
|
||||
if (overLifetime.scale) {
|
||||
particle.scale = lerp(overLifetime.scale[0], overLifetime.scale[1], lifeRatio);
|
||||
particle.scale = particle.initialScale * lerpKeyframes(overLifetime.scale, lifeRatio);
|
||||
}
|
||||
if (overLifetime.opacity) {
|
||||
particle.opacity = lerp(overLifetime.opacity[0], overLifetime.opacity[1], lifeRatio);
|
||||
particle.opacity = lerpKeyframes(overLifetime.opacity, lifeRatio);
|
||||
}
|
||||
if (overLifetime.rotationSpeed) {
|
||||
particle.rotation += overLifetime.rotationSpeed * deltaTime;
|
||||
@ -642,7 +689,7 @@ var SpriteEffectInstance = class {
|
||||
}
|
||||
particle.position.x += particle.velocity.x * deltaTime;
|
||||
particle.position.y += particle.velocity.y * deltaTime;
|
||||
this.syncMesh(particle);
|
||||
this.syncMesh(particle, resolution);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -667,7 +714,7 @@ var SpriteEffectInstance = class {
|
||||
* 파티클 상태를 Three.js 메쉬에 동기화
|
||||
* 정규화 좌표(0-1) → NDC(-1~1) 변환, y축 반전
|
||||
*/
|
||||
syncMesh(particle) {
|
||||
syncMesh(particle, resolution) {
|
||||
const mesh = this.meshes[particle.index];
|
||||
if (!mesh) return;
|
||||
if (!particle.active) {
|
||||
@ -678,7 +725,8 @@ var SpriteEffectInstance = class {
|
||||
mesh.position.x = particle.position.x * 2 - 1;
|
||||
mesh.position.y = -(particle.position.y * 2 - 1);
|
||||
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;
|
||||
const mat = mesh.material;
|
||||
mat.opacity = particle.opacity;
|
||||
@ -718,9 +766,10 @@ var SpriteEffectInstance = class {
|
||||
};
|
||||
|
||||
// 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 dy = point.y - center.y;
|
||||
const dy = (point.y - center.y) / aspect;
|
||||
return dx * dx + dy * dy <= radius * radius;
|
||||
};
|
||||
var SpriteEffectManager = class {
|
||||
@ -748,8 +797,12 @@ var SpriteEffectManager = class {
|
||||
for (const effectConfig of area.effects) {
|
||||
const key = `${area.id}::${effectConfig.id}`;
|
||||
activeKeys.add(key);
|
||||
if (this.instances.has(key)) continue;
|
||||
console.log("[SpriteEffectManager] \uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131:", key, effectConfig.spriteUrl);
|
||||
const existing = this.instances.get(key);
|
||||
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);
|
||||
this.instances.set(key, instance);
|
||||
this.effectGroup.add(instance.group);
|
||||
@ -769,12 +822,12 @@ var SpriteEffectManager = class {
|
||||
* @param deltaTime 초 단위 프레임 시간
|
||||
* @param touchState 마우스/터치 상태
|
||||
*/
|
||||
update(effectAreas, deltaTime, touchState) {
|
||||
update(effectAreas, deltaTime, touchState, resolution) {
|
||||
const currentTouchingAreas = /* @__PURE__ */ new Set();
|
||||
if (touchState.isDragging && touchState.position) {
|
||||
for (const area of effectAreas) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -790,7 +843,7 @@ var SpriteEffectManager = class {
|
||||
instance.triggerBurst(touchState.position ?? area.position);
|
||||
}
|
||||
}
|
||||
instance.update(deltaTime, area.position);
|
||||
instance.update(deltaTime, area.position, resolution);
|
||||
}
|
||||
}
|
||||
this.previousTouchingAreas = currentTouchingAreas;
|
||||
@ -1260,6 +1313,14 @@ var DEFAULT_AREA = {
|
||||
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
|
||||
var import_jsx_runtime = require("react/jsx-runtime");
|
||||
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.");
|
||||
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);
|
||||
sceneRef.current = scene;
|
||||
const vertPath = vertexShaderPath || "/shaders/distortion.vert.glsl";
|
||||
@ -1346,10 +1407,13 @@ var ImageDistortion = ({
|
||||
}, [vertexShaderPath, fragmentShaderPath]);
|
||||
(0, import_react4.useEffect)(() => {
|
||||
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;
|
||||
}
|
||||
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);
|
||||
const loader = new THREE4.TextureLoader();
|
||||
loader.load(
|
||||
@ -1445,13 +1509,15 @@ var ImageDistortion = ({
|
||||
});
|
||||
if (spriteManagerRef.current) {
|
||||
const mouseState = mouseInteractionHook.getMouseState();
|
||||
const resolution = sceneRef.current?.getResolution() ?? { x: 1, y: 1 };
|
||||
spriteManagerRef.current.update(
|
||||
spriteEffectAreas,
|
||||
deltaTime,
|
||||
{
|
||||
position: mouseState.position ?? null,
|
||||
isDragging: mouseState.isDragging
|
||||
}
|
||||
},
|
||||
resolution
|
||||
);
|
||||
sceneRef.current?.render();
|
||||
}
|
||||
@ -1900,7 +1966,8 @@ var EditorCanvas = ({
|
||||
for (let i = spriteEffectAreas.length - 1; i >= 0; i--) {
|
||||
const sa = spriteEffectAreas[i];
|
||||
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;
|
||||
if (dx * dx + dy * dy <= radius * radius) {
|
||||
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 },
|
||||
velocity: { x: 0, y: 0 },
|
||||
scale: 1,
|
||||
initialScale: 1,
|
||||
rotation: 0,
|
||||
opacity: 1,
|
||||
age: 0,
|
||||
@ -422,6 +423,16 @@ var SpriteParticlePool = class {
|
||||
// src/engine/SpriteEffectInstance.ts
|
||||
var randomRange = (min, max) => min + Math.random() * (max - min);
|
||||
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 {
|
||||
constructor(config) {
|
||||
this.texture = null;
|
||||
@ -452,7 +463,17 @@ var SpriteEffectInstance = class {
|
||||
this.group.add(mesh);
|
||||
return mesh;
|
||||
});
|
||||
if (config.emoji) {
|
||||
this.createEmojiTexture(config.emoji);
|
||||
} else if (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) {
|
||||
@ -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개 방출
|
||||
* @param center 방출 중심 (정규화 좌표 0-1)
|
||||
@ -511,7 +557,8 @@ var SpriteEffectInstance = class {
|
||||
const speed = randomRange(config.initialSpeed[0], config.initialSpeed[1]);
|
||||
particle.velocity.x = Math.cos(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.opacity = 1;
|
||||
particle.lifetime = randomRange(config.lifetime[0], config.lifetime[1]);
|
||||
@ -541,7 +588,7 @@ var SpriteEffectInstance = class {
|
||||
this.emitOne(center);
|
||||
}
|
||||
}
|
||||
update(deltaTime, emitCenter) {
|
||||
update(deltaTime, emitCenter, resolution) {
|
||||
if (!this.ready) return;
|
||||
if (this.config.trigger === "ambient") {
|
||||
this.updateAmbientEmit(deltaTime, emitCenter);
|
||||
@ -562,10 +609,10 @@ var SpriteEffectInstance = class {
|
||||
const lifeRatio = particle.age / particle.lifetime;
|
||||
if (overLifetime) {
|
||||
if (overLifetime.scale) {
|
||||
particle.scale = lerp(overLifetime.scale[0], overLifetime.scale[1], lifeRatio);
|
||||
particle.scale = particle.initialScale * lerpKeyframes(overLifetime.scale, lifeRatio);
|
||||
}
|
||||
if (overLifetime.opacity) {
|
||||
particle.opacity = lerp(overLifetime.opacity[0], overLifetime.opacity[1], lifeRatio);
|
||||
particle.opacity = lerpKeyframes(overLifetime.opacity, lifeRatio);
|
||||
}
|
||||
if (overLifetime.rotationSpeed) {
|
||||
particle.rotation += overLifetime.rotationSpeed * deltaTime;
|
||||
@ -581,7 +628,7 @@ var SpriteEffectInstance = class {
|
||||
}
|
||||
particle.position.x += particle.velocity.x * deltaTime;
|
||||
particle.position.y += particle.velocity.y * deltaTime;
|
||||
this.syncMesh(particle);
|
||||
this.syncMesh(particle, resolution);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -606,7 +653,7 @@ var SpriteEffectInstance = class {
|
||||
* 파티클 상태를 Three.js 메쉬에 동기화
|
||||
* 정규화 좌표(0-1) → NDC(-1~1) 변환, y축 반전
|
||||
*/
|
||||
syncMesh(particle) {
|
||||
syncMesh(particle, resolution) {
|
||||
const mesh = this.meshes[particle.index];
|
||||
if (!mesh) return;
|
||||
if (!particle.active) {
|
||||
@ -617,7 +664,8 @@ var SpriteEffectInstance = class {
|
||||
mesh.position.x = particle.position.x * 2 - 1;
|
||||
mesh.position.y = -(particle.position.y * 2 - 1);
|
||||
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;
|
||||
const mat = mesh.material;
|
||||
mat.opacity = particle.opacity;
|
||||
@ -657,9 +705,10 @@ var SpriteEffectInstance = class {
|
||||
};
|
||||
|
||||
// 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 dy = point.y - center.y;
|
||||
const dy = (point.y - center.y) / aspect;
|
||||
return dx * dx + dy * dy <= radius * radius;
|
||||
};
|
||||
var SpriteEffectManager = class {
|
||||
@ -687,8 +736,12 @@ var SpriteEffectManager = class {
|
||||
for (const effectConfig of area.effects) {
|
||||
const key = `${area.id}::${effectConfig.id}`;
|
||||
activeKeys.add(key);
|
||||
if (this.instances.has(key)) continue;
|
||||
console.log("[SpriteEffectManager] \uC778\uC2A4\uD134\uC2A4 \uC0DD\uC131:", key, effectConfig.spriteUrl);
|
||||
const existing = this.instances.get(key);
|
||||
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);
|
||||
this.instances.set(key, instance);
|
||||
this.effectGroup.add(instance.group);
|
||||
@ -708,12 +761,12 @@ var SpriteEffectManager = class {
|
||||
* @param deltaTime 초 단위 프레임 시간
|
||||
* @param touchState 마우스/터치 상태
|
||||
*/
|
||||
update(effectAreas, deltaTime, touchState) {
|
||||
update(effectAreas, deltaTime, touchState, resolution) {
|
||||
const currentTouchingAreas = /* @__PURE__ */ new Set();
|
||||
if (touchState.isDragging && touchState.position) {
|
||||
for (const area of effectAreas) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -729,7 +782,7 @@ var SpriteEffectManager = class {
|
||||
instance.triggerBurst(touchState.position ?? area.position);
|
||||
}
|
||||
}
|
||||
instance.update(deltaTime, area.position);
|
||||
instance.update(deltaTime, area.position, resolution);
|
||||
}
|
||||
}
|
||||
this.previousTouchingAreas = currentTouchingAreas;
|
||||
@ -1199,6 +1252,14 @@ var DEFAULT_AREA = {
|
||||
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
|
||||
import { jsx } from "react/jsx-runtime";
|
||||
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.");
|
||||
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);
|
||||
sceneRef.current = scene;
|
||||
const vertPath = vertexShaderPath || "/shaders/distortion.vert.glsl";
|
||||
@ -1285,10 +1346,13 @@ var ImageDistortion = ({
|
||||
}, [vertexShaderPath, fragmentShaderPath]);
|
||||
useEffect3(() => {
|
||||
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;
|
||||
}
|
||||
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);
|
||||
const loader = new THREE4.TextureLoader();
|
||||
loader.load(
|
||||
@ -1384,13 +1448,15 @@ var ImageDistortion = ({
|
||||
});
|
||||
if (spriteManagerRef.current) {
|
||||
const mouseState = mouseInteractionHook.getMouseState();
|
||||
const resolution = sceneRef.current?.getResolution() ?? { x: 1, y: 1 };
|
||||
spriteManagerRef.current.update(
|
||||
spriteEffectAreas,
|
||||
deltaTime,
|
||||
{
|
||||
position: mouseState.position ?? null,
|
||||
isDragging: mouseState.isDragging
|
||||
}
|
||||
},
|
||||
resolution
|
||||
);
|
||||
sceneRef.current?.render();
|
||||
}
|
||||
@ -1839,7 +1905,8 @@ var EditorCanvas = ({
|
||||
for (let i = spriteEffectAreas.length - 1; i >= 0; i--) {
|
||||
const sa = spriteEffectAreas[i];
|
||||
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;
|
||||
if (dx * dx + dy * dy <= radius * radius) {
|
||||
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",
|
||||
"version": "1.3.0",
|
||||
"version": "1.5.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@baekryang/responsive-image-canvas",
|
||||
"version": "1.3.0",
|
||||
"version": "1.5.5",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.2",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@baekryang/responsive-image-canvas",
|
||||
"version": "1.5.4",
|
||||
"version": "1.5.5",
|
||||
"publishConfig": {
|
||||
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
|
||||
},
|
||||
|
||||
@ -8,6 +8,7 @@ import { SpriteEffectManager } from '@/engine/SpriteEffectManager';
|
||||
import { useAnimationFrame } from '@/hooks/useAnimationFrame';
|
||||
import { useMouseInteraction } from '@/hooks/useMouseInteraction';
|
||||
import { SHADER_CONFIG } from '@/utils/constants';
|
||||
import { describeSrc } from '@/utils/describeSrc';
|
||||
import { MouseInteractionConfig } from '@/types/interaction';
|
||||
|
||||
/**
|
||||
@ -147,11 +148,16 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
||||
// 이미지 텍스처 로드
|
||||
useEffect(() => {
|
||||
if (!imageSrc || !isReady) {
|
||||
console.log('[ImageDistortion] 이미지 로드 스킵:', { imageSrc, isReady });
|
||||
console.log('[ImageDistortion] 이미지 로드 스킵:', {
|
||||
imageSrc: describeSrc(imageSrc),
|
||||
isReady,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ImageDistortion] 이미지 로드 시작:', imageSrc);
|
||||
// describeSrc: data: URL을 그대로 찍으면 DevTools가 그 한 줄을 렌더하느라
|
||||
// 메인 스레드가 수 초간 멈춘다. 너무 길면 길이만 남기고 본문은 버린다.
|
||||
console.log('[ImageDistortion] 이미지 로드 시작:', describeSrc(imageSrc));
|
||||
setImageLoaded(false);
|
||||
|
||||
const loader = new THREE.TextureLoader();
|
||||
|
||||
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