HF-Bike / index.html
AntDX316
updated to index.html
d80df21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Bike Geometry Visualizer</title>
<style>
html, body {
height: 100%; /* Ensure body takes full height */
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
color: #333;
}
body {
display: flex;
flex-direction: column; /* Stack title above main content */
padding: 20px;
box-sizing: border-box;
min-height: 100vh;
}
h1 {
text-align: center;
margin-bottom: 10px;
flex-shrink: 0; /* Prevent title from shrinking */
color: #2c3e50;
font-weight: 600;
font-size: 2.2rem;
text-shadow: 1px 1px 1px rgba(0,0,0,0.05);
}
.tool-description {
text-align: center;
margin: 0 auto 25px auto;
max-width: 600px;
color: #7f8c8d;
font-size: 1.1rem;
line-height: 1.5;
}
/* Main layout container */
#main-layout {
display: flex;
flex-direction: row; /* Arrange children side-by-side */
width: 100%;
flex-grow: 1; /* Allow this container to fill remaining vertical space */
gap: 30px; /* Increased space between SVG and controls */
align-items: flex-start; /* Align items to the top */
max-width: 1800px;
margin: 0 auto;
}
/* Bike container */
#bike-container {
flex: 1 1 65%; /* Grow, shrink, basis 65% */
max-width: 1200px; /* Max width for SVG */
border: none;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
padding: 20px;
align-self: stretch; /* Make it stretch to the height of the flex container if needed */
display: flex; /* To center SVG if it's smaller */
justify-content: center;
align-items: center;
transition: all 0.3s ease;
}
#bike-container:hover {
box-shadow: 0 12px 32px rgba(0,0,0,0.12);
}
svg {
display: block;
width: 100%;
height: auto; /* Maintain aspect ratio */
max-height: 95%; /* Prevent SVG overflow if container is constrained */
}
/* Controls container */
#controls {
flex: 0 0 380px; /* Don't grow, don't shrink, fixed basis */
padding: 25px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
/* Layout within controls */
display: grid;
grid-template-columns: auto 1fr auto; /* Label | Slider | Value */
gap: 14px 18px;
align-items: center;
align-content: start; /* Prevent items stretching vertically */
/* Make controls scrollable if they exceed viewport height */
max-height: calc(100vh - 120px);
overflow-y: auto;
overflow-x: hidden; /* Hide horizontal scrollbar */
scrollbar-width: thin;
scrollbar-color: #ccc #f8f9fa;
}
#controls::-webkit-scrollbar {
width: 8px;
}
#controls::-webkit-scrollbar-track {
background: #f8f9fa;
}
#controls::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 10px;
border: 2px solid #f8f9fa;
}
/* SVG Styles - Enhanced for visual appeal */
.frame-tube {
stroke: #2980b9;
stroke-width: 8;
stroke-linecap: round;
filter: drop-shadow(0px 2px 3px rgba(0,0,0,0.25));
transition: stroke 0.3s ease;
}
/* Different frame tube colors for visual interest */
#seat-tube { stroke: #2980b9; }
#top-tube { stroke: #3498db; }
#down-tube { stroke: #2c3e50; }
#chainstay { stroke: #34495e; }
#seatstay { stroke: #3498db; }
.wheel {
stroke: #34495e;
stroke-width: 6;
fill: none;
filter: drop-shadow(0px 3px 4px rgba(0,0,0,0.2));
transition: all 0.3s ease;
}
.spokes {
stroke: #bdc3c7;
stroke-width: 1.2;
transition: stroke 0.3s ease;
}
.component {
stroke: #7f8c8d;
stroke-width: 5;
stroke-linecap: round;
filter: drop-shadow(0px 2px 2px rgba(0,0,0,0.15));
transition: stroke 0.3s ease;
}
/* Component-specific styling */
#fork { stroke: #8e44ad; }
#head-tube { stroke: #16a085; }
#stem { stroke: #d35400; }
#handlebar { stroke: #c0392b; }
#seatpost { stroke: #27ae60; }
.headset-spacer {
stroke: #95a5a6;
stroke-width: 9;
stroke-linecap: butt;
}
.drivetrain {
stroke: #7f8c8d;
stroke-width: 5;
fill: none;
stroke-linecap: round;
transition: stroke 0.3s ease;
}
.pedal {
fill: #2c3e50;
transition: fill 0.3s ease;
}
.joint {
fill: #e74c3c;
stroke: none;
filter: drop-shadow(0px 1px 1px rgba(0,0,0,0.2));
}
.axle {
fill: #3498db;
stroke: none;
filter: drop-shadow(0px 1px 1px rgba(0,0,0,0.2));
}
.saddle {
fill: #2c3e50;
filter: drop-shadow(0px 2px 3px rgba(0,0,0,0.25));
transition: fill 0.3s ease;
}
/* Enhanced Control Styling */
label {
text-align: right;
font-size: 0.9em;
color: #555;
font-weight: 500;
}
input[type="range"] {
width: 100%;
cursor: grab;
-webkit-appearance: none;
background: transparent;
margin: 7px 0;
}
input[type="range"]:focus {
outline: none;
}
/* Webkit (Chrome, Safari, Edge) styling */
input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 6px;
cursor: pointer;
background: #e0e0e0;
border-radius: 3px;
border: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 18px;
width: 18px;
border-radius: 50%;
background: #3a6ea5;
cursor: grab;
margin-top: -6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
transition: background 0.15s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: #2980b9;
}
input[type="range"]::-webkit-slider-thumb:active {
cursor: grabbing;
background: #2471a3;
}
/* Firefox styling */
input[type="range"]::-moz-range-track {
width: 100%;
height: 6px;
cursor: pointer;
background: #e0e0e0;
border-radius: 3px;
border: none;
}
input[type="range"]::-moz-range-thumb {
height: 18px;
width: 18px;
border-radius: 50%;
background: #3a6ea5;
cursor: grab;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
border: none;
}
input[type="range"]:active::-moz-range-thumb {
cursor: grabbing;
background: #2471a3;
}
.value-display {
font-weight: 600;
min-width: 60px;
text-align: left;
font-family: monospace;
font-size: 0.95em;
background: #f5f7fa;
padding: 4px 8px;
border-radius: 4px;
color: #34495e;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
/* Section headers for control groups */
.control-section {
grid-column: 1 / -1;
margin-top: 10px;
margin-bottom: 5px;
padding-bottom: 5px;
border-bottom: 1px solid #e0e0e0;
font-weight: 600;
color: #2c3e50;
font-size: 1.05em;
}
/* Make the first section header not have the top margin */
.control-section:first-of-type {
margin-top: 0;
}
/* End of styling */
input[type="range"]:active {
cursor: grabbing;
}
.value-display {
font-weight: bold;
min-width: 60px;
text-align: left;
font-family: 'Consolas', monospace;
font-size: 0.9em;
color: #2c3e50;
background: #f5f5f5;
padding: 3px 6px;
border-radius: 4px;
}
/* Media Query for responsive design */
@media (max-width: 1000px) {
body {
padding: 15px;
}
h1 {
font-size: 1.8rem;
margin-bottom: 15px;
}
#main-layout {
flex-direction: column; /* Stack SVG and controls vertically */
align-items: center; /* Center items horizontally */
height: auto; /* Let content define height */
gap: 20px;
}
#bike-container {
width: 100%;
flex-basis: auto; /* Reset basis */
margin-bottom: 10px; /* Add space below SVG */
align-self: auto; /* Reset self alignment */
max-height: 60vh; /* Limit height on small screens */
}
#controls {
width: 100%;
max-width: 600px; /* Wider controls when stacked */
flex-basis: auto; /* Reset basis */
max-height: none; /* Don't limit height */
padding: 20px;
gap: 12px 15px; /* Slightly reduce gap on small screens */
}
}
/* Extra small screens */
@media (max-width: 600px) {
body {
padding: 10px;
}
h1 {
font-size: 1.5rem;
margin-bottom: 12px;
}
#bike-container {
padding: 10px;
}
#controls {
padding: 15px;
gap: 10px 10px;
}
label {
font-size: 0.8em;
}
.value-display {
font-size: 0.8em;
min-width: 50px;
}
}
/* Remove all footer styling as it's no longer needed */
</style>
</head>
<body>
<h1>Interactive Bike Geometry Visualizer</h1>
<p class="tool-description">Adjust the sliders to customize your bike's geometry and see changes in real-time.</p>
<!-- NEW: Main layout wrapper -->
<div id="main-layout">
<div id="bike-container">
<svg id="bike-svg" viewBox="-425 -225 2000 1500" preserveAspectRatio="xMidYMid meet">
<!-- Groups for organization -->
<g id="bike-elements">
<!-- Wheels -->
<g id="rear-wheel-group">
<circle id="rear-wheel" class="wheel" r="340" />
<line class="spokes" /> <line class="spokes" /> <line class="spokes" /> <line class="spokes" />
<circle id="rear-axle" class="axle" r="8" />
</g>
<g id="front-wheel-group">
<circle id="front-wheel" class="wheel" r="340" />
<line class="spokes" /> <line class="spokes" /> <line class="spokes" /> <line class="spokes" />
<circle id="front-axle" class="axle" r="8" />
</g>
<!-- Frame -->
<g id="frame">
<line id="seat-tube" class="frame-tube" />
<line id="top-tube" class="frame-tube" />
<line id="down-tube" class="frame-tube" />
<line id="chainstay" class="frame-tube" />
<line id="seatstay" class="frame-tube" />
</g>
<!-- Components -->
<g id="components">
<line id="fork" class="component" />
<line id="head-tube" class="component" stroke-width="9"/>
<line id="headset-spacers" class="headset-spacer" /> <!-- Visual for stack -->
<line id="stem" class="component" />
<line id="handlebar" class="component" />
<line id="seatpost" class="component" />
<g id="saddle-group">
<path id="saddle" class="saddle" d="M-35,0 Q0,-25 35,0 L 45,-15 L -45,-15 Z" />
</g>
<!-- Drivetrain -->
<line id="crank-arm" class="drivetrain" />
<circle id="pedal" class="pedal" r="6" />
</g>
<!-- Joints (drawn last to be on top) -->
<g id="joints">
<circle id="bottom-bracket" class="joint" r="12" />
<circle id="seat-cluster" class="joint" r="8" />
<circle id="head-tube-top" class="joint" r="8" />
<circle id="head-tube-bottom" class="joint" r="8" />
<circle id="stem-clamp" class="joint" r="5" /> <!-- Where stem clamps bar -->
</g>
</g>
</svg>
</div>
<div id="controls">
<!-- Geometry -->
<div class="control-section">Frame Geometry</div>
<label for="wheelRadius">Wheel Radius:</label>
<input type="range" id="wheelRadius" min="250" max="400" value="340" step="5">
<span class="value-display" id="wheelRadiusValue">340mm</span>
<label for="seatTubeLength">Seat Tube (C-T):</label>
<input type="range" id="seatTubeLength" min="350" max="650" value="520" step="10">
<span class="value-display" id="seatTubeLengthValue">520mm</span>
<label for="effectiveTopTube">Eff. Top Tube:</label>
<input type="range" id="effectiveTopTube" min="450" max="650" value="550" step="10">
<span class="value-display" id="effectiveTopTubeValue">550mm</span>
<label for="chainstayLength">Chainstay:</label>
<input type="range" id="chainstayLength" min="380" max="480" value="425" step="5">
<span class="value-display" id="chainstayLengthValue">425mm</span>
<label for="headTubeAngle">Head Angle:</label>
<input type="range" id="headTubeAngle" min="65" max="78" value="72" step="0.5">
<span class="value-display" id="headTubeAngleValue">72.0°</span>
<label for="seatTubeAngle">Seat Angle:</label>
<input type="range" id="seatTubeAngle" min="68" max="78" value="73.5" step="0.5">
<span class="value-display" id="seatTubeAngleValue">73.5°</span>
<label for="bbDrop">BB Drop:</label>
<input type="range" id="bbDrop" min="50" max="90" value="70" step="2">
<span class="value-display" id="bbDropValue">70mm</span>
<label for="headTubeLength">Head Tube:</label>
<input type="range" id="headTubeLength" min="80" max="220" value="140" step="5">
<span class="value-display" id="headTubeLengthValue">140mm</span>
<!-- Fork/Front End -->
<div class="control-section">Front End</div>
<label for="forkLength">Fork (Axle-Crown):</label>
<input type="range" id="forkLength" min="350" max="500" value="400" step="5">
<span class="value-display" id="forkLengthValue">400mm</span>
<label for="forkRake">Fork Rake:</label>
<input type="range" id="forkRake" min="30" max="65" value="45" step="1">
<span class="value-display" id="forkRakeValue">45mm</span>
<!-- Cockpit -->
<div class="control-section">Cockpit</div>
<label for="stemStackHeight">Stem Stack:</label>
<input type="range" id="stemStackHeight" min="0" max="50" value="10" step="2">
<span class="value-display" id="stemStackHeightValue">10mm</span>
<label for="stemLength">Stem Length:</label>
<input type="range" id="stemLength" min="50" max="140" value="90" step="5">
<span class="value-display" id="stemLengthValue">90mm</span>
<label for="stemAngle">Stem Angle:</label>
<input type="range" id="stemAngle" min="-20" max="30" value="6" step="1">
<span class="value-display" id="stemAngleValue"></span>
<label for="handlebarWidth">Handlebar Width:</label>
<input type="range" id="handlebarWidth" min="360" max="800" value="420" step="10">
<span class="value-display" id="handlebarWidthValue">420mm</span>
<label for="handlebarRise">Handlebar Rise:</label>
<input type="range" id="handlebarRise" min="-30" max="50" value="15" step="5">
<span class="value-display" id="handlebarRiseValue">15mm</span>
<!-- Seat -->
<div class="control-section">Saddle & Seatpost</div>
<label for="seatpostExposure">Seatpost Exposure:</label>
<input type="range" id="seatpostExposure" min="50" max="300" value="150" step="10">
<span class="value-display" id="seatpostExposureValue">150mm</span>
<label for="saddleSetback">Saddle Setback:</label>
<input type="range" id="saddleSetback" min="-10" max="40" value="20" step="2">
<span class="value-display" id="saddleSetbackValue">20mm</span>
<!-- Drivetrain -->
<div class="control-section">Drivetrain</div>
<label for="crankLength">Crank Length:</label>
<input type="range" id="crankLength" min="150" max="180" value="172.5" step="2.5">
<span class="value-display" id="crankLengthValue">172.5mm</span>
</div> <!-- End controls -->
</div> <!-- End main-layout -->
<!-- Footer removed as requested -->
<script>
// --- JavaScript remains exactly the same as before ---
const svg = document.getElementById('bike-svg');
const bikeElements = document.getElementById('bike-elements');
// --- DOM Element References ---
const rearWheel = document.getElementById('rear-wheel');
const frontWheel = document.getElementById('front-wheel');
const rearAxleCircle = document.getElementById('rear-axle');
const frontAxleCircle = document.getElementById('front-axle');
const seatTubeLine = document.getElementById('seat-tube');
const topTubeLine = document.getElementById('top-tube');
const downTubeLine = document.getElementById('down-tube');
const chainstayLine = document.getElementById('chainstay');
const seatstayLine = document.getElementById('seatstay');
const forkLine = document.getElementById('fork');
const headTubeLine = document.getElementById('head-tube');
const headsetSpacersLine = document.getElementById('headset-spacers');
const stemLine = document.getElementById('stem');
const handlebarLine = document.getElementById('handlebar');
const seatpostLine = document.getElementById('seatpost');
const saddleGroup = document.getElementById('saddle-group');
const saddlePath = document.getElementById('saddle'); // Path itself for potential scaling if needed
const crankArmLine = document.getElementById('crank-arm');
const pedalCircle = document.getElementById('pedal');
const bbCircle = document.getElementById('bottom-bracket');
const seatClusterCircle = document.getElementById('seat-cluster');
const headTubeTopCircle = document.getElementById('head-tube-top');
const headTubeBottomCircle = document.getElementById('head-tube-bottom');
const stemClampCircle = document.getElementById('stem-clamp');
const rearWheelGroup = document.getElementById('rear-wheel-group');
const frontWheelGroup = document.getElementById('front-wheel-group');
const rearSpokes = rearWheelGroup.querySelectorAll('.spokes');
const frontSpokes = frontWheelGroup.querySelectorAll('.spokes');
// --- Input Controls ---
const controls = {
wheelRadius: document.getElementById('wheelRadius'),
seatTubeLength: document.getElementById('seatTubeLength'),
effectiveTopTube: document.getElementById('effectiveTopTube'),
chainstayLength: document.getElementById('chainstayLength'),
headTubeAngle: document.getElementById('headTubeAngle'),
seatTubeAngle: document.getElementById('seatTubeAngle'),
bbDrop: document.getElementById('bbDrop'),
forkLength: document.getElementById('forkLength'),
forkRake: document.getElementById('forkRake'),
headTubeLength: document.getElementById('headTubeLength'),
stemStackHeight: document.getElementById('stemStackHeight'),
stemLength: document.getElementById('stemLength'),
stemAngle: document.getElementById('stemAngle'),
handlebarWidth: document.getElementById('handlebarWidth'),
handlebarRise: document.getElementById('handlebarRise'),
seatpostExposure: document.getElementById('seatpostExposure'),
saddleSetback: document.getElementById('saddleSetback'),
crankLength: document.getElementById('crankLength'),
};
// --- Value Displays ---
const displays = { // Map IDs to display elements
wheelRadius: document.getElementById('wheelRadiusValue'),
seatTubeLength: document.getElementById('seatTubeLengthValue'),
effectiveTopTube: document.getElementById('effectiveTopTubeValue'),
chainstayLength: document.getElementById('chainstayLengthValue'),
headTubeAngle: document.getElementById('headTubeAngleValue'),
seatTubeAngle: document.getElementById('seatTubeAngleValue'),
bbDrop: document.getElementById('bbDropValue'),
forkLength: document.getElementById('forkLengthValue'),
forkRake: document.getElementById('forkRakeValue'),
headTubeLength: document.getElementById('headTubeLengthValue'),
stemStackHeight: document.getElementById('stemStackHeightValue'),
stemLength: document.getElementById('stemLengthValue'),
stemAngle: document.getElementById('stemAngleValue'),
handlebarWidth: document.getElementById('handlebarWidthValue'),
handlebarRise: document.getElementById('handlebarRiseValue'),
seatpostExposure: document.getElementById('seatpostExposureValue'),
saddleSetback: document.getElementById('saddleSetbackValue'),
crankLength: document.getElementById('crankLengthValue'),
};
// --- Constants ---
const SVG_OFFSET_X = 100; // Minimal offset to properly center the bike
const SVG_OFFSET_Y = 550; // Centered vertically in the viewbox
const CRANK_ANGLE_DEG = 45; // Fixed angle for crank arm visualization
// --- Calculation and Update Function ---
function updateBike() {
// 1. Read values
const params = {};
for (const key in controls) {
params[key] = parseFloat(controls[key].value);
// Update display
let displayValue = params[key];
let unit = 'mm';
if (key.includes('Angle')) {
unit = '°';
displays[key].textContent = `${displayValue.toFixed(1)}${unit}`;
} else if (key === 'crankLength') {
displays[key].textContent = `${displayValue.toFixed(1)}${unit}`;
}
else {
displays[key].textContent = `${displayValue}${unit}`;
}
}
// 2. Convert angles to radians
const headAngleRad = params.headTubeAngle * Math.PI / 180;
const seatAngleRad = params.seatTubeAngle * Math.PI / 180;
const stemAngleRad = params.stemAngle * Math.PI / 180; // Angle relative to horizontal
const crankAngleRad = CRANK_ANGLE_DEG * Math.PI / 180;
// 3. Calculate Key Point Coordinates (relative to rear axle)
const rearAxle = { x: 0, y: 0 };
const bb = { x: params.chainstayLength, y: params.bbDrop };
const seatCluster = {
x: bb.x - params.seatTubeLength * Math.cos(seatAngleRad),
y: bb.y - params.seatTubeLength * Math.sin(seatAngleRad)
};
// Estimate Head Tube Top based on ETT and Seat Cluster
const ettPointOnSeatpostLine = {
x: seatCluster.x,
y: seatCluster.y // Approx horizontal from seat cluster
};
const headTubeTopInitial = { // Point on steerer axis BEFORE stack
x: ettPointOnSeatpostLine.x + params.effectiveTopTube,
y: ettPointOnSeatpostLine.y
};
// Calculate Head Tube Bottom from Initial Top based on HT Length & Angle
const headTubeBottom = {
x: headTubeTopInitial.x + params.headTubeLength * Math.cos(headAngleRad),
y: headTubeTopInitial.y + params.headTubeLength * Math.sin(headAngleRad)
};
// Front Axle: Use Fork Length (Axle-Crown) and Rake
const steererPointAxleLevel = {
x: headTubeBottom.x + params.forkLength * Math.cos(headAngleRad),
y: headTubeBottom.y + params.forkLength * Math.sin(headAngleRad)
};
const rakeAngleRad = headAngleRad + Math.PI / 2; // 90 deg offset
const rakeVector = {
x: params.forkRake * Math.cos(rakeAngleRad),
y: params.forkRake * Math.sin(rakeAngleRad)
};
const frontAxle = {
x: steererPointAxleLevel.x + rakeVector.x,
y: steererPointAxleLevel.y + rakeVector.y
};
// --- Cockpit Calculations ---
const steererVec = {
x: -Math.cos(headAngleRad), // Points up along steerer
y: -Math.sin(headAngleRad)
};
const stemStartPoint = { // Base of stem on steerer
x: headTubeTopInitial.x + params.stemStackHeight * steererVec.x,
y: headTubeTopInitial.y + params.stemStackHeight * steererVec.y
};
const stemEnd = { // Handlebar Clamp Center
x: stemStartPoint.x + params.stemLength * Math.cos(stemAngleRad),
y: stemStartPoint.y - params.stemLength * Math.sin(stemAngleRad) // Positive angle = UP (smaller Y)
};
const halfBarWidth = params.handlebarWidth / 2;
const handlebarCenter = {
x: stemEnd.x,
y: stemEnd.y - params.handlebarRise // Positive rise = UP (smaller Y)
};
const handlebarLeft = { x: handlebarCenter.x - halfBarWidth, y: handlebarCenter.y };
const handlebarRight = { x: handlebarCenter.x + halfBarWidth, y: handlebarCenter.y };
// --- Seat Calculations ---
const seatpostTopNominal = { // Point along seat tube axis
x: seatCluster.x - params.seatpostExposure * Math.cos(seatAngleRad),
y: seatCluster.y - params.seatpostExposure * Math.sin(seatAngleRad)
};
const seatpostTop = { // Apply Saddle Setback
x: seatpostTopNominal.x - params.saddleSetback, // positive = rearward = smaller X
y: seatpostTopNominal.y
};
// --- Drivetrain Calculation ---
const crankEnd = {
x: bb.x + params.crankLength * Math.cos(crankAngleRad),
y: bb.y + params.crankLength * Math.sin(crankAngleRad) // Y positive is down
};
// 4. Apply SVG Offsets
const applyOffset = (point) => ({ x: point.x + SVG_OFFSET_X, y: point.y + SVG_OFFSET_Y });
const svgRearAxle = applyOffset(rearAxle);
const svgBB = applyOffset(bb);
const svgSeatCluster = applyOffset(seatCluster);
const svgHeadTubeTopInitial = applyOffset(headTubeTopInitial); // For HT line start
const svgHeadTubeBottom = applyOffset(headTubeBottom);
const svgFrontAxle = applyOffset(frontAxle);
const svgStemStartPoint = applyOffset(stemStartPoint); // Where stem visually starts
const svgStemEnd = applyOffset(stemEnd); // Where bar clamps
const svgHandlebarLeft = applyOffset(handlebarLeft);
const svgHandlebarRight = applyOffset(handlebarRight);
const svgSeatpostTop = applyOffset(seatpostTop); // Saddle clamp position
const svgCrankEnd = applyOffset(crankEnd);
// 5. Update SVG Element Attributes
// Update Wheels & Axles
rearWheel.setAttribute('cx', svgRearAxle.x); rearWheel.setAttribute('cy', svgRearAxle.y);
rearWheel.setAttribute('r', params.wheelRadius);
rearAxleCircle.setAttribute('cx', svgRearAxle.x); rearAxleCircle.setAttribute('cy', svgRearAxle.y);
frontWheel.setAttribute('cx', svgFrontAxle.x); frontWheel.setAttribute('cy', svgFrontAxle.y);
frontWheel.setAttribute('r', params.wheelRadius);
frontAxleCircle.setAttribute('cx', svgFrontAxle.x); frontAxleCircle.setAttribute('cy', svgFrontAxle.y);
// Update Spokes
const updateSpokes = (spokes, center, radius) => {
spokes[0].setAttribute('x1', center.x - radius); spokes[0].setAttribute('y1', center.y);
spokes[0].setAttribute('x2', center.x + radius); spokes[0].setAttribute('y2', center.y);
spokes[1].setAttribute('x1', center.x); spokes[1].setAttribute('y1', center.y - radius);
spokes[1].setAttribute('x2', center.x); spokes[1].setAttribute('y2', center.y + radius);
spokes[2].setAttribute('x1', center.x - radius * 0.707); spokes[2].setAttribute('y1', center.y - radius * 0.707);
spokes[2].setAttribute('x2', center.x + radius * 0.707); spokes[2].setAttribute('y2', center.y + radius * 0.707);
spokes[3].setAttribute('x1', center.x - radius * 0.707); spokes[3].setAttribute('y1', center.y + radius * 0.707);
spokes[3].setAttribute('x2', center.x + radius * 0.707); spokes[3].setAttribute('y2', center.y - radius * 0.707);
};
updateSpokes(rearSpokes, svgRearAxle, params.wheelRadius);
updateSpokes(frontSpokes, svgFrontAxle, params.wheelRadius);
// Update Frame Tubes
seatTubeLine.setAttribute('x1', svgBB.x); seatTubeLine.setAttribute('y1', svgBB.y);
seatTubeLine.setAttribute('x2', svgSeatCluster.x); seatTubeLine.setAttribute('y2', svgSeatCluster.y);
topTubeLine.setAttribute('x1', svgSeatCluster.x); topTubeLine.setAttribute('y1', svgSeatCluster.y);
topTubeLine.setAttribute('x2', svgHeadTubeTopInitial.x); topTubeLine.setAttribute('y2', svgHeadTubeTopInitial.y); // Connects to initial HT top
downTubeLine.setAttribute('x1', svgBB.x); downTubeLine.setAttribute('y1', svgBB.y);
downTubeLine.setAttribute('x2', svgHeadTubeBottom.x); downTubeLine.setAttribute('y2', svgHeadTubeBottom.y);
chainstayLine.setAttribute('x1', svgBB.x); chainstayLine.setAttribute('y1', svgBB.y);
chainstayLine.setAttribute('x2', svgRearAxle.x); chainstayLine.setAttribute('y2', svgRearAxle.y);
seatstayLine.setAttribute('x1', svgSeatCluster.x); seatstayLine.setAttribute('y1', svgSeatCluster.y);
seatstayLine.setAttribute('x2', svgRearAxle.x); seatstayLine.setAttribute('y2', svgRearAxle.y);
// Update Components
headTubeLine.setAttribute('x1', svgHeadTubeTopInitial.x); headTubeLine.setAttribute('y1', svgHeadTubeTopInitial.y);
headTubeLine.setAttribute('x2', svgHeadTubeBottom.x); headTubeLine.setAttribute('y2', svgHeadTubeBottom.y);
forkLine.setAttribute('x1', svgHeadTubeBottom.x); forkLine.setAttribute('y1', svgHeadTubeBottom.y);
forkLine.setAttribute('x2', svgFrontAxle.x); forkLine.setAttribute('y2', svgFrontAxle.y);
// Headset Spacers
if (params.stemStackHeight > 0) {
headsetSpacersLine.setAttribute('x1', svgHeadTubeTopInitial.x); headsetSpacersLine.setAttribute('y1', svgHeadTubeTopInitial.y);
headsetSpacersLine.setAttribute('x2', svgStemStartPoint.x); headsetSpacersLine.setAttribute('y2', svgStemStartPoint.y);
headsetSpacersLine.style.display = 'inline';
} else {
headsetSpacersLine.style.display = 'none';
}
stemLine.setAttribute('x1', svgStemStartPoint.x); stemLine.setAttribute('y1', svgStemStartPoint.y);
stemLine.setAttribute('x2', svgStemEnd.x); stemLine.setAttribute('y2', svgStemEnd.y);
handlebarLine.setAttribute('x1', svgHandlebarLeft.x); handlebarLine.setAttribute('y1', svgHandlebarLeft.y);
handlebarLine.setAttribute('x2', svgHandlebarRight.x); handlebarLine.setAttribute('y2', svgHandlebarRight.y);
seatpostLine.setAttribute('x1', svgSeatCluster.x); seatpostLine.setAttribute('y1', svgSeatCluster.y);
seatpostLine.setAttribute('x2', svgSeatpostTop.x); // Use the setback-adjusted top point
seatpostLine.setAttribute('y2', svgSeatpostTop.y); // for the visible post line end
// Update Saddle Position (Group transform)
const saddleAngleDeg = (seatAngleRad * 180 / Math.PI) - 90; // Approx align with seat tube
saddleGroup.setAttribute('transform', `translate(${svgSeatpostTop.x}, ${svgSeatpostTop.y}) rotate(${saddleAngleDeg})`);
// Update Drivetrain
crankArmLine.setAttribute('x1', svgBB.x); crankArmLine.setAttribute('y1', svgBB.y);
crankArmLine.setAttribute('x2', svgCrankEnd.x); crankArmLine.setAttribute('y2', svgCrankEnd.y);
pedalCircle.setAttribute('cx', svgCrankEnd.x); pedalCircle.setAttribute('cy', svgCrankEnd.y);
// Update Joint Markers
bbCircle.setAttribute('cx', svgBB.x); bbCircle.setAttribute('cy', svgBB.y);
seatClusterCircle.setAttribute('cx', svgSeatCluster.x); seatClusterCircle.setAttribute('cy', svgSeatCluster.y);
headTubeTopCircle.setAttribute('cx', svgHeadTubeTopInitial.x); headTubeTopCircle.setAttribute('cy', svgHeadTubeTopInitial.y);
headTubeBottomCircle.setAttribute('cx', svgHeadTubeBottom.x); headTubeBottomCircle.setAttribute('cy', svgHeadTubeBottom.y);
stemClampCircle.setAttribute('cx', svgStemEnd.x); stemClampCircle.setAttribute('cy', svgStemEnd.y);
}
// --- Event Listeners ---
for (const key in controls) {
controls[key].addEventListener('input', updateBike);
}
// --- Initial Draw ---
updateBike();
// Function to reset all slider controls to default values
function resetToDefaults() {
// Reset each slider to its default value
for (const key in controls) {
controls[key].value = controls[key].defaultValue;
}
// Update the visualization
updateBike();
}
</script>
</body>
</html>