复变函数可视化工具:动态演示复平面变换
引言
在复变函数的学习过程中,如何直观地理解函数对复平面的变换一直是一个挑战。为了帮助学习者更好地理解复变函数的几何意义,我开发了一个基于Web的复变函数可视化工具。这个工具能够动态展示复变函数如何将复平面上的点进行映射变换,让抽象的数学概念变得生动可见。最后会给出完整代码。
功能概述
界面布局
工具采用左右对比的展示方式:
- 左侧显示原始复平面,包含标准网格和坐标轴
- 右侧实时展示函数变换后的结果,通过动画效果直观呈现变换过程
支持的函数变换
目前支持以下几种典型的复变函数:
-
f(z) = z (恒等映射)
- 作为参考基准,帮助理解其他变换
- 输出等同于输入,网格保持不变
-
f(z) = z² (平方映射)
- 展示了复数平方的几何效果
- 可以观察到角度加倍、距离平方的现象
-
f(z) = 1/z (倒数映射)
- 演示了复数倒数的几何意义
- 体现了圆反演的特性
-
f(z) = z² + 1
- 展示了复数多项式的行为
- 观察平移变换的效果
-
f(z) = e^z (指数映射)
- 展示了复指数函数的周期性
- 观察条带区域如何映射到螺旋形状
技术实现
复数运算
核心是实现了Complex类处理复数运算:
javascript
class Complex {
constructor(re, im) {
this.re = re;
this.im = im;
}
// 复数加法
plus(other) {
return new Complex(
this.re + other.re,
this.im + other.im
);
}
// 复数乘法
times(other) {
return new Complex(
this.re * other.re - this.im * other.im,
this.re * other.im + this.im * other.re
);
}
// 其他复数运算...
}
动画实现
采用requestAnimationFrame实现平滑动画效果:
javascript
function animate() {
if (!playing) return;
t += 0.01; // 控制动画速度
if (t > 1) {
t = 1;
playing = false;
}
drawTransGrid();
if (playing) {
requestAnimationFrame(animate);
}
}
绘图技术
使用Canvas进行网格绘制,主要步骤:
- 坐标变换:将数学坐标映射到画布坐标
- 网格线绘制:通过循环绘制水平和垂直线条
- 实时更新:根据动画参数t更新网格位置
使用指南
-
选择函数
- 从下拉菜单中选择要观察的函数
- 每个函数都有其特定的几何特性
-
控制动画
- 点击"播放"开始动画
- "暂停"可以在任意时刻停止观察
- "重置"返回初始状态
-
观察要点
- 关注坐标轴的变化
- 观察网格线的扭曲方式
- 注意特殊点的映射关系
教育价值
这个工具在数学教育中有多重价值:
-
直观理解
- 将抽象的数学概念可视化
- 帮助建立几何直觉
-
互动学习
- 学习者可以自主探索
- 即时观察变换效果
-
概念联系
- 建立代数和几何的联系
- 理解不同函数间的关系
技术特点
-
响应式设计
- 适配不同屏幕尺寸
- 良好的移动端体验
-
性能优化
- 使用requestAnimationFrame确保动画流畅
- 优化绘图算法提高效率
-
代码组织
- 模块化设计
- 清晰的代码结构
未来展望
计划添加的功能:
-
更多复变函数
- 三角函数
- 对数函数
- 有理函数
-
交互增强
- 自定义函数输入
- 放大缩小功能
- 特殊点标记
-
教育功能
- 添加教学注释
- 保存动画过程
- 导出图像功能
完整代码
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>复数域函数动态平面变形示例</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center p-4">
<h1 class="text-2xl font-bold mb-4">复数域函数动态平面变形示例</h1>
<!-- 函数选择区 -->
<div class="mb-4 flex space-x-2 items-center">
<label for="funcSelect" class="mr-2">选择函数:</label>
<select id="funcSelect" class="p-1 border rounded">
<option value="z">f(z) = z</option>
<option value="z2">f(z) = z²</option>
<option value="1z">f(z) = 1/z</option>
<option value="z2plus1">f(z) = z² + 1</option>
<option value="expz">f(z) = e^z</option>
</select>
<!-- 动画控制按钮 -->
<button id="playBtn" class="px-2 py-1 bg-blue-500 text-white rounded">播放动画</button>
<button id="pauseBtn" class="px-2 py-1 bg-red-500 text-white rounded">暂停动画</button>
<button id="resetBtn" class="px-2 py-1 bg-gray-500 text-white rounded">复位</button>
</div>
<!-- 画布容器 -->
<div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
<!-- 左侧:原平面 -->
<div class="flex flex-col items-center">
<h2 class="font-semibold mb-2">原平面 (Domain)</h2>
<canvas id="domainCanvas" width="400" height="400" class="border border-gray-300"></canvas>
</div>
<!-- 右侧:变形过程 (可动态演示从 t=0 到 t=1) -->
<div class="flex flex-col items-center">
<h2 class="font-semibold mb-2">动态变形 (Intermediate → Range)</h2>
<canvas id="transCanvas" width="400" height="400" class="border border-gray-300"></canvas>
</div>
</div>
<script>
// ========== 全局变量 ==========
const domainCanvas = document.getElementById("domainCanvas");
const domainCtx = domainCanvas.getContext("2d");
const transCanvas = document.getElementById("transCanvas");
const transCtx = transCanvas.getContext("2d");
const funcSelect = document.getElementById("funcSelect");
const playBtn = document.getElementById("playBtn");
const pauseBtn = document.getElementById("pauseBtn");
const resetBtn = document.getElementById("resetBtn");
// 画布大小
const WIDTH = 400;
const HEIGHT = 400;
// 数学坐标范围
const XMIN = -2, XMAX = 2;
const YMIN = -2, YMAX = 2;
// 网格步进
const STEPS = 20;
// 动画控制
let animationRequest = null;
let t = 0; // 插值参数 (0 ~ 1)
let playing = false; // 是否正在播放
// ========== 复数结构和函数集 ==========
class Complex {
constructor(re, im) {
this.re = re;
this.im = im;
}
plus(other) {
return new Complex(this.re + other.re, this.im + other.im);
}
times(other) {
return new Complex(
this.re * other.re - this.im * other.im,
this.re * other.im + this.im * other.re
);
}
reciprocal() {
const denom = this.re * this.re + this.im * this.im;
return new Complex(this.re / denom, -this.im / denom);
}
exp() {
// e^(x+iy) = e^x (cos y + i sin y)
const r = Math.exp(this.re);
return new Complex(r * Math.cos(this.im), r * Math.sin(this.im));
}
}
// (1)恒等映射 f(z) = z
function f_z(z) {
return z;
}
// (2)平方映射 f(z) = z^2
function f_z2(z) {
return z.times(z);
}
// (3)倒数映射 f(z) = 1/z
function f_1z(z) {
return new Complex(1, 0).times(z.reciprocal());
}
// (4)z^2 + 1
function f_z2plus1(z) {
return z.times(z).plus(new Complex(1, 0));
}
// (5)指数映射 f(z) = e^z
function f_expz(z) {
return z.exp();
}
// 根据选择返回当前函数
function getCurrentFunc() {
switch (funcSelect.value) {
case "z": return f_z;
case "z2": return f_z2;
case "1z": return f_1z;
case "z2plus1": return f_z2plus1;
case "expz": return f_expz;
}
return f_z; // 默认
}
// ========== 坐标变换函数 ==========
// 将数学坐标映射到画布像素坐标
function toCanvasX(x) {
return ((x - XMIN) / (XMAX - XMIN)) * WIDTH;
}
function toCanvasY(y) {
// 画布 Y 轴向下
return HEIGHT - ((y - YMIN) / (YMAX - YMIN)) * HEIGHT;
}
// ========== 绘图函数 ==========
// 绘制原平面的网格
function drawDomainGrid(ctx) {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
ctx.strokeStyle = "#ccc";
ctx.lineWidth = 1;
const stepX = (XMAX - XMIN) / STEPS;
const stepY = (YMAX - YMIN) / STEPS;
// 水平线
for (let i = 0; i <= STEPS; i++) {
const y = YMIN + i * stepY;
ctx.beginPath();
ctx.moveTo(toCanvasX(XMIN), toCanvasY(y));
ctx.lineTo(toCanvasX(XMAX), toCanvasY(y));
ctx.stroke();
}
// 垂直线
for (let j = 0; j <= STEPS; j++) {
const x = XMIN + j * stepX;
ctx.beginPath();
ctx.moveTo(toCanvasX(x), toCanvasY(YMIN));
ctx.lineTo(toCanvasX(x), toCanvasY(YMAX));
ctx.stroke();
}
// 画坐标轴
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
// x 轴
ctx.beginPath();
ctx.moveTo(toCanvasX(XMIN), toCanvasY(0));
ctx.lineTo(toCanvasX(XMAX), toCanvasY(0));
ctx.stroke();
// y 轴
ctx.beginPath();
ctx.moveTo(toCanvasX(0), toCanvasY(YMIN));
ctx.lineTo(toCanvasX(0), toCanvasY(YMAX));
ctx.stroke();
}
// 在插值下绘制变形网格
// t=0 时画与 domainGrid 同位置
// t=1 时画与 f(z) 后对应的位置
function drawTransformedGrid(ctx, transformFunc, t) {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
ctx.strokeStyle = "#ccc";
ctx.lineWidth = 1;
const stepX = (XMAX - XMIN) / STEPS;
const stepY = (YMAX - YMIN) / STEPS;
// 插值函数:z(t) = (1-t)*z + t*f(z)
function interpolate(z, w, t) {
return new Complex(
(1 - t) * z.re + t * w.re,
(1 - t) * z.im + t * w.im
);
}
// 每条水平线
for (let i = 0; i <= STEPS; i++) {
const y = YMIN + i * stepY;
ctx.beginPath();
let firstPoint = true;
for (let x = XMIN; x <= XMAX + 1e-9; x += stepX / 4) {
const z = new Complex(x, y);
const w = transformFunc(z);
const zt = interpolate(z, w, t);
const px = toCanvasX(zt.re);
const py = toCanvasY(zt.im);
if (firstPoint) {
ctx.moveTo(px, py);
firstPoint = false;
} else {
ctx.lineTo(px, py);
}
}
ctx.stroke();
}
// 每条垂直线
for (let j = 0; j <= STEPS; j++) {
const x = XMIN + j * stepX;
ctx.beginPath();
let firstPoint = true;
for (let y = YMIN; y <= YMAX + 1e-9; y += stepY / 4) {
const z = new Complex(x, y);
const w = transformFunc(z);
const zt = interpolate(z, w, t);
const px = toCanvasX(zt.re);
const py = toCanvasY(zt.im);
if (firstPoint) {
ctx.moveTo(px, py);
firstPoint = false;
} else {
ctx.lineTo(px, py);
}
}
ctx.stroke();
}
// 画插值后的 x 轴和 y 轴
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
// x 轴: y=0
ctx.beginPath();
let firstAxisX = true;
for (let x = XMIN; x <= XMAX + 1e-9; x += stepX / 4) {
const z = new Complex(x, 0);
const w = transformFunc(z);
const zt = interpolate(z, w, t);
if (firstAxisX) {
ctx.moveTo(toCanvasX(zt.re), toCanvasY(zt.im));
firstAxisX = false;
} else {
ctx.lineTo(toCanvasX(zt.re), toCanvasY(zt.im));
}
}
ctx.stroke();
// y 轴: x=0
ctx.beginPath();
let firstAxisY = true;
for (let y = YMIN; y <= YMAX + 1e-9; y += stepY / 4) {
const z = new Complex(0, y);
const w = transformFunc(z);
const zt = interpolate(z, w, t);
if (firstAxisY) {
ctx.moveTo(toCanvasX(zt.re), toCanvasY(zt.im));
firstAxisY = false;
} else {
ctx.lineTo(toCanvasX(zt.re), toCanvasY(zt.im));
}
}
ctx.stroke();
}
// ========== 动画相关函数 ==========
function animate() {
if (!playing) return;
t += 0.01; // 调整速度
if (t > 1) {
t = 1;
playing = false;
}
drawTransGrid(); // 绘制当前插值下的网格
if (playing) {
animationRequest = requestAnimationFrame(animate);
}
}
function drawAll() {
// 先画左侧固定的 domain 网格
drawDomainGrid(domainCtx);
// 再画右侧根据 t 的插值绘制结果
drawTransGrid();
}
function drawTransGrid() {
const currentFunc = getCurrentFunc();
drawTransformedGrid(transCtx, currentFunc, t);
}
// ========== 按钮事件 ==========
playBtn.addEventListener("click", () => {
if (t >= 1) t = 0; // 如果已经到结尾,再次播放则从头开始
playing = true;
if (!animationRequest) animate();
});
pauseBtn.addEventListener("click", () => {
playing = false;
animationRequest = null;
});
resetBtn.addEventListener("click", () => {
playing = false;
t = 0;
animationRequest = null;
drawAll();
});
// 在函数下拉变更时,复位并重新绘制
funcSelect.addEventListener("change", () => {
playing = false;
t = 0;
animationRequest = null;
drawAll();
});
// 页面载入时初始绘制
window.onload = () => {
drawAll();
};
</script>
</body>
</html>
结语
这个复变函数可视化工具不仅是一个教学辅助工具,也是理解复变函数几何意义的有力帮手。通过动态、直观的方式展示复变函数的变换效果,让学习复变函数变得更加生动有趣。欢迎教师和学生使用这个工具进行教学和学习,也欢迎开发者参与改进和扩展这个项目。
参考资源:
- 复变函数教材
- HTML5 Canvas文档
- JavaScript动画最佳实践