2.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>高阶傅里叶分形实验室</title>
  7. <style>
  8. body {
  9. margin: 0;
  10. overflow: hidden;
  11. background-color: #050505;
  12. font-family: 'Segoe UI', sans-serif;
  13. color: #ddd;
  14. }
  15. canvas { display: block; }
  16. /* 控制面板样式 */
  17. #controls {
  18. position: absolute;
  19. top: 10px;
  20. left: 10px;
  21. width: 280px;
  22. max-height: 95vh;
  23. overflow-y: auto;
  24. background: rgba(20, 20, 30, 0.85);
  25. backdrop-filter: blur(10px);
  26. padding: 20px;
  27. border-radius: 12px;
  28. border: 1px solid #444;
  29. box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  30. transition: opacity 0.3s;
  31. }
  32. #controls:hover { opacity: 1; }
  33. h3 { margin: 0 0 15px 0; font-size: 16px; color: #fff; border-bottom: 1px solid #555; padding-bottom: 10px; }
  34. .group { margin-bottom: 15px; }
  35. .slider-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; }
  36. label { font-size: 12px; color: #aaa; }
  37. span.val { font-size: 12px; font-weight: bold; color: #4facfe; width: 40px; text-align: right;}
  38. input[type=range] { width: 100%; margin: 5px 0; cursor: pointer; accent-color: #4facfe; }
  39. select { width: 100%; background: #333; color: white; border: 1px solid #555; padding: 5px; border-radius: 4px; }
  40. button {
  41. width: 100%; padding: 8px; margin-top: 10px;
  42. background: linear-gradient(45deg, #4facfe, #00f2fe);
  43. border: none; border-radius: 6px; color: #000; font-weight: bold; cursor: pointer;
  44. }
  45. button:hover { filter: brightness(1.1); }
  46. .preset-btn {
  47. width: 30%; font-size: 10px; padding: 5px; margin: 2px 0; display: inline-block;
  48. background: #333; color: white; border: 1px solid #555;
  49. }
  50. /* 隐藏滚动条但保留功能 */
  51. ::-webkit-scrollbar { width: 6px; }
  52. ::-webkit-scrollbar-track { background: #1a1a1a; }
  53. ::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
  54. </style>
  55. </head>
  56. <body>
  57. <div id="controls">
  58. <h3>分形参数核心</h3>
  59. <div class="group">
  60. <label>频率模式 (Frequency Mode)</label>
  61. <select id="freqMode">
  62. <option value="power">指数 (分形/Fractal)</option>
  63. <option value="linear">线性 (万花尺/Spiro)</option>
  64. <option value="prime">质数 (混沌/Chaos)</option>
  65. </select>
  66. </div>
  67. <div class="group">
  68. <div class="slider-row"><label>圆的数量 (N)</label><span class="val" id="nVal"></span></div>
  69. <input type="range" id="nSlider" min="1" max="50" value="2">
  70. </div>
  71. <div class="group">
  72. <div class="slider-row"><label>频率倍率 (Rate)</label><span class="val" id="kVal"></span></div>
  73. <input type="range" id="kSlider" min="-10" max="10" step="0.05" value="0">
  74. <div style="font-size:10px; color:#666;">负数=反向旋转, 非整数=不闭合</div>
  75. </div>
  76. <div class="group">
  77. <div class="slider-row"><label>半径衰减 (Decay)</label><span class="val" id="rVal"></span></div>
  78. <input type="range" id="rSlider" min="0.1" max="0.9" step="0.01" value="0.1">
  79. </div>
  80. <div class="group">
  81. <div class="slider-row"><label>相位偏移 (Phase Shift)</label><span class="val" id="pVal"></span></div>
  82. <input type="range" id="pSlider" min="0" max="360" step="1" value="0">
  83. <div style="font-size:10px; color:#666;">增加此值以扭曲图形</div>
  84. </div>
  85. <div class="group">
  86. <div class="slider-row"><label>画笔粗细</label><span class="val" id="pwVal"></span></div>
  87. <input type="range" id="pwSlider" min="1" max="30" step="0.5" value="1.5">
  88. <div style="font-size:10px; color:#666;">改变半径的长度</div>
  89. </div>
  90. <h3>渲染设置</h3>
  91. <div class="group">
  92. <div class="slider-row"><label>绘制速度 (Speed)</label><span class="val" id="sVal"></span></div>
  93. <input type="range" id="sSlider" min="1" max="50" step="1" value="5">
  94. </div>
  95. <div class="group">
  96. <div class="slider-row"><label>分辨率 (Resolution)</label><span class="val" id="resVal"></span></div>
  97. <input type="range" id="resSlider" min="0.001" max="0.05" step="0.001" value="0.005">
  98. <div style="font-size:10px; color:#666;">越小越平滑 (需降低速度)</div>
  99. </div>
  100. <div style="border-top:1px solid #444; padding-top:10px;">
  101. <label>预设 (Presets):</label><br>
  102. <button class="preset-btn" onclick="setPreset(1)">经典分形</button>
  103. <button class="preset-btn" onclick="setPreset(2)">方形填空</button>
  104. <button class="preset-btn" onclick="setPreset(3)">生物触手</button>
  105. <button class="preset-btn" onclick="setPreset(4)">量子云</button>
  106. <button class="preset-btn" onclick="setPreset(5)">三叶结</button>
  107. <button class="preset-btn" onclick="setPreset(6)">混乱风暴</button>
  108. </div>
  109. <button onclick="resetPath()">清空画布并重绘</button>
  110. </div>
  111. <canvas id="canvas"></canvas>
  112. <script>
  113. const canvas = document.getElementById('canvas');
  114. const ctx = canvas.getContext('2d');
  115. let width, height;
  116. // 状态变量
  117. let time = 0;
  118. let path = [];
  119. // 参数对象
  120. const params = {
  121. N: 2,
  122. k: -2, // 频率系数
  123. decay: 0.10, // 半径衰减
  124. penWidth: 1.5, // 半径长度
  125. phaseStep: 0, // 相位偏移 (度)
  126. speed: 5, // 每一帧计算多少次 (绘制速度)
  127. resolution: 0.005, // 时间步长
  128. mode: 'power' // 频率模式
  129. };
  130. // 绑定UI元素
  131. const ui = {
  132. N: document.getElementById('nSlider'),
  133. k: document.getElementById('kSlider'),
  134. decay: document.getElementById('rSlider'),
  135. penWidth: document.getElementById('pwSlider'),
  136. phase: document.getElementById('pSlider'),
  137. speed: document.getElementById('sSlider'),
  138. res: document.getElementById('resSlider'),
  139. mode: document.getElementById('freqMode'),
  140. vals: {
  141. N: document.getElementById('nVal'),
  142. k: document.getElementById('kVal'),
  143. decay: document.getElementById('rVal'),
  144. penWidth: document.getElementById('pwVal'),
  145. phase: document.getElementById('pVal'),
  146. speed: document.getElementById('sVal'),
  147. res: document.getElementById('resVal')
  148. }
  149. };
  150. function updateParams() {
  151. params.N = parseInt(ui.N.value);
  152. params.k = parseFloat(ui.k.value);
  153. params.decay = parseFloat(ui.decay.value);
  154. params.penWidth = parseFloat(ui.penWidth.value);
  155. params.phaseStep = parseFloat(ui.phase.value) * (Math.PI / 180); // 转弧度
  156. params.speed = parseInt(ui.speed.value);
  157. params.resolution = parseFloat(ui.res.value);
  158. params.mode = ui.mode.value;
  159. // 更新显示数值
  160. ui.vals.N.innerText = params.N;
  161. ui.vals.k.innerText = params.k.toFixed(2);
  162. ui.vals.decay.innerText = params.decay.toFixed(2);
  163. ui.vals.penWidth.innerText = params.penWidth.toFixed(1);
  164. ui.vals.phase.innerText = ui.phase.value + "°";
  165. ui.vals.speed.innerText = params.speed;
  166. ui.vals.res.innerText = params.resolution;
  167. }
  168. // 监听所有输入
  169. Object.values(ui).forEach(el => {
  170. if(el.tagName === 'DIV') return; // 跳过显示的div
  171. if(el.addEventListener) {
  172. el.addEventListener('input', () => {
  173. updateParams();
  174. if(el === ui.speed || el === ui.res) return; // 速度变化不重置
  175. resetPath();
  176. });
  177. }
  178. });
  179. function resetPath() {
  180. path = [];
  181. time = 0;
  182. // 稍微预热一下,避免从原点弹射
  183. // time = -Math.PI;
  184. }
  185. function resize() {
  186. width = canvas.width = window.innerWidth;
  187. height = canvas.height = window.innerHeight;
  188. }
  189. window.addEventListener('resize', resize);
  190. resize();
  191. updateParams();
  192. // 预设功能
  193. function setPreset(id) {
  194. switch(id) {
  195. case 1: // 经典
  196. 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;
  197. break;
  198. case 2: // 方形
  199. 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;
  200. break;
  201. case 3: // 生物
  202. 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;
  203. break;
  204. case 4: // 量子云
  205. 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;
  206. break;
  207. case 5: // 三叶结
  208. 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;
  209. break;
  210. case 6: // 混乱
  211. 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;
  212. break;
  213. }
  214. updateParams();
  215. resetPath();
  216. }
  217. // 核心渲染循环
  218. function draw() {
  219. ctx.fillStyle = '#050505';
  220. ctx.fillRect(0, 0, width, height);
  221. let cx = width / 2;
  222. let cy = height / 2;
  223. // 1. 批量计算点 (Speed 参数决定一次算多少个点,加快轨迹生成)
  224. for (let s = 0; s < params.speed; s++) {
  225. let x = cx;
  226. let y = cy;
  227. // 初始半径
  228. let r = Math.min(width, height) / 5;
  229. for (let i = 0; i < params.N; i++) {
  230. let freq;
  231. // --- 算法核心差异 ---
  232. if (params.mode === 'power') {
  233. // 经典分形: freq = k^i
  234. freq = Math.pow(params.k, i);
  235. } else if (params.mode === 'linear') {
  236. // 万花尺模式: freq = k * (i+1)
  237. freq = params.k * (i + 1);
  238. } else {
  239. // 伪质数/混乱模式
  240. freq = (i % 2 === 0 ? 1 : -1) * (i * i + 1) * params.k * 0.1;
  241. }
  242. // 相位:每个圆比上一个多转 phaseStep
  243. let currentPhase = i * params.phaseStep;
  244. x += r * Math.cos(time * freq + currentPhase);
  245. y += r * Math.sin(time * freq + currentPhase);
  246. r *= params.decay;
  247. }
  248. path.push({x, y, h: (time * 40) % 360});
  249. // 限制轨迹长度 (防止内存爆炸,但要足够长以看到复杂图案)
  250. if (path.length > 15000) path.shift();
  251. time += params.resolution;
  252. }
  253. // 2. 绘制“机械臂” (只绘制当前时刻的结构,为了看清原理)
  254. // 我们需要重新计算一次当前时刻的各级圆心位置
  255. let armX = cx, armY = cy;
  256. let armR = Math.min(width, height) / 5;
  257. ctx.lineWidth = 1;
  258. for (let i = 0; i < params.N; i++) {
  259. let prevX = armX;
  260. let prevY = armY;
  261. let freq;
  262. if (params.mode === 'power') freq = Math.pow(params.k, i);
  263. else if (params.mode === 'linear') freq = params.k * (i + 1);
  264. else freq = (i % 2 === 0 ? 1 : -1) * (i * i + 1) * params.k * 0.1;
  265. let currentPhase = i * params.phaseStep;
  266. armX += armR * Math.cos(time * freq + currentPhase);
  267. armY += armR * Math.sin(time * freq + currentPhase);
  268. // 画圆
  269. ctx.strokeStyle = `rgba(255, 255, 255, 0.15)`;
  270. ctx.beginPath();
  271. ctx.arc(prevX, prevY, armR, 0, Math.PI * 2);
  272. ctx.stroke();
  273. // 画臂
  274. ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
  275. ctx.beginPath();
  276. ctx.moveTo(prevX, prevY);
  277. ctx.lineTo(armX, armY);
  278. ctx.stroke();
  279. // 关节
  280. ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
  281. ctx.beginPath();
  282. ctx.arc(armX, armY, 2, 0, Math.PI*2);
  283. ctx.fill();
  284. armR *= params.decay;
  285. }
  286. // 3. 绘制轨迹
  287. if (path.length > 1) {
  288. ctx.lineWidth = params.penWidth;
  289. // 使用 globalCompositeOperation 实现一种发光叠加效果
  290. ctx.globalCompositeOperation = 'screen';
  291. // 为了性能,分段绘制
  292. ctx.beginPath();
  293. for (let i = 0; i < path.length - 1; i++) {
  294. // 简单的性能优化:只有颜色变化大时才断开 stroke,
  295. // 或者为了完美彩虹效果,我们每段都 stroke (较慢但好看)
  296. // 这里采用折中:每 10 个点换一次颜色
  297. if (i === 0) ctx.moveTo(path[i].x, path[i].y);
  298. ctx.lineTo(path[i+1].x, path[i+1].y);
  299. if (i % 20 === 0 || i === path.length - 2) {
  300. ctx.strokeStyle = `hsla(${path[i].h}, 80%, 60%, 0.8)`;
  301. ctx.stroke();
  302. ctx.beginPath();
  303. ctx.moveTo(path[i+1].x, path[i+1].y);
  304. }
  305. }
  306. ctx.globalCompositeOperation = 'source-over';
  307. }
  308. requestAnimationFrame(draw);
  309. }
  310. draw();
  311. </script>
  312. </body>
  313. </html>