@inventure71/paddockjs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +86 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +176 -0
- package/README.md +165 -0
- package/assets/f1-broadcast-panel-surface.png +0 -0
- package/assets/f1-car-sprite-game.png +0 -0
- package/assets/f1-driver-helmet.png +0 -0
- package/assets/f1-safety-car-sprite.png +0 -0
- package/assets/f1-texture-asphalt.png +0 -0
- package/assets/f1Logo.png +0 -0
- package/docs/architecture.md +209 -0
- package/docs/concepts.md +178 -0
- package/docs/data_contract.md +360 -0
- package/docs/data_model.md +167 -0
- package/docs/index.md +15 -0
- package/docs/learnings.md +47 -0
- package/docs/rules.md +181 -0
- package/docs/system_specs.md +211 -0
- package/package.json +85 -0
- package/src/api/PaddockSimulatorController.js +233 -0
- package/src/app/F1SimulatorApp.js +1293 -0
- package/src/app/domBindings.js +64 -0
- package/src/config/defaultAssets.js +29 -0
- package/src/config/defaultOptions.js +184 -0
- package/src/data/championship.js +225 -0
- package/src/data/customFields.js +14 -0
- package/src/data/demoDrivers.js +134 -0
- package/src/data/driverData.js +71 -0
- package/src/data/normalizeDrivers.js +21 -0
- package/src/data/vehicleData.js +81 -0
- package/src/index.d.ts +399 -0
- package/src/index.js +116 -0
- package/src/rendering/proceduralTrackAsset.js +517 -0
- package/src/rendering/renderSnapshot.js +23 -0
- package/src/simulation/driverController.js +253 -0
- package/src/simulation/raceSimulation.js +933 -0
- package/src/simulation/simMath.js +34 -0
- package/src/simulation/trackModel.js +607 -0
- package/src/simulation/units.js +30 -0
- package/src/simulation/vehiclePhysics.js +104 -0
- package/src/standalone.js +15 -0
- package/src/styles.css +1818 -0
- package/src/ui/componentTemplates.js +229 -0
- package/src/ui/shellTemplate.js +43 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
function buttonHiddenAttribute(isVisible) {
|
|
2
|
+
return isVisible ? '' : ' hidden';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function createLoadingMarkup(label = 'Loading') {
|
|
6
|
+
return `
|
|
7
|
+
<div class="paddock-loading" data-paddock-loading aria-label="${escapeHtml(label)} loading">
|
|
8
|
+
<div class="paddock-loading__lights" aria-hidden="true">
|
|
9
|
+
<span></span>
|
|
10
|
+
<span></span>
|
|
11
|
+
<span></span>
|
|
12
|
+
<span></span>
|
|
13
|
+
<span></span>
|
|
14
|
+
</div>
|
|
15
|
+
<span class="paddock-loading__label">${escapeHtml(label)}</span>
|
|
16
|
+
</div>
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function escapeHtml(value) {
|
|
21
|
+
return String(value ?? '')
|
|
22
|
+
.replaceAll('&', '&')
|
|
23
|
+
.replaceAll('<', '<')
|
|
24
|
+
.replaceAll('>', '>')
|
|
25
|
+
.replaceAll('"', '"')
|
|
26
|
+
.replaceAll("'", ''');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createRaceControlsMarkup({
|
|
30
|
+
title,
|
|
31
|
+
kicker,
|
|
32
|
+
backLinkHref,
|
|
33
|
+
backLinkLabel,
|
|
34
|
+
showBackLink,
|
|
35
|
+
}) {
|
|
36
|
+
return `
|
|
37
|
+
<header class="sim-topbar" data-paddock-component="race-controls">
|
|
38
|
+
<a class="sim-backlink" href="${escapeHtml(backLinkHref)}"${buttonHiddenAttribute(showBackLink)}>${escapeHtml(backLinkLabel)}</a>
|
|
39
|
+
<div class="sim-title-block">
|
|
40
|
+
<p class="sim-kicker">${escapeHtml(kicker)}</p>
|
|
41
|
+
<h1>${escapeHtml(title)}</h1>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="sim-controls" aria-label="Race controls">
|
|
44
|
+
${createSafetyCarControlMarkup({ compact: true })}
|
|
45
|
+
<button class="sim-control" type="button" data-restart-race>Restart</button>
|
|
46
|
+
</div>
|
|
47
|
+
${createLoadingMarkup('Race controls')}
|
|
48
|
+
</header>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createSafetyCarControlMarkup({ compact = false } = {}) {
|
|
53
|
+
const className = compact
|
|
54
|
+
? 'sim-control sim-control--safety'
|
|
55
|
+
: 'sim-control sim-control--safety standalone-control';
|
|
56
|
+
const componentAttribute = compact ? '' : ' data-paddock-component="safety-car-control"';
|
|
57
|
+
return `<button class="${className}" type="button" data-safety-car aria-pressed="false"${componentAttribute}>Safety Car</button>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createTimingTowerMarkup({ totalLaps, assets }) {
|
|
61
|
+
return `
|
|
62
|
+
<aside class="sim-timing broadcast-tower" data-paddock-component="timing-tower" data-timing-tower aria-label="Timing tower">
|
|
63
|
+
<div class="broadcast-tower-frame">
|
|
64
|
+
<div class="broadcast-brand">
|
|
65
|
+
<img class="broadcast-f1-logo" src="${escapeHtml(assets.f1Logo)}" alt="F1" />
|
|
66
|
+
</div>
|
|
67
|
+
<div class="broadcast-lap">
|
|
68
|
+
<span>Lap</span>
|
|
69
|
+
<strong data-tower-lap-readout>1</strong>
|
|
70
|
+
<span>/</span>
|
|
71
|
+
<span data-tower-total-laps>${escapeHtml(totalLaps)}</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="broadcast-gap-mode" role="group" aria-label="Timing gap mode">
|
|
74
|
+
<button type="button" data-timing-gap-mode="interval" aria-pressed="true">Int</button>
|
|
75
|
+
<button type="button" data-timing-gap-mode="leader" aria-pressed="false">Gap</button>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="broadcast-safety-banner" data-tower-safety-banner>
|
|
78
|
+
<span>FIA</span>
|
|
79
|
+
<strong>Safety Car</strong>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="broadcast-column-head" aria-hidden="true">
|
|
82
|
+
<span>Pos</span>
|
|
83
|
+
<span>Team</span>
|
|
84
|
+
<span>Project</span>
|
|
85
|
+
<span data-timing-gap-label>Int</span>
|
|
86
|
+
<span>Tyre</span>
|
|
87
|
+
</div>
|
|
88
|
+
<ol class="timing-list" data-timing-list></ol>
|
|
89
|
+
</div>
|
|
90
|
+
${createLoadingMarkup('Timing tower')}
|
|
91
|
+
</aside>
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createCameraControlsMarkup({ embedded = false } = {}) {
|
|
96
|
+
const className = embedded ? 'camera-controls' : 'camera-controls camera-controls--external';
|
|
97
|
+
return `
|
|
98
|
+
<div class="${className}" data-paddock-component="camera-controls" aria-label="Camera controls">
|
|
99
|
+
<button type="button" data-camera-mode="overview" aria-pressed="false">Overview</button>
|
|
100
|
+
<button type="button" data-camera-mode="leader" aria-pressed="true">Leader</button>
|
|
101
|
+
<button type="button" data-camera-mode="selected" aria-pressed="false">Selected</button>
|
|
102
|
+
<button type="button" data-camera-mode="show-all" aria-pressed="false">Show all</button>
|
|
103
|
+
<button type="button" data-zoom-out aria-label="Zoom out">-</button>
|
|
104
|
+
<button type="button" data-zoom-in aria-label="Zoom in">+</button>
|
|
105
|
+
${createLoadingMarkup('Camera controls')}
|
|
106
|
+
</div>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createRaceCanvasMarkup({
|
|
111
|
+
includeRaceDataPanel = false,
|
|
112
|
+
includeTimingTower = false,
|
|
113
|
+
timingTowerVerticalFit,
|
|
114
|
+
assets,
|
|
115
|
+
totalLaps,
|
|
116
|
+
ui = {},
|
|
117
|
+
} = {}) {
|
|
118
|
+
const showFps = ui.showFps !== false;
|
|
119
|
+
const showEmbeddedCameraControls = ui.cameraControls !== 'external' && ui.cameraControls !== false;
|
|
120
|
+
const timingFit = (timingTowerVerticalFit ?? ui.timingTowerVerticalFit) === 'scroll'
|
|
121
|
+
? 'scroll'
|
|
122
|
+
: 'expand-race-view';
|
|
123
|
+
const classNames = ['sim-canvas-panel'];
|
|
124
|
+
if (includeTimingTower) {
|
|
125
|
+
classNames.push('sim-canvas-panel--with-timing-tower', `sim-canvas-panel--timing-${timingFit}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `
|
|
129
|
+
<section class="${classNames.join(' ')}" data-paddock-component="race-canvas" aria-label="Track view">
|
|
130
|
+
<div class="track-canvas" data-track-canvas></div>
|
|
131
|
+
${includeTimingTower ? createTimingTowerMarkup({ totalLaps, assets }) : ''}
|
|
132
|
+
${showFps ? `
|
|
133
|
+
<div class="fps-counter" aria-label="Frames per second">
|
|
134
|
+
<span>FPS</span>
|
|
135
|
+
<strong data-fps-readout>--</strong>
|
|
136
|
+
</div>
|
|
137
|
+
` : ''}
|
|
138
|
+
<div class="start-lights" data-start-lights aria-live="polite">
|
|
139
|
+
<div class="start-lights__label" data-start-lights-label>Race start</div>
|
|
140
|
+
<div class="start-lights__gantry" aria-hidden="true">
|
|
141
|
+
<span></span>
|
|
142
|
+
<span></span>
|
|
143
|
+
<span></span>
|
|
144
|
+
<span></span>
|
|
145
|
+
<span></span>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
${showEmbeddedCameraControls ? createCameraControlsMarkup({ embedded: true }) : ''}
|
|
149
|
+
${includeRaceDataPanel ? createRaceDataPanelMarkup({ assets, ui }) : ''}
|
|
150
|
+
<div class="race-finish-panel" data-race-finish-panel hidden aria-live="polite">
|
|
151
|
+
<span>Race winner</span>
|
|
152
|
+
<strong data-race-finish-winner>--</strong>
|
|
153
|
+
<ol data-race-finish-classification></ol>
|
|
154
|
+
</div>
|
|
155
|
+
${createLoadingMarkup('Race view')}
|
|
156
|
+
</section>
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createRaceDataPanelMarkup({ ui = {} } = {}) {
|
|
161
|
+
const sizeMode = ui.raceDataBannerSize === 'auto' ? 'auto' : 'custom';
|
|
162
|
+
return `
|
|
163
|
+
<div class="race-data-panel race-data-panel--${sizeMode}" data-paddock-component="race-data-panel" data-race-data-panel aria-live="polite">
|
|
164
|
+
<div class="race-data-copy">
|
|
165
|
+
<span class="race-data-kicker" data-race-data-kicker>Project</span>
|
|
166
|
+
<strong data-race-data-title>Select driver</strong>
|
|
167
|
+
<span class="race-data-subtitle" data-race-data-subtitle>Race entry</span>
|
|
168
|
+
</div>
|
|
169
|
+
<strong class="race-data-number" data-race-data-number>--</strong>
|
|
170
|
+
<button class="race-data-link" type="button" data-race-data-open>Open project</button>
|
|
171
|
+
${createLoadingMarkup('Race data')}
|
|
172
|
+
</div>
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function createCarDriverOverviewMarkup({ assets }) {
|
|
177
|
+
const cells = Array.from({ length: 7 }, (_, index) => `
|
|
178
|
+
<div class="car-overview-cell car-overview-cell--slot-${index + 1}" data-overview-field data-overview-slot="${index}">
|
|
179
|
+
<span data-overview-field-label>--</span>
|
|
180
|
+
<strong data-overview-field-value>--</strong>
|
|
181
|
+
</div>
|
|
182
|
+
`).join('');
|
|
183
|
+
|
|
184
|
+
return `
|
|
185
|
+
<section class="car-overview" data-paddock-component="car-driver-overview" aria-label="Selected car and driver overview">
|
|
186
|
+
<div class="car-overview-header">
|
|
187
|
+
<span data-car-overview-title>Car overview</span>
|
|
188
|
+
<strong data-car-overview-code>---</strong>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="car-overview-toggle" role="group" aria-label="Overview mode">
|
|
191
|
+
<button type="button" data-overview-mode="vehicle" aria-pressed="true">Car</button>
|
|
192
|
+
<button type="button" data-overview-mode="driver" aria-pressed="false">Driver</button>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="car-overview-diagram" style="--driver-color: #e10600">
|
|
195
|
+
${cells}
|
|
196
|
+
<div class="car-overview-car" aria-hidden="true">
|
|
197
|
+
<img class="car-overview-car-image" data-car-overview-image src="${escapeHtml(assets.carOverview)}" alt="" />
|
|
198
|
+
<span class="car-overview-icon" data-car-overview-icon>--</span>
|
|
199
|
+
<span class="car-overview-number" data-car-overview-number>00</span>
|
|
200
|
+
<span class="car-overview-core-stat" data-car-overview-core-stat>Car</span>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
${createLoadingMarkup('Car and driver overview')}
|
|
204
|
+
</section>
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createTelemetryPanelMarkup(options, { includeOverview = options.ui?.telemetryIncludesOverview !== false } = {}) {
|
|
209
|
+
return `
|
|
210
|
+
<aside class="sim-telemetry" data-paddock-component="telemetry-panel" aria-label="Selected car telemetry">
|
|
211
|
+
<div class="telemetry-header">
|
|
212
|
+
<span data-selected-code>---</span>
|
|
213
|
+
<strong data-selected-name>Select car</strong>
|
|
214
|
+
</div>
|
|
215
|
+
<dl class="telemetry-grid">
|
|
216
|
+
<div><dt>Speed</dt><dd data-telemetry-speed>0 km/h</dd></div>
|
|
217
|
+
<div><dt>Throttle</dt><dd data-telemetry-throttle>0%</dd></div>
|
|
218
|
+
<div><dt>Brake</dt><dd data-telemetry-brake>0%</dd></div>
|
|
219
|
+
<div><dt>Tyres</dt><dd data-telemetry-tyres>0%</dd></div>
|
|
220
|
+
<div><dt>DRS</dt><dd data-telemetry-drs>OFF</dd></div>
|
|
221
|
+
<div><dt>Surface</dt><dd data-telemetry-surface>TRACK</dd></div>
|
|
222
|
+
<div><dt>Interval</dt><dd data-telemetry-gap>--</dd></div>
|
|
223
|
+
<div><dt>Leader</dt><dd data-telemetry-leader-gap>--</dd></div>
|
|
224
|
+
</dl>
|
|
225
|
+
${includeOverview ? createCarDriverOverviewMarkup(options) : ''}
|
|
226
|
+
${createLoadingMarkup('Telemetry')}
|
|
227
|
+
</aside>
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCameraControlsMarkup,
|
|
3
|
+
createRaceCanvasMarkup,
|
|
4
|
+
createRaceControlsMarkup,
|
|
5
|
+
createTelemetryPanelMarkup,
|
|
6
|
+
createTimingTowerMarkup,
|
|
7
|
+
} from './componentTemplates.js';
|
|
8
|
+
|
|
9
|
+
export function createF1SimulatorShell({
|
|
10
|
+
title,
|
|
11
|
+
kicker,
|
|
12
|
+
backLinkHref,
|
|
13
|
+
backLinkLabel,
|
|
14
|
+
showBackLink,
|
|
15
|
+
totalLaps,
|
|
16
|
+
assets,
|
|
17
|
+
ui = {},
|
|
18
|
+
}) {
|
|
19
|
+
const layoutPreset = ui.layoutPreset === 'left-tower-overlay' ? 'left-tower-overlay' : 'standard';
|
|
20
|
+
const timingFit = ui.timingTowerVerticalFit === 'scroll' ? 'scroll' : 'expand-race-view';
|
|
21
|
+
const raceDataBannerSize = ui.raceDataBannerSize === 'auto' ? 'auto' : 'custom';
|
|
22
|
+
const shellClasses = ['f1-sim-component', 'sim-shell'];
|
|
23
|
+
if (layoutPreset === 'left-tower-overlay') shellClasses.push('sim-shell--left-tower-overlay');
|
|
24
|
+
shellClasses.push(`sim-shell--timing-${timingFit}`);
|
|
25
|
+
shellClasses.push(`sim-shell--race-data-${raceDataBannerSize}`);
|
|
26
|
+
const cameraControls = ui.cameraControls === 'external'
|
|
27
|
+
? `<div class="sim-external-camera-controls">${createCameraControlsMarkup()}</div>`
|
|
28
|
+
: '';
|
|
29
|
+
|
|
30
|
+
return `
|
|
31
|
+
<main class="${shellClasses.join(' ')}" data-f1-simulator-shell data-layout-preset="${layoutPreset}">
|
|
32
|
+
<section class="sim-workspace" aria-label="F1 race simulator">
|
|
33
|
+
${createRaceControlsMarkup({ title, kicker, backLinkHref, backLinkLabel, showBackLink })}
|
|
34
|
+
${cameraControls}
|
|
35
|
+
<div class="sim-grid">
|
|
36
|
+
${createTimingTowerMarkup({ totalLaps, assets })}
|
|
37
|
+
${createRaceCanvasMarkup({ includeRaceDataPanel: ui.showRaceDataPanel !== false, assets, ui })}
|
|
38
|
+
${createTelemetryPanelMarkup({ assets, ui })}
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
</main>
|
|
42
|
+
`;
|
|
43
|
+
}
|