|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
作品介绍:
通过鼠标可以在圆中转动角度,在下图中的三角函数图像上显示该角度所在的位置
在左侧的小面板上显示坐标位置、j角度值以及三角函数值
- <!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:#f7ebc5;
- --panel:#ffffff;
- --ink:#222;
- --grid:#ececec;
- --axes:#333;
- --cos:#1f6afe;
- --sin:#1fbf6b;
- --tan:#ff6a3d;
- --point:#e03131;
- --shadow:0 6px 18px rgba(0,0,0,.08);
- --radius:12px;
- }
- *{box-sizing:border-box}
- body{
- margin:0;
- background:var(--bg);
- color:var(--ink);
- font:14px/1.45 system-ui,-apple-system,Segoe UI,Roboto,"PingFang SC","Microsoft YaHei","Noto Sans",Arial,sans-serif;
- }
- .app{
- min-height:100vh;
- padding:14px;
- display:grid;
- grid-template-columns: 280px 1fr 240px;
- grid-template-rows: auto 1fr;
- gap:14px;
- }
- @media (max-width: 1080px){
- .app{
- grid-template-columns: 1fr;
- grid-template-rows: auto auto auto;
- }
- }
- .card{
- background:var(--panel);
- border-radius:var(--radius);
- box-shadow:var(--shadow);
- padding:12px;
- }
- .panel-left{grid-column:1;grid-row:1/span 2}
- .center-top{grid-column:2;grid-row:1}
- .center-bottom{grid-column:2;grid-row:2}
- .panel-right{grid-column:3;grid-row:1/span 2}
- @media (max-width:1080px){
- .panel-left{grid-column:1;grid-row:auto}
- .center-top{grid-column:1}
- .center-bottom{grid-column:1}
- .panel-right{grid-column:1}
- }
- h3{margin:6px 0 10px;font-size:16px}
- .kv{display:grid;grid-template-columns: 88px 1fr; gap:6px 8px; align-items:center}
- .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
- .canvas-wrap{position:relative;width:100%;aspect-ratio:1/1;min-height:260px}
- .plot-wrap{position:relative;width:100%;aspect-ratio: 5/2; min-height:240px}
- canvas{position:absolute;inset:0;width:100%;height:100%;display:block;border-radius:8px;background:#fff}
- .ctrls{display:grid;gap:10px}
- .ctrls .row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
- label{user-select:none}
- input[type="range"]{width:100%}
- button{padding:8px 12px;border-radius:10px;border:1px solid #d6dde4;background:#fff;cursor:pointer}
- button.primary{background:linear-gradient(180deg,#ffd27d,#ff9f1a);color:#222;border:none}
- </style>
- </head>
- <body>
- <div class="app">
- <!-- 左侧数值面板 -->
- <section class="card panel-left">
- <h3>值</h3>
- <div class="kv">
- <div>角</div><div id="angleRead" class="mono">0.0°</div>
- <div>(x, y)</div><div id="xyRead" class="mono">(1.000, 0.000)</div>
- <div id="exprName" class="mono">cosθ =</div>
- <div id="exprVal" class="mono">x / 1 = 1.000</div>
- </div>
- <hr/>
- <div>
- <label><input type="radio" name="unit" value="deg" checked> 度</label>
- <label style="margin-left:12px"><input type="radio" name="unit" value="rad"> 弧度</label>
- </div>
- </section>
- <!-- 中央单位圆(半径=1,坐标补全,视角拉远) -->
- <section class="card center-top">
- <div class="canvas-wrap">
- <canvas id="circle" aria-label="单位圆画布"></canvas>
- </div>
- </section>
- <!-- 下方函数曲线(坐标补全) -->
- <section class="card center-bottom">
- <div class="plot-wrap">
- <canvas id="plot" aria-label="三角函数曲线画布"></canvas>
- </div>
- </section>
- <!-- 右侧控制 -->
- <aside class="card panel-right">
- <h3>显示</h3>
- <div class="ctrls">
- <div>
- <label><input type="radio" name="func" value="cos" checked> <span style="color:var(--cos)">cos</span></label>
- <label style="margin-left:12px"><input type="radio" name="func" value="sin"> <span style="color:var(--sin)">sin</span></label>
- <label style="margin-left:12px"><input type="radio" name="func" value="tan"> <span style="color:var(--tan)">tan</span></label>
- </div>
- <div class="row">
- <label><input type="checkbox" id="annotations" checked> 标识</label>
- <label><input type="checkbox" id="grid" checked> 网格</label>
- </div>
- <hr/>
- <div class="row" style="align-items:center">
- <label style="width:56px">角度</label>
- <input type="range" id="thetaRange" min="-720" max="720" step="0.1" value="0" />
- <span class="mono" id="thetaLabel">0°</span>
- </div>
- <div class="row">
- <button id="reset">重置</button>
- <button id="save" class="primary">下载 PNG</button>
- </div>
- </div>
- </aside>
- </div>
- <script>
- (() => {
- // 视角:单位圆视图
- const VIEW_RANGE = 2;
- const GRID_STEP = 0.5;
- const MARGIN = 18;
- // 状态
- const state = {
- theta: 0, // 角度(度),可达 ±720°
- func: 'cos',
- unit: 'deg',
- showGrid: true,
- showAnnotations: true
- };
- // DOM
- const circle = document.getElementById('circle');
- const plot = document.getElementById('plot');
- const ctxC = circle.getContext('2d');
- const ctxP = plot.getContext('2d');
- const thetaRange = document.getElementById('thetaRange');
- const thetaLabel = document.getElementById('thetaLabel');
- const angleRead = document.getElementById('angleRead');
- const xyRead = document.getElementById('xyRead');
- const exprName = document.getElementById('exprName');
- const exprVal = document.getElementById('exprVal');
- const radiosFunc = [...document.querySelectorAll('input[name="func"]')];
- const radiosUnit = [...document.querySelectorAll('input[name="unit"]')];
- const chkGrid = document.getElementById('grid');
- const chkAnno = document.getElementById('annotations');
- const btnReset = document.getElementById('reset');
- const btnSave = document.getElementById('save');
- // 工具
- const deg2rad = d => d * Math.PI / 180;
- const rad2deg = r => r * 180 / Math.PI;
- const clamp = (v,min,max)=>Math.min(max,Math.max(min,v));
- const fmt = n => {
- if (!isFinite(n)) return '—';
- const near0 = Math.abs(n) < 5e-4 ? 0 : n;
- return near0.toFixed(3);
- };
- const getVar = name => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
- const colorFor = f => f==='cos' ? getVar('--cos') : (f==='sin' ? getVar('--sin') : getVar('--tan'));
- // 自适应画布
- function fitCanvas(canvas, ctx){
- const rect = canvas.getBoundingClientRect();
- const dpr = Math.max(1, window.devicePixelRatio || 1);
- 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);
- }
- function resizeAll(){
- fitCanvas(circle, ctxC);
- fitCanvas(plot, ctxP);
- drawAll();
- }
- window.addEventListener('resize', resizeAll, {passive:true});
- // 角度滑块
- thetaRange.addEventListener('input', e=>{
- state.theta = +e.target.value;
- syncUI();
- drawAll();
- });
- // 函数选择
- radiosFunc.forEach(r=>r.addEventListener('change', e=>{
- if(e.target.checked){ state.func = e.target.value; drawAll(); syncUI(); }
- }));
- // 单位选择
- radiosUnit.forEach(r=>r.addEventListener('change', e=>{
- if(e.target.checked){ state.unit = e.target.value; syncUI(); drawAll(); }
- }));
- // 选项
- chkGrid.addEventListener('change', e=>{ state.showGrid = e.target.checked; drawAll(); });
- chkAnno.addEventListener('change', e=>{ state.showAnnotations = e.target.checked; drawAll(); });
- // 拖动单位圆上的红点(角度解包,支持连续累计到 ±720°)
- (function enableCircleDrag(){
- let dragging = false;
- let lastRawDeg = 0; // 上一帧 atan2 的原始角(-180,180]
- function rawDegFromEvent(ev){
- const rect = circle.getBoundingClientRect();
- const cx = rect.width/2, cy = rect.height/2;
- const x = ev.clientX - (rect.left + cx);
- const y = ev.clientY - (rect.top + cy);
- return rad2deg(Math.atan2(-y, x)); // [-180,180]
- }
- function onMove(ev){
- if (!dragging) return;
- const raw = rawDegFromEvent(ev);
- let delta = raw - lastRawDeg;
- // 解包到最短角差(避免 180↔-180 跳变)
- if (delta > 180) delta -= 360;
- else if (delta < -180) delta += 360;
- state.theta = clamp(state.theta + delta, -720, 720);
- lastRawDeg = raw;
- thetaRange.value = state.theta;
- syncUI();
- drawAll();
- }
- circle.addEventListener('pointerdown', ev=>{
- dragging = true;
- circle.setPointerCapture(ev.pointerId);
- lastRawDeg = rawDegFromEvent(ev); // 记录起始角,不立刻改 θ
- });
- circle.addEventListener('pointermove', onMove);
- circle.addEventListener('pointerup', ()=> dragging = false);
- circle.addEventListener('pointercancel',()=> dragging = false);
- })();
- // 文本 UI
- function syncUI(){
- const deg = state.theta;
- const rad = deg2rad(deg);
- thetaLabel.textContent = `${deg.toFixed(1)}°`;
- angleRead.textContent = state.unit==='deg' ? `${deg.toFixed(1)}°` : `${fmt(rad)} rad`;
- const x = Math.cos(rad), y = Math.sin(rad);
- xyRead.textContent = `(${fmt(x)}, ${fmt(y)})`;
- let name = state.func + 'θ =';
- let expr;
- if (state.func === 'cos') expr = `x / 1 = ${fmt(x)}`;
- else if (state.func === 'sin') expr = `y / 1 = ${fmt(y)}`;
- else expr = (Math.abs(Math.cos(rad)) < 1e-6) ? '未定义' : `y / x = ${fmt(Math.tan(rad))}`;
- exprName.textContent = name;
- exprVal.textContent = expr;
- }
- // 绘制单位圆(半径=1,坐标补全 + 多圈角度弧)
- function drawCircle(){
- const rect = circle.getBoundingClientRect();
- const w = rect.width, h = rect.height;
- const cx = w/2, cy = h/2;
- // 1 单位像素
- const unitPx = (Math.min(w,h) - 2*MARGIN) / (2*VIEW_RANGE);
- const sx = x => cx + x * unitPx;
- const sy = y => cy - y * unitPx;
- // 背景
- ctxC.clearRect(0,0,w,h);
- ctxC.fillStyle = '#ffffff';
- ctxC.fillRect(0,0,w,h);
- // 网格
- if (state.showGrid){
- ctxC.save();
- ctxC.strokeStyle = '#f3f3f3';
- ctxC.lineWidth = 1;
- ctxC.beginPath();
- for(let x = -VIEW_RANGE; x <= VIEW_RANGE + 1e-9; x += GRID_STEP){
- const X = Math.round(sx(x)) + 0.5;
- ctxC.moveTo(X, 0); ctxC.lineTo(X, h);
- }
- for(let y = -VIEW_RANGE; y <= VIEW_RANGE + 1e-9; y += GRID_STEP){
- const Y = Math.round(sy(y)) + 0.5;
- ctxC.moveTo(0, Y); ctxC.lineTo(w, Y);
- }
- ctxC.stroke();
- ctxC.restore();
- }
- // 坐标轴 + 箭头
- const axesColor = getVar('--axes') || '#333';
- ctxC.save();
- ctxC.strokeStyle = axesColor;
- ctxC.lineWidth = 1.6;
- const y0 = sy(0);
- ctxC.beginPath(); ctxC.moveTo(MARGIN/2, y0); ctxC.lineTo(w - MARGIN/2, y0); ctxC.stroke();
- const x0 = sx(0);
- ctxC.beginPath(); ctxC.moveTo(x0, MARGIN/2); ctxC.lineTo(x0, h - MARGIN/2); ctxC.stroke();
- drawArrow(ctxC, w - MARGIN/2, y0, 'right', axesColor);
- drawArrow(ctxC, x0, MARGIN/2, 'up', axesColor);
- // 刻度(主整数,次 0.5)
- ctxC.strokeStyle = '#8b98a8';
- ctxC.lineWidth = 1;
- for (let v=-VIEW_RANGE; v<=VIEW_RANGE+1e-9; v+=GRID_STEP){
- const X = sx(v), len = (Math.abs(v % 1) < 1e-9) ? 6 : 3;
- ctxC.beginPath(); ctxC.moveTo(X, y0-len); ctxC.lineTo(X, y0+len); ctxC.stroke();
- }
- for (let v=-VIEW_RANGE; v<=VIEW_RANGE+1e-9; v+=GRID_STEP){
- const Y = sy(v), len = (Math.abs(v % 1) < 1e-9) ? 6 : 3;
- ctxC.beginPath(); ctxC.moveTo(x0-len, Y); ctxC.lineTo(x0+len, Y); ctxC.stroke();
- }
- // 标签
- ctxC.fillStyle = '#445';
- ctxC.font = '12px system-ui, -apple-system, Segoe UI, Roboto';
- ctxC.textAlign = 'center'; ctxC.textBaseline = 'top';
- for (let v=-VIEW_RANGE; v<=VIEW_RANGE; v++){
- const X = sx(v); ctxC.fillText(String(v), X, y0 + 8);
- }
- ctxC.textAlign = 'right'; ctxC.textBaseline = 'middle';
- for (let v=-VIEW_RANGE; v<=VIEW_RANGE; v++){
- const Y = sy(v); ctxC.fillText(String(v), x0 - 8, Y);
- }
- ctxC.textAlign = 'left'; ctxC.textBaseline = 'top';
- ctxC.fillText('0', x0 + 6, y0 + 6);
- ctxC.textAlign = 'right'; ctxC.textBaseline = 'top'; ctxC.fillText('x', w - MARGIN/2 - 6, y0 + 6);
- ctxC.textAlign = 'left'; ctxC.textBaseline = 'bottom'; ctxC.fillText('y', x0 + 6, MARGIN/2 + 12);
- ctxC.restore();
- // 单位圆(半径=1)
- ctxC.beginPath();
- ctxC.lineWidth = 2;
- ctxC.strokeStyle = '#000';
- ctxC.arc(sx(0), sy(0), 1 * unitPx, 0, Math.PI*2);
- ctxC.stroke();
- // 当前点
- const rad = deg2rad(state.theta);
- const px = sx(Math.cos(rad));
- const py = sy(Math.sin(rad));
- // 半径线
- ctxC.beginPath();
- ctxC.moveTo(sx(0), sy(0));
- ctxC.lineTo(px, py);
- ctxC.strokeStyle = '#2b60ff';
- ctxC.lineWidth = 2;
- ctxC.stroke();
- // 角度弧:第一圈在内、第二圈向外扩展
- if (state.showAnnotations){
- drawAngleRings(ctxC, sx(0), sy(0), unitPx, rad);
- }
- // 投影线
- if (state.showAnnotations){
- ctxC.save();
- ctxC.setLineDash([6,4]);
- ctxC.strokeStyle = '#000';
- ctxC.lineWidth = 1.2;
- ctxC.beginPath(); ctxC.moveTo(px, py); ctxC.lineTo(px, sy(0)); ctxC.stroke();
- ctxC.beginPath(); ctxC.moveTo(px, py); ctxC.lineTo(sx(0), py); ctxC.stroke();
- ctxC.restore();
- }
- // 红点
- ctxC.beginPath();
- ctxC.arc(px, py, 5, 0, Math.PI*2);
- ctxC.fillStyle = getVar('--point') || '#e03131';
- ctxC.strokeStyle = '#fff';
- ctxC.lineWidth = 2;
- ctxC.fill(); ctxC.stroke();
- }
- // 多圈角度弧(第一圈=内圈,第二圈=外圈)
- function drawAngleRings(ctx, cx, cy, unitPx, thetaRad){
- const dir = thetaRad >= 0 ? 1 : -1;
- const A = Math.abs(thetaRad);
- const fullTurns = Math.floor(A / (2*Math.PI)); // 已完成的整圈数:0,1,2
- const rem = A - fullTurns * (2*Math.PI); // 当前圈的剩余角度
- const baseR = unitPx * 0.45; // 内圈半径(更靠近圆心、更小)
- const gap = unitPx * 0.14; // 圈间距(向外渐增)
- ctx.save();
- ctx.strokeStyle = '#2b60ff';
- ctx.lineWidth = 2;
- ctx.lineCap = 'round';
- // 1) 先把已经完成的整圈,从内往外画满圈(无箭头)
- for (let i = 0; i < fullTurns; i++){
- const r = Math.max(6, baseR + i*gap);
- ctx.beginPath();
- ctx.arc(cx, cy, r, 0, Math.PI*2, dir < 0);
- ctx.stroke();
- }
- // 2) 当前未完成的圈,画在“下一圈”(外侧)上,并加箭头
- if (rem > 1e-6){
- const rRem = Math.max(6, baseR + fullTurns*gap);
- drawArcSegmentWithArrow(ctx, cx, cy, rRem, dir * rem);
- }
- ctx.restore();
- }
- // 从 0 到 theta 的弧线 + 箭头(theta 可正可负)
- function drawArcSegmentWithArrow(ctx, cx, cy, r, theta){
- const steps = Math.max(12, Math.ceil(Math.abs(theta) / (Math.PI / 48)));
- let xPrev = cx + r, yPrev = cy; // 起点:+x 方向
- let xCurr = xPrev, yCurr = yPrev;
- ctx.save();
- ctx.strokeStyle = '#2b60ff';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(xPrev, yPrev);
- for (let i=1;i<=steps;i++){
- const t = theta * (i/steps);
- const x = cx + r * Math.cos(t);
- const y = cy - r * Math.sin(t);
- xPrev = xCurr; yPrev = yCurr;
- xCurr = x; yCurr = y;
- ctx.lineTo(x, y);
- }
- ctx.stroke();
- // 箭头
- const vx = xCurr - xPrev;
- const vy = yCurr - yPrev;
- const len = Math.hypot(vx, vy) || 1;
- const ux = vx/len, uy = vy/len;
- const ah = 8, aw = 6;
- ctx.fillStyle = '#2b60ff';
- ctx.beginPath();
- ctx.moveTo(xCurr, yCurr);
- ctx.lineTo(xCurr - ux*ah + (-uy)*aw, yCurr - uy*ah + (ux)*aw);
- ctx.lineTo(xCurr - ux*ah - (-uy)*aw, yCurr - uy*ah - (ux)*aw);
- ctx.closePath();
- ctx.fill();
- ctx.restore();
- }
- // 绘制三角函数曲线(坐标补全)
- function drawPlot(){
- const rect = plot.getBoundingClientRect();
- const w = rect.width, h = rect.height;
- const pad = 18;
- const left = pad+4, right = w-pad-4, top = pad, bottom = h-pad;
- const innerW = right-left, innerH = bottom-top;
- ctxP.clearRect(0,0,w,h);
- ctxP.fillStyle = '#fff';
- ctxP.fillRect(0,0,w,h);
- // x 范围:以当前 θ 为中心 ±2π
- const theta0 = deg2rad(state.theta);
- const xMin = theta0 - 2*Math.PI;
- const xMax = theta0 + 2*Math.PI;
- // y 范围
- const yMax = (state.func==='tan') ? 3 : 1.2;
- const yMin = -yMax;
- // 映射
- const sx = t => left + (t - xMin) / (xMax - xMin) * innerW;
- const sy = y => bottom - (y - yMin) / (yMax - yMin) * innerH;
- // 网格
- if (state.showGrid){
- ctxP.save();
- ctxP.strokeStyle = getVar('--grid') || '#ececec';
- ctxP.lineWidth = 1;
- // 竖向网格:每 π/6
- const stepMinor = Math.PI/6;
- const sMin = Math.ceil(xMin/stepMinor);
- const sMax = Math.floor(xMax/stepMinor);
- ctxP.beginPath();
- for(let k=sMin; k<=sMax; k++){
- const x = sx(k*stepMinor);
- ctxP.moveTo(x, top); ctxP.lineTo(x, bottom);
- }
- ctxP.stroke();
- // 横向网格
- ctxP.beginPath();
- if (state.func==='tan'){
- for (let v=-3; v<=3; v++){
- const y = sy(v);
- ctxP.moveTo(left, y); ctxP.lineTo(right, y);
- }
- }else{
- const ys = [-1,-0.5,0,0.5,1];
- ys.forEach(v=>{ const y=sy(v); ctxP.moveTo(left,y); ctxP.lineTo(right,y); });
- }
- ctxP.stroke();
- ctxP.restore();
- }
- // 坐标轴 + 箭头
- const axesColor = getVar('--axes') || '#333';
- ctxP.save();
- ctxP.strokeStyle = axesColor;
- ctxP.lineWidth = 1.6;
- const y0 = sy(0);
- ctxP.beginPath(); ctxP.moveTo(left, y0); ctxP.lineTo(right, y0); ctxP.stroke();
- drawArrow(ctxP, right, y0, 'right', axesColor);
- if (xMin < 0 && xMax > 0){
- const x0 = sx(0);
- ctxP.beginPath(); ctxP.moveTo(x0, top); ctxP.lineTo(x0, bottom); ctxP.stroke();
- drawArrow(ctxP, x0, top, 'up', axesColor);
- }
- ctxP.restore();
- // 刻度与标签(x 主:π/2;次:π/6)
- ctxP.save();
- ctxP.fillStyle = '#445';
- ctxP.strokeStyle = '#8b98a8';
- ctxP.font = '12px system-ui, -apple-system, Segoe UI, Roboto';
- const stepMajor = Math.PI/2, stepMinor = Math.PI/6;
- const kMinM = Math.ceil(xMin/stepMinor), kMaxM = Math.floor(xMax/stepMinor);
- for(let k=kMinM; k<=kMaxM; k++){
- const X = sx(k*stepMinor);
- const isMajor = (k % 3 === 0);
- const len = isMajor ? 7 : 4;
- ctxP.beginPath(); ctxP.moveTo(X, y0-len); ctxP.lineTo(X, y0+len); ctxP.stroke();
- }
- // 主刻度标签(π/2 倍数)
- const kMin = Math.ceil(xMin/stepMajor), kMax = Math.floor(xMax/stepMajor);
- ctxP.textAlign = 'center'; ctxP.textBaseline = 'top';
- for(let k=kMin; k<=kMax; k++){
- ctxP.fillText(piLabelFromK(k), sx(k*stepMajor), y0 + 8);
- }
- ctxP.textAlign = 'right'; ctxP.textBaseline = 'top';
- ctxP.fillText('θ', right - 6, y0 + 8);
- // y 刻度与标签
- const yTicks = (state.func==='tan') ? [-3,-2,-1,0,1,2,3] : [-1,-0.5,0,0.5,1];
- const xForTicks = (xMin < 0 && xMax > 0) ? sx(0) : left;
- ctxP.textAlign = 'right'; ctxP.textBaseline = 'middle';
- yTicks.forEach(v=>{
- const Y = sy(v);
- const len = (Math.abs(v % 1) < 1e-9) ? 7 : 4;
- ctxP.beginPath(); ctxP.moveTo(xForTicks-len, Y); ctxP.lineTo(xForTicks+len, Y); ctxP.stroke();
- ctxP.fillText(String(v), xForTicks - 8, Y);
- });
- ctxP.textAlign = 'left'; ctxP.textBaseline = 'bottom';
- const yName = state.func + 'θ';
- const xAxisForName = (xMin < 0 && xMax > 0) ? sx(0) + 6 : left + 6;
- ctxP.fillText(yName, xAxisForName, top + 14);
- ctxP.restore();
- // tan 渐近线
- if (state.func==='tan'){
- ctxP.save();
- ctxP.setLineDash([6,6]);
- ctxP.strokeStyle = '#f1a47f';
- ctxP.lineWidth = 1.2;
- const step = Math.PI;
- const start = Math.ceil((xMin - Math.PI/2)/step)*step + Math.PI/2;
- for (let x = start; x <= xMax; x += step){
- const X = sx(x);
- ctxP.beginPath(); ctxP.moveTo(X, top); ctxP.lineTo(X, bottom); ctxP.stroke();
- }
- ctxP.restore();
- }
- // 曲线
- ctxP.save();
- ctxP.strokeStyle = colorFor(state.func);
- ctxP.lineWidth = 2;
- const fn = (t)=>{
- if (state.func==='cos') return Math.cos(t);
- if (state.func==='sin') return Math.sin(t);
- const c = Math.cos(t);
- if (Math.abs(c) < 1e-6) return Infinity;
- return Math.tan(t);
- };
- const samples = Math.max(240, Math.floor(innerW));
- let first = true;
- let lastY = null;
- ctxP.beginPath();
- for(let i=0;i<=samples;i++){
- const t = xMin + (i/samples)*(xMax-xMin);
- let y = fn(t);
- if (state.func==='tan'){
- if (!isFinite(y) || Math.abs(Math.cos(t))<0.015){ lastY=null; first=true; continue; }
- y = Math.max(-3, Math.min(3, y));
- }
- const X = sx(t), Y = sy(y);
- if (first){ ctxP.moveTo(X, Y); first = false; }
- else{
- if (state.func==='tan' && lastY !== null && Math.abs(Y - lastY) > innerH*0.5){
- ctxP.moveTo(X, Y);
- }else ctxP.lineTo(X, Y);
- }
- lastY = Y;
- }
- ctxP.stroke();
- ctxP.restore();
- // 当前 θ 的标记
- const fx = fn(theta0);
- const X0 = sx(theta0);
- if (state.showAnnotations){
- ctxP.save();
- ctxP.setLineDash([6,4]);
- ctxP.strokeStyle = '#999';
- ctxP.beginPath(); ctxP.moveTo(X0, top); ctxP.lineTo(X0, bottom); ctxP.stroke();
- ctxP.restore();
- }
- if (isFinite(fx) && !(state.func==='tan' && Math.abs(Math.cos(theta0))<1e-6)){
- const Y0 = sy(Math.max(-3, Math.min(3, fx)));
- ctxP.beginPath();
- ctxP.arc(X0, Y0, 5, 0, Math.PI*2);
- ctxP.fillStyle = getVar('--point');
- ctxP.strokeStyle = '#fff';
- ctxP.lineWidth = 2;
- ctxP.fill(); ctxP.stroke();
- }
- }
- // π 标签:t = k*(π/2) 的文本
- const gcd = (a,b)=> b===0 ? Math.abs(a) : gcd(b, a % b);
- function piLabelFromK(k){
- if (k === 0) return '0';
- const g = gcd(Math.abs(k), 2);
- const num = k / g;
- const den = 2 / g;
- const s = (num === 1) ? '' : (num === -1 ? '-' : String(num));
- return s + 'π' + (den === 1 ? '' : '/' + den);
- }
- // 画箭头
- function drawArrow(ctx, x, y, dir, color){
- ctx.save();
- ctx.fillStyle = color || '#333';
- const L = 10, W = 6;
- ctx.beginPath();
- if (dir === 'right'){
- ctx.moveTo(x, y);
- ctx.lineTo(x - L, y - W/2);
- ctx.lineTo(x - L, y + W/2);
- }else if (dir === 'up'){
- ctx.moveTo(x, y);
- ctx.lineTo(x - W/2, y + L);
- ctx.lineTo(x + W/2, y + L);
- }
- ctx.closePath();
- ctx.fill();
- ctx.restore();
- }
- function drawAll(){
- drawCircle();
- drawPlot();
- }
- // 重置
- btnReset.addEventListener('click', ()=>{
- state.theta = 0;
- thetaRange.value = 0;
- state.func = 'cos';
- radiosFunc.forEach(r=> r.checked = (r.value==='cos'));
- state.unit = 'deg';
- radiosUnit.forEach(r=> r.checked = (r.value==='deg'));
- state.showGrid = true; chkGrid.checked = true;
- state.showAnnotations = true; chkAnno.checked = true;
- syncUI();
- drawAll();
- });
- // 下载 PNG
- btnSave.addEventListener('click', ()=>{
- drawAll();
- const cw = circle.getBoundingClientRect().width;
- const ch = circle.getBoundingClientRect().height;
- const pw = plot.getBoundingClientRect().width;
- const ph = plot.getBoundingClientRect().height;
- const W = Math.max(cw, pw);
- const H = ch + 10 + ph;
- const off = document.createElement('canvas');
- off.width = W; off.height = H;
- const g = off.getContext('2d');
- g.fillStyle = '#fff'; g.fillRect(0,0,W,H);
- const cImg = new Image();
- const pImg = new Image();
- const once = (img, src) => new Promise(res => { img.onload = res; img.src = src; });
- Promise.all([
- once(cImg, circle.toDataURL('image/png')),
- once(pImg, plot.toDataURL('image/png'))
- ]).then(()=>{
- const cx = (W - cw)/2;
- const px = (W - pw)/2;
- g.drawImage(cImg, cx, 0, cw, ch);
- g.drawImage(pImg, px, ch + 10, pw, ph);
- const a = document.createElement('a');
- a.download = `trig_${state.func}_theta${state.theta.toFixed(1)}.png`;
- a.href = off.toDataURL('image/png');
- a.click();
- });
- });
- // 启动
- resizeAll();
- syncUI();
- drawAll();
- })();
- </script>
- </body>
- </html>
复制代码
|
评分
-
参与人数 1 | 荣誉 +5 |
鱼币 +5 |
贡献 +5 |
C币 +5 |
收起
理由
|
小甲鱼
| + 5 |
+ 5 |
+ 5 |
+ 5 |
鱼C有你更精彩^_^ |
查看全部评分
|