|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
二次函数可视化
交互式的二次函数 y=ax^2+bx+c 可视化工具。可以在右侧面板调节我们 a、b、c(范围 ±10)
画布中央的坐标系会实时显示对应抛物线的变化。
特点:
还有一个下载的按钮,可以用png的形式记录下当下的函数图像。
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>二次函数可视化</title>
- <style>
- :root{
- --bg:#eaf6f8;
- --panel:#ffffff;
- --ink:#223;
- --muted:#5b6573;
- --grid:#dfe7ec;
- --axes:#3b4856;
- --accent:#cd671f; /* 曲线颜色 */
- --axisAccent:#b046ff; /* 对称轴 */
- --vertex:#7a2fff; /* 顶点 */
- --root:#1f7ae0; /* 根 */
- --shadow: 0 6px 18px rgba(0,0,0,.08);
- --radius:12px;
- }
- *{box-sizing:border-box}
- html,body{height:100%}
- body{
- margin:0;
- font: 14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, "PingFang SC", "Microsoft YaHei", "Noto Sans", Arial, sans-serif;
- color:var(--ink);
- background:var(--bg);
- }
- .app{
- min-height:100vh;
- padding:16px;
- display:flex;
- gap:18px;
- align-items:stretch;
- justify-content:center;
- }
- /* 左侧:画布区域 */
- .canvas-col{
- flex:1 1 640px;
- display:flex;
- align-items:center;
- justify-content:center;
- }
- .canvas-card{
- width:100%;
- max-width:900px;
- background:var(--panel);
- border-radius:var(--radius);
- box-shadow:var(--shadow);
- padding:12px;
- }
- /* 让画布保持 4:3 比例且自适应 */
- .canvas-wrap{
- position:relative;
- width:100%;
- aspect-ratio: 4/3;
- min-height: 300px;
- }
- canvas#graph{
- position:absolute;
- inset:0;
- width:100%;
- height:100%;
- display:block;
- border-radius:8px;
- background:#fff;
- }
- /* 右侧:控制面板 */
- .panel{
- flex:0 0 340px;
- background:var(--panel);
- border-radius:var(--radius);
- box-shadow:var(--shadow);
- padding:16px;
- display:flex;
- flex-direction:column;
- gap:14px;
- max-height:calc(100vh - 32px);
- overflow:auto;
- }
- .title{
- border:1px solid #e8eef3;
- border-radius:10px;
- padding:12px;
- background:#fbfdff;
- }
- .title .formula{
- font-size:16px;
- font-weight:600;
- letter-spacing:.2px;
- }
- .title .live{
- margin-top:6px;
- font-size:18px;
- font-weight:700;
- }
- .live .code{
- display:inline-block;
- padding:2px 6px;
- border-radius:8px;
- background:#f2f6ff;
- }
- .controls{
- display:grid;
- grid-template-columns: 1fr;
- gap:10px;
- }
- .row{
- display:grid;
- grid-template-columns: 56px 1fr 90px;
- gap:10px;
- align-items:center;
- }
- .row.wide{
- grid-template-columns: 110px 1fr 90px;
- }
- label.key{
- color:var(--muted);
- font-weight:600;
- }
- input[type="range"]{
- width:100%;
- appearance:none;
- height:6px;
- border-radius:999px;
- background:#e8eef3;
- outline:none;
- }
- input[type="range"]::-webkit-slider-thumb{
- -webkit-appearance:none;
- appearance:none;
- width:18px;height:18px;border-radius:50%;
- background:var(--accent);
- border:none;
- box-shadow:0 2px 6px rgba(0,0,0,.2);
- cursor:pointer;
- }
- input[type="number"]{
- width:100%;
- padding:6px 8px;
- border:1px solid #dde4ea;
- border-radius:8px;
- outline:none;
- }
- input[type="number"]:focus{
- border-color:#86b7fe;
- box-shadow:0 0 0 3px rgba(13,110,253,.15);
- }
- .options{
- border:1px dashed #d7dee6;
- border-radius:10px;
- padding:10px;
- display:grid;
- gap:8px;
- background:#fafcff;
- }
- .options label{
- display:flex;
- align-items:center;
- gap:10px;
- font-weight:600;
- }
- .info{
- border-radius:10px;
- background:#f7fafc;
- padding:10px;
- color:#334;
- line-height:1.6;
- }
- .note{
- color:#a23b2a;
- font-weight:600;
- }
- .buttons{
- display:flex;
- gap:10px;
- }
- button{
- padding:8px 12px;
- border-radius:10px;
- border:1px solid #d6dde4;
- background:#fff;
- cursor:pointer;
- }
- button.primary{
- background:linear-gradient(180deg, #ffb26b, #ff7a18);
- color:#fff;border:none;
- }
- button:hover{filter:brightness(.98)}
- /* 移动端布局:上下排列 */
- @media (max-width: 980px){
- .app{flex-direction:column}
- .panel{max-height:none}
- .canvas-card{max-width:100%}
- }
- </style>
- </head>
- <body>
- <div class="app">
- <main class="canvas-col">
- <div class="canvas-card">
- <div class="canvas-wrap">
- <canvas id="graph" aria-label="二次函数图像画布"></canvas>
- </div>
- </div>
- </main>
- <aside class="panel">
- <div class="title">
- <div class="formula">y = a·x² + b·x + c</div>
- <div class="live">
- <span class="code">
- y = <b id="aLabel">1.00</b>·x² + <b id="bLabel">0.00</b>·x + <b id="cLabel">0.00</b>
- </span>
- </div>
- </div>
- <div class="controls">
- <div class="row">
- <label class="key" for="a">a</label>
- <input id="a" type="range" min="-10" max="10" step="0.1" value="1">
- <input id="aNum" type="number" min="-10" max="10" step="0.1" value="1">
- </div>
- <div class="row">
- <label class="key" for="b">b</label>
- <input id="b" type="range" min="-10" max="10" step="0.1" value="0">
- <input id="bNum" type="number" min="-10" max="10" step="0.1" value="0">
- </div>
- <div class="row">
- <label class="key" for="c">c</label>
- <input id="c" type="range" min="-10" max="10" step="0.1" value="0">
- <input id="cNum" type="number" min="-10" max="10" step="0.1" value="0">
- </div>
- <div class="row wide">
- <label class="key" for="xRange">范围 |x| ≤ <span id="rangeLabel">10</span></label>
- <input id="xRange" type="range" min="5" max="20" step="1" value="10">
- <input id="xRangeNum" type="number" min="5" max="20" step="1" value="10">
- </div>
- </div>
- <div class="options">
- <label><input type="checkbox" id="showVertex"> 顶点</label>
- <label><input type="checkbox" id="showAxis"> 对称轴</label>
- <label><input type="checkbox" id="showRoots"> 根</label>
- <label><input type="checkbox" id="showGrid" checked> 坐标(网格与刻度)</label>
- </div>
- <div class="info">
- <div>顶点 V = (<span id="vx">—</span>, <span id="vy">—</span>)</div>
- <div>对称轴 x = <span id="axis">—</span></div>
- <div>根:<span id="roots">—</span></div>
- <div class="note" id="note"></div>
- </div>
- <div class="buttons">
- <button id="reset">重置</button>
- <button id="save" class="primary">下载 PNG</button>
- </div>
- </aside>
- </div>
- <script>
- (() => {
- // 元素引用
- const canvas = document.getElementById('graph');
- const ctx = canvas.getContext('2d');
- const aRange = document.getElementById('a');
- const bRange = document.getElementById('b');
- const cRange = document.getElementById('c');
- const aNum = document.getElementById('aNum');
- const bNum = document.getElementById('bNum');
- const cNum = document.getElementById('cNum');
- const aLabel = document.getElementById('aLabel');
- const bLabel = document.getElementById('bLabel');
- const cLabel = document.getElementById('cLabel');
- const xRange = document.getElementById('xRange');
- const xRangeNum = document.getElementById('xRangeNum');
- const rangeLabel = document.getElementById('rangeLabel');
- const chkVertex = document.getElementById('showVertex');
- const chkAxis = document.getElementById('showAxis');
- const chkRoots = document.getElementById('showRoots');
- const chkGrid = document.getElementById('showGrid');
- const vxEl = document.getElementById('vx');
- const vyEl = document.getElementById('vy');
- const axisEl = document.getElementById('axis');
- const rootsEl = document.getElementById('roots');
- const noteEl = document.getElementById('note');
- const btnReset = document.getElementById('reset');
- const btnSave = document.getElementById('save');
- // 状态
- const state = {
- a: 1, b: 0, c: 0,
- xMax: 10,
- showVertex: false,
- showAxis: false,
- showRoots: false,
- showGrid: true,
- };
- // 工具函数(两位小数,避免 -0.00)
- const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
- const fmt = (v) => {
- if (!isFinite(v)) return '—';
- const nearZero = Math.abs(v) < 5e-3 ? 0 : v; // 小于 0.005 视为 0
- return Number(nearZero).toFixed(2);
- };
- function updateLabels(){
- aLabel.textContent = fmt(state.a);
- bLabel.textContent = fmt(state.b);
- cLabel.textContent = fmt(state.c);
- rangeLabel.textContent = state.xMax;
- // 顶点/对称轴/根 数值
- const info = computeInfo();
- if (info.isQuadratic){
- vxEl.textContent = fmt(info.vx);
- vyEl.textContent = fmt(info.vy);
- axisEl.textContent = fmt(info.vx);
- if (info.roots.length === 0) {
- rootsEl.textContent = '无实根';
- } else if (info.roots.length === 1) {
- rootsEl.textContent = `x = ${fmt(info.roots[0])}`;
- } else {
- rootsEl.textContent = `x₁ = ${fmt(info.roots[0])}, x₂ = ${fmt(info.roots[1])}`;
- }
- noteEl.textContent = '';
- } else {
- // a = 0 时为一次函数或常数
- vxEl.textContent = '—';
- vyEl.textContent = '—';
- axisEl.textContent = '—';
- if (Math.abs(state.b) > 1e-9){
- rootsEl.textContent = `线性:x = ${fmt(-state.c/state.b)}`;
- } else {
- rootsEl.textContent = (Math.abs(state.c) < 1e-9) ? '恒等式(任意 x)' : '无解';
- }
- noteEl.textContent = '注意:a = 0 时,y = bx + c(不是二次函数)';
- }
- }
- function syncInputsFromState(){
- // 滑块和数字框同步
- aRange.value = state.a; aNum.value = state.a;
- bRange.value = state.b; bNum.value = state.b;
- cRange.value = state.c; cNum.value = state.c;
- xRange.value = state.xMax; xRangeNum.value = state.xMax;
- chkVertex.checked = state.showVertex;
- chkAxis.checked = state.showAxis;
- chkRoots.checked = state.showRoots;
- chkGrid.checked = state.showGrid;
- updateLabels();
- }
- // 计算顶点/根等
- function computeInfo(){
- const {a,b,c} = state;
- if (Math.abs(a) < 1e-12){
- let root = [];
- if (Math.abs(b) > 1e-12){
- root = [-c/b];
- } else {
- root = [];
- }
- return {isQuadratic:false, vx:NaN, vy:NaN, roots:root};
- }
- const vx = -b/(2*a);
- const vy = a*vx*vx + b*vx + c;
- const D = b*b - 4*a*c;
- let roots = [];
- if (D > 1e-12){
- const s = Math.sqrt(D);
- roots = [(-b - s)/(2*a), (-b + s)/(2*a)];
- roots.sort((m,n)=>m-n);
- } else if (Math.abs(D) <= 1e-12){
- roots = [-b/(2*a)];
- }
- return {isQuadratic:true, vx, vy, roots};
- }
- // 坐标系换算与绘图
- let deviceRatio = 1;
- let pxPerUnit = 1;
- let halfW = 0, halfH = 0;
- function resizeCanvas(){
- const rect = canvas.getBoundingClientRect();
- const dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1));
- deviceRatio = dpr;
- canvas.width = Math.floor(rect.width * dpr);
- canvas.height = Math.floor(rect.height * dpr);
- ctx.setTransform(1,0,0,1,0,0);
- ctx.scale(dpr, dpr);
- halfW = rect.width / 2;
- halfH = rect.height / 2;
- pxPerUnit = rect.width / (2 * state.xMax);
- draw();
- }
- function toScreen(x, y){
- return {
- x: halfW + x * pxPerUnit,
- y: halfH - y * pxPerUnit
- };
- }
- function drawGrid(){
- if (!state.showGrid) return;
- ctx.save();
- ctx.lineWidth = 1;
- ctx.strokeStyle = '#eef3f7';
- const xMax = state.xMax;
- const yMax = halfH / pxPerUnit;
- // 细网格(1 单位)
- ctx.beginPath();
- for(let i = Math.ceil(-xMax); i <= Math.floor(xMax); i++){
- const p = toScreen(i, 0).x;
- ctx.moveTo(p, 0);
- ctx.lineTo(p, halfH*2);
- }
- for(let j = Math.ceil(-yMax); j <= Math.floor(yMax); j++){
- const p = toScreen(0, j).y;
- ctx.moveTo(0, p);
- ctx.lineTo(halfW*2, p);
- }
- ctx.stroke();
- // 坐标轴
- ctx.lineWidth = 1.5;
- ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--axes') || '#3b4856';
- // x 轴
- ctx.beginPath();
- ctx.moveTo(0, toScreen(0,0).y);
- ctx.lineTo(halfW*2, toScreen(0,0).y);
- ctx.stroke();
- // y 轴
- ctx.beginPath();
- ctx.moveTo(toScreen(0,0).x, 0);
- ctx.lineTo(toScreen(0,0).x, halfH*2);
- ctx.stroke();
- // 刻度与数字标记(每 1 单位)
- ctx.fillStyle = '#445';
- ctx.font = '12px system-ui, -apple-system, Segoe UI, Roboto, "PingFang SC", "Microsoft YaHei"';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
- // x 轴刻度
- for(let i = Math.ceil(-xMax); i <= Math.floor(xMax); i++){
- const sx = toScreen(i, 0).x;
- const sy = toScreen(0,0).y;
- ctx.beginPath();
- ctx.moveTo(sx, sy - 4);
- ctx.lineTo(sx, sy + 4);
- ctx.strokeStyle = '#8b98a8';
- ctx.lineWidth = 1;
- ctx.stroke();
- if (i !== 0){
- ctx.fillText(i.toString(), sx, sy + 6);
- }
- }
- // y 轴刻度
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- for(let j = Math.ceil(-yMax); j <= Math.floor(yMax); j++){
- const sy = toScreen(0, j).y;
- const sx = toScreen(0,0).x;
- ctx.beginPath();
- ctx.moveTo(sx - 4, sy);
- ctx.lineTo(sx + 4, sy);
- ctx.strokeStyle = '#8b98a8';
- ctx.lineWidth = 1;
- ctx.stroke();
- if (j !== 0){
- ctx.fillText(j.toString(), sx - 6, sy);
- }
- }
- // 轴箭头与标签
- ctx.fillStyle = '#222';
- // x 箭头
- {
- const y0 = toScreen(0,0).y;
- const xR = halfW*2 - 8;
- ctx.beginPath();
- ctx.moveTo(xR, y0);
- ctx.lineTo(xR-8, y0-5);
- ctx.lineTo(xR-8, y0+5);
- ctx.closePath();
- ctx.fill();
- ctx.fillText('x', xR-14, y0+10);
- }
- // y 箭头
- {
- const x0 = toScreen(0,0).x;
- const yT = 8;
- ctx.beginPath();
- ctx.moveTo(x0, yT);
- ctx.lineTo(x0-5, yT+8);
- ctx.lineTo(x0+5, yT+8);
- ctx.closePath();
- ctx.fill();
- ctx.fillText('y', x0+10, yT+4);
- }
- ctx.restore();
- }
- function drawParabola(){
- const {a,b,c} = state;
- ctx.save();
- ctx.lineWidth = 3;
- ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--accent') || '#ff7a18';
- ctx.beginPath();
- const xMax = state.xMax;
- const stepWorld = Math.max(1 / pxPerUnit, 0.01); // 约每像素采样
- let first = true;
- for(let x = -xMax; x <= xMax; x += stepWorld){
- const y = a*x*x + b*x + c;
- const p = toScreen(x, y);
- if (first){
- ctx.moveTo(p.x, p.y);
- first = false;
- } else {
- ctx.lineTo(p.x, p.y);
- }
- }
- // 最右端确保落笔
- {
- const y = a*xMax*xMax + b*xMax + c;
- const p = toScreen(xMax, y);
- ctx.lineTo(p.x, p.y);
- }
- ctx.stroke();
- ctx.restore();
- }
- function drawVertexAndAxis(){
- const info = computeInfo();
- if (!info.isQuadratic) return;
- // 对称轴
- if (state.showAxis){
- const vx = info.vx;
- const top = toScreen(vx, 99999).y;
- const bot = toScreen(vx, -99999).y;
- ctx.save();
- ctx.setLineDash([8, 6]);
- ctx.lineDashOffset = 0;
- ctx.strokeStyle = getComputedStyle(document.documentElement).getPropertyValue('--axisAccent') || '#b046ff';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(toScreen(vx,0).x, top);
- ctx.lineTo(toScreen(vx,0).x, bot);
- ctx.stroke();
- ctx.restore();
- }
- // 顶点
- if (state.showVertex){
- const p = toScreen(info.vx, info.vy);
- ctx.save();
- ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--vertex') || '#7a2fff';
- ctx.strokeStyle = '#fff';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.arc(p.x, p.y, 5, 0, Math.PI*2);
- ctx.fill();
- ctx.stroke();
- // 标签(两位小数)
- ctx.font = '12px system-ui';
- ctx.fillStyle = '#222';
- ctx.textAlign = 'left';
- ctx.textBaseline = 'bottom';
- ctx.fillText(`V(${fmt(info.vx)}, ${fmt(info.vy)})`, p.x + 8, p.y - 6);
- ctx.restore();
- }
- }
- function drawRoots(){
- if (!state.showRoots) return;
- const info = computeInfo();
- const {a,b,c} = state;
- ctx.save();
- ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--root') || '#1f7ae0';
- ctx.strokeStyle = '#fff';
- ctx.lineWidth = 2;
- if (info.isQuadratic){
- if (info.roots.length > 0){
- for(const r of info.roots){
- const p = toScreen(r, 0);
- ctx.beginPath();
- ctx.arc(p.x, p.y, 5, 0, Math.PI*2);
- ctx.fill(); ctx.stroke();
- }
- }
- } else {
- // a=0 的一次函数根(若存在)
- if (Math.abs(b) > 1e-9){
- const r = -c/b;
- const p = toScreen(r, 0);
- ctx.beginPath();
- ctx.arc(p.x, p.y, 5, 0, Math.PI*2);
- ctx.fill(); ctx.stroke();
- }
- }
- ctx.restore();
- }
- function clear(){
- const rect = canvas.getBoundingClientRect();
- ctx.clearRect(0,0,rect.width,rect.height);
- // 白底(避免导出透明)
- ctx.save();
- ctx.fillStyle = '#ffffff';
- ctx.fillRect(0,0,rect.width,rect.height);
- ctx.restore();
- }
- function draw(){
- clear();
- drawGrid();
- drawParabola();
- drawVertexAndAxis();
- drawRoots();
- updateLabels();
- }
- // 事件绑定
- function bindTwoWay(rangeEl, numEl, onChange){
- const apply = (v) => {
- if (numEl.min !== '') v = clamp(+v, +numEl.min, +numEl.max);
- numEl.value = v;
- rangeEl.value = v;
- onChange(+v);
- };
- rangeEl.addEventListener('input', e => apply(e.target.value));
- numEl.addEventListener('input', e => apply(e.target.value));
- numEl.addEventListener('change', e => apply(e.target.value));
- }
- bindTwoWay(aRange, aNum, v => { state.a = +v; draw(); });
- bindTwoWay(bRange, bNum, v => { state.b = +v; draw(); });
- bindTwoWay(cRange, cNum, v => { state.c = +v; draw(); });
- bindTwoWay(xRange, xRangeNum, v => {
- state.xMax = Math.round(+v);
- resizeCanvas();
- syncInputsFromState();
- draw();
- });
- // 选项
- chkVertex.addEventListener('change', e => { state.showVertex = e.target.checked; draw(); });
- chkAxis.addEventListener('change', e => { state.showAxis = e.target.checked; draw(); });
- chkRoots.addEventListener('change', e => { state.showRoots = e.target.checked; draw(); });
- chkGrid.addEventListener('change', e => { state.showGrid = e.target.checked; draw(); });
- // 重置
- btnReset.addEventListener('click', () => {
- state.a = 1; state.b = 0; state.c = 0;
- state.xMax = 10;
- state.showVertex = false;
- state.showAxis = false;
- state.showRoots = false;
- state.showGrid = true;
- syncInputsFromState();
- resizeCanvas();
- });
- // 保存 PNG
- btnSave.addEventListener('click', () => {
- draw();
- const link = document.createElement('a');
- link.download = `quadratic_a${fmt(state.a)}_b${fmt(state.b)}_c${fmt(state.c)}.png`;
- link.href = canvas.toDataURL('image/png');
- link.click();
- });
- // 自适应
- window.addEventListener('resize', resizeCanvas, {passive:true});
- // 初始化
- syncInputsFromState();
- resizeCanvas();
- })();
- </script>
- </body>
- </html>
复制代码
|
评分
-
参与人数 3 | 荣誉 +7 |
鱼币 +9 |
贡献 +9 |
C币 +6 |
收起
理由
|
不二如是
| + 2 |
+ 3 |
+ 3 |
+ 3 |
鱼C有你更精彩^_^ |
小甲鱼的二师兄
| + 3 |
+ 3 |
+ 3 |
|
鱼C有你更精彩^_^ |
小甲鱼
| + 2 |
+ 3 |
+ 3 |
+ 3 |
鱼C有你更精彩^_^ |
查看全部评分
|