鱼C论坛

 找回密码
 立即注册
查看: 935|回复: 64

[技术交流] 二次函数可视化

[复制链接]
发表于 2025-8-14 18:44:02 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
二次函数可视化

二次函数.png

交互式的二次函数 y=ax^2+bx+c 可视化工具。可以在右侧面板调节我们 a、b、c(范围 ±10)
画布中央的坐标系会实时显示对应抛物线的变化。


二次函数.gif

特点:
还有一个下载的按钮,可以用png的形式记录下当下的函数图像。


  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" />
  6.   <title>二次函数可视化</title>
  7.   <style>
  8.     :root{
  9.       --bg:#eaf6f8;
  10.       --panel:#ffffff;
  11.       --ink:#223;
  12.       --muted:#5b6573;
  13.       --grid:#dfe7ec;
  14.       --axes:#3b4856;
  15.       --accent:#cd671f;   /* 曲线颜色 */
  16.       --axisAccent:#b046ff; /* 对称轴 */
  17.       --vertex:#7a2fff;   /* 顶点 */
  18.       --root:#1f7ae0;     /* 根 */
  19.       --shadow: 0 6px 18px rgba(0,0,0,.08);
  20.       --radius:12px;
  21.     }
  22.     *{box-sizing:border-box}
  23.     html,body{height:100%}
  24.     body{
  25.       margin:0;
  26.       font: 14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, "PingFang SC", "Microsoft YaHei", "Noto Sans", Arial, sans-serif;
  27.       color:var(--ink);
  28.       background:var(--bg);
  29.     }
  30.     .app{
  31.       min-height:100vh;
  32.       padding:16px;
  33.       display:flex;
  34.       gap:18px;
  35.       align-items:stretch;
  36.       justify-content:center;
  37.     }
  38.     /* 左侧:画布区域 */
  39.     .canvas-col{
  40.       flex:1 1 640px;
  41.       display:flex;
  42.       align-items:center;
  43.       justify-content:center;
  44.     }
  45.     .canvas-card{
  46.       width:100%;
  47.       max-width:900px;
  48.       background:var(--panel);
  49.       border-radius:var(--radius);
  50.       box-shadow:var(--shadow);
  51.       padding:12px;
  52.     }
  53.     /* 让画布保持 4:3 比例且自适应 */
  54.     .canvas-wrap{
  55.       position:relative;
  56.       width:100%;
  57.       aspect-ratio: 4/3;
  58.       min-height: 300px;
  59.     }
  60.     canvas#graph{
  61.       position:absolute;
  62.       inset:0;
  63.       width:100%;
  64.       height:100%;
  65.       display:block;
  66.       border-radius:8px;
  67.       background:#fff;
  68.     }

  69.     /* 右侧:控制面板 */
  70.     .panel{
  71.       flex:0 0 340px;
  72.       background:var(--panel);
  73.       border-radius:var(--radius);
  74.       box-shadow:var(--shadow);
  75.       padding:16px;
  76.       display:flex;
  77.       flex-direction:column;
  78.       gap:14px;
  79.       max-height:calc(100vh - 32px);
  80.       overflow:auto;
  81.     }
  82.     .title{
  83.       border:1px solid #e8eef3;
  84.       border-radius:10px;
  85.       padding:12px;
  86.       background:#fbfdff;
  87.     }
  88.     .title .formula{
  89.       font-size:16px;
  90.       font-weight:600;
  91.       letter-spacing:.2px;
  92.     }
  93.     .title .live{
  94.       margin-top:6px;
  95.       font-size:18px;
  96.       font-weight:700;
  97.     }
  98.     .live .code{
  99.       display:inline-block;
  100.       padding:2px 6px;
  101.       border-radius:8px;
  102.       background:#f2f6ff;
  103.     }

  104.     .controls{
  105.       display:grid;
  106.       grid-template-columns: 1fr;
  107.       gap:10px;
  108.     }
  109.     .row{
  110.       display:grid;
  111.       grid-template-columns: 56px 1fr 90px;
  112.       gap:10px;
  113.       align-items:center;
  114.     }
  115.     .row.wide{
  116.       grid-template-columns: 110px 1fr 90px;
  117.     }
  118.     label.key{
  119.       color:var(--muted);
  120.       font-weight:600;
  121.     }
  122.     input[type="range"]{
  123.       width:100%;
  124.       appearance:none;
  125.       height:6px;
  126.       border-radius:999px;
  127.       background:#e8eef3;
  128.       outline:none;
  129.     }
  130.     input[type="range"]::-webkit-slider-thumb{
  131.       -webkit-appearance:none;
  132.       appearance:none;
  133.       width:18px;height:18px;border-radius:50%;
  134.       background:var(--accent);
  135.       border:none;
  136.       box-shadow:0 2px 6px rgba(0,0,0,.2);
  137.       cursor:pointer;
  138.     }
  139.     input[type="number"]{
  140.       width:100%;
  141.       padding:6px 8px;
  142.       border:1px solid #dde4ea;
  143.       border-radius:8px;
  144.       outline:none;
  145.     }
  146.     input[type="number"]:focus{
  147.       border-color:#86b7fe;
  148.       box-shadow:0 0 0 3px rgba(13,110,253,.15);
  149.     }

  150.     .options{
  151.       border:1px dashed #d7dee6;
  152.       border-radius:10px;
  153.       padding:10px;
  154.       display:grid;
  155.       gap:8px;
  156.       background:#fafcff;
  157.     }
  158.     .options label{
  159.       display:flex;
  160.       align-items:center;
  161.       gap:10px;
  162.       font-weight:600;
  163.     }

  164.     .info{
  165.       border-radius:10px;
  166.       background:#f7fafc;
  167.       padding:10px;
  168.       color:#334;
  169.       line-height:1.6;
  170.     }
  171.     .note{
  172.       color:#a23b2a;
  173.       font-weight:600;
  174.     }

  175.     .buttons{
  176.       display:flex;
  177.       gap:10px;
  178.     }
  179.     button{
  180.       padding:8px 12px;
  181.       border-radius:10px;
  182.       border:1px solid #d6dde4;
  183.       background:#fff;
  184.       cursor:pointer;
  185.     }
  186.     button.primary{
  187.       background:linear-gradient(180deg, #ffb26b, #ff7a18);
  188.       color:#fff;border:none;
  189.     }
  190.     button:hover{filter:brightness(.98)}

  191.     /* 移动端布局:上下排列 */
  192.     @media (max-width: 980px){
  193.       .app{flex-direction:column}
  194.       .panel{max-height:none}
  195.       .canvas-card{max-width:100%}
  196.     }
  197.   </style>
  198. </head>
  199. <body>
  200.   <div class="app">
  201.     <main class="canvas-col">
  202.       <div class="canvas-card">
  203.         <div class="canvas-wrap">
  204.           <canvas id="graph" aria-label="二次函数图像画布"></canvas>
  205.         </div>
  206.       </div>
  207.     </main>

  208.     <aside class="panel">
  209.       <div class="title">
  210.         <div class="formula">y = a·x&#178; + b·x + c</div>
  211.         <div class="live">
  212.           <span class="code">
  213.             y = <b id="aLabel">1.00</b>·x&#178; + <b id="bLabel">0.00</b>·x + <b id="cLabel">0.00</b>
  214.           </span>
  215.         </div>
  216.       </div>

  217.       <div class="controls">
  218.         <div class="row">
  219.           <label class="key" for="a">a</label>
  220.           <input id="a" type="range" min="-10" max="10" step="0.1" value="1">
  221.           <input id="aNum" type="number" min="-10" max="10" step="0.1" value="1">
  222.         </div>
  223.         <div class="row">
  224.           <label class="key" for="b">b</label>
  225.           <input id="b" type="range" min="-10" max="10" step="0.1" value="0">
  226.           <input id="bNum" type="number" min="-10" max="10" step="0.1" value="0">
  227.         </div>
  228.         <div class="row">
  229.           <label class="key" for="c">c</label>
  230.           <input id="c" type="range" min="-10" max="10" step="0.1" value="0">
  231.           <input id="cNum" type="number" min="-10" max="10" step="0.1" value="0">
  232.         </div>

  233.         <div class="row wide">
  234.           <label class="key" for="xRange">范围 |x| ≤ <span id="rangeLabel">10</span></label>
  235.           <input id="xRange" type="range" min="5" max="20" step="1" value="10">
  236.           <input id="xRangeNum" type="number" min="5" max="20" step="1" value="10">
  237.         </div>
  238.       </div>

  239.       <div class="options">
  240.         <label><input type="checkbox" id="showVertex"> 顶点</label>
  241.         <label><input type="checkbox" id="showAxis"> 对称轴</label>
  242.         <label><input type="checkbox" id="showRoots"> 根</label>
  243.         <label><input type="checkbox" id="showGrid" checked> 坐标(网格与刻度)</label>
  244.       </div>

  245.       <div class="info">
  246.         <div>顶点 V = (<span id="vx">—</span>, <span id="vy">—</span>)</div>
  247.         <div>对称轴 x = <span id="axis">—</span></div>
  248.         <div>根:<span id="roots">—</span></div>
  249.         <div class="note" id="note"></div>
  250.       </div>

  251.       <div class="buttons">
  252.         <button id="reset">重置</button>
  253.         <button id="save" class="primary">下载 PNG</button>
  254.       </div>
  255.     </aside>
  256.   </div>

  257.   <script>
  258.   (() => {
  259.     // 元素引用
  260.     const canvas = document.getElementById('graph');
  261.     const ctx = canvas.getContext('2d');

  262.     const aRange = document.getElementById('a');
  263.     const bRange = document.getElementById('b');
  264.     const cRange = document.getElementById('c');
  265.     const aNum = document.getElementById('aNum');
  266.     const bNum = document.getElementById('bNum');
  267.     const cNum = document.getElementById('cNum');

  268.     const aLabel = document.getElementById('aLabel');
  269.     const bLabel = document.getElementById('bLabel');
  270.     const cLabel = document.getElementById('cLabel');

  271.     const xRange = document.getElementById('xRange');
  272.     const xRangeNum = document.getElementById('xRangeNum');
  273.     const rangeLabel = document.getElementById('rangeLabel');

  274.     const chkVertex = document.getElementById('showVertex');
  275.     const chkAxis = document.getElementById('showAxis');
  276.     const chkRoots = document.getElementById('showRoots');
  277.     const chkGrid = document.getElementById('showGrid');

  278.     const vxEl = document.getElementById('vx');
  279.     const vyEl = document.getElementById('vy');
  280.     const axisEl = document.getElementById('axis');
  281.     const rootsEl = document.getElementById('roots');
  282.     const noteEl = document.getElementById('note');

  283.     const btnReset = document.getElementById('reset');
  284.     const btnSave = document.getElementById('save');

  285.     // 状态
  286.     const state = {
  287.       a: 1, b: 0, c: 0,
  288.       xMax: 10,
  289.       showVertex: false,
  290.       showAxis: false,
  291.       showRoots: false,
  292.       showGrid: true,
  293.     };

  294.     // 工具函数(两位小数,避免 -0.00)
  295.     const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
  296.     const fmt = (v) => {
  297.       if (!isFinite(v)) return '—';
  298.       const nearZero = Math.abs(v) < 5e-3 ? 0 : v; // 小于 0.005 视为 0
  299.       return Number(nearZero).toFixed(2);
  300.     };

  301.     function updateLabels(){
  302.       aLabel.textContent = fmt(state.a);
  303.       bLabel.textContent = fmt(state.b);
  304.       cLabel.textContent = fmt(state.c);

  305.       rangeLabel.textContent = state.xMax;

  306.       // 顶点/对称轴/根 数值
  307.       const info = computeInfo();
  308.       if (info.isQuadratic){
  309.         vxEl.textContent = fmt(info.vx);
  310.         vyEl.textContent = fmt(info.vy);
  311.         axisEl.textContent = fmt(info.vx);
  312.         if (info.roots.length === 0) {
  313.           rootsEl.textContent = '无实根';
  314.         } else if (info.roots.length === 1) {
  315.           rootsEl.textContent = `x = ${fmt(info.roots[0])}`;
  316.         } else {
  317.           rootsEl.textContent = `x&#8321; = ${fmt(info.roots[0])}, x&#8322; = ${fmt(info.roots[1])}`;
  318.         }
  319.         noteEl.textContent = '';
  320.       } else {
  321.         // a = 0 时为一次函数或常数
  322.         vxEl.textContent = '—';
  323.         vyEl.textContent = '—';
  324.         axisEl.textContent = '—';
  325.         if (Math.abs(state.b) > 1e-9){
  326.           rootsEl.textContent = `线性:x = ${fmt(-state.c/state.b)}`;
  327.         } else {
  328.           rootsEl.textContent = (Math.abs(state.c) < 1e-9) ? '恒等式(任意 x)' : '无解';
  329.         }
  330.         noteEl.textContent = '注意:a = 0 时,y = bx + c(不是二次函数)';
  331.       }
  332.     }

  333.     function syncInputsFromState(){
  334.       // 滑块和数字框同步
  335.       aRange.value = state.a; aNum.value = state.a;
  336.       bRange.value = state.b; bNum.value = state.b;
  337.       cRange.value = state.c; cNum.value = state.c;

  338.       xRange.value = state.xMax; xRangeNum.value = state.xMax;

  339.       chkVertex.checked = state.showVertex;
  340.       chkAxis.checked = state.showAxis;
  341.       chkRoots.checked = state.showRoots;
  342.       chkGrid.checked = state.showGrid;

  343.       updateLabels();
  344.     }

  345.     // 计算顶点/根等
  346.     function computeInfo(){
  347.       const {a,b,c} = state;
  348.       if (Math.abs(a) < 1e-12){
  349.         let root = [];
  350.         if (Math.abs(b) > 1e-12){
  351.           root = [-c/b];
  352.         } else {
  353.           root = [];
  354.         }
  355.         return {isQuadratic:false, vx:NaN, vy:NaN, roots:root};
  356.       }
  357.       const vx = -b/(2*a);
  358.       const vy = a*vx*vx + b*vx + c;
  359.       const D = b*b - 4*a*c;
  360.       let roots = [];
  361.       if (D > 1e-12){
  362.         const s = Math.sqrt(D);
  363.         roots = [(-b - s)/(2*a), (-b + s)/(2*a)];
  364.         roots.sort((m,n)=>m-n);
  365.       } else if (Math.abs(D) <= 1e-12){
  366.         roots = [-b/(2*a)];
  367.       }
  368.       return {isQuadratic:true, vx, vy, roots};
  369.     }

  370.     // 坐标系换算与绘图
  371.     let deviceRatio = 1;
  372.     let pxPerUnit = 1;
  373.     let halfW = 0, halfH = 0;

  374.     function resizeCanvas(){
  375.       const rect = canvas.getBoundingClientRect();
  376.       const dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1));
  377.       deviceRatio = dpr;

  378.       canvas.width = Math.floor(rect.width * dpr);
  379.       canvas.height = Math.floor(rect.height * dpr);

  380.       ctx.setTransform(1,0,0,1,0,0);
  381.       ctx.scale(dpr, dpr);

  382.       halfW = rect.width / 2;
  383.       halfH = rect.height / 2;

  384.       pxPerUnit = rect.width / (2 * state.xMax);

  385.       draw();
  386.     }

  387.     function toScreen(x, y){
  388.       return {
  389.         x: halfW + x * pxPerUnit,
  390.         y: halfH - y * pxPerUnit
  391.       };
  392.     }

  393.     function drawGrid(){
  394.       if (!state.showGrid) return;

  395.       ctx.save();
  396.       ctx.lineWidth = 1;
  397.       ctx.strokeStyle = '#eef3f7';

  398.       const xMax = state.xMax;
  399.       const yMax = halfH / pxPerUnit;

  400.       // 细网格(1 单位)
  401.       ctx.beginPath();
  402.       for(let i = Math.ceil(-xMax); i <= Math.floor(xMax); i++){
  403.         const p = toScreen(i, 0).x;
  404.         ctx.moveTo(p, 0);
  405.         ctx.lineTo(p, halfH*2);
  406.       }
  407.       for(let j = Math.ceil(-yMax); j <= Math.floor(yMax); j++){
  408.         const p = toScreen(0, j).y;
  409.         ctx.moveTo(0, p);
  410.         ctx.lineTo(halfW*2, p);
  411.       }
  412.       ctx.stroke();

  413.       // 坐标轴
  414.       ctx.lineWidth = 1.5;
  415.       ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--axes') || '#3b4856';

  416.       // x 轴
  417.       ctx.beginPath();
  418.       ctx.moveTo(0, toScreen(0,0).y);
  419.       ctx.lineTo(halfW*2, toScreen(0,0).y);
  420.       ctx.stroke();

  421.       // y 轴
  422.       ctx.beginPath();
  423.       ctx.moveTo(toScreen(0,0).x, 0);
  424.       ctx.lineTo(toScreen(0,0).x, halfH*2);
  425.       ctx.stroke();

  426.       // 刻度与数字标记(每 1 单位)
  427.       ctx.fillStyle = '#445';
  428.       ctx.font = '12px system-ui, -apple-system, Segoe UI, Roboto, "PingFang SC", "Microsoft YaHei"';
  429.       ctx.textAlign = 'center';
  430.       ctx.textBaseline = 'top';

  431.       // x 轴刻度
  432.       for(let i = Math.ceil(-xMax); i <= Math.floor(xMax); i++){
  433.         const sx = toScreen(i, 0).x;
  434.         const sy = toScreen(0,0).y;
  435.         ctx.beginPath();
  436.         ctx.moveTo(sx, sy - 4);
  437.         ctx.lineTo(sx, sy + 4);
  438.         ctx.strokeStyle = '#8b98a8';
  439.         ctx.lineWidth = 1;
  440.         ctx.stroke();
  441.         if (i !== 0){
  442.           ctx.fillText(i.toString(), sx, sy + 6);
  443.         }
  444.       }

  445.       // y 轴刻度
  446.       ctx.textAlign = 'right';
  447.       ctx.textBaseline = 'middle';
  448.       for(let j = Math.ceil(-yMax); j <= Math.floor(yMax); j++){
  449.         const sy = toScreen(0, j).y;
  450.         const sx = toScreen(0,0).x;
  451.         ctx.beginPath();
  452.         ctx.moveTo(sx - 4, sy);
  453.         ctx.lineTo(sx + 4, sy);
  454.         ctx.strokeStyle = '#8b98a8';
  455.         ctx.lineWidth = 1;
  456.         ctx.stroke();
  457.         if (j !== 0){
  458.           ctx.fillText(j.toString(), sx - 6, sy);
  459.         }
  460.       }

  461.       // 轴箭头与标签
  462.       ctx.fillStyle = '#222';
  463.       // x 箭头
  464.       {
  465.         const y0 = toScreen(0,0).y;
  466.         const xR = halfW*2 - 8;
  467.         ctx.beginPath();
  468.         ctx.moveTo(xR, y0);
  469.         ctx.lineTo(xR-8, y0-5);
  470.         ctx.lineTo(xR-8, y0+5);
  471.         ctx.closePath();
  472.         ctx.fill();
  473.         ctx.fillText('x', xR-14, y0+10);
  474.       }
  475.       // y 箭头
  476.       {
  477.         const x0 = toScreen(0,0).x;
  478.         const yT = 8;
  479.         ctx.beginPath();
  480.         ctx.moveTo(x0, yT);
  481.         ctx.lineTo(x0-5, yT+8);
  482.         ctx.lineTo(x0+5, yT+8);
  483.         ctx.closePath();
  484.         ctx.fill();
  485.         ctx.fillText('y', x0+10, yT+4);
  486.       }

  487.       ctx.restore();
  488.     }

  489.     function drawParabola(){
  490.       const {a,b,c} = state;

  491.       ctx.save();
  492.       ctx.lineWidth = 3;
  493.       ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--accent') || '#ff7a18';
  494.       ctx.beginPath();

  495.       const xMax = state.xMax;
  496.       const stepWorld = Math.max(1 / pxPerUnit, 0.01); // 约每像素采样
  497.       let first = true;

  498.       for(let x = -xMax; x <= xMax; x += stepWorld){
  499.         const y = a*x*x + b*x + c;
  500.         const p = toScreen(x, y);
  501.         if (first){
  502.           ctx.moveTo(p.x, p.y);
  503.           first = false;
  504.         } else {
  505.           ctx.lineTo(p.x, p.y);
  506.         }
  507.       }
  508.       // 最右端确保落笔
  509.       {
  510.         const y = a*xMax*xMax + b*xMax + c;
  511.         const p = toScreen(xMax, y);
  512.         ctx.lineTo(p.x, p.y);
  513.       }
  514.       ctx.stroke();
  515.       ctx.restore();
  516.     }

  517.     function drawVertexAndAxis(){
  518.       const info = computeInfo();
  519.       if (!info.isQuadratic) return;

  520.       // 对称轴
  521.       if (state.showAxis){
  522.         const vx = info.vx;
  523.         const top = toScreen(vx,  99999).y;
  524.         const bot = toScreen(vx, -99999).y;

  525.         ctx.save();
  526.         ctx.setLineDash([8, 6]);
  527.         ctx.lineDashOffset = 0;
  528.         ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--axisAccent') || '#b046ff';
  529.         ctx.lineWidth = 2;
  530.         ctx.beginPath();
  531.         ctx.moveTo(toScreen(vx,0).x, top);
  532.         ctx.lineTo(toScreen(vx,0).x, bot);
  533.         ctx.stroke();
  534.         ctx.restore();
  535.       }

  536.       // 顶点
  537.       if (state.showVertex){
  538.         const p = toScreen(info.vx, info.vy);
  539.         ctx.save();
  540.         ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--vertex') || '#7a2fff';
  541.         ctx.strokeStyle = '#fff';
  542.         ctx.lineWidth = 2;
  543.         ctx.beginPath();
  544.         ctx.arc(p.x, p.y, 5, 0, Math.PI*2);
  545.         ctx.fill();
  546.         ctx.stroke();
  547.         // 标签(两位小数)
  548.         ctx.font = '12px system-ui';
  549.         ctx.fillStyle = '#222';
  550.         ctx.textAlign = 'left';
  551.         ctx.textBaseline = 'bottom';
  552.         ctx.fillText(`V(${fmt(info.vx)}, ${fmt(info.vy)})`, p.x + 8, p.y - 6);
  553.         ctx.restore();
  554.       }
  555.     }

  556.     function drawRoots(){
  557.       if (!state.showRoots) return;
  558.       const info = computeInfo();
  559.       const {a,b,c} = state;

  560.       ctx.save();
  561.       ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--root') || '#1f7ae0';
  562.       ctx.strokeStyle = '#fff';
  563.       ctx.lineWidth = 2;

  564.       if (info.isQuadratic){
  565.         if (info.roots.length > 0){
  566.           for(const r of info.roots){
  567.             const p = toScreen(r, 0);
  568.             ctx.beginPath();
  569.             ctx.arc(p.x, p.y, 5, 0, Math.PI*2);
  570.             ctx.fill(); ctx.stroke();
  571.           }
  572.         }
  573.       } else {
  574.         // a=0 的一次函数根(若存在)
  575.         if (Math.abs(b) > 1e-9){
  576.           const r = -c/b;
  577.           const p = toScreen(r, 0);
  578.           ctx.beginPath();
  579.           ctx.arc(p.x, p.y, 5, 0, Math.PI*2);
  580.           ctx.fill(); ctx.stroke();
  581.         }
  582.       }
  583.       ctx.restore();
  584.     }

  585.     function clear(){
  586.       const rect = canvas.getBoundingClientRect();
  587.       ctx.clearRect(0,0,rect.width,rect.height);
  588.       // 白底(避免导出透明)
  589.       ctx.save();
  590.       ctx.fillStyle = '#ffffff';
  591.       ctx.fillRect(0,0,rect.width,rect.height);
  592.       ctx.restore();
  593.     }

  594.     function draw(){
  595.       clear();
  596.       drawGrid();
  597.       drawParabola();
  598.       drawVertexAndAxis();
  599.       drawRoots();
  600.       updateLabels();
  601.     }

  602.     // 事件绑定
  603.     function bindTwoWay(rangeEl, numEl, onChange){
  604.       const apply = (v) => {
  605.         if (numEl.min !== '') v = clamp(+v, +numEl.min, +numEl.max);
  606.         numEl.value = v;
  607.         rangeEl.value = v;
  608.         onChange(+v);
  609.       };
  610.       rangeEl.addEventListener('input', e => apply(e.target.value));
  611.       numEl.addEventListener('input', e => apply(e.target.value));
  612.       numEl.addEventListener('change', e => apply(e.target.value));
  613.     }

  614.     bindTwoWay(aRange, aNum, v => { state.a = +v; draw(); });
  615.     bindTwoWay(bRange, bNum, v => { state.b = +v; draw(); });
  616.     bindTwoWay(cRange, cNum, v => { state.c = +v; draw(); });

  617.     bindTwoWay(xRange, xRangeNum, v => {
  618.       state.xMax = Math.round(+v);
  619.       resizeCanvas();
  620.       syncInputsFromState();
  621.       draw();
  622.     });

  623.     // 选项
  624.     chkVertex.addEventListener('change', e => { state.showVertex = e.target.checked; draw(); });
  625.     chkAxis.addEventListener('change', e => { state.showAxis = e.target.checked; draw(); });
  626.     chkRoots.addEventListener('change', e => { state.showRoots = e.target.checked; draw(); });
  627.     chkGrid.addEventListener('change', e => { state.showGrid = e.target.checked; draw(); });

  628.     // 重置
  629.     btnReset.addEventListener('click', () => {
  630.       state.a = 1; state.b = 0; state.c = 0;
  631.       state.xMax = 10;
  632.       state.showVertex = false;
  633.       state.showAxis = false;
  634.       state.showRoots = false;
  635.       state.showGrid = true;
  636.       syncInputsFromState();
  637.       resizeCanvas();
  638.     });

  639.     // 保存 PNG
  640.     btnSave.addEventListener('click', () => {
  641.       draw();
  642.       const link = document.createElement('a');
  643.       link.download = `quadratic_a${fmt(state.a)}_b${fmt(state.b)}_c${fmt(state.c)}.png`;
  644.       link.href = canvas.toDataURL('image/png');
  645.       link.click();
  646.     });

  647.     // 自适应
  648.     window.addEventListener('resize', resizeCanvas, {passive:true});
  649.     // 初始化
  650.     syncInputsFromState();
  651.     resizeCanvas();
  652.   })();
  653.   </script>
  654. </body>
  655. </html>
复制代码



评分

参与人数 3荣誉 +7 鱼币 +9 贡献 +9 C币 +6 收起 理由
不二如是 + 2 + 3 + 3 + 3 鱼C有你更精彩^_^
小甲鱼的二师兄 + 3 + 3 + 3 鱼C有你更精彩^_^
小甲鱼 + 2 + 3 + 3 + 3 鱼C有你更精彩^_^

查看全部评分

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2025-8-14 20:59:15 From FishC Mobile | 显示全部楼层
收藏一下
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 20:59:42 From FishC Mobile | 显示全部楼层
学到了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 20:59:57 From FishC Mobile | 显示全部楼层
这个是不是可以给中学生上课用了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:00:08 | 显示全部楼层
学习了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:00:09 | 显示全部楼层
很好,寡人又学到知识了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:00:43 | 显示全部楼层
向大佬学习
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:00:44 From FishC Mobile | 显示全部楼层
学到了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:01:44 | 显示全部楼层
给娃搞一个
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:01:50 | 显示全部楼层
这是不是能给初中生可视化讲几何题了:)
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:02:02 | 显示全部楼层

这个完全可以用于中学生上课用,楼主厉害了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:02:39 From FishC Mobile | 显示全部楼层
哇!码住
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:04:44 From FishC Mobile | 显示全部楼层
太厉害了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:05:16 | 显示全部楼层
学到了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:10:45 From FishC Mobile | 显示全部楼层
这么多,
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:13:02 From FishC Mobile | 显示全部楼层
太厉害了吧,码住码住
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:13:03 From FishC Mobile | 显示全部楼层
学习了学习了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:17:54 From FishC Mobile | 显示全部楼层
学习了
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:18:38 From FishC Mobile | 显示全部楼层
我来打卡学习啦
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2025-8-14 21:18:50 From FishC Mobile | 显示全部楼层
要是我们以前有这东西,数学能不好吗
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2025-9-9 22:16

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表