Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>GT2 Timing Belt — Two-Pulley Simulator v1.1</title>
- <style>
- :root {
- --bg: #0f172a; /* slate-900 */
- --panel: #111827; /* gray-900 */
- --muted: #94a3b8; /* slate-400 */
- --text: #e5e7eb; /* gray-200 */
- --accent: #22d3ee; /* cyan-400 */
- --accent-2: #a78bfa; /* violet-400 */
- --ok: #34d399; /* emerald-400 */
- --warn: #f59e0b; /* amber-500 */
- }
- * { box-sizing: border-box; }
- body {
- margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
- background: linear-gradient(180deg, #0b1220, #0f172a 40%);
- color: var(--text);
- }
- header { padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; }
- header h1 { font-size: 18px; margin: 0; letter-spacing: 0.3px; font-weight: 600; }
- header .sub { color: var(--muted); font-size: 12px; }
- .wrap { display: grid; grid-template-columns: 360px 1fr; gap: 16px; padding: 0 16px 16px; }
- .panel {
- background: radial-gradient(1200px 600px at 0% -10%, rgba(34,211,238,.08), transparent 60%),
- radial-gradient(1000px 500px at 120% -10%, rgba(167,139,250,.08), transparent 60%),
- var(--panel);
- border: 1px solid rgba(148,163,184,.15);
- border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,.35);
- }
- .controls { padding: 16px; position: sticky; top: 8px; }
- .row { display: grid; grid-template-columns: 1fr 140px; align-items: end; gap: 10px; margin-bottom: 12px; }
- .row label { font-size: 13px; color: var(--muted); }
- .row input[type="number"], .row input[type="range"] {
- width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(148,163,184,.2); background: #0b1220; color: var(--text);
- outline: none; transition: border-color .2s;
- }
- .row input:focus { border-color: var(--accent); }
- .kpis { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
- .kpi { border: 1px solid rgba(148,163,184,.15); border-radius: 12px; padding: 12px; }
- .kpi .label { color: var(--muted); font-size: 12px; }
- .kpi .value { font-size: 16px; font-weight: 700; margin-top: 4px; letter-spacing: .2px; }
- .sim { position: relative; }
- canvas { width: 100%; height: 70vh; display: block; border-radius: 16px; }
- .note { font-size: 12px; color: var(--muted); margin-top: 8px; line-height: 1.35; }
- .small { font-size: 11px; color: var(--muted); }
- .legend { display:flex; gap:10px; align-items:center; padding:10px 16px; color:var(--muted); font-size:16px; }
- .chip { display:inline-flex; align-items:center; gap:6px; }
- .dot { width:10px; height:10px; border-radius:999px; display:inline-block; }
- .belt { background: var(--accent); }
- .pulley { background: var(--accent-2); }
- .pitch { background: var(--ok); }
- .vector { background: var(--warn); }
- </style>
- </head>
- <body>
- <header>
- <div>
- <h1>GT2 Synchronous Belt — Two-Pulley Visual Simulator <span style="color:var(--accent-2)">v1.1</span></h1>
- <div class="sub">Pitch = 2.00 mm • Input rotates at 1 rev/s • Output speed follows tooth ratio</div>
- </div>
- </header>
- <div class="wrap">
- <section class="panel controls" aria-label="controls">
- <div class="row">
- <label for="teeth1">Input pulley teeth (12–24)</label>
- <input id="teeth1" type="number" min="12" max="24" step="1" value="20" />
- </div>
- <div class="row">
- <label for="teeth2">Output pulley teeth (24–60)</label>
- <input id="teeth2" type="number" min="24" max="60" step="1" value="40" />
- </div>
- <div class="row">
- <label for="centerC">Center distance C (mm)</label>
- <input id="centerC" type="range" min="0" max="100" step="1" value="0" />
- </div>
- <div class="kpis">
- <div class="kpi">
- <div class="label">Pitch diameter — input / output (mm)</div>
- <div class="value" id="pitchDiam"></div>
- </div>
- <div class="kpi">
- <div class="label">Gear ratio (out ÷ in)</div>
- <div class="value" id="ratio"></div>
- </div>
- <div class="kpi">
- <div class="label">Minimum center distance C<sub>min</sub> (mm)</div>
- <div class="value" id="cmin"></div>
- </div>
- <div class="kpi">
- <div class="label">Corresponding belt length L(C<sub>min</sub>) (mm)</div>
- <div class="value" id="lmin"></div>
- </div>
- </div>
- <div class="kpis" style="margin-top:6px">
- <div class="kpi" style="grid-column: span 2;">
- <div class="label" style="font-weight:700">Current Properties</div>
- <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-top:8px;">
- <div>
- <div class="label">Current center distance C (mm)</div>
- <div class="value" id="cnow"></div>
- </div>
- <div>
- <div class="label">Current belt length L(C) (mm)</div>
- <div class="value" id="lcurr"></div>
- </div>
- </div>
- </div>
- <div class="kpi">
- <div class="label">Output speed (rev/s)</div>
- <div class="value" id="ospeed"></div>
- </div>
- </div>
- <p class="note">
- <strong>Notes</strong>: Pitch diameter D = (teeth × pitch)/π. Open-belt length is approximated along pitch circles by
- L ≈ 2C + (π/2)(D₁ + D₂) + (D₂ − D₁)²/(4C). Minimum feasible C occurs at D₁ and D₂ just touching: C<sub>min</sub> = (D₁ + D₂)/2.
- </p>
- <p class="small">This is a geometric, pitch-line visualization (not a tooth-level interference check). GT2 pitch = 2.00 mm.</p>
- </section>
- <section class="panel sim" aria-label="simulation">
- <canvas id="view" width="1280" height="800" aria-label="belt simulation canvas"></canvas>
- <div class="legend">
- <span class="chip"><span class="dot belt"></span> Belt (animated dash at GT2 pitch)</span>
- <span class="chip"><span class="dot pulley"></span> Pitch circles (pulleys)</span>
- <span class="chip"><span class="dot pitch"></span> Ticks every 10 teeth</span>
- <span class="chip"><span class="dot vector"></span> Center vector C</span>
- </div>
- </section>
- </div>
- <script>
- // ----- Constants -----
- const PITCH = 2.0; // mm (GT2)
- const INPUT_RPS = 1.0; // revs per second for input pulley
- // Elements
- const teeth1El = document.getElementById('teeth1');
- const teeth2El = document.getElementById('teeth2');
- const centerCEl = document.getElementById('centerC');
- const pitchDiamEl = document.getElementById('pitchDiam');
- const ratioEl = document.getElementById('ratio');
- const cminEl = document.getElementById('cmin');
- const lminEl = document.getElementById('lmin');
- const cnowEl = document.getElementById('cnow');
- const lcurrEl = document.getElementById('lcurr');
- const ospeedEl = document.getElementById('ospeed');
- const canvas = document.getElementById('view');
- const ctx = canvas.getContext('2d');
- // View state
- const state = {
- N1: 20, N2: 40,
- D1: 0, D2: 0, // pitch diameters (mm)
- r1: 0, r2: 0, // mm
- Cmin: 0, // mm
- C: 0, // current center distance (mm)
- Lmin: 0, // belt length at Cmin (mm)
- scale: 2.0, // px per mm (auto-fit later)
- angle1: 0, // radians, input pulley
- angle2: 0, // radians, output pulley
- lastT: performance.now()
- };
- function clampInt(v, lo, hi) {
- v = Math.round(v);
- return Math.max(lo, Math.min(hi, v));
- }
- function fmt1(x) { return (Math.round(x*10)/10).toFixed(1); } // mm to 0.1
- function fmt2(x) { return (Math.round(x*100)/100).toFixed(2); } // ratios/speeds
- function computeGeometry() {
- state.N1 = clampInt(parseInt(teeth1El.value||20,10), 12, 24);
- state.N2 = clampInt(parseInt(teeth2El.value||40,10), 24, 60);
- teeth1El.value = state.N1; teeth2El.value = state.N2;
- state.D1 = (state.N1 * PITCH) / Math.PI; // mm
- state.D2 = (state.N2 * PITCH) / Math.PI; // mm
- state.r1 = state.D1 / 2; state.r2 = state.D2 / 2;
- state.Cmin = (state.D1 + state.D2) / 2; // mm
- state.Lmin = 2*state.Cmin + (Math.PI/2)*(state.D1 + state.D2) + Math.pow((state.D2 - state.D1),2)/(4*state.Cmin);
- // Update slider bounds to [Cmin, Cmin+100], preserve current value within new range
- const minC = state.Cmin;
- const maxC = state.Cmin + 100; // mm
- centerCEl.min = Math.floor(minC); // slider in mm integer steps
- centerCEl.max = Math.ceil(maxC);
- if (!centerCEl.dataset.init) { centerCEl.value = Math.round(minC); centerCEl.dataset.init = '1'; }
- let Cslider = parseFloat(centerCEl.value);
- if (isNaN(Cslider)) Cslider = minC;
- Cslider = Math.max(minC, Math.min(maxC, Cslider));
- centerCEl.value = Math.round(Cslider);
- state.C = Cslider;
- // Auto-fit scale to canvas nicely
- const margin = 60; // px
- const spanMM = state.C + state.r1 + state.r2; // total span in mm across
- const neededPx = canvas.width - 2*margin;
- state.scale = Math.max(1.2, neededPx / spanMM); // px per mm
- // UI outputs (rounded per request)
- pitchDiamEl.textContent = `${fmt1(state.D1)} / ${fmt1(state.D2)}`;
- ratioEl.textContent = fmt2(state.N2 / state.N1) + ' : 1';
- cminEl.textContent = fmt1(state.Cmin);
- lminEl.textContent = fmt1( round2mm(state.Lmin) );
- cnowEl.textContent = fmt1(state.C);
- const Lcurr = 2*state.C + (Math.PI/2)*(state.D1 + state.D2) + Math.pow((state.D2 - state.D1),2)/(4*state.C);
- lcurrEl.textContent = fmt1( round2mm(Lcurr) );
- const outRPS = INPUT_RPS * (state.N1 / state.N2);
- ospeedEl.textContent = fmt2(outRPS);
- }
- function round2mm(x){ return Math.round(x/2)*2; }
- function beltLen(C, D1, D2){ return 2*C + (Math.PI/2)*(D1 + D2) + Math.pow((D2 - D1),2)/(4*C); }
- // Find nearest C (within ±0.6 mm search window) that yields belt length in 2 mm increments
- function snapCtoBelt2mm(Cguess){
- const step = 0.1; // mm resolution for search
- let bestC = Cguess, bestErr = 1e9;
- const min = Math.max(state.Cmin, Cguess - 0.6);
- const max = Math.min(state.Cmin + 100, Cguess + 0.6);
- for (let C=min; C<=max + 1e-9; C+=step){
- const L = beltLen(C, state.D1, state.D2);
- const err = Math.abs(L - round2mm(L));
- if (err < bestErr){ bestErr = err; bestC = C; }
- }
- // reflect snapped value back to slider (integer mm) while staying inside bounds
- centerCEl.value = String(Math.round(bestC));
- return bestC;
- }
- // Compute upper/lower external tangent points via line construction
- function tangentPoints(r1, r2, C, upper=true) {
- const denom = Math.max(1e-6, Math.sqrt(Math.max(1e-9, C*C - (r2 - r1)*(r2 - r1))));
- const m = ((r2 - r1) / denom) * (upper ? +1 : -1);
- const s = Math.sqrt(1 + m*m);
- const b = (upper ? +1 : -1) * r1 * s;
- const nux = -m / s; const nuy = 1 / s;
- const p1x = r1 * (upper ? nux : -nux);
- const p1y = r1 * (upper ? nuy : -nuy);
- const p2x = C - r2 * (upper ? nux : -nux);
- const p2y = r2 * (upper ? nuy : -nuy);
- return { p1: {x:p1x, y:p1y}, p2: {x:p2x, y:p2y}, m, b };
- }
- function draw() {
- const { r1, r2, C, scale } = state;
- const W = canvas.width, H = canvas.height;
- const margin = 60;
- // World-to-screen: place center of input pulley at (margin, H/2)
- const origin = { x: margin + r1*scale, y: H/2 };
- const center1 = { x: origin.x, y: origin.y };
- const center2 = { x: origin.x + C*scale, y: origin.y };
- // Background
- ctx.clearRect(0,0,W,H);
- const grad = ctx.createLinearGradient(0,0,0,H);
- grad.addColorStop(0,'#0b1220'); grad.addColorStop(1,'#0f172a');
- ctx.fillStyle = grad; ctx.fillRect(0,0,W,H);
- // Center vector with arrow
- ctx.save();
- ctx.strokeStyle = '#f59e0b';
- ctx.lineWidth = 4;
- ctx.setLineDash([]);
- ctx.beginPath();
- ctx.moveTo(center1.x, center1.y);
- ctx.lineTo(center2.x, center2.y);
- ctx.stroke();
- // arrow head at center2
- const ah = 14; const aw = 8;
- ctx.beginPath();
- ctx.moveTo(center2.x, center2.y);
- ctx.lineTo(center2.x - ah, center2.y - aw);
- ctx.lineTo(center2.x - ah, center2.y + aw);
- ctx.closePath();
- ctx.fillStyle = '#f59e0b';
- ctx.fill();
- // label C
- ctx.fillStyle = '#f59e0b';
- ctx.font = '16px ui-sans-serif, system-ui';
- ctx.fillText(`C = ${fmt1(state.C)} mm`, (center1.x+center2.x)/2 - 30, center1.y - 10);
- ctx.restore();
- // Compute tangent points (upper and lower)
- const up = tangentPoints(r1, r2, C, true);
- const lo = tangentPoints(r1, r2, C, false);
- // Helper to convert world(mm) to screen(px)
- const mm2px = (pt) => ({ x: origin.x + pt.x*scale, y: origin.y - pt.y*scale });
- const sP1u = mm2px(up.p1), sP2u = mm2px(up.p2);
- const sP1l = mm2px(lo.p1), sP2l = mm2px(lo.p2);
- const sC1 = mm2px({x:0,y:0});
- const sC2 = mm2px({x:C,y:0});
- // Belt with animated dashes
- ctx.save();
- ctx.lineWidth = 6; // px
- ctx.strokeStyle = '#22d3ee';
- const dashPx = Math.max(6, state.scale * PITCH);
- ctx.setLineDash([dashPx*0.5, dashPx*0.5]);
- const omega1 = 2*Math.PI * INPUT_RPS; // rad/s
- const vmm = omega1 * r1; // mm/s
- const vpx = vmm * state.scale;
- const now = performance.now();
- const dt = (now - state.lastT) / 1000;
- state.lastT = now;
- state.angle1 = (state.angle1 + omega1*dt) % (Math.PI*2);
- const omega2 = omega1 * (state.N1 / state.N2);
- state.angle2 = (state.angle2 + omega2*dt) % (Math.PI*2);
- ctx.lineDashOffset = (ctx.lineDashOffset - vpx*dt) % (dashPx*1000);
- function angleOf(pt, center) { return Math.atan2(center.y - pt.y, pt.x - center.x); }
- const a1u = angleOf(sP1u, sC1);
- const a1l = angleOf(sP1l, sC1);
- const a2u = angleOf(sP2u, sC2);
- const a2l = angleOf(sP2l, sC2);
- // Draw belt: upper span
- ctx.beginPath();
- ctx.moveTo(sP1u.x, sP1u.y);
- ctx.lineTo(sP2u.x, sP2u.y);
- ctx.stroke();
- // Arc around output pulley from upper to lower (clockwise)
- ctx.beginPath();
- ctx.arc(sC2.x, sC2.y, r2*scale, a2u, a2l, true);
- ctx.stroke();
- // Lower span
- ctx.beginPath();
- ctx.moveTo(sP2l.x, sP2l.y);
- ctx.lineTo(sP1l.x, sP1l.y);
- ctx.stroke();
- // Arc around input pulley from upper to lower on LEFT side
- ctx.beginPath();
- ctx.arc(sC1.x, sC1.y, r1*scale, a1u, a1l, false);
- ctx.stroke();
- ctx.restore();
- // Pulleys — pitch circles with thicker ticks (every 10 teeth)
- function drawPulley(center, r, N, angle) {
- ctx.save();
- // pitch circle
- ctx.strokeStyle = '#a78bfa';
- ctx.lineWidth = 4;
- ctx.beginPath(); ctx.arc(center.x, center.y, r*scale, 0, Math.PI*2); ctx.stroke();
- // hub
- ctx.lineWidth = 8; ctx.globalAlpha = .2;
- ctx.beginPath(); ctx.arc(center.x, center.y, Math.max(4, r*scale*0.2), 0, Math.PI*2); ctx.stroke();
- ctx.globalAlpha = 1;
- // ticks every 10 teeth (thicker & longer)
- const step = 10; const tickCount = Math.max(1, Math.floor(N / step));
- for (let i=0; i<tickCount; i++) {
- const t = (i * step) / N; // 0..1
- const a = angle + t * Math.PI*2;
- const x1 = center.x + Math.cos(a) * (r*scale);
- const y1 = center.y + Math.sin(a) * (r*scale);
- const x2 = center.x + Math.cos(a) * (r*scale + 16);
- const y2 = center.y + Math.sin(a) * (r*scale + 16);
- ctx.strokeStyle = '#34d399';
- ctx.lineWidth = 4; // thicker
- ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
- }
- ctx.restore();
- }
- drawPulley(sC1, r1, state.N1, state.angle1);
- drawPulley(sC2, r2, state.N2, state.angle2);
- requestAnimationFrame(draw);
- }
- // Event handling
- function onChange() { computeGeometry(); }
- teeth1El.addEventListener('input', onChange);
- teeth2El.addEventListener('input', onChange);
- centerCEl.addEventListener('input', onChange);
- // Init & kick off
- computeGeometry();
- state.lastT = performance.now();
- requestAnimationFrame(draw);
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment