godrays and mountains

main
Norman Köhring 6 years ago
parent 2613c391db
commit f00990a3be

2690
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -21,9 +21,11 @@ export default {
html,body,#app {
display: block;
width: 100vw;
height: 100vh;
background: black;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>

@ -0,0 +1,51 @@
<template>
<canvas ref="canvas" id="background"></canvas>
</template>
<script>
import solarQuartet from './solar-quartet'
export default {
name: 'background',
props: {
x: Number
},
data () {
return {
sunY: -50.0,
redraw: null
}
},
mounted () {
const canvasSize = 512
const godraysSize = 128
const canvas = this.$refs.canvas
const godraysCanvas = document.createElement('canvas')
canvas.width = canvasSize
canvas.height = canvasSize
godraysCanvas.width = godraysSize
godraysCanvas.height = godraysSize
this.redraw = solarQuartet.bind(
null,
canvas, canvas.getContext('2d'), canvasSize, canvasSize,
godraysCanvas, godraysCanvas.getContext('2d'), godraysSize, godraysSize,
)
this.redraw(this.x, this.sunY)
},
watch: {
x (x) {
this.redraw(x, this.sunY)
}
}
}
</script>
<style>
#background {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
background: black;
}
</style>

@ -7,6 +7,7 @@
@keydown.left="goLeft($event)"
@keydown.space="jump($event)"
/>
<mountain-background :x="x + 65536" />
<div id="wrap">
<template v-for="(row, y) in rows">
<div v-for="(block, x) in row" class="block" :class="[block.type]" :data-x="x" :data-y="y" />
@ -19,6 +20,7 @@
<script>
import Level from './level'
import MountainBackground from './Background'
const WIDTH = 32 + 2
const HEIGHT = 32 + 2
@ -28,6 +30,7 @@ const level = new Level(WIDTH, HEIGHT)
export default {
name: 'field',
components: { MountainBackground },
data () {
return {
x: 0,
@ -145,6 +148,7 @@ export default {
height: 1024px;
margin: auto;
overflow: hidden;
background-color: #56F;
}
#field > input {
position: absolute;
@ -187,7 +191,6 @@ export default {
display: flex;
flex-flow: row wrap;
}
.block { background-color: #56F; }
.block.grass { background-image: url(./assets/grass01.png); }
.block.tree_top_left { background-image: url(./assets/tree_top_left.png); }

@ -0,0 +1,108 @@
/* 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
// it should be an offset from the middle somewhere between -24 and 24
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.
emissionGradient.addColorStop(.1, '#0C0804') // pixels in radius 0 to 4.4 (44 * .1).
emissionGradient.addColorStop(.2, '#060201') // everything past radius 8.8.
// 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
// TODO: can this be used for day and night, too?
const skyGradient = ctx.createLinearGradient(0, 0, 0, height)
skyGradient.addColorStop(0, '#2a3e55') // Blueish at the top.
skyGradient.addColorStop(.7, '#8d4835') // Reddish at the bottom.
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 brown with variable lightness
// (darker at the front)
ctx.fillStyle = `hsl(7, 23%, ${23-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);
}
Loading…
Cancel
Save