fancy background is back!
This commit is contained in:
parent
ecb4c9b54b
commit
8f281fbdf9
8 changed files with 194 additions and 57 deletions
27
src/App.vue
27
src/App.vue
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import type { Block, Direction, Item, InventoryItem } from './types.d'
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ref, computed, watch, onMounted, useTemplateRef } from 'vue'
|
||||
import Help from './screens/help.vue'
|
||||
import Inventory from './screens/inventory.vue'
|
||||
import Background from './Background.vue'
|
||||
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
||||
import { getItem, getItemClass } from './level/items'
|
||||
|
@ -11,15 +12,15 @@ import createLevel from './level'
|
|||
import usePlayer from './util/usePlayer'
|
||||
import useTime from './util/useTime'
|
||||
import useInput from './util/useInput'
|
||||
import useLightMap from './util/useLightMap'
|
||||
import useLightMask from './util/useLightMask'
|
||||
|
||||
const { updateTime, time, timeOfDay, clock } = useTime()
|
||||
const { player, direction, dx, dy, pocket, unpocket } = usePlayer()
|
||||
const { inputX, inputY, running, paused, help, inventory } = useInput()
|
||||
const level = createLevel(STAGE_WIDTH + 2, STAGE_HEIGHT + 2)
|
||||
|
||||
const lightMapEl = ref<HTMLCanvasElement | undefined>(undefined)
|
||||
let updateLightMap: ReturnType<typeof useLightMap>
|
||||
const lightMaskEl = useTemplateRef<HTMLCanvasElement>('light-mask')
|
||||
let updateLightMap = (() => {}) as ReturnType<typeof useLightMask>
|
||||
|
||||
pocket(getItem('tool_shovel_wood'))
|
||||
pocket(getItem('tool_sword_wood'))
|
||||
|
@ -208,15 +209,14 @@ function selectTool(item: InventoryItem) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (lightMapEl.value) {
|
||||
const canvas = lightMapEl.value
|
||||
if (lightMaskEl.value) {
|
||||
const canvas = lightMaskEl.value
|
||||
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
||||
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
||||
const ctx = canvas.getContext('2d')!
|
||||
updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier)
|
||||
updateLightMap = useLightMask(ctx, floorX, floorY, tx, ty, time, lightBarrier)
|
||||
} else {
|
||||
console.warn('lightmap deactivated')
|
||||
updateLightMap = (() => {}) as ReturnType<typeof useLightMap>
|
||||
}
|
||||
lastTick = performance.now()
|
||||
move(lastTick)
|
||||
|
@ -225,6 +225,10 @@ onMounted(() => {
|
|||
|
||||
<template>
|
||||
<div id="field" :class="timeOfDay">
|
||||
<Background :time :x />
|
||||
<div id="parallax">
|
||||
</div>
|
||||
|
||||
<div id="blocks" :style="{transform: `translate(${tx}px, ${ty}px)`}">
|
||||
<template v-for="(row, y) in mapGrid">
|
||||
<div v-for="(block, x) in row"
|
||||
|
@ -255,11 +259,8 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="light-mask" ref="lightMapEl"
|
||||
:style="{
|
||||
transform: `translate(${tx}px, ${ty}px)`,
|
||||
mixBlendMode: paused && debug ? 'normal' : 'multiply',
|
||||
}"
|
||||
<canvas id="light-mask" ref="light-mask"
|
||||
:style="{ transform: `translate(${tx}px, ${ty}px)` }"
|
||||
/>
|
||||
<div id="beam" v-if="arriving"></div>
|
||||
<div id="level-indicator">
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import useBackground from './util/useBackground'
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
||||
import { calcSunAngle } from './util/useTime'
|
||||
|
||||
export interface Props {
|
||||
time: number
|
||||
|
@ -11,8 +12,7 @@ export interface Props {
|
|||
const props = defineProps<Props>()
|
||||
const canvas = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
const p = Math.PI / -10
|
||||
const sunY = computed(() => Math.sin(props.time * p))
|
||||
const sunY = computed(() => calcSunAngle(props.time))
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -25,7 +25,10 @@ onMounted(() => {
|
|||
~~(STAGE_HEIGHT * BLOCK_SIZE / 2.0),
|
||||
)
|
||||
|
||||
watch(props, () => drawBackground(props.x, sunY.value), { immediate: true })
|
||||
watch(props, () => {
|
||||
console.log('drawing background', sunY.value)
|
||||
drawBackground(props.x, sunY.value)
|
||||
}, { immediate: true })
|
||||
})
|
||||
|
||||
</script>
|
||||
|
|
|
@ -155,12 +155,14 @@
|
|||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
#light-mask {
|
||||
#parallax, #background, #light-mask {
|
||||
position: absolute;
|
||||
top: calc(var(--block-size) * -1);
|
||||
left: calc(var(--block-size) * -1);
|
||||
width: calc(100% + var(--block-size) * 2);
|
||||
height: calc(100% + var(--block-size) * 2);
|
||||
mix-blend-mode: multiply;
|
||||
pointer-events: none;
|
||||
}
|
||||
#light-mask {
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 72 KiB |
|
@ -18,7 +18,13 @@ function hsl(h: number, s: number, l: number): string {
|
|||
* @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) {
|
||||
function renderGodrays(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
cx: number,
|
||||
cy: number,
|
||||
sunY: number,
|
||||
r = 44,
|
||||
) {
|
||||
const w = ctx.canvas.width
|
||||
const h = ctx.canvas.height
|
||||
|
||||
|
@ -36,11 +42,14 @@ function renderGodrays(ctx: CanvasRenderingContext2D, cx: number, cy: number, su
|
|||
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.
|
||||
// pixels in radius 0 to 4.4 (44 * .1)
|
||||
emissionGradient.addColorStop(.1, hsl(30, 50, 3.1 * emissionStrength))
|
||||
emissionGradient.addColorStop(.2, hsl(12, 71, 1.4 * emissionStrength))
|
||||
|
||||
// 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).
|
||||
|
||||
// And set the fillstyle to black, we'll use it to paint our occlusion (mountains)
|
||||
ctx.fillStyle = '#000'
|
||||
|
||||
return emissionStrength
|
||||
|
@ -51,8 +60,12 @@ function renderGodrays(ctx: CanvasRenderingContext2D, cx: number, cy: number, su
|
|||
* 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)
|
||||
function calcMountainHeight(
|
||||
pos: number,
|
||||
roughness: number,
|
||||
freqs = [1721, 947, 547, 233, 73, 31, 7],
|
||||
) {
|
||||
return freqs.reduce((height, freq) => height * roughness - Math.cos(freq * pos), 0)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,7 +77,14 @@ function calcMountainHeight(pos: number, roughness: number, frequencies = [1721,
|
|||
* @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) {
|
||||
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
|
||||
|
@ -127,19 +147,27 @@ function renderSky(ctx: CanvasRenderingContext2D, sunY: number) {
|
|||
* @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) {
|
||||
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 ctx = canvasEl.getContext('2d')
|
||||
const grCtx = grCanvasEl.getContext('2d')
|
||||
if (grCtx === null) return () => {} // for real, how old is it?
|
||||
if (ctx === null || grCtx === null) {
|
||||
console.error('BACKGROUND CANVAS ERROR: Failed to set up canvas?!')
|
||||
return () => {} // for real, how old is it?
|
||||
}
|
||||
|
||||
grCanvasEl.width = grW
|
||||
grCanvasEl.height = grH
|
||||
|
@ -153,7 +181,10 @@ export default function useBackground (canvasEl: HTMLCanvasElement, w: number, h
|
|||
* @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)
|
||||
ctx.globalCompositeOperation = grCtx.globalCompositeOperation = 'source-over'
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
grCtx.clearRect(0, 0, grW, grH)
|
||||
|
||||
const emissionStrength = renderGodrays(grCtx, sunCenterX, sunCenterY, sunY)
|
||||
renderSky(ctx, sunY)
|
||||
renderMountains(ctx, grCtx, frame, sunY, mountainLayers, emissionStrength)
|
||||
|
|
|
@ -3,7 +3,7 @@ import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
|
|||
|
||||
type RefOrComputed<T> = Ref<T> | ComputedRef<T>
|
||||
|
||||
export default function useLightMap(
|
||||
export default function useLightMask(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: RefOrComputed<number>,
|
||||
y: RefOrComputed<number>,
|
||||
|
@ -43,16 +43,29 @@ export default function useLightMap(
|
|||
}
|
||||
|
||||
function drawPlayerLight(sizeMul:number) {
|
||||
const t = time.value
|
||||
|
||||
const playerLight = ctx.createRadialGradient(
|
||||
playerX - tx.value, playerY - ty.value, 0,
|
||||
playerX - tx.value, playerY - ty.value, playerLightSize * sizeMul
|
||||
)
|
||||
|
||||
// Add color stops: white in the center to transparent white
|
||||
playerLight.addColorStop(0.0, "#FFFF");
|
||||
playerLight.addColorStop(0.4, "#FFFF");
|
||||
playerLight.addColorStop(0.7, "#FFFA");
|
||||
playerLight.addColorStop(1, "#FFF1");
|
||||
// Add color stops for a light around the player
|
||||
// Night time, lets tone down the light a bit
|
||||
if (t > 900 || t < 100) {
|
||||
playerLight.addColorStop(0.7, "#AA7A");
|
||||
playerLight.addColorStop(1, "#AA70");
|
||||
|
||||
// Morning
|
||||
} else if (t < 150) {
|
||||
playerLight.addColorStop(0.7, "#CCAA");
|
||||
playerLight.addColorStop(1, "#CCA0");
|
||||
|
||||
// Day (neutral white)
|
||||
} else {
|
||||
playerLight.addColorStop(0.7, "#FFFA");
|
||||
playerLight.addColorStop(1, "#FFF0");
|
||||
}
|
||||
|
||||
// Set the fill style and draw a rectangle
|
||||
ctx.fillStyle = playerLight;
|
||||
|
@ -93,7 +106,7 @@ export default function useLightMap(
|
|||
|
||||
return function update() {
|
||||
// first, throw the world in complete darkness
|
||||
ctx.fillStyle = '#000'
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
|
||||
// second, find and bring light into the world
|
58
src/util/useTime.test.ts
Normal file
58
src/util/useTime.test.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { calcTimeOfDay, renderClock, calcSunAngle } from './useTime'
|
||||
|
||||
describe('useTime composable', () => {
|
||||
describe('clock', () => {
|
||||
it('renders 2:00 at tick 0', () => {
|
||||
expect(renderClock(0)).toEqual('2:00')
|
||||
})
|
||||
it('renders 14:00 at tick 500', () => {
|
||||
expect(renderClock(500)).toEqual('14:00')
|
||||
})
|
||||
it('does not break on meanless values', () => {
|
||||
expect(renderClock(9500)).toBeDefined()
|
||||
expect(renderClock(-500)).toBeDefined()
|
||||
})
|
||||
})
|
||||
describe('time of day', () => {
|
||||
it('sets the correct time of day', () => {
|
||||
expect(calcTimeOfDay(0)).toEqual('night')
|
||||
expect(calcTimeOfDay(900)).toEqual('night')
|
||||
expect(calcTimeOfDay(100)).toEqual('morning0')
|
||||
expect(calcTimeOfDay(145)).toEqual('morning1')
|
||||
expect(calcTimeOfDay(200)).toEqual('morning2')
|
||||
expect(calcTimeOfDay(700)).toEqual('evening0')
|
||||
expect(calcTimeOfDay(800)).toEqual('evening1')
|
||||
expect(calcTimeOfDay(850)).toEqual('evening2')
|
||||
expect(calcTimeOfDay(300)).toEqual('day')
|
||||
expect(calcTimeOfDay(400)).toEqual('day')
|
||||
expect(calcTimeOfDay(555)).toEqual('day')
|
||||
})
|
||||
})
|
||||
describe('sun angle', () => {
|
||||
it('returns -10 degrees over night', () => {
|
||||
expect(calcSunAngle(900)).toEqual(-10)
|
||||
expect(calcSunAngle(945)).toEqual(-10)
|
||||
expect(calcSunAngle(0)).toEqual(-10)
|
||||
expect(calcSunAngle(57)).toEqual(-10)
|
||||
})
|
||||
it('returns -90 degrees over day', () => {
|
||||
expect(calcSunAngle(250)).toEqual(-90)
|
||||
expect(calcSunAngle(300)).toEqual(-90)
|
||||
expect(calcSunAngle(557)).toEqual(-90)
|
||||
expect(Math.round(calcSunAngle(699))).toEqual(-90)
|
||||
})
|
||||
it('raises in the morning', () => {
|
||||
expect(calcSunAngle(100)).toEqual(-20)
|
||||
expect(calcSunAngle(150)).toEqual(-45)
|
||||
expect(calcSunAngle(200)).toEqual(-70)
|
||||
expect(calcSunAngle(240)).toEqual(-90)
|
||||
})
|
||||
it('sinks in the evening', () => {
|
||||
expect(calcSunAngle(700)).toEqual(-90)
|
||||
expect(calcSunAngle(750)).toEqual(-70)
|
||||
expect(calcSunAngle(800)).toEqual(-50)
|
||||
expect(Math.round(calcSunAngle(899))).toEqual(-10)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,30 +1,59 @@
|
|||
import { ref, computed } from 'vue'
|
||||
|
||||
type TimeOfDay =
|
||||
| 'day'
|
||||
| 'night'
|
||||
| 'morning0'
|
||||
| 'morning1'
|
||||
| 'morning2'
|
||||
| 'evening0'
|
||||
| 'evening1'
|
||||
| 'evening2'
|
||||
|
||||
export function calcTimeOfDay(tick: number): TimeOfDay {
|
||||
if (tick >= 900 || tick < 80) return 'night'
|
||||
if (tick >= 80 && tick < 120) return 'morning0'
|
||||
if (tick >= 120 && tick < 150) return 'morning1'
|
||||
if (tick >= 150 && tick < 240) return 'morning2'
|
||||
if (tick >= 700 && tick < 800) return 'evening0'
|
||||
if (tick >= 800 && tick < 850) return 'evening1'
|
||||
if (tick >= 850 && tick < 900) return 'evening2'
|
||||
return 'day'
|
||||
}
|
||||
|
||||
export function renderClock(tick: number): string {
|
||||
const t = tick * 86.4 // 1000 ticks to 86400 seconds (per day)
|
||||
const h = ~~(t / 3600.0)
|
||||
const m = ~~((t / 3600.0 - h) * 60.0)
|
||||
return `${(h + 2) % 24}:${m < 10 ? '0' : ''}${m}`
|
||||
}
|
||||
|
||||
function calcProgress(tick: number, start: number, end: number, span: number): number {
|
||||
return (tick - start) / (end - start) * span
|
||||
}
|
||||
|
||||
// calculates the suns angle from -90 (top, at noon), to 0 (at midnight)
|
||||
export function calcSunAngle(tick: number): number {
|
||||
// night time: -10 degrees fixed
|
||||
if (tick >= 900 || tick < 80) return -10
|
||||
// sunrise: gradually move from -10 to -90, by mapping 80 -> 240 to -10 -> -90
|
||||
if (tick >= 80 && tick < 240) return -10 - calcProgress(tick, 80, 240, 80)
|
||||
// sundawn: gradually move from -90 to -10, by mapping 700 -> 900 to -90 -> -10
|
||||
if (tick >= 700 && tick < 900) return -90 + calcProgress(tick, 700, 900, 80)
|
||||
// day time: -90 degrees fixed
|
||||
return -90
|
||||
}
|
||||
|
||||
export default function useTime() {
|
||||
// the day is split in 1000 parts, so we start in the morning
|
||||
const time = ref(230)
|
||||
const time = ref(250)
|
||||
|
||||
function updateTime() {
|
||||
time.value = (time.value + 0.1) % 1000
|
||||
}
|
||||
|
||||
const timeOfDay = computed(() => {
|
||||
if (time.value >= 900 || time.value < 80) return 'night'
|
||||
if (time.value >= 80 && time.value < 120) return 'morning0'
|
||||
if (time.value >= 120 && time.value < 150) return 'morning1'
|
||||
if (time.value >= 150 && time.value < 240) return 'morning2'
|
||||
if (time.value >= 700 && time.value < 800) return 'evening0'
|
||||
if (time.value >= 800 && time.value < 850) return 'evening1'
|
||||
if (time.value >= 850 && time.value < 900) return 'evening2'
|
||||
return 'day'
|
||||
})
|
||||
|
||||
const clock = computed(() => {
|
||||
const t = time.value * 86.4 // 1000 ticks to 86400 seconds (per day)
|
||||
const h = ~~(t / 3600.0)
|
||||
const m = ~~((t / 3600.0 - h) * 60.0)
|
||||
return `${(h + 2) % 24}:${m < 10 ? '0' : ''}${m}`
|
||||
})
|
||||
const timeOfDay = computed(() => calcTimeOfDay(time.value))
|
||||
const clock = computed(() => renderClock(time.value))
|
||||
|
||||
return { time, updateTime, timeOfDay, clock }
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue