- Performance needs to be good with all devices
- Controls take a lot of space and aren't always important
- Many small visual details can be overlooked, so rather polish the important features
- Let compressors help in making the code compact
1. Visual approach
My initial idea was to do a very realistic looking demo of a car driving along a highway on a dark night. I watched some dashcam videos on YouTube and realized that what we see in real life is very limited - it's mainly just lights and silhouettes. I made notes on what I saw and how I could try and implement it. But first I needed a good yet small engine for road construction and rendering.
I wanted the demo to have true perpective 3D drawing combined with cheap tricks. I found out that I only needed to do the center line with real polygons and somehow that makes everything else around it look more realistic. Also using shadowBlur on the center line alone is enough to give the impression that all lights are glowing. I would have liked to do a lot of blurred lights but the performance cost was huge and I didn't want to fall into the same trap as last year. So the initial idea of photorealistic graphics was dropped in favor of vintage video game looks. Another thing that helps to fool the eye is high speed. This allowed the graphics to be very simple and raw as you can see from the "sprite sheet" below. Car lights are slightly offset to give a hint that the cars are at a small angle. Turn signal and emergency lights are animated. All these graphics are drawn with fillRect.
2. Track engine
3. Rendering tricks
4. Creating the atmosphere
c.fillStyle="hsl("+99+m%99+",50%,9%)";
c.fillRect(-600,-600,1200,600);
// Flickering stars
for(c.fillStyle=c.shadowColor="#FFF",i=600;i--;)c.fillRect(400-i*i%800,-i%600,1,Z*i%.9);
// Moon
c.fillText("(",99,-99);
5. Conclusions and source code
Using modulus operator (remainder) on the track position index is the key to placing objects along the track, but also helps in adding all sorts of effects Cosine waves are everywhere to achieve that analog feel HTML5 canvas translate and rotate functions were used to do visually striking effects with very small cost
// Create track
for(T=[B=F=Z=D=i=0];i<600;i++){
if(i%30<1)m=D,D=1800-(i%4-2)*600;
T[i]=m+(D-m)*(.5-Math.cos(i%30/9)/2)
}
// Timer loop
setInterval(function(){
m=Z/5|0;
// Reset canvas and fill with black
c.fillRect(0,0,a.width=a.height=600,600);
// Dragon flight waving and tilting
c.translate(300,300+Math.cos(Z/99)*99);
c.rotate(i>540?Z/50%6:Math.cos(i/9)/9);
// Sky with some polar light effects
c.fillStyle="hsl("+99+m%99+",50%,9%)";
c.fillRect(-600,-600,1200,600);
// Flickering stars
for(c.fillStyle=c.shadowColor="#FFF",i=600;i--;)c.fillRect(400-i*i%800,-i%600,1,Z*i%.9);
// Moon
c.fillText("(",99,-99);
// Loop through visible distance of the track
for (i=99+m;i>m;i--){
D=i-Z/5;
// Hills
y=Math.cos(Math.cos(i/9+i/50)+8)*300;
// Calculate road and camera direction from track data
O=T[m]+(T[m+1]-T[m])*(Z%5)/5-T[i];
if(i%3<1){
// Draw ground and hills
c.rotate(Math.cos(i)/9); // Do we need to rotate at all?
c.fillRect(-600,300/D+y/D+9,1200,Math.cos(i));
c.rotate(Math.cos(i)/-9);
// Draw white lines
c.shadowBlur=D<9?30:0;
if(i%2){
c.beginPath();
c.moveTo(-30/D+O/D,300/D+y/D);
c.lineTo(30/D+O/D,300/D+y/D)
}
if(i%2<1){
c.lineTo(30/D+O/D,300/D+y/D);
c.lineTo(-30/D+O/D,300/D+y/D);
c.fill()
}
c.shadowBlur=0
}
// Road
c.fillStyle="#000";
c.fillRect(-600/D+O/D,300/D+y/D,1200/D,60/D);
if(i%8<1){ // Draw street or tunnel lights
c.fillRect(-800/D+O/D,300/D+y/D-800/D,30/D,800/D);
c.fillStyle="#FFA";
i%400<80?c.fillRect(-100/D+O/D,300/D+y/D-600/D,200/D,50/D):c.fillRect(-600/D+O/D,300/D+y/D-800/D,80/D,20/D)
}
if(i%400<80){ // Tunnel walls
c.fillStyle=i%2?0:"#222";
c.fillRect(600/D+O/D,300/D+y/D-900/D,1400/D,900/D);
c.fillRect(-2e3/D+O/D,300/D+y/D-900/D,1400/D,900/D);
c.fillRect(-2e3/D+O/D,300/D+y/D-900/D,4e3/D,300/D)
}
// Draw cars
c.fillStyle="#000";
if((i+~~B)%30==0){
c.fillRect(150/D+O/D,300/D+y/D-200/D,260/D,160/D);
c.fillStyle="red";
c.fillRect(180/D+O/D,300/D+y/D-90/D,60/D,30/D);
c.fillRect(340/D+O/D,300/D+y/D-90/D,60/D,30/D);
// Turn signal
c.fillStyle="#FFA";
i%300<99&&i%9<5?c.fillRect(140/D+O/D,300/D+y/D-90/D,60/D,30/D):0
}
if((i+F)%40<1){
c.fillStyle="#000";
c.fillRect(-400/D+O/D,300/D+y/D-200/D,260/D,1/D*160);
c.fillStyle="#FFA";
c.fillRect(-240/D+O/D,300/D+y/D-90/D,60/D,30/D);
c.fillRect(-400/D+O/D,300/D+y/D-90/D,60/D,30/D);
// Police car
if(i%200>99)
c.fillStyle=i%2?"red":"#00F",
c.fillRect(i%3-(i%2*80+260)/D+O/D,300/D+y/D-220/D,60/D,30/D)
}
// City
if(i>500&&i%4<1)for(j=i%9*8;j--;)
c.fillStyle=j%5?"#FFA":"#999",
c.fillRect((i%12?-2e3:2e3)/D-j%5*200/D+O/D,300/D+y/D-(j-j%5)*60/D,200/D,200/D);
c.fillStyle="#999"
}
// Integrate car movement
B-=.3;F+=.5;
// Move forward and loop back to start if track ends
Z++;Z%=3e3
},20)