You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

131 lines
6.0 KiB
JavaScript

/* Adapted from the original "Solar Quartet" by y0natan
* https://codepen.io/y0natan/pen/MVvxBM
* https://js1k.com/2018-coins/demo/3075
*/
// sunY sets the height of the sun and with this the time of the day
// where 0 is lowest (night) and -100 is highest (day), other values are possible
// but don't make much sense / difference
export default function drawFrame (canvas, ctx, width, height, grCanvas, grCtx, grWidth, grHeight, frame, sunY) {
// reset canvas state
canvas.width = width
canvas.height = height
grCanvas.width = grWidth
grCanvas.height = grHeight
const sunCenterX = grWidth / 2
const sunCenterY = grHeight / 2 + sunY
// Set the godrays' context fillstyle to a newly created gradient
// which we also run through our abbreviator.
let emissionGradient = grCtx.createRadialGradient(
sunCenterX, sunCenterY, // The sun's center.
0, // Start radius.
sunCenterX, sunCenterY, // The sun's center.
44 // End radius.
)
grCtx.fillStyle = emissionGradient
// Now we addColorStops. This needs to be a dark gradient because our
// godrays effect will basically overlay it on top of itself many many times,
// so anything lighter will result in lots of white.
// If you're not space-bound you can add another stop or two, maybe fade out to black,
// but this actually looks good enough.
// a black "gradient" means no emission, so we fade to black as transition to night or day
let emissionStrength = 1.0
if (sunY > -30) emissionStrength -= Math.max((30 + sunY) / 5, 0.0)
else if (sunY < -60) emissionStrength += Math.min(1 + (60 + sunY) / 5, 0.0)
emissionGradient.addColorStop(.1, `hsl(30,50%,${3.1 * emissionStrength}%)`) // pixels in radius 0 to 4.4 (44 * .1).
emissionGradient.addColorStop(.2, `hsl(12,71%,${1.4 * emissionStrength}%)`) // pixels in radius 0 to 4.4 (44 * .1).
// Now paint the gradient all over our godrays canvas.
grCtx.fillRect(0, 0, grWidth, grHeight)
// And set the fillstyle to black, we'll use it to paint our occlusion (mountains).
grCtx.fillStyle = '#000'
// Paint the sky
const skyGradient = ctx.createLinearGradient(0, 0, 0, height)
// hue from blue to red depending on the suns position
const skyHue = 360 + sunY
// lesser saturation at day so that the red fades away
const skySaturation = 100 + sunY
// darker at night
const skyLightness = Math.min(sunY * -1 - 10, 55)
const skyHSLTop = `hsl(220, 70%, ${skyLightness}%)`
const skyHSLBottom = `hsl(${skyHue}, ${skySaturation}%, ${skyLightness}%)`
skyGradient.addColorStop(0, skyHSLTop)
skyGradient.addColorStop(.7, skyHSLBottom)
ctx.fillStyle = skyGradient
ctx.fillRect(0, 0, width, height)
// Our mountains will be made by summing up sine waves of varying frequencies and amplitudes.
function mountainHeight(position, roughness) {
// Our frequencies (prime numbers to avoid extra repetitions).
// TODO: play with the numbers
let frequencies = [1721, 947, 547, 233, 73, 31, 7]
// Add them up.
return frequencies.reduce((height, freq) => height * roughness - Math.cos(freq * position), 0)
}
// Draw 4 layers of mountains.
for(let i = 0; i < 4; i++) {
// Set the main canvas fillStyle to a shade of green-brown with variable lightness
// depending on sunY and depth
if (sunY > -60) {
ctx.fillStyle = `hsl(5, 23%, ${33*emissionStrength - i*6*emissionStrength}%)`
} else {
ctx.fillStyle = `hsl(${220 - i*40}, 23%, ${33-i*6}%)`
}
// For each column in our canvas...
for(let x = width; x--;) {
// Ok, I don't really remember the details here, basically the (frame+frame*i*i) makes the
// near mountains move faster than the far ones. We divide by large numbers because our
// mountains repeat at position 1/7*Math.PI*2 or something like that...
let mountainPosition = (frame * 2 * i**2) / 1000 + x / 2000;
// Make further mountains more jagged, adds a bit of realism and also makes the godrays
// look nicer.
let mountainRoughness = i / 19 - .5;
// 128 is the middle, i * 25 moves the nearer mountains lower on the screen.
let y = 128 + i * 25 + mountainHeight(mountainPosition, mountainRoughness) * 45;
// Paint a 1px-wide rectangle from the mountain's top to below the bottom of the canvas.
ctx.fillRect(x, y, 1, 999); // 999 can be any large number...
// Paint the same thing in black on the godrays emission canvas, which is 1/4 the size,
// and move it one pixel down (otherwise there can be a tiny underlit space between the
// mountains and the sky).
grCtx.fillRect(x/4, y/4+1, 1, 999);
}
}
// The godrays are generated by adding up RGB values, gCt is the bane of all js golfers -
// globalCompositeOperation. Set it to 'lighter' on both canvases.
ctx.globalCompositeOperation = grCtx.globalCompositeOperation = 'lighter';
// NOW - let's light this motherfucker up! We'll make several passes over our emission canvas,
// each time adding an enlarged copy of it to itself so at the first pass we get 2 copies, then 4,
// then 8, then 16 etc... We square our scale factor at each iteration.
for (let scaleFactor = 1.07; scaleFactor < 5; scaleFactor *= scaleFactor) {
// The x, y, width and height arguments for drawImage keep the light source in the same
// spot on the enlarged copy. It basically boils down to multiplying a 2D matrix by itself.
// There's probably a better way to do this, but I couldn't figure it out.
// For reference, here's an AS3 version (where BitmapData:draw takes a matrix argument):
// https://github.com/yonatan/volumetrics/blob/d3849027213e9499742cc4dfd2838c6032f4d9d3/src/org/zozuar/volumetrics/EffectContainer.as#L208-L209
grCtx.drawImage(
grCanvas,
(grWidth - grWidth * scaleFactor) / 2,
(grHeight - grHeight * scaleFactor) / 2 - sunY * scaleFactor + sunY,
grWidth * scaleFactor,
grHeight * scaleFactor
)
}
// Draw godrays to output canvas (whose globalCompositeOperation is already set to 'lighter').
ctx.drawImage(grCanvas, 0, 0, width, height);
}