AntDX316
commited on
Commit
·
d80df21
1
Parent(s):
6b29610
updated to index.html
Browse files- index.html +845 -0
index.html
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.0">
|
| 6 |
+
<title>Interactive Bike Geometry Visualizer</title>
|
| 7 |
+
<style>
|
| 8 |
+
html, body {
|
| 9 |
+
height: 100%; /* Ensure body takes full height */
|
| 10 |
+
margin: 0;
|
| 11 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 12 |
+
background-color: #f8f9fa;
|
| 13 |
+
color: #333;
|
| 14 |
+
}
|
| 15 |
+
body {
|
| 16 |
+
display: flex;
|
| 17 |
+
flex-direction: column; /* Stack title above main content */
|
| 18 |
+
padding: 20px;
|
| 19 |
+
box-sizing: border-box;
|
| 20 |
+
min-height: 100vh;
|
| 21 |
+
}
|
| 22 |
+
h1 {
|
| 23 |
+
text-align: center;
|
| 24 |
+
margin-bottom: 10px;
|
| 25 |
+
flex-shrink: 0; /* Prevent title from shrinking */
|
| 26 |
+
color: #2c3e50;
|
| 27 |
+
font-weight: 600;
|
| 28 |
+
font-size: 2.2rem;
|
| 29 |
+
text-shadow: 1px 1px 1px rgba(0,0,0,0.05);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.tool-description {
|
| 33 |
+
text-align: center;
|
| 34 |
+
margin: 0 auto 25px auto;
|
| 35 |
+
max-width: 600px;
|
| 36 |
+
color: #7f8c8d;
|
| 37 |
+
font-size: 1.1rem;
|
| 38 |
+
line-height: 1.5;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/* Main layout container */
|
| 42 |
+
#main-layout {
|
| 43 |
+
display: flex;
|
| 44 |
+
flex-direction: row; /* Arrange children side-by-side */
|
| 45 |
+
width: 100%;
|
| 46 |
+
flex-grow: 1; /* Allow this container to fill remaining vertical space */
|
| 47 |
+
gap: 30px; /* Increased space between SVG and controls */
|
| 48 |
+
align-items: flex-start; /* Align items to the top */
|
| 49 |
+
max-width: 1800px;
|
| 50 |
+
margin: 0 auto;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* Bike container */
|
| 54 |
+
#bike-container {
|
| 55 |
+
flex: 1 1 65%; /* Grow, shrink, basis 65% */
|
| 56 |
+
max-width: 1200px; /* Max width for SVG */
|
| 57 |
+
border: none;
|
| 58 |
+
background-color: #fff;
|
| 59 |
+
border-radius: 12px;
|
| 60 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
|
| 61 |
+
padding: 20px;
|
| 62 |
+
align-self: stretch; /* Make it stretch to the height of the flex container if needed */
|
| 63 |
+
display: flex; /* To center SVG if it's smaller */
|
| 64 |
+
justify-content: center;
|
| 65 |
+
align-items: center;
|
| 66 |
+
transition: all 0.3s ease;
|
| 67 |
+
}
|
| 68 |
+
#bike-container:hover {
|
| 69 |
+
box-shadow: 0 12px 32px rgba(0,0,0,0.12);
|
| 70 |
+
}
|
| 71 |
+
svg {
|
| 72 |
+
display: block;
|
| 73 |
+
width: 100%;
|
| 74 |
+
height: auto; /* Maintain aspect ratio */
|
| 75 |
+
max-height: 95%; /* Prevent SVG overflow if container is constrained */
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* Controls container */
|
| 79 |
+
#controls {
|
| 80 |
+
flex: 0 0 380px; /* Don't grow, don't shrink, fixed basis */
|
| 81 |
+
padding: 25px;
|
| 82 |
+
background-color: #fff;
|
| 83 |
+
border-radius: 12px;
|
| 84 |
+
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
|
| 85 |
+
/* Layout within controls */
|
| 86 |
+
display: grid;
|
| 87 |
+
grid-template-columns: auto 1fr auto; /* Label | Slider | Value */
|
| 88 |
+
gap: 14px 18px;
|
| 89 |
+
align-items: center;
|
| 90 |
+
align-content: start; /* Prevent items stretching vertically */
|
| 91 |
+
/* Make controls scrollable if they exceed viewport height */
|
| 92 |
+
max-height: calc(100vh - 120px);
|
| 93 |
+
overflow-y: auto;
|
| 94 |
+
overflow-x: hidden; /* Hide horizontal scrollbar */
|
| 95 |
+
scrollbar-width: thin;
|
| 96 |
+
scrollbar-color: #ccc #f8f9fa;
|
| 97 |
+
}
|
| 98 |
+
#controls::-webkit-scrollbar {
|
| 99 |
+
width: 8px;
|
| 100 |
+
}
|
| 101 |
+
#controls::-webkit-scrollbar-track {
|
| 102 |
+
background: #f8f9fa;
|
| 103 |
+
}
|
| 104 |
+
#controls::-webkit-scrollbar-thumb {
|
| 105 |
+
background-color: #ccc;
|
| 106 |
+
border-radius: 10px;
|
| 107 |
+
border: 2px solid #f8f9fa;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* SVG Styles - Enhanced for visual appeal */
|
| 111 |
+
.frame-tube {
|
| 112 |
+
stroke: #2980b9;
|
| 113 |
+
stroke-width: 8;
|
| 114 |
+
stroke-linecap: round;
|
| 115 |
+
filter: drop-shadow(0px 2px 3px rgba(0,0,0,0.25));
|
| 116 |
+
transition: stroke 0.3s ease;
|
| 117 |
+
}
|
| 118 |
+
/* Different frame tube colors for visual interest */
|
| 119 |
+
#seat-tube { stroke: #2980b9; }
|
| 120 |
+
#top-tube { stroke: #3498db; }
|
| 121 |
+
#down-tube { stroke: #2c3e50; }
|
| 122 |
+
#chainstay { stroke: #34495e; }
|
| 123 |
+
#seatstay { stroke: #3498db; }
|
| 124 |
+
.wheel {
|
| 125 |
+
stroke: #34495e;
|
| 126 |
+
stroke-width: 6;
|
| 127 |
+
fill: none;
|
| 128 |
+
filter: drop-shadow(0px 3px 4px rgba(0,0,0,0.2));
|
| 129 |
+
transition: all 0.3s ease;
|
| 130 |
+
}
|
| 131 |
+
.spokes {
|
| 132 |
+
stroke: #bdc3c7;
|
| 133 |
+
stroke-width: 1.2;
|
| 134 |
+
transition: stroke 0.3s ease;
|
| 135 |
+
}
|
| 136 |
+
.component {
|
| 137 |
+
stroke: #7f8c8d;
|
| 138 |
+
stroke-width: 5;
|
| 139 |
+
stroke-linecap: round;
|
| 140 |
+
filter: drop-shadow(0px 2px 2px rgba(0,0,0,0.15));
|
| 141 |
+
transition: stroke 0.3s ease;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/* Component-specific styling */
|
| 145 |
+
#fork { stroke: #8e44ad; }
|
| 146 |
+
#head-tube { stroke: #16a085; }
|
| 147 |
+
#stem { stroke: #d35400; }
|
| 148 |
+
#handlebar { stroke: #c0392b; }
|
| 149 |
+
#seatpost { stroke: #27ae60; }
|
| 150 |
+
|
| 151 |
+
.headset-spacer {
|
| 152 |
+
stroke: #95a5a6;
|
| 153 |
+
stroke-width: 9;
|
| 154 |
+
stroke-linecap: butt;
|
| 155 |
+
}
|
| 156 |
+
.drivetrain {
|
| 157 |
+
stroke: #7f8c8d;
|
| 158 |
+
stroke-width: 5;
|
| 159 |
+
fill: none;
|
| 160 |
+
stroke-linecap: round;
|
| 161 |
+
transition: stroke 0.3s ease;
|
| 162 |
+
}
|
| 163 |
+
.pedal {
|
| 164 |
+
fill: #2c3e50;
|
| 165 |
+
transition: fill 0.3s ease;
|
| 166 |
+
}
|
| 167 |
+
.joint {
|
| 168 |
+
fill: #e74c3c;
|
| 169 |
+
stroke: none;
|
| 170 |
+
filter: drop-shadow(0px 1px 1px rgba(0,0,0,0.2));
|
| 171 |
+
}
|
| 172 |
+
.axle {
|
| 173 |
+
fill: #3498db;
|
| 174 |
+
stroke: none;
|
| 175 |
+
filter: drop-shadow(0px 1px 1px rgba(0,0,0,0.2));
|
| 176 |
+
}
|
| 177 |
+
.saddle {
|
| 178 |
+
fill: #2c3e50;
|
| 179 |
+
filter: drop-shadow(0px 2px 3px rgba(0,0,0,0.25));
|
| 180 |
+
transition: fill 0.3s ease;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/* Enhanced Control Styling */
|
| 184 |
+
label {
|
| 185 |
+
text-align: right;
|
| 186 |
+
font-size: 0.9em;
|
| 187 |
+
color: #555;
|
| 188 |
+
font-weight: 500;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
input[type="range"] {
|
| 192 |
+
width: 100%;
|
| 193 |
+
cursor: grab;
|
| 194 |
+
-webkit-appearance: none;
|
| 195 |
+
background: transparent;
|
| 196 |
+
margin: 7px 0;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
input[type="range"]:focus {
|
| 200 |
+
outline: none;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/* Webkit (Chrome, Safari, Edge) styling */
|
| 204 |
+
input[type="range"]::-webkit-slider-runnable-track {
|
| 205 |
+
width: 100%;
|
| 206 |
+
height: 6px;
|
| 207 |
+
cursor: pointer;
|
| 208 |
+
background: #e0e0e0;
|
| 209 |
+
border-radius: 3px;
|
| 210 |
+
border: none;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 214 |
+
-webkit-appearance: none;
|
| 215 |
+
height: 18px;
|
| 216 |
+
width: 18px;
|
| 217 |
+
border-radius: 50%;
|
| 218 |
+
background: #3a6ea5;
|
| 219 |
+
cursor: grab;
|
| 220 |
+
margin-top: -6px;
|
| 221 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
| 222 |
+
transition: background 0.15s ease;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
input[type="range"]::-webkit-slider-thumb:hover {
|
| 226 |
+
background: #2980b9;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
input[type="range"]::-webkit-slider-thumb:active {
|
| 230 |
+
cursor: grabbing;
|
| 231 |
+
background: #2471a3;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/* Firefox styling */
|
| 235 |
+
input[type="range"]::-moz-range-track {
|
| 236 |
+
width: 100%;
|
| 237 |
+
height: 6px;
|
| 238 |
+
cursor: pointer;
|
| 239 |
+
background: #e0e0e0;
|
| 240 |
+
border-radius: 3px;
|
| 241 |
+
border: none;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
input[type="range"]::-moz-range-thumb {
|
| 245 |
+
height: 18px;
|
| 246 |
+
width: 18px;
|
| 247 |
+
border-radius: 50%;
|
| 248 |
+
background: #3a6ea5;
|
| 249 |
+
cursor: grab;
|
| 250 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
| 251 |
+
border: none;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
input[type="range"]:active::-moz-range-thumb {
|
| 255 |
+
cursor: grabbing;
|
| 256 |
+
background: #2471a3;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.value-display {
|
| 260 |
+
font-weight: 600;
|
| 261 |
+
min-width: 60px;
|
| 262 |
+
text-align: left;
|
| 263 |
+
font-family: monospace;
|
| 264 |
+
font-size: 0.95em;
|
| 265 |
+
background: #f5f7fa;
|
| 266 |
+
padding: 4px 8px;
|
| 267 |
+
border-radius: 4px;
|
| 268 |
+
color: #34495e;
|
| 269 |
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* Section headers for control groups */
|
| 273 |
+
.control-section {
|
| 274 |
+
grid-column: 1 / -1;
|
| 275 |
+
margin-top: 10px;
|
| 276 |
+
margin-bottom: 5px;
|
| 277 |
+
padding-bottom: 5px;
|
| 278 |
+
border-bottom: 1px solid #e0e0e0;
|
| 279 |
+
font-weight: 600;
|
| 280 |
+
color: #2c3e50;
|
| 281 |
+
font-size: 1.05em;
|
| 282 |
+
}
|
| 283 |
+
/* Make the first section header not have the top margin */
|
| 284 |
+
.control-section:first-of-type {
|
| 285 |
+
margin-top: 0;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/* End of styling */
|
| 289 |
+
|
| 290 |
+
input[type="range"]:active {
|
| 291 |
+
cursor: grabbing;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.value-display {
|
| 295 |
+
font-weight: bold;
|
| 296 |
+
min-width: 60px;
|
| 297 |
+
text-align: left;
|
| 298 |
+
font-family: 'Consolas', monospace;
|
| 299 |
+
font-size: 0.9em;
|
| 300 |
+
color: #2c3e50;
|
| 301 |
+
background: #f5f5f5;
|
| 302 |
+
padding: 3px 6px;
|
| 303 |
+
border-radius: 4px;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* Media Query for responsive design */
|
| 307 |
+
@media (max-width: 1000px) {
|
| 308 |
+
body {
|
| 309 |
+
padding: 15px;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
h1 {
|
| 313 |
+
font-size: 1.8rem;
|
| 314 |
+
margin-bottom: 15px;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
#main-layout {
|
| 318 |
+
flex-direction: column; /* Stack SVG and controls vertically */
|
| 319 |
+
align-items: center; /* Center items horizontally */
|
| 320 |
+
height: auto; /* Let content define height */
|
| 321 |
+
gap: 20px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
#bike-container {
|
| 325 |
+
width: 100%;
|
| 326 |
+
flex-basis: auto; /* Reset basis */
|
| 327 |
+
margin-bottom: 10px; /* Add space below SVG */
|
| 328 |
+
align-self: auto; /* Reset self alignment */
|
| 329 |
+
max-height: 60vh; /* Limit height on small screens */
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
#controls {
|
| 333 |
+
width: 100%;
|
| 334 |
+
max-width: 600px; /* Wider controls when stacked */
|
| 335 |
+
flex-basis: auto; /* Reset basis */
|
| 336 |
+
max-height: none; /* Don't limit height */
|
| 337 |
+
padding: 20px;
|
| 338 |
+
gap: 12px 15px; /* Slightly reduce gap on small screens */
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
/* Extra small screens */
|
| 343 |
+
@media (max-width: 600px) {
|
| 344 |
+
body {
|
| 345 |
+
padding: 10px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
h1 {
|
| 349 |
+
font-size: 1.5rem;
|
| 350 |
+
margin-bottom: 12px;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
#bike-container {
|
| 354 |
+
padding: 10px;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
#controls {
|
| 358 |
+
padding: 15px;
|
| 359 |
+
gap: 10px 10px;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
label {
|
| 363 |
+
font-size: 0.8em;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.value-display {
|
| 367 |
+
font-size: 0.8em;
|
| 368 |
+
min-width: 50px;
|
| 369 |
+
}
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
/* Remove all footer styling as it's no longer needed */
|
| 373 |
+
</style>
|
| 374 |
+
</head>
|
| 375 |
+
<body>
|
| 376 |
+
|
| 377 |
+
<h1>Interactive Bike Geometry Visualizer</h1>
|
| 378 |
+
<p class="tool-description">Adjust the sliders to customize your bike's geometry and see changes in real-time.</p>
|
| 379 |
+
|
| 380 |
+
<!-- NEW: Main layout wrapper -->
|
| 381 |
+
<div id="main-layout">
|
| 382 |
+
|
| 383 |
+
<div id="bike-container">
|
| 384 |
+
<svg id="bike-svg" viewBox="-425 -225 2000 1500" preserveAspectRatio="xMidYMid meet">
|
| 385 |
+
<!-- Groups for organization -->
|
| 386 |
+
<g id="bike-elements">
|
| 387 |
+
<!-- Wheels -->
|
| 388 |
+
<g id="rear-wheel-group">
|
| 389 |
+
<circle id="rear-wheel" class="wheel" r="340" />
|
| 390 |
+
<line class="spokes" /> <line class="spokes" /> <line class="spokes" /> <line class="spokes" />
|
| 391 |
+
<circle id="rear-axle" class="axle" r="8" />
|
| 392 |
+
</g>
|
| 393 |
+
<g id="front-wheel-group">
|
| 394 |
+
<circle id="front-wheel" class="wheel" r="340" />
|
| 395 |
+
<line class="spokes" /> <line class="spokes" /> <line class="spokes" /> <line class="spokes" />
|
| 396 |
+
<circle id="front-axle" class="axle" r="8" />
|
| 397 |
+
</g>
|
| 398 |
+
|
| 399 |
+
<!-- Frame -->
|
| 400 |
+
<g id="frame">
|
| 401 |
+
<line id="seat-tube" class="frame-tube" />
|
| 402 |
+
<line id="top-tube" class="frame-tube" />
|
| 403 |
+
<line id="down-tube" class="frame-tube" />
|
| 404 |
+
<line id="chainstay" class="frame-tube" />
|
| 405 |
+
<line id="seatstay" class="frame-tube" />
|
| 406 |
+
</g>
|
| 407 |
+
|
| 408 |
+
<!-- Components -->
|
| 409 |
+
<g id="components">
|
| 410 |
+
<line id="fork" class="component" />
|
| 411 |
+
<line id="head-tube" class="component" stroke-width="9"/>
|
| 412 |
+
<line id="headset-spacers" class="headset-spacer" /> <!-- Visual for stack -->
|
| 413 |
+
<line id="stem" class="component" />
|
| 414 |
+
<line id="handlebar" class="component" />
|
| 415 |
+
<line id="seatpost" class="component" />
|
| 416 |
+
<g id="saddle-group">
|
| 417 |
+
<path id="saddle" class="saddle" d="M-35,0 Q0,-25 35,0 L 45,-15 L -45,-15 Z" />
|
| 418 |
+
</g>
|
| 419 |
+
<!-- Drivetrain -->
|
| 420 |
+
<line id="crank-arm" class="drivetrain" />
|
| 421 |
+
<circle id="pedal" class="pedal" r="6" />
|
| 422 |
+
</g>
|
| 423 |
+
|
| 424 |
+
<!-- Joints (drawn last to be on top) -->
|
| 425 |
+
<g id="joints">
|
| 426 |
+
<circle id="bottom-bracket" class="joint" r="12" />
|
| 427 |
+
<circle id="seat-cluster" class="joint" r="8" />
|
| 428 |
+
<circle id="head-tube-top" class="joint" r="8" />
|
| 429 |
+
<circle id="head-tube-bottom" class="joint" r="8" />
|
| 430 |
+
<circle id="stem-clamp" class="joint" r="5" /> <!-- Where stem clamps bar -->
|
| 431 |
+
</g>
|
| 432 |
+
</g>
|
| 433 |
+
</svg>
|
| 434 |
+
</div>
|
| 435 |
+
|
| 436 |
+
<div id="controls">
|
| 437 |
+
<!-- Geometry -->
|
| 438 |
+
<div class="control-section">Frame Geometry</div>
|
| 439 |
+
|
| 440 |
+
<label for="wheelRadius">Wheel Radius:</label>
|
| 441 |
+
<input type="range" id="wheelRadius" min="250" max="400" value="340" step="5">
|
| 442 |
+
<span class="value-display" id="wheelRadiusValue">340mm</span>
|
| 443 |
+
|
| 444 |
+
<label for="seatTubeLength">Seat Tube (C-T):</label>
|
| 445 |
+
<input type="range" id="seatTubeLength" min="350" max="650" value="520" step="10">
|
| 446 |
+
<span class="value-display" id="seatTubeLengthValue">520mm</span>
|
| 447 |
+
|
| 448 |
+
<label for="effectiveTopTube">Eff. Top Tube:</label>
|
| 449 |
+
<input type="range" id="effectiveTopTube" min="450" max="650" value="550" step="10">
|
| 450 |
+
<span class="value-display" id="effectiveTopTubeValue">550mm</span>
|
| 451 |
+
|
| 452 |
+
<label for="chainstayLength">Chainstay:</label>
|
| 453 |
+
<input type="range" id="chainstayLength" min="380" max="480" value="425" step="5">
|
| 454 |
+
<span class="value-display" id="chainstayLengthValue">425mm</span>
|
| 455 |
+
|
| 456 |
+
<label for="headTubeAngle">Head Angle:</label>
|
| 457 |
+
<input type="range" id="headTubeAngle" min="65" max="78" value="72" step="0.5">
|
| 458 |
+
<span class="value-display" id="headTubeAngleValue">72.0°</span>
|
| 459 |
+
|
| 460 |
+
<label for="seatTubeAngle">Seat Angle:</label>
|
| 461 |
+
<input type="range" id="seatTubeAngle" min="68" max="78" value="73.5" step="0.5">
|
| 462 |
+
<span class="value-display" id="seatTubeAngleValue">73.5°</span>
|
| 463 |
+
|
| 464 |
+
<label for="bbDrop">BB Drop:</label>
|
| 465 |
+
<input type="range" id="bbDrop" min="50" max="90" value="70" step="2">
|
| 466 |
+
<span class="value-display" id="bbDropValue">70mm</span>
|
| 467 |
+
|
| 468 |
+
<label for="headTubeLength">Head Tube:</label>
|
| 469 |
+
<input type="range" id="headTubeLength" min="80" max="220" value="140" step="5">
|
| 470 |
+
<span class="value-display" id="headTubeLengthValue">140mm</span>
|
| 471 |
+
|
| 472 |
+
<!-- Fork/Front End -->
|
| 473 |
+
<div class="control-section">Front End</div>
|
| 474 |
+
|
| 475 |
+
<label for="forkLength">Fork (Axle-Crown):</label>
|
| 476 |
+
<input type="range" id="forkLength" min="350" max="500" value="400" step="5">
|
| 477 |
+
<span class="value-display" id="forkLengthValue">400mm</span>
|
| 478 |
+
|
| 479 |
+
<label for="forkRake">Fork Rake:</label>
|
| 480 |
+
<input type="range" id="forkRake" min="30" max="65" value="45" step="1">
|
| 481 |
+
<span class="value-display" id="forkRakeValue">45mm</span>
|
| 482 |
+
|
| 483 |
+
<!-- Cockpit -->
|
| 484 |
+
<div class="control-section">Cockpit</div>
|
| 485 |
+
|
| 486 |
+
<label for="stemStackHeight">Stem Stack:</label>
|
| 487 |
+
<input type="range" id="stemStackHeight" min="0" max="50" value="10" step="2">
|
| 488 |
+
<span class="value-display" id="stemStackHeightValue">10mm</span>
|
| 489 |
+
|
| 490 |
+
<label for="stemLength">Stem Length:</label>
|
| 491 |
+
<input type="range" id="stemLength" min="50" max="140" value="90" step="5">
|
| 492 |
+
<span class="value-display" id="stemLengthValue">90mm</span>
|
| 493 |
+
|
| 494 |
+
<label for="stemAngle">Stem Angle:</label>
|
| 495 |
+
<input type="range" id="stemAngle" min="-20" max="30" value="6" step="1">
|
| 496 |
+
<span class="value-display" id="stemAngleValue">6°</span>
|
| 497 |
+
|
| 498 |
+
<label for="handlebarWidth">Handlebar Width:</label>
|
| 499 |
+
<input type="range" id="handlebarWidth" min="360" max="800" value="420" step="10">
|
| 500 |
+
<span class="value-display" id="handlebarWidthValue">420mm</span>
|
| 501 |
+
|
| 502 |
+
<label for="handlebarRise">Handlebar Rise:</label>
|
| 503 |
+
<input type="range" id="handlebarRise" min="-30" max="50" value="15" step="5">
|
| 504 |
+
<span class="value-display" id="handlebarRiseValue">15mm</span>
|
| 505 |
+
|
| 506 |
+
<!-- Seat -->
|
| 507 |
+
<div class="control-section">Saddle & Seatpost</div>
|
| 508 |
+
|
| 509 |
+
<label for="seatpostExposure">Seatpost Exposure:</label>
|
| 510 |
+
<input type="range" id="seatpostExposure" min="50" max="300" value="150" step="10">
|
| 511 |
+
<span class="value-display" id="seatpostExposureValue">150mm</span>
|
| 512 |
+
|
| 513 |
+
<label for="saddleSetback">Saddle Setback:</label>
|
| 514 |
+
<input type="range" id="saddleSetback" min="-10" max="40" value="20" step="2">
|
| 515 |
+
<span class="value-display" id="saddleSetbackValue">20mm</span>
|
| 516 |
+
|
| 517 |
+
<!-- Drivetrain -->
|
| 518 |
+
<div class="control-section">Drivetrain</div>
|
| 519 |
+
|
| 520 |
+
<label for="crankLength">Crank Length:</label>
|
| 521 |
+
<input type="range" id="crankLength" min="150" max="180" value="172.5" step="2.5">
|
| 522 |
+
<span class="value-display" id="crankLengthValue">172.5mm</span>
|
| 523 |
+
</div> <!-- End controls -->
|
| 524 |
+
|
| 525 |
+
</div> <!-- End main-layout -->
|
| 526 |
+
|
| 527 |
+
<!-- Footer removed as requested -->
|
| 528 |
+
|
| 529 |
+
<script>
|
| 530 |
+
// --- JavaScript remains exactly the same as before ---
|
| 531 |
+
const svg = document.getElementById('bike-svg');
|
| 532 |
+
const bikeElements = document.getElementById('bike-elements');
|
| 533 |
+
|
| 534 |
+
// --- DOM Element References ---
|
| 535 |
+
const rearWheel = document.getElementById('rear-wheel');
|
| 536 |
+
const frontWheel = document.getElementById('front-wheel');
|
| 537 |
+
const rearAxleCircle = document.getElementById('rear-axle');
|
| 538 |
+
const frontAxleCircle = document.getElementById('front-axle');
|
| 539 |
+
const seatTubeLine = document.getElementById('seat-tube');
|
| 540 |
+
const topTubeLine = document.getElementById('top-tube');
|
| 541 |
+
const downTubeLine = document.getElementById('down-tube');
|
| 542 |
+
const chainstayLine = document.getElementById('chainstay');
|
| 543 |
+
const seatstayLine = document.getElementById('seatstay');
|
| 544 |
+
const forkLine = document.getElementById('fork');
|
| 545 |
+
const headTubeLine = document.getElementById('head-tube');
|
| 546 |
+
const headsetSpacersLine = document.getElementById('headset-spacers');
|
| 547 |
+
const stemLine = document.getElementById('stem');
|
| 548 |
+
const handlebarLine = document.getElementById('handlebar');
|
| 549 |
+
const seatpostLine = document.getElementById('seatpost');
|
| 550 |
+
const saddleGroup = document.getElementById('saddle-group');
|
| 551 |
+
const saddlePath = document.getElementById('saddle'); // Path itself for potential scaling if needed
|
| 552 |
+
const crankArmLine = document.getElementById('crank-arm');
|
| 553 |
+
const pedalCircle = document.getElementById('pedal');
|
| 554 |
+
const bbCircle = document.getElementById('bottom-bracket');
|
| 555 |
+
const seatClusterCircle = document.getElementById('seat-cluster');
|
| 556 |
+
const headTubeTopCircle = document.getElementById('head-tube-top');
|
| 557 |
+
const headTubeBottomCircle = document.getElementById('head-tube-bottom');
|
| 558 |
+
const stemClampCircle = document.getElementById('stem-clamp');
|
| 559 |
+
|
| 560 |
+
const rearWheelGroup = document.getElementById('rear-wheel-group');
|
| 561 |
+
const frontWheelGroup = document.getElementById('front-wheel-group');
|
| 562 |
+
const rearSpokes = rearWheelGroup.querySelectorAll('.spokes');
|
| 563 |
+
const frontSpokes = frontWheelGroup.querySelectorAll('.spokes');
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
// --- Input Controls ---
|
| 567 |
+
const controls = {
|
| 568 |
+
wheelRadius: document.getElementById('wheelRadius'),
|
| 569 |
+
seatTubeLength: document.getElementById('seatTubeLength'),
|
| 570 |
+
effectiveTopTube: document.getElementById('effectiveTopTube'),
|
| 571 |
+
chainstayLength: document.getElementById('chainstayLength'),
|
| 572 |
+
headTubeAngle: document.getElementById('headTubeAngle'),
|
| 573 |
+
seatTubeAngle: document.getElementById('seatTubeAngle'),
|
| 574 |
+
bbDrop: document.getElementById('bbDrop'),
|
| 575 |
+
forkLength: document.getElementById('forkLength'),
|
| 576 |
+
forkRake: document.getElementById('forkRake'),
|
| 577 |
+
headTubeLength: document.getElementById('headTubeLength'),
|
| 578 |
+
stemStackHeight: document.getElementById('stemStackHeight'),
|
| 579 |
+
stemLength: document.getElementById('stemLength'),
|
| 580 |
+
stemAngle: document.getElementById('stemAngle'),
|
| 581 |
+
handlebarWidth: document.getElementById('handlebarWidth'),
|
| 582 |
+
handlebarRise: document.getElementById('handlebarRise'),
|
| 583 |
+
seatpostExposure: document.getElementById('seatpostExposure'),
|
| 584 |
+
saddleSetback: document.getElementById('saddleSetback'),
|
| 585 |
+
crankLength: document.getElementById('crankLength'),
|
| 586 |
+
};
|
| 587 |
+
|
| 588 |
+
// --- Value Displays ---
|
| 589 |
+
const displays = { // Map IDs to display elements
|
| 590 |
+
wheelRadius: document.getElementById('wheelRadiusValue'),
|
| 591 |
+
seatTubeLength: document.getElementById('seatTubeLengthValue'),
|
| 592 |
+
effectiveTopTube: document.getElementById('effectiveTopTubeValue'),
|
| 593 |
+
chainstayLength: document.getElementById('chainstayLengthValue'),
|
| 594 |
+
headTubeAngle: document.getElementById('headTubeAngleValue'),
|
| 595 |
+
seatTubeAngle: document.getElementById('seatTubeAngleValue'),
|
| 596 |
+
bbDrop: document.getElementById('bbDropValue'),
|
| 597 |
+
forkLength: document.getElementById('forkLengthValue'),
|
| 598 |
+
forkRake: document.getElementById('forkRakeValue'),
|
| 599 |
+
headTubeLength: document.getElementById('headTubeLengthValue'),
|
| 600 |
+
stemStackHeight: document.getElementById('stemStackHeightValue'),
|
| 601 |
+
stemLength: document.getElementById('stemLengthValue'),
|
| 602 |
+
stemAngle: document.getElementById('stemAngleValue'),
|
| 603 |
+
handlebarWidth: document.getElementById('handlebarWidthValue'),
|
| 604 |
+
handlebarRise: document.getElementById('handlebarRiseValue'),
|
| 605 |
+
seatpostExposure: document.getElementById('seatpostExposureValue'),
|
| 606 |
+
saddleSetback: document.getElementById('saddleSetbackValue'),
|
| 607 |
+
crankLength: document.getElementById('crankLengthValue'),
|
| 608 |
+
};
|
| 609 |
+
|
| 610 |
+
// --- Constants ---
|
| 611 |
+
const SVG_OFFSET_X = 100; // Minimal offset to properly center the bike
|
| 612 |
+
const SVG_OFFSET_Y = 550; // Centered vertically in the viewbox
|
| 613 |
+
const CRANK_ANGLE_DEG = 45; // Fixed angle for crank arm visualization
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
// --- Calculation and Update Function ---
|
| 617 |
+
function updateBike() {
|
| 618 |
+
// 1. Read values
|
| 619 |
+
const params = {};
|
| 620 |
+
for (const key in controls) {
|
| 621 |
+
params[key] = parseFloat(controls[key].value);
|
| 622 |
+
// Update display
|
| 623 |
+
let displayValue = params[key];
|
| 624 |
+
let unit = 'mm';
|
| 625 |
+
if (key.includes('Angle')) {
|
| 626 |
+
unit = '°';
|
| 627 |
+
displays[key].textContent = `${displayValue.toFixed(1)}${unit}`;
|
| 628 |
+
} else if (key === 'crankLength') {
|
| 629 |
+
displays[key].textContent = `${displayValue.toFixed(1)}${unit}`;
|
| 630 |
+
}
|
| 631 |
+
else {
|
| 632 |
+
displays[key].textContent = `${displayValue}${unit}`;
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
// 2. Convert angles to radians
|
| 637 |
+
const headAngleRad = params.headTubeAngle * Math.PI / 180;
|
| 638 |
+
const seatAngleRad = params.seatTubeAngle * Math.PI / 180;
|
| 639 |
+
const stemAngleRad = params.stemAngle * Math.PI / 180; // Angle relative to horizontal
|
| 640 |
+
const crankAngleRad = CRANK_ANGLE_DEG * Math.PI / 180;
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
// 3. Calculate Key Point Coordinates (relative to rear axle)
|
| 644 |
+
const rearAxle = { x: 0, y: 0 };
|
| 645 |
+
const bb = { x: params.chainstayLength, y: params.bbDrop };
|
| 646 |
+
|
| 647 |
+
const seatCluster = {
|
| 648 |
+
x: bb.x - params.seatTubeLength * Math.cos(seatAngleRad),
|
| 649 |
+
y: bb.y - params.seatTubeLength * Math.sin(seatAngleRad)
|
| 650 |
+
};
|
| 651 |
+
|
| 652 |
+
// Estimate Head Tube Top based on ETT and Seat Cluster
|
| 653 |
+
const ettPointOnSeatpostLine = {
|
| 654 |
+
x: seatCluster.x,
|
| 655 |
+
y: seatCluster.y // Approx horizontal from seat cluster
|
| 656 |
+
};
|
| 657 |
+
const headTubeTopInitial = { // Point on steerer axis BEFORE stack
|
| 658 |
+
x: ettPointOnSeatpostLine.x + params.effectiveTopTube,
|
| 659 |
+
y: ettPointOnSeatpostLine.y
|
| 660 |
+
};
|
| 661 |
+
|
| 662 |
+
// Calculate Head Tube Bottom from Initial Top based on HT Length & Angle
|
| 663 |
+
const headTubeBottom = {
|
| 664 |
+
x: headTubeTopInitial.x + params.headTubeLength * Math.cos(headAngleRad),
|
| 665 |
+
y: headTubeTopInitial.y + params.headTubeLength * Math.sin(headAngleRad)
|
| 666 |
+
};
|
| 667 |
+
|
| 668 |
+
// Front Axle: Use Fork Length (Axle-Crown) and Rake
|
| 669 |
+
const steererPointAxleLevel = {
|
| 670 |
+
x: headTubeBottom.x + params.forkLength * Math.cos(headAngleRad),
|
| 671 |
+
y: headTubeBottom.y + params.forkLength * Math.sin(headAngleRad)
|
| 672 |
+
};
|
| 673 |
+
const rakeAngleRad = headAngleRad + Math.PI / 2; // 90 deg offset
|
| 674 |
+
const rakeVector = {
|
| 675 |
+
x: params.forkRake * Math.cos(rakeAngleRad),
|
| 676 |
+
y: params.forkRake * Math.sin(rakeAngleRad)
|
| 677 |
+
};
|
| 678 |
+
const frontAxle = {
|
| 679 |
+
x: steererPointAxleLevel.x + rakeVector.x,
|
| 680 |
+
y: steererPointAxleLevel.y + rakeVector.y
|
| 681 |
+
};
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
// --- Cockpit Calculations ---
|
| 685 |
+
const steererVec = {
|
| 686 |
+
x: -Math.cos(headAngleRad), // Points up along steerer
|
| 687 |
+
y: -Math.sin(headAngleRad)
|
| 688 |
+
};
|
| 689 |
+
const stemStartPoint = { // Base of stem on steerer
|
| 690 |
+
x: headTubeTopInitial.x + params.stemStackHeight * steererVec.x,
|
| 691 |
+
y: headTubeTopInitial.y + params.stemStackHeight * steererVec.y
|
| 692 |
+
};
|
| 693 |
+
|
| 694 |
+
const stemEnd = { // Handlebar Clamp Center
|
| 695 |
+
x: stemStartPoint.x + params.stemLength * Math.cos(stemAngleRad),
|
| 696 |
+
y: stemStartPoint.y - params.stemLength * Math.sin(stemAngleRad) // Positive angle = UP (smaller Y)
|
| 697 |
+
};
|
| 698 |
+
|
| 699 |
+
const halfBarWidth = params.handlebarWidth / 2;
|
| 700 |
+
const handlebarCenter = {
|
| 701 |
+
x: stemEnd.x,
|
| 702 |
+
y: stemEnd.y - params.handlebarRise // Positive rise = UP (smaller Y)
|
| 703 |
+
};
|
| 704 |
+
const handlebarLeft = { x: handlebarCenter.x - halfBarWidth, y: handlebarCenter.y };
|
| 705 |
+
const handlebarRight = { x: handlebarCenter.x + halfBarWidth, y: handlebarCenter.y };
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
// --- Seat Calculations ---
|
| 709 |
+
const seatpostTopNominal = { // Point along seat tube axis
|
| 710 |
+
x: seatCluster.x - params.seatpostExposure * Math.cos(seatAngleRad),
|
| 711 |
+
y: seatCluster.y - params.seatpostExposure * Math.sin(seatAngleRad)
|
| 712 |
+
};
|
| 713 |
+
const seatpostTop = { // Apply Saddle Setback
|
| 714 |
+
x: seatpostTopNominal.x - params.saddleSetback, // positive = rearward = smaller X
|
| 715 |
+
y: seatpostTopNominal.y
|
| 716 |
+
};
|
| 717 |
+
|
| 718 |
+
|
| 719 |
+
// --- Drivetrain Calculation ---
|
| 720 |
+
const crankEnd = {
|
| 721 |
+
x: bb.x + params.crankLength * Math.cos(crankAngleRad),
|
| 722 |
+
y: bb.y + params.crankLength * Math.sin(crankAngleRad) // Y positive is down
|
| 723 |
+
};
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
// 4. Apply SVG Offsets
|
| 727 |
+
const applyOffset = (point) => ({ x: point.x + SVG_OFFSET_X, y: point.y + SVG_OFFSET_Y });
|
| 728 |
+
|
| 729 |
+
const svgRearAxle = applyOffset(rearAxle);
|
| 730 |
+
const svgBB = applyOffset(bb);
|
| 731 |
+
const svgSeatCluster = applyOffset(seatCluster);
|
| 732 |
+
const svgHeadTubeTopInitial = applyOffset(headTubeTopInitial); // For HT line start
|
| 733 |
+
const svgHeadTubeBottom = applyOffset(headTubeBottom);
|
| 734 |
+
const svgFrontAxle = applyOffset(frontAxle);
|
| 735 |
+
const svgStemStartPoint = applyOffset(stemStartPoint); // Where stem visually starts
|
| 736 |
+
const svgStemEnd = applyOffset(stemEnd); // Where bar clamps
|
| 737 |
+
const svgHandlebarLeft = applyOffset(handlebarLeft);
|
| 738 |
+
const svgHandlebarRight = applyOffset(handlebarRight);
|
| 739 |
+
const svgSeatpostTop = applyOffset(seatpostTop); // Saddle clamp position
|
| 740 |
+
const svgCrankEnd = applyOffset(crankEnd);
|
| 741 |
+
|
| 742 |
+
|
| 743 |
+
// 5. Update SVG Element Attributes
|
| 744 |
+
// Update Wheels & Axles
|
| 745 |
+
rearWheel.setAttribute('cx', svgRearAxle.x); rearWheel.setAttribute('cy', svgRearAxle.y);
|
| 746 |
+
rearWheel.setAttribute('r', params.wheelRadius);
|
| 747 |
+
rearAxleCircle.setAttribute('cx', svgRearAxle.x); rearAxleCircle.setAttribute('cy', svgRearAxle.y);
|
| 748 |
+
|
| 749 |
+
frontWheel.setAttribute('cx', svgFrontAxle.x); frontWheel.setAttribute('cy', svgFrontAxle.y);
|
| 750 |
+
frontWheel.setAttribute('r', params.wheelRadius);
|
| 751 |
+
frontAxleCircle.setAttribute('cx', svgFrontAxle.x); frontAxleCircle.setAttribute('cy', svgFrontAxle.y);
|
| 752 |
+
|
| 753 |
+
// Update Spokes
|
| 754 |
+
const updateSpokes = (spokes, center, radius) => {
|
| 755 |
+
spokes[0].setAttribute('x1', center.x - radius); spokes[0].setAttribute('y1', center.y);
|
| 756 |
+
spokes[0].setAttribute('x2', center.x + radius); spokes[0].setAttribute('y2', center.y);
|
| 757 |
+
spokes[1].setAttribute('x1', center.x); spokes[1].setAttribute('y1', center.y - radius);
|
| 758 |
+
spokes[1].setAttribute('x2', center.x); spokes[1].setAttribute('y2', center.y + radius);
|
| 759 |
+
spokes[2].setAttribute('x1', center.x - radius * 0.707); spokes[2].setAttribute('y1', center.y - radius * 0.707);
|
| 760 |
+
spokes[2].setAttribute('x2', center.x + radius * 0.707); spokes[2].setAttribute('y2', center.y + radius * 0.707);
|
| 761 |
+
spokes[3].setAttribute('x1', center.x - radius * 0.707); spokes[3].setAttribute('y1', center.y + radius * 0.707);
|
| 762 |
+
spokes[3].setAttribute('x2', center.x + radius * 0.707); spokes[3].setAttribute('y2', center.y - radius * 0.707);
|
| 763 |
+
};
|
| 764 |
+
updateSpokes(rearSpokes, svgRearAxle, params.wheelRadius);
|
| 765 |
+
updateSpokes(frontSpokes, svgFrontAxle, params.wheelRadius);
|
| 766 |
+
|
| 767 |
+
// Update Frame Tubes
|
| 768 |
+
seatTubeLine.setAttribute('x1', svgBB.x); seatTubeLine.setAttribute('y1', svgBB.y);
|
| 769 |
+
seatTubeLine.setAttribute('x2', svgSeatCluster.x); seatTubeLine.setAttribute('y2', svgSeatCluster.y);
|
| 770 |
+
|
| 771 |
+
topTubeLine.setAttribute('x1', svgSeatCluster.x); topTubeLine.setAttribute('y1', svgSeatCluster.y);
|
| 772 |
+
topTubeLine.setAttribute('x2', svgHeadTubeTopInitial.x); topTubeLine.setAttribute('y2', svgHeadTubeTopInitial.y); // Connects to initial HT top
|
| 773 |
+
|
| 774 |
+
downTubeLine.setAttribute('x1', svgBB.x); downTubeLine.setAttribute('y1', svgBB.y);
|
| 775 |
+
downTubeLine.setAttribute('x2', svgHeadTubeBottom.x); downTubeLine.setAttribute('y2', svgHeadTubeBottom.y);
|
| 776 |
+
|
| 777 |
+
chainstayLine.setAttribute('x1', svgBB.x); chainstayLine.setAttribute('y1', svgBB.y);
|
| 778 |
+
chainstayLine.setAttribute('x2', svgRearAxle.x); chainstayLine.setAttribute('y2', svgRearAxle.y);
|
| 779 |
+
|
| 780 |
+
seatstayLine.setAttribute('x1', svgSeatCluster.x); seatstayLine.setAttribute('y1', svgSeatCluster.y);
|
| 781 |
+
seatstayLine.setAttribute('x2', svgRearAxle.x); seatstayLine.setAttribute('y2', svgRearAxle.y);
|
| 782 |
+
|
| 783 |
+
// Update Components
|
| 784 |
+
headTubeLine.setAttribute('x1', svgHeadTubeTopInitial.x); headTubeLine.setAttribute('y1', svgHeadTubeTopInitial.y);
|
| 785 |
+
headTubeLine.setAttribute('x2', svgHeadTubeBottom.x); headTubeLine.setAttribute('y2', svgHeadTubeBottom.y);
|
| 786 |
+
|
| 787 |
+
forkLine.setAttribute('x1', svgHeadTubeBottom.x); forkLine.setAttribute('y1', svgHeadTubeBottom.y);
|
| 788 |
+
forkLine.setAttribute('x2', svgFrontAxle.x); forkLine.setAttribute('y2', svgFrontAxle.y);
|
| 789 |
+
|
| 790 |
+
// Headset Spacers
|
| 791 |
+
if (params.stemStackHeight > 0) {
|
| 792 |
+
headsetSpacersLine.setAttribute('x1', svgHeadTubeTopInitial.x); headsetSpacersLine.setAttribute('y1', svgHeadTubeTopInitial.y);
|
| 793 |
+
headsetSpacersLine.setAttribute('x2', svgStemStartPoint.x); headsetSpacersLine.setAttribute('y2', svgStemStartPoint.y);
|
| 794 |
+
headsetSpacersLine.style.display = 'inline';
|
| 795 |
+
} else {
|
| 796 |
+
headsetSpacersLine.style.display = 'none';
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
stemLine.setAttribute('x1', svgStemStartPoint.x); stemLine.setAttribute('y1', svgStemStartPoint.y);
|
| 800 |
+
stemLine.setAttribute('x2', svgStemEnd.x); stemLine.setAttribute('y2', svgStemEnd.y);
|
| 801 |
+
|
| 802 |
+
handlebarLine.setAttribute('x1', svgHandlebarLeft.x); handlebarLine.setAttribute('y1', svgHandlebarLeft.y);
|
| 803 |
+
handlebarLine.setAttribute('x2', svgHandlebarRight.x); handlebarLine.setAttribute('y2', svgHandlebarRight.y);
|
| 804 |
+
|
| 805 |
+
seatpostLine.setAttribute('x1', svgSeatCluster.x); seatpostLine.setAttribute('y1', svgSeatCluster.y);
|
| 806 |
+
seatpostLine.setAttribute('x2', svgSeatpostTop.x); // Use the setback-adjusted top point
|
| 807 |
+
seatpostLine.setAttribute('y2', svgSeatpostTop.y); // for the visible post line end
|
| 808 |
+
|
| 809 |
+
// Update Saddle Position (Group transform)
|
| 810 |
+
const saddleAngleDeg = (seatAngleRad * 180 / Math.PI) - 90; // Approx align with seat tube
|
| 811 |
+
saddleGroup.setAttribute('transform', `translate(${svgSeatpostTop.x}, ${svgSeatpostTop.y}) rotate(${saddleAngleDeg})`);
|
| 812 |
+
|
| 813 |
+
// Update Drivetrain
|
| 814 |
+
crankArmLine.setAttribute('x1', svgBB.x); crankArmLine.setAttribute('y1', svgBB.y);
|
| 815 |
+
crankArmLine.setAttribute('x2', svgCrankEnd.x); crankArmLine.setAttribute('y2', svgCrankEnd.y);
|
| 816 |
+
pedalCircle.setAttribute('cx', svgCrankEnd.x); pedalCircle.setAttribute('cy', svgCrankEnd.y);
|
| 817 |
+
|
| 818 |
+
// Update Joint Markers
|
| 819 |
+
bbCircle.setAttribute('cx', svgBB.x); bbCircle.setAttribute('cy', svgBB.y);
|
| 820 |
+
seatClusterCircle.setAttribute('cx', svgSeatCluster.x); seatClusterCircle.setAttribute('cy', svgSeatCluster.y);
|
| 821 |
+
headTubeTopCircle.setAttribute('cx', svgHeadTubeTopInitial.x); headTubeTopCircle.setAttribute('cy', svgHeadTubeTopInitial.y);
|
| 822 |
+
headTubeBottomCircle.setAttribute('cx', svgHeadTubeBottom.x); headTubeBottomCircle.setAttribute('cy', svgHeadTubeBottom.y);
|
| 823 |
+
stemClampCircle.setAttribute('cx', svgStemEnd.x); stemClampCircle.setAttribute('cy', svgStemEnd.y);
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
// --- Event Listeners ---
|
| 827 |
+
for (const key in controls) {
|
| 828 |
+
controls[key].addEventListener('input', updateBike);
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
// --- Initial Draw ---
|
| 832 |
+
updateBike();
|
| 833 |
+
|
| 834 |
+
// Function to reset all slider controls to default values
|
| 835 |
+
function resetToDefaults() {
|
| 836 |
+
// Reset each slider to its default value
|
| 837 |
+
for (const key in controls) {
|
| 838 |
+
controls[key].value = controls[key].defaultValue;
|
| 839 |
+
}
|
| 840 |
+
// Update the visualization
|
| 841 |
+
updateBike();
|
| 842 |
+
}
|
| 843 |
+
</script>
|
| 844 |
+
</body>
|
| 845 |
+
</html>
|