(canvas: HTMLCanvasElement)
| 851 | * @param canvas - The canvas element to draw into. Must support 2D context. |
| 852 | */ |
| 853 | export function createRenderer(canvas: HTMLCanvasElement): Renderer { |
| 854 | const maybeCtx = canvas.getContext("2d"); |
| 855 | if (!maybeCtx) { |
| 856 | throw new Error("[traffic-intersection] Failed to get 2D canvas context"); |
| 857 | } |
| 858 | const ctx: CanvasRenderingContext2D = maybeCtx; |
| 859 | let rafId = 0; |
| 860 | let frameCount = 0; |
| 861 | const vehicles = spawnVehicles(); |
| 862 | const pedestrians = spawnPedestrians(); |
| 863 | |
| 864 | function frame(getCompositeState: () => string): void { |
| 865 | frameCount++; |
| 866 | const composite = getCompositeState(); |
| 867 | const signals = getSignalStates(composite); |
| 868 | |
| 869 | // --- Update --- |
| 870 | const aheadMap = computeVehicleAheadPositions(vehicles); |
| 871 | for (const v of vehicles) { |
| 872 | const vSig = vehicleSignalForDirection(v.direction, signals); |
| 873 | updateVehicle(v, vSig, aheadMap.get(v) ?? null); |
| 874 | } |
| 875 | for (const ped of pedestrians) { |
| 876 | const pSig = pedSignalForCrosswalk(ped.crosswalk, signals); |
| 877 | updatePedestrian(ped, pSig); |
| 878 | } |
| 879 | |
| 880 | // --- Draw --- |
| 881 | drawGround(ctx); |
| 882 | drawRoads(ctx); |
| 883 | drawLaneMarkings(ctx); |
| 884 | drawCrosswalks(ctx); |
| 885 | drawStopLines(ctx); |
| 886 | |
| 887 | for (const v of vehicles) { |
| 888 | drawVehicle(ctx, v); |
| 889 | } |
| 890 | for (const ped of pedestrians) { |
| 891 | drawPedestrian(ctx, ped); |
| 892 | } |
| 893 | |
| 894 | // Vehicle signals — one per phase, on opposite corners |
| 895 | drawVehicleSignal(ctx, VEHICLE_SIGNAL_POSITIONS.ns, signals.nsVehicle); |
| 896 | drawVehicleSignal(ctx, VEHICLE_SIGNAL_POSITIONS.ew, signals.ewVehicle); |
| 897 | |
| 898 | // Pedestrian signals — ns crosswalks use ewPed, ew crosswalks use nsPed |
| 899 | // (ns crosswalks cross the N/S road, safe when E/W phase is active) |
| 900 | drawPedSignal(ctx, PED_SIGNAL_POSITIONS["ns-top"], signals.ewPed, frameCount); |
| 901 | drawPedSignal(ctx, PED_SIGNAL_POSITIONS["ns-bottom"], signals.ewPed, frameCount); |
| 902 | drawPedSignal(ctx, PED_SIGNAL_POSITIONS["ew-left"], signals.nsPed, frameCount); |
| 903 | drawPedSignal(ctx, PED_SIGNAL_POSITIONS["ew-right"], signals.nsPed, frameCount); |
| 904 | |
| 905 | rafId = requestAnimationFrame(() => frame(getCompositeState)); |
| 906 | } |
| 907 | |
| 908 | return { |
| 909 | start(getCompositeState: () => string): void { |
| 910 | if (rafId !== 0) { |
no test coverage detected