Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover"><style>:root{color-scheme:light;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}html{scroll-padding-top:env(safe-area-inset-top,0px)}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#fff;color:#000}img{max-width:100%}[hidden]:not([hidden=until-found i]){display:none!important}</style></head><body>
- <title>Coastline Laneways</title>
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Chakra+Petch:wght@400;600&family=Overpass:wght@600;700;800&display=swap">
- <style>
- :root {
- color-scheme: dark;
- --bg: #0d0a22;
- --panel: #17133a;
- --line: #34307a;
- --ink: #f4ead2;
- --muted: #a9a3cf;
- --accent: #ff4d6d;
- --gold: #ffc93c;
- --font-display: 'Press Start 2P', ui-monospace, Menlo, Consolas, monospace;
- --font-body: 'Chakra Petch', system-ui, -apple-system, 'Segoe UI', sans-serif;
- }
- html, body { background: var(--bg); }
- body {
- margin: 0;
- background: var(--bg);
- color: var(--ink);
- font-family: var(--font-body);
- font-size: 15px;
- line-height: 1.5;
- padding-inline: 16px;
- padding-block: 20px 32px;
- -webkit-tap-highlight-color: transparent;
- }
- .wrap { max-width: 880px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
- header { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 16px; }
- h1 {
- margin: 0; font-family: ui-monospace, Menlo, Consolas, 'Courier New', monospace; font-weight: 700; font-size: 1.5rem;
- letter-spacing: .04em; color: var(--gold); text-wrap: balance;
- }
- .sub { margin: 0; color: var(--muted); font-size: .9rem; min-width: 0; }
- .cab {
- background: #000; border: 3px solid var(--line); border-radius: 6px; padding: 6px;
- box-shadow: 0 0 0 1px #000, 0 12px 40px rgba(255, 77, 109, .12);
- }
- canvas#game { display: block; width: 100%; max-width: 100%; height: auto; aspect-ratio: 8 / 5; background: #000; touch-action: manipulation; outline: none; }
- .row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px; }
- .keys { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px 16px; margin: 0; padding: 0; }
- .keys div { display: flex; align-items: center; gap: 10px; min-width: 0; }
- .keys dt { margin: 0; display: flex; gap: 4px; flex: none; }
- .keys dd { margin: 0; color: var(--muted); font-size: .85rem; }
- kbd {
- font-family: var(--font-display); font-size: .6rem; line-height: 1; padding: 6px 7px; min-width: 14px; text-align: center;
- color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-bottom-width: 3px; border-radius: 3px;
- }
- .tip { margin: 0; color: var(--muted); font-size: .85rem; max-width: 62ch; }
- .tip b { color: var(--ink); font-weight: 600; }
- button#mute {
- font-family: var(--font-display); font-size: .6rem; color: var(--ink); background: var(--panel);
- border: 1px solid var(--line); border-radius: 3px; padding: 9px 12px; cursor: pointer;
- }
- button#mute:hover { border-color: var(--gold); }
- button#mute:focus-visible, .pad button:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
- .pad { display: none; grid-template-columns: 1fr 1fr 1.2fr 1.2fr; gap: 8px; }
- .pad button {
- font-family: var(--font-display); font-size: .7rem; color: var(--ink); background: var(--panel);
- border: 2px solid var(--line); border-bottom-width: 5px; border-radius: 6px; padding: 18px 4px;
- touch-action: none; user-select: none; -webkit-user-select: none;
- }
- .pad button.on { background: var(--accent); border-color: var(--accent); transform: translateY(2px); }
- .pad .gear { grid-column: 1 / 4; padding-block: 12px; }
- @media (pointer: coarse), (max-width: 700px) { .pad { display: grid; } }
- #stage { display: flex; flex-direction: column; gap: 16px; }
- #stage:fullscreen, #stage:-webkit-full-screen {
- background: #000; width: 100%; height: 100%; justify-content: center; align-items: center; gap: 8px; padding: 0; margin: 0;
- }
- #stage:fullscreen .cab, #stage:-webkit-full-screen .cab {
- border: 0; border-radius: 0; padding: 0; box-shadow: none; background: #000;
- width: min(100vw, calc(100vh * 1.6));
- }
- @media (pointer: coarse) {
- #stage:fullscreen .cab, #stage:-webkit-full-screen .cab { width: min(100vw, calc((100vh - 130px) * 1.6)); }
- #stage:fullscreen .pad, #stage:-webkit-full-screen .pad { width: min(100vw, 640px); padding-inline: 8px; box-sizing: border-box; }
- }
- #stage.playing { position: fixed; inset: 0; z-index: 100; box-sizing: border-box; width: 100%; height: 100%; background: #000; justify-content: center; align-items: center; gap: 8px;
- padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px); }
- #stage.playing .cab { border: 0; border-radius: 0; padding: 0; box-shadow: none; background: #000; width: min(100vw, calc(100vh * 1.6)); width: min(100vw, calc(100dvh * 1.6)); }
- @media (pointer: coarse) {
- #stage.playing .cab { width: min(100vw, calc((100vh - 130px) * 1.6)); width: min(100vw, calc((100dvh - 130px) * 1.6)); }
- #stage.playing .pad { width: min(100vw, 640px); padding-inline: 8px; box-sizing: border-box; }
- }
- html.playing, html.playing body { overflow: hidden; }
- @media (prefers-reduced-motion: reduce) { .pad button.on { transform: none; } }
- </style>
- <main class="wrap">
- <header>
- <h1>Coastline Laneways</h1>
- <p class="sub">Six lanes, five stages. Beat the clock to every checkpoint.</p>
- </header>
- <div id="stage">
- <div class="cab">
- <canvas id="game" width="640" height="400" tabindex="0" aria-label="Coastline Laneways game screen"></canvas>
- </div>
- <div class="pad" id="pad" aria-label="Touch controls">
- <button type="button" data-k="left">LEFT</button>
- <button type="button" data-k="right">RIGHT</button>
- <button type="button" data-k="gas">GAS</button>
- <button type="button" data-k="brake">BRAKE</button>
- <button type="button" data-k="gear" class="gear">SHIFT GEAR (LO / HI)</button>
- <button type="button" data-k="pause">PAUSE</button>
- </div>
- </div>
- <div class="row">
- <p class="tip">Start in <b>LO</b> gear, then shift to <b>HI</b> once you are moving. Grab coins (50), fuel cans (250, one tank lasts 40% of a stage) and lightning bolts (5 seconds at 120% speed, indestructible, +10 per bot you smash). Each checkpoint adds 55 seconds. Run out of fuel and the game is over; below half a tank the fuel alarm beeps until you refuel.</p>
- <button type="button" id="mute" aria-pressed="false">Sound on</button>
- </div>
- <dl class="keys">
- <div><dt><kbd>←</kbd><kbd>→</kbd></dt><dd>Steer</dd></div>
- <div><dt><kbd>↑</kbd></dt><dd>Gas</dd></div>
- <div><dt><kbd>↓</kbd></dt><dd>Brake</dd></div>
- <div><dt><kbd>Shift</kbd></dt><dd>Gear LO / HI</dd></div>
- <div><dt><kbd>Enter</kbd></dt><dd>Start, confirm</dd></div>
- <div><dt><kbd>Q</kbd></dt><dd>Pause / quit</dd></div>
- <div><dt><kbd>M</kbd></dt><dd>Mute</dd></div>
- </dl>
- </main>
- <script>
- (function () {
- 'use strict';
- const W = 640, H = 400;
- const cv = document.getElementById('game');
- const ctx = cv.getContext('2d');
- const FONT = '"Press Start 2P", ui-monospace, Menlo, Consolas, monospace';
- const SEG = 200, RUMBLE = 4, LANES = 6, LANEW = 1000;
- const OUTERW = LANEW * 1.2;
- const LANE_W = [OUTERW, LANEW, LANEW, LANEW, LANEW, OUTERW];
- const LANE_HALF = LANE_W.reduce(function (a, b) { return a + b; }, 0) / 2;
- const SHOULDER = 500;
- const ROADW = LANE_HALF + SHOULDER;
- const LANE_B = LANE_HALF / ROADW;
- const LANE_E = (function () { const e = [-LANE_HALF]; LANE_W.forEach(function (w) { e.push(e[e.length - 1] + w); }); return e.map(function (v) { return v / ROADW; }); })();
- const UPM = 290;
- const CAMH = 1650, DRAW = 240, FOV = 100;
- const HOR = Math.round(H * 0.43);
- const CAMD = 1 / Math.tan((FOV / 2) * Math.PI / 180);
- const PLAYERZ = CAMH * CAMD;
- const MAXSPD = SEG * 60, LOWMAX = MAXSPD * 0.6;
- const ACCEL = MAXSPD / 4.5, BRAKING = -MAXSPD * 0.9, DECEL = -MAXSPD / 6;
- const OFFDECEL = -MAXSPD / 2.2, OFFLIMIT = MAXSPD / 4, CENTRIF = 0.22;
- const FOGD = 1.3;
- const CHK = 110;
- const GSTART = 12;
- const KM_PER_SEG = 1.36 / 1000;
- const LANE_C = [0, 1, 2, 3, 4, 5].map(function (i) { return (LANE_E[i] + LANE_E[i + 1]) / 2; });
- const LANE_SPEED = [0.90, 0.80, 0.70, 0.60, 0.50, 0.40];
- const LANE_DENS = [0.80, 0.70, 0.60, 0.50, 0.40, 0.30];
- const MAX_VISIBLE = 30;
- const RAIL_LAT = 1.10, RAIL_LIM = 1.01;
- const ITEM_SPEED = 0.5;
- const TANK_SHARE = 0.4;
- const TURBO_TIME = 5, TURBO_SPEED = 1.2;
- const PTS_COIN = 50, PTS_GAS = 250, PTS_BOT = 10;
- const STAGE_NAMES = ['COASTAL HIGHWAY', 'RED MESA', 'PINE RIDGE', 'HARBOR LIGHTS', 'MOON BAY'];
- const TOWNS = [
- ['PALM CAPE', 'SALT FLATS', 'SUNRISE BEACH'],
- ['RED MESA', 'DUST CREEK', 'CANYON ROAD'],
- ['PINE RIDGE', 'FOG HOLLOW', 'AMBER WOOD'],
- ['HARBOR TOWN', 'OLD MILL', 'LIGHT PIER'],
- ['MOON BAY', 'STAR PORT', 'GLASS CITY']
- ];
- const PAL = [
- { sky: ['#2f93ff', '#c4ecff'], grass: ['#ecd596', '#e3c987'], road: ['#74747f', '#6d6d78'], rumble: ['#f0f0f0', '#e03a3e'], lane: '#f4f4f4', fog: '#c4ecff', sun: { x: .78, h: 150, r: 20, c: '#fff6c8' }, far: '#8bbcf0', near: '#3b86d8', clouds: '#ffffff' },
- { sky: ['#ff8f4f', '#ffe2a6'], grass: ['#d98a50', '#cf7f46'], road: ['#7b6a6e', '#746468'], rumble: ['#f0f0f0', '#d9392f'], lane: '#f4eedd', fog: '#ffe2a6', sun: { x: .3, h: 110, r: 34, c: '#fff0b0' }, far: '#e0a36c', near: '#b4573a', clouds: '#ffd7b0' },
- { sky: ['#4fa8e0', '#d2f0ff'], grass: ['#3aa04c', '#329643'], road: ['#5f6574', '#585d6c'], rumble: ['#f0f0f0', '#2d6fd6'], lane: '#f4f4f4', fog: '#d2f0ff', sun: { x: .2, h: 160, r: 18, c: '#ffffe0' }, far: '#7aa7c8', near: '#1f6b3a', clouds: '#ffffff' },
- { sky: ['#4b2386', '#ff9a5a'], grass: ['#3f7c52', '#397249'], road: ['#505060', '#4a4a5a'], rumble: ['#f0f0f0', '#e0457b'], lane: '#f6e7c4', fog: '#ff9a5a', sun: { x: .66, h: 30, r: 50, c: '#ffd36a' }, far: '#6a3a8c', near: '#2a1f4f', clouds: '#c46aa0' },
- { sky: ['#070723', '#2b2b72'], grass: ['#15324c', '#112b43'], road: ['#2d2d46', '#282840'], rumble: ['#9ad4ff', '#ff5fa8'], lane: '#cfd8ff', fog: '#2b2b72', sun: { x: .8, h: 150, r: 16, c: '#f3f6ff' }, far: '#1a1a55', near: '#0c0c30', stars: 1 }
- ];
- const BIOME = [
- ['palm', 'palm', 'bush', 'rock'],
- ['cactus', 'cactus', 'rock', 'bush'],
- ['pine', 'pine', 'pine', 'bush', 'house'],
- ['lamp', 'building', 'building', 'palm', 'house'],
- ['lamp', 'palm', 'building', 'bush']
- ];
- const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
- const interp = (a, b, p) => a + (b - a) * p;
- const easeIn = (a, b, p) => a + (b - a) * Math.pow(p, 2);
- const easeInOut = (a, b, p) => a + (b - a) * ((-Math.cos(p * Math.PI) / 2) + 0.5);
- function rng(seed) {
- return function () {
- seed |= 0; seed = seed + 0x6D2B79F5 | 0;
- let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
- };
- }
- function overlap(x1, w1, x2, w2, pc) {
- const half = (pc || 1) / 2;
- return !((x1 + w1 * half) < (x2 - w2 * half) || (x1 - w1 * half) > (x2 + w2 * half));
- }
- function shade(hex, amt) {
- const n = parseInt(hex.slice(1), 16);
- let r = n >> 16, g = (n >> 8) & 255, b = n & 255;
- const f = v => clamp(Math.round(amt < 0 ? v * (1 + amt) : v + (255 - v) * amt), 0, 255);
- r = f(r); g = f(g); b = f(b);
- return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
- }
- function mk(w, h, fn) {
- const c = document.createElement('canvas');
- c.width = w; c.height = h;
- fn(c.getContext('2d'), w, h);
- return c;
- }
- function rr(g, x, y, w, h, r) {
- g.beginPath();
- g.moveTo(x + r, y); g.lineTo(x + w - r, y); g.quadraticCurveTo(x + w, y, x + w, y + r);
- g.lineTo(x + w, y + h - r); g.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
- g.lineTo(x + r, y + h); g.quadraticCurveTo(x, y + h, x, y + h - r);
- g.lineTo(x, y + r); g.quadraticCurveTo(x, y, x + r, y); g.closePath();
- }
- function poly(x1, y1, x2, y2, x3, y3, x4, y4, c) {
- ctx.fillStyle = c; ctx.beginPath();
- ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.lineTo(x4, y4);
- ctx.closePath(); ctx.fill();
- }
- function txt(s, x, y, size, color, align, stroke) {
- ctx.font = size + 'px ' + FONT;
- ctx.textAlign = align || 'left';
- ctx.textBaseline = 'alphabetic';
- ctx.lineJoin = 'round';
- if (stroke !== false) {
- ctx.lineWidth = Math.max(3, size / 3.5);
- ctx.strokeStyle = '#120a2a';
- ctx.strokeText(s, x, y);
- }
- ctx.fillStyle = color;
- ctx.fillText(s, x, y);
- }
- const _cache = {};
- function cached(key, fn) { return _cache[key] || (_cache[key] = fn()); }
- const S_PX = 170;
- const SPR = {};
- function wfM(m) { return m * UPM / ROADW; }
- function makeSpr(c, wf, extra) {
- const ch = [c];
- let cur = c;
- while (cur.width > 24) {
- const nw = Math.max(1, cur.width >> 1), nh = Math.max(1, cur.height >> 1);
- const src = cur;
- const n = mk(nw, nh, function (g) { g.imageSmoothingQuality = 'high'; g.drawImage(src, 0, 0, nw, nh); });
- ch.push(n); cur = n;
- }
- const s = { w: c.width, h: c.height, wf: wf, ch: ch, solid: 0, cw: 0 };
- if (extra) for (const k in extra) s[k] = extra[k];
- return s;
- }
- function lg(g, x0, y0, x1, y1, stops) {
- const gr = g.createLinearGradient(x0, y0, x1, y1);
- for (let i = 0; i < stops.length; i++) gr.addColorStop(stops[i][0], stops[i][1]);
- return gr;
- }
- function bbox(pts) {
- let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
- for (let i = 0; i < pts.length; i++) { x0 = Math.min(x0, pts[i][0]); x1 = Math.max(x1, pts[i][0]); y0 = Math.min(y0, pts[i][1]); y1 = Math.max(y1, pts[i][1]); }
- return { x0: x0, y0: y0, x1: x1, y1: y1 };
- }
- function rp(g, pts, r) {
- const n = pts.length;
- g.beginPath();
- for (let i = 0; i < n; i++) {
- const p0 = pts[(i + n - 1) % n], p1 = pts[i], p2 = pts[(i + 1) % n];
- const d1 = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) || 1, d2 = Math.hypot(p2[0] - p1[0], p2[1] - p1[1]) || 1;
- const q = Math.min(r, d1 / 2, d2 / 2);
- const ax = p1[0] + (p0[0] - p1[0]) * q / d1, ay = p1[1] + (p0[1] - p1[1]) * q / d1;
- const bx = p1[0] + (p2[0] - p1[0]) * q / d2, by = p1[1] + (p2[1] - p1[1]) * q / d2;
- if (i === 0) g.moveTo(ax, ay); else g.lineTo(ax, ay);
- g.quadraticCurveTo(p1[0], p1[1], bx, by);
- }
- g.closePath();
- }
- function rect(x, y, w, h) { return [[x, y], [x + w, y], [x + w, y + h], [x, y + h]]; }
- function both(g, w, fn) {
- fn(false);
- g.save(); g.translate(w, 0); g.scale(-1, 1); fn(true); g.restore();
- }
- function paint(g, pts, color, r, o) {
- o = o || {};
- const b = bbox(pts), bw = b.x1 - b.x0, bh = b.y1 - b.y0;
- rp(g, pts, r);
- g.fillStyle = lg(g, 0, b.y0, 0, b.y1, [[0, shade(color, .36)], [.25, shade(color, .12)], [.55, color], [1, shade(color, -.42)]]);
- g.fill();
- g.save(); rp(g, pts, r); g.clip();
- g.fillStyle = lg(g, b.x0, 0, b.x1, 0, [[0, 'rgba(0,0,0,.45)'], [.12, 'rgba(0,0,0,.10)'], [.5, 'rgba(255,255,255,.07)'], [.88, 'rgba(0,0,0,.10)'], [1, 'rgba(0,0,0,.45)']]);
- g.fillRect(b.x0, b.y0, bw, bh);
- if (!o.noSpec) {
- g.fillStyle = lg(g, 0, b.y0, 0, b.y1, [[0, 'rgba(255,255,255,0)'], [.1, 'rgba(255,255,255,.30)'], [.2, 'rgba(255,255,255,0)'], [1, 'rgba(255,255,255,0)']]);
- g.fillRect(b.x0, b.y0, bw, bh);
- }
- g.restore();
- rp(g, pts, r); g.strokeStyle = 'rgba(0,0,0,.5)'; g.lineWidth = .012; g.stroke();
- }
- function plastic(g, pts, r) {
- const b = bbox(pts);
- rp(g, pts, r);
- g.fillStyle = lg(g, 0, b.y0, 0, b.y1, [[0, '#33363b'], [1, '#0b0c0e']]); g.fill();
- g.strokeStyle = 'rgba(0,0,0,.6)'; g.lineWidth = .01; g.stroke();
- }
- function chromeFill(g, x, y, w, h) {
- g.fillStyle = lg(g, 0, y, 0, y + h, [[0, '#f4f6f8'], [.4, '#9aa0a8'], [.55, '#e3e6ea'], [1, '#585d64']]);
- g.fillRect(x, y, w, h);
- }
- function glass(g, pts, r, inner) {
- const b = bbox(pts), bw = b.x1 - b.x0;
- rp(g, pts, r);
- g.fillStyle = lg(g, 0, b.y0, 0, b.y1, [[0, '#8ba2b7'], [.3, '#45586b'], [1, '#0d1218']]); g.fill();
- g.save(); rp(g, pts, r); g.clip();
- if (inner) inner(b);
- g.fillStyle = 'rgba(255,255,255,.17)';
- g.beginPath(); g.moveTo(b.x0 + bw * .12, b.y1); g.lineTo(b.x0 + bw * .34, b.y1); g.lineTo(b.x0 + bw * .62, b.y0); g.lineTo(b.x0 + bw * .40, b.y0); g.closePath(); g.fill();
- g.fillStyle = 'rgba(255,255,255,.08)';
- g.beginPath(); g.moveTo(b.x0 + bw * .52, b.y1); g.lineTo(b.x0 + bw * .64, b.y1); g.lineTo(b.x0 + bw * .86, b.y0); g.lineTo(b.x0 + bw * .76, b.y0); g.closePath(); g.fill();
- g.strokeStyle = 'rgba(0,0,0,.26)'; g.lineWidth = .004;
- for (let y = b.y1 - .05; y > b.y0 + .05; y -= .05) { g.beginPath(); g.moveTo(b.x0, y); g.lineTo(b.x1, y); g.stroke(); }
- g.restore();
- rp(g, pts, r); g.strokeStyle = '#090b0e'; g.lineWidth = .018; g.stroke();
- }
- function headrests(g, cx, y, gap, w, h) {
- g.fillStyle = '#12161b';
- [-1, 1].forEach(function (s) {
- rp(g, rect(cx + s * gap - w / 2, y, w, h), .03); g.fill();
- });
- }
- function shadow(g, w, h, k) {
- g.save(); g.translate(w / 2, h - .005); g.scale(1, .075);
- const r = w * (k || .68);
- const gr = g.createRadialGradient(0, 0, r * .1, 0, 0, r);
- gr.addColorStop(0, 'rgba(0,0,0,.8)'); gr.addColorStop(.65, 'rgba(0,0,0,.42)'); gr.addColorStop(1, 'rgba(0,0,0,0)');
- g.fillStyle = gr; g.beginPath(); g.arc(0, 0, r, 0, 7); g.fill(); g.restore();
- }
- function tire(g, x, y, w, h) {
- rp(g, rect(x, y, w, h), .035);
- g.fillStyle = lg(g, x, 0, x + w, 0, [[0, '#050505'], [.18, '#1b1b1d'], [.5, '#2b2b2e'], [.82, '#19191b'], [1, '#050505']]); g.fill();
- g.strokeStyle = 'rgba(0,0,0,.75)'; g.lineWidth = .009;
- for (let i = 1; i < 4; i++) { g.beginPath(); g.moveTo(x + w * i / 4, y + .02); g.lineTo(x + w * i / 4, y + h - .01); g.stroke(); }
- g.fillStyle = 'rgba(255,255,255,.07)'; g.fillRect(x + .01, y + .005, w - .02, .03);
- }
- function plate(g, cx, y, pw, ph, txt) {
- const x = cx - pw / 2;
- rp(g, rect(x, y, pw, ph), .014);
- g.fillStyle = lg(g, 0, y, 0, y + ph, [[0, '#ffffff'], [1, '#d6dae0']]); g.fill();
- g.strokeStyle = '#2b2f36'; g.lineWidth = .008; g.stroke();
- g.fillStyle = '#1a3a8a'; g.font = '700 ' + (ph * .52) + 'px Overpass, Arial, sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle';
- g.fillText(txt, cx, y + ph * .58, pw - .06);
- g.fillStyle = '#b9c1cc'; g.fillRect(x + .012, y + .01, pw - .024, ph * .1);
- g.fillStyle = '#4b5563'; g.beginPath(); g.arc(x + .035, y + ph * .5, .008, 0, 7); g.arc(x + pw - .035, y + ph * .5, .008, 0, 7); g.fill();
- }
- function tail(g, pts, r) {
- const b = bbox(pts), cx = (b.x0 + b.x1) / 2, cy = (b.y0 + b.y1) / 2, rad = Math.max(b.x1 - b.x0, b.y1 - b.y0);
- const gl = g.createRadialGradient(cx, cy, 0, cx, cy, rad * .7);
- gl.addColorStop(0, 'rgba(255,50,40,.22)'); gl.addColorStop(1, 'rgba(255,50,40,0)');
- g.fillStyle = gl; g.fillRect(cx - rad, cy - rad, rad * 2, rad * 2);
- rp(g, pts, r);
- g.fillStyle = lg(g, 0, b.y0, 0, b.y1, [[0, '#ff776b'], [.4, '#d3121f'], [1, '#68060f']]); g.fill();
- g.save(); rp(g, pts, r); g.clip();
- g.strokeStyle = 'rgba(255,175,165,.32)'; g.lineWidth = .006;
- for (let x = b.x0 - .05; x < b.x1; x += .045) { g.beginPath(); g.moveTo(x, b.y1); g.lineTo(x + .035, b.y0); g.stroke(); }
- g.fillStyle = 'rgba(255,225,215,.5)'; g.fillRect(b.x0 + .02, cy - .007, b.x1 - b.x0 - .04, .014);
- g.fillStyle = 'rgba(255,255,255,.26)'; g.fillRect(b.x0, b.y0, b.x1 - b.x0, (b.y1 - b.y0) * .2);
- g.restore();
- rp(g, pts, r); g.strokeStyle = '#220406'; g.lineWidth = .012; g.stroke();
- }
- function lens(g, x, y, w, h, col) {
- rp(g, rect(x, y, w, h), .012);
- g.fillStyle = lg(g, 0, y, 0, y + h, [[0, shade(col, .3)], [1, shade(col, -.35)]]); g.fill();
- g.strokeStyle = '#1a0a05'; g.lineWidth = .008; g.stroke();
- }
- function exhaust(g, cx, cy, rx, ry) {
- g.fillStyle = '#050506'; g.beginPath(); g.ellipse(cx, cy, rx * 1.15, ry * 1.2, 0, 0, 7); g.fill();
- g.fillStyle = lg(g, cx - rx, 0, cx + rx, 0, [[0, '#8e949b'], [.45, '#f4f6f8'], [1, '#5a6068']]);
- g.beginPath(); g.ellipse(cx, cy, rx, ry, 0, 0, 7); g.fill();
- g.fillStyle = '#0b0b0d'; g.beginPath(); g.ellipse(cx, cy + ry * .1, rx * .68, ry * .62, 0, 0, 7); g.fill();
- }
- function vehicle(wM, hM, draw) {
- const W = Math.ceil(wM * S_PX) + 20, H = Math.ceil(hM * S_PX) + 6;
- const c = mk(W, H, function (g) {
- g.imageSmoothingQuality = 'high';
- g.translate(10, 0); g.scale(S_PX, S_PX);
- draw(g, wM, hM);
- });
- return makeSpr(c, W / S_PX * UPM / ROADW, { cw: wM * UPM / ROADW, ground: 0 });
- }
- function vSedan(c, plateTxt, taxi) {
- return vehicle(1.85, 1.45, function (g, w, h) {
- shadow(g, w, h);
- tire(g, .1, 1.12, .24, .33); tire(g, w - .34, 1.12, .24, .33);
- both(g, w, function () { paint(g, [[.07, .42], [.21, .40], [.22, .50], [.08, .51]], c, .025, { noSpec: true }); });
- paint(g, [[.19, .72], [.31, .31], [.49, .15], [w - .49, .15], [w - .31, .31], [w - .19, .72]], c, .09);
- glass(g, [[.27, .68], [.385, .32], [.53, .215], [w - .53, .215], [w - .385, .32], [w - .27, .68]], .07, function (b) {
- headrests(g, w / 2, .44, .27, .17, .13);
- g.fillStyle = '#ff4338'; g.fillRect(w / 2 - .2, b.y1 - .045, .4, .018);
- });
- if (taxi) {
- paint(g, rect(w / 2 - .22, .06, .44, .1), '#f4f0e0', .03);
- g.fillStyle = '#2b2b2b'; g.font = '700 .06px Overpass, Arial'; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText('TAXI', w / 2, .112);
- }
- paint(g, [[.09, .66], [w - .09, .66], [w - .012, .80], [w - .02, 1.08], [w - .07, 1.29], [.07, 1.29], [.02, 1.08], [.012, .80]], c, .07);
- g.fillStyle = 'rgba(255,255,255,.14)'; rp(g, [[.12, .675], [w - .12, .675], [w - .05, .76], [.05, .76]], .03); g.fill();
- both(g, w, function () {
- tail(g, [[.035, .79], [.58, .755], [.60, .81], [.56, .91], [.065, .955]], .02);
- lens(g, .075, .885, .2, .035, '#ff9a1a');
- });
- chromeFill(g, .60, .835, w - 1.2, .022);
- g.fillStyle = lg(g, 0, .70, 0, .78, [[0, '#f2f4f6'], [1, '#6c727a']]); g.beginPath(); g.ellipse(w / 2, .765, .045, .032, 0, 0, 7); g.fill();
- plate(g, w / 2, .88, .40, .20, plateTxt);
- plastic(g, [[.06, 1.11], [w - .06, 1.11], [w - .10, 1.285], [.10, 1.285]], .05);
- both(g, w, function () { lens(g, .115, 1.135, .11, .04, '#e5222c'); });
- exhaust(g, .42, 1.225, .055, .033); exhaust(g, w - .42, 1.225, .055, .033);
- });
- }
- function vHatch(c, plateTxt) {
- return vehicle(1.8, 1.5, function (g, w, h) {
- shadow(g, w, h);
- tire(g, .1, 1.17, .23, .33); tire(g, w - .33, 1.17, .23, .33);
- both(g, w, function () { paint(g, [[.05, .46], [.19, .44], [.20, .54], [.06, .55]], c, .025, { noSpec: true }); });
- paint(g, [[.14, .80], [.22, .30], [.40, .12], [w - .40, .12], [w - .22, .30], [w - .14, .80]], c, .09);
- glass(g, [[.22, .76], [.30, .32], [.43, .20], [w - .43, .20], [w - .30, .32], [w - .22, .76]], .07, function (b) {
- headrests(g, w / 2, .5, .26, .16, .13);
- g.fillStyle = '#10141a'; g.fillRect(b.x0, b.y1 - .12, b.x1 - b.x0, .12);
- });
- paint(g, [[.36, .10], [w - .36, .10], [w - .31, .165], [.31, .165]], c, .03);
- paint(g, [[.08, .72], [w - .08, .72], [w - .012, .86], [w - .02, 1.15], [w - .07, 1.34], [.07, 1.34], [.02, 1.15], [.012, .86]], c, .07);
- both(g, w, function () {
- tail(g, [[.035, .80], [.23, .78], [.25, .98], [.07, 1.02], [.03, .92]], .02);
- lens(g, .05, 1.0, .17, .04, '#ff9a1a');
- });
- chromeFill(g, .30, .80, w - .6, .02);
- plate(g, w / 2, .92, .40, .20, plateTxt);
- plastic(g, [[.06, 1.17], [w - .06, 1.17], [w - .10, 1.335], [.10, 1.335]], .05);
- exhaust(g, .48, 1.285, .05, .03);
- both(g, w, function () { lens(g, .12, 1.195, .1, .04, '#e5222c'); });
- });
- }
- function vSuv(c, plateTxt) {
- return vehicle(1.95, 1.75, function (g, w, h) {
- shadow(g, w, h);
- tire(g, .1, 1.36, .26, .39); tire(g, w - .36, 1.36, .26, .39);
- both(g, w, function () { paint(g, [[.06, .52], [.2, .5], [.21, .62], [.07, .63]], c, .025, { noSpec: true }); });
- paint(g, [[.11, .88], [.16, .26], [.26, .10], [w - .26, .10], [w - .16, .26], [w - .11, .88]], c, .08);
- chromeFill(g, .22, .075, w - .44, .02);
- glass(g, [[.19, .82], [.23, .31], [.31, .20], [w - .31, .20], [w - .23, .31], [w - .19, .82]], .06, function (b) {
- headrests(g, w / 2, .52, .3, .17, .14);
- g.fillStyle = '#10141a'; g.fillRect(b.x0, b.y1 - .16, b.x1 - b.x0, .16);
- });
- paint(g, [[.28, .075], [w - .28, .075], [w - .24, .15], [.24, .15]], c, .03);
- paint(g, [[.07, .80], [w - .07, .80], [w - .01, .94], [w - .02, 1.34], [w - .07, 1.52], [.07, 1.52], [.02, 1.34], [.01, .94]], c, .07);
- both(g, w, function () {
- tail(g, [[.03, .90], [.20, .86], [.23, 1.05], [.21, 1.22], [.06, 1.26], [.025, 1.1]], .02);
- lens(g, .05, 1.12, .12, .04, '#ff9a1a');
- });
- plate(g, w / 2, 1.03, .40, .20, plateTxt);
- g.fillStyle = '#1b1d21'; g.fillRect(w / 2 - .15, .94, .3, .03);
- plastic(g, [[.06, 1.34], [w - .06, 1.34], [w - .10, 1.50], [.10, 1.50]], .05);
- g.fillStyle = lg(g, 0, 1.30, 0, 1.36, [[0, '#d6dade'], [1, '#7a8088']]); g.fillRect(.2, 1.345, w - .4, .02);
- both(g, w, function () { lens(g, .11, 1.37, .11, .04, '#e5222c'); });
- exhaust(g, .46, 1.46, .05, .03);
- });
- }
- function vCoupe(c, plateTxt) {
- return vehicle(1.9, 1.2, function (g, w, h) {
- shadow(g, w, h);
- tire(g, .05, .93, .28, .27); tire(g, w - .33, .93, .28, .27);
- paint(g, [[.25, .56], [.40, .22], [.58, .13], [w - .58, .13], [w - .40, .22], [w - .25, .56]], c, .08);
- glass(g, [[.33, .53], [.46, .26], [.60, .185], [w - .60, .185], [w - .46, .26], [w - .33, .53]], .06, function () { headrests(g, w / 2, .36, .24, .15, .12); });
- paint(g, [[.06, .50], [w - .06, .50], [w - .005, .62], [w - .005, .88], [w - .05, 1.03], [.05, 1.03], [.005, .88], [.005, .62]], c, .07);
- paint(g, [[.18, .46], [w - .18, .46], [w - .21, .535], [.21, .535]], c, .03);
- both(g, w, function () { tail(g, [[.03, .60], [.78, .585], [.82, .70], [.06, .76]], .025); });
- g.fillStyle = lg(g, 0, .6, 0, .67, [[0, '#a30f1a'], [1, '#4d060c']]); g.fillRect(.8, .595, w - 1.6, .07);
- plate(g, w / 2, .72, .40, .19, plateTxt);
- plastic(g, [[.10, .86], [w - .10, .86], [w - .15, 1.03], [.15, 1.03]], .04);
- g.strokeStyle = 'rgba(255,255,255,.12)'; g.lineWidth = .008;
- for (let i = 1; i < 7; i++) { g.beginPath(); g.moveTo(.3 + i * .2, .9); g.lineTo(.3 + i * .2, 1.02); g.stroke(); }
- exhaust(g, .46, .98, .05, .03); exhaust(g, .62, .98, .05, .03); exhaust(g, w - .46, .98, .05, .03); exhaust(g, w - .62, .98, .05, .03);
- });
- }
- function vPickup(c, plateTxt) {
- return vehicle(2.0, 1.95, function (g, w, h) {
- shadow(g, w, h);
- tire(g, .07, 1.5, .28, .45); tire(g, w - .35, 1.5, .28, .45);
- paint(g, [[.17, .82], [.20, .22], [.30, .09], [w - .30, .09], [w - .20, .22], [w - .17, .82]], c, .07);
- glass(g, [[.33, .50], [.34, .24], [.42, .17], [w - .42, .17], [w - .34, .24], [w - .33, .50]], .04, function () { headrests(g, w / 2, .36, .28, .14, .14); });
- both(g, w, function () { g.fillStyle = '#e8b04a'; rp(g, rect(.32, .56, .05, .02), .005); g.fill(); });
- rp(g, [[.07, .78], [w - .07, .78], [w - .035, .98], [.035, .98]], .02);
- g.fillStyle = lg(g, 0, .78, 0, .98, [[0, '#1b1d20'], [1, '#08090a']]); g.fill();
- g.strokeStyle = 'rgba(255,255,255,.07)'; g.lineWidth = .008;
- for (let i = 1; i < 9; i++) { g.beginPath(); g.moveTo(.1 + i * (w - .2) / 9, .8); g.lineTo(.07 + i * (w - .14) / 9, .97); g.stroke(); }
- paint(g, rect(.0, .80, .09, .22), c, .03); paint(g, rect(w - .09, .80, .09, .22), c, .03);
- paint(g, [[.02, .94], [w - .02, .94], [w - .0, 1.0], [w - .0, 1.46], [w - .05, 1.5], [.05, 1.5], [.0, 1.46], [.0, 1.0]], c, .05);
- g.strokeStyle = 'rgba(0,0,0,.28)'; g.lineWidth = .01; rp(g, [[.2, 1.05], [w - .2, 1.05], [w - .2, 1.38], [.2, 1.38]], .03); g.stroke();
- both(g, w, function () { tail(g, [[.03, .97], [.17, .97], [.17, 1.34], [.04, 1.36], [.02, 1.1]], .02); lens(g, .035, 1.2, .12, .04, '#ff9a1a'); });
- chromeFill(g, w / 2 - .15, 1.0, .3, .04);
- g.fillStyle = 'rgba(255,255,255,.55)'; g.font = '700 .09px Overpass, Arial'; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText('RANGER', w / 2, 1.1);
- plate(g, w / 2, 1.18, .40, .20, plateTxt);
- paint(g, [[.06, 1.50], [w - .06, 1.50], [w - .09, 1.62], [.09, 1.62]], '#aeb4bb', .03);
- both(g, w, function () { lens(g, .11, 1.53, .11, .035, '#e5222c'); });
- g.fillStyle = '#0d0e10'; g.fillRect(w / 2 - .06, 1.6, .12, .08);
- });
- }
- function vVan(c, plateTxt) {
- return vehicle(2.05, 2.25, function (g, w, h) {
- shadow(g, w, h);
- tire(g, .12, 1.86, .24, .39); tire(g, w - .36, 1.86, .24, .39);
- both(g, w, function () { paint(g, [[.06, .52], [.2, .5], [.21, .62], [.07, .63]], c, .025, { noSpec: true }); });
- paint(g, [[.06, .05], [w - .06, .05], [w - .015, .14], [w - .015, 1.92], [w - .07, 2.08], [.07, 2.08], [.015, 1.92], [.015, .14]], c, .07);
- both(g, w, function () {
- glass(g, [[.15, .20], [w / 2 - .06, .20], [w / 2 - .06, .90], [.15, .90]], .04, function (b) {
- g.fillStyle = '#12161b'; g.fillRect(b.x0, b.y1 - .22, b.x1 - b.x0, .22);
- });
- g.fillStyle = '#0c0d0f'; g.fillRect(w / 2 - .12, 1.10, .035, .09);
- });
- g.strokeStyle = 'rgba(0,0,0,.55)'; g.lineWidth = .012;
- g.beginPath(); g.moveTo(w / 2, .08); g.lineTo(w / 2, 1.86); g.stroke();
- g.strokeStyle = 'rgba(0,0,0,.22)'; g.lineWidth = .008; rp(g, [[.1, 1.0], [w / 2 - .03, 1.0], [w / 2 - .03, 1.75], [.1, 1.75]], .03); g.stroke(); rp(g, [[w / 2 + .03, 1.0], [w - .1, 1.0], [w - .1, 1.75], [w / 2 + .03, 1.75]], .03); g.stroke();
- both(g, w, function () {
- tail(g, [[.025, 1.02], [.15, 1.0], [.16, 1.44], [.04, 1.46]], .02);
- lens(g, .03, 1.46, .13, .1, '#f2f2f2');
- lens(g, .03, .94, .13, .06, '#ff9a1a');
- });
- plate(g, w / 2, 1.5, .40, .20, plateTxt);
- plastic(g, [[.07, 1.82], [w - .07, 1.82], [w - .10, 2.06], [.10, 2.06]], .05);
- g.fillStyle = lg(g, 0, 1.8, 0, 1.86, [[0, '#cfd3d8'], [1, '#6b7078']]); g.fillRect(.2, 1.82, w - .4, .035);
- both(g, w, function () { lens(g, .12, 1.88, .12, .045, '#e5222c'); });
- });
- }
- function vSemi(boxCol, logo, logoCol, plateTxt) {
- return vehicle(2.55, 4.05, function (g, w, h) {
- shadow(g, w, h, .62);
- g.fillStyle = lg(g, 0, 2.9, 0, 4.0, [[0, '#0a0b0d'], [.6, '#16181c'], [1, '#26292e']]); g.fillRect(.2, 2.9, w - .4, 1.1);
- g.fillStyle = lg(g, 0, 2.9, 0, 3.12, [[0, '#4a4f57'], [1, '#17181b']]); g.fillRect(.3, 2.9, w - .6, .2);
- g.fillStyle = '#2e3238'; g.fillRect(.5, 3.35, w - 1.0, .09); g.fillRect(.5, 3.7, w - 1.0, .09);
- g.fillStyle = '#3c4148'; g.fillRect(.7, 3.1, .07, .62); g.fillRect(w - .77, 3.1, .07, .62);
- g.fillStyle = '#5a6068'; g.fillRect(.9, 3.45, w - 1.8, .05);
- g.fillStyle = '#1a1c20'; g.fillRect(.15, 2.88, w - .3, .1);
- both(g, w, function () { tire(g, .10, 3.2, .23, .85); tire(g, .35, 3.2, .23, .85); });
- both(g, w, function () {
- rp(g, rect(.22, 3.12, .3, .62), .02); g.fillStyle = '#0d0e10'; g.fill();
- g.fillStyle = '#e8e8e8'; g.fillRect(.27, 3.4, .2, .045);
- });
- paint(g, [[.0, .06], [w, .06], [w, 2.9], [0, 2.9]], boxCol, .03, { noSpec: true });
- g.save(); rp(g, [[.0, .06], [w, .06], [w, 2.9], [0, 2.9]], .03); g.clip();
- g.strokeStyle = 'rgba(0,0,0,.09)'; g.lineWidth = .01;
- for (let x = .06; x < w; x += .085) { g.beginPath(); g.moveTo(x, .1); g.lineTo(x, 2.85); g.stroke(); }
- g.fillStyle = lg(g, 0, .06, 0, 2.9, [[0, 'rgba(255,255,255,.2)'], [.12, 'rgba(255,255,255,0)'], [1, 'rgba(0,0,0,.2)']]); g.fillRect(0, .06, w, 2.84);
- g.restore();
- chromeFill(g, 0, .06, w, .06); chromeFill(g, 0, 2.82, w, .08);
- g.strokeStyle = 'rgba(0,0,0,.55)'; g.lineWidth = .014;
- g.beginPath(); g.moveTo(w / 2, .12); g.lineTo(w / 2, 2.82); g.stroke();
- both(g, w, function () {
- chromeFill(g, .03, .2, .05, 2.5);
- [.46, .6].forEach(function (x) { chromeFill(g, x, .22, .022, 2.46); });
- chromeFill(g, .52 - .01, 1.35, .14, .05);
- g.fillStyle = 'rgba(0,0,0,.18)'; g.fillRect(.09, .24, .012, 2.5);
- });
- g.fillStyle = logoCol; g.font = '800 .34px Overpass, Arial, sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle';
- if ('letterSpacing' in g) g.letterSpacing = '0.02px';
- g.fillText(logo, w / 2, 1.0, w - .3);
- if ('letterSpacing' in g) g.letterSpacing = '0px';
- g.fillRect(.3, 1.28, w - .6, .045);
- g.font = '700 .1px Overpass, Arial'; g.fillText('FREIGHT · EXPRESS · COAST TO COAST', w / 2, 1.5);
- for (let x = 0; x < w; x += .12) { g.fillStyle = (x / .12) % 2 < 1 ? '#c8202a' : '#f2f2f2'; g.fillRect(x, 2.66, Math.min(.12, w - x), .07); }
- for (let y = 1.9; y < 2.66; y += .12) { g.fillStyle = (y / .12) % 2 < 1 ? '#c8202a' : '#f2f2f2'; g.fillRect(0, y, .06, .12); g.fillRect(w - .06, y, .06, .12); }
- both(g, w, function () {
- [.16, .34].forEach(function (x) {
- const gr = g.createRadialGradient(x, 2.5, .005, x, 2.5, .08);
- gr.addColorStop(0, '#ff9188'); gr.addColorStop(.5, '#d3121f'); gr.addColorStop(1, '#5a050c');
- g.fillStyle = 'rgba(255,50,40,.25)'; g.beginPath(); g.arc(x, 2.5, .15, 0, 7); g.fill();
- g.fillStyle = '#111'; g.beginPath(); g.arc(x, 2.5, .09, 0, 7); g.fill();
- g.fillStyle = gr; g.beginPath(); g.arc(x, 2.5, .075, 0, 7); g.fill();
- });
- lens(g, .1, 2.24, .08, .05, '#ff9a1a');
- });
- plate(g, w - .62, 2.1, .40, .2, plateTxt);
- g.fillStyle = lg(g, 0, 3.0, 0, 3.16, [[0, '#8a9098'], [1, '#2c2f34']]); g.fillRect(.25, 3.02, w - .5, .13);
- g.fillStyle = '#17181b'; g.fillRect(.4, 2.9, .05, .13); g.fillRect(w - .45, 2.9, .05, .13);
- both(g, w, function () { g.fillStyle = '#17181b'; g.fillRect(.6, 3.15, .02, .5); });
- });
- }
- function vConvertible() {
- return vehicle(1.95, 1.15, function (g, w, h) {
- shadow(g, w, h, .7);
- tire(g, .03, .85, .32, .3); tire(g, w - .35, .85, .32, .3);
- const red = '#d4121e';
- paint(g, [[.30, .035], [w - .30, .035], [w - .24, .16], [.24, .16]], red, .035);
- rp(g, [[.34, .06], [w - .34, .06], [w - .29, .14], [.29, .14]], .02);
- g.fillStyle = lg(g, 0, .06, 0, .14, [[0, '#6f879c'], [1, '#1c2a37']]); g.fill();
- both(g, w, function () {
- rp(g, [[.30, .40], [.34, .20], [.46, .17], [.84, .17], [.90, .20], [.92, .40]], .05);
- g.fillStyle = lg(g, 0, .17, 0, .40, [[0, '#d9b072'], [1, '#8c6531']]); g.fill();
- g.strokeStyle = 'rgba(0,0,0,.4)'; g.lineWidth = .01; g.stroke();
- });
- const man = { cx: .62, hair: '#4a2a17', shirt: '#2f6fd6' };
- const wom = { cx: w - .62, hair: '#f3d37e', shirt: '#f4f1ec' };
- [man, wom].forEach(function (p, i) {
- g.fillStyle = lg(g, p.cx - .22, 0, p.cx + .22, 0, [[0, shade(p.shirt, -.25)], [.5, p.shirt], [1, shade(p.shirt, -.3)]]);
- rp(g, [[p.cx - .22, .42], [p.cx - .2, .27], [p.cx - .1, .21], [p.cx + .1, .21], [p.cx + .2, .27], [p.cx + .22, .42]], .06); g.fill();
- g.fillStyle = '#d9a67f'; g.fillRect(p.cx - .035, .15, .07, .08);
- g.fillStyle = lg(g, 0, .0, 0, .2, [[0, shade(p.hair, .15)], [1, shade(p.hair, -.25)]]);
- g.beginPath(); g.ellipse(p.cx, .115, .092, .105, 0, 0, 7); g.fill();
- if (i === 1) {
- g.beginPath(); g.moveTo(p.cx - .09, .10); g.quadraticCurveTo(p.cx - .2, .2, p.cx - .17, .31); g.lineTo(p.cx - .07, .22); g.fill();
- g.beginPath(); g.moveTo(p.cx + .09, .10); g.quadraticCurveTo(p.cx + .2, .2, p.cx + .15, .30); g.lineTo(p.cx + .07, .22); g.fill();
- }
- });
- paint(g, [[.10, .40], [w - .10, .40], [w - .01, .58], [w, .80], [w - .05, 1.0], [.05, 1.0], [0, .80], [.01, .58]], red, .07);
- paint(g, [[.32, .385], [w - .32, .385], [w - .30, .43], [.30, .43]], red, .02);
- rp(g, [[.50, .44], [w - .50, .44], [w - .44, .60], [.44, .60]], .02);
- g.fillStyle = '#0c0d0f'; g.fill();
- g.strokeStyle = 'rgba(255,255,255,.22)'; g.lineWidth = .01;
- for (let y = .47; y < .6; y += .03) { g.beginPath(); g.moveTo(.5, y); g.lineTo(w - .5, y); g.stroke(); }
- both(g, w, function () {
- rp(g, [[.05, .56], [.30, .52], [.30, .74], [.04, .74]], .02); g.fillStyle = '#0c0d0f'; g.fill();
- g.strokeStyle = 'rgba(255,255,255,.18)'; g.lineWidth = .008;
- for (let y = .6; y < .73; y += .035) { g.beginPath(); g.moveTo(.06, y); g.lineTo(.29, y - .02); g.stroke(); }
- });
- rp(g, [[.22, .64], [w - .22, .64], [w - .20, .92], [.20, .92]], .03);
- g.fillStyle = lg(g, 0, .64, 0, .92, [[0, '#26282c'], [1, '#0a0b0c']]); g.fill();
- both(g, w, function () {
- [.36, .60].forEach(function (x) {
- g.fillStyle = 'rgba(255,40,30,.3)'; g.beginPath(); g.arc(x, .78, .15, 0, 7); g.fill();
- g.fillStyle = '#9ea4ab'; g.beginPath(); g.arc(x, .78, .097, 0, 7); g.fill();
- g.fillStyle = '#111'; g.beginPath(); g.arc(x, .78, .088, 0, 7); g.fill();
- const gr = g.createRadialGradient(x - .02, .75, .005, x, .78, .08);
- gr.addColorStop(0, '#ffb0a6'); gr.addColorStop(.45, '#e0182a'); gr.addColorStop(1, '#6a0610');
- g.fillStyle = gr; g.beginPath(); g.arc(x, .78, .078, 0, 7); g.fill();
- g.strokeStyle = 'rgba(255,255,255,.2)'; g.lineWidth = .006; g.beginPath(); g.arc(x, .78, .05, 0, 7); g.stroke();
- });
- });
- plate(g, w / 2, .70, .40, .2, 'OR 1986');
- plastic(g, [[.14, .93], [w - .14, .93], [w - .22, 1.0], [.22, 1.0]], .02);
- g.strokeStyle = 'rgba(255,255,255,.14)'; g.lineWidth = .007;
- for (let i = 1; i < 12; i++) { g.beginPath(); g.moveTo(.2 + i * (w - .4) / 12, .94); g.lineTo(.2 + i * (w - .4) / 12, .995); g.stroke(); }
- exhaust(g, w / 2 - .30, .995, .055, .032); exhaust(g, w / 2 + .30, .995, .055, .032);
- g.fillStyle = '#f4d35e'; g.beginPath(); g.moveTo(w / 2, .435); g.lineTo(w / 2 + .03, .46); g.lineTo(w / 2, .485); g.lineTo(w / 2 - .03, .46); g.fill();
- });
- }
- const SG = 75;
- function sf(size, weight) { return (weight || 700) + ' ' + size + 'px Overpass, "Arial Narrow", Arial, sans-serif'; }
- function stext(g, s, x, y, size, color, align, weight, spacing) {
- g.font = sf(size, weight); g.textAlign = align || 'center'; g.textBaseline = 'middle';
- if ('letterSpacing' in g) g.letterSpacing = (spacing || 0) + 'px';
- g.fillStyle = color; g.fillText(s, x, y);
- if ('letterSpacing' in g) g.letterSpacing = '0px';
- }
- function fitText(g, s, x, y, maxW, size, color, weight, spacing) {
- let sz = size;
- g.font = sf(sz, weight);
- while (sz > 8 && g.measureText(s).width > maxW) { sz -= 2; g.font = sf(sz, weight); }
- stext(g, s, x, y, sz, color, 'center', weight, spacing);
- }
- function postFill(g, x, y, w, h) {
- g.fillStyle = lg(g, x, 0, x + w, 0, [[0, '#6f757d'], [.3, '#eef0f3'], [.65, '#aab0b8'], [1, '#575c64']]);
- g.fillRect(x, y, w, h);
- }
- function panel(g, x, y, w, h, bg, border, r, borderW) {
- g.fillStyle = 'rgba(0,0,0,.28)'; rr(g, x + 3, y + 5, w, h, r); g.fill();
- rr(g, x, y, w, h, r); g.fillStyle = '#d3d8de'; g.fill();
- rr(g, x + 1.5, y + 1.5, w - 3, h - 3, r - 1); g.fillStyle = lg(g, 0, y, 0, y + h, [[0, shade(bg, .14)], [1, shade(bg, -.12)]]); g.fill();
- rr(g, x + 7, y + 7, w - 14, h - 14, Math.max(2, r - 4)); g.strokeStyle = border; g.lineWidth = borderW || 3; g.stroke();
- g.save(); rr(g, x + 1.5, y + 1.5, w - 3, h - 3, r - 1); g.clip();
- g.fillStyle = 'rgba(255,255,255,.09)'; g.beginPath(); g.moveTo(x, y + h * .65); g.lineTo(x + w * .55, y); g.lineTo(x + w * .8, y); g.lineTo(x, y + h * .95); g.closePath(); g.fill();
- g.restore();
- }
- function baseShadow(g, cx, y, rx) {
- g.save(); g.translate(cx, y); g.scale(1, .22);
- const gr = g.createRadialGradient(0, 0, 1, 0, 0, rx);
- gr.addColorStop(0, 'rgba(0,0,0,.5)'); gr.addColorStop(1, 'rgba(0,0,0,0)');
- g.fillStyle = gr; g.beginPath(); g.arc(0, 0, rx, 0, 7); g.fill(); g.restore();
- }
- function signSprite(wM, hM, draw, extra) {
- const W = Math.ceil(wM * SG), H = Math.ceil(hM * SG);
- const c = mk(W, H, function (g, w, h) { g.imageSmoothingQuality = 'high'; draw(g, w, h); });
- return makeSpr(c, wfM(wM), extra);
- }
- function sgSpeed(limit) {
- return signSprite(2.0, 4.6, function (g, w, h) {
- postFill(g, w / 2 - 4.5, 150, 9, h - 150); baseShadow(g, w / 2, h - 2, 26);
- panel(g, 14, 6, w - 28, 160, '#f6f6f2', '#101010', 10, 3.5);
- stext(g, 'SPEED', w / 2, 36, 28, '#101010', 'center', 800, 1);
- stext(g, 'LIMIT', w / 2, 62, 28, '#101010', 'center', 800, 1);
- stext(g, String(limit), w / 2, 118, 70, '#101010', 'center', 800, 0);
- g.fillStyle = '#7b8088'; g.beginPath(); g.arc(w / 2 - 20, 155, 2.2, 0, 7); g.arc(w / 2 + 20, 155, 2.2, 0, 7); g.fill();
- }, { solid: .02 });
- }
- function sgKeepRight() {
- return signSprite(1.9, 4.6, function (g, w, h) {
- postFill(g, w / 2 - 4.5, 160, 9, h - 160); baseShadow(g, w / 2, h - 2, 26);
- panel(g, 14, 6, w - 28, 170, '#f6f6f2', '#101010', 10, 3.5);
- ['KEEP', 'RIGHT', 'EXCEPT', 'TO PASS'].forEach(function (s, i) { stext(g, s, w / 2, 32 + i * 38, 31, '#101010', 'center', 800, 1); });
- }, { solid: .02 });
- }
- function sgKm(km) {
- return signSprite(1.3, 3.6, function (g, w, h) {
- postFill(g, w / 2 - 3.5, 100, 7, h - 100); baseShadow(g, w / 2, h - 2, 22);
- panel(g, 8, 4, w - 16, 104, '#0d6b3a', '#ffffff', 7, 2.5);
- stext(g, 'KM', w / 2, 28, 20, '#ffffff', 'center', 700, 2);
- stext(g, String(km), w / 2, 66, 46, '#ffffff', 'center', 800, 0);
- }, { solid: .015 });
- }
- function sgGreen(top, mid, bot) {
- return signSprite(3.6, 4.3, function (g, w, h) {
- postFill(g, 50, 120, 8, h - 120); postFill(g, w - 58, 120, 8, h - 120); baseShadow(g, 54, h - 2, 24); baseShadow(g, w - 54, h - 2, 24);
- panel(g, 6, 4, w - 12, 128, '#0d6b3a', '#ffffff', 8, 3);
- fitText(g, top, w / 2, 40, w - 70, 30, '#ffffff', 700, 1);
- fitText(g, mid, w / 2, 76, w - 70, 40, '#ffffff', 800, 1);
- fitText(g, bot, w / 2, 110, w - 70, 24, '#ffffff', 700, 1);
- }, { solid: .02 });
- }
- function sgRest(km) {
- return signSprite(1.9, 4.3, function (g, w, h) {
- postFill(g, w / 2 - 4, 150, 8, h - 150); baseShadow(g, w / 2, h - 2, 26);
- panel(g, 10, 4, w - 20, 154, '#0b4f9c', '#ffffff', 8, 3);
- g.fillStyle = '#ffffff'; rr(g, w / 2 - 25, 22, 50, 54, 6); g.fill();
- stext(g, 'P', w / 2, 51, 44, '#0b4f9c', 'center', 800, 0);
- stext(g, 'REST AREA', w / 2, 100, 22, '#ffffff', 'center', 700, 1);
- stext(g, km, w / 2, 130, 22, '#ffffff', 'center', 700, 0);
- }, { solid: .02 });
- }
- function sgChevron(dir) {
- return signSprite(1.5, 3.7, function (g, w, h) {
- postFill(g, w / 2 - 4, 110, 8, h - 110); baseShadow(g, w / 2, h - 2, 22);
- panel(g, 6, 4, w - 12, 120, '#f4c20d', '#111111', 8, 3);
- g.fillStyle = '#111'; g.save(); g.translate(w / 2, 64); g.scale(dir, 1);
- g.beginPath(); g.moveTo(-22, -40); g.lineTo(2, -40); g.lineTo(26, 0); g.lineTo(2, 40); g.lineTo(-22, 40); g.lineTo(2, 0); g.closePath(); g.fill();
- g.restore();
- }, { solid: .015 });
- }
- function sgCurve(dir) {
- return signSprite(2.0, 4.6, function (g, w, h) {
- postFill(g, w / 2 - 4.5, 130, 9, h - 130); baseShadow(g, w / 2, h - 2, 26);
- g.save(); g.translate(w / 2, 80);
- g.fillStyle = 'rgba(0,0,0,.28)'; g.save(); g.rotate(Math.PI / 4); rr(g, -57, -52, 114, 114, 10); g.fill(); g.restore();
- g.rotate(Math.PI / 4);
- rr(g, -58, -58, 116, 116, 10); g.fillStyle = '#d3d8de'; g.fill();
- rr(g, -56.5, -56.5, 113, 113, 9); g.fillStyle = lg(g, 0, -56, 0, 56, [[0, '#ffd21e'], [1, '#e0aa00']]); g.fill();
- rr(g, -49, -49, 98, 98, 6); g.strokeStyle = '#111'; g.lineWidth = 3; g.stroke();
- g.rotate(-Math.PI / 4); g.scale(dir, 1);
- g.strokeStyle = '#111'; g.lineWidth = 9; g.lineCap = 'butt';
- g.beginPath(); g.moveTo(-14, 40); g.lineTo(-14, 6); g.quadraticCurveTo(-14, -18, 14, -18); g.stroke();
- g.fillStyle = '#111'; g.beginPath(); g.moveTo(10, -36); g.lineTo(34, -18); g.lineTo(10, 0); g.closePath(); g.fill();
- g.restore();
- }, { solid: .02 });
- }
- function sgBillboard(kind) {
- const W = 800, H = 470;
- const c = mk(W, H, function (g) {
- g.imageSmoothingQuality = 'high';
- baseShadow(g, 175, H - 4, 60); baseShadow(g, W - 175, H - 4, 60);
- postFill(g, 165, 330, 20, H - 330); postFill(g, W - 185, 330, 20, H - 330);
- g.strokeStyle = '#4a4f57'; g.lineWidth = 6;
- g.beginPath(); g.moveTo(175, 400); g.lineTo(300, 340); g.moveTo(W - 175, 400); g.lineTo(W - 300, 340); g.stroke();
- g.fillStyle = '#6a7079'; g.fillRect(24, 26, W - 48, 8);
- g.strokeStyle = '#8b9199'; g.lineWidth = 3; g.beginPath(); g.moveTo(24, 6); g.lineTo(W - 24, 6); g.stroke();
- for (let x = 30; x < W - 24; x += 46) { g.beginPath(); g.moveTo(x, 6); g.lineTo(x, 26); g.stroke(); }
- g.fillStyle = '#2b2f36'; g.fillRect(20, 34, W - 40, 306);
- g.fillStyle = '#4d535c'; g.fillRect(20, 34, W - 40, 6);
- const ax = 32, ay = 46, aw = W - 64, ah = 282;
- g.save(); g.beginPath(); g.rect(ax, ay, aw, ah); g.clip();
- billboardArt(g, kind, ax, ay, aw, ah);
- g.fillStyle = lg(g, 0, ay, 0, ay + ah, [[0, 'rgba(255,255,255,.10)'], [.3, 'rgba(255,255,255,0)'], [1, 'rgba(0,0,0,.14)']]); g.fillRect(ax, ay, aw, ah);
- g.restore();
- [120, 400, 680].forEach(function (x) {
- g.fillStyle = '#2b2f36'; g.fillRect(x - 3, 10, 6, 16);
- g.fillStyle = '#1b1d21'; rr(g, x - 16, 22, 32, 12, 4); g.fill();
- g.fillStyle = '#fff6cc'; g.fillRect(x - 12, 30, 24, 3);
- const gr = g.createLinearGradient(0, 34, 0, 120); gr.addColorStop(0, 'rgba(255,246,204,.22)'); gr.addColorStop(1, 'rgba(255,246,204,0)');
- g.fillStyle = gr; g.beginPath(); g.moveTo(x - 12, 34); g.lineTo(x + 12, 34); g.lineTo(x + 60, 130); g.lineTo(x - 60, 130); g.closePath(); g.fill();
- });
- });
- return makeSpr(c, wfM(W / SG * 1.04), { solid: .0 });
- }
- function billboardArt(g, kind, x, y, w, h) {
- g.textBaseline = 'middle'; g.textAlign = 'center';
- if (kind === 0) {
- g.fillStyle = lg(g, 0, y, 0, y + h, [[0, '#3a1d6e'], [.45, '#ff6a4d'], [.75, '#ffc86a'], [1, '#2a1250']]); g.fillRect(x, y, w, h);
- g.fillStyle = '#fff2c0'; g.beginPath(); g.arc(x + w * .72, y + h * .62, 54, 0, 7); g.fill();
- g.fillStyle = 'rgba(40,10,70,.9)'; g.fillRect(x, y + h * .72, w, h * .28);
- g.fillStyle = '#1b0a36'; g.beginPath();
- g.moveTo(x + 70, y + h); g.quadraticCurveTo(x + 84, y + h * .6, x + 76, y + h * .35); g.lineTo(x + 84, y + h * .35); g.quadraticCurveTo(x + 100, y + h * .6, x + 92, y + h); g.fill();
- for (let i = 0; i < 6; i++) { const a = -2.9 + i * .55; g.beginPath(); g.moveTo(x + 80, y + h * .36); g.quadraticCurveTo(x + 80 + Math.cos(a) * 40, y + h * .36 + Math.sin(a) * 30 - 12, x + 80 + Math.cos(a) * 70, y + h * .36 + Math.sin(a) * 40 + 22); g.lineWidth = 7; g.strokeStyle = '#1b0a36'; g.stroke(); }
- stext(g, 'SUNSET COAST', x + w * .56, y + 74, 62, '#ffffff', 'center', 800, 2);
- stext(g, 'RESORT & SPA', x + w * .56, y + 130, 40, '#ffe6a8', 'center', 700, 8);
- stext(g, 'Book your escape · sunsetcoast.example', x + w * .56, y + h - 26, 22, '#ffffff', 'center', 600, 1);
- } else if (kind === 1) {
- g.fillStyle = lg(g, 0, y, 0, y + h, [[0, '#ffe36a'], [1, '#ff9a1f']]); g.fillRect(x, y, w, h);
- const cx = x + w * .25, cy = y + h * .52;
- g.fillStyle = '#ff8a00'; g.beginPath(); g.arc(cx, cy, 100, 0, 7); g.fill();
- g.fillStyle = '#ffd27a'; g.beginPath(); g.arc(cx, cy, 88, 0, 7); g.fill();
- g.strokeStyle = '#ff9d1a'; g.lineWidth = 5;
- for (let i = 0; i < 8; i++) { const a = i * Math.PI / 4; g.beginPath(); g.moveTo(cx, cy); g.lineTo(cx + Math.cos(a) * 88, cy + Math.sin(a) * 88); g.stroke(); }
- g.fillStyle = '#ffb347'; g.beginPath(); g.arc(cx, cy, 14, 0, 7); g.fill();
- g.fillStyle = '#2c8f3f'; g.beginPath(); g.ellipse(cx + 104, cy - 82, 36, 16, -.6, 0, 7); g.fill();
- stext(g, 'FRESH', x + w * .64, y + 76, 74, '#7a2d00', 'center', 800, 3);
- stext(g, 'CITRUS', x + w * .64, y + 150, 74, '#ffffff', 'center', 800, 3);
- stext(g, 'Squeezed this morning.', x + w * .64, y + h - 36, 28, '#7a2d00', 'center', 700, 0);
- } else if (kind === 2) {
- g.fillStyle = '#0c1226'; g.fillRect(x, y, w, h);
- g.strokeStyle = '#1f2a55'; g.lineWidth = 2;
- for (let i = 0; i < 12; i++) { g.beginPath(); g.moveTo(x, y + 20 + i * 24); g.lineTo(x + w, y + 20 + i * 24); g.stroke(); }
- g.shadowColor = '#ff2e63'; g.shadowBlur = 28; stext(g, 'ROUTE 9', x + w / 2, y + 82, 92, '#ff6b8f', 'center', 800, 4);
- g.shadowColor = '#08d9d6'; g.shadowBlur = 24; stext(g, 'DINER', x + w / 2, y + 168, 74, '#7cf4f1', 'center', 800, 14);
- g.shadowBlur = 0; g.shadowColor = 'transparent';
- g.fillStyle = '#f9d65c'; rr(g, x + w / 2 - 150, y + h - 62, 300, 38, 19); g.fill();
- stext(g, 'OPEN 24 HOURS', x + w / 2, y + h - 43, 26, '#1b1b1b', 'center', 800, 3);
- } else {
- g.fillStyle = lg(g, 0, y, 0, y + h, [[0, '#0e4a9a'], [1, '#1b86d6']]); g.fillRect(x, y, w, h);
- g.strokeStyle = 'rgba(255,255,255,.35)'; g.lineWidth = 5;
- for (let r = 0; r < 4; r++) { g.beginPath(); for (let i = 0; i <= w; i += 8) { const yy = y + h - 70 + r * 18 + Math.sin(i / 38 + r) * 9; if (i) g.lineTo(x + i, yy); else g.moveTo(x + i, yy); } g.stroke(); }
- g.fillStyle = '#ffffff'; g.beginPath(); g.arc(x + 82, y + 82, 40, 0, 7); g.fill();
- stext(g, 'B', x + 82, y + 84, 54, '#0e4a9a', 'center', 800, 0);
- stext(g, 'BLUE HARBOR', x + w * .6, y + 74, 66, '#ffffff', 'center', 800, 2);
- stext(g, 'BANK', x + w * .6, y + 134, 56, '#cfe8ff', 'center', 800, 18);
- stext(g, 'Bank on the tide.', x + w * .6, y + h - 82, 28, '#ffffff', 'center', 600, 1);
- }
- }
- function sgGantry(kind, a, b, c3) {
- const W = 1100, H = 600;
- const cv = mk(W, H, function (g) {
- g.imageSmoothingQuality = 'high';
- [30, W - 62].forEach(function (x) {
- baseShadow(g, x + 16, H - 3, 40);
- g.fillStyle = lg(g, x, 0, x + 32, 0, [[0, '#59606a'], [.35, '#d9dde2'], [.7, '#98a0a9'], [1, '#4a5058']]); g.fillRect(x, 60, 32, H - 60);
- g.fillStyle = '#3d4249'; g.fillRect(x - 8, H - 22, 48, 22);
- g.strokeStyle = 'rgba(0,0,0,.18)'; g.lineWidth = 2; for (let y = 90; y < H - 30; y += 60) { g.beginPath(); g.moveTo(x, y); g.lineTo(x + 32, y); g.stroke(); }
- });
- const ty0 = 18, ty1 = 84;
- g.fillStyle = lg(g, 0, ty0, 0, ty1, [[0, '#d9dde2'], [1, '#8d949c']]); g.fillRect(14, ty0, W - 28, 9); g.fillRect(14, ty1 - 9, W - 28, 9);
- g.strokeStyle = '#6e757d'; g.lineWidth = 5; g.beginPath();
- for (let x = 14; x < W - 28; x += 44) { g.moveTo(x, ty1 - 5); g.lineTo(x + 22, ty0 + 5); g.lineTo(x + 44, ty1 - 5); }
- g.stroke();
- g.fillStyle = 'rgba(0,0,0,.2)'; g.fillRect(14, ty1, W - 28, 6);
- if (kind === 0 || kind === 1) {
- const py = 98, ph = 196;
- const mk2 = function (x, w, l1, l2, l3) {
- g.fillStyle = '#7a8088'; g.fillRect(x + 50, ty1, 8, py - ty1 + 4); g.fillRect(x + w - 58, ty1, 8, py - ty1 + 4);
- panel(g, x, py, w, ph, '#0d6b3a', '#ffffff', 10, 4);
- fitText(g, l1, x + w / 2, py + 52, w - 80, 34, '#ffffff', 700, 2);
- fitText(g, l2, x + w / 2, py + 108, w - 80, 62, '#ffffff', 800, 1);
- fitText(g, l3, x + w / 2, py + 160, w - 80, 34, '#ffffff', 700, 1);
- };
- if (kind === 0) { mk2(104, 430, a[0], a[1], a[2]); mk2(566, 430, b[0], b[1], b[2]); }
- else {
- mk2(104, 520, a[0], a[1], a[2]);
- const x = 660, w = 330;
- g.fillStyle = '#7a8088'; g.fillRect(x + 40, ty1, 8, py - ty1 + 4); g.fillRect(x + w - 48, ty1, 8, py - ty1 + 4);
- rr(g, x, py, w, ph, 8); g.fillStyle = '#c9ced4'; g.fill();
- rr(g, x + 5, py + 5, w - 10, ph - 10, 6); g.fillStyle = '#07080a'; g.fill();
- g.shadowColor = '#ffb020'; g.shadowBlur = 10;
- c3.forEach(function (s, i) { stext(g, s, x + w / 2, py + 54 + i * 54, 36, '#ffb020', 'center', 700, 3); });
- g.shadowBlur = 0; g.shadowColor = 'transparent';
- g.fillStyle = 'rgba(0,0,0,.35)';
- for (let yy = py + 8; yy < py + ph - 8; yy += 4) g.fillRect(x + 6, yy, w - 12, 1);
- for (let xx = x + 8; xx < x + w - 8; xx += 4) g.fillRect(xx, py + 6, 1, ph - 12);
- }
- } else {
- const bx = 90, by = 96, bw = W - 180, bh = 210;
- g.fillStyle = '#7a8088'; g.fillRect(bx + 60, ty1, 8, 20); g.fillRect(bx + bw - 68, ty1, 8, 20); g.fillRect(W / 2 - 4, ty1, 8, 20);
- g.fillStyle = 'rgba(0,0,0,.3)'; rr(g, bx + 4, by + 6, bw, bh, 8); g.fill();
- rr(g, bx, by, bw, bh, 8); g.fillStyle = '#c9ced4'; g.fill();
- if (kind === 4) {
- const sq = 21;
- for (let r = 0; r < Math.floor((bh - 12) / sq); r++) for (let cI = 0; cI < Math.floor((bw - 12) / sq); cI++) { g.fillStyle = (r + cI) % 2 ? '#101010' : '#f4f4f4'; g.fillRect(bx + 6 + cI * sq, by + 6 + r * sq, sq, sq); }
- rr(g, W / 2 - 230, by + 40, 460, 130, 10); g.fillStyle = '#101010'; g.fill();
- stext(g, a, W / 2, by + bh / 2 + 4, 94, '#ffd23a', 'center', 800, 6);
- } else {
- rr(g, bx + 6, by + 6, bw - 12, bh - 12, 6);
- g.fillStyle = kind === 2 ? lg(g, 0, by, 0, by + bh, [[0, '#0f7a43'], [1, '#0a5530']]) : lg(g, 0, by, 0, by + bh, [[0, '#b3122a'], [1, '#7a0a1c']]); g.fill();
- g.strokeStyle = '#ffffff'; g.lineWidth = 5; rr(g, bx + 18, by + 18, bw - 36, bh - 36, 4); g.stroke();
- stext(g, a, W / 2, by + 78, 92, '#ffffff', 'center', 800, 8);
- stext(g, b, W / 2, by + 150, 44, kind === 2 ? '#ffe27a' : '#ffffff', 'center', 700, 6);
- }
- [bx + 24, bx + bw - 24].forEach(function (x) {
- g.fillStyle = '#ffd23a'; g.beginPath(); g.arc(x, by + bh + 10, 6, 0, 7); g.fill();
- });
- }
- [200, 550, 900].forEach(function (x) { g.fillStyle = '#2b2f36'; rr(g, x - 24, ty1 + 4, 48, 10, 4); g.fill(); g.fillStyle = '#fff6cc'; g.fillRect(x - 18, ty1 + 11, 36, 3); });
- });
- return makeSpr(cv, 2.55, { solid: 0, high: true });
- }
- function sceneSprite(wM, w, h, draw, extra) {
- const c = mk(w, h, function (g) { g.imageSmoothingQuality = 'high'; draw(g, w, h); });
- return makeSpr(c, wfM(wM), extra);
- }
- function flipH(c) {
- return mk(c.width, c.height, function (g) { g.translate(c.width, 0); g.scale(-1, 1); g.drawImage(c, 0, 0); });
- }
- function buildScenery() {
- SPR.palm = sceneSprite(5.4, 160, 280, function (g) {
- baseShadow(g, 88, 276, 40);
- g.fillStyle = lg(g, 70, 0, 100, 0, [[0, '#6b4524'], [.5, '#a47440'], [1, '#5a3a1c']]);
- g.beginPath(); g.moveTo(74, 278); g.quadraticCurveTo(94, 180, 82, 84); g.lineTo(96, 84); g.quadraticCurveTo(108, 180, 98, 278); g.fill();
- g.strokeStyle = 'rgba(40,24,10,.5)'; g.lineWidth = 3;
- for (let y = 262; y > 100; y -= 14) { g.beginPath(); g.moveTo(76 + (y < 180 ? 4 : 0), y); g.lineTo(98, y - 5); g.stroke(); }
- for (let i = 0; i < 9; i++) {
- const a = -Math.PI + i * Math.PI / 8, cx = 90, cy = 84;
- const ex = cx + Math.cos(a) * 76, ey = cy + Math.sin(a) * 52 + 38;
- g.fillStyle = i % 2 ? '#2f9a42' : '#1f7a33';
- g.beginPath(); g.moveTo(cx, cy);
- g.quadraticCurveTo(cx + Math.cos(a) * 56, cy + Math.sin(a) * 62 - 24, ex, ey);
- g.quadraticCurveTo(cx + Math.cos(a) * 42, cy + Math.sin(a) * 30 + 8, cx, cy + 8); g.fill();
- g.strokeStyle = 'rgba(10,50,20,.5)'; g.lineWidth = 1.5; g.beginPath(); g.moveTo(cx, cy + 2); g.quadraticCurveTo(cx + Math.cos(a) * 50, cy + Math.sin(a) * 50 - 14, ex, ey); g.stroke();
- }
- g.fillStyle = '#4b2f14'; g.beginPath(); g.arc(82, 94, 7, 0, 7); g.arc(97, 96, 7, 0, 7); g.arc(90, 102, 7, 0, 7); g.fill();
- }, { solid: .035 });
- SPR.bush = sceneSprite(3.2, 128, 80, function (g) {
- baseShadow(g, 64, 76, 50);
- [['#1b6b30', 36, 54, 28], ['#1b6b30', 90, 56, 26], ['#237f3a', 64, 44, 34], ['#2f9a4a', 50, 38, 22], ['#34a653', 82, 36, 20]].forEach(function (b) {
- const gr = g.createRadialGradient(b[1] - 8, b[2] - 10, 2, b[1], b[2], b[3]);
- gr.addColorStop(0, shade(b[0], .25)); gr.addColorStop(1, shade(b[0], -.2));
- g.fillStyle = gr; g.beginPath(); g.arc(b[1], b[2], b[3], 0, 7); g.fill();
- });
- });
- SPR.rock = sceneSprite(3.4, 128, 96, function (g) {
- baseShadow(g, 64, 92, 56);
- g.fillStyle = lg(g, 0, 10, 0, 94, [[0, '#9a8fa0'], [1, '#5a4f60']]);
- g.beginPath(); g.moveTo(4, 94); g.lineTo(18, 42); g.lineTo(46, 12); g.lineTo(88, 18); g.lineTo(118, 52); g.lineTo(124, 94); g.closePath(); g.fill();
- g.fillStyle = 'rgba(255,255,255,.2)'; g.beginPath(); g.moveTo(18, 42); g.lineTo(46, 12); g.lineTo(88, 18); g.lineTo(62, 46); g.closePath(); g.fill();
- g.fillStyle = 'rgba(0,0,0,.25)'; g.beginPath(); g.moveTo(62, 46); g.lineTo(88, 18); g.lineTo(118, 52); g.lineTo(124, 94); g.lineTo(80, 94); g.closePath(); g.fill();
- }, { solid: .09 });
- SPR.cactus = sceneSprite(2.8, 96, 192, function (g) {
- baseShadow(g, 48, 188, 30);
- const col = lg(g, 36, 0, 62, 0, [[0, '#1f6e38'], [.4, '#3da35a'], [1, '#1b5d30']]);
- g.fillStyle = col; rr(g, 36, 12, 26, 178, 13); g.fill();
- rr(g, 8, 70, 18, 62, 9); g.fill(); rr(g, 8, 114, 40, 18, 8); g.fill();
- rr(g, 72, 46, 18, 56, 9); g.fill(); rr(g, 56, 86, 36, 17, 8); g.fill();
- g.strokeStyle = 'rgba(0,0,0,.18)'; g.lineWidth = 2;
- for (let x = 42; x < 60; x += 6) { g.beginPath(); g.moveTo(x, 20); g.lineTo(x, 184); g.stroke(); }
- }, { solid: .035 });
- SPR.pine = sceneSprite(4.4, 128, 240, function (g) {
- baseShadow(g, 64, 236, 36);
- g.fillStyle = '#4a2f16'; g.fillRect(58, 190, 12, 48);
- const cols = ['#164f2a', '#1d6836', '#26803f', '#2f9a4c'];
- for (let i = 0; i < 5; i++) {
- const y = 8 + i * 38, wd = 20 + i * 12;
- g.fillStyle = lg(g, 64 - wd, 0, 64 + wd, 0, [[0, shade(cols[i % 4], -.25)], [.5, shade(cols[i % 4], .1)], [1, shade(cols[i % 4], -.3)]]);
- g.beginPath(); g.moveTo(64, y); g.lineTo(64 + wd, y + 66); g.lineTo(64 - wd, y + 66); g.closePath(); g.fill();
- }
- }, { solid: .035 });
- SPR.house = sceneSprite(11, 256, 192, function (g) {
- baseShadow(g, 128, 188, 110);
- g.fillStyle = lg(g, 0, 80, 0, 188, [[0, '#f3e8cc'], [1, '#d3c4a0']]); g.fillRect(24, 80, 208, 108);
- g.fillStyle = '#b8402a'; g.beginPath(); g.moveTo(8, 88); g.lineTo(128, 14); g.lineTo(248, 88); g.closePath(); g.fill();
- g.fillStyle = '#7e2a1a'; g.beginPath(); g.moveTo(128, 14); g.lineTo(248, 88); g.lineTo(200, 88); g.lineTo(128, 40); g.closePath(); g.fill();
- g.fillStyle = '#5a3a1a'; g.fillRect(108, 128, 40, 60);
- [[44, 108], [176, 108]].forEach(function (p) { g.fillStyle = '#8fd0ff'; g.fillRect(p[0], p[1], 38, 36); g.fillStyle = '#f3e8cc'; g.fillRect(p[0] + 18, p[1], 3, 36); g.fillRect(p[0], p[1] + 16, 38, 3); });
- }, { solid: .25 });
- const bcols = [['#5a4a8a', '#ffd36a'], ['#2f5f8a', '#aee3ff'], ['#7a3a5a', '#ffb0c8']];
- SPR.bld = bcols.map(function (c, i) {
- return sceneSprite(15, 224, 360, function (g) {
- baseShadow(g, 112, 356, 100);
- g.fillStyle = lg(g, 16, 0, 208, 0, [[0, shade(c[0], .1)], [1, shade(c[0], -.3)]]); g.fillRect(16, 20, 192, 340);
- g.fillStyle = shade(c[0], -.35); g.fillRect(16, 20, 192, 10);
- for (let y = 46; y < 340; y += 36) for (let x = 30; x < 180; x += 32) {
- g.fillStyle = ((x * 7 + y * 3 + i) % 5 < 3) ? c[1] : shade(c[0], .22); g.fillRect(x, y, 19, 23);
- }
- }, { solid: .3 });
- });
- const L = mk(160, 400, function (g, w, h) {
- g.imageSmoothingQuality = 'high';
- baseShadow(g, 130, h - 6, 24);
- g.fillStyle = lg(g, 124, 0, 138, 0, [[0, '#6c727a'], [.4, '#d4d8de'], [1, '#555b63']]); g.fillRect(124, 52, 11, h - 52);
- g.fillStyle = '#4a4f57'; g.fillRect(118, h - 26, 23, 26);
- g.strokeStyle = '#8b9199'; g.lineWidth = 7; g.lineCap = 'round'; g.beginPath(); g.moveTo(129, 70); g.quadraticCurveTo(129, 28, 70, 24); g.stroke();
- g.fillStyle = '#2b2f36'; rr(g, 22, 18, 62, 16, 7); g.fill();
- g.fillStyle = '#fff4c0'; g.fillRect(30, 30, 46, 4);
- const gr = g.createRadialGradient(53, 34, 2, 53, 40, 44); gr.addColorStop(0, 'rgba(255,240,170,.55)'); gr.addColorStop(1, 'rgba(255,240,170,0)');
- g.fillStyle = gr; g.fillRect(0, 10, 110, 80);
- });
- SPR.lampR = makeSpr(L, wfM(160 / 30), { solid: .02 });
- SPR.lampL = makeSpr(flipH(L), wfM(160 / 30), { solid: .02 });
- }
- function itemSprite(wM, hM, draw, extra) {
- const W = Math.ceil(wM * 70), H = Math.ceil(hM * 70);
- const c = mk(W, H, function (g) { g.imageSmoothingQuality = 'high'; draw(g, W, H); });
- return makeSpr(c, wfM(wM), extra);
- }
- function halo(g, cx, cy, r, col) {
- const gr = g.createRadialGradient(cx, cy, r * .15, cx, cy, r);
- gr.addColorStop(0, col); gr.addColorStop(1, 'rgba(255,255,255,0)');
- g.fillStyle = gr; g.fillRect(cx - r, cy - r, r * 2, r * 2);
- }
- function buildItems() {
- SPR.coin = [];
- for (let k = 0; k < 8; k++) {
- const sx = Math.max(.14, Math.abs(Math.cos(k * Math.PI / 8)));
- SPR.coin.push(itemSprite(1.5, 1.5, function (g, w, h) {
- halo(g, w / 2, h / 2, w * .5, 'rgba(255,214,70,.5)');
- g.save(); g.translate(w / 2, h / 2); g.scale(sx, 1);
- const R = w * .36;
- g.fillStyle = lg(g, -R, -R, R, R, [[0, '#fff3a6'], [.45, '#f4b71d'], [1, '#a9680a']]);
- g.beginPath(); g.arc(0, 0, R, 0, 7); g.fill();
- g.lineWidth = R * .12; g.strokeStyle = '#8a5206'; g.stroke();
- g.fillStyle = lg(g, 0, -R, 0, R, [[0, '#ffe88a'], [1, '#e0a012']]);
- g.beginPath(); g.arc(0, 0, R * .74, 0, 7); g.fill();
- g.strokeStyle = 'rgba(138,82,6,.7)'; g.lineWidth = R * .05; g.stroke();
- g.fillStyle = '#9a5c08';
- g.beginPath();
- for (let i = 0; i < 10; i++) { const a = -Math.PI / 2 + i * Math.PI / 5, rr_ = i % 2 ? R * .17 : R * .42; g.lineTo(Math.cos(a) * rr_, Math.sin(a) * rr_); }
- g.closePath(); g.fill();
- g.restore();
- g.fillStyle = 'rgba(255,255,255,.5)'; g.beginPath(); g.ellipse(w / 2 - w * .1 * sx, h / 2 - h * .17, w * .08 * sx, h * .04, -.5, 0, 7); g.fill();
- }));
- }
- SPR.gas = itemSprite(1.7, 2.0, function (g, w, h) {
- halo(g, w / 2, h / 2, w * .6, 'rgba(255,90,60,.45)');
- const x = w * .2, y = h * .24, bw = w * .6, bh = h * .62;
- g.fillStyle = 'rgba(0,0,0,.25)'; rr(g, x + 4, y + 5, bw, bh, 10); g.fill();
- rr(g, x, y, bw, bh, 10); g.fillStyle = lg(g, x, 0, x + bw, 0, [[0, '#9c1111'], [.35, '#f0402c'], [.7, '#d32218'], [1, '#7a0c0c']]); g.fill();
- g.strokeStyle = '#4d0808'; g.lineWidth = 3; g.stroke();
- g.strokeStyle = 'rgba(0,0,0,.28)'; g.lineWidth = 3;
- g.beginPath(); g.moveTo(x + bw * .12, y + bh * .38); g.lineTo(x + bw * .88, y + bh * .38); g.moveTo(x + bw * .12, y + bh * .86); g.lineTo(x + bw * .88, y + bh * .86); g.stroke();
- g.fillStyle = '#f7f1dc'; rr(g, x + bw * .18, y + bh * .46, bw * .64, bh * .32, 5); g.fill();
- g.fillStyle = '#b01818'; g.font = '800 ' + Math.round(bh * .2) + 'px Overpass, Arial, sans-serif'; g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText('FUEL', w / 2, y + bh * .62);
- g.strokeStyle = '#3a3a3a'; g.lineWidth = 6; g.beginPath(); g.moveTo(x + bw * .15, y + 2); g.quadraticCurveTo(x + bw * .15, y - h * .12, x + bw * .4, y - h * .12); g.lineTo(x + bw * .55, y - h * .12); g.stroke();
- g.fillStyle = '#e8c21f'; g.fillRect(x + bw * .62, y - h * .1, bw * .22, h * .12); g.fillStyle = '#222'; g.fillRect(x + bw * .66, y - h * .14, bw * .14, h * .05);
- g.fillStyle = 'rgba(255,255,255,.3)'; rr(g, x + bw * .08, y + bh * .06, bw * .1, bh * .26, 4); g.fill();
- });
- SPR.bolt = itemSprite(1.9, 1.9, function (g, w, h) {
- halo(g, w / 2, h / 2, w * .62, 'rgba(120,200,255,.65)');
- const R = w * .38;
- g.fillStyle = lg(g, 0, h / 2 - R, 0, h / 2 + R, [[0, '#4aa8ff'], [1, '#1b3fb8']]);
- g.beginPath(); g.arc(w / 2, h / 2, R, 0, 7); g.fill();
- g.lineWidth = 5; g.strokeStyle = '#e7f4ff'; g.stroke();
- g.save(); g.translate(w / 2, h / 2); g.scale(R / 50, R / 50);
- g.beginPath(); g.moveTo(8, -44); g.lineTo(-24, 6); g.lineTo(-3, 6); g.lineTo(-12, 44); g.lineTo(26, -8); g.lineTo(4, -8); g.lineTo(18, -44); g.closePath();
- g.fillStyle = '#ffe14a'; g.fill(); g.lineWidth = 4; g.strokeStyle = '#8a5a00'; g.lineJoin = 'round'; g.stroke();
- g.restore();
- });
- }
- function buildSprites() {
- const R = rng(7);
- const plates = function () { const L = 'ABCDEFGHJKLMNPRSTUVWXYZ'; return L[Math.floor(R() * L.length)] + L[Math.floor(R() * L.length)] + L[Math.floor(R() * L.length)] + ' ' + (100 + Math.floor(R() * 900)); };
- buildScenery();
- buildItems();
- const white = '#eef0f2', black = '#17191d', silver = '#a5abb3', grey = '#5b626b', blue = '#1f3f7a', red = '#8e1b24', green = '#2b5a3f', tan = '#c4b08c', teal = '#1e5f66', maroon = '#5a1c2c';
- SPR.cars = [];
- SPR.cars.push(vSedan(white, plates()), vSedan(black, plates()), vSedan(silver, plates()), vSedan(grey, plates()), vSedan(blue, plates()), vSedan(red, plates()));
- SPR.cars.push(vHatch(red, plates()), vHatch(silver, plates()), vHatch(teal, plates()));
- SPR.cars.push(vSuv(black, plates()), vSuv(white, plates()), vSuv(grey, plates()), vSuv(maroon, plates()));
- SPR.cars.push(vCoupe('#d9a400', plates()), vCoupe(blue, plates()));
- SPR.cars.push(vPickup(white, plates()), vPickup(green, plates()), vPickup(red, plates()));
- SPR.cars.push(vVan(white, plates()), vVan(silver, plates()));
- SPR.cars.push(vSedan('#f2c230', plates(), true));
- SPR.trucks = [vSemi('#f1f3f5', 'NORTHWAY', '#1b3f8a', plates()), vSemi('#e9ecef', 'BLUELINE', '#0d6b9a', plates()), vSemi('#2c4a8c', 'TRANSCOAST', '#ffffff', plates()), vSemi('#c8202a', 'REDHAWK', '#ffffff', plates())];
- SPR.player = vConvertible();
- SPR.billboards = [sgBillboard(0), sgBillboard(1), sgBillboard(2), sgBillboard(3)];
- SPR.warnL = sgCurve(-1); SPR.warnR = sgCurve(1);
- SPR.chevL = sgChevron(-1); SPR.chevR = sgChevron(1);
- SPR.keepRight = sgKeepRight();
- SPR.speed = [sgSpeed(120), sgSpeed(100)];
- SPR.checkpoint = sgGantry(2, 'CHECKPOINT', 'EXTRA TIME');
- SPR.start = sgGantry(3, 'START', 'GOOD LUCK');
- SPR.goal = sgGantry(4, 'GOAL');
- }
- const BGL = [];
- const STARS = [];
- function buildBackgrounds() {
- const R = rng(99);
- for (let i = 0; i < 70; i++) STARS.push([R() * W, R(), R()]);
- PAL.forEach((pal, col) => {
- const r = rng(col * 31 + 5);
- const far = mk(1024, 96, (g, w, h) => {
- const ph1 = r() * 6, ph2 = r() * 6, ph3 = r() * 6;
- g.fillStyle = pal.far; g.beginPath(); g.moveTo(0, h);
- for (let x = 0; x <= w; x += 8) {
- const t = x / w * Math.PI * 2;
- const y = h * (0.52 + 0.2 * Math.sin(t * 3 + ph1) + 0.13 * Math.sin(t * 7 + ph2) + 0.06 * Math.sin(t * 13 + ph3));
- g.lineTo(x, h - y);
- }
- g.lineTo(w, h); g.closePath(); g.fill();
- });
- const near = mk(1024, 56, (g, w, h) => {
- if (col === 0) {
- g.fillStyle = pal.near; g.fillRect(0, h - 16, w, 16);
- g.fillStyle = 'rgba(255,255,255,.55)';
- for (let i = 0; i < 70; i++) g.fillRect(r() * w, h - 14 + r() * 12, 6 + r() * 8, 1.5);
- g.fillStyle = '#f4f0e0'; g.beginPath(); g.moveTo(300, h - 16); g.lineTo(312, h - 38); g.lineTo(326, h - 16); g.fill();
- g.beginPath(); g.moveTo(640, h - 16); g.lineTo(648, h - 30); g.lineTo(660, h - 16); g.fill();
- } else if (col === 1) {
- g.fillStyle = pal.near;
- for (let i = 0; i < 7; i++) {
- const x = i * 150 + r() * 60, wd = 40 + r() * 50, hh = 22 + r() * 24;
- g.fillRect(x, h - hh, wd, hh);
- g.beginPath(); g.moveTo(x - 8, h); g.lineTo(x, h - hh); g.lineTo(x + wd, h - hh); g.lineTo(x + wd + 8, h); g.fill();
- }
- } else if (col === 2) {
- for (let i = 0; i < 90; i++) {
- const x = r() * w, hh = 20 + r() * 30;
- g.fillStyle = r() < .5 ? pal.near : shade(pal.near, 0.15);
- g.beginPath(); g.moveTo(x, h - hh); g.lineTo(x + 9, h); g.lineTo(x - 9, h); g.fill();
- }
- } else {
- for (let x = 0; x < w;) {
- const wd = 14 + r() * 22, hh = (col === 4 ? 20 : 14) + r() * (col === 4 ? 32 : 26);
- g.fillStyle = pal.near; g.fillRect(x, h - hh, wd, hh);
- const lit = col === 4 ? '#ffe28a' : '#ffd8a0';
- for (let wy = h - hh + 4; wy < h - 3; wy += 6) for (let wx = x + 3; wx < x + wd - 3; wx += 5) {
- if (r() < (col === 4 ? 0.55 : 0.3)) { g.fillStyle = r() < .2 ? '#7fe0ff' : lit; g.fillRect(wx, wy, 2, 3); }
- }
- x += wd + 1 + r() * 4;
- }
- }
- });
- let cloud = null;
- if (col < 4) {
- cloud = mk(1024, 130, (g, w) => {
- g.fillStyle = pal.clouds; g.globalAlpha = col === 3 ? 0.6 : 0.9;
- for (let i = 0; i < 9; i++) {
- const cx = i * 114 + r() * 50, cy = 20 + r() * 80, s = 0.7 + r() * 0.9;
- [-1, 0, 1].forEach(o => {
- const x0 = cx + o * 1024;
- g.beginPath();
- g.ellipse(x0, cy, 34 * s, 10 * s, 0, 0, 7); g.ellipse(x0 - 18 * s, cy + 3 * s, 20 * s, 8 * s, 0, 0, 7);
- g.ellipse(x0 + 20 * s, cy + 2 * s, 22 * s, 8 * s, 0, 0, 7); g.ellipse(x0 + 2 * s, cy - 7 * s, 18 * s, 9 * s, 0, 0, 7);
- g.fill();
- });
- }
- });
- }
- BGL.push({ far, near, cloud });
- });
- }
- let segs = [], trackLen = 0, cars = [], items = [];
- function lastY() { return segs.length ? segs[segs.length - 1].p2.world.y : 0; }
- function addSegment(curve, y) {
- const n = segs.length;
- segs.push({
- index: n,
- p1: { world: { y: lastY(), z: n * SEG }, camera: {}, screen: {} },
- p2: { world: { y: y, z: (n + 1) * SEG }, camera: {}, screen: {} },
- curve: curve, sprites: [], cars: [], items: [], railL: false, railR: false, clip: 0, fog: 0
- });
- }
- function addRoad(enter, hold, leave, curve, y) {
- const startY = lastY(), endY = startY + y * SEG, total = enter + hold + leave;
- let n;
- for (n = 0; n < enter; n++) addSegment(easeIn(0, curve, n / enter), easeInOut(startY, endY, n / total));
- for (n = 0; n < hold; n++) addSegment(curve, easeInOut(startY, endY, (enter + n) / total));
- for (n = 0; n < leave; n++) addSegment(easeInOut(curve, 0, n / leave), easeInOut(startY, endY, (enter + hold + n) / total));
- }
- function buildStage(col) {
- segs = [];
- const R = rng(col * 1013 + 11);
- addRoad(20, 80, 20, 0, 0);
- const target = 2300 + col * 150;
- const kinds = ['curve', 'curve', 'curve', 'hill', 'scurve', 'straight', 'hillcurve', 'bumps'];
- while (segs.length < target) {
- const kind = kinds[Math.floor(R() * kinds.length)];
- const dir = R() < 0.5 ? -1 : 1;
- const mag = 1 + R() * 2.6 + col * 0.25;
- const h = 8 + R() * 16;
- const hs = R() < 0.5 ? 1 : -1;
- const len = 30 + Math.floor(R() * 50);
- if (kind === 'curve') addRoad(25, len, 25, dir * mag, 0);
- else if (kind === 'straight') addRoad(15, 60 + Math.floor(R() * 60), 15, 0, 0);
- else if (kind === 'hill') { addRoad(25, 30, 25, 0, hs * h); addRoad(25, 30, 25, 0, -hs * h); }
- else if (kind === 'scurve') { addRoad(25, 35, 25, dir * mag, 0); addRoad(25, 35, 25, -dir * mag * 0.9, 0); }
- else if (kind === 'hillcurve') { addRoad(25, len, 25, dir * mag, hs * h); addRoad(25, len, 25, -dir * mag * 0.8, -hs * h); }
- else { for (let i = 0; i < 4; i++) { addRoad(8, 10, 8, 0, 3); addRoad(8, 10, 8, 0, -3); } }
- }
- addRoad(30, 30, 30, 0, -lastY() / SEG);
- addRoad(10, 240, 10, 0, 0);
- trackLen = segs.length * SEG;
- const L = segs.length;
- ['railL', 'railR'].forEach(function (key) {
- let i = 40, on = R() < 0.5;
- while (i < L - CHK - 12) {
- const len = on ? 60 + Math.floor(R() * 140) : 30 + Math.floor(R() * 110);
- if (on) for (let k = i; k < Math.min(i + len, L - CHK - 12); k++) segs[k][key] = true;
- i += len; on = !on;
- }
- });
- }
- function distStr(idx, endSeg) {
- const km = Math.max(0, endSeg - idx) * KM_PER_SEG;
- return km >= 1 ? km.toFixed(1) + ' km' : Math.max(100, Math.round(km * 10) * 100) + ' m';
- }
- function put(i, spr, offset) {
- if (i < 0 || i >= segs.length) return;
- segs[i].sprites.push({ src: spr, offset: offset, solid: spr.solid || 0 });
- }
- function populate(col, first, carZ0) {
- const R = rng(col * 733 + 191);
- const L = segs.length, endSeg = L - CHK, types = BIOME[col];
- for (let i = 8; i < L - 4; i += 2 + Math.floor(R() * 5)) {
- [-1, 1].forEach(function (side) {
- if (R() < 0.7) {
- const t = types[Math.floor(R() * types.length)];
- if (t === 'lamp') return;
- const s = t === 'building' ? SPR.bld[Math.floor(R() * 3)] : SPR[t];
- const big = (t === 'house' || t === 'building') ? 1.1 : 0;
- put(i, s, side * (1.55 + big + s.wf / 2 + R() * 1.8));
- }
- });
- }
- if (types.indexOf('lamp') >= 0) {
- for (let i = 10; i < endSeg - 4; i += 14) {
- put(i, SPR.lampR, 1.02); put(i + 7, SPR.lampL, -1.02);
- }
- }
- const towns = TOWNS[col];
- let i = 46, n = 0;
- while (i < endSeg - 70) {
- const t = n % 8;
- if (t === 0) put(i, SPR.speed[n % 2], 1.3);
- else if (t === 1) put(i, cached('km' + (10 + col * 7 + n), function () { return sgKm(10 + col * 7 + n); }), 1.27);
- else if (t === 2) {
- const nm = towns[n % 3], d = distStr(i, endSeg);
- put(i, cached('gr' + nm + d, function () { return sgGreen('EXIT ' + (8 + col * 3 + (n % 5)), nm, d); }), 1.75);
- }
- else if (t === 3) put(i, SPR.billboards[(n + col) % 4], (n % 2 ? 1 : -1) * 3.05);
- else if (t === 4) put(i, SPR.keepRight, 1.3);
- else if (t === 5) {
- const d = distStr(i, endSeg);
- if ((n >> 3) % 2 === 0) {
- const a = ['CHECKPOINT', TOWNS[col][0], d], b = ['EXIT ' + (12 + col), towns[1], '1 km'];
- put(i, cached('g0' + col + d, function () { return sgGantry(0, a, b); }), 0);
- } else {
- const a = ['NEXT', towns[2], d], v = ['CHECKPOINT', d, 'TIME +55 S'];
- put(i, cached('g1' + col + d, function () { return sgGantry(1, a, null, v); }), 0);
- }
- }
- else if (t === 6) put(i, cached('rest' + col, function () { return sgRest(col % 2 ? '2 km' : '5 km'); }), 1.3);
- else put(i, SPR.billboards[(n + col + 2) % 4], (n % 2 ? -1 : 1) * 3.05);
- i += 52 + Math.floor(R() * 40); n++;
- }
- for (let k = 80; k < endSeg - 40; k++) {
- const c = segs[k].curve, pc = segs[k - 1].curve;
- if (Math.abs(c) > 1.6 && Math.abs(pc) <= 1.6) {
- put(Math.max(8, k - 40), c > 0 ? SPR.warnR : SPR.warnL, c > 0 ? -1.3 : 1.3);
- }
- if (Math.abs(c) > 1.9 && k % 6 === 0) put(k, c > 0 ? SPR.chevR : SPR.chevL, c > 0 ? -1.3 : 1.3);
- }
- put(GSTART, first ? SPR.start : SPR.checkpoint, 0);
- put(endSeg, col === 4 ? SPR.goal : SPR.checkpoint, 0);
- cars = [];
- const baseGap = 21 - col * 0.8;
- for (let lane = 0; lane < LANES; lane++) {
- const gap = baseGap / LANE_DENS[lane];
- let s = 60 + Math.floor(R() * gap);
- while (s < endSeg + 60) {
- const truck = R() < 0.09;
- const spr = truck ? SPR.trucks[Math.floor(R() * SPR.trucks.length)] : SPR.cars[Math.floor(R() * SPR.cars.length)];
- const car = { z: s * SEG, offset: LANE_C[lane], lane: LANE_C[lane], speed: MAXSPD * LANE_SPEED[lane], spr: spr, w: spr.cw, pct: 0 };
- cars.push(car);
- s += Math.max(9, Math.round(gap * (0.75 + R() * 0.5)));
- }
- }
- cars.sort(function (p, q) { return p.z - q.z; });
- for (let i = 0; i < cars.length; i++) {
- let j = i, n = 0;
- while (j < cars.length && cars[j].z - cars[i].z < DRAW * SEG) { n++; j++; }
- if (n > MAX_VISIBLE) { cars.splice(i + Math.floor((j - i) / 2), 1); i--; }
- }
- cars.forEach(function (c) { segs[Math.floor(c.z / SEG)].cars.push(c); });
- items = [];
- const lanePick = function () { return LANE_C[Math.floor(R() * LANES)]; };
- const span = endSeg - 40;
- const taken = [];
- const add = function (seg, type, off) {
- const zMeet = (seg + 0.5) * SEG;
- const it = { type: type, offset: off, phase: R() * 6.28, taken: false, z: carZ0 + (zMeet - carZ0) * ITEM_SPEED, pct: 0 };
- it.pct = (it.z % SEG) / SEG;
- items.push(it);
- segs[Math.floor(it.z / SEG)].items.push(it);
- };
- for (let k = 0; k < 10; k++) { const s = Math.round(40 + span * (0.04 + 0.095 * k)); taken.push(s); add(s, 'gas', lanePick()); }
- for (let k = 1; k <= 3; k++) { const s = Math.round(40 + span * 0.25 * k); taken.push(s); add(s, 'bolt', lanePick()); }
- for (let k = 0; k < 8; k++) {
- let s = Math.round(40 + span * (0.07 + 0.115 * k) + (R() - .5) * 20);
- for (let tries = 0; tries < 12 && taken.some(function (q) { return s + 40 > q - 6 && s < q + 6; }); tries++) s += 12;
- const off = lanePick();
- for (let c = 0; c < 10; c++) add(s + c * 3, 'coin', off);
- }
- }
- function findSeg(z) {
- const i = Math.floor(z / SEG);
- return segs[((i % segs.length) + segs.length) % segs.length];
- }
- const P = { turbo: 0, pos: 0, x: 0, speed: 0, gear: 0, crash: 0, invuln: 0, spin: 0, shake: 0, steer: 0 };
- const G = { fuel: 1, coins: 0, floats: [], col: 0, time: 0, score: 0, hi: 0, station: 0, count: 3, stageT: 0, bannerT: 0, banner: '', bannerSub: '', flash: 0, over: 0, t: 0, pt: 0, quitSel: 1, beepT: 0, reason: '' };
- let state = 'title';
- let bg1 = 0, bg2 = 0;
- const parts = [];
- const keys = {}, touch = { left: 0, right: 0, gas: 0, brake: 0 };
- try { G.hi = parseInt(localStorage.getItem('coastline-hi') || '0', 10) || 0; } catch (e) { }
- function startStage(col, first, carZ0) {
- G.col = col;
- buildStage(col);
- populate(col, first, carZ0 === undefined ? PLAYERZ : carZ0);
- G.stageT = 3;
- bg1 = 0; bg2 = 0;
- }
- function resetRun() {
- G.reason = ''; G.beepT = 0; G.score = 0; G.time = 75; G.fuel = 1; G.coins = 0; G.floats.length = 0; P.turbo = 0;
- P.pos = 0; P.x = 0; P.speed = 0; P.gear = 0; P.crash = 0; P.invuln = 0; P.spin = 0; P.shake = 0; P.steer = 0;
- parts.length = 0;
- startStage(0, true);
- }
- function startAttract() {
- state = 'title';
- P.turbo = 0;
- if (typeof playLock === 'function') { playLock(false); exitFullscreen(); }
- G.time = 75;
- P.pos = 0; P.x = LANE_C[3]; P.speed = 7600; P.crash = 0; P.invuln = 0;
- startStage(0, true);
- G.stageT = 0;
- }
- let AC = null, master, musicBus, sfxBus, noiseBuf, engOsc, engOsc2, engGain, engFilt, muted = false;
- let seqTimer = null, seqStep = 0, seqNext = 0;
- const mtof = m => 440 * Math.pow(2, (m - 69) / 12);
- const STATIONS = [
- { name: 'SUNRISE FM', tag: 'BRIGHT & BREEZY', tempo: 118, root: 0, bass: 0, kick: [0, 4, 8, 12], chords: [[0, 0], [7, 0], [9, 1], [5, 0]],
- leadA: [4, -1, 3, -1, 2, -1, 3, 4, -1, 2, -1, 3, -1, -1, 1, -1], leadB: [3, -1, 4, -1, 3, 2, -1, 1, -1, 2, 3, -1, 4, -1, -1, -1], wave: 'square' },
- { name: 'PALM DRIVE', tag: 'SLOW SUNSET GROOVE', tempo: 104, root: 9, bass: 1, kick: [0, 6, 10], chords: [[0, 1], [8, 0], [3, 0], [10, 0]],
- leadA: [2, -1, -1, 3, -1, 2, -1, 1, 0, -1, -1, 1, -1, 2, -1, -1], leadB: [3, -1, 2, -1, 1, -1, 2, 3, -1, 4, -1, 3, -1, -1, 2, -1], wave: 'triangle' },
- { name: 'NIGHT CRUISE', tag: 'NEON HIGHWAY', tempo: 126, root: 2, bass: 2, kick: [0, 4, 8, 12], chords: [[0, 1], [8, 0], [3, 0], [10, 0]],
- leadA: [4, 3, -1, 2, -1, 3, 4, -1, 2, -1, 3, -1, 4, -1, 3, 2], leadB: [1, -1, 2, 3, -1, 4, -1, 3, -1, 2, -1, 1, -1, 2, -1, -1], wave: 'sawtooth' }
- ];
- function initAudio() {
- if (AC) { if (AC.state === 'suspended') AC.resume(); return; }
- try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { AC = null; return; }
- master = AC.createGain(); master.gain.value = muted ? 0 : 0.7; master.connect(AC.destination);
- musicBus = AC.createGain(); musicBus.gain.value = 0.45; musicBus.connect(master);
- sfxBus = AC.createGain(); sfxBus.gain.value = 0.8; sfxBus.connect(master);
- noiseBuf = AC.createBuffer(1, AC.sampleRate, AC.sampleRate);
- const d = noiseBuf.getChannelData(0);
- for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
- engOsc = AC.createOscillator(); engOsc.type = 'sawtooth';
- engOsc2 = AC.createOscillator(); engOsc2.type = 'square';
- engFilt = AC.createBiquadFilter(); engFilt.type = 'lowpass'; engFilt.frequency.value = 520;
- engGain = AC.createGain(); engGain.gain.value = 0;
- engOsc.connect(engFilt); engOsc2.connect(engFilt); engFilt.connect(engGain); engGain.connect(sfxBus);
- engOsc.start(); engOsc2.start();
- }
- function tone(type, f, t, dur, vol, bus, slideTo) {
- const o = AC.createOscillator(), g = AC.createGain();
- o.type = type; o.frequency.setValueAtTime(f, t);
- if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, t + dur);
- g.gain.setValueAtTime(0.0001, t);
- g.gain.exponentialRampToValueAtTime(vol, t + 0.01);
- g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
- o.connect(g); g.connect(bus || musicBus);
- o.start(t); o.stop(t + dur + 0.03);
- }
- function noiseHit(t, dur, vol, freq, bus) {
- const s = AC.createBufferSource(); s.buffer = noiseBuf;
- const f = AC.createBiquadFilter(); f.type = 'highpass'; f.frequency.value = freq;
- const g = AC.createGain();
- g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
- s.connect(f); f.connect(g); g.connect(bus || musicBus);
- s.start(t); s.stop(t + dur + 0.03);
- }
- function playStep(step, t) {
- const st = STATIONS[G.station];
- const bar = Math.floor(step / 16) % 4, s = step % 16;
- const ch = st.chords[bar];
- const rootMidi = 36 + st.root + ch[0];
- const tones = ch[1] ? [0, 3, 7, 12, 15] : [0, 4, 7, 12, 16];
- const sd = 60 / st.tempo / 4;
- if (st.bass === 0 && s % 2 === 0) tone('sawtooth', mtof(rootMidi + (s % 4 === 2 ? 12 : 0)), t, sd * 1.6, 0.22);
- if (st.bass === 1 && (s % 4 === 2 || s === 0 || s === 7)) tone('triangle', mtof(rootMidi + (s === 7 ? 7 : 0)), t, sd * 2.2, 0.32);
- if (st.bass === 2) tone('sawtooth', mtof(rootMidi + (s % 4 === 3 ? 12 : 0)), t, sd * 0.9, 0.17);
- const pat = (Math.floor(step / 16) % 2) ? st.leadB : st.leadA;
- const li = pat[s];
- if (li >= 0) tone(st.wave, mtof(rootMidi + 36 + tones[li]), t, sd * 1.7, 0.1);
- if (s === 0) { tones.slice(0, 3).forEach(o => tone('triangle', mtof(rootMidi + 24 + o), t, sd * 14, 0.05)); }
- if (st.kick.indexOf(s) >= 0) tone('sine', 150, t, 0.14, 0.5, musicBus, 40);
- if (s % 8 === 4) noiseHit(t, 0.12, 0.28, 1800);
- if (s % 2 === 1 || st.bass === 2) noiseHit(t, 0.03, 0.09, 7000);
- }
- function seqSched() {
- if (!AC) return;
- while (seqNext < AC.currentTime + 0.16) {
- playStep(seqStep, seqNext);
- seqNext += 60 / STATIONS[G.station].tempo / 4;
- seqStep++;
- }
- }
- function startMusic() {
- if (!AC) return;
- seqStep = 0; seqNext = AC.currentTime + 0.08;
- if (!seqTimer) seqTimer = setInterval(seqSched, 25);
- }
- function stopMusic() { if (seqTimer) { clearInterval(seqTimer); seqTimer = null; } }
- function sfxCrash() {
- if (!AC) return; const t = AC.currentTime;
- noiseHit(t, 0.7, 0.9, 300, sfxBus); tone('sawtooth', 220, t, 0.6, 0.35, sfxBus, 40);
- }
- function sfxBeep(hi) { if (!AC) return; tone('square', hi ? 880 : 440, AC.currentTime, hi ? 0.5 : 0.18, 0.2, sfxBus); }
- function sfxChime() {
- if (!AC) return; const t = AC.currentTime;
- [72, 76, 79, 84].forEach((m, i) => tone('square', mtof(m), t + i * 0.09, 0.3, 0.16, sfxBus));
- }
- function sfxClick() { if (!AC) return; tone('square', 1200, AC.currentTime, 0.04, 0.12, sfxBus); }
- function setMuted(m) {
- muted = m;
- if (AC) master.gain.setTargetAtTime(m ? 0 : 0.7, AC.currentTime, 0.02);
- const b = document.getElementById('mute');
- if (b) { b.setAttribute('aria-pressed', m ? 'true' : 'false'); b.textContent = m ? 'Sound off' : 'Sound on'; }
- }
- function engineAudio(sp, active) {
- if (!AC || !engGain) return;
- const t = AC.currentTime;
- engOsc.frequency.setTargetAtTime(36 + sp * 90 + (P.gear ? 0 : 10), t, 0.05);
- engOsc2.frequency.setTargetAtTime((36 + sp * 90) * 1.5, t, 0.05);
- engFilt.frequency.setTargetAtTime(300 + sp * 900, t, 0.05);
- engGain.gain.setTargetAtTime(active && !muted ? 0.05 + 0.05 * sp : 0, t, 0.08);
- }
- const GAMEKEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space', 'ShiftLeft', 'ShiftRight'];
- window.addEventListener('keydown', e => {
- if (GAMEKEYS.indexOf(e.code) >= 0) e.preventDefault();
- if (e.repeat) { keys[e.code] = true; return; }
- keys[e.code] = true;
- if (e.code === 'KeyM') { setMuted(!muted); return; }
- menuKey(e.code);
- });
- window.addEventListener('keyup', e => { keys[e.code] = false; });
- window.addEventListener('blur', () => { for (const k in keys) keys[k] = false; });
- function readInput() {
- return {
- left: keys.ArrowLeft || keys.KeyA || touch.left,
- right: keys.ArrowRight || keys.KeyD || touch.right,
- gas: keys.ArrowUp || keys.KeyW || keys.KeyZ || touch.gas,
- brake: keys.ArrowDown || keys.KeyS || keys.KeyX || touch.brake
- };
- }
- function toggleGear() {
- if (state !== 'play' && state !== 'countdown') return;
- P.gear = P.gear ? 0 : 1; sfxClick();
- }
- function menuKey(code) {
- const confirm = code === 'Enter' || code === 'Space';
- if (state === 'title') { if (confirm) { initAudio(); state = 'radio'; G.station = 0; startMusic(); } return; }
- if (state === 'radio') {
- if (code === 'ArrowLeft' || code === 'KeyA') { selectStation(-1); }
- else if (code === 'ArrowRight' || code === 'KeyD') { selectStation(1); }
- else if (confirm) { startGame(); }
- return;
- }
- if (paused) {
- if (code === 'KeyY') quitGame();
- else if (code === 'KeyN' || code === 'KeyQ' || code === 'Escape') setPaused(false);
- else if (code === 'ArrowLeft' || code === 'KeyA') { G.quitSel = 0; sfxClick(); }
- else if (code === 'ArrowRight' || code === 'KeyD') { G.quitSel = 1; sfxClick(); }
- else if (confirm) { if (G.quitSel === 0) quitGame(); else setPaused(false); }
- return;
- }
- if (state === 'countdown' || state === 'play') {
- if (code === 'KeyQ') { setPaused(true); return; }
- if (code === 'ShiftLeft' || code === 'ShiftRight' || code === 'Space' || code === 'KeyC') toggleGear();
- return;
- }
- if ((state === 'over' || state === 'goal') && confirm && G.over > 2.5) { stopMusic(); startAttract(); }
- }
- function selectStation(d) {
- G.station = (G.station + d + STATIONS.length) % STATIONS.length;
- sfxClick(); startMusic();
- }
- function startGame() {
- initAudio();
- enterFullscreen(); playLock(true);
- resetRun();
- state = 'countdown'; G.count = 3; G.over = 0;
- sfxBeep(false);
- }
- Array.prototype.forEach.call(document.querySelectorAll('[data-k]'), btn => {
- const k = btn.getAttribute('data-k');
- const down = e => {
- e.preventDefault(); initAudio();
- if (k === 'pause') { if (state === 'play' || state === 'countdown') setPaused(!paused); }
- else if (k === 'gear') { if (state === 'play' || state === 'countdown') toggleGear(); }
- else touch[k] = 1;
- btn.classList.add('on');
- };
- const up = e => { if (k !== 'gear' && k !== 'pause') touch[k] = 0; btn.classList.remove('on'); };
- btn.addEventListener('pointerdown', down);
- btn.addEventListener('pointerup', up);
- btn.addEventListener('pointercancel', up);
- btn.addEventListener('pointerleave', up);
- btn.addEventListener('contextmenu', e => e.preventDefault());
- });
- const muteBtn = document.getElementById('mute');
- if (muteBtn) muteBtn.addEventListener('click', () => { setMuted(!muted); });
- cv.addEventListener('pointerdown', e => {
- const r = cv.getBoundingClientRect(), fx = (e.clientX - r.left) / r.width, fy = (e.clientY - r.top) / r.height;
- if (paused) {
- const px = fx * W, py = fy * H;
- QUIT_BTN.forEach(function (b, i) { if (px >= b.x && px <= b.x + b.w && py >= b.y && py <= b.y + b.h) { if (i === 0) quitGame(); else setPaused(false); } });
- return;
- }
- if (state === 'title') menuKey('Enter');
- else if (state === 'radio') { if (fx < 0.28) menuKey('ArrowLeft'); else if (fx > 0.72) menuKey('ArrowRight'); else menuKey('Enter'); }
- else if (state === 'over' || state === 'goal') menuKey('Enter');
- });
- const PLAYERW = 1.95 * UPM / ROADW;
- function sfxCoin() { if (!AC) return; const t = AC.currentTime; tone('square', 988, t, .07, .13, sfxBus); tone('square', 1319, t + .07, .14, .13, sfxBus); }
- function sfxFuel() { if (!AC) return; const t = AC.currentTime; [523, 659, 784].forEach(function (f, i) { tone('triangle', f, t + i * .07, .16, .22, sfxBus); }); }
- function sfxBolt() { if (!AC) return; const t = AC.currentTime; tone('sawtooth', 220, t, .5, .22, sfxBus, 1760); noiseHit(t, .25, .25, 3000, sfxBus); }
- function sfxSmash() { if (!AC) return; const t = AC.currentTime; noiseHit(t, .22, .5, 500, sfxBus); tone('square', 180, t, .15, .2, sfxBus, 60); }
- let paused = false;
- function sfxLowFuel() { if (!AC) return; const t = AC.currentTime; tone('square', 1046, t, .11, .16, sfxBus); }
- function setPaused(on) {
- if (on === paused) return;
- paused = on; G.pt = 0; G.quitSel = 1;
- if (AC) { try { on ? AC.suspend() : AC.resume(); } catch (e) { } }
- for (const k in keys) keys[k] = false;
- touch.left = touch.right = touch.gas = touch.brake = 0;
- }
- function quitGame() {
- paused = false;
- if (AC) { try { AC.resume(); } catch (e) { } }
- stopMusic(); startAttract();
- }
- const QUIT_BTN = [{ x: 186, y: 226, w: 124, h: 44 }, { x: 330, y: 226, w: 124, h: 44 }];
- function drawQuit() {
- ctx.fillStyle = 'rgba(8,4,24,.72)'; ctx.fillRect(0, 0, W, H);
- ctx.fillStyle = '#1d1750'; ctx.fillRect(150, 120, 340, 180);
- ctx.strokeStyle = '#ffc93c'; ctx.lineWidth = 3; ctx.strokeRect(150, 120, 340, 180);
- txt('PAUSED', W / 2, 150, 10, '#aaa4dc', 'center', false);
- txt('QUIT GAME?', W / 2, 196, 20, '#ffffff', 'center');
- ['YES', 'NO'].forEach(function (label, i) {
- const b = QUIT_BTN[i], sel = G.quitSel === i;
- ctx.fillStyle = sel ? (i ? '#2fae5a' : '#ff4d6d') : '#34307a'; ctx.fillRect(b.x, b.y, b.w, b.h);
- ctx.strokeStyle = sel ? '#ffffff' : '#5a54a8'; ctx.lineWidth = sel ? 3 : 2; ctx.strokeRect(b.x, b.y, b.w, b.h);
- txt(label, b.x + b.w / 2, b.y + 29, 16, '#ffffff', 'center', false);
- });
- txt('Y / N - ARROWS + ENTER - Q TO RESUME', W / 2, 288, 7, '#aaa4dc', 'center', false);
- }
- function floatText(text, x, y, color) { G.floats.push({ text: text, x: x, y: y, life: 1, color: color || '#ffffff' }); }
- let realFS = false;
- function setPlayLayout(on) {
- try {
- const el = document.getElementById('stage');
- if (el) el.classList.toggle('playing', on);
- document.documentElement.classList.toggle('playing', on);
- document.documentElement.style.overflow = on ? 'hidden' : '';
- document.body.style.overflow = on ? 'hidden' : '';
- if (on) window.scrollTo(0, 0);
- } catch (e) { }
- }
- function enterFullscreen() {
- const el = document.getElementById('stage');
- if (!el || document.fullscreenElement || document.webkitFullscreenElement) return;
- const f = el.requestFullscreen || el.webkitRequestFullscreen;
- if (!f) return;
- try { const r = f.call(el); if (r && r.catch) r.catch(function () { }); } catch (e) { }
- }
- function exitFullscreen() {
- if (!(document.fullscreenElement || document.webkitFullscreenElement)) return;
- const f = document.exitFullscreen || document.webkitExitFullscreen;
- try { const r = f && f.call(document); if (r && r.catch) r.catch(function () { }); } catch (e) { }
- }
- function playLock(on) { setPlayLayout(on); }
- function onFsChange() {
- const on = !!(document.fullscreenElement || document.webkitFullscreenElement);
- if (on) realFS = true;
- else if (realFS) { realFS = false; if (state === 'play' || state === 'countdown') setPlayLayout(false); }
- }
- document.addEventListener('fullscreenchange', onFsChange);
- document.addEventListener('webkitfullscreenchange', onFsChange);
- function accelFor(s) {
- if (P.gear === 0) return ACCEL * 1.6;
- return ACCEL * (0.3 + 0.7 * clamp(s / LOWMAX, 0, 1));
- }
- function updateCarOffset(car, carSeg, playerSeg) {
- const look = 20;
- if (carSeg.index - playerSeg.index > DRAW) return 0;
- for (let i = 1; i < look; i++) {
- const seg = segs[(carSeg.index + i) % segs.length];
- if (seg === playerSeg && car.speed > P.speed && overlap(P.x, PLAYERW, car.offset, car.w, 1.25)) {
- const dir = car.offset > 0.5 ? -1 : (car.offset < -0.5 ? 1 : (car.offset > P.x ? 1 : -1));
- return dir / i * (car.speed - P.speed) / MAXSPD;
- }
- for (let j = 0; j < seg.cars.length; j++) {
- const o = seg.cars[j];
- if (car.speed > o.speed && overlap(car.offset, car.w, o.offset, o.w, 1.25)) {
- const dir = o.offset > 0.5 ? -1 : (o.offset < -0.5 ? 1 : (car.offset > o.offset ? 1 : -1));
- return dir / i * (car.speed - o.speed) / MAXSPD;
- }
- }
- }
- return (car.lane - car.offset) * 0.06;
- }
- function updateCars(dt, playerSeg) {
- for (let n = 0; n < cars.length; n++) {
- const car = cars[n];
- const old = findSeg(car.z);
- car.offset += updateCarOffset(car, old, playerSeg) * dt * 15;
- car.offset = clamp(car.offset, -0.8, 0.8);
- car.z += dt * car.speed;
- if (car.z >= trackLen) car.z -= trackLen;
- car.pct = (car.z % SEG) / SEG;
- const nw = findSeg(car.z);
- if (old !== nw) {
- const i = old.cars.indexOf(car);
- if (i >= 0) old.cars.splice(i, 1);
- nw.cars.push(car);
- }
- }
- }
- function sparks(n, x, y, c) {
- for (let i = 0; i < n; i++) parts.push({ x: x + (Math.random() - .5) * 60, y: y, vx: (Math.random() - .5) * 220, vy: -60 - Math.random() * 160, life: 0.3 + Math.random() * 0.4, c: c || '#ffd36a', r: 1.5 + Math.random() * 2 });
- }
- function doCrash() {
- if (P.crash > 0 || P.invuln > 0 || P.turbo > 0) return;
- P.crash = 2.3; P.spin = 0; P.shake = 1;
- sfxCrash();
- for (let i = 0; i < 18; i++) parts.push({ x: W / 2 + (Math.random() - .5) * 80, y: 330, vx: (Math.random() - .5) * 260, vy: -80 - Math.random() * 200, life: 0.8 + Math.random() * 0.6, c: ['#ffd36a', '#ff7a2f', '#f4f4f4'][i % 3], r: 2 + Math.random() * 3 });
- }
- function moveItems(dz) {
- for (let i = 0; i < items.length; i++) {
- const it = items[i];
- if (it.taken) continue;
- const old = findSeg(it.z);
- it.z += dz;
- if (it.z >= trackLen - SEG) { it.taken = true; const k = old.items.indexOf(it); if (k >= 0) old.items.splice(k, 1); continue; }
- it.pct = (it.z % SEG) / SEG;
- const nw = findSeg(it.z);
- if (nw !== old) { const k = old.items.indexOf(it); if (k >= 0) old.items.splice(k, 1); nw.items.push(it); }
- }
- }
- function collect(it) {
- it.taken = true;
- const px = W / 2 + (it.offset - P.x) * ROADW * (1 / CAMH) * W / 2;
- if (it.type === 'coin') { G.score += PTS_COIN; G.coins++; floatText('+' + PTS_COIN, W / 2, 300, '#ffd23a'); sfxCoin(); }
- else if (it.type === 'gas') { G.score += PTS_GAS; G.fuel = 1; floatText('+' + PTS_GAS + ' FUEL FULL', W / 2, 290, '#ff8a6a'); sfxFuel(); }
- else { P.turbo = TURBO_TIME; floatText('TURBO!', W / 2, 290, '#7fd0ff'); sfxBolt(); G.flash = 0.18; }
- void px;
- }
- function smashCar(c, sg) {
- const i = sg.cars.indexOf(c); if (i >= 0) sg.cars.splice(i, 1);
- const j = cars.indexOf(c); if (j >= 0) cars.splice(j, 1);
- G.score += PTS_BOT; P.shake = Math.max(P.shake, 0.35);
- const px = W / 2 + (c.offset - P.x) * ROADW * (1 / CAMH) * W / 2;
- floatText('+' + PTS_BOT, clamp(px, 40, W - 40), 270, '#ffffff');
- sparks(16, clamp(px, 40, W - 40), 300, '#ffb347'); sparks(8, clamp(px, 40, W - 40), 300, '#ffffff');
- sfxSmash();
- }
- function nextStage() {
- const carIdx = (P.pos + PLAYERZ) / SEG;
- const over = carIdx - (segs.length - CHK);
- G.time += 55;
- G.banner = 'CHECKPOINT'; G.bannerSub = '+55 SEC'; G.bannerT = 2.2; G.flash = 0.35;
- startStage(G.col + 1, false, (GSTART + over) * SEG);
- P.pos = Math.max(0, (GSTART + over) * SEG - PLAYERZ);
- sfxChime();
- }
- function driveUpdate(dt) {
- const inp = readInput();
- const live = state === 'play' && P.crash <= 0;
- const segP = findSeg(P.pos + PLAYERZ);
- const sp = P.speed / MAXSPD;
- const start = P.pos;
- updateCars(dt, segP);
- P.pos += P.speed * dt;
- moveItems(P.speed * dt * ITEM_SPEED);
- const dx = dt * 1.0 * sp;
- const turbo = P.turbo > 0;
- if (P.crash <= 0) {
- if (live) { if (inp.left) P.x -= dx; if (inp.right) P.x += dx; }
- P.x -= dx * sp * segP.curve * CENTRIF;
- }
- const steerT = (P.crash <= 0 && live) ? ((inp.right ? 1 : 0) - (inp.left ? 1 : 0)) : 0;
- P.steer += (steerT - P.steer) * Math.min(1, dt * 10);
- if (P.crash > 0) {
- P.speed = Math.max(0, P.speed - MAXSPD * 0.5 * dt);
- P.crash -= dt; P.spin += dt;
- if (P.crash <= 0) { P.invuln = 2; P.speed = 0; P.x = clamp(P.x, -0.8, 0.8); }
- } else if (turbo && state === 'play') {
- if (inp.brake) P.speed += BRAKING * dt; else P.speed = Math.min(MAXSPD * TURBO_SPEED, P.speed + ACCEL * 3 * dt);
- } else if (live && inp.gas && G.fuel > 0) P.speed += accelFor(P.speed) * dt;
- else if (live && inp.brake) P.speed += BRAKING * dt;
- else if (state === 'over' || state === 'goal') P.speed += BRAKING * 0.6 * dt;
- else P.speed += DECEL * dt;
- if (P.invuln > 0) P.invuln -= dt;
- P.shake = Math.max(0, P.shake - dt * 1.6);
- const lim = turbo ? MAXSPD * TURBO_SPEED : (P.gear ? MAXSPD : LOWMAX);
- if (P.speed > lim) P.speed = Math.max(lim, P.speed - MAXSPD * 0.3 * dt);
- if (state === 'play' && P.crash <= 0) {
- if (P.x < -RAIL_LIM && segP.railL) { P.x = -RAIL_LIM; if (!turbo) P.speed = Math.max(0, P.speed - MAXSPD * 0.6 * dt); if (P.speed > 1500 && Math.random() < 0.8) sparks(2, W / 2 - 70, 345); }
- else if (P.x > RAIL_LIM && segP.railR) { P.x = RAIL_LIM; if (!turbo) P.speed = Math.max(0, P.speed - MAXSPD * 0.6 * dt); if (P.speed > 1500 && Math.random() < 0.8) sparks(2, W / 2 + 70, 345); }
- }
- const off = Math.abs(P.x) > 1.05;
- if (off && P.speed > OFFLIMIT && P.crash <= 0 && !turbo) {
- P.speed += OFFDECEL * dt;
- if (Math.random() < 0.7) parts.push({ x: W / 2 + (Math.random() - .5) * 120, y: 345, vx: (Math.random() - .5) * 90, vy: -30 - Math.random() * 60, life: 0.5, c: PAL[G.col].grass[0], r: 2 + Math.random() * 3 });
- }
- P.x = clamp(P.x, -2.4, 2.4);
- P.speed = clamp(P.speed, 0, MAXSPD * TURBO_SPEED);
- if (P.crash <= 0 && P.invuln <= 0 && state === 'play') {
- if (off) {
- for (let i = 0; i < segP.sprites.length; i++) {
- const s = segP.sprites[i];
- if (s.solid > 0 && P.speed > 1500 && Math.abs(P.x - s.offset) < PLAYERW / 2 + s.solid) { doCrash(); break; }
- }
- }
- const nx = findSeg(P.pos + PLAYERZ + SEG);
- [segP, nx].forEach(function (sg) {
- for (let i = 0; i < sg.cars.length && P.crash <= 0; i++) {
- const c = sg.cars[i];
- if (P.speed > c.speed + 300 && overlap(P.x, PLAYERW, c.offset, c.w, 0.85)) doCrash();
- }
- });
- }
- if (turbo && state === 'play') {
- [segP, findSeg(P.pos + PLAYERZ + SEG)].forEach(function (sg) {
- for (let i = sg.cars.length - 1; i >= 0; i--) {
- const c = sg.cars[i];
- if (overlap(P.x, PLAYERW, c.offset, c.w, 0.95)) smashCar(c, sg);
- }
- });
- }
- if (state === 'play' && P.crash <= 0) {
- const cz = P.pos + PLAYERZ;
- for (let i = 0; i < items.length; i++) {
- const it = items[i];
- if (!it.taken && Math.abs(it.z - cz) < SEG * 1.1 && Math.abs(P.x - it.offset) < PLAYERW / 2 + 0.04) collect(it);
- }
- }
- const dpos = P.pos - start;
- if (state === 'play') {
- G.fuel = Math.max(0, G.fuel - dpos / (TANK_SHARE * (segs.length - CHK - GSTART) * SEG));
- if (P.turbo > 0) P.turbo = Math.max(0, P.turbo - dt);
- }
- bg1 += segP.curve * dpos / SEG * 0.18;
- bg2 += segP.curve * dpos / SEG * 0.55;
- if (state === 'play') {
- G.score += P.speed * dt * 0.02;
- G.time -= dt;
- if (G.time <= 0) { G.time = 0; endRun('TIME UP'); }
- else if (G.fuel <= 0) endRun('OUT OF FUEL');
- } else G.over += dt;
- const carIdx = (P.pos + PLAYERZ) / SEG;
- if (state === 'play' && carIdx >= segs.length - CHK + 2) {
- if (G.col < 4) nextStage();
- else {
- state = 'goal'; G.over = 0; P.turbo = 0;
- playLock(false); exitFullscreen();
- G.score += Math.floor(G.time) * 100;
- sfxChime();
- }
- }
- if (G.stageT > 0) G.stageT -= dt;
- if (G.bannerT > 0) G.bannerT -= dt;
- if (G.flash > 0) G.flash -= dt;
- if ((state === 'over' || state === 'goal') && G.over > 0.5 && G.score > G.hi) {
- G.hi = Math.floor(G.score);
- try { localStorage.setItem('coastline-hi', String(G.hi)); } catch (e) { }
- }
- }
- function endRun(why) {
- state = 'over'; G.over = 0; G.reason = why; P.turbo = 0; G.beepT = 0;
- stopMusicSoon(); playLock(false); exitFullscreen();
- }
- function stopMusicSoon() { setTimeout(function () { if (state === 'over') stopMusic(); }, 1200); }
- function attract(dt) {
- const segP = findSeg(P.pos + PLAYERZ);
- const start = P.pos;
- P.speed = 7600;
- updateCars(dt, segP);
- P.pos += P.speed * dt;
- if (P.pos > trackLen - 400 * SEG) { P.pos = 0; }
- P.x = LANE_C[3] + Math.sin(G.t * 0.5) * 0.2;
- P.steer = Math.cos(G.t * 0.5) * 0.5;
- const dpos = P.pos - start;
- if (dpos > 0) { bg1 += segP.curve * dpos / SEG * 0.18; bg2 += segP.curve * dpos / SEG * 0.55; }
- }
- function update(dt) {
- if (paused) { G.pt += dt; engineAudio(0, false); return; }
- G.t += dt;
- for (let i = parts.length - 1; i >= 0; i--) {
- const p = parts[i]; p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 300 * dt; p.life -= dt;
- if (p.life <= 0) parts.splice(i, 1);
- }
- for (let i = G.floats.length - 1; i >= 0; i--) { const f = G.floats[i]; f.life -= dt * 0.9; f.y -= dt * 40; if (f.life <= 0) G.floats.splice(i, 1); }
- if (state === 'title' || state === 'radio') {
- attract(dt); engineAudio(0.4, false); return;
- }
- if (state === 'countdown') {
- const before = Math.ceil(G.count);
- G.count -= dt;
- const after = Math.ceil(G.count);
- if (after !== before) { if (after > 0) sfxBeep(false); else { sfxBeep(true); } }
- if (G.count <= -0.7) { state = 'play'; startMusic(); }
- if (G.stageT > 0) G.stageT -= dt;
- engineAudio(0.1, true);
- return;
- }
- driveUpdate(dt);
- if (state === 'play' && G.fuel <= 0.5) {
- G.beepT -= dt;
- if (G.beepT <= 0) { sfxLowFuel(); G.beepT = G.fuel < 0.2 ? 0.28 : 0.55; }
- } else G.beepT = 0;
- engineAudio(P.speed / MAXSPD, state === 'play' || (state === 'over' && P.speed > 200));
- }
- function project(p, camX, camY, camZ) {
- p.camera.x = (p.world.x || 0) - camX;
- p.camera.y = p.world.y - camY;
- p.camera.z = p.world.z - camZ;
- p.screen.scale = CAMD / p.camera.z;
- p.screen.x = Math.round(W / 2 + p.screen.scale * p.camera.x * W / 2);
- p.screen.y = Math.round(HOR - p.screen.scale * p.camera.y * H / 2);
- p.screen.w = Math.round(p.screen.scale * ROADW * W / 2);
- }
- function tile(img, off, y) {
- let x = -(((off % 1024) + 1024) % 1024);
- while (x < W) { ctx.drawImage(img, x, y); x += 1024; }
- }
- function drawBackground(pal, col) {
- const g = ctx.createLinearGradient(0, 0, 0, HOR);
- g.addColorStop(0, pal.sky[0]); g.addColorStop(1, pal.sky[1]);
- ctx.fillStyle = g; ctx.fillRect(0, 0, W, HOR + 2);
- if (pal.stars) {
- ctx.fillStyle = '#fff';
- for (let i = 0; i < STARS.length; i++) {
- const s = STARS[i];
- const x = (((s[0] - bg1 * 0.3) % W) + W) % W;
- ctx.globalAlpha = 0.35 + 0.65 * s[2];
- ctx.fillRect(x, s[1] * HOR * 0.8, 1.6, 1.6);
- }
- ctx.globalAlpha = 1;
- }
- const s = pal.sun, sx = W * s.x, sy = HOR - s.h;
- const gl = ctx.createRadialGradient(sx, sy, s.r * 0.5, sx, sy, s.r * 3);
- gl.addColorStop(0, 'rgba(255,255,255,.35)'); gl.addColorStop(1, 'rgba(255,255,255,0)');
- ctx.fillStyle = gl; ctx.fillRect(sx - s.r * 3, sy - s.r * 3, s.r * 6, s.r * 6);
- ctx.fillStyle = s.c; ctx.beginPath(); ctx.arc(sx, sy, s.r, 0, 7); ctx.fill();
- const L = BGL[col];
- if (L.cloud) { ctx.globalAlpha = 1; tile(L.cloud, bg1 * 0.8, 10); }
- tile(L.far, bg1, HOR - L.far.height + 2);
- tile(L.near, bg2, HOR - L.near.height + 2);
- ctx.fillStyle = pal.grass[0]; ctx.fillRect(0, HOR, W, H - HOR);
- }
- function renderSegment(seg, pal, fog, ci, near) {
- const p1 = seg.p1.screen, p2 = seg.p2.screen;
- const x1 = p1.x, y1 = p1.y, w1 = p1.w, x2 = p2.x, y2 = p2.y, w2 = p2.w;
- const r1 = w1 / 20, r2 = w2 / 20;
- const b1 = w1 * LANE_B, b2 = w2 * LANE_B;
- ctx.fillStyle = pal.grass[ci]; ctx.fillRect(0, y2, W, y1 - y2);
- poly(x1 - w1 - r1, y1, x1 - w1, y1, x2 - w2, y2, x2 - w2 - r2, y2, pal.rumble[ci]);
- poly(x1 + w1 + r1, y1, x1 + w1, y1, x2 + w2, y2, x2 + w2 + r2, y2, pal.rumble[ci]);
- poly(x1 - w1, y1, x1 - b1, y1, x2 - b2, y2, x2 - w2, y2, pal.shoulder[ci]);
- poly(x1 + b1, y1, x1 + w1, y1, x2 + w2, y2, x2 + b2, y2, pal.shoulder[ci]);
- poly(x1 - b1, y1, x1 + b1, y1, x2 + b2, y2, x2 - b2, y2, pal.road[ci]);
- if (near) {
- for (let l = 0; l < LANES; l++) {
- const cf = LANE_C[l], hf = (LANE_E[l + 1] - LANE_E[l]) / 2;
- [-.26, .26].forEach(function (o) {
- const f = cf + hf * 2 * o, a1 = x1 + w1 * f, a2 = x2 + w2 * f, t1 = w1 * hf * .2, t2 = w2 * hf * .2;
- poly(a1 - t1, y1, a1 + t1, y1, a2 + t2, y2, a2 - t2, y2, 'rgba(0,0,0,.065)');
- });
- }
- }
- const e1 = w1 / 80, e2 = w2 / 80;
- poly(x1 - b1 - e1, y1, x1 - b1 + e1, y1, x2 - b2 + e2, y2, x2 - b2 - e2, y2, '#e9b91f');
- poly(x1 + b1 - e1, y1, x1 + b1 + e1, y1, x2 + b2 + e2, y2, x2 + b2 - e2, y2, pal.lane);
- if (ci === 0) {
- const l1 = w1 / 95, l2 = w2 / 95;
- for (let l = 1; l < LANES; l++) {
- const lx1 = x1 + w1 * LANE_E[l], lx2 = x2 + w2 * LANE_E[l];
- poly(lx1 - l1, y1, lx1 + l1, y1, lx2 + l2, y2, lx2 - l2, y2, pal.lane);
- }
- }
- if (fog > 0.01) {
- ctx.globalAlpha = fog; ctx.fillStyle = pal.fog; ctx.fillRect(0, y2, W, y1 - y2); ctx.globalAlpha = 1;
- }
- }
- function drawSprite(s, scale, cx, by, clipY, alpha) {
- const dw = s.wf * ROADW * scale * W / 2;
- if (!(dw >= 1.2) || dw > 60000) return;
- const dh = dw * s.h / s.w;
- const dx = cx - dw / 2, dy = by - dh;
- const bottom = clipY ? Math.min(by, clipY) : by;
- const vx0 = Math.max(dx, 0), vx1 = Math.min(dx + dw, W), vy0 = Math.max(dy, 0), vy1 = Math.min(bottom, H);
- if (vx1 - vx0 < 0.5 || vy1 - vy0 < 0.5) return;
- let lv = s.ch[0];
- for (let k = 1; k < s.ch.length; k++) { if (s.ch[k].width >= dw) lv = s.ch[k]; else break; }
- const lw = lv.width, lh = lv.height;
- ctx.globalAlpha = alpha;
- ctx.drawImage(lv, (vx0 - dx) / dw * lw, (vy0 - dy) / dh * lh, (vx1 - vx0) / dw * lw, (vy1 - vy0) / dh * lh, vx0, vy0, vx1 - vx0, vy1 - vy0);
- ctx.globalAlpha = 1;
- }
- function drawRail(seg, side, ci, alpha) {
- const p1 = seg.p1.screen, p2 = seg.p2.screen;
- if (seg.p1.camera.z < 60 || seg.p2.camera.z < 60) return;
- const clip = seg.clip, k = H / 2, concrete = G.col >= 3;
- const lat = RAIL_LAT;
- const X1 = p1.x + side * p1.w * lat, X2 = p2.x + side * p2.w * lat;
- const lo = concrete ? 0 : 170, hi = concrete ? 300 : 330;
- const yb1 = Math.min(p1.y - p1.scale * lo * k, clip), yb2 = Math.min(p2.y - p2.scale * lo * k, clip);
- const yt1 = Math.min(p1.y - p1.scale * hi * k, clip), yt2 = Math.min(p2.y - p2.scale * hi * k, clip);
- if (yt1 >= clip && yt2 >= clip) return;
- ctx.globalAlpha = alpha;
- const g1 = Math.min(p1.y, clip), g2 = Math.min(p2.y, clip);
- if (concrete) {
- poly(X1, g1, X1, yt1, X2, yt2, X2, g2, ci ? '#a9a8a2' : '#b6b5ae');
- poly(X1, yt1 + (yb1 - yt1) * 0, X1, yt1 + (g1 - yt1) * 0.1, X2, yt2 + (g2 - yt2) * 0.1, X2, yt2, '#d4d2ca');
- } else {
- poly(X1, g1, X1, yb1, X2, yb2, X2, g2, 'rgba(0,0,0,.22)');
- poly(X1, yb1, X1, yt1, X2, yt2, X2, yb2, ci ? '#a7adb5' : '#bcc2c9');
- poly(X1, yt1, X1, yt1 + (yb1 - yt1) * .22, X2, yt2 + (yb2 - yt2) * .22, X2, yt2, '#e4e8ec');
- poly(X1, yb1 - (yb1 - yt1) * .2, X1, yb1, X2, yb2, X2, yb2 - (yb2 - yt2) * .2, 'rgba(0,0,0,.18)');
- if (seg.index % 2 === 0) {
- const pw = Math.max(1, p1.scale * 56 * W / 2), top = Math.min(p1.y - p1.scale * 360 * k, clip);
- ctx.fillStyle = '#6b7078'; ctx.fillRect(X1 - pw / 2, top, pw, g1 - top);
- }
- }
- ctx.globalAlpha = 1;
- }
- function drawPlayer(cx, by, pw) {
- const s = SPR.player;
- const ph = pw * s.h / s.w;
- const t = P.crash > 0 ? 2.3 - P.crash : 0;
- const inp = readInput();
- let lv = s.ch[0];
- for (let k = 1; k < s.ch.length; k++) { if (s.ch[k].width >= pw) lv = s.ch[k]; else break; }
- ctx.save();
- let jitter = 0;
- if (P.speed > 200 && P.speed / MAXSPD > 0.1) jitter = Math.random() < 0.5 ? 0 : 1;
- if (Math.abs(P.x) > 1.05 && P.speed > 1500) jitter += (Math.random() - .5) * 5;
- let y = by - ph / 2 + ph * 0.03 - jitter;
- const x = cx;
- if (P.crash > 0) {
- const f = clamp(t / 1.2, 0, 1);
- y -= Math.sin(f * Math.PI) * 80;
- ctx.translate(x, y); ctx.rotate(f < 1 ? f * Math.PI * 4 : 0); ctx.translate(-x, -y);
- } else if (P.invuln > 0 && Math.floor(G.t * 14) % 2 === 0) {
- ctx.globalAlpha = 0.45;
- }
- ctx.translate(x, y);
- ctx.transform(1, 0, P.steer * -0.08, 1, 0, 0);
- ctx.scale(1 - Math.abs(P.steer) * 0.04, 1);
- if (P.turbo > 0) {
- const pulse = 0.55 + 0.25 * Math.sin(G.t * 30);
- const gr = ctx.createRadialGradient(0, 0, pw * .1, 0, 0, pw * .8);
- gr.addColorStop(0, 'rgba(255,240,120,' + pulse + ')'); gr.addColorStop(.5, 'rgba(120,200,255,' + (pulse * .5) + ')'); gr.addColorStop(1, 'rgba(120,200,255,0)');
- ctx.fillStyle = gr; ctx.fillRect(-pw, -ph, pw * 2, ph * 2);
- }
- ctx.drawImage(lv, -pw / 2, -ph / 2, pw, ph);
- if (inp.brake && P.crash <= 0 && state === 'play') {
- ctx.fillStyle = 'rgba(255,70,80,.55)';
- [-.27, .27].forEach(function (o) { ctx.beginPath(); ctx.arc(pw * o, ph * .22, pw * .09, 0, 7); ctx.fill(); });
- }
- ctx.restore();
- ctx.globalAlpha = 1;
- if (P.crash > 0 && t < 1.7) {
- [[-1, '#f3d37e', '#f4f1ec'], [1, '#4a2a17', '#2f6fd6']].forEach(function (pp) {
- const px = cx + pp[0] * t * 210, py = by - ph * 0.7 - 200 * t + 260 * t * t;
- ctx.fillStyle = pp[2]; ctx.fillRect(px - 5, py, 10, 12);
- ctx.fillStyle = '#d9a67f'; ctx.beginPath(); ctx.arc(px, py - 4, 6, 0, 7); ctx.fill();
- ctx.fillStyle = pp[1]; ctx.beginPath(); ctx.arc(px, py - 6, 6, Math.PI, 0); ctx.fill();
- });
- }
- }
- function render() {
- const pal = PAL[G.col];
- ctx.save();
- if (P.shake > 0) ctx.translate((Math.random() - .5) * P.shake * 10, (Math.random() - .5) * P.shake * 10);
- drawBackground(pal, G.col);
- const L = segs.length;
- const baseSeg = findSeg(P.pos);
- const basePct = (P.pos % SEG) / SEG;
- const playerSeg = findSeg(P.pos + PLAYERZ);
- const playerPct = ((P.pos + PLAYERZ) % SEG) / SEG;
- const playerY = interp(playerSeg.p1.world.y, playerSeg.p2.world.y, playerPct);
- let maxy = H, x = 0, dx = -(baseSeg.curve * basePct);
- const camY = playerY + CAMH;
- const drawn = [];
- for (let n = 0; n < DRAW; n++) {
- const seg = segs[(baseSeg.index + n) % L];
- const looped = seg.index < baseSeg.index;
- seg.fog = 1 - 1 / Math.exp((n / DRAW) * (n / DRAW) * FOGD);
- seg.looped = looped;
- project(seg.p1, P.x * ROADW - x, camY, P.pos - (looped ? trackLen : 0));
- project(seg.p2, P.x * ROADW - x - dx, camY, P.pos - (looped ? trackLen : 0));
- x += dx; dx += seg.curve;
- seg.clip = maxy;
- drawn.push(seg);
- if (seg.p1.camera.z <= CAMD || seg.p2.screen.y >= seg.p1.screen.y || seg.p2.screen.y >= maxy) continue;
- renderSegment(seg, pal, seg.fog, Math.floor(seg.index / RUMBLE) % 2, n < 110);
- maxy = seg.p1.screen.y;
- }
- const showPlayer = state !== 'title' && state !== 'radio';
- for (let n = drawn.length - 1; n >= 0; n--) {
- const seg = drawn[n];
- const a = 1 - seg.fog * 0.8;
- const ci = Math.floor(seg.index / RUMBLE) % 2;
- if (seg.looped && seg !== playerSeg) continue;
- if (seg.railL) drawRail(seg, -1, ci, a);
- if (seg.railR) drawRail(seg, 1, ci, a);
- for (let i = 0; i < seg.cars.length; i++) {
- const car = seg.cars[i];
- const sc = interp(seg.p1.screen.scale, seg.p2.screen.scale, car.pct);
- const cx = interp(seg.p1.screen.x, seg.p2.screen.x, car.pct) + sc * car.offset * ROADW * W / 2;
- const cy = interp(seg.p1.screen.y, seg.p2.screen.y, car.pct);
- drawSprite(car.spr, sc, cx, cy, seg.clip, a);
- }
- for (let i = 0; i < seg.sprites.length; i++) {
- const s = seg.sprites[i];
- if (seg.p1.camera.z < 40) continue;
- const sc = seg.p1.screen.scale;
- const cx = seg.p1.screen.x + sc * s.offset * ROADW * W / 2;
- drawSprite(s.src, sc, cx, seg.p1.screen.y, seg.clip, a);
- }
- for (let i = 0; i < seg.items.length; i++) {
- const it = seg.items[i];
- if (it.taken || seg.p1.camera.z < 80) continue;
- const sc = interp(seg.p1.screen.scale, seg.p2.screen.scale, it.pct);
- const cx = interp(seg.p1.screen.x, seg.p2.screen.x, it.pct) + sc * it.offset * ROADW * W / 2;
- const gy = interp(seg.p1.screen.y, seg.p2.screen.y, it.pct);
- const spr = it.type === 'coin' ? SPR.coin[Math.floor((G.t * 9 + it.phase * 2) % 8)] : (it.type === 'gas' ? SPR.gas : SPR.bolt);
- const hover = it.type === 'coin' ? 330 : 420, bob = Math.sin(G.t * 3 + it.phase) * 45;
- const dw = spr.wf * ROADW * sc * W / 2, dh = dw * spr.h / spr.w;
- const cyy = gy - sc * (hover + bob) * H / 2;
- ctx.globalAlpha = a * 0.28; ctx.fillStyle = '#000';
- ctx.beginPath(); ctx.ellipse(cx, gy, dw * 0.28, Math.max(1, dw * 0.07), 0, 0, 7); ctx.fill(); ctx.globalAlpha = 1;
- drawSprite(spr, sc, cx, cyy + dh / 2, seg.clip, a);
- }
- if (seg === playerSeg && showPlayer) {
- const sc = CAMD / PLAYERZ;
- const py = HOR - sc * (playerY - camY) * H / 2;
- drawPlayer(W / 2, py, SPR.player.wf * ROADW * sc * W / 2);
- }
- }
- for (let i = 0; i < parts.length; i++) {
- const p = parts[i]; ctx.globalAlpha = clamp(p.life * 2, 0, 1);
- ctx.fillStyle = p.c; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, 7); ctx.fill();
- }
- ctx.globalAlpha = 1;
- ctx.restore();
- if (G.flash > 0) { ctx.globalAlpha = clamp(G.flash * 2.5, 0, 1); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, W, H); ctx.globalAlpha = 1; }
- drawUI();
- if (paused) drawQuit();
- }
- function drawStageBar() {
- const ox = 14, oy = 14;
- ctx.fillStyle = 'rgba(18,10,42,.62)'; ctx.fillRect(ox - 8, oy - 8, 182, 58);
- txt('STAGE ' + (G.col + 1) + '/5', ox, oy + 10, 9, '#ffc93c');
- const prog = clamp(((P.pos + PLAYERZ) / SEG) / (segs.length - CHK), 0, 1);
- for (let i = 0; i < 5; i++) {
- const x = ox + i * 33, y = oy + 20;
- ctx.fillStyle = '#3a3580'; ctx.fillRect(x, y, 29, 9);
- if (i < G.col) { ctx.fillStyle = '#ffc93c'; ctx.fillRect(x, y, 29, 9); }
- else if (i === G.col) { ctx.fillStyle = '#ff4d6d'; ctx.fillRect(x, y, 29 * prog, 9); }
- }
- txt(STAGE_NAMES[G.col], ox, oy + 44, 7, '#ffffff', 'left', false);
- }
- function drawUI() {
- if (state === 'title') return drawTitle();
- if (state === 'radio') return drawRadio();
- drawStageBar();
- txt('TIME', W / 2, 28, 10, '#ffc93c', 'center');
- const low = G.time < 10 && Math.floor(G.t * 4) % 2 === 0;
- txt(String(Math.ceil(G.time)).padStart(2, '0'), W / 2, 62, 32, low ? '#ff4d6d' : '#ffffff', 'center');
- txt('SCORE', W - 14, 28, 10, '#ffc93c', 'right');
- txt(String(Math.floor(G.score)).padStart(7, '0'), W - 14, 52, 18, '#ffffff', 'right');
- txt('HI ' + String(G.hi).padStart(7, '0'), W - 14, 74, 9, '#aaa4dc', 'right');
- const kmh = Math.round(P.speed / MAXSPD * 293);
- txt(String(kmh).padStart(3, ' '), W - 62, H - 18, 28, '#ffffff', 'right');
- txt('KM/H', W - 14, H - 18, 10, '#ffc93c', 'right');
- ctx.fillStyle = 'rgba(18,10,42,.7)'; ctx.fillRect(12, H - 50, 64, 38);
- txt('GEAR', 18, H - 36, 8, '#ffc93c');
- txt(P.gear ? 'HI' : 'LO', 18, H - 18, 14, P.gear ? '#ff4d6d' : '#7fe0ff');
- ctx.fillStyle = 'rgba(18,10,42,.7)'; ctx.fillRect(84, H - 50, 136, 38);
- txt('FUEL', 90, H - 36, 8, '#ffc93c');
- const fcol = G.fuel > 0.4 ? '#5be37a' : (G.fuel > 0.2 ? '#ffc93c' : (Math.floor(G.t * 4) % 2 ? '#ff4d6d' : '#7a1f35'));
- for (let i = 0; i < 10; i++) {
- ctx.fillStyle = (i + 1) / 10 <= G.fuel + 0.0001 || (i / 10 < G.fuel) ? fcol : '#3a3580';
- ctx.fillRect(90 + i * 12.4, H - 28, 10, 11);
- }
- if (P.turbo > 0) {
- txt('TURBO', W / 2, 96, 10, '#7fd0ff', 'center');
- ctx.fillStyle = '#1b2a6a'; ctx.fillRect(W / 2 - 60, 104, 120, 8);
- ctx.fillStyle = '#ffe14a'; ctx.fillRect(W / 2 - 60, 104, 120 * P.turbo / TURBO_TIME, 8);
- }
- if (state === 'play' && G.fuel <= 0.5 && Math.floor(G.t * 3) % 2 === 0) txt('LOW FUEL', W / 2, 130, 14, '#ff4d6d', 'center');
- for (let i = 0; i < G.floats.length; i++) {
- const f = G.floats[i]; ctx.globalAlpha = clamp(f.life * 1.5, 0, 1);
- txt(f.text, f.x, f.y, 11, f.color, 'center'); ctx.globalAlpha = 1;
- }
- if (state === 'play' && G.stageT > 0 && G.stageT < 2.6 && !(G.bannerT > 0)) {
- txt('STAGE ' + (G.col + 1), W / 2, 150, 18, '#ffc93c', 'center');
- txt(STAGE_NAMES[G.col], W / 2, 178, 12, '#ffffff', 'center');
- }
- if (G.bannerT > 0) {
- txt(G.banner, W / 2, 150, 24, '#ffc93c', 'center');
- txt(G.bannerSub, W / 2, 182, 16, '#ffffff', 'center');
- }
- if (state === 'countdown') {
- txt('STAGE 1', W / 2, 120, 16, '#ffc93c', 'center');
- txt(STAGE_NAMES[0], W / 2, 146, 11, '#ffffff', 'center');
- const n = Math.ceil(G.count);
- if (n > 0) txt(String(n), W / 2, 230, 64, '#ffffff', 'center');
- else txt('GO!', W / 2, 230, 56, '#ff4d6d', 'center');
- }
- if (state === 'over') {
- txt('GAME OVER', W / 2, 160, 32, '#ff4d6d', 'center');
- if (G.reason) txt(G.reason, W / 2, 186, 12, '#ffc93c', 'center');
- txt('SCORE ' + Math.floor(G.score), W / 2, 222, 14, '#ffffff', 'center');
- if (G.over > 2.5 && Math.floor(G.t * 2) % 2 === 0) txt('PRESS ENTER OR TAP', W / 2, 262, 11, '#ffc93c', 'center');
- }
- if (state === 'goal') {
- ctx.fillStyle = 'rgba(18,10,42,.55)'; ctx.fillRect(0, 96, W, 150);
- txt('CONGRATULATIONS!', W / 2, 138, 22, '#ffc93c', 'center');
- txt('YOU REACHED MOON BAY', W / 2, 170, 12, '#ffffff', 'center');
- txt('SCORE ' + Math.floor(G.score) + ' (+' + (Math.floor(G.time) * 100) + ' TIME BONUS)', W / 2, 200, 9, '#aee3ff', 'center');
- if (G.over > 2.5 && Math.floor(G.t * 2) % 2 === 0) txt('PRESS ENTER OR TAP', W / 2, 232, 10, '#ffc93c', 'center');
- }
- }
- const TITLE_FONT = 'ui-monospace, Menlo, Consolas, "Courier New", monospace';
- function drawTitle() {
- ctx.fillStyle = 'rgba(18,10,42,.35)'; ctx.fillRect(0, 0, W, H);
- ctx.save();
- ctx.translate(W / 2, 104); ctx.transform(1, 0, -0.12, 1, 0, 0);
- const g = ctx.createLinearGradient(0, -40, 0, 70);
- g.addColorStop(0, '#fff2a0'); g.addColorStop(0.5, '#ffc93c'); g.addColorStop(1, '#ff4d6d');
- ctx.font = 'bold 62px ' + TITLE_FONT; ctx.textAlign = 'center'; ctx.lineJoin = 'round';
- ctx.lineWidth = 12; ctx.strokeStyle = '#120a2a'; ctx.strokeText('COASTLINE', 0, 0);
- ctx.fillStyle = g; ctx.fillText('COASTLINE', 0, 0);
- ctx.font = 'bold 78px ' + TITLE_FONT; ctx.lineWidth = 13; ctx.strokeText('LANEWAYS', 0, 76);
- ctx.fillStyle = g; ctx.fillText('LANEWAYS', 0, 76);
- ctx.restore();
- if (Math.floor(G.t * 2) % 2 === 0) txt('PRESS ENTER OR TAP', W / 2, 300, 14, '#ffffff', 'center');
- txt('HI ' + String(G.hi).padStart(7, '0'), W / 2, 332, 10, '#ffc93c', 'center');
- txt('FIVE STAGES - SIX LANES - ONE COAST', W / 2, 372, 8, '#aee3ff', 'center');
- }
- function drawRadio() {
- ctx.fillStyle = 'rgba(12,6,34,.78)'; ctx.fillRect(0, 0, W, H);
- txt('CHOOSE YOUR RADIO', W / 2, 62, 18, '#ffc93c', 'center');
- const st = STATIONS[G.station];
- const cx = W / 2, cy = 200;
- ctx.fillStyle = '#1d1750'; ctx.fillRect(cx - 190, cy - 76, 380, 152);
- ctx.strokeStyle = '#ffc93c'; ctx.lineWidth = 3; ctx.strokeRect(cx - 190, cy - 76, 380, 152);
- txt(st.name, cx, cy - 34, 20, '#ffffff', 'center');
- txt(st.tag, cx, cy - 10, 9, '#aee3ff', 'center');
- for (let i = 0; i < 16; i++) {
- const h = 6 + (0.5 + 0.5 * Math.sin(G.t * (4 + i * 0.7) + i * 1.3)) * 40;
- ctx.fillStyle = i % 2 ? '#ff4d6d' : '#ffc93c';
- ctx.fillRect(cx - 120 + i * 15, cy + 62 - h, 10, h);
- }
- ctx.fillStyle = '#ffc93c';
- const nudge = Math.sin(G.t * 6) * 3;
- ctx.beginPath(); ctx.moveTo(cx - 224 - nudge, cy); ctx.lineTo(cx - 204 - nudge, cy - 18); ctx.lineTo(cx - 204 - nudge, cy + 18); ctx.fill();
- ctx.beginPath(); ctx.moveTo(cx + 224 + nudge, cy); ctx.lineTo(cx + 204 + nudge, cy - 18); ctx.lineTo(cx + 204 + nudge, cy + 18); ctx.fill();
- for (let i = 0; i < STATIONS.length; i++) {
- ctx.fillStyle = i === G.station ? '#ffc93c' : '#4a4490';
- ctx.beginPath(); ctx.arc(cx - 16 + i * 16, cy + 100, 4, 0, 7); ctx.fill();
- }
- txt('LEFT / RIGHT TO TUNE', W / 2, 320, 9, '#aaa4dc', 'center');
- if (Math.floor(G.t * 2) % 2 === 0) txt('ENTER TO START', W / 2, 352, 13, '#ffffff', 'center');
- }
- let last = 0, acc = 0, running = false;
- function frame(ts) {
- const t = ts / 1000;
- const dt = last ? Math.min(0.1, t - last) : 0;
- last = t; acc += dt;
- const STEP = 1 / 60;
- while (acc >= STEP) { update(STEP); acc -= STEP; }
- render();
- requestAnimationFrame(frame);
- }
- function init() {
- PAL.forEach(function (p) { p.shoulder = [shade(p.road[0], .09), shade(p.road[1], .09)]; });
- buildSprites();
- buildBackgrounds();
- startAttract();
- setMuted(false);
- window.__coastline.ready = true;
- running = true;
- requestAnimationFrame(frame);
- }
- ctx.fillStyle = '#0d0a22'; ctx.fillRect(0, 0, W, H);
- txt('LOADING', W / 2, H / 2, 16, '#ffc93c', 'center');
- const fontsReady = (document.fonts && document.fonts.load)
- ? Promise.race([
- Promise.all([document.fonts.load('700 20px Overpass'), document.fonts.load('800 20px Overpass'), document.fonts.load('16px "Press Start 2P"')]),
- new Promise(function (r) { setTimeout(r, 2500); })
- ])
- : Promise.resolve();
- window.__coastline = {
- ready: false, update: update, render: render, G: G, P: P, menuKey: menuKey, keys: keys,
- state: function () { return state; }, startStage: startStage, segs: function () { return segs; },
- spr: function () { return SPR; }, cars: function () { return cars; }, items: function () { return items; }, fs: enterFullscreen, paused: function () { return paused; }
- };
- fontsReady.catch(function () { }).then(init);
- })();
- </script>
- </body></html>
Advertisement