/* render.js — Tron-style vector rendering of the play field, obstacles, goal * and the rig (car + trailer). Glow is done with shadowBlur on neon strokes. */ (function (TOW) { 'use strict'; var COL = { bg: '#04070e', grid: 'rgba(40,120,160,0.16)', border: '#19d3ff', car: '#19d3ff', trailer: '#7af9c0', wheel: '#ffd54a', hitch: '#ffffff', trail: 'rgba(122,249,192,0.5)', goal: '#39ff8b', goalBad: '#ffe14d', wall: '#ff4d6d', parkedCar: '#b27bff', hedge: '#5ee06a', cone: '#ff9d3a' }; function obColor(t) { if (t === 'car') return COL.parkedCar; if (t === 'hedge') return COL.hedge; if (t === 'cone') return COL.cone; return COL.wall; } function neonRect(ctx, b, stroke, lw, blur) { var c = TOW.corners(b); ctx.save(); ctx.lineWidth = lw; ctx.strokeStyle = stroke; ctx.shadowColor = stroke; ctx.shadowBlur = blur; ctx.beginPath(); ctx.moveTo(c[0].x, c[0].y); for (var i = 1; i < 4; i++) ctx.lineTo(c[i].x, c[i].y); ctx.closePath(); ctx.stroke(); ctx.restore(); } function fillRectFaint(ctx, b, fill) { var c = TOW.corners(b); ctx.save(); ctx.fillStyle = fill; ctx.beginPath(); ctx.moveTo(c[0].x, c[0].y); for (var i = 1; i < 4; i++) ctx.lineTo(c[i].x, c[i].y); ctx.closePath(); ctx.fill(); ctx.restore(); } function drawGrid(ctx, W, H) { ctx.save(); ctx.strokeStyle = COL.grid; ctx.lineWidth = 1; var step = 40, x, y; ctx.beginPath(); for (x = step; x < W; x += step) { ctx.moveTo(x, 0); ctx.lineTo(x, H); } for (y = step; y < H; y += step) { ctx.moveTo(0, y); ctx.lineTo(W, y); } ctx.stroke(); ctx.restore(); } function drawGoal(ctx, goal, ok) { var b = { cx: goal.x, cy: goal.y, hw: goal.w / 2, hh: goal.h / 2, a: goal.heading }; var col = ok ? COL.goal : COL.goalBad; fillRectFaint(ctx, b, ok ? 'rgba(57,255,139,0.12)' : 'rgba(255,225,77,0.07)'); ctx.save(); ctx.setLineDash([12, 9]); neonRect(ctx, b, col, 2.5, 16); ctx.restore(); // arrow showing desired trailer direction (axle -> hitch) ctx.save(); ctx.translate(goal.x, goal.y); ctx.rotate(goal.heading); ctx.strokeStyle = col; ctx.fillStyle = col; ctx.shadowColor = col; ctx.shadowBlur = 12; ctx.lineWidth = 2.5; var L = Math.min(goal.w, goal.h) * 0.3; ctx.beginPath(); ctx.moveTo(-L, 0); ctx.lineTo(L, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(L, 0); ctx.lineTo(L - 10, -7); ctx.lineTo(L - 10, 7); ctx.closePath(); ctx.fill(); ctx.restore(); } function drawTrail(ctx, pts) { if (pts.length < 2) return; ctx.save(); ctx.strokeStyle = COL.trail; ctx.lineWidth = 2; ctx.shadowColor = COL.trail; ctx.shadowBlur = 6; ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); for (var i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y); ctx.stroke(); ctx.restore(); } // small filled wheel rectangle centered at p, oriented by ang function drawWheel(ctx, p, ang, len, wid, col) { ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(ang); ctx.fillStyle = col; ctx.shadowColor = col; ctx.shadowBlur = 8; ctx.fillRect(-len / 2, -wid / 2, len, wid); ctx.restore(); } function drawRig(ctx, veh) { var g = veh.geo; var bd = veh.bodies(); var ct = Math.cos(veh.theta), st = Math.sin(veh.theta); var cf = Math.cos(veh.phi), sf = Math.sin(veh.phi); // --- trailer (draw first so the car sits on top at the hitch) // drawbar / tongue ctx.save(); ctx.strokeStyle = COL.trailer; ctx.shadowColor = COL.trailer; ctx.shadowBlur = 10; ctx.lineWidth = 4; var tBodyFront = { x: bd.hitch.x - g.tongue * cf, y: bd.hitch.y - g.tongue * sf }; ctx.beginPath(); ctx.moveTo(bd.hitch.x, bd.hitch.y); ctx.lineTo(tBodyFront.x, tBodyFront.y); ctx.stroke(); ctx.restore(); fillRectFaint(ctx, bd.trailerBox, 'rgba(122,249,192,0.10)'); neonRect(ctx, bd.trailerBox, COL.trailer, 3, 14); // trailer wheels at the trailer axle var twoff = (g.trailerWidth / 2) + 3; drawWheel(ctx, { x: bd.trailerAxle.x - sf * twoff, y: bd.trailerAxle.y + cf * twoff }, veh.phi, 20, 7, COL.trailer); drawWheel(ctx, { x: bd.trailerAxle.x + sf * twoff, y: bd.trailerAxle.y - cf * twoff }, veh.phi, 20, 7, COL.trailer); // --- car fillRectFaint(ctx, bd.carBox, 'rgba(25,211,255,0.10)'); neonRect(ctx, bd.carBox, COL.car, 3, 14); // rear wheels (aligned with body) var rwoff = (g.carWidth / 2) + 2; drawWheel(ctx, { x: bd.rax.x - st * rwoff, y: bd.rax.y + ct * rwoff }, veh.theta, 20, 8, COL.car); drawWheel(ctx, { x: bd.rax.x + st * rwoff, y: bd.rax.y - ct * rwoff }, veh.theta, 20, 8, COL.car); // front wheels (steered) var sa = veh.theta + veh.steer; drawWheel(ctx, { x: bd.frontAxle.x - st * rwoff, y: bd.frontAxle.y + ct * rwoff }, sa, 22, 8, COL.wheel); drawWheel(ctx, { x: bd.frontAxle.x + st * rwoff, y: bd.frontAxle.y - ct * rwoff }, sa, 22, 8, COL.wheel); // a "nose" line so heading is obvious ctx.save(); ctx.strokeStyle = COL.car; ctx.shadowColor = COL.car; ctx.shadowBlur = 10; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(bd.carBox.cx, bd.carBox.cy); ctx.lineTo(bd.carBox.cx + ct * (g.L / 2 + g.frontOverhang), bd.carBox.cy + st * (g.L / 2 + g.frontOverhang)); ctx.stroke(); ctx.restore(); // hitch joint ctx.save(); ctx.fillStyle = COL.hitch; ctx.shadowColor = COL.hitch; ctx.shadowBlur = 10; ctx.beginPath(); ctx.arc(bd.hitch.x, bd.hitch.y, 4, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function drawScene(ctx, state) { var W = TOW.WORLD.W, H = TOW.WORLD.H; ctx.fillStyle = COL.bg; ctx.fillRect(0, 0, W, H); drawGrid(ctx, W, H); // border neonRect(ctx, { cx: W / 2, cy: H / 2, hw: W / 2 - 3, hh: H / 2 - 3, a: 0 }, COL.border, 2, 10); drawGoal(ctx, state.map.goal, state.won); drawTrail(ctx, state.trail); var obs = state.map.obstacles; for (var i = 0; i < obs.length; i++) { var o = obs[i]; var b = { cx: o.x, cy: o.y, hw: o.w / 2, hh: o.h / 2, a: o.a || 0 }; var col = obColor(o.type); fillRectFaint(ctx, b, 'rgba(255,255,255,0.04)'); neonRect(ctx, b, col, 2.5, 12); } drawRig(ctx, state.veh); if (state.bumpFlash > 0) { ctx.save(); ctx.fillStyle = 'rgba(255,77,109,' + (0.28 * state.bumpFlash) + ')'; ctx.fillRect(0, 0, W, H); ctx.restore(); } } TOW.render = { drawScene: drawScene, COL: COL }; })(window.TOW = window.TOW || {});