SOURCE / PINNED RELEASE
Made of little things.
Big Rig Rush
- Release
- ec9b6549f752…
- Author-recorded commit
- 74b729258b1a…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-0a99f0cae40
Archive hash verified: 900e22810340ccb1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { METERS } from './a10.js';
/**
* The A-10C Thunderbolt II in the game's own vehicle style: chunky rounded shapes, a
* soft gloss and a hard offset shadow, like the rig and the dozer. Laid out in metres
* on the real airframe's proportions (16.3 m long, 17.5 m span): straight wings with
* drooped tips, gear pods ahead of the wing, the two TF34 pods high on the rear
* fuselage, the twin fins at the tailplane tips, the bubble canopy well forward, the
* GAU-8's muzzle under the nose, the shark mouth, and whatever stores are still on.
*
* Banking reads at a glance: the wing that rolls down shortens and darkens, the one that
* comes up lengthens and catches the light, and the fins lean over into the turn.
*/
export interface A10Stores {
mk82: number;
gbu31: number;
mavericks: number;
}
const GREY = '#7d878e';
const LIGHT = '#95a0a7';
const DARK = '#59626a';
const INK = '#2b2b2e';
function rrect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number, fill: string | CanvasGradient): void {
ctx.beginPath();
ctx.roundRect(x, y, w, h, r);
ctx.fillStyle = fill;
ctx.fill();
}
function poly(ctx: CanvasRenderingContext2D, pts: number[], fill: string): void {
ctx.beginPath();
ctx.moveTo(pts[0], pts[1]);
for (let i = 2; i < pts.length; i += 2) ctx.lineTo(pts[i], pts[i + 1]);
ctx.closePath();
ctx.fillStyle = fill;
ctx.fill();
}
function gloss(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number): void {
const g = ctx.createLinearGradient(0, y, 0, y + h);
g.addColorStop(0, 'rgba(255, 255, 255, 0.22)');
g.addColorStop(0.45, 'rgba(255, 255, 255, 0)');
g.addColorStop(1, 'rgba(0, 0, 0, 0.25)');
rrect(ctx, x, y, w, h, r, g);
}
/** The wing's plan, root to tip, for one side (side = 1 starboard, −1 port), at `len` of full span. */
function wingPts(side: number, len: number): number[] {
const t = 0.8 + (8.75 - 0.8) * len;
const mid = 0.8 + (3.4 - 0.8) * Math.min(1, len * 1.2);
return [1.55, 0.8 * side, 1.55, mid * side, 1.0, t * side, 0.7, (t + 0.15) * side, -0.7, (t + 0.15) * side, -0.85, t * side, -1.9, mid * side, -1.9, 0.8 * side];
}
/** The jet's outline, for its shadow and its dark rim. */
function silhouette(ctx: CanvasRenderingContext2D, bank: number, fill: string): void {
const s = Math.sin(bank);
for (const side of [-1, 1]) {
const low = side * s > 0;
poly(ctx, wingPts(side, 1 - (low ? 0.3 : 0) * Math.abs(s)), fill);
poly(ctx, [-5.85, 0.35 * side, -6.05, 2.95 * side, -7.5, 2.95 * side, -7.65, 0.35 * side], fill);
}
rrect(ctx, -7.9, -0.9, 16.7, 1.8, 0.9, fill);
for (const side of [-1, 1]) rrect(ctx, -5.2, side * 1.55 - 0.68, 4, 1.36, 0.68, fill);
}
/** The A-10's shadow on the ground (true size), banked with it. */
export function drawA10Shadow(ctx: CanvasRenderingContext2D, x: number, y: number, angle: number, bank: number, alpha: number, scale = 1): void {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
const k = scale / METERS;
ctx.scale(k, k * Math.cos(bank * 0.6));
silhouette(ctx, bank, `rgba(0, 0, 0, ${alpha})`);
ctx.restore();
}
/**
* Draw the A-10 at (x, y) heading `angle`, rolled `bank` radians (positive: right wing
* down), `scale` times bigger than ground-level things (it's nearer the camera).
*/
export function drawA10(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
angle: number,
bank: number,
scale: number,
throttle: number,
time: number,
muzzle: boolean,
stores: A10Stores = { mk82: 6, gbu31: 1, mavericks: 2 },
): void {
const s = Math.sin(bank);
const k = scale / METERS;
// A hard shadow just behind it, like every other vehicle (and a rim over busy ground).
ctx.save();
ctx.translate(x + 0.5 / METERS, y + 0.7 / METERS);
ctx.rotate(angle);
ctx.scale(k, k);
silhouette(ctx, bank, 'rgba(0, 0, 0, 0.32)');
ctx.restore();
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.scale(k, k);
// Stores under the wings: only their noses and fins show past the leading edge.
const bomb = (bx: number, by: number, len: number, r: number, color: string) => {
rrect(ctx, bx - len / 2, by - r, len, r * 2, r, color);
ctx.fillStyle = INK;
ctx.fillRect(bx - len / 2 - 0.1, by - r * 2, 0.3, r * 4);
ctx.fillStyle = '#ffd400';
ctx.fillRect(bx + len * 0.32, by - r, 0.12, r * 2);
};
const mk82Stations = [3.0, -3.0, 4.2, -4.2, 5.4, -5.4];
for (let i = 0; i < Math.min(6, stores.mk82); i++) bomb(0.85, mk82Stations[i], 2.2, 0.16, '#4f5a45');
if (stores.gbu31 > 0) bomb(0.75, 2.0, 3.9, 0.25, '#5d6550');
for (const [i, my] of [7.0, -7.0].entries()) {
if (i >= stores.mavericks) continue;
rrect(ctx, -0.7, my - 0.17, 2.6, 0.34, 0.17, '#d9d6cc');
ctx.fillStyle = INK;
ctx.fillRect(1.75, my - 0.12, 0.2, 0.24);
}
// Gear pods ahead of the wing roots.
for (const side of [-1, 1]) rrect(ctx, 0.7, side * 2.3 - 0.4, 2.3, 0.8, 0.4, LIGHT);
// Wings: the low one shorter and in shadow, the high one long and in the light.
for (const side of [-1, 1]) {
const low = side * s > 0;
const lean = Math.abs(s);
const len = 1 - (low ? 0.3 : 0) * lean;
poly(ctx, wingPts(side, len), GREY);
// Lighter centre section, the drooped tip and the trailing-edge flaps and ailerons.
poly(ctx, [1.55, 0.8 * side, 1.55, 3.2 * side, 0.4, 3.2 * side, 0.4, 0.8 * side], LIGHT);
const tip = 0.8 + (8.75 - 0.8) * len;
poly(ctx, [1.0, tip * side, 0.7, (tip + 0.15) * side, -0.7, (tip + 0.15) * side, -0.85, tip * side], DARK);
ctx.fillStyle = DARK;
ctx.fillRect(-1.9, side < 0 ? -tip * 0.66 : 0.9, 0.42, tip * 0.66 - 0.9);
ctx.fillStyle = 'rgba(0, 0, 0, 0.18)';
ctx.fillRect(-1.5, side < 0 ? -tip : tip * 0.7, 0.3, tip * 0.3);
if (lean > 0.05) poly(ctx, wingPts(side, len), low ? `rgba(0, 0, 0, ${0.3 * lean})` : `rgba(255, 255, 255, ${0.16 * lean})`);
}
// Tailplane.
for (const side of [-1, 1]) {
poly(ctx, [-5.85, 0.35 * side, -6.05, 2.95 * side, -7.5, 2.95 * side, -7.65, 0.35 * side], GREY);
ctx.fillStyle = DARK;
ctx.fillRect(-7.65, side < 0 ? -2.85 : 0.4, 0.4, 2.45);
}
// Fuselage: a long rounded body, a lighter spine, the gloss.
rrect(ctx, -7.9, -0.9, 16.7, 1.8, 0.9, GREY);
rrect(ctx, -6.5, -0.42, 13.5, 0.84, 0.42, LIGHT);
gloss(ctx, -7.9, -0.9, 16.7, 1.8, 0.9);
// Shark mouth on the nose, with teeth.
ctx.fillStyle = '#c0392b';
ctx.beginPath();
ctx.moveTo(8.7, -0.55);
ctx.lineTo(7.2, 0);
ctx.lineTo(8.7, 0.55);
ctx.fill();
ctx.fillStyle = '#ffffff';
for (let i = 0; i < 3; i++) ctx.fillRect(7.6 + i * 0.35, -0.3 + (i % 2) * 0.45, 0.16, 0.18);
// The gun's muzzle, just left of the nose.
ctx.fillStyle = INK;
ctx.fillRect(8.6, -0.42, 0.55, 0.26);
// Bubble canopy with its glint.
ctx.fillStyle = '#1d2a3c';
ctx.beginPath();
ctx.ellipse(5.1, 0, 1.35, 0.55, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(170, 205, 255, 0.55)';
ctx.beginPath();
ctx.ellipse(5.45, -0.2, 0.55, 0.15, -0.2, 0, Math.PI * 2);
ctx.fill();
// The two engine pods on their pylons, with intakes and hot exhausts.
for (const side of [-1, 1]) {
const ey = side * 1.55;
ctx.fillStyle = DARK;
ctx.fillRect(-4.2, Math.min(ey, 0) + (side < 0 ? 0.5 : 0.3), 1.8, 0.75);
rrect(ctx, -5.2, ey - 0.68, 4, 1.36, 0.68, '#6a737a');
gloss(ctx, -5.2, ey - 0.68, 4, 1.36, 0.68);
rrect(ctx, -1.45, ey - 0.55, 0.3, 1.1, 0.15, INK);
rrect(ctx, -5.3, ey - 0.45, 0.3, 0.9, 0.15, INK);
if (throttle > 0.25) {
const flick = 0.85 + Math.sin(time * 40 + side) * 0.15;
ctx.fillStyle = `rgba(255, 170, 70, ${0.15 + throttle * 0.35})`;
ctx.beginPath();
ctx.ellipse(-5.6, ey, 0.45 * throttle * flick + 0.1, 0.38, 0, 0, Math.PI * 2);
ctx.fill();
}
}
// Twin fins at the tailplane tips: rolled over with the jet, they lean into the turn.
for (const side of [-1, 1]) {
const fy = side * 2.95;
const lean = s * 2.2;
poly(ctx, [-5.6, fy, -6.4, fy + lean, -7.6, fy + lean, -7.8, fy], LIGHT);
rrect(ctx, -7.6, fy + lean - 0.15, 1.2, 0.3, 0.15, DARK);
}
// Wingtip nav lights: red to port, green to starboard.
const tipL = 0.8 + 7.95 * (1 - (-s > 0 ? 0.3 * Math.abs(s) : 0)) + 0.15;
const tipR = 0.8 + 7.95 * (1 - (s > 0 ? 0.3 * Math.abs(s) : 0)) + 0.15;
ctx.fillStyle = '#ff3b3b';
ctx.fillRect(-0.2, -tipL - 0.15, 0.4, 0.3);
ctx.fillStyle = '#3dff7a';
ctx.fillRect(-0.2, tipR - 0.15, 0.4, 0.3);
// The GAU-8's muzzle flash.
if (muzzle) {
const len = 3 + Math.random() * 2.5;
poly(ctx, [9.1, -0.3, 9.1 + len, -1.2 - Math.random() * 0.6, 9.1 + len * 0.7, -0.3, 9.1 + len, 0.6 + Math.random() * 0.6], 'rgba(255, 245, 200, 0.95)');
ctx.fillStyle = 'rgba(255, 170, 60, 0.85)';
ctx.beginPath();
ctx.arc(9.3, -0.3, 0.7, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
