일차 함수 그래프: 기울기와 y절편으로 이해하는 선형 관계
일차 함수의 수학적 정의부터 기울기 시각화, 실생활 응용, JavaScript 그래프 렌더링까지 완벽 분석
선형 보간(Linear Interpolation, LERP)은 두 값 사이의 중간값을 선형적으로 계산하는 방법입니다. 컴퓨터 그래픽스, 애니메이션, 게임 개발에서 가장 기본적이면서도 강력한 도구로, 부드러운 전환 효과를 만드는 핵심 수학 개념입니다.
예: 온도계
오전 10시: 15°C
오후 2시: 25°C
정오(12시)의 온도는?
→ 선형 보간 사용: (15 + 25) / 2 = 20°C
이처럼 선형 보간은 두 알려진 값 사이의 미지값을 추정하는 데 사용됩니다.
두 값 와 사이에서 매개변수 (0 ≤ t ≤ 1)에 따른 보간값:
동일 공식의 두 가지 형태:
가산 형태:
가중 평균 형태:
경계 조건:
선형성 증명:
일 때, (단, ):
두 점 와 사이의 보간:
예제: 와 사이에서 :
/**
* 선형 보간 함수
* @param {number} a - 시작값
* @param {number} b - 끝값
* @param {number} t - 보간 매개변수 (0 ≤ t ≤ 1)
* @returns {number} 보간된 값
*/
function lerp(a, b, t) {
return a + t * (b - a);
}
// 사용 예
console.log(lerp(0, 100, 0)); // 0
console.log(lerp(0, 100, 0.5)); // 50
console.log(lerp(0, 100, 1)); // 100
console.log(lerp(10, 20, 0.3)); // 13// 2D 벡터
function lerp2D(p0, p1, t) {
return {
x: lerp(p0.x, p1.x, t),
y: lerp(p0.y, p1.y, t),
};
}
// 3D 벡터
function lerp3D(p0, p1, t) {
return {
x: lerp(p0.x, p1.x, t),
y: lerp(p0.y, p1.y, t),
z: lerp(p0.z, p1.z, t),
};
}
// 사용 예: 카메라 위치 이동
const cameraStart = { x: 0, y: 0, z: 10 };
const cameraEnd = { x: 5, y: 3, z: 15 };
for (let t = 0; t <= 1; t += 0.1) {
const cameraPos = lerp3D(cameraStart, cameraEnd, t);
console.log(`t=${t.toFixed(1)}:`, cameraPos);
}// RGB 색상 보간
function lerpColor(color1, color2, t) {
return {
r: Math.round(lerp(color1.r, color2.r, t)),
g: Math.round(lerp(color1.g, color2.g, t)),
b: Math.round(lerp(color1.b, color2.b, t)),
};
}
// 사용 예: 빨강에서 파랑으로 전환
const red = { r: 255, g: 0, b: 0 };
const blue = { r: 0, g: 0, b: 255 };
for (let t = 0; t <= 1; t += 0.25) {
const color = lerpColor(red, blue, t);
console.log(`t=${t}:`, `rgb(${color.r}, ${color.g}, ${color.b})`);
}
// 출력:
// t=0: rgb(255, 0, 0) // 빨강
// t=0.25: rgb(191, 0, 64)
// t=0.5: rgb(128, 0, 128) // 보라
// t=0.75: rgb(64, 0, 191)
// t=1: rgb(0, 0, 255) // 파랑CSS transition도 내부적으로 LERP를 사용합니다:
.box {
width: 100px;
transition: width 1s linear;
}
.box:hover {
width: 200px;
}위 CSS는 다음 JavaScript와 동일합니다:
const box = document.querySelector('.box');
const duration = 1000; // 1초
const startWidth = 100;
const endWidth = 200;
let startTime;
function animate(currentTime) {
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
const t = Math.min(elapsed / duration, 1); // 0 ~ 1로 정규화
const width = lerp(startWidth, endWidth, t);
box.style.width = `${width}px`;
if (t < 1) {
requestAnimationFrame(animate);
}
}
box.addEventListener('mouseenter', () => {
startTime = null;
requestAnimationFrame(animate);
});LERP는 선형이므로 등속 운동을 만듭니다. 자연스러운 애니메이션을 위해 easing 함수를 조합합니다:
// Easing 함수들
const easing = {
linear: t => t,
easeInQuad: t => t * t,
easeOutQuad: t => t * (2 - t),
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeInCubic: t => t * t * t,
easeOutCubic: t => (--t) * t * t + 1,
};
// LERP + Easing
function lerpEased(a, b, t, easingFunc = easing.linear) {
const easedT = easingFunc(t);
return lerp(a, b, easedT);
}
// 사용 예: 부드러운 감속
for (let t = 0; t <= 1; t += 0.2) {
const linearPos = lerp(0, 100, t);
const easedPos = lerpEased(0, 100, t, easing.easeOutQuad);
console.log(`t=${t}: linear=${linearPos}, eased=${easedPos.toFixed(1)}`);
}
// 출력:
// t=0: linear=0, eased=0.0
// t=0.2: linear=20, eased=36.0
// t=0.4: linear=40, eased=64.0
// t=0.6: linear=60, eased=84.0
// t=0.8: linear=80, eased=96.0
// t=1: linear=100, eased=100.0class SmoothScroll {
constructor() {
this.current = window.scrollY;
this.target = window.scrollY;
this.ease = 0.1; // 보간 속도
this.update();
}
setTarget(target) {
this.target = target;
}
update = () => {
// LERP로 부드러운 스크롤
this.current = lerp(this.current, this.target, this.ease);
// 거의 도달하면 정확히 맞춤
if (Math.abs(this.target - this.current) < 0.1) {
this.current = this.target;
}
window.scrollTo(0, this.current);
requestAnimationFrame(this.update);
};
}
const smoothScroll = new SmoothScroll();
// 사용
document.querySelector('.link').addEventListener('click', (e) => {
e.preventDefault();
const target = document.querySelector(e.target.hash);
smoothScroll.setTarget(target.offsetTop);
});class Character {
constructor(x, y) {
this.position = { x, y };
this.targetPosition = { x, y };
this.speed = 0.1; // LERP 속도
}
moveTo(x, y) {
this.targetPosition = { x, y };
}
update() {
// LERP로 부드러운 이동
this.position.x = lerp(this.position.x, this.targetPosition.x, this.speed);
this.position.y = lerp(this.position.y, this.targetPosition.y, this.speed);
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, 10, 0, Math.PI * 2);
ctx.fill();
}
}
// 게임 루프
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const character = new Character(100, 100);
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
character.moveTo(e.clientX - rect.left, e.clientY - rect.top);
});
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
character.update();
character.draw(ctx);
requestAnimationFrame(gameLoop);
}
gameLoop();class Camera {
constructor() {
this.position = { x: 0, y: 0 };
this.followSpeed = 0.05;
}
follow(target) {
// LERP로 부드러운 카메라 추적
this.position.x = lerp(this.position.x, target.x, this.followSpeed);
this.position.y = lerp(this.position.y, target.y, this.followSpeed);
}
apply(ctx) {
ctx.translate(-this.position.x, -this.position.y);
}
}import { gsap } from 'gsap';
// GSAP 내부적으로 LERP 사용
gsap.to('.box', {
x: 200,
duration: 1,
ease: 'power2.out', // easing + LERP
});
// 커스텀 LERP 애니메이션
const obj = { value: 0 };
gsap.to(obj, {
value: 100,
duration: 2,
onUpdate: () => {
console.log(obj.value.toFixed(2));
},
});import { motion } from 'framer-motion';
function AnimatedComponent() {
return (
<motion.div
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
>
내용
</motion.div>
);
}import * as THREE from 'three';
// Vector3.lerp() 메서드 제공
const start = new THREE.Vector3(0, 0, 0);
const end = new THREE.Vector3(10, 10, 10);
const result = new THREE.Vector3();
result.lerpVectors(start, end, 0.5); // (5, 5, 5)
// 애니메이션 루프에서 사용
function animate() {
camera.position.lerp(targetPosition, 0.1);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}// [주의] 나쁜 예: 매 프레임마다 차이 계산
function badLerp(a, b, t) {
return a + t * (b - a); // (b - a) 매번 계산
}
for (let i = 0; i < 1000; i++) {
const value = badLerp(0, 100, i / 1000);
}
// [권장] 좋은 예: 차이를 미리 계산
const diff = 100 - 0;
for (let i = 0; i < 1000; i++) {
const value = 0 + (i / 1000) * diff;
}class SmoothValue {
constructor(initial) {
this.current = initial;
this.target = initial;
}
update() {
const diff = Math.abs(this.target - this.current);
// 거의 도달하면 바로 설정 (부동소수점 오차 방지)
if (diff < 0.001) {
this.current = this.target;
return false; // 업데이트 불필요
}
this.current = lerp(this.current, this.target, 0.1);
return true; // 계속 업데이트 필요
}
}// SIMD를 지원하는 환경에서 여러 값을 한 번에 보간
// (WebAssembly 또는 네이티브 코드에서 유용)
function lerpArray(a, b, t, result) {
const len = a.length;
for (let i = 0; i < len; i++) {
result[i] = a[i] + t * (b[i] - a[i]);
}
}
const a = new Float32Array([0, 10, 20, 30]);
const b = new Float32Array([100, 110, 120, 130]);
const result = new Float32Array(4);
lerpArray(a, b, 0.5, result); // [50, 60, 70, 80]역 선형 보간은 보간된 값으로부터 매개변수 를 역산합니다.
/**
* 역 선형 보간
* @param {number} a - 시작값
* @param {number} b - 끝값
* @param {number} value - 보간된 값
* @returns {number} 매개변수 t
*/
function inverseLerp(a, b, value) {
return (value - a) / (b - a);
}
// 사용 예: 스크롤 진행률 계산
const scrollStart = 0;
const scrollEnd = document.body.scrollHeight - window.innerHeight;
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
const progress = inverseLerp(scrollStart, scrollEnd, scrollY);
console.log(`스크롤 진행률: ${(progress * 100).toFixed(1)}%`);
});Inverse LERP + LERP 조합으로 값의 범위를 변환합니다:
/**
* 값의 범위를 변환
* @param {number} value - 입력 값
* @param {number} inMin - 입력 최소값
* @param {number} inMax - 입력 최대값
* @param {number} outMin - 출력 최소값
* @param {number} outMax - 출력 최대값
* @returns {number} 변환된 값
*/
function remap(value, inMin, inMax, outMin, outMax) {
const t = inverseLerp(inMin, inMax, value);
return lerp(outMin, outMax, t);
}
// 예: 온도 (섭씨 → 화씨)
const celsius = 25;
const fahrenheit = remap(celsius, 0, 100, 32, 212);
console.log(`${celsius}°C = ${fahrenheit}°F`); // 77°F
// 예: 마우스 X 위치 → 회전 각도
canvas.addEventListener('mousemove', (e) => {
const mouseX = e.clientX;
const angle = remap(mouseX, 0, window.innerWidth, -90, 90);
element.style.transform = `rotate(${angle}deg)`;
});<!DOCTYPE html>
<html>
<head>
<style>
.progress-container {
width: 300px;
height: 20px;
background: #ddd;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(to right, #4CAF50, #8BC34A);
width: 0%;
transition: width 0.3s ease-out;
}
</style>
</head>
<body>
<div class="progress-container">
<div class="progress-bar" id="progressBar"></div>
</div>
<button onclick="updateProgress()">진행</button>
<script>
function lerp(a, b, t) {
return a + t * (b - a);
}
let currentProgress = 0;
let targetProgress = 0;
function updateProgress() {
targetProgress = Math.min(targetProgress + 20, 100);
}
function animate() {
// LERP로 부드러운 진행
currentProgress = lerp(currentProgress, targetProgress, 0.1);
const progressBar = document.getElementById('progressBar');
progressBar.style.width = `${currentProgress}%`;
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>선형 보간(LERP)은 다음과 같은 이유로 애니메이션과 그래픽스의 핵심입니다:
활용 분야:
LERP를 마스터하면 부드럽고 자연스러운 사용자 경험을 만드는 강력한 도구를 얻게 됩니다.
일차 함수의 수학적 정의부터 기울기 시각화, 실생활 응용, JavaScript 그래프 렌더링까지 완벽 분석
접선의 수학적 정의부터 Bezier 곡선, 애니메이션 타이밍 함수, 그래픽스 응용까지 완벽 분석
선형 보간을 통한 Cubic-bezier 곡선 생성 원리와 CSS 애니메이션에서의 활용