Add lens effect and step easing functions

- 단계별(Step) 이징 함수 추가 (steps2 ~ steps10)
- 영역별 렌즈 왜곡 효과(볼록/오목) 기능 및 셰이더 로직 추가
- 에디터 파라미터 패널에 렌즈 효과 슬라이더 및 스텝 이징 옵션 추가
- 관련 상수, 타입 정의 및 유니폼 변수 업데이트
- 패키지 버전 업데이트 (1.2.7)
This commit is contained in:
BaekRyang 2026-02-25 14:35:45 +09:00
parent 031230bc36
commit 6d9dd082c1
17 changed files with 212 additions and 18 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
//git.bnovalab.com/api/packages/baekryang/npm/:_authToken=a2ed709f39e95662493a92305555a4bf70f6fe10

View File

@ -4,6 +4,7 @@ uniform vec2 u_points[32]; // 최대 8영역 × 4포인트 (정규화된
uniform int u_numAreas;
uniform vec2 u_dragVectors[8]; // 드래그 벡터 (정규화된 좌표 0-1)
uniform float u_distortionStrengths[8];
uniform float u_lensEffects[8];
varying vec2 vUv;
@ -74,6 +75,15 @@ void main() {
// dragVector는 정규화된 좌표(0-1)이므로 바로 사용
vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i];
texCoord += distortion;
// 렌즈 왜곡 효과 (방사형 UV 왜곡)
if (abs(u_lensEffects[i]) > 0.001) {
vec2 centered = uv_local - vec2(0.5);
float dist2 = dot(centered, centered);
float lensK = u_lensEffects[i] * 2.0; // 강도 스케일링
vec2 lensDistortion = centered * lensK * dist2;
texCoord += lensDistortion * u_distortionStrengths[i];
}
}
}
}

13
dist/index.d.mts vendored
View File

@ -11,7 +11,7 @@ interface Point {
/**
*
*/
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad' | 'easeInCubic' | 'easeOutCubic';
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'steps2' | 'steps3' | 'steps4' | 'steps5' | 'steps6' | 'steps8' | 'steps10';
/**
*
*/
@ -68,6 +68,11 @@ interface DistortionArea {
influenceRadius: number;
maxStrength: number;
};
/** 렌즈 효과 설정 (선택사항) */
lensEffect?: {
/** 렌즈 강도 (양수: 볼록, 음수: 오목, 0: 없음, 범위: -1.0 ~ 1.0) */
strength: number;
};
}
/**
*
@ -96,6 +101,8 @@ interface ShaderUniforms {
u_dragVectors: THREE.IUniform<Float32Array>;
/** 각 영역의 왜곡 강도 배열 */
u_distortionStrengths: THREE.IUniform<Float32Array>;
/** 각 영역의 렌즈 효과 강도 배열 */
u_lensEffects: THREE.IUniform<Float32Array>;
}
/**
*
@ -388,6 +395,8 @@ declare const SHADER_CONFIG: {
readonly MAX_DRAG_VECTORS: 8;
/** 최대 강도 배열 크기 */
readonly MAX_STRENGTHS: 8;
/** 최대 렌즈 효과 배열 크기 */
readonly MAX_LENS_EFFECTS: 8;
};
/**
*
@ -418,6 +427,8 @@ declare const DEFAULT_AREA: {
readonly x: -0.1;
readonly y: -0.1;
};
/** 기본 렌즈 효과 강도 */
readonly LENS_STRENGTH: 0;
};
/**

13
dist/index.d.ts vendored
View File

@ -11,7 +11,7 @@ interface Point {
/**
*
*/
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad' | 'easeInCubic' | 'easeOutCubic';
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'steps2' | 'steps3' | 'steps4' | 'steps5' | 'steps6' | 'steps8' | 'steps10';
/**
*
*/
@ -68,6 +68,11 @@ interface DistortionArea {
influenceRadius: number;
maxStrength: number;
};
/** 렌즈 효과 설정 (선택사항) */
lensEffect?: {
/** 렌즈 강도 (양수: 볼록, 음수: 오목, 0: 없음, 범위: -1.0 ~ 1.0) */
strength: number;
};
}
/**
*
@ -96,6 +101,8 @@ interface ShaderUniforms {
u_dragVectors: THREE.IUniform<Float32Array>;
/** 각 영역의 왜곡 강도 배열 */
u_distortionStrengths: THREE.IUniform<Float32Array>;
/** 각 영역의 렌즈 효과 강도 배열 */
u_lensEffects: THREE.IUniform<Float32Array>;
}
/**
*
@ -388,6 +395,8 @@ declare const SHADER_CONFIG: {
readonly MAX_DRAG_VECTORS: 8;
/** 최대 강도 배열 크기 */
readonly MAX_STRENGTHS: 8;
/** 최대 렌즈 효과 배열 크기 */
readonly MAX_LENS_EFFECTS: 8;
};
/**
*
@ -418,6 +427,8 @@ declare const DEFAULT_AREA: {
readonly x: -0.1;
readonly y: -0.1;
};
/** 기본 렌즈 효과 강도 */
readonly LENS_STRENGTH: 0;
};
/**

57
dist/index.js vendored
View File

@ -96,7 +96,8 @@ var ThreeScene = class {
u_numAreas: { value: 0 },
u_dragVectors: { value: new Float32Array(16) },
// 8벡터 × 2(x,y)
u_distortionStrengths: { value: new Float32Array(8) }
u_distortionStrengths: { value: new Float32Array(8) },
u_lensEffects: { value: new Float32Array(8) }
};
this.handleResize();
window.addEventListener("resize", this.handleResize);
@ -242,6 +243,7 @@ var ShaderManager = class {
};
// src/utils/easing.ts
var createStepEasing = (steps) => (t) => Math.floor(t * steps) / steps;
var easingFunctions = {
linear: (t) => t,
easeIn: (t) => t * t,
@ -250,7 +252,14 @@ var easingFunctions = {
easeInQuad: (t) => t * t,
easeOutQuad: (t) => t * (2 - t),
easeInCubic: (t) => t * t * t,
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3)
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
steps2: createStepEasing(2),
steps3: createStepEasing(3),
steps4: createStepEasing(4),
steps5: createStepEasing(5),
steps6: createStepEasing(6),
steps8: createStepEasing(8),
steps10: createStepEasing(10)
};
var applyEasing = (progress, easingType) => {
const clampedProgress = Math.max(0, Math.min(1, progress));
@ -808,7 +817,9 @@ var SHADER_CONFIG = {
/** 최대 드래그 벡터 개수 */
MAX_DRAG_VECTORS: 8,
/** 최대 강도 배열 크기 */
MAX_STRENGTHS: 8
MAX_STRENGTHS: 8,
/** 최대 렌즈 효과 배열 크기 */
MAX_LENS_EFFECTS: 8
};
var ANIMATION_CONFIG = {
/** 목표 FPS */
@ -826,7 +837,9 @@ var DEFAULT_AREA = {
/** 기본 벡터 A */
VECTOR_A: { x: 0.1, y: 0.1 },
/** 기본 벡터 B */
VECTOR_B: { x: -0.1, y: -0.1 }
VECTOR_B: { x: -0.1, y: -0.1 },
/** 기본 렌즈 효과 강도 */
LENS_STRENGTH: 0
};
// src/components/ImageDistortion.tsx
@ -958,11 +971,16 @@ var ImageDistortion = ({
currentAreas.forEach((area, index) => {
strengths[index] = area.distortionStrength;
});
const lensEffects = new Float32Array(SHADER_CONFIG.MAX_LENS_EFFECTS);
currentAreas.forEach((area, index) => {
lensEffects[index] = area.lensEffect?.strength ?? 0;
});
sceneRef.current.updateUniforms({
u_numAreas: { value: currentAreas.length },
u_points: { value: points },
u_dragVectors: { value: dragVectors },
u_distortionStrengths: { value: strengths }
u_distortionStrengths: { value: strengths },
u_lensEffects: { value: lensEffects }
});
sceneRef.current.render();
}, [currentAreas, isReady]);
@ -1092,7 +1110,14 @@ var EASING_OPTIONS = [
{ value: "easeOut", label: "\uAC10\uC18D (Ease Out)" },
{ value: "easeInOut", label: "\uAC00\uAC10\uC18D (Ease In Out)" },
{ value: "easeInQuad", label: "\uAC00\uC18D\xB2 (Ease In Quad)" },
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" }
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" },
{ value: "steps2", label: "2\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps3", label: "3\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps4", label: "4\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps5", label: "5\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps6", label: "6\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps8", label: "8\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps10", label: "10\uB2E8\uACC4 \uC2A4\uD15D" }
];
var ParameterPanel = ({ area, onUpdateArea }) => {
if (!area) {
@ -1154,6 +1179,26 @@ var ParameterPanel = ({ area, onUpdateArea }) => {
}
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "parameter-group", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { children: [
"\uB80C\uC988 \uD6A8\uACFC: ",
(area.lensEffect?.strength ?? 0) > 0 ? "\uBCFC\uB85D " : (area.lensEffect?.strength ?? 0) < 0 ? "\uC624\uBAA9 " : "",
((area.lensEffect?.strength ?? 0) * 100).toFixed(0),
"%"
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"input",
{
type: "range",
min: "-1",
max: "1",
step: "0.01",
value: area.lensEffect?.strength ?? 0,
onChange: (e) => onUpdateArea({ lensEffect: { strength: parseFloat(e.target.value) } }),
className: "slider"
}
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "parameter-group", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "point-coord", children: [

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

57
dist/index.mjs vendored
View File

@ -36,7 +36,8 @@ var ThreeScene = class {
u_numAreas: { value: 0 },
u_dragVectors: { value: new Float32Array(16) },
// 8벡터 × 2(x,y)
u_distortionStrengths: { value: new Float32Array(8) }
u_distortionStrengths: { value: new Float32Array(8) },
u_lensEffects: { value: new Float32Array(8) }
};
this.handleResize();
window.addEventListener("resize", this.handleResize);
@ -182,6 +183,7 @@ var ShaderManager = class {
};
// src/utils/easing.ts
var createStepEasing = (steps) => (t) => Math.floor(t * steps) / steps;
var easingFunctions = {
linear: (t) => t,
easeIn: (t) => t * t,
@ -190,7 +192,14 @@ var easingFunctions = {
easeInQuad: (t) => t * t,
easeOutQuad: (t) => t * (2 - t),
easeInCubic: (t) => t * t * t,
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3)
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
steps2: createStepEasing(2),
steps3: createStepEasing(3),
steps4: createStepEasing(4),
steps5: createStepEasing(5),
steps6: createStepEasing(6),
steps8: createStepEasing(8),
steps10: createStepEasing(10)
};
var applyEasing = (progress, easingType) => {
const clampedProgress = Math.max(0, Math.min(1, progress));
@ -748,7 +757,9 @@ var SHADER_CONFIG = {
/** 최대 드래그 벡터 개수 */
MAX_DRAG_VECTORS: 8,
/** 최대 강도 배열 크기 */
MAX_STRENGTHS: 8
MAX_STRENGTHS: 8,
/** 최대 렌즈 효과 배열 크기 */
MAX_LENS_EFFECTS: 8
};
var ANIMATION_CONFIG = {
/** 목표 FPS */
@ -766,7 +777,9 @@ var DEFAULT_AREA = {
/** 기본 벡터 A */
VECTOR_A: { x: 0.1, y: 0.1 },
/** 기본 벡터 B */
VECTOR_B: { x: -0.1, y: -0.1 }
VECTOR_B: { x: -0.1, y: -0.1 },
/** 기본 렌즈 효과 강도 */
LENS_STRENGTH: 0
};
// src/components/ImageDistortion.tsx
@ -898,11 +911,16 @@ var ImageDistortion = ({
currentAreas.forEach((area, index) => {
strengths[index] = area.distortionStrength;
});
const lensEffects = new Float32Array(SHADER_CONFIG.MAX_LENS_EFFECTS);
currentAreas.forEach((area, index) => {
lensEffects[index] = area.lensEffect?.strength ?? 0;
});
sceneRef.current.updateUniforms({
u_numAreas: { value: currentAreas.length },
u_points: { value: points },
u_dragVectors: { value: dragVectors },
u_distortionStrengths: { value: strengths }
u_distortionStrengths: { value: strengths },
u_lensEffects: { value: lensEffects }
});
sceneRef.current.render();
}, [currentAreas, isReady]);
@ -1032,7 +1050,14 @@ var EASING_OPTIONS = [
{ value: "easeOut", label: "\uAC10\uC18D (Ease Out)" },
{ value: "easeInOut", label: "\uAC00\uAC10\uC18D (Ease In Out)" },
{ value: "easeInQuad", label: "\uAC00\uC18D\xB2 (Ease In Quad)" },
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" }
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" },
{ value: "steps2", label: "2\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps3", label: "3\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps4", label: "4\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps5", label: "5\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps6", label: "6\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps8", label: "8\uB2E8\uACC4 \uC2A4\uD15D" },
{ value: "steps10", label: "10\uB2E8\uACC4 \uC2A4\uD15D" }
];
var ParameterPanel = ({ area, onUpdateArea }) => {
if (!area) {
@ -1094,6 +1119,26 @@ var ParameterPanel = ({ area, onUpdateArea }) => {
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs2("label", { children: [
"\uB80C\uC988 \uD6A8\uACFC: ",
(area.lensEffect?.strength ?? 0) > 0 ? "\uBCFC\uB85D " : (area.lensEffect?.strength ?? 0) < 0 ? "\uC624\uBAA9 " : "",
((area.lensEffect?.strength ?? 0) * 100).toFixed(0),
"%"
] }),
/* @__PURE__ */ jsx3(
"input",
{
type: "range",
min: "-1",
max: "1",
step: "0.01",
value: area.lensEffect?.strength ?? 0,
onChange: (e) => onUpdateArea({ lensEffect: { strength: parseFloat(e.target.value) } }),
className: "slider"
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsx3("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }),
/* @__PURE__ */ jsx3("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ jsxs2("div", { className: "point-coord", children: [

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",
"version": "1.2.6",
"version": "1.2.7",
"publishConfig": {
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
},

View File

@ -196,11 +196,18 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
strengths[index] = area.distortionStrength;
});
// 렌즈 효과 배열 생성
const lensEffects = new Float32Array(SHADER_CONFIG.MAX_LENS_EFFECTS);
currentAreas.forEach((area, index) => {
lensEffects[index] = area.lensEffect?.strength ?? 0;
});
sceneRef.current.updateUniforms({
u_numAreas: { value: currentAreas.length },
u_points: { value: points },
u_dragVectors: { value: dragVectors },
u_distortionStrengths: { value: strengths },
u_lensEffects: { value: lensEffects },
});
sceneRef.current.render();

View File

@ -13,6 +13,13 @@ const EASING_OPTIONS: { value: EasingFunction; label: string }[] = [
{ value: 'easeInOut', label: '가감속 (Ease In Out)' },
{ value: 'easeInQuad', label: '가속² (Ease In Quad)' },
{ value: 'easeOutQuad', label: '감속² (Ease Out Quad)' },
{ value: 'steps2', label: '2단계 스텝' },
{ value: 'steps3', label: '3단계 스텝' },
{ value: 'steps4', label: '4단계 스텝' },
{ value: 'steps5', label: '5단계 스텝' },
{ value: 'steps6', label: '6단계 스텝' },
{ value: 'steps8', label: '8단계 스텝' },
{ value: 'steps10', label: '10단계 스텝' },
];
export const ParameterPanel: React.FC<ParameterPanelProps> = ({ area, onUpdateArea }) => {
@ -84,6 +91,22 @@ export const ParameterPanel: React.FC<ParameterPanelProps> = ({ area, onUpdateAr
</select>
</div>
{/* 렌즈 효과 */}
<div className="parameter-group">
<label>
: {((area.lensEffect?.strength ?? 0) > 0 ? '볼록 ' : (area.lensEffect?.strength ?? 0) < 0 ? '오목 ' : '')}{((area.lensEffect?.strength ?? 0) * 100).toFixed(0)}%
</label>
<input
type="range"
min="-1"
max="1"
step="0.01"
value={area.lensEffect?.strength ?? 0}
onChange={(e) => onUpdateArea({ lensEffect: { strength: parseFloat(e.target.value) } })}
className="slider"
/>
</div>
{/* 포인트 좌표 (읽기 전용 표시) */}
<div className="parameter-group">
<label> ( )</label>

View File

@ -34,6 +34,7 @@ export class ThreeScene {
u_numAreas: { value: 0 },
u_dragVectors: { value: new Float32Array(16) }, // 8벡터 × 2(x,y)
u_distortionStrengths: { value: new Float32Array(8) },
u_lensEffects: { value: new Float32Array(8) },
};
this.handleResize();

View File

@ -4,6 +4,7 @@ uniform vec2 u_points[32]; // 최대 8영역 × 4포인트 (정규화된
uniform int u_numAreas;
uniform vec2 u_dragVectors[8]; // 드래그 벡터 (정규화된 좌표 0-1)
uniform float u_distortionStrengths[8];
uniform float u_lensEffects[8];
varying vec2 vUv;
@ -74,6 +75,15 @@ void main() {
// dragVector는 정규화된 좌표(0-1)이므로 바로 사용
vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i];
texCoord += distortion;
// 렌즈 왜곡 효과 (방사형 UV 왜곡)
if (abs(u_lensEffects[i]) > 0.001) {
vec2 centered = uv_local - vec2(0.5);
float dist2 = dot(centered, centered);
float lensK = u_lensEffects[i] * 2.0; // 강도 스케일링
vec2 lensDistortion = centered * lensK * dist2;
texCoord += lensDistortion * u_distortionStrengths[i];
}
}
}
}

View File

@ -17,7 +17,14 @@ export type EasingFunction =
| 'easeInQuad'
| 'easeOutQuad'
| 'easeInCubic'
| 'easeOutCubic';
| 'easeOutCubic'
| 'steps2'
| 'steps3'
| 'steps4'
| 'steps5'
| 'steps6'
| 'steps8'
| 'steps10';
/**
*
@ -92,6 +99,11 @@ export interface DistortionArea {
influenceRadius: number;
maxStrength: number;
};
/** 렌즈 효과 설정 (선택사항) */
lensEffect?: {
/** 렌즈 강도 (양수: 볼록, 음수: 오목, 0: 없음, 범위: -1.0 ~ 1.0) */
strength: number;
};
}
/**

View File

@ -17,6 +17,8 @@ export interface ShaderUniforms {
u_dragVectors: THREE.IUniform<Float32Array>;
/** 각 영역의 왜곡 강도 배열 */
u_distortionStrengths: THREE.IUniform<Float32Array>;
/** 각 영역의 렌즈 효과 강도 배열 */
u_lensEffects: THREE.IUniform<Float32Array>;
}
/**

View File

@ -10,6 +10,8 @@ export const SHADER_CONFIG = {
MAX_DRAG_VECTORS: 8,
/** 최대 강도 배열 크기 */
MAX_STRENGTHS: 8,
/** 최대 렌즈 효과 배열 크기 */
MAX_LENS_EFFECTS: 8,
} as const;
/**
@ -36,4 +38,6 @@ export const DEFAULT_AREA = {
VECTOR_A: { x: 0.1, y: 0.1 },
/** 기본 벡터 B */
VECTOR_B: { x: -0.1, y: -0.1 },
/** 기본 렌즈 효과 강도 */
LENS_STRENGTH: 0,
} as const;

View File

@ -2,6 +2,10 @@ import { type EasingFunction } from '../types';
type EasingFunc = (t: number) => number;
/** 스텝 이징 헬퍼: floor(t * n) / n → n단계로 양자화 */
const createStepEasing = (steps: number): EasingFunc =>
(t) => Math.floor(t * steps) / steps;
/**
*
*/
@ -17,6 +21,14 @@ const easingFunctions: Record<EasingFunction, EasingFunc> = {
easeInCubic: (t) => t * t * t,
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
steps2: createStepEasing(2),
steps3: createStepEasing(3),
steps4: createStepEasing(4),
steps5: createStepEasing(5),
steps6: createStepEasing(6),
steps8: createStepEasing(8),
steps10: createStepEasing(10),
};
/**