godrays and mountains
This commit is contained in:
parent
2613c391db
commit
f00990a3be
5 changed files with 1510 additions and 1346 deletions
2690
package-lock.json
generated
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>
|
||||
|
|
51
src/Background.vue
Normal file
51
src/Background.vue
Normal file
|
@ -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); }
|
||||
|
|
108
src/solar-quartet.js
Normal file
108
src/solar-quartet.js
Normal file
|
@ -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…
Reference in a new issue