AntDX316 commited on
Commit
d80df21
·
1 Parent(s): 6b29610

updated to index.html

Browse files
Files changed (1) hide show
  1. 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>