minimojs 1.0.0-alpha.2 → 1.0.0-alpha.21

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.
Files changed (55) hide show
  1. package/README.md +82 -285
  2. package/dist/internal/AnimationSystem.js +345 -0
  3. package/dist/internal/AssetSystem.d.ts +1 -0
  4. package/dist/internal/AssetSystem.js +101 -0
  5. package/dist/internal/BackgroundSystem.d.ts +1 -0
  6. package/dist/internal/BackgroundSystem.js +26 -0
  7. package/dist/internal/CanvasSystem.d.ts +1 -0
  8. package/dist/internal/CanvasSystem.js +51 -0
  9. package/dist/internal/ExplosionSystem.d.ts +1 -0
  10. package/dist/internal/ExplosionSystem.js +540 -0
  11. package/dist/internal/InputSystem.d.ts +1 -0
  12. package/dist/internal/InputSystem.js +265 -0
  13. package/dist/internal/LoopSystem.d.ts +1 -0
  14. package/dist/internal/LoopSystem.js +61 -0
  15. package/dist/internal/PhysicsSystem.d.ts +1 -0
  16. package/dist/internal/PhysicsSystem.js +174 -0
  17. package/dist/internal/RenderSystem.d.ts +1 -0
  18. package/dist/internal/RenderSystem.js +910 -0
  19. package/dist/internal/SoundSystem.d.ts +1 -0
  20. package/dist/internal/SoundSystem.js +55 -0
  21. package/dist/internal/SpriteSystem.d.ts +1 -0
  22. package/dist/internal/SpriteSystem.js +32 -0
  23. package/dist/internal/TextSystem.d.ts +1 -0
  24. package/dist/internal/TextSystem.js +16 -0
  25. package/dist/internal/TimerSystem.d.ts +1 -0
  26. package/dist/internal/TimerSystem.js +43 -0
  27. package/dist/internal/TrailSystem.d.ts +1 -0
  28. package/dist/internal/TrailSystem.js +116 -0
  29. package/dist/internal/TransitionSystem.d.ts +1 -0
  30. package/dist/internal/TransitionSystem.js +74 -0
  31. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.d.ts +1 -0
  32. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.js +198 -0
  33. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.d.ts +1 -0
  34. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.js +120 -0
  35. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.d.ts +1 -0
  36. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.js +599 -0
  37. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.d.ts +1 -0
  38. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.js +13 -0
  39. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.d.ts +1 -0
  40. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.js +447 -0
  41. package/dist/minimo-arcaderacer.d.ts +1431 -0
  42. package/dist/minimo-arcaderacer.js +2060 -0
  43. package/dist/minimo.d.ts +1412 -162
  44. package/dist/minimo.js +2083 -816
  45. package/package.json +3 -2
  46. package/dist/animations.js +0 -30
  47. package/dist/audio.js +0 -17
  48. package/dist/game.js +0 -1105
  49. package/dist/input.js +0 -185
  50. package/dist/internal-types.js +0 -4
  51. package/dist/physics.js +0 -10
  52. package/dist/render.js +0 -75
  53. package/dist/sprite.js +0 -149
  54. package/dist/timers.js +0 -23
  55. /package/dist/{pointer-info.js → internal/AnimationSystem.d.ts} +0 -0
@@ -0,0 +1,599 @@
1
+ /** @internal */
2
+ export class ArcadeRacerRenderSystem {
3
+ constructor(
4
+ /** @internal */ engine) {
5
+ this.engine = engine;
6
+ }
7
+ renderFrame(ctx, width, height) {
8
+ const render = {
9
+ ctx,
10
+ width,
11
+ height,
12
+ current: this.engine.trackSystem.sampleTrack(this.engine.distanceState),
13
+ horizonY: this.engine.horizonY,
14
+ stripCount: this.engine.stripCount,
15
+ drawDistance: this.engine.drawDistance,
16
+ roadBandSpan: this.engine.roadBandSpan,
17
+ shoulderBandSpan: this.engine.shoulderBandSpan,
18
+ grassBandSpan: this.engine.grassBandSpan,
19
+ laneCount: this.engine.laneCount,
20
+ trafficBehindVisibilityDistance: this.engine.trafficBehindVisibilityDistance,
21
+ playerProjectionAheadDistance: this.engine.playerProjectionAheadDistance,
22
+ distanceState: this.engine.distanceState,
23
+ headingState: this.engine.headingState,
24
+ playerLane: this.engine.playerLaneState,
25
+ playerScreenY: this.engine.playerScreenYState,
26
+ playerBaseScale: this.engine.playerBaseScaleState,
27
+ playerVisual: this.engine.playerVisualState,
28
+ playerBodyWidth: this.engine.playerBodyWidthState,
29
+ playerBodyLength: this.engine.playerBodyLengthState,
30
+ playerBodyOffsetX: this.engine.playerBodyOffsetXState,
31
+ playerBodyOffsetY: this.engine.playerBodyOffsetYState,
32
+ playerResolvedBody: this.engine.playerResolvedBodyState,
33
+ theme: this.engine.themeState,
34
+ groundFill: this.engine.groundFillState,
35
+ horizon: this.engine.horizonState,
36
+ minimap: this.engine.minimapState,
37
+ debug: this.engine.debugState,
38
+ billboards: this.engine.billboardsState,
39
+ traffic: this.engine.trafficState,
40
+ horizonMarkers: this.engine.horizonMarkersState,
41
+ backgroundLayers: this.engine.backgroundLayersState,
42
+ minimapPoints: this.engine.trackSystem.getMinimapPathPoints(),
43
+ resolveVisualSurface: this.engine.resolveVisualSurface.bind(this.engine),
44
+ projectRoadPoint: this.engine.projectRoadPoint.bind(this.engine),
45
+ projectTrafficPoint: this.engine.projectTrafficPoint.bind(this.engine),
46
+ projectObject: (current, ahead, billboard, renderWidth, renderHeight) => this.engine.projectObject(current, ahead, billboard, renderWidth, renderHeight),
47
+ sampleMinimapPoint: (points, distance) => this.engine.sampleMinimapPoint(points, distance),
48
+ getPlayerScreenX: this.engine.getPlayerScreenX.bind(this.engine),
49
+ getDebugCollisionRectFromVisual: this.engine.getDebugCollisionRectFromVisual.bind(this.engine),
50
+ getRealCollisionRect: this.engine.getRealCollisionRect.bind(this.engine),
51
+ clamp01: this.engine.clamp01.bind(this.engine),
52
+ easeInOut: this.engine.easeInOut.bind(this.engine),
53
+ getShortestAngleDelta: this.engine.getShortestAngleDelta.bind(this.engine),
54
+ };
55
+ this.renderResolvedFrame(render);
56
+ }
57
+ /** @internal */
58
+ renderResolvedFrame(render) {
59
+ this.drawBackground(render);
60
+ this.drawRoad(render);
61
+ this.drawSceneObjects(render);
62
+ this.drawMinimap(render);
63
+ }
64
+ /** @internal */
65
+ drawBackground(render) {
66
+ if (render.backgroundLayers.length > 0) {
67
+ this.drawImageBackground(render);
68
+ return;
69
+ }
70
+ this.drawFallbackBackground(render);
71
+ }
72
+ /** @internal */
73
+ drawFallbackBackground(render) {
74
+ const { ctx, width, height, horizonY, theme } = render;
75
+ const sky = ctx.createLinearGradient(0, 0, 0, horizonY + 80);
76
+ sky.addColorStop(0, theme.skyTop);
77
+ sky.addColorStop(0.55, theme.skyBottom);
78
+ sky.addColorStop(1, theme.skyBottom);
79
+ ctx.fillStyle = sky;
80
+ ctx.fillRect(0, 0, width, horizonY + 80);
81
+ ctx.fillStyle = theme.horizonGlow;
82
+ ctx.fillRect(0, horizonY - 8, width, 18);
83
+ const sunX = width * 0.74;
84
+ const sunY = Math.max(40, horizonY * 0.54);
85
+ const sun = ctx.createRadialGradient(sunX, sunY, 8, sunX, sunY, 56);
86
+ sun.addColorStop(0, theme.sunInner);
87
+ sun.addColorStop(0.45, "rgba(255, 205, 113, 0.75)");
88
+ sun.addColorStop(1, theme.sunOuter);
89
+ ctx.fillStyle = sun;
90
+ ctx.beginPath();
91
+ ctx.arc(sunX, sunY, 56, 0, Math.PI * 2);
92
+ ctx.fill();
93
+ ctx.fillStyle = theme.mountainBack;
94
+ ctx.beginPath();
95
+ ctx.moveTo(0, horizonY + 18);
96
+ for (let x = 0; x <= width; x += 8) {
97
+ const y = horizonY +
98
+ 24 +
99
+ Math.sin(x * 0.012 + render.distanceState * 0.00015) * 22 +
100
+ Math.sin(x * 0.021 - render.distanceState * 0.00008) * 11;
101
+ ctx.lineTo(x, y);
102
+ }
103
+ ctx.lineTo(width, horizonY + 90);
104
+ ctx.lineTo(0, horizonY + 90);
105
+ ctx.closePath();
106
+ ctx.fill();
107
+ ctx.fillStyle = theme.mountainFront;
108
+ ctx.beginPath();
109
+ ctx.moveTo(0, horizonY + 34);
110
+ for (let x = 0; x <= width; x += 7) {
111
+ const y = horizonY +
112
+ 42 +
113
+ Math.sin(x * 0.01 + render.distanceState * 0.00019 + 1.1) * 30 +
114
+ Math.sin(x * 0.018 - render.distanceState * 0.0001 - 0.7) * 16;
115
+ ctx.lineTo(x, y);
116
+ }
117
+ ctx.lineTo(width, horizonY + 115);
118
+ ctx.lineTo(0, horizonY + 115);
119
+ ctx.closePath();
120
+ ctx.fill();
121
+ this.drawDirectionalHorizon(render);
122
+ if (render.groundFill.enabled) {
123
+ ctx.fillStyle = render.groundFill.color ?? theme.grassA;
124
+ ctx.fillRect(0, horizonY, width, height - horizonY);
125
+ }
126
+ }
127
+ /** @internal */
128
+ drawImageBackground(render) {
129
+ const { ctx, width, height, horizonY, theme } = render;
130
+ ctx.fillStyle = theme.skyBottom;
131
+ ctx.fillRect(0, 0, width, horizonY + 80);
132
+ for (const layer of render.backgroundLayers) {
133
+ const surface = render.resolveVisualSurface(layer.visual);
134
+ const drawWidth = Math.max(1, surface.width * layer.baseScale);
135
+ const drawHeight = Math.max(1, surface.height * layer.baseScale);
136
+ const distanceOffset = render.distanceState * layer.parallaxDistance;
137
+ const headingOffset = (render.headingState / 90) * width * layer.parallaxHeading;
138
+ const stride = drawWidth + layer.spacing;
139
+ const y = layer.y;
140
+ ctx.save();
141
+ ctx.globalAlpha = layer.alpha;
142
+ if (layer.repeat) {
143
+ let startX = layer.xOffset - distanceOffset - headingOffset;
144
+ startX = ((startX % stride) + stride) % stride - stride;
145
+ for (let x = startX; x < width + stride; x += stride) {
146
+ ctx.drawImage(surface.source, Math.round(x), Math.round(y), drawWidth, drawHeight);
147
+ }
148
+ }
149
+ else {
150
+ const x = width / 2 -
151
+ drawWidth / 2 +
152
+ layer.xOffset -
153
+ distanceOffset -
154
+ headingOffset;
155
+ ctx.drawImage(surface.source, Math.round(x), Math.round(y), drawWidth, drawHeight);
156
+ }
157
+ ctx.restore();
158
+ }
159
+ this.drawDirectionalHorizon(render);
160
+ if (render.groundFill.enabled) {
161
+ ctx.fillStyle = render.groundFill.color ?? theme.grassA;
162
+ ctx.fillRect(0, horizonY, width, height - horizonY);
163
+ }
164
+ }
165
+ /** @internal */
166
+ drawDirectionalHorizon(render) {
167
+ if (render.horizonMarkers.length === 0) {
168
+ return;
169
+ }
170
+ const visible = render.horizonMarkers
171
+ .map((marker) => {
172
+ const influenceAngle = marker.influenceAngle ?? render.horizon.influenceAngle;
173
+ const signedDelta = render.getShortestAngleDelta(render.headingState, marker.angle);
174
+ const absDelta = Math.abs(signedDelta);
175
+ if (absDelta > influenceAngle) {
176
+ return null;
177
+ }
178
+ const weight = 1 - absDelta / influenceAngle;
179
+ const easedWeight = render.easeInOut(weight);
180
+ const parallaxFactor = marker.parallaxFactor ?? render.horizon.parallaxFactor;
181
+ const x = render.width / 2 +
182
+ (signedDelta / 90) * render.width * parallaxFactor;
183
+ return {
184
+ marker,
185
+ absDelta,
186
+ alpha: marker.alpha * easedWeight,
187
+ x,
188
+ y: render.horizonY +
189
+ render.horizon.baseYOffset +
190
+ marker.yOffset,
191
+ };
192
+ })
193
+ .filter((entry) => entry !== null && entry.alpha > 0.001)
194
+ .sort((a, b) => a.absDelta - b.absDelta)
195
+ .slice(0, render.horizon.maxVisibleMarkers)
196
+ .sort((a, b) => a.alpha - b.alpha);
197
+ for (const entry of visible) {
198
+ this.drawVisual(render, entry.marker.visual, entry.x, entry.y, entry.marker.baseScale, entry.alpha, 0.5, 1);
199
+ }
200
+ }
201
+ /** @internal */
202
+ drawMinimap(render) {
203
+ if (!render.minimap.visible || render.minimapPoints.length < 2) {
204
+ return;
205
+ }
206
+ const { ctx } = render;
207
+ const points = render.minimapPoints;
208
+ const x = render.minimap.x;
209
+ const y = render.minimap.y;
210
+ const width = render.minimap.width;
211
+ const height = render.minimap.height;
212
+ const padding = render.minimap.padding;
213
+ let minX = Number.POSITIVE_INFINITY;
214
+ let maxX = Number.NEGATIVE_INFINITY;
215
+ let minY = Number.POSITIVE_INFINITY;
216
+ let maxY = Number.NEGATIVE_INFINITY;
217
+ for (const point of points) {
218
+ if (point.x < minX)
219
+ minX = point.x;
220
+ if (point.x > maxX)
221
+ maxX = point.x;
222
+ if (point.y < minY)
223
+ minY = point.y;
224
+ if (point.y > maxY)
225
+ maxY = point.y;
226
+ }
227
+ const innerWidth = Math.max(1, width - padding * 2);
228
+ const innerHeight = Math.max(1, height - padding * 2);
229
+ const boundsWidth = Math.max(1, maxX - minX);
230
+ const boundsHeight = Math.max(1, maxY - minY);
231
+ const scale = Math.min(innerWidth / boundsWidth, innerHeight / boundsHeight);
232
+ const offsetX = x + padding + (innerWidth - boundsWidth * scale) / 2 - minX * scale;
233
+ const offsetY = y + padding + (innerHeight - boundsHeight * scale) / 2 - minY * scale;
234
+ ctx.save();
235
+ ctx.globalAlpha = render.clamp01(render.minimap.alpha);
236
+ ctx.fillStyle = render.minimap.backgroundColor;
237
+ this.fillRoundedRect(ctx, x, y, width, height, 12);
238
+ if (render.minimap.borderWidth > 0) {
239
+ ctx.strokeStyle = render.minimap.borderColor;
240
+ ctx.lineWidth = render.minimap.borderWidth;
241
+ this.strokeRoundedRect(ctx, x, y, width, height, 12);
242
+ }
243
+ ctx.beginPath();
244
+ ctx.lineJoin = "round";
245
+ ctx.lineCap = "round";
246
+ ctx.lineWidth = render.minimap.trackLineWidth;
247
+ ctx.strokeStyle = render.minimap.trackColor;
248
+ ctx.moveTo(offsetX + points[0].x * scale, offsetY + points[0].y * scale);
249
+ for (let i = 1; i < points.length; i++) {
250
+ ctx.lineTo(offsetX + points[i].x * scale, offsetY + points[i].y * scale);
251
+ }
252
+ const startPoint = points[0];
253
+ const endPoint = points[points.length - 1];
254
+ const closureDistance = Math.hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
255
+ const closureTolerance = Math.max(boundsWidth, boundsHeight) * 0.08;
256
+ if (closureDistance <= closureTolerance) {
257
+ ctx.closePath();
258
+ }
259
+ ctx.stroke();
260
+ const playerPoint = render.sampleMinimapPoint(points, render.distanceState);
261
+ const playerX = offsetX + playerPoint.x * scale;
262
+ const playerY = offsetY + playerPoint.y * scale;
263
+ const radius = render.minimap.playerRadius;
264
+ ctx.beginPath();
265
+ ctx.arc(playerX, playerY, radius, 0, Math.PI * 2);
266
+ ctx.fillStyle = render.minimap.playerColor;
267
+ ctx.fill();
268
+ ctx.lineWidth = Math.max(1, render.minimap.borderWidth * 0.75);
269
+ ctx.strokeStyle = render.minimap.playerStrokeColor;
270
+ ctx.stroke();
271
+ ctx.restore();
272
+ }
273
+ /** @internal */
274
+ drawRoad(render) {
275
+ const frame = this.buildRoadDrawFrame(render);
276
+ for (const strip of frame.strips) {
277
+ this.engine.resolveRoadDrawer(strip.trackDistance).drawStrip(render.ctx, frame, strip);
278
+ }
279
+ this.engine.roadDrawerState.drawOverlay?.(render.ctx, frame);
280
+ }
281
+ /** @internal */
282
+ buildRoadDrawFrame(render) {
283
+ const strips = [];
284
+ const { width, height, current } = render;
285
+ for (let i = 0; i < render.stripCount; i++) {
286
+ const tFar = i / render.stripCount;
287
+ const tNear = (i + 1) / render.stripCount;
288
+ const aheadFar = (1 - tFar) * render.drawDistance;
289
+ const aheadNear = (1 - tNear) * render.drawDistance;
290
+ const absoluteFarDistance = render.distanceState + aheadFar;
291
+ const absoluteNearDistance = render.distanceState + aheadNear;
292
+ const far = render.projectRoadPoint(current, aheadFar, width, height);
293
+ const near = render.projectRoadPoint(current, aheadNear, width, height);
294
+ const farY = Math.floor(far.y);
295
+ const nearY = Math.ceil(near.y) + 1;
296
+ if (nearY <= farY)
297
+ continue;
298
+ const stripeIndex = Math.floor(render.distanceState * 0.05 + i);
299
+ const grassBandIndex = Math.floor(stripeIndex / render.grassBandSpan);
300
+ const shoulderBandIndex = Math.floor(stripeIndex / render.shoulderBandSpan);
301
+ const roadBandIndex = Math.floor(stripeIndex / render.roadBandSpan);
302
+ const shoulderFar = far.roadWidth * 0.12;
303
+ const shoulderNear = near.roadWidth * 0.12;
304
+ const leftOuterFar = far.x - far.roadWidth / 2 - shoulderFar;
305
+ const rightOuterFar = far.x + far.roadWidth / 2 + shoulderFar;
306
+ const leftOuterNear = near.x - near.roadWidth / 2 - shoulderNear;
307
+ const rightOuterNear = near.x + near.roadWidth / 2 + shoulderNear;
308
+ const leftRoadFar = far.x - far.roadWidth / 2;
309
+ const rightRoadFar = far.x + far.roadWidth / 2;
310
+ const leftRoadNear = near.x - near.roadWidth / 2;
311
+ const rightRoadNear = near.x + near.roadWidth / 2;
312
+ const laneMarkers = this.buildLaneMarkerStrips(render, stripeIndex, far, near, leftRoadFar, rightRoadFar, leftRoadNear, rightRoadNear);
313
+ const absoluteDistance = render.distanceState + (aheadFar + aheadNear) * 0.5;
314
+ strips.push({
315
+ index: strips.length,
316
+ stripeIndex,
317
+ absoluteDistance,
318
+ trackDistance: this.engine.trackSystem.wrapDistance(absoluteDistance),
319
+ farDistance: absoluteFarDistance,
320
+ nearDistance: absoluteNearDistance,
321
+ farProjection: this.toPublicProjection(far),
322
+ nearProjection: this.toPublicProjection(near),
323
+ farSample: this.toPublicSample(this.engine.trackSystem.sampleTrack(absoluteFarDistance)),
324
+ nearSample: this.toPublicSample(this.engine.trackSystem.sampleTrack(absoluteNearDistance)),
325
+ farY,
326
+ nearY,
327
+ grassBandIndex,
328
+ shoulderBandIndex,
329
+ roadBandIndex,
330
+ shoulderFar,
331
+ shoulderNear,
332
+ leftOuterFar,
333
+ rightOuterFar,
334
+ leftOuterNear,
335
+ rightOuterNear,
336
+ leftRoadFar,
337
+ rightRoadFar,
338
+ leftRoadNear,
339
+ rightRoadNear,
340
+ laneMarkers,
341
+ });
342
+ }
343
+ return {
344
+ width: render.width,
345
+ height: render.height,
346
+ horizonY: render.horizonY,
347
+ distanceState: render.distanceState,
348
+ drawDistance: render.drawDistance,
349
+ stripCount: render.stripCount,
350
+ laneCount: render.laneCount,
351
+ currentSample: this.toPublicSample(render.current),
352
+ theme: render.theme,
353
+ strips,
354
+ projectDistance: (absoluteDistance) => this.projectRoadSurfaceAtDistance(render, absoluteDistance),
355
+ };
356
+ }
357
+ /** @internal */
358
+ projectRoadSurfaceAtDistance(render, absoluteDistance) {
359
+ const ahead = absoluteDistance - render.distanceState;
360
+ const projection = render.projectRoadPoint(render.current, ahead, render.width, render.height);
361
+ const shoulderWidth = projection.roadWidth * 0.12;
362
+ return {
363
+ trackDistance: this.engine.trackSystem.wrapDistance(absoluteDistance),
364
+ absoluteDistance,
365
+ projection: this.toPublicProjection(projection),
366
+ shoulderWidth,
367
+ leftRoadX: projection.x - projection.roadWidth / 2,
368
+ rightRoadX: projection.x + projection.roadWidth / 2,
369
+ leftOuterX: projection.x - projection.roadWidth / 2 - shoulderWidth,
370
+ rightOuterX: projection.x + projection.roadWidth / 2 + shoulderWidth,
371
+ };
372
+ }
373
+ /** @internal */
374
+ buildLaneMarkerStrips(render, stripeIndex, far, near, leftRoadFar, rightRoadFar, leftRoadNear, rightRoadNear) {
375
+ const laneSeparators = Math.max(0, render.laneCount - 1);
376
+ if (!(laneSeparators > 0 && near.roadWidth > 90 && stripeIndex % 7 < 3)) {
377
+ return [];
378
+ }
379
+ const markers = [];
380
+ for (let laneIndex = 1; laneIndex < render.laneCount; laneIndex++) {
381
+ const laneT = laneIndex / render.laneCount;
382
+ const markerFarX = leftRoadFar + laneT * (rightRoadFar - leftRoadFar);
383
+ const markerNearX = leftRoadNear + laneT * (rightRoadNear - leftRoadNear);
384
+ markers.push({
385
+ farX: markerFarX,
386
+ nearX: markerNearX,
387
+ farWidth: Math.max(2, far.roadWidth * 0.028),
388
+ nearWidth: Math.max(2, near.roadWidth * 0.028),
389
+ });
390
+ }
391
+ return markers;
392
+ }
393
+ /** @internal */
394
+ toPublicProjection(projection) {
395
+ return {
396
+ x: projection.x,
397
+ y: projection.y,
398
+ roadWidth: projection.roadWidth,
399
+ scale: projection.scale,
400
+ t: projection.t,
401
+ };
402
+ }
403
+ /** @internal */
404
+ toPublicSample(sample) {
405
+ return {
406
+ lateralOffset: sample.lateralOffset,
407
+ elevation: sample.elevation,
408
+ curve: sample.curve,
409
+ hill: sample.hill,
410
+ worldX: sample.worldX,
411
+ worldY: sample.worldY,
412
+ heading: sample.heading,
413
+ };
414
+ }
415
+ /** @internal */
416
+ drawSceneObjects(render) {
417
+ const visible = [];
418
+ for (const billboard of render.billboards) {
419
+ const ahead = billboard.distance - render.distanceState;
420
+ if (ahead <= 0 || ahead > render.drawDistance)
421
+ continue;
422
+ const projection = render.projectObject(render.current, ahead, billboard, render.width, render.height);
423
+ if (!projection)
424
+ continue;
425
+ visible.push({
426
+ depth: ahead,
427
+ x: projection.x,
428
+ y: projection.y,
429
+ bottom: this.getVisualBottom(render, billboard.visual, projection.x, projection.y, projection.scale * billboard.baseScale, billboard.anchorX, billboard.anchorY),
430
+ scale: projection.scale * billboard.baseScale,
431
+ alpha: billboard.alpha,
432
+ visual: billboard.visual,
433
+ kind: "billboard",
434
+ anchorX: billboard.anchorX,
435
+ anchorY: billboard.anchorY,
436
+ });
437
+ }
438
+ for (const traffic of render.traffic) {
439
+ const ahead = traffic.distance - render.distanceState;
440
+ if (ahead <= -render.trafficBehindVisibilityDistance ||
441
+ ahead > render.drawDistance) {
442
+ continue;
443
+ }
444
+ const projection = render.projectTrafficPoint(render.current, ahead, render.width, render.height);
445
+ const x = projection.x + traffic.lane * projection.roadWidth * 0.38;
446
+ visible.push({
447
+ depth: ahead,
448
+ x,
449
+ y: projection.y,
450
+ bottom: this.getVisualBottom(render, traffic.visual, x, projection.y, projection.scale * traffic.baseScale, 0.5, 1),
451
+ scale: projection.scale * traffic.baseScale,
452
+ alpha: traffic.alpha,
453
+ visual: traffic.visual,
454
+ kind: "traffic",
455
+ anchorX: 0.5,
456
+ anchorY: 1,
457
+ bodyWidth: traffic.bodyWidth,
458
+ bodyLength: traffic.bodyLength,
459
+ bodyReferenceScale: traffic.baseScale,
460
+ bodyOffsetX: traffic.bodyOffsetX,
461
+ bodyOffsetY: traffic.bodyOffsetY,
462
+ resolvedBody: traffic.resolvedBody,
463
+ collisionAhead: ahead,
464
+ collisionLane: traffic.lane,
465
+ collisionCenterX: x,
466
+ });
467
+ }
468
+ const playerProjection = render.projectTrafficPoint(render.current, render.playerProjectionAheadDistance, render.width, render.height);
469
+ const playerCenterX = render.getPlayerScreenX();
470
+ visible.push({
471
+ depth: render.playerProjectionAheadDistance,
472
+ x: playerCenterX,
473
+ y: render.playerScreenY,
474
+ bottom: this.getVisualBottom(render, render.playerVisual, playerCenterX, render.playerScreenY, playerProjection.scale * render.playerBaseScale, 0.5, 1),
475
+ scale: playerProjection.scale * render.playerBaseScale,
476
+ alpha: 1,
477
+ visual: render.playerVisual,
478
+ kind: "player",
479
+ anchorX: 0.5,
480
+ anchorY: 1,
481
+ bodyWidth: render.playerBodyWidth,
482
+ bodyLength: render.playerBodyLength,
483
+ bodyReferenceScale: render.playerBaseScale,
484
+ bodyOffsetX: render.playerBodyOffsetX,
485
+ bodyOffsetY: render.playerBodyOffsetY,
486
+ resolvedBody: render.playerResolvedBody,
487
+ collisionAhead: render.playerProjectionAheadDistance,
488
+ collisionLane: render.playerLane,
489
+ collisionCenterX: playerCenterX,
490
+ });
491
+ visible.sort((a, b) => {
492
+ if (a.bottom !== b.bottom) {
493
+ return a.bottom - b.bottom;
494
+ }
495
+ return b.depth - a.depth;
496
+ });
497
+ for (const entry of visible) {
498
+ const visualRect = this.drawVisual(render, entry.visual, entry.x, entry.y, entry.scale, entry.alpha, entry.anchorX, entry.anchorY);
499
+ if (entry.kind !== "billboard") {
500
+ this.drawCarDebugBounds(render, entry.kind, visualRect, entry);
501
+ }
502
+ }
503
+ }
504
+ /** @internal */
505
+ drawVisual(render, visual, x, y, scale, alpha, anchorX, anchorY) {
506
+ const surface = render.resolveVisualSurface(visual);
507
+ const rect = this.getVisualRect(surface, x, y, scale, anchorX, anchorY);
508
+ render.ctx.save();
509
+ render.ctx.globalAlpha = render.clamp01(alpha);
510
+ render.ctx.drawImage(surface.source, rect.x, rect.y, rect.width, rect.height);
511
+ render.ctx.restore();
512
+ return rect;
513
+ }
514
+ /** @internal */
515
+ getVisualRect(surface, x, y, scale, anchorX, anchorY) {
516
+ const drawWidth = Math.max(1, surface.width * Math.max(0, scale));
517
+ const drawHeight = Math.max(1, surface.height * Math.max(0, scale));
518
+ const anchorOffsetX = drawWidth * Math.max(0, Math.min(1, anchorX));
519
+ const anchorOffsetY = drawHeight * Math.max(0, Math.min(1, anchorY));
520
+ return {
521
+ x: Math.round(x - anchorOffsetX),
522
+ y: Math.round(y - anchorOffsetY),
523
+ width: drawWidth,
524
+ height: drawHeight,
525
+ };
526
+ }
527
+ /** @internal */
528
+ getVisualBottom(render, visual, x, y, scale, anchorX, anchorY) {
529
+ const surface = render.resolveVisualSurface(visual);
530
+ const rect = this.getVisualRect(surface, x, y, scale, anchorX, anchorY);
531
+ return rect.y + rect.height;
532
+ }
533
+ /** @internal */
534
+ drawCarDebugBounds(render, kind, visualRect, entry) {
535
+ if (!render.debug.enabled)
536
+ return;
537
+ const { ctx } = render;
538
+ ctx.save();
539
+ ctx.lineWidth = render.debug.lineWidth;
540
+ ctx.setLineDash([]);
541
+ if (render.debug.visualBounds) {
542
+ ctx.strokeStyle = render.debug.visualBoundsColor;
543
+ ctx.strokeRect(visualRect.x, visualRect.y, visualRect.width, visualRect.height);
544
+ }
545
+ const collisionRect = render.debug.collisionBounds &&
546
+ Number.isFinite(entry.bodyWidth) &&
547
+ Number.isFinite(entry.bodyLength)
548
+ ? render.getDebugCollisionRectFromVisual(visualRect, entry.visual, entry.bodyReferenceScale ?? 1, entry.bodyWidth, entry.bodyLength, entry.bodyOffsetX ?? 0, entry.bodyOffsetY ?? 0)
549
+ : null;
550
+ if (render.debug.collisionBounds && collisionRect) {
551
+ ctx.strokeStyle =
552
+ kind === "player"
553
+ ? render.debug.playerCollisionBoundsColor
554
+ : render.debug.trafficCollisionBoundsColor;
555
+ ctx.setLineDash([6, 4]);
556
+ ctx.strokeRect(collisionRect.x, collisionRect.y, collisionRect.width, collisionRect.height);
557
+ }
558
+ const realCollisionRect = render.debug.realCollisionBounds &&
559
+ Number.isFinite(entry.bodyWidth) &&
560
+ Number.isFinite(entry.bodyLength) &&
561
+ Number.isFinite(entry.collisionAhead) &&
562
+ Number.isFinite(entry.collisionLane)
563
+ ? render.getRealCollisionRect(render.current, render.width, render.height, entry.collisionAhead, entry.collisionLane, entry.visual, entry.bodyReferenceScale ?? 1, entry.bodyWidth, entry.bodyLength, entry.bodyOffsetX ?? 0, entry.bodyOffsetY ?? 0, entry.resolvedBody, visualRect.x + visualRect.width / 2, visualRect.y + visualRect.height)
564
+ : null;
565
+ if (render.debug.realCollisionBounds && realCollisionRect) {
566
+ ctx.strokeStyle =
567
+ kind === "player"
568
+ ? render.debug.playerRealCollisionBoundsColor
569
+ : render.debug.trafficRealCollisionBoundsColor;
570
+ ctx.setLineDash([2, 4]);
571
+ ctx.strokeRect(realCollisionRect.x, realCollisionRect.y, realCollisionRect.width, realCollisionRect.height);
572
+ }
573
+ ctx.restore();
574
+ }
575
+ /** @internal */
576
+ fillRoundedRect(ctx, x, y, width, height, radius) {
577
+ const r = Math.max(0, Math.min(radius, width / 2, height / 2));
578
+ ctx.beginPath();
579
+ ctx.moveTo(x + r, y);
580
+ ctx.arcTo(x + width, y, x + width, y + height, r);
581
+ ctx.arcTo(x + width, y + height, x, y + height, r);
582
+ ctx.arcTo(x, y + height, x, y, r);
583
+ ctx.arcTo(x, y, x + width, y, r);
584
+ ctx.closePath();
585
+ ctx.fill();
586
+ }
587
+ /** @internal */
588
+ strokeRoundedRect(ctx, x, y, width, height, radius) {
589
+ const r = Math.max(0, Math.min(radius, width / 2, height / 2));
590
+ ctx.beginPath();
591
+ ctx.moveTo(x + r, y);
592
+ ctx.arcTo(x + width, y, x + width, y + height, r);
593
+ ctx.arcTo(x + width, y + height, x, y + height, r);
594
+ ctx.arcTo(x, y + height, x, y, r);
595
+ ctx.arcTo(x, y, x + width, y, r);
596
+ ctx.closePath();
597
+ ctx.stroke();
598
+ }
599
+ }
@@ -0,0 +1,13 @@
1
+ import { DrawSprite } from "../../minimo.js";
2
+ /** @internal */
3
+ export class ArcadeRacerRoadSprite extends DrawSprite {
4
+ constructor(width, height,
5
+ /** @internal */
6
+ redrawFrame) {
7
+ super(width, height);
8
+ this.redrawFrame = redrawFrame;
9
+ }
10
+ redraw(ctx) {
11
+ this.redrawFrame(ctx, this.width, this.height);
12
+ }
13
+ }