dmalawey

PulleySimulatorV1.1

Aug 15th, 2025
138
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 16.87 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="utf-8" />
  5.   <meta name="viewport" content="width=device-width, initial-scale=1" />
  6.   <title>GT2 Timing Belt — Two-Pulley Simulator v1.1</title>
  7.   <style>
  8.     :root {
  9.       --bg: #0f172a;         /* slate-900 */
  10.       --panel: #111827;      /* gray-900 */
  11.       --muted: #94a3b8;      /* slate-400 */
  12.       --text: #e5e7eb;       /* gray-200 */
  13.       --accent: #22d3ee;     /* cyan-400 */
  14.       --accent-2: #a78bfa;   /* violet-400 */
  15.       --ok: #34d399;         /* emerald-400 */
  16.       --warn: #f59e0b;       /* amber-500 */
  17.     }
  18.     * { box-sizing: border-box; }
  19.     body {
  20.       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";
  21.       background: linear-gradient(180deg, #0b1220, #0f172a 40%);
  22.       color: var(--text);
  23.     }
  24.     header { padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; }
  25.     header h1 { font-size: 18px; margin: 0; letter-spacing: 0.3px; font-weight: 600; }
  26.     header .sub { color: var(--muted); font-size: 12px; }
  27.  
  28.     .wrap { display: grid; grid-template-columns: 360px 1fr; gap: 16px; padding: 0 16px 16px; }
  29.     .panel {
  30.       background: radial-gradient(1200px 600px at 0% -10%, rgba(34,211,238,.08), transparent 60%),
  31.                   radial-gradient(1000px 500px at 120% -10%, rgba(167,139,250,.08), transparent 60%),
  32.                   var(--panel);
  33.       border: 1px solid rgba(148,163,184,.15);
  34.       border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,.35);
  35.     }
  36.     .controls { padding: 16px; position: sticky; top: 8px; }
  37.     .row { display: grid; grid-template-columns: 1fr 140px; align-items: end; gap: 10px; margin-bottom: 12px; }
  38.     .row label { font-size: 13px; color: var(--muted); }
  39.     .row input[type="number"], .row input[type="range"] {
  40.       width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(148,163,184,.2); background: #0b1220; color: var(--text);
  41.       outline: none; transition: border-color .2s;
  42.     }
  43.     .row input:focus { border-color: var(--accent); }
  44.  
  45.     .kpis { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
  46.     .kpi { border: 1px solid rgba(148,163,184,.15); border-radius: 12px; padding: 12px; }
  47.     .kpi .label { color: var(--muted); font-size: 12px; }
  48.     .kpi .value { font-size: 16px; font-weight: 700; margin-top: 4px; letter-spacing: .2px; }
  49.  
  50.     .sim { position: relative; }
  51.     canvas { width: 100%; height: 70vh; display: block; border-radius: 16px; }
  52.  
  53.     .note { font-size: 12px; color: var(--muted); margin-top: 8px; line-height: 1.35; }
  54.     .small { font-size: 11px; color: var(--muted); }
  55.  
  56.     .legend { display:flex; gap:10px; align-items:center; padding:10px 16px; color:var(--muted); font-size:16px; }
  57.     .chip { display:inline-flex; align-items:center; gap:6px; }
  58.     .dot { width:10px; height:10px; border-radius:999px; display:inline-block; }
  59.     .belt { background: var(--accent); }
  60.     .pulley { background: var(--accent-2); }
  61.     .pitch { background: var(--ok); }
  62.     .vector { background: var(--warn); }
  63.   </style>
  64. </head>
  65. <body>
  66.   <header>
  67.     <div>
  68.       <h1>GT2 Synchronous Belt — Two-Pulley Visual Simulator <span style="color:var(--accent-2)">v1.1</span></h1>
  69.       <div class="sub">Pitch = 2.00 mm • Input rotates at 1 rev/s • Output speed follows tooth ratio</div>
  70.     </div>
  71.   </header>
  72.  
  73.   <div class="wrap">
  74.     <section class="panel controls" aria-label="controls">
  75.       <div class="row">
  76.         <label for="teeth1">Input pulley teeth (12–24)</label>
  77.         <input id="teeth1" type="number" min="12" max="24" step="1" value="20" />
  78.       </div>
  79.       <div class="row">
  80.         <label for="teeth2">Output pulley teeth (24–60)</label>
  81.         <input id="teeth2" type="number" min="24" max="60" step="1" value="40" />
  82.       </div>
  83.       <div class="row">
  84.         <label for="centerC">Center distance C (mm)</label>
  85.         <input id="centerC" type="range" min="0" max="100" step="1" value="0" />
  86.       </div>
  87.  
  88.       <div class="kpis">
  89.         <div class="kpi">
  90.           <div class="label">Pitch diameter — input / output (mm)</div>
  91.           <div class="value" id="pitchDiam"></div>
  92.         </div>
  93.         <div class="kpi">
  94.           <div class="label">Gear ratio (out ÷ in)</div>
  95.           <div class="value" id="ratio"></div>
  96.         </div>
  97.         <div class="kpi">
  98.           <div class="label">Minimum center distance C<sub>min</sub> (mm)</div>
  99.           <div class="value" id="cmin"></div>
  100.         </div>
  101.         <div class="kpi">
  102.           <div class="label">Corresponding belt length L(C<sub>min</sub>) (mm)</div>
  103.           <div class="value" id="lmin"></div>
  104.         </div>
  105.       </div>
  106.  
  107.       <div class="kpis" style="margin-top:6px">
  108.         <div class="kpi" style="grid-column: span 2;">
  109.           <div class="label" style="font-weight:700">Current Properties</div>
  110.           <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-top:8px;">
  111.             <div>
  112.               <div class="label">Current center distance C (mm)</div>
  113.               <div class="value" id="cnow"></div>
  114.             </div>
  115.             <div>
  116.               <div class="label">Current belt length L(C) (mm)</div>
  117.               <div class="value" id="lcurr"></div>
  118.             </div>
  119.           </div>
  120.         </div>
  121.         <div class="kpi">
  122.           <div class="label">Output speed (rev/s)</div>
  123.           <div class="value" id="ospeed"></div>
  124.         </div>
  125.       </div>
  126.  
  127.       <p class="note">
  128.         <strong>Notes</strong>: Pitch diameter D = (teeth × pitch)/π. Open-belt length is approximated along pitch circles by
  129.         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.
  130.       </p>
  131.       <p class="small">This is a geometric, pitch-line visualization (not a tooth-level interference check). GT2 pitch = 2.00 mm.</p>
  132.     </section>
  133.  
  134.     <section class="panel sim" aria-label="simulation">
  135.       <canvas id="view" width="1280" height="800" aria-label="belt simulation canvas"></canvas>
  136.       <div class="legend">
  137.         <span class="chip"><span class="dot belt"></span> Belt (animated dash at GT2 pitch)</span>
  138.         <span class="chip"><span class="dot pulley"></span> Pitch circles (pulleys)</span>
  139.         <span class="chip"><span class="dot pitch"></span> Ticks every 10 teeth</span>
  140.         <span class="chip"><span class="dot vector"></span> Center vector C</span>
  141.       </div>
  142.     </section>
  143.   </div>
  144.  
  145.   <script>
  146.     // ----- Constants -----
  147.     const PITCH = 2.0; // mm (GT2)
  148.     const INPUT_RPS = 1.0; // revs per second for input pulley
  149.  
  150.     // Elements
  151.     const teeth1El = document.getElementById('teeth1');
  152.     const teeth2El = document.getElementById('teeth2');
  153.     const centerCEl  = document.getElementById('centerC');
  154.  
  155.     const pitchDiamEl = document.getElementById('pitchDiam');
  156.     const ratioEl     = document.getElementById('ratio');
  157.     const cminEl      = document.getElementById('cmin');
  158.     const lminEl      = document.getElementById('lmin');
  159.     const cnowEl      = document.getElementById('cnow');
  160.     const lcurrEl     = document.getElementById('lcurr');
  161.     const ospeedEl    = document.getElementById('ospeed');
  162.  
  163.     const canvas = document.getElementById('view');
  164.     const ctx = canvas.getContext('2d');
  165.  
  166.     // View state
  167.     const state = {
  168.       N1: 20, N2: 40,
  169.       D1: 0, D2: 0, // pitch diameters (mm)
  170.       r1: 0, r2: 0, // mm
  171.       Cmin: 0,      // mm
  172.       C: 0,         // current center distance (mm)
  173.       Lmin: 0,      // belt length at Cmin (mm)
  174.       scale: 2.0,   // px per mm (auto-fit later)
  175.       angle1: 0,    // radians, input pulley
  176.       angle2: 0,    // radians, output pulley
  177.       lastT: performance.now()
  178.     };
  179.  
  180.     function clampInt(v, lo, hi) {
  181.       v = Math.round(v);
  182.       return Math.max(lo, Math.min(hi, v));
  183.     }
  184.  
  185.     function fmt1(x) { return (Math.round(x*10)/10).toFixed(1); } // mm to 0.1
  186.     function fmt2(x) { return (Math.round(x*100)/100).toFixed(2); } // ratios/speeds
  187.  
  188.     function computeGeometry() {
  189.       state.N1 = clampInt(parseInt(teeth1El.value||20,10), 12, 24);
  190.       state.N2 = clampInt(parseInt(teeth2El.value||40,10), 24, 60);
  191.       teeth1El.value = state.N1; teeth2El.value = state.N2;
  192.  
  193.       state.D1 = (state.N1 * PITCH) / Math.PI; // mm
  194.       state.D2 = (state.N2 * PITCH) / Math.PI; // mm
  195.       state.r1 = state.D1 / 2; state.r2 = state.D2 / 2;
  196.  
  197.       state.Cmin = (state.D1 + state.D2) / 2; // mm
  198.       state.Lmin = 2*state.Cmin + (Math.PI/2)*(state.D1 + state.D2) + Math.pow((state.D2 - state.D1),2)/(4*state.Cmin);
  199.  
  200.       // Update slider bounds to [Cmin, Cmin+100], preserve current value within new range
  201.       const minC = state.Cmin;
  202.       const maxC = state.Cmin + 100; // mm
  203.       centerCEl.min = Math.floor(minC); // slider in mm integer steps
  204.       centerCEl.max = Math.ceil(maxC);
  205.       if (!centerCEl.dataset.init) { centerCEl.value = Math.round(minC); centerCEl.dataset.init = '1'; }
  206.       let Cslider = parseFloat(centerCEl.value);
  207.       if (isNaN(Cslider)) Cslider = minC;
  208.       Cslider = Math.max(minC, Math.min(maxC, Cslider));
  209.       centerCEl.value = Math.round(Cslider);
  210.       state.C = Cslider;
  211.  
  212.       // Auto-fit scale to canvas nicely
  213.       const margin = 60; // px
  214.       const spanMM = state.C + state.r1 + state.r2; // total span in mm across
  215.       const neededPx = canvas.width - 2*margin;
  216.       state.scale = Math.max(1.2, neededPx / spanMM); // px per mm
  217.  
  218.       // UI outputs (rounded per request)
  219.       pitchDiamEl.textContent = `${fmt1(state.D1)} / ${fmt1(state.D2)}`;
  220.       ratioEl.textContent = fmt2(state.N2 / state.N1) + ' : 1';
  221.       cminEl.textContent = fmt1(state.Cmin);
  222.       lminEl.textContent = fmt1( round2mm(state.Lmin) );
  223.       cnowEl.textContent = fmt1(state.C);
  224.       const Lcurr = 2*state.C + (Math.PI/2)*(state.D1 + state.D2) + Math.pow((state.D2 - state.D1),2)/(4*state.C);
  225.       lcurrEl.textContent = fmt1( round2mm(Lcurr) );
  226.       const outRPS = INPUT_RPS * (state.N1 / state.N2);
  227.       ospeedEl.textContent = fmt2(outRPS);
  228.     }
  229.  
  230.     function round2mm(x){ return Math.round(x/2)*2; }
  231.     function beltLen(C, D1, D2){ return 2*C + (Math.PI/2)*(D1 + D2) + Math.pow((D2 - D1),2)/(4*C); }
  232.     // Find nearest C (within ±0.6 mm search window) that yields belt length in 2 mm increments
  233.     function snapCtoBelt2mm(Cguess){
  234.       const step = 0.1; // mm resolution for search
  235.       let bestC = Cguess, bestErr = 1e9;
  236.       const min = Math.max(state.Cmin, Cguess - 0.6);
  237.       const max = Math.min(state.Cmin + 100, Cguess + 0.6);
  238.       for (let C=min; C<=max + 1e-9; C+=step){
  239.        const L = beltLen(C, state.D1, state.D2);
  240.        const err = Math.abs(L - round2mm(L));
  241.        if (err < bestErr){ bestErr = err; bestC = C; }
  242.      }
  243.      // reflect snapped value back to slider (integer mm) while staying inside bounds
  244.      centerCEl.value = String(Math.round(bestC));
  245.      return bestC;
  246.    }
  247.  
  248.    // Compute upper/lower external tangent points via line construction
  249.    function tangentPoints(r1, r2, C, upper=true) {
  250.      const denom = Math.max(1e-6, Math.sqrt(Math.max(1e-9, C*C - (r2 - r1)*(r2 - r1))));
  251.      const m = ((r2 - r1) / denom) * (upper ? +1 : -1);
  252.      const s = Math.sqrt(1 + m*m);
  253.      const b = (upper ? +1 : -1) * r1 * s;
  254.      const nux = -m / s; const nuy = 1 / s;
  255.      const p1x = r1 * (upper ? nux : -nux);
  256.      const p1y = r1 * (upper ? nuy : -nuy);
  257.      const p2x = C - r2 * (upper ? nux : -nux);
  258.      const p2y =     r2 * (upper ? nuy : -nuy);
  259.      return { p1: {x:p1x, y:p1y}, p2: {x:p2x, y:p2y}, m, b };
  260.    }
  261.  
  262.    function draw() {
  263.      const { r1, r2, C, scale } = state;
  264.      const W = canvas.width, H = canvas.height;
  265.      const margin = 60;
  266.  
  267.      // World-to-screen: place center of input pulley at (margin, H/2)
  268.      const origin = { x: margin + r1*scale, y: H/2 };
  269.      const center1 = { x: origin.x, y: origin.y };
  270.      const center2 = { x: origin.x + C*scale, y: origin.y };
  271.  
  272.      // Background
  273.      ctx.clearRect(0,0,W,H);
  274.      const grad = ctx.createLinearGradient(0,0,0,H);
  275.      grad.addColorStop(0,'#0b1220'); grad.addColorStop(1,'#0f172a');
  276.      ctx.fillStyle = grad; ctx.fillRect(0,0,W,H);
  277.  
  278.      // Center vector with arrow
  279.      ctx.save();
  280.      ctx.strokeStyle = '#f59e0b';
  281.      ctx.lineWidth = 4;
  282.      ctx.setLineDash([]);
  283.      ctx.beginPath();
  284.      ctx.moveTo(center1.x, center1.y);
  285.      ctx.lineTo(center2.x, center2.y);
  286.      ctx.stroke();
  287.      // arrow head at center2
  288.      const ah = 14; const aw = 8;
  289.      ctx.beginPath();
  290.      ctx.moveTo(center2.x, center2.y);
  291.      ctx.lineTo(center2.x - ah, center2.y - aw);
  292.      ctx.lineTo(center2.x - ah, center2.y + aw);
  293.      ctx.closePath();
  294.      ctx.fillStyle = '#f59e0b';
  295.      ctx.fill();
  296.      // label C
  297.      ctx.fillStyle = '#f59e0b';
  298.      ctx.font = '16px ui-sans-serif, system-ui';
  299.      ctx.fillText(`C = ${fmt1(state.C)} mm`, (center1.x+center2.x)/2 - 30, center1.y - 10);
  300.      ctx.restore();
  301.  
  302.      // Compute tangent points (upper and lower)
  303.      const up = tangentPoints(r1, r2, C, true);
  304.      const lo = tangentPoints(r1, r2, C, false);
  305.  
  306.      // Helper to convert world(mm) to screen(px)
  307.      const mm2px = (pt) => ({ x: origin.x + pt.x*scale, y: origin.y - pt.y*scale });
  308.       const sP1u = mm2px(up.p1), sP2u = mm2px(up.p2);
  309.       const sP1l = mm2px(lo.p1), sP2l = mm2px(lo.p2);
  310.       const sC1  = mm2px({x:0,y:0});
  311.       const sC2  = mm2px({x:C,y:0});
  312.  
  313.       // Belt with animated dashes
  314.       ctx.save();
  315.       ctx.lineWidth = 6; // px
  316.       ctx.strokeStyle = '#22d3ee';
  317.       const dashPx = Math.max(6, state.scale * PITCH);
  318.       ctx.setLineDash([dashPx*0.5, dashPx*0.5]);
  319.       const omega1 = 2*Math.PI * INPUT_RPS; // rad/s
  320.       const vmm = omega1 * r1; // mm/s
  321.       const vpx = vmm * state.scale;
  322.       const now = performance.now();
  323.       const dt = (now - state.lastT) / 1000;
  324.       state.lastT = now;
  325.       state.angle1 = (state.angle1 + omega1*dt) % (Math.PI*2);
  326.       const omega2 = omega1 * (state.N1 / state.N2);
  327.       state.angle2 = (state.angle2 + omega2*dt) % (Math.PI*2);
  328.       ctx.lineDashOffset = (ctx.lineDashOffset - vpx*dt) % (dashPx*1000);
  329.  
  330.       function angleOf(pt, center) { return Math.atan2(center.y - pt.y, pt.x - center.x); }
  331.       const a1u = angleOf(sP1u, sC1);
  332.       const a1l = angleOf(sP1l, sC1);
  333.       const a2u = angleOf(sP2u, sC2);
  334.       const a2l = angleOf(sP2l, sC2);
  335.  
  336.       // Draw belt: upper span
  337.       ctx.beginPath();
  338.       ctx.moveTo(sP1u.x, sP1u.y);
  339.       ctx.lineTo(sP2u.x, sP2u.y);
  340.       ctx.stroke();
  341.  
  342.       // Arc around output pulley from upper to lower (clockwise)
  343.       ctx.beginPath();
  344.       ctx.arc(sC2.x, sC2.y, r2*scale, a2u, a2l, true);
  345.       ctx.stroke();
  346.  
  347.       // Lower span
  348.       ctx.beginPath();
  349.       ctx.moveTo(sP2l.x, sP2l.y);
  350.       ctx.lineTo(sP1l.x, sP1l.y);
  351.       ctx.stroke();
  352.  
  353.       // Arc around input pulley from upper to lower on LEFT side
  354.       ctx.beginPath();
  355.       ctx.arc(sC1.x, sC1.y, r1*scale, a1u, a1l, false);
  356.       ctx.stroke();
  357.       ctx.restore();
  358.  
  359.       // Pulleys — pitch circles with thicker ticks (every 10 teeth)
  360.       function drawPulley(center, r, N, angle) {
  361.         ctx.save();
  362.         // pitch circle
  363.         ctx.strokeStyle = '#a78bfa';
  364.         ctx.lineWidth = 4;
  365.         ctx.beginPath(); ctx.arc(center.x, center.y, r*scale, 0, Math.PI*2); ctx.stroke();
  366.         // hub
  367.         ctx.lineWidth = 8; ctx.globalAlpha = .2;
  368.         ctx.beginPath(); ctx.arc(center.x, center.y, Math.max(4, r*scale*0.2), 0, Math.PI*2); ctx.stroke();
  369.         ctx.globalAlpha = 1;
  370.         // ticks every 10 teeth (thicker & longer)
  371.        const step = 10; const tickCount = Math.max(1, Math.floor(N / step));
  372.         for (let i=0; i<tickCount; i++) {
  373.          const t = (i * step) / N; // 0..1
  374.          const a = angle + t * Math.PI*2;
  375.          const x1 = center.x + Math.cos(a) * (r*scale);
  376.          const y1 = center.y + Math.sin(a) * (r*scale);
  377.          const x2 = center.x + Math.cos(a) * (r*scale + 16);
  378.          const y2 = center.y + Math.sin(a) * (r*scale + 16);
  379.          ctx.strokeStyle = '#34d399';
  380.          ctx.lineWidth = 4;  // thicker
  381.          ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
  382.        }
  383.        ctx.restore();
  384.      }
  385.  
  386.      drawPulley(sC1, r1, state.N1, state.angle1);
  387.      drawPulley(sC2, r2, state.N2, state.angle2);
  388.  
  389.      requestAnimationFrame(draw);
  390.    }
  391.  
  392.    // Event handling
  393.    function onChange() { computeGeometry(); }
  394.  
  395.    teeth1El.addEventListener('input', onChange);
  396.    teeth2El.addEventListener('input', onChange);
  397.    centerCEl.addEventListener('input', onChange);
  398.  
  399.    // Init & kick off
  400.    computeGeometry();
  401.    state.lastT = performance.now();
  402.    requestAnimationFrame(draw);
  403.  </script>
  404. </body>
  405. </html>
  406.  
Advertisement
Add Comment
Please, Sign In to add comment