debugging highlights
This commit is contained in:
parent
1b3360862f
commit
3c8b289310
4 changed files with 338 additions and 47 deletions
25
src/App.vue
25
src/App.vue
|
@ -138,12 +138,14 @@ const move = (thisTick: number): void => {
|
||||||
|
|
||||||
// do nothing when paused
|
// do nothing when paused
|
||||||
if (paused.value) {
|
if (paused.value) {
|
||||||
lastTick = thisTick // reset tick, to avoid huge tickDelta
|
// reset tick, to avoid tickDelta and resulting character teleport
|
||||||
|
lastTick = thisTick
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const tickDelta = thisTick - lastTick
|
const tickDelta = thisTick - lastTick
|
||||||
lastTimeUpdate += tickDelta
|
lastTimeUpdate += tickDelta
|
||||||
|
|
||||||
// update in-game time every 60ms by 0.1
|
// update in-game time every 60ms by 0.1
|
||||||
// then a day needs 10000 updates, and it takes about 10 minutes
|
// then a day needs 10000 updates, and it takes about 10 minutes
|
||||||
if (lastTimeUpdate > 60) {
|
if (lastTimeUpdate > 60) {
|
||||||
|
@ -189,23 +191,6 @@ const move = (thisTick: number): void => {
|
||||||
lastTick = thisTick
|
lastTick = thisTick
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcBrightness(level: number, row: number) {
|
|
||||||
const barrier = lightBarrier.value[row]
|
|
||||||
const barrierLeft = lightBarrier.value[row - 1]
|
|
||||||
const barrierRight = lightBarrier.value[row + 1]
|
|
||||||
|
|
||||||
let delta = barrier - level - (floorY.value - 3)
|
|
||||||
const deltaL = Math.min(3, barrierLeft - level - (floorY.value - 3))
|
|
||||||
const deltaR = Math.min(3, barrierRight - level - (floorY.value - 3))
|
|
||||||
|
|
||||||
if (delta > 3) delta = 3
|
|
||||||
else if (delta < 0) delta = 0
|
|
||||||
|
|
||||||
if (deltaR > delta || deltaL > delta) delta = Math.max(deltaL, deltaR) - 1
|
|
||||||
|
|
||||||
return `sun-${delta}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTool(item: InventoryItem) {
|
function selectTool(item: InventoryItem) {
|
||||||
inventorySelection.value = item
|
inventorySelection.value = item
|
||||||
}
|
}
|
||||||
|
@ -228,7 +213,9 @@ onMounted(() => {
|
||||||
<div id="blocks" :style="{transform: `translate(${tx}px, ${ty}px)`}">
|
<div id="blocks" :style="{transform: `translate(${tx}px, ${ty}px)`}">
|
||||||
<template v-for="(row, y) in rows">
|
<template v-for="(row, y) in rows">
|
||||||
<div v-for="(block, x) in row"
|
<div v-for="(block, x) in row"
|
||||||
:class="['block', block.type]"
|
:class="['block', block.type, {
|
||||||
|
highlight: x === player.x && y == player.y
|
||||||
|
}]"
|
||||||
@click="interactWith(x, y, block)"
|
@click="interactWith(x, y, block)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
35
src/Background.vue
Normal file
35
src/Background.vue
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import useBackground from './util/useBackground'
|
||||||
|
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
time: number
|
||||||
|
x: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const canvas = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
|
const p = Math.PI / -10
|
||||||
|
const sunY = computed(() => Math.sin(props.time * p))
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const canvasEl = canvas.value
|
||||||
|
if (canvasEl === null) return
|
||||||
|
|
||||||
|
const drawBackground = useBackground(
|
||||||
|
canvasEl,
|
||||||
|
~~(STAGE_WIDTH * BLOCK_SIZE / 2.0),
|
||||||
|
~~(STAGE_HEIGHT * BLOCK_SIZE / 2.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(props, () => drawBackground(props.x, sunY.value), { immediate: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas ref="canvas" id="background"></canvas>
|
||||||
|
</template>
|
|
@ -1,6 +1,8 @@
|
||||||
.block, #player {
|
.block,
|
||||||
|
#player {
|
||||||
transition: filter .5s linear;
|
transition: filter .5s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: var(--block-size);
|
width: var(--block-size);
|
||||||
|
@ -10,6 +12,7 @@
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block::after {
|
.block::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -23,40 +26,120 @@
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block.damage-0::after { background-position-x: 0px; }
|
.block.damage-0::after {
|
||||||
.block.damage-1::after { background-position-x: calc(var(--block-size) * -1); }
|
background-position-x: 0px;
|
||||||
.block.damage-2::after { background-position-x: calc(var(--block-size) * -2); }
|
}
|
||||||
.block.damage-3::after { background-position-x: calc(var(--block-size) * -3); }
|
|
||||||
.block.damage-4::after { background-position-x: calc(var(--block-size) * -4); }
|
|
||||||
.block.damage-5::after { background-position-x: calc(var(--block-size) * -5); }
|
|
||||||
.block.damage-6::after { background-position-x: calc(var(--block-size) * -6); }
|
|
||||||
|
|
||||||
.block.grass { background-image: url(/Tiles/dirt_grass.png); }
|
.block.damage-1::after {
|
||||||
|
background-position-x: calc(var(--block-size) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
.block.treeCrown, .block.treeLeaves { background-image: url(/Tiles/leaves_transparent.png); }
|
.block.damage-2::after {
|
||||||
.block.treeTrunk { background-image: url(/Tiles/trunk_mid.png); }
|
background-position-x: calc(var(--block-size) * -2);
|
||||||
.block.treeRoot { background-image: url(/Tiles/trunk_bottom.png); }
|
}
|
||||||
|
|
||||||
.block.soil { background-image: url(/Tiles/dirt.png); }
|
.block.damage-3::after {
|
||||||
.block.soilGravel { background-image: url(/Tiles/gravel_dirt.png); }
|
background-position-x: calc(var(--block-size) * -3);
|
||||||
.block.stoneGravel { background-image: url(/Tiles/gravel_stone.png); }
|
}
|
||||||
.block.stone { background-image: url(/Tiles/stone.png); }
|
|
||||||
.block.bedrock { background-image: url(/Tiles/greystone.png); }
|
|
||||||
.block.cave { background-color: #000; }
|
|
||||||
|
|
||||||
.block.brickWall { background-image: url(/Tiles/brick_grey.png); }
|
.block.damage-4::after {
|
||||||
|
background-position-x: calc(var(--block-size) * -4);
|
||||||
|
}
|
||||||
|
|
||||||
#field .block:hover { outline: 1px solid white; z-index: 10; }
|
.block.damage-5::after {
|
||||||
|
background-position-x: calc(var(--block-size) * -5);
|
||||||
|
}
|
||||||
|
|
||||||
.morning0 .block, .morning0 #player {filter: saturate(50%); }
|
.block.damage-6::after {
|
||||||
.morning1 .block, .morning1 #player { filter: saturate(100%); }
|
background-position-x: calc(var(--block-size) * -6);
|
||||||
.morning2 .block, .morning2 #player { filter: saturate(120%); }
|
}
|
||||||
|
|
||||||
.evening0 .block, .evening0 #player { filter: saturate(90%); }
|
.block.grass {
|
||||||
.evening1 .block, .evening1 #player { filter: saturate(70%); }
|
background-image: url(/Tiles/dirt_grass.png);
|
||||||
.evening2 .block, .evening2 #player { filter: saturate(50%); }
|
}
|
||||||
|
|
||||||
.night .block, .night #player { filter: saturate(30%); }
|
.block.treeCrown,
|
||||||
|
.block.treeLeaves {
|
||||||
|
background-image: url(/Tiles/leaves_transparent.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.treeTrunk {
|
||||||
|
background-image: url(/Tiles/trunk_mid.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.treeRoot {
|
||||||
|
background-image: url(/Tiles/trunk_bottom.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.soil {
|
||||||
|
background-image: url(/Tiles/dirt.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.soilGravel {
|
||||||
|
background-image: url(/Tiles/gravel_dirt.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.stoneGravel {
|
||||||
|
background-image: url(/Tiles/gravel_stone.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.stone {
|
||||||
|
background-image: url(/Tiles/stone.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.bedrock {
|
||||||
|
background-image: url(/Tiles/greystone.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.cave {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.brickWall {
|
||||||
|
background-image: url(/Tiles/brick_grey.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
#field .block:hover,
|
||||||
|
#field .block.block.highlight {
|
||||||
|
filter: brightness(1.2) grayscale(1.0);
|
||||||
|
outline: 1px solid white;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.morning0 .block,
|
||||||
|
.morning0 #player {
|
||||||
|
filter: saturate(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.morning1 .block,
|
||||||
|
.morning1 #player {
|
||||||
|
filter: saturate(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.morning2 .block,
|
||||||
|
.morning2 #player {
|
||||||
|
filter: saturate(120%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.evening0 .block,
|
||||||
|
.evening0 #player {
|
||||||
|
filter: saturate(90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.evening1 .block,
|
||||||
|
.evening1 #player {
|
||||||
|
filter: saturate(70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.evening2 .block,
|
||||||
|
.evening2 #player {
|
||||||
|
filter: saturate(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.night .block,
|
||||||
|
.night #player {
|
||||||
|
filter: saturate(30%);
|
||||||
|
}
|
||||||
|
|
||||||
#blocks {
|
#blocks {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
186
src/util/useBackground.ts
Normal file
186
src/util/useBackground.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hsl - creates hsl color string from h, s and l values
|
||||||
|
* @param h: number - hue
|
||||||
|
* @param s: number - saturation
|
||||||
|
* @param l: number - lightness
|
||||||
|
*/
|
||||||
|
function hsl(h: number, s: number, l: number): string {
|
||||||
|
return `hsl(${h}, ${s}%, ${l}%)`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* render godrays
|
||||||
|
* @param ctx: CanvasRenderingContext2D - where to draw
|
||||||
|
* @param cx: number - x-axis center of the "sun"
|
||||||
|
* @param cy: number - y-axis center of the "sun"
|
||||||
|
* @param sunY: number - the position (height) of the "sun" in the sky
|
||||||
|
* @param r: number [44] - the radius of the "sun"
|
||||||
|
* @returns emissionStrength: number - emission intensity blends over the mountains
|
||||||
|
*/
|
||||||
|
function renderGodrays(ctx: CanvasRenderingContext2D, cx: number, cy: number, sunY: number, r = 44) {
|
||||||
|
const w = ctx.canvas.width
|
||||||
|
const h = ctx.canvas.height
|
||||||
|
|
||||||
|
const emissionGradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, r)
|
||||||
|
ctx.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.
|
||||||
|
ctx.fillRect(0, 0, w, h)
|
||||||
|
// And set the fillstyle to black, we'll use it to paint our occlusion (mountains).
|
||||||
|
ctx.fillStyle = '#000'
|
||||||
|
|
||||||
|
return emissionStrength
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* calculate mountain height
|
||||||
|
* Mountains are made by summing up sine waves with varying frequencies and amplitudes
|
||||||
|
* The frequencies are prime, to avoid extra repetitions
|
||||||
|
*/
|
||||||
|
function calcMountainHeight(pos: number, roughness: number, frequencies = [1721, 947, 547, 233, 73, 31, 7]) {
|
||||||
|
return frequencies.reduce((height, freq) => height * roughness - Math.cos(freq * pos), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* render mountains
|
||||||
|
* @param ctx: CanvasRenderingContext2D - where to draw
|
||||||
|
* @param grCtx: CanvasRenderingContext2D - for drawing mountain shadows on the godray canvas
|
||||||
|
* @param frame: number - current frame (position on the x axis)
|
||||||
|
* @param sunY: number - position (height) of the "sun" in the sky
|
||||||
|
* @param layers: number - amount of mountain layers for parallax effect
|
||||||
|
* @param emissionStrength: number - intensity of the godrays
|
||||||
|
*/
|
||||||
|
function renderMountains(ctx: CanvasRenderingContext2D, grCtx: CanvasRenderingContext2D, frame: number, sunY: number, layers: number, emissionStrength: number) {
|
||||||
|
const w = ctx.canvas.width
|
||||||
|
const h = ctx.canvas.height
|
||||||
|
const grDiv = w / grCtx.canvas.width
|
||||||
|
|
||||||
|
for (let i = 0; i < layers; i++) {
|
||||||
|
// Set the main canvas fillStyle to a shade of green-brown with variable lightness
|
||||||
|
// depending on sunY and depth
|
||||||
|
ctx.fillStyle = sunY > -60
|
||||||
|
? hsl(5, 23, 33*emissionStrength - i*6*emissionStrength)
|
||||||
|
: hsl(220 - i*40, 23, 33-i*6)
|
||||||
|
|
||||||
|
for (let x = w; 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...
|
||||||
|
const pos = (frame * 2 * i**2) / 1000 + x / 2000
|
||||||
|
// Make further mountains more jagged, adds a bit of realism and also makes the godrays
|
||||||
|
// look nicer.
|
||||||
|
const roughness = i / 19 - .5
|
||||||
|
// 128 is the middle, i * 25 moves the nearer mountains lower on the screen.
|
||||||
|
let y = 128 + i * 25 + calcMountainHeight(pos, roughness) * 45
|
||||||
|
// Paint a 1px-wide rectangle from the mountain's top to below the bottom of the canvas.
|
||||||
|
ctx.fillRect(x, y, 1, h)
|
||||||
|
// 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/grDiv, y/grDiv+1, 1, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* render sky
|
||||||
|
* @param ctx: CanvasRenderingContext2D - where to draw
|
||||||
|
* @param sunY: number - the position (height) of the "sun" in the sky
|
||||||
|
*/
|
||||||
|
function renderSky(ctx: CanvasRenderingContext2D, sunY: number) {
|
||||||
|
const w = ctx.canvas.width
|
||||||
|
const h = ctx.canvas.height
|
||||||
|
|
||||||
|
const skyGradient = ctx.createLinearGradient(0, 0, 0, h)
|
||||||
|
const skyHue = 360 + sunY // hue from blue to red, depending on the suns position
|
||||||
|
const skySaturation = 100 + sunY // less saturation at day so that the red fades away
|
||||||
|
const skyLightness = Math.min(sunY * -1 - 10, 55) // darker at night
|
||||||
|
|
||||||
|
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, w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useBackground
|
||||||
|
* @param canvasEl: HTMLCanvasElement - the canvas to draw the background on.
|
||||||
|
* @param w: number - the (pixel) width of the canvas. The element itself can have a different width.
|
||||||
|
* @param h: number - the (pixel) height of the canvas. The element itself can have a different height.
|
||||||
|
* @param rayQuality: number [8] - The quality of the sunrays (divides the resolution, so higher value means lower quality)
|
||||||
|
* @param mountainLayers: number [4] - How many layers of mountains are used for parallax effect?
|
||||||
|
*/
|
||||||
|
export default function useBackground (canvasEl: HTMLCanvasElement, w: number, h: number, rayQuality = 8, mountainLayers = 4) {
|
||||||
|
canvasEl.width = w
|
||||||
|
canvasEl.height = h
|
||||||
|
|
||||||
|
const grW = w / rayQuality
|
||||||
|
const grH = h / rayQuality
|
||||||
|
|
||||||
|
const ctx = canvasEl.getContext('2d')
|
||||||
|
if (ctx === null) return // like, how old is your browser?
|
||||||
|
|
||||||
|
const grCanvasEl = document.createElement('canvas')
|
||||||
|
const grCtx = grCanvasEl.getContext('2d')
|
||||||
|
if (grCtx === null) return // like, how old is your browser?
|
||||||
|
|
||||||
|
grCanvasEl.width = grW
|
||||||
|
grCanvasEl.height = grH
|
||||||
|
|
||||||
|
const sunCenterX = grCanvasEl.width / 2
|
||||||
|
const sunCenterY = grCanvasEl.height / 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* draw one frame of the background
|
||||||
|
* @param frame: number - the position on the x axis, to calculate the paralax background
|
||||||
|
* @param sunY: number - the position (height) of the sun in the sky
|
||||||
|
*/
|
||||||
|
return function drawFrame (frame: number, sunY: number) {
|
||||||
|
console.log('drawing frame', frame, sunY)
|
||||||
|
const emissionStrength = renderGodrays(grCtx, sunCenterX, sunCenterY, sunY)
|
||||||
|
renderSky(ctx, sunY)
|
||||||
|
renderMountains(ctx, grCtx, frame, sunY, mountainLayers, emissionStrength)
|
||||||
|
|
||||||
|
// 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 m**f** 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(
|
||||||
|
grCanvasEl,
|
||||||
|
(grW - grW * scaleFactor) / 2,
|
||||||
|
(grH - grH * scaleFactor) / 2 - sunY * scaleFactor + sunY,
|
||||||
|
grW * scaleFactor,
|
||||||
|
grH * scaleFactor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw godrays to output canvas (whose globalCompositeOperation is already set to 'lighter').
|
||||||
|
ctx.drawImage(grCanvasEl, 0, 0, w, h);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue