미적분학 개념의 시각적 이해와 코드 구현
Google AI Studio 에서 3Blue1Brown-The essence of calculus 스타일로 미적분 개념을 시각적으로 이해하는 웹 페이지 작성
– Gemini 2.5 Pro 05-06은 코딩, 추론, 멀티모달 이해 분야에서 뛰어난 성능을 보이는 최신 AI 모델
– 이 가이드에서는 3Blue1Brown의 수학적 시각화 접근법에서 영감을 받아, 복잡한 수학적 개념을 이해하고 시각화
Google AI Studio https://aistudio.google.com
Interactive Calculus Concepts: https://songpd.com/vibecode/calculus_viz.html
인터랙티브 미적분학 개념 https://songpd.com/vibecode/calculus_viz_kr.html
미적분 개념을 시각적으로 이해하는 웹 도구 소개
수학은 추상적인 개념이 많아 학습자들이 이해하기 어려운 경우가 많습니다. 특히 미적분은 변화율과 누적량을 다루는 만큼, 개념을 시각적으로 표현하면 학습에 큰 도움이 됩니다. 이번 글에서는 이러한 미적분 개념을 시각화하여 학습할 수 있는 웹 도구를 소개합니다.
주요 기능 및 특징
1. 도함수 시각화: 접선의 기울기 이해하기
이 도구는 함수의 도함수를 시각적으로 보여줍니다. 사용자는 함수 f(x)f(x)와 특정 지점 xx, 그리고 작은 변화량 hh를 입력하여, 해당 지점에서의 접선의 기울기를 시각적으로 확인할 수 있습니다. 이를 통해 도함수의 개념과 접선의 의미를 직관적으로 이해할 수 있습니다.
2. 정적분 시각화: 곡선 아래 면적 계산하기
정적분은 곡선 아래의 면적을 계산하는 개념입니다. 이 도구는 사용자가 함수 f(x)f(x), 구간 [a, b], 그리고 분할 개수 N을 입력하면, 해당 구간을 N개의 직사각형으로 나누어 근사 면적을 계산하고 시각화합니다. 이를 통해 리만 합의 개념과 정적분의 의미를 쉽게 이해할 수 있습니다.
3. 벡터 필드 시각화: 공간에서의 힘과 흐름 표현하기
벡터 필드는 공간의 각 지점에 벡터를 할당하여, 힘의 방향과 크기, 유체의 흐름 등을 표현합니다. 이 도구는 사용자가 벡터 필드의 x 및 y 성분을 정의하면, 2차원 평면에 해당 벡터 필드를 시각화하여 보여줍니다. 이를 통해 물리적 현상이나 수학적 개념을 직관적으로 이해할 수 있습니다.
활용 사례 및 추천 대상
- 수학 교사 및 강사: 수업 중 미적분 개념을 시각적으로 설명할 때 유용합니다.
- 대학생 및 고등학생: 미적분 개념을 직관적으로 이해하고자 하는 학습자에게 적합합니다.
- 자기주도 학습자: 독학으로 미적분을 공부하는 이들에게 개념 이해를 돕는 도구로 활용할 수 있습니다.
[ 미적분 개념 시각화 웹 도구 작성 과정]
3Blue1Brown 스타일로 미적분학의 핵심 개념들을 직관적으로 이해하고 시각화할 수 있도록 한글로 된 설명을 제공하고, 이를 인터랙티브하게 탐색할 수 있는 단일 HTML 웹 애플리케이션 작성
1. 미분: “얼마나 빠르게 변하고 있는가?”의 본질
자동차가 시간에 따라 움직인 거리를 나타내는 함수가 있다고 상상해 보세요. 두 특정 시간 지점 사이의 평균 속도를 알고 싶다면, 이동한 거리를 걸린 시간으로 나누면 됩니다. 기하학적으로 이는 시간-거리 그래프에서 두 점을 잇는 직선( 할선 )의 기울기와 같습니다.
하지만 순간 속도를 알고 싶다면 어떨까요? 바로 지금 이 순간의 속도 말입니다. 자동차 속도계가 이걸 알려주죠. 속도계는 어떻게 “알고” 있을까요?
미적분학은 이 질문에 답하기 위해, 두 시간 지점이 무한히 가까워질 때 평균 속도가 어떻게 되는지를 묻습니다.
두 번째 점이 첫 번째 점에 점점 더 가까이 다가가면, 두 점을 잇는 할선은 회전하면서 마치 첫 번째 점에서 곡선에 살짝 “입맞춤”하는 듯한 직선처럼 보이기 시작합니다. 이 “입맞춤”하는 선을 접선이라고 부릅니다.
한 지점에서 함수의 미분값은 바로 이 접선의 기울기입니다. 이는 그 지점에서 함수의 순간적인 변화율을 알려줍니다. 만약 함수가 시간에 따른 위치라면, 미분값은 순간 속도입니다. 일반적인 곡선 y=f(x)의 경우, 미분값 f'(x)는 각 지점 x에서 곡선이 얼마나 가파른지를 알려줍니다.
핵심 아이디어: 미분값은 (x, f(x))와 (x+h, f(x+h)) 사이의 할선들의 기울기를 살펴보고, h(x의 작은 변화량)가 0으로 줄어들 때 어떤 일이 일어나는지를 통해 찾습니다.
2. 정적분: “무한히 많은 아주 작은 것들을 더하는 것”의 본질
곡선, 예를 들어 y = x^2 아래, x = 0에서 x = 3 사이의 넓이를 구하고 싶다고 해봅시다. 이 모양은 직사각형이나 삼각형처럼 단순하지 않아서 바로 적용할 수 있는 공식이 없습니다.
만약 직사각형들을 사용해서 넓이를 근사한다면 어떨까요? 구간 [0, 3]을 몇 개의 작은 구간으로 나누고, 각 구간마다 함수의 값(예: 구간의 왼쪽 끝, 오른쪽 끝, 또는 중간 지점)으로 높이가 결정되고 구간의 너비가 밑변이 되는 직사각형을 그릴 수 있습니다. 이 직사각형들의 넓이의 합은 곡선 아래 넓이의 근사값을 제공합니다. 이것이 리만 합입니다.
몇 개의 직사각형만 사용하면 근사값은 거칠 것입니다. 하지만 더 많은 직사각형을 사용한다면 어떨까요? 훨씬 더 많이요? 직사각형의 개수를 늘려 각 직사각형을 더 가늘게 만들수록, 우리의 근사값은 점점 더 정확해집니다.
정적분은 곡선 아래의 정확한 넓이입니다. 이는 직사각형의 개수가 무한대로 갈 때 (따라서 각 직사각형의 너비는 0으로 줄어들 때) 이 직사각형 넓이들의 합이 수렴하는 값입니다. 이는 무한히 많은 극소량의 기여를 합산하는 방법입니다.
핵심 아이디어: 정적분은 리만 합의 극한입니다. 무한히 많은 극소량의 조각들을 합산하여 (넓이, 거리, 부피와 같은) 총 누적량을 찾는 방법입니다.
3. 벡터 미적분학과 벡터장: 공간에서의 흐름과 힘 묘사하기
바람이 부는 것을 상상해 보세요. 공기 중의 모든 지점에서 바람은 방향과 속력(크기)을 가집니다. 우리는 각 지점에 화살표를 사용하여 이를 나타낼 수 있습니다: 화살표의 방향은 바람의 방향을, 길이는 바람의 속력을 보여줍니다. 공간(또는 2D 평면)의 각 지점에 하나씩 있는 이 화살표들의 모음을 벡터장이라고 합니다.
더 공식적으로, 벡터장은 정의역의 각 지점에 벡터를 할당하는 함수입니다. 예를 들어, 2D에서 벡터장 F는 한 지점 (x, y)를 입력받아 벡터 <P(x,y), Q(x,y)>를 출력할 수 있습니다.
- P(x,y)는 (x,y)에서의 벡터의 x-성분을 결정하는 함수입니다.
- Q(x,y)는 (x,y)에서의 벡터의 y-성분을 결정하는 함수입니다.
벡터장은 다음을 묘사하는 데 매우 유용합니다:
- 흐름: 유체 흐름 (파이프 속의 물, 날개 주변의 공기).
- 힘: 중력장, 전기장, 자기장. 한 지점의 벡터는 그곳에서 입자가 경험할 힘을 알려줍니다.
- 기울기(Gradient): 다변수 함수(예: 표면의 온도)의 경우, 기울기는 가장 가파른 상승 방향을 가리키는 벡터장입니다.
벡터장을 시각화하는 것은 일반적으로 격자점을 그리고 각 격자점에 해당하는 벡터(표시를 위해 적절히 축척됨)를 배치하는 것을 포함합니다. 우리는 종종 발산원(벡터가 멀어지는 곳), 수렴점(벡터가 모이는 곳) 또는 회전 패턴과 같은 양상을 볼 수 있습니다.
핵심 아이디어: 벡터장은 한 영역의 모든 지점에 방향과 크기를 할당하여, 흐름이나 힘과 같은 현상을 모델링하고 이해할 수 있게 합니다.
인터랙티브 시각화 (HTML/JavaScript – 한글 버전)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>인터랙티브 미적분학 시각화</title>
<style>
body { font-family: 'Malgun Gothic', sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { text-align: center; color: #1a5276; }
h2 { color: #2a7a9c; border-bottom: 2px solid #2a7a9c; padding-bottom: 5px; }
h3 { color: #3a9cb0; }
canvas { border: 1px solid #ccc; display: block; margin: 10px auto; }
.controls { margin-bottom: 15px; padding: 10px; background-color: #e9e9e9; border-radius: 4px; }
.controls label { margin-right: 10px; }
.controls input[type="text"], .controls input[type="number"] {
padding: 5px;
margin-right: 15px;
border: 1px solid #ccc;
border-radius: 4px;
}
.controls input[type="range"] { vertical-align: middle; }
.explanation { margin-top: 15px; line-height: 1.6; }
.math-principles { background-color: #f0f8ff; padding: 10px; border-left: 4px solid #2a7a9c; margin-top: 10px; border-radius: 4px;}
code { background-color: #eee; padding: 2px 4px; border-radius: 3px; font-family: 'D2Coding', monospace; }
.output-value { font-weight: bold; color: #d9534f; }
</style>
<!-- Math.js 라이브러리 로드 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.8.0/math.min.js"></script>
</head>
<body>
<h1>인터랙티브 미적분학 개념 (3Blue1Brown 스타일)</h1>
<!-- 미분 섹션 -->
<div class="container">
<h2>1. 미분: 접선의 기울기</h2>
<div class="explanation">
<p>함수의 한 지점에서의 미분값은 그 지점에서 함수의 '가파른 정도' 또는 순간적인 변화율을 나타냅니다. 먼저 곡선 위의 두 점 <code>(x, f(x))</code>와 <code>(x+h, f(x+h))</code>를 연결하는 <strong>할선</strong>을 고려합니다. 이 할선의 기울기는 <code>(f(x+h) - f(x)) / h</code>입니다.</p>
<p><code>h</code>(두 점 사이의 수평 거리)를 점점 더 작게 만들면, 할선은 <code>x</code> 지점에서 곡선에 대한 <strong>접선</strong>에 가까워집니다. 이 접선의 기울기가 바로 미분값, <code>f'(x)</code>입니다.</p>
</div>
<div class="controls">
<label for="derivativeFunc">f(x) = </label>
<input type="text" id="derivativeFunc" value="x^2" size="10">
<label for="derivativeX">x = </label>
<input type="range" id="derivativeX" min="-5" max="5" value="1" step="0.1">
<span id="derivativeXValue">1</span>
<label for="derivativeH">h = </label>
<input type="range" id="derivativeH" min="0.01" max="3" value="2" step="0.01">
<span id="derivativeHValue">2</span>
<p>할선 기울기: <span id="secantSlope" class="output-value">계산 중...</span> | 접선 기울기 근사값 (f'(x)): <span id="tangentSlope" class="output-value">계산 중...</span></p>
</div>
<canvas id="derivativeCanvas" width="500" height="300"></canvas>
<div class="math-principles">
<h4>수학적 원리:</h4>
<ul>
<li><strong>함수 그래프:</strong> 곡선 <code>y = f(x)</code>를 그립니다.</li>
<li><strong>할선:</strong> 두 점 <code>P1 = (x_val, f(x_val))</code>과 <code>P2 = (x_val + h_val, f(x_val + h_val))</code>을 연결합니다.</li>
<li><strong>할선의 기울기:</strong> <code>m_secant = (f(x_val + h_val) - f(x_val)) / h_val</code>.</li>
<li><strong>접선 (근사):</strong> <code>h_val</code>이 0에 가까워짐에 따라 할선은 접선이 됩니다. 이 접선의 기울기가 미분값입니다. 시각화를 위해 매우 작은 <code>h</code>를 사용하여 근사합니다.</li>
</ul>
</div>
</div>
<!-- 정적분 섹션 -->
<div class="container">
<h2>2. 정적분: 곡선 아래 넓이</h2>
<div class="explanation">
<p>함수 <code>f(x)</code>의 <code>a</code>부터 <code>b</code>까지의 정적분은 해당 구간에서 곡선과 x축 사이의 누적된 넓이를 나타냅니다. 이 넓이를 <strong>리만 합</strong>을 사용하여 근사할 수 있습니다. 이는 구간 <code>[a, b]</code>를 <code>N</code>개의 더 작은 부분 구간(직사각형)으로 나누는 것을 포함합니다.</p>
<p>각 직사각형의 너비는 <code>dx = (b-a)/N</code>입니다. 높이는 각 부분 구간의 왼쪽 끝점, 오른쪽 끝점 또는 중간 지점에서의 함수 값으로 결정될 수 있습니다. <code>N</code>(직사각형의 개수)이 증가할수록 이 직사각형들의 넓이 합은 곡선 아래의 실제 넓이에 더 가까워집니다.</p>
</div>
<div class="controls">
<label for="integralFunc">f(x) = </label>
<input type="text" id="integralFunc" value="x^2 + 1" size="10">
<label for="integralA">a = </label>
<input type="number" id="integralA" value="0" step="0.1" style="width: 50px;">
<label for="integralB">b = </label>
<input type="number" id="integralB" value="3" step="0.1" style="width: 50px;">
<label for="integralN">N (직사각형 개수) = </label>
<input type="range" id="integralN" min="1" max="100" value="10" step="1">
<span id="integralNValue">10</span>
<p>넓이 근사값 (리만 합): <span id="riemannSumArea" class="output-value">계산 중...</span></p>
</div>
<canvas id="integralCanvas" width="500" height="300"></canvas>
<div class="math-principles">
<h4>수학적 원리:</h4>
<ul>
<li><strong>함수 그래프:</strong> 곡선 <code>y = f(x)</code>를 그립니다.</li>
<li><strong>구간 분할:</strong> 구간 <code>[a, b]</code>를 <code>N</code>개의 부분 구간으로 나누며, 각 구간의 너비는 <code>dx = (b-a)/N</code>입니다.</li>
<li><strong>직사각형 넓이:</strong> i번째 직사각형에 대해 (간단히 왼쪽 끝점 사용):
<ul>
<li><code>x_i = a + i * dx</code></li>
<li>높이 = <code>f(x_i)</code></li>
<li>넓이_i = <code>f(x_i) * dx</code></li>
</ul>
</li>
<li><strong>리만 합:</strong> 총 근사 넓이는 <code>i</code>가 0부터 <code>N-1</code>까지 변할 때 <code>넓이_i</code>의 합입니다.</li>
<li><strong>정적분:</strong> 이 합의 N이 무한대로 갈 때의 극한값입니다.</li>
</ul>
</div>
</div>
<!-- 벡터장 섹션 -->
<div class="container">
<h2>3. 벡터장: 공간에서의 힘과 흐름</h2>
<div class="explanation">
<p>벡터장은 2D 평면 또는 3D 공간의 모든 지점에 벡터(방향과 크기를 가진 화살표)를 할당합니다. 이는 성분 함수, 예를 들어 <code>F(x,y) = <P(x,y), Q(x,y)></code>로 정의됩니다. <code>P(x,y)</code>는 지점 <code>(x,y)</code>에서 벡터의 x-성분을, <code>Q(x,y)</code>는 y-성분을 제공합니다.</p>
<p>벡터장은 바람의 패턴, 유체의 흐름, 또는 힘의 장(중력이나 전자기력 등)과 같은 물리적 현상을 나타낼 수 있습니다. 다양한 격자 지점에 화살표를 그려 시각화합니다.</p>
</div>
<div class="controls">
<label for="vectorPFunc">P(x,y) (x-성분) = </label>
<input type="text" id="vectorPFunc" value="-y" size="10">
<label for="vectorQFunc">Q(x,y) (y-성분) = </label>
<input type="text" id="vectorQFunc" value="x" size="10">
<label for="arrowScale">화살표 크기: </label>
<input type="range" id="arrowScale" min="0.1" max="2" value="0.5" step="0.05">
<span id="arrowScaleValue">0.5</span>
</div>
<canvas id="vectorFieldCanvas" width="400" height="400"></canvas>
<div class="math-principles">
<h4>수학적 원리:</h4>
<ul>
<li><strong>벡터 성분:</strong> 격자의 각 지점 <code>(x,y)</code>에 대해 <code>vx = P(x,y)</code>와 <code>vy = Q(x,y)</code>를 계산합니다.</li>
<li><strong>화살표 표현:</strong> <code>(x,y)</code>에서 시작하여 <code>(x + scale*vx, y + scale*vy)</code> 근처에서 끝나는 화살표를 그립니다. 'scale' 인자는 시각적 명확성을 위한 것입니다.</li>
<li><strong>일반적인 예:</strong>
<ul>
<li>회전형: <code>P(x,y) = -y</code>, <code>Q(x,y) = x</code> (원형 흐름 생성).</li>
<li>방사형 (발산형): <code>P(x,y) = x</code>, <code>Q(x,y) = y</code> (벡터가 원점에서 바깥으로 향함).</li>
<li>상수형: <code>P(x,y) = 1</code>, <code>Q(x,y) = 0</code> (모든 벡터가 오른쪽을 향함).</li>
</ul>
</li>
</ul>
</div>
</div>
<script>
// --- 미분 데모 ---
const derivativeCanvas = document.getElementById('derivativeCanvas');
const derivativeCtx = derivativeCanvas.getContext('2d');
const derivativeFuncInput = document.getElementById('derivativeFunc');
const derivativeXSlider = document.getElementById('derivativeX');
const derivativeXValueSpan = document.getElementById('derivativeXValue');
const derivativeHSlider = document.getElementById('derivativeH');
const derivativeHValueSpan = document.getElementById('derivativeHValue');
const secantSlopeSpan = document.getElementById('secantSlope');
const tangentSlopeSpan = document.getElementById('tangentSlope');
let derivativeScope = { xMin: -5, xMax: 5, yMin: -5, yMax: 10 };
function transformDerivativeX(x) {
return (x - derivativeScope.xMin) * (derivativeCanvas.width / (derivativeScope.xMax - derivativeScope.xMin));
}
function transformDerivativeY(y) {
return derivativeCanvas.height - (y - derivativeScope.yMin) * (derivativeCanvas.height / (derivativeScope.yMax - derivativeScope.yMin));
}
function drawDerivativeAxes() {
derivativeCtx.strokeStyle = '#ccc';
derivativeCtx.lineWidth = 1;
// X축
derivativeCtx.beginPath();
derivativeCtx.moveTo(0, transformDerivativeY(0));
derivativeCtx.lineTo(derivativeCanvas.width, transformDerivativeY(0));
derivativeCtx.stroke();
// Y축
derivativeCtx.beginPath();
derivativeCtx.moveTo(transformDerivativeX(0), 0);
derivativeCtx.lineTo(transformDerivativeX(0), derivativeCanvas.height);
derivativeCtx.stroke();
derivativeCtx.fillStyle = '#aaa';
derivativeCtx.font = '10px Arial';
for (let i = Math.floor(derivativeScope.xMin); i <= derivativeScope.xMax; i++) {
if (i !== 0) derivativeCtx.fillText(i, transformDerivativeX(i) + 2, transformDerivativeY(0) - 2);
}
for (let i = Math.floor(derivativeScope.yMin); i <= derivativeScope.yMax; i++) {
if (i !== 0) derivativeCtx.fillText(i, transformDerivativeX(0) + 2, transformDerivativeY(i) + 10);
}
}
function drawDerivativeDemo() {
derivativeCtx.clearRect(0, 0, derivativeCanvas.width, derivativeCanvas.height);
drawDerivativeAxes();
const funcStr = derivativeFuncInput.value;
const x_val = parseFloat(derivativeXSlider.value);
let h_val = parseFloat(derivativeHSlider.value);
derivativeXValueSpan.textContent = x_val.toFixed(2);
derivativeHValueSpan.textContent = h_val.toFixed(2);
let f;
try {
const node = math.parse(funcStr);
f = x => node.evaluate({x: x});
} catch (e) {
secantSlopeSpan.textContent = "잘못된 함수";
tangentSlopeSpan.textContent = "N/A";
return;
}
// 함수 그리기
derivativeCtx.strokeStyle = '#2a7a9c';
derivativeCtx.lineWidth = 2;
derivativeCtx.beginPath();
for (let i = 0; i <= derivativeCanvas.width; i++) {
const x_canv = i;
const x_world = (x_canv / derivativeCanvas.width) * (derivativeScope.xMax - derivativeScope.xMin) + derivativeScope.xMin;
try {
const y_world = f(x_world);
if (isNaN(y_world) || !isFinite(y_world)) continue;
if (i === 0 || (isNaN(f( ((i-1)/derivativeCanvas.width) * (derivativeScope.xMax - derivativeScope.xMin) + derivativeScope.xMin )))) {
derivativeCtx.moveTo(x_canv, transformDerivativeY(y_world));
} else {
derivativeCtx.lineTo(x_canv, transformDerivativeY(y_world));
}
} catch (e) {}
}
derivativeCtx.stroke();
try {
const y1 = f(x_val);
const y2 = f(x_val + h_val);
const p1_canv_x = transformDerivativeX(x_val);
const p1_canv_y = transformDerivativeY(y1);
const p2_canv_x = transformDerivativeX(x_val + h_val);
const p2_canv_y = transformDerivativeY(y2);
// 점 그리기
derivativeCtx.fillStyle = 'red';
derivativeCtx.beginPath();
derivativeCtx.arc(p1_canv_x, p1_canv_y, 4, 0, 2 * Math.PI);
derivativeCtx.fill();
derivativeCtx.fillStyle = 'orange';
derivativeCtx.beginPath();
derivativeCtx.arc(p2_canv_x, p2_canv_y, 4, 0, 2 * Math.PI);
derivativeCtx.fill();
// 할선 그리기
derivativeCtx.strokeStyle = 'orange';
derivativeCtx.lineWidth = 1.5;
derivativeCtx.beginPath();
const slopeSec = (y2 - y1) / h_val;
secantSlopeSpan.textContent = slopeSec.toFixed(3);
const sec_y_start = y1 - slopeSec * (x_val - derivativeScope.xMin);
const sec_y_end = y1 + slopeSec * (derivativeScope.xMax - x_val);
derivativeCtx.moveTo(transformDerivativeX(derivativeScope.xMin), transformDerivativeY(sec_y_start));
derivativeCtx.lineTo(transformDerivativeX(derivativeScope.xMax), transformDerivativeY(sec_y_end));
derivativeCtx.stroke();
// 접선 그리기 (매우 작은 h로 근사)
const h_tangent = 0.0001;
const y_tangent_near = f(x_val + h_tangent);
const slopeTan = (y_tangent_near - y1) / h_tangent;
tangentSlopeSpan.textContent = slopeTan.toFixed(3);
derivativeCtx.strokeStyle = 'green';
derivativeCtx.lineWidth = 1.5;
derivativeCtx.beginPath();
const tan_y_start = y1 - slopeTan * (x_val - derivativeScope.xMin);
const tan_y_end = y1 + slopeTan * (derivativeScope.xMax - x_val);
derivativeCtx.moveTo(transformDerivativeX(derivativeScope.xMin), transformDerivativeY(tan_y_start));
derivativeCtx.lineTo(transformDerivativeX(derivativeScope.xMax), transformDerivativeY(tan_y_end));
derivativeCtx.stroke();
} catch (e) {
secantSlopeSpan.textContent = "계산 오류";
tangentSlopeSpan.textContent = "N/A";
}
}
[derivativeFuncInput, derivativeXSlider, derivativeHSlider].forEach(el => el.addEventListener('input', drawDerivativeDemo));
drawDerivativeDemo();
// --- 정적분 데모 ---
const integralCanvas = document.getElementById('integralCanvas');
const integralCtx = integralCanvas.getContext('2d');
const integralFuncInput = document.getElementById('integralFunc');
const integralASlider = document.getElementById('integralA');
const integralBSlider = document.getElementById('integralB');
const integralNSlider = document.getElementById('integralN');
const integralNValueSpan = document.getElementById('integralNValue');
const riemannSumAreaSpan = document.getElementById('riemannSumArea');
let integralScope = { xMin: -1, xMax: 4, yMin: -1, yMax: 15 };
function transformIntegralX(x) {
return (x - integralScope.xMin) * (integralCanvas.width / (integralScope.xMax - integralScope.xMin));
}
function transformIntegralY(y) {
return integralCanvas.height - (y - integralScope.yMin) * (integralCanvas.height / (integralScope.yMax - integralScope.yMin));
}
function drawIntegralAxes() {
integralCtx.strokeStyle = '#ccc';
integralCtx.lineWidth = 1;
// X축
integralCtx.beginPath();
integralCtx.moveTo(0, transformIntegralY(0));
integralCtx.lineTo(integralCanvas.width, transformIntegralY(0));
integralCtx.stroke();
// Y축
integralCtx.beginPath();
integralCtx.moveTo(transformIntegralX(0), 0);
integralCtx.lineTo(transformIntegralX(0), integralCanvas.height);
integralCtx.stroke();
integralCtx.fillStyle = '#aaa';
integralCtx.font = '10px Arial';
for (let i = Math.floor(integralScope.xMin); i <= integralScope.xMax; i++) {
if (i !== 0) integralCtx.fillText(i, transformIntegralX(i) + 2, transformIntegralY(0) - 2);
}
const yStep = integralScope.yMax > 20 ? 5 : (integralScope.yMax > 10 ? 2 : 1);
for (let i = Math.floor(integralScope.yMin / yStep) * yStep; i <= integralScope.yMax; i+= yStep ) {
if (i !== 0) integralCtx.fillText(i, transformIntegralX(0) + 2, transformIntegralY(i) +10);
}
}
function drawIntegralDemo() {
integralCtx.clearRect(0, 0, integralCanvas.width, integralCanvas.height);
const a = parseFloat(integralASlider.value);
const b = parseFloat(integralBSlider.value);
const N = parseInt(integralNSlider.value);
const funcStr = integralFuncInput.value;
integralScope.xMin = Math.min(-1, a - 1, b -1);
integralScope.xMax = Math.max(4, a + 1, b + 1);
let f_integral;
let maxYInInterval = -Infinity;
let minYInInterval = Infinity;
try {
const node = math.parse(funcStr);
f_integral = x_val => node.evaluate({x: x_val});
for (let x_test = Math.min(a,b); x_test <= Math.max(a,b); x_test += (Math.abs(b-a))/100 || 0.01) {
const y_test = f_integral(x_test);
if (!isNaN(y_test) && isFinite(y_test)) {
maxYInInterval = Math.max(maxYInInterval, y_test);
minYInInterval = Math.min(minYInInterval, y_test);
}
}
integralScope.yMin = Math.min(0, minYInInterval - 1);
integralScope.yMax = Math.max(5, maxYInInterval + 1);
if (minYInInterval === Infinity) integralScope.yMin = -1; // Default if no valid points
if (maxYInInterval === -Infinity) integralScope.yMax = 5; // Default
} catch (e) {
riemannSumAreaSpan.textContent = "잘못된 함수";
drawIntegralAxes(); // Draw axes even if function is bad
return;
}
drawIntegralAxes();
integralNValueSpan.textContent = N;
if (a === b) {
riemannSumAreaSpan.textContent = "0.000";
return;
}
const actual_a = Math.min(a,b);
const actual_b = Math.max(a,b);
// 함수 그리기
integralCtx.strokeStyle = '#2a7a9c';
integralCtx.lineWidth = 2;
integralCtx.beginPath();
let firstPoint = true;
for (let i = 0; i <= integralCanvas.width; i++) {
const x_canv = i;
const x_world = (x_canv / integralCanvas.width) * (integralScope.xMax - integralScope.xMin) + integralScope.xMin;
try {
const y_world = f_integral(x_world);
if (isNaN(y_world) || !isFinite(y_world)) { firstPoint = true; continue; }
if (firstPoint) {
integralCtx.moveTo(x_canv, transformIntegralY(y_world));
firstPoint = false;
} else {
integralCtx.lineTo(x_canv, transformIntegralY(y_world));
}
} catch (e) { firstPoint = true; }
}
integralCtx.stroke();
// 리만 합 직사각형 그리기 (왼쪽 합)
const dx = (actual_b - actual_a) / N;
let totalArea = 0;
integralCtx.fillStyle = 'rgba(58, 156, 176, 0.5)';
integralCtx.strokeStyle = 'rgba(40, 120, 140, 0.8)';
integralCtx.lineWidth = 0.5;
for (let i = 0; i < N; i++) {
const x_i = actual_a + i * dx;
let rectHeight_world = 0;
try {
rectHeight_world = f_integral(x_i);
if (isNaN(rectHeight_world) || !isFinite(rectHeight_world)) rectHeight_world = 0;
} catch(e) { rectHeight_world = 0; }
const rectX_canv = transformIntegralX(x_i);
const rectY_canv_top = transformIntegralY(rectHeight_world);
const rectWidth_canv = transformIntegralX(x_i + dx) - rectX_canv;
const baseLineY_canv = transformIntegralY(0);
let rectHeight_canv;
if (rectHeight_world >= 0) {
rectHeight_canv = baseLineY_canv - rectY_canv_top;
integralCtx.fillRect(rectX_canv, rectY_canv_top, rectWidth_canv, rectHeight_canv);
integralCtx.strokeRect(rectX_canv, rectY_canv_top, rectWidth_canv, rectHeight_canv);
} else {
rectHeight_canv = rectY_canv_top - baseLineY_canv;
integralCtx.fillRect(rectX_canv, baseLineY_canv, rectWidth_canv, rectHeight_canv);
integralCtx.strokeRect(rectX_canv, baseLineY_canv, rectWidth_canv, rectHeight_canv);
}
totalArea += rectHeight_world * dx;
}
if (a > b) totalArea *= -1; // If integrating backwards
riemannSumAreaSpan.textContent = totalArea.toFixed(3);
}
[integralFuncInput, integralASlider, integralBSlider, integralNSlider].forEach(el => el.addEventListener('input', drawIntegralDemo));
drawIntegralDemo();
// --- 벡터장 데모 ---
const vectorCanvas = document.getElementById('vectorFieldCanvas');
const vectorCtx = vectorCanvas.getContext('2d');
const vectorPFuncInput = document.getElementById('vectorPFunc');
const vectorQFuncInput = document.getElementById('vectorQFunc');
const arrowScaleSlider = document.getElementById('arrowScale');
const arrowScaleValueSpan = document.getElementById('arrowScaleValue');
const vectorGridSize = 20;
const vectorWorldMin = -5;
const vectorWorldMax = 5;
function drawVectorFieldDemo() {
vectorCtx.clearRect(0, 0, vectorCanvas.width, vectorCanvas.height);
const pFuncStr = vectorPFuncInput.value;
const qFuncStr = vectorQFuncInput.value;
const arrowScale = parseFloat(arrowScaleSlider.value);
arrowScaleValueSpan.textContent = arrowScale.toFixed(2);
let pFuncNode, qFuncNode;
try {
pFuncNode = math.parse(pFuncStr);
qFuncNode = math.parse(qFuncStr);
} catch (e) {
vectorCtx.fillStyle = 'red';
vectorCtx.font = '12px "Malgun Gothic"';
vectorCtx.fillText("잘못된 함수입니다", 10, 20);
return;
}
const step = vectorCanvas.width / vectorGridSize;
const worldStep = (vectorWorldMax - vectorWorldMin) / vectorGridSize;
vectorCtx.strokeStyle = '#3a9cb0';
vectorCtx.lineWidth = 1;
for (let i = 0; i < vectorGridSize; i++) {
for (let j = 0; j < vectorGridSize; j++) {
const canvasX = (i + 0.5) * step;
const canvasY = (j + 0.5) * step;
const worldX = vectorWorldMin + (i + 0.5) * worldStep;
const worldY = vectorWorldMin + (vectorGridSize - 1 - j - 0.5) * worldStep;
try {
const vx = pFuncNode.evaluate({x: worldX, y: worldY});
const vy = qFuncNode.evaluate({x: worldX, y: worldY});
if (isNaN(vx) || isNaN(vy) || !isFinite(vx) || !isFinite(vy)) continue;
const magnitude = Math.sqrt(vx*vx + vy*vy);
if (magnitude === 0) continue;
const displayMagnitudeFactor = step * arrowScale * 0.7;
const displayVx = (vx / magnitude) * displayMagnitudeFactor;
const displayVy = (vy / magnitude) * displayMagnitudeFactor;
vectorCtx.beginPath();
vectorCtx.moveTo(canvasX, canvasY);
vectorCtx.lineTo(canvasX + displayVx, canvasY - displayVy);
vectorCtx.stroke();
const angle = Math.atan2(-displayVy, displayVx);
vectorCtx.beginPath();
vectorCtx.moveTo(canvasX + displayVx, canvasY - displayVy);
vectorCtx.lineTo(canvasX + displayVx - 5 * Math.cos(angle - Math.PI / 6), canvasY - displayVy - 5 * Math.sin(angle - Math.PI / 6));
vectorCtx.moveTo(canvasX + displayVx, canvasY - displayVy);
vectorCtx.lineTo(canvasX + displayVx - 5 * Math.cos(angle + Math.PI / 6), canvasY - displayVy - 5 * Math.sin(angle + Math.PI / 6));
vectorCtx.stroke();
} catch (e) { /* 한 지점에서 함수 실패 시 건너뛰기 */ }
}
}
// Draw origin crosshairs
vectorCtx.strokeStyle = '#ccc';
vectorCtx.lineWidth = 0.5;
vectorCtx.beginPath();
vectorCtx.moveTo(vectorCanvas.width/2, 0);
vectorCtx.lineTo(vectorCanvas.width/2, vectorCanvas.height);
vectorCtx.stroke();
vectorCtx.beginPath();
vectorCtx.moveTo(0, vectorCanvas.height/2);
vectorCtx.lineTo(vectorCanvas.width, vectorCanvas.height/2);
vectorCtx.stroke();
}
[vectorPFuncInput, vectorQFuncInput, arrowScaleSlider].forEach(el => el.addEventListener('input', drawVectorFieldDemo));
drawVectorFieldDemo();
</script>
</body>
</html>