二次函数可视化
二次函数可视化交互式的二次函数 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{
width:100%;
appearance:none;
height:6px;
border-radius:999px;
background:#e8eef3;
outline:none;
}
input::-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{
width:100%;
padding:6px 8px;
border:1px solid #dde4ea;
border-radius:8px;
outline:none;
}
input: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)}`;
} else {
rootsEl.textContent = `x₁ = ${fmt(info.roots)}, x₂ = ${fmt(info.roots)}`;
}
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();
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>
收藏一下 学到了 这个是不是可以给中学生上课用了 学习了 很好,寡人又学到知识了{:10_256:} 向大佬学习{:10_327:} 学到了 给娃搞一个 这是不是能给初中生可视化讲几何题了:)
这个完全可以用于中学生上课用,楼主厉害了 哇!码住 太厉害了 学到了 这么多, 太厉害了吧,码住码住 学习了学习了 学习了 我来打卡学习啦 要是我们以前有这东西,数学能不好吗