Refactor step easing into independent snapSteps property

- EasingFunction에서 step 옵션을 제거하고 독립적인 snapSteps 속성으로 분리
- AnimationLoop에 snapSteps 기반의 움직임 양자화 로직 구현
- 에디터 파라미터 패널에 움직임 단계 조절 슬라이더 추가
- 기본 설정값에 SNAP_STEPS 추가 및 패키지 버전 업데이트 (1.2.10)
This commit is contained in:
BaekRyang 2026-02-25 16:14:46 +09:00
parent 6d9dd082c1
commit ecf3e81101
13 changed files with 180 additions and 101 deletions

View File

@ -13,7 +13,8 @@
"Bash(findstr:*)",
"Bash(npm link:*)",
"Bash(find:*)",
"Bash(nul)"
"Bash(nul)",
"Bash(cd:*)"
],
"deny": [],
"ask": []

6
dist/index.d.mts vendored
View File

@ -11,7 +11,7 @@ interface Point {
/**
*
*/
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'steps2' | 'steps3' | 'steps4' | 'steps5' | 'steps6' | 'steps8' | 'steps10';
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad' | 'easeInCubic' | 'easeOutCubic';
/**
*
*/
@ -73,6 +73,8 @@ interface DistortionArea {
/** 렌즈 강도 (양수: 볼록, 음수: 오목, 0: 없음, 범위: -1.0 ~ 1.0) */
strength: number;
};
/** 스텝 양자화 단계 수 (0=없음, 1~5단계, 이징과 독립적으로 적용) */
snapSteps?: number;
}
/**
*
@ -429,6 +431,8 @@ declare const DEFAULT_AREA: {
};
/** 기본 렌즈 효과 강도 */
readonly LENS_STRENGTH: 0;
/** 기본 스텝 양자화 단계 수 (0=없음) */
readonly SNAP_STEPS: 0;
};
/**

6
dist/index.d.ts vendored
View File

@ -11,7 +11,7 @@ interface Point {
/**
*
*/
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'steps2' | 'steps3' | 'steps4' | 'steps5' | 'steps6' | 'steps8' | 'steps10';
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad' | 'easeInCubic' | 'easeOutCubic';
/**
*
*/
@ -73,6 +73,8 @@ interface DistortionArea {
/** 렌즈 강도 (양수: 볼록, 음수: 오목, 0: 없음, 범위: -1.0 ~ 1.0) */
strength: number;
};
/** 스텝 양자화 단계 수 (0=없음, 1~5단계, 이징과 독립적으로 적용) */
snapSteps?: number;
}
/**
*
@ -429,6 +431,8 @@ declare const DEFAULT_AREA: {
};
/** 기본 렌즈 효과 강도 */
readonly LENS_STRENGTH: 0;
/** 기본 스텝 양자화 단계 수 (0=없음) */
readonly SNAP_STEPS: 0;
};
/**

63
dist/index.js vendored
View File

@ -243,7 +243,6 @@ 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,
@ -252,14 +251,7 @@ var easingFunctions = {
easeInQuad: (t) => t * t,
easeOutQuad: (t) => t * (2 - t),
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)
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3)
};
var applyEasing = (progress, easingType) => {
const clampedProgress = Math.max(0, Math.min(1, progress));
@ -353,14 +345,33 @@ var AnimationLoop = class {
}
const easedProgress = applyEasing(progress, movement.easing);
let dragVector;
const snapSteps = area.snapSteps ?? 0;
if (movement.preset && isRotationPreset(movement.preset)) {
const angle = easedProgress * Math.PI * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === "rotate-cw" ? 1 : -1;
if (snapSteps > 0) {
const totalAngleSteps = snapSteps * 4;
const rawAngle = progress * Math.PI * 2;
const quantizedAngle = Math.round(rawAngle / (Math.PI * 2) * totalAngleSteps) / totalAngleSteps * Math.PI * 2;
dragVector = {
x: Math.cos(quantizedAngle * direction) * radius,
y: Math.sin(quantizedAngle * direction) * radius
};
} else {
const angle = easedProgress * Math.PI * 2;
dragVector = {
x: Math.cos(angle * direction) * radius,
y: Math.sin(angle * direction) * radius
};
}
} else {
if (snapSteps > 0) {
const oscillation = Math.sin(progress * Math.PI * 2);
const quantized = Math.round(oscillation * snapSteps) / snapSteps;
dragVector = {
x: baseVector.x * quantized,
y: baseVector.y * quantized
};
} else {
const oscillation = Math.sin(easedProgress * Math.PI * 2);
dragVector = {
@ -368,6 +379,7 @@ var AnimationLoop = class {
y: baseVector.y * oscillation
};
}
}
return {
...area,
dragVector
@ -839,7 +851,9 @@ var DEFAULT_AREA = {
/** 기본 벡터 B */
VECTOR_B: { x: -0.1, y: -0.1 },
/** 기본 렌즈 효과 강도 */
LENS_STRENGTH: 0
LENS_STRENGTH: 0,
/** 기본 스텝 양자화 단계 수 (0=없음) */
SNAP_STEPS: 0
};
// src/components/ImageDistortion.tsx
@ -1110,14 +1124,7 @@ 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: "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" }
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" }
];
var ParameterPanel = ({ area, onUpdateArea }) => {
if (!area) {
@ -1199,6 +1206,24 @@ var ParameterPanel = ({ area, onUpdateArea }) => {
}
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "parameter-group", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { children: [
"\uC6C0\uC9C1\uC784 \uB2E8\uACC4: ",
(area.snapSteps ?? 0) === 0 ? "\uC5C6\uC74C" : `${area.snapSteps}\uB2E8\uACC4`
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"input",
{
type: "range",
min: "0",
max: "5",
step: "1",
value: area.snapSteps ?? 0,
onChange: (e) => onUpdateArea({ snapSteps: parseInt(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

63
dist/index.mjs vendored
View File

@ -183,7 +183,6 @@ 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,
@ -192,14 +191,7 @@ var easingFunctions = {
easeInQuad: (t) => t * t,
easeOutQuad: (t) => t * (2 - t),
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)
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3)
};
var applyEasing = (progress, easingType) => {
const clampedProgress = Math.max(0, Math.min(1, progress));
@ -293,14 +285,33 @@ var AnimationLoop = class {
}
const easedProgress = applyEasing(progress, movement.easing);
let dragVector;
const snapSteps = area.snapSteps ?? 0;
if (movement.preset && isRotationPreset(movement.preset)) {
const angle = easedProgress * Math.PI * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === "rotate-cw" ? 1 : -1;
if (snapSteps > 0) {
const totalAngleSteps = snapSteps * 4;
const rawAngle = progress * Math.PI * 2;
const quantizedAngle = Math.round(rawAngle / (Math.PI * 2) * totalAngleSteps) / totalAngleSteps * Math.PI * 2;
dragVector = {
x: Math.cos(quantizedAngle * direction) * radius,
y: Math.sin(quantizedAngle * direction) * radius
};
} else {
const angle = easedProgress * Math.PI * 2;
dragVector = {
x: Math.cos(angle * direction) * radius,
y: Math.sin(angle * direction) * radius
};
}
} else {
if (snapSteps > 0) {
const oscillation = Math.sin(progress * Math.PI * 2);
const quantized = Math.round(oscillation * snapSteps) / snapSteps;
dragVector = {
x: baseVector.x * quantized,
y: baseVector.y * quantized
};
} else {
const oscillation = Math.sin(easedProgress * Math.PI * 2);
dragVector = {
@ -308,6 +319,7 @@ var AnimationLoop = class {
y: baseVector.y * oscillation
};
}
}
return {
...area,
dragVector
@ -779,7 +791,9 @@ var DEFAULT_AREA = {
/** 기본 벡터 B */
VECTOR_B: { x: -0.1, y: -0.1 },
/** 기본 렌즈 효과 강도 */
LENS_STRENGTH: 0
LENS_STRENGTH: 0,
/** 기본 스텝 양자화 단계 수 (0=없음) */
SNAP_STEPS: 0
};
// src/components/ImageDistortion.tsx
@ -1050,14 +1064,7 @@ 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: "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" }
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" }
];
var ParameterPanel = ({ area, onUpdateArea }) => {
if (!area) {
@ -1139,6 +1146,24 @@ var ParameterPanel = ({ area, onUpdateArea }) => {
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs2("label", { children: [
"\uC6C0\uC9C1\uC784 \uB2E8\uACC4: ",
(area.snapSteps ?? 0) === 0 ? "\uC5C6\uC74C" : `${area.snapSteps}\uB2E8\uACC4`
] }),
/* @__PURE__ */ jsx3(
"input",
{
type: "range",
min: "0",
max: "5",
step: "1",
value: area.snapSteps ?? 0,
onChange: (e) => onUpdateArea({ snapSteps: parseInt(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.7",
"version": "1.2.10",
"publishConfig": {
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
},

View File

@ -13,13 +13,6 @@ 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 }) => {
@ -107,6 +100,22 @@ export const ParameterPanel: React.FC<ParameterPanelProps> = ({ area, onUpdateAr
/>
</div>
{/* 스텝 양자화 */}
<div className="parameter-group">
<label>
: {(area.snapSteps ?? 0) === 0 ? '없음' : `${area.snapSteps}단계`}
</label>
<input
type="range"
min="0"
max="5"
step="1"
value={area.snapSteps ?? 0}
onChange={(e) => onUpdateArea({ snapSteps: parseInt(e.target.value) })}
className="slider"
/>
</div>
{/* 포인트 좌표 (읽기 전용 표시) */}
<div className="parameter-group">
<label> ( )</label>

View File

@ -41,24 +41,50 @@ export class AnimationLoop {
// 벡터 계산
let dragVector: Point;
// 스텝 양자화 (영역 속성에서 가져옴)
const snapSteps = area.snapSteps ?? 0;
// 회전 프리셋인 경우 원운동
if (movement.preset && isRotationPreset(movement.preset)) {
const angle = easedProgress * Math.PI * 2;
const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y);
const direction = movement.preset === 'rotate-cw' ? 1 : -1;
if (snapSteps > 0) {
// 스텝 양자화: 각도를 이산적 단계로 양자화
const totalAngleSteps = snapSteps * 4;
const rawAngle = progress * Math.PI * 2;
const quantizedAngle = Math.round(rawAngle / (Math.PI * 2) * totalAngleSteps) / totalAngleSteps * Math.PI * 2;
dragVector = {
x: Math.cos(quantizedAngle * direction) * radius,
y: Math.sin(quantizedAngle * direction) * radius,
};
} else {
const angle = easedProgress * Math.PI * 2;
dragVector = {
x: Math.cos(angle * direction) * radius,
y: Math.sin(angle * direction) * radius,
};
}
} else {
// 일반 왕복 모션 (sin 기반으로 진짜 좌↔우/상↔하 왕복)
// sin(0)=0 → sin(π/2)=1 → sin(π)=0 → sin(3π/2)=-1 → sin(2π)=0
if (snapSteps > 0) {
// 스텝 양자화: oscillation 출력을 양자화
// Math.round(sin * N) / N → 한 방향 N단계, 전체 2N+1개 위치
const oscillation = Math.sin(progress * Math.PI * 2);
const quantized = Math.round(oscillation * snapSteps) / snapSteps;
dragVector = {
x: baseVector.x * quantized,
y: baseVector.y * quantized,
};
} else {
const oscillation = Math.sin(easedProgress * Math.PI * 2);
dragVector = {
x: baseVector.x * oscillation,
y: baseVector.y * oscillation,
};
}
}
return {
...area,

View File

@ -17,14 +17,7 @@ export type EasingFunction =
| 'easeInQuad'
| 'easeOutQuad'
| 'easeInCubic'
| 'easeOutCubic'
| 'steps2'
| 'steps3'
| 'steps4'
| 'steps5'
| 'steps6'
| 'steps8'
| 'steps10';
| 'easeOutCubic';
/**
*
@ -104,6 +97,8 @@ export interface DistortionArea {
/** 렌즈 강도 (양수: 볼록, 음수: 오목, 0: 없음, 범위: -1.0 ~ 1.0) */
strength: number;
};
/** 스텝 양자화 단계 수 (0=없음, 1~5단계, 이징과 독립적으로 적용) */
snapSteps?: number;
}
/**

View File

@ -40,4 +40,6 @@ export const DEFAULT_AREA = {
VECTOR_B: { x: -0.1, y: -0.1 },
/** 기본 렌즈 효과 강도 */
LENS_STRENGTH: 0,
/** 기본 스텝 양자화 단계 수 (0=없음) */
SNAP_STEPS: 0,
} as const;

View File

@ -2,10 +2,6 @@ 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;
/**
*
*/
@ -21,14 +17,6 @@ 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),
};
/**