| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>高阶傅里叶分形实验室</title>
- <style>
- body {
- margin: 0;
- overflow: hidden;
- background-color: #050505;
- font-family: 'Segoe UI', sans-serif;
- color: #ddd;
- }
- canvas { display: block; }
-
- /* 控制面板样式 */
- #controls {
- position: absolute;
- top: 10px;
- left: 10px;
- width: 280px;
- max-height: 95vh;
- overflow-y: auto;
- background: rgba(20, 20, 30, 0.85);
- backdrop-filter: blur(10px);
- padding: 20px;
- border-radius: 12px;
- border: 1px solid #444;
- box-shadow: 0 8px 32px rgba(0,0,0,0.5);
- transition: opacity 0.3s;
- }
- #controls:hover { opacity: 1; }
-
- h3 { margin: 0 0 15px 0; font-size: 16px; color: #fff; border-bottom: 1px solid #555; padding-bottom: 10px; }
- .group { margin-bottom: 15px; }
- .slider-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; }
- label { font-size: 12px; color: #aaa; }
- span.val { font-size: 12px; font-weight: bold; color: #4facfe; width: 40px; text-align: right;}
- input[type=range] { width: 100%; margin: 5px 0; cursor: pointer; accent-color: #4facfe; }
- select { width: 100%; background: #333; color: white; border: 1px solid #555; padding: 5px; border-radius: 4px; }
-
- button {
- width: 100%; padding: 8px; margin-top: 10px;
- background: linear-gradient(45deg, #4facfe, #00f2fe);
- border: none; border-radius: 6px; color: #000; font-weight: bold; cursor: pointer;
- }
- button:hover { filter: brightness(1.1); }
-
- .preset-btn {
- width: 30%; font-size: 10px; padding: 5px; margin: 2px 0; display: inline-block;
- background: #333; color: white; border: 1px solid #555;
- }
- /* 隐藏滚动条但保留功能 */
- ::-webkit-scrollbar { width: 6px; }
- ::-webkit-scrollbar-track { background: #1a1a1a; }
- ::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
- </style>
- </head>
- <body>
- <div id="controls">
- <h3>分形参数核心</h3>
-
- <div class="group">
- <label>频率模式 (Frequency Mode)</label>
- <select id="freqMode">
- <option value="power">指数 (分形/Fractal)</option>
- <option value="linear">线性 (万花尺/Spiro)</option>
- <option value="prime">质数 (混沌/Chaos)</option>
- </select>
- </div>
- <div class="group">
- <div class="slider-row"><label>圆的数量 (N)</label><span class="val" id="nVal"></span></div>
- <input type="range" id="nSlider" min="1" max="50" value="2">
- </div>
- <div class="group">
- <div class="slider-row"><label>频率倍率 (Rate)</label><span class="val" id="kVal"></span></div>
- <input type="range" id="kSlider" min="-10" max="10" step="0.05" value="0">
- <div style="font-size:10px; color:#666;">负数=反向旋转, 非整数=不闭合</div>
- </div>
- <div class="group">
- <div class="slider-row"><label>半径衰减 (Decay)</label><span class="val" id="rVal"></span></div>
- <input type="range" id="rSlider" min="0.1" max="0.9" step="0.01" value="0.1">
- </div>
-
- <div class="group">
- <div class="slider-row"><label>相位偏移 (Phase Shift)</label><span class="val" id="pVal"></span></div>
- <input type="range" id="pSlider" min="0" max="360" step="1" value="0">
- <div style="font-size:10px; color:#666;">增加此值以扭曲图形</div>
- </div>
- <div class="group">
- <div class="slider-row"><label>画笔粗细</label><span class="val" id="pwVal"></span></div>
- <input type="range" id="pwSlider" min="1" max="30" step="0.5" value="1.5">
- <div style="font-size:10px; color:#666;">改变半径的长度</div>
- </div>
- <h3>渲染设置</h3>
- <div class="group">
- <div class="slider-row"><label>绘制速度 (Speed)</label><span class="val" id="sVal"></span></div>
- <input type="range" id="sSlider" min="1" max="50" step="1" value="5">
- </div>
- <div class="group">
- <div class="slider-row"><label>分辨率 (Resolution)</label><span class="val" id="resVal"></span></div>
- <input type="range" id="resSlider" min="0.001" max="0.05" step="0.001" value="0.005">
- <div style="font-size:10px; color:#666;">越小越平滑 (需降低速度)</div>
- </div>
- <div style="border-top:1px solid #444; padding-top:10px;">
- <label>预设 (Presets):</label><br>
- <button class="preset-btn" onclick="setPreset(1)">经典分形</button>
- <button class="preset-btn" onclick="setPreset(2)">方形填空</button>
- <button class="preset-btn" onclick="setPreset(3)">生物触手</button>
- <button class="preset-btn" onclick="setPreset(4)">量子云</button>
- <button class="preset-btn" onclick="setPreset(5)">三叶结</button>
- <button class="preset-btn" onclick="setPreset(6)">混乱风暴</button>
- </div>
-
- <button onclick="resetPath()">清空画布并重绘</button>
- </div>
- <canvas id="canvas"></canvas>
- <script>
- const canvas = document.getElementById('canvas');
- const ctx = canvas.getContext('2d');
- let width, height;
- // 状态变量
- let time = 0;
- let path = [];
-
- // 参数对象
- const params = {
- N: 2,
- k: -2, // 频率系数
- decay: 0.10, // 半径衰减
- penWidth: 1.5, // 半径长度
- phaseStep: 0, // 相位偏移 (度)
- speed: 5, // 每一帧计算多少次 (绘制速度)
- resolution: 0.005, // 时间步长
- mode: 'power' // 频率模式
- };
- // 绑定UI元素
- const ui = {
- N: document.getElementById('nSlider'),
- k: document.getElementById('kSlider'),
- decay: document.getElementById('rSlider'),
- penWidth: document.getElementById('pwSlider'),
- phase: document.getElementById('pSlider'),
- speed: document.getElementById('sSlider'),
- res: document.getElementById('resSlider'),
- mode: document.getElementById('freqMode'),
- vals: {
- N: document.getElementById('nVal'),
- k: document.getElementById('kVal'),
- decay: document.getElementById('rVal'),
- penWidth: document.getElementById('pwVal'),
- phase: document.getElementById('pVal'),
- speed: document.getElementById('sVal'),
- res: document.getElementById('resVal')
- }
- };
- function updateParams() {
- params.N = parseInt(ui.N.value);
- params.k = parseFloat(ui.k.value);
- params.decay = parseFloat(ui.decay.value);
- params.penWidth = parseFloat(ui.penWidth.value);
- params.phaseStep = parseFloat(ui.phase.value) * (Math.PI / 180); // 转弧度
- params.speed = parseInt(ui.speed.value);
- params.resolution = parseFloat(ui.res.value);
- params.mode = ui.mode.value;
- // 更新显示数值
- ui.vals.N.innerText = params.N;
- ui.vals.k.innerText = params.k.toFixed(2);
- ui.vals.decay.innerText = params.decay.toFixed(2);
- ui.vals.penWidth.innerText = params.penWidth.toFixed(1);
- ui.vals.phase.innerText = ui.phase.value + "°";
- ui.vals.speed.innerText = params.speed;
- ui.vals.res.innerText = params.resolution;
- }
- // 监听所有输入
- Object.values(ui).forEach(el => {
- if(el.tagName === 'DIV') return; // 跳过显示的div
- if(el.addEventListener) {
- el.addEventListener('input', () => {
- updateParams();
- if(el === ui.speed || el === ui.res) return; // 速度变化不重置
- resetPath();
- });
- }
- });
- function resetPath() {
- path = [];
- time = 0;
- // 稍微预热一下,避免从原点弹射
- // time = -Math.PI;
- }
- function resize() {
- width = canvas.width = window.innerWidth;
- height = canvas.height = window.innerHeight;
- }
- window.addEventListener('resize', resize);
- resize();
- updateParams();
- // 预设功能
- function setPreset(id) {
- switch(id) {
- case 1: // 经典
- ui.mode.value = 'power'; ui.N.value = 12; ui.k.value = -2; ui.decay.value = 0.52; ui.phase.value = 0;ui.penWidth.value = 1.5;
- break;
- case 2: // 方形
- ui.mode.value = 'power'; ui.N.value = 10; ui.k.value = -3; ui.decay.value = 0.45; ui.phase.value = 90;ui.penWidth.value = 1.5;
- break;
- case 3: // 生物
- ui.mode.value = 'power'; ui.N.value = 20; ui.k.value = 1.1; ui.decay.value = 0.88; ui.phase.value = 12;ui.penWidth.value = 1.5;
- break;
- case 4: // 量子云
- ui.mode.value = 'linear'; ui.N.value = 30; ui.k.value = 12; ui.decay.value = 0.85; ui.phase.value = 137;ui.penWidth.value = 1.5;
- break;
- case 5: // 三叶结
- ui.mode.value = 'linear'; ui.N.value = 3; ui.k.value = -2; ui.decay.value = 0.4; ui.phase.value = 0;ui.penWidth.value = 1.5;
- break;
- case 6: // 混乱
- ui.mode.value = 'power'; ui.N.value = 8; ui.k.value = -2.2; ui.decay.value = 0.6; ui.phase.value = 45;ui.penWidth.value = 1.5;
- break;
- }
- updateParams();
- resetPath();
- }
- // 核心渲染循环
- function draw() {
- ctx.fillStyle = '#050505';
- ctx.fillRect(0, 0, width, height);
- let cx = width / 2;
- let cy = height / 2;
- // 1. 批量计算点 (Speed 参数决定一次算多少个点,加快轨迹生成)
- for (let s = 0; s < params.speed; s++) {
- let x = cx;
- let y = cy;
-
- // 初始半径
- let r = Math.min(width, height) / 5;
- for (let i = 0; i < params.N; i++) {
- let freq;
-
- // --- 算法核心差异 ---
- if (params.mode === 'power') {
- // 经典分形: freq = k^i
- freq = Math.pow(params.k, i);
- } else if (params.mode === 'linear') {
- // 万花尺模式: freq = k * (i+1)
- freq = params.k * (i + 1);
- } else {
- // 伪质数/混乱模式
- freq = (i % 2 === 0 ? 1 : -1) * (i * i + 1) * params.k * 0.1;
- }
- // 相位:每个圆比上一个多转 phaseStep
- let currentPhase = i * params.phaseStep;
- x += r * Math.cos(time * freq + currentPhase);
- y += r * Math.sin(time * freq + currentPhase);
- r *= params.decay;
- }
- path.push({x, y, h: (time * 40) % 360});
-
- // 限制轨迹长度 (防止内存爆炸,但要足够长以看到复杂图案)
- if (path.length > 15000) path.shift();
- time += params.resolution;
- }
- // 2. 绘制“机械臂” (只绘制当前时刻的结构,为了看清原理)
- // 我们需要重新计算一次当前时刻的各级圆心位置
- let armX = cx, armY = cy;
- let armR = Math.min(width, height) / 5;
-
- ctx.lineWidth = 1;
- for (let i = 0; i < params.N; i++) {
- let prevX = armX;
- let prevY = armY;
- let freq;
- if (params.mode === 'power') freq = Math.pow(params.k, i);
- else if (params.mode === 'linear') freq = params.k * (i + 1);
- else freq = (i % 2 === 0 ? 1 : -1) * (i * i + 1) * params.k * 0.1;
-
- let currentPhase = i * params.phaseStep;
- armX += armR * Math.cos(time * freq + currentPhase);
- armY += armR * Math.sin(time * freq + currentPhase);
- // 画圆
- ctx.strokeStyle = `rgba(255, 255, 255, 0.15)`;
- ctx.beginPath();
- ctx.arc(prevX, prevY, armR, 0, Math.PI * 2);
- ctx.stroke();
- // 画臂
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
- ctx.beginPath();
- ctx.moveTo(prevX, prevY);
- ctx.lineTo(armX, armY);
- ctx.stroke();
-
- // 关节
- ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
- ctx.beginPath();
- ctx.arc(armX, armY, 2, 0, Math.PI*2);
- ctx.fill();
- armR *= params.decay;
- }
- // 3. 绘制轨迹
- if (path.length > 1) {
- ctx.lineWidth = params.penWidth;
- // 使用 globalCompositeOperation 实现一种发光叠加效果
- ctx.globalCompositeOperation = 'screen';
-
- // 为了性能,分段绘制
- ctx.beginPath();
- for (let i = 0; i < path.length - 1; i++) {
- // 简单的性能优化:只有颜色变化大时才断开 stroke,
- // 或者为了完美彩虹效果,我们每段都 stroke (较慢但好看)
- // 这里采用折中:每 10 个点换一次颜色
- if (i === 0) ctx.moveTo(path[i].x, path[i].y);
-
- ctx.lineTo(path[i+1].x, path[i+1].y);
-
- if (i % 20 === 0 || i === path.length - 2) {
- ctx.strokeStyle = `hsla(${path[i].h}, 80%, 60%, 0.8)`;
- ctx.stroke();
- ctx.beginPath();
- ctx.moveTo(path[i+1].x, path[i+1].y);
- }
- }
- ctx.globalCompositeOperation = 'source-over';
- }
- requestAnimationFrame(draw);
- }
- draw();
- </script>
- </body>
- </html>
|