Compare commits

..

No commits in common. "8f281fbdf9c17af948280397e19c10e60b29d2e8" and "7098bffd2550cfbabdd0e8c18b465c517d4e4736" have entirely different histories.

10 changed files with 58 additions and 196 deletions

View file

@ -1,9 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Block, Direction, Item, InventoryItem } from './types.d' import type { Block, Direction, Item, InventoryItem } from './types.d'
import { ref, computed, watch, onMounted, useTemplateRef } from 'vue' import { ref, computed, watch, onMounted } from 'vue'
import Help from './screens/help.vue' import Help from './screens/help.vue'
import Inventory from './screens/inventory.vue' import Inventory from './screens/inventory.vue'
import Background from './Background.vue'
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def' import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
import { getItem, getItemClass } from './level/items' import { getItem, getItemClass } from './level/items'
@ -12,15 +11,15 @@ import createLevel from './level'
import usePlayer from './util/usePlayer' import usePlayer from './util/usePlayer'
import useTime from './util/useTime' import useTime from './util/useTime'
import useInput from './util/useInput' import useInput from './util/useInput'
import useLightMask from './util/useLightMask' import useLightMap from './util/useLightMap'
const { updateTime, time, timeOfDay, clock } = useTime() const { updateTime, time, timeOfDay, clock } = useTime()
const { player, direction, dx, dy, pocket, unpocket } = usePlayer() const { player, direction, dx, dy, pocket, unpocket } = usePlayer()
const { inputX, inputY, running, paused, help, inventory } = useInput() const { inputX, inputY, running, paused, help, inventory } = useInput()
const level = createLevel(STAGE_WIDTH + 2, STAGE_HEIGHT + 2) const level = createLevel(STAGE_WIDTH + 2, STAGE_HEIGHT + 2)
const lightMaskEl = useTemplateRef<HTMLCanvasElement>('light-mask') const lightMapEl = ref<HTMLCanvasElement | undefined>(undefined)
let updateLightMap = (() => {}) as ReturnType<typeof useLightMask> let updateLightMap: ReturnType<typeof useLightMap>
pocket(getItem('tool_shovel_wood')) pocket(getItem('tool_shovel_wood'))
pocket(getItem('tool_sword_wood')) pocket(getItem('tool_sword_wood'))
@ -209,14 +208,15 @@ function selectTool(item: InventoryItem) {
} }
onMounted(() => { onMounted(() => {
if (lightMaskEl.value) { if (lightMapEl.value) {
const canvas = lightMaskEl.value const canvas = lightMapEl.value
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
updateLightMap = useLightMask(ctx, floorX, floorY, tx, ty, time, lightBarrier) updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier)
} else { } else {
console.warn('lightmap deactivated') console.warn('lightmap deactivated')
updateLightMap = (() => {}) as ReturnType<typeof useLightMap>
} }
lastTick = performance.now() lastTick = performance.now()
move(lastTick) move(lastTick)
@ -225,10 +225,6 @@ onMounted(() => {
<template> <template>
<div id="field" :class="timeOfDay"> <div id="field" :class="timeOfDay">
<Background :time :x />
<div id="parallax">
</div>
<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 mapGrid"> <template v-for="(row, y) in mapGrid">
<div v-for="(block, x) in row" <div v-for="(block, x) in row"
@ -259,8 +255,11 @@ onMounted(() => {
</div> </div>
</div> </div>
<canvas id="light-mask" ref="light-mask" <canvas id="light-mask" ref="lightMapEl"
:style="{ transform: `translate(${tx}px, ${ty}px)` }" :style="{
transform: `translate(${tx}px, ${ty}px)`,
mixBlendMode: paused && debug ? 'normal' : 'multiply',
}"
/> />
<div id="beam" v-if="arriving"></div> <div id="beam" v-if="arriving"></div>
<div id="level-indicator"> <div id="level-indicator">

View file

@ -2,7 +2,6 @@
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import useBackground from './util/useBackground' import useBackground from './util/useBackground'
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def' import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
import { calcSunAngle } from './util/useTime'
export interface Props { export interface Props {
time: number time: number
@ -12,7 +11,8 @@ export interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const canvas = ref<HTMLCanvasElement | null>(null) const canvas = ref<HTMLCanvasElement | null>(null)
const sunY = computed(() => calcSunAngle(props.time)) const p = Math.PI / -10
const sunY = computed(() => Math.sin(props.time * p))
onMounted(() => { onMounted(() => {
@ -25,10 +25,7 @@ onMounted(() => {
~~(STAGE_HEIGHT * BLOCK_SIZE / 2.0), ~~(STAGE_HEIGHT * BLOCK_SIZE / 2.0),
) )
watch(props, () => { watch(props, () => drawBackground(props.x, sunY.value), { immediate: true })
console.log('drawing background', sunY.value)
drawBackground(props.x, sunY.value)
}, { immediate: true })
}) })
</script> </script>

View file

@ -155,14 +155,12 @@
flex-flow: row wrap; flex-flow: row wrap;
} }
#parallax, #background, #light-mask { #light-mask {
position: absolute; position: absolute;
top: calc(var(--block-size) * -1); top: calc(var(--block-size) * -1);
left: calc(var(--block-size) * -1); left: calc(var(--block-size) * -1);
width: calc(100% + var(--block-size) * 2); width: calc(100% + var(--block-size) * 2);
height: calc(100% + var(--block-size) * 2); height: calc(100% + var(--block-size) * 2);
mix-blend-mode: multiply;
pointer-events: none; pointer-events: none;
} }
#light-mask {
mix-blend-mode: multiply;
}

View file

@ -13,4 +13,3 @@
.item.block-wood { background-image: url("/Tiles/wood.png"); } .item.block-wood { background-image: url("/Tiles/wood.png"); }
.item.block-dirt { background-image: url("/Tiles/dirt.png"); } .item.block-dirt { background-image: url("/Tiles/dirt.png"); }
.item.block-stone { background-image: url("/Tiles/stone.png"); } .item.block-stone { background-image: url("/Tiles/stone.png"); }
.item.block-gravel { background-image: url("/Tiles/gravel_stone.png"); }

BIN
src/assets/mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View file

@ -17,7 +17,7 @@ export const items = {
block_dirt: { id: 'block_dirt', name: 'dirt', type: 'block', icon: 'dirt', builds: 'soil' } as Item, block_dirt: { id: 'block_dirt', name: 'dirt', type: 'block', icon: 'dirt', builds: 'soil' } as Item,
block_wood: { id: 'block_wood', name: 'wood', type: 'block', icon: 'wood', builds: 'treeTrunk' } as Item, block_wood: { id: 'block_wood', name: 'wood', type: 'block', icon: 'wood', builds: 'treeTrunk' } as Item,
block_stone: { id: 'block_stone', name: 'stone', type: 'block', icon: 'stone', builds: 'brickWall' } as Item, block_stone: { id: 'block_stone', name: 'stone', type: 'block', icon: 'stone', builds: 'brickWall' } as Item,
block_gravel: { id: 'block_gravel', name: 'gravel', type: 'block', icon: 'gravel' /*, builds??? TODO */ } as Item, block_gravel: { id: 'block_gravel', name: 'gravel', type: 'block', icon: 'stone' /*, builds??? TODO */ } as Item,
ore_coal: { id: 'ore_coal', name: 'coal', type: 'ore', icon: 'ore_coal' } as Item, ore_coal: { id: 'ore_coal', name: 'coal', type: 'ore', icon: 'ore_coal' } as Item,
ore_iron: { id: 'ore_iron', name: 'iron', type: 'ore', icon: 'ore_iron' } as Item, ore_iron: { id: 'ore_iron', name: 'iron', type: 'ore', icon: 'ore_iron' } as Item,

View file

@ -18,13 +18,7 @@ function hsl(h: number, s: number, l: number): string {
* @param r: number [44] - the radius of the "sun" * @param r: number [44] - the radius of the "sun"
* @returns emissionStrength: number - emission intensity blends over the mountains * @returns emissionStrength: number - emission intensity blends over the mountains
*/ */
function renderGodrays( function renderGodrays(ctx: CanvasRenderingContext2D, cx: number, cy: number, sunY: number, r = 44) {
ctx: CanvasRenderingContext2D,
cx: number,
cy: number,
sunY: number,
r = 44,
) {
const w = ctx.canvas.width const w = ctx.canvas.width
const h = ctx.canvas.height const h = ctx.canvas.height
@ -42,14 +36,11 @@ function renderGodrays(
if (sunY > -30) emissionStrength -= Math.max((30 + sunY) / 5, 0.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) else if (sunY < -60) emissionStrength += Math.min(1 + (60 + sunY) / 5, 0.0)
// pixels in radius 0 to 4.4 (44 * .1) emissionGradient.addColorStop(.1, hsl(30, 50, 3.1 * emissionStrength)) // 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)) // pixels in radius 0 to 4.4 (44 * .1).
emissionGradient.addColorStop(.2, hsl(12, 71, 1.4 * emissionStrength)) // Now paint the gradient all over our godrays canvas.
// Now paint the gradient all over our godrays canvas
ctx.fillRect(0, 0, w, h) 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' ctx.fillStyle = '#000'
return emissionStrength return emissionStrength
@ -60,12 +51,8 @@ function renderGodrays(
* Mountains are made by summing up sine waves with varying frequencies and amplitudes * Mountains are made by summing up sine waves with varying frequencies and amplitudes
* The frequencies are prime, to avoid extra repetitions * The frequencies are prime, to avoid extra repetitions
*/ */
function calcMountainHeight( function calcMountainHeight(pos: number, roughness: number, frequencies = [1721, 947, 547, 233, 73, 31, 7]) {
pos: number, return frequencies.reduce((height, freq) => height * roughness - Math.cos(freq * pos), 0)
roughness: number,
freqs = [1721, 947, 547, 233, 73, 31, 7],
) {
return freqs.reduce((height, freq) => height * roughness - Math.cos(freq * pos), 0)
} }
/** /**
@ -77,14 +64,7 @@ function calcMountainHeight(
* @param layers: number - amount of mountain layers for parallax effect * @param layers: number - amount of mountain layers for parallax effect
* @param emissionStrength: number - intensity of the godrays * @param emissionStrength: number - intensity of the godrays
*/ */
function renderMountains( function renderMountains(ctx: CanvasRenderingContext2D, grCtx: CanvasRenderingContext2D, frame: number, sunY: number, layers: number, emissionStrength: number) {
ctx: CanvasRenderingContext2D,
grCtx: CanvasRenderingContext2D,
frame: number,
sunY: number,
layers: number,
emissionStrength: number,
) {
const w = ctx.canvas.width const w = ctx.canvas.width
const h = ctx.canvas.height const h = ctx.canvas.height
const grDiv = w / grCtx.canvas.width const grDiv = w / grCtx.canvas.width
@ -147,27 +127,19 @@ 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 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? * @param mountainLayers: number [4] - How many layers of mountains are used for parallax effect?
*/ */
export default function useBackground( export default function useBackground (canvasEl: HTMLCanvasElement, w: number, h: number, rayQuality = 8, mountainLayers = 4) {
canvasEl: HTMLCanvasElement,
w: number,
h: number,
rayQuality = 8,
mountainLayers = 4,
) {
canvasEl.width = w canvasEl.width = w
canvasEl.height = h canvasEl.height = h
const grW = w / rayQuality const grW = w / rayQuality
const grH = h / rayQuality const grH = h / rayQuality
const grCanvasEl = document.createElement('canvas')
const ctx = canvasEl.getContext('2d') 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') const grCtx = grCanvasEl.getContext('2d')
if (ctx === null || grCtx === null) { if (grCtx === null) return () => {} // for real, how old is it?
console.error('BACKGROUND CANVAS ERROR: Failed to set up canvas?!')
return () => {} // for real, how old is it?
}
grCanvasEl.width = grW grCanvasEl.width = grW
grCanvasEl.height = grH grCanvasEl.height = grH
@ -181,10 +153,7 @@ export default function useBackground(
* @param sunY: number - the position (height) of the sun in the sky * @param sunY: number - the position (height) of the sun in the sky
*/ */
return function drawFrame (frame: number, sunY: number) { return function drawFrame (frame: number, sunY: number) {
ctx.globalCompositeOperation = grCtx.globalCompositeOperation = 'source-over' console.log('drawing frame', frame, sunY)
ctx.clearRect(0, 0, w, h)
grCtx.clearRect(0, 0, grW, grH)
const emissionStrength = renderGodrays(grCtx, sunCenterX, sunCenterY, sunY) const emissionStrength = renderGodrays(grCtx, sunCenterX, sunCenterY, sunY)
renderSky(ctx, sunY) renderSky(ctx, sunY)
renderMountains(ctx, grCtx, frame, sunY, mountainLayers, emissionStrength) renderMountains(ctx, grCtx, frame, sunY, mountainLayers, emissionStrength)

View file

@ -3,7 +3,7 @@ import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
type RefOrComputed<T> = Ref<T> | ComputedRef<T> type RefOrComputed<T> = Ref<T> | ComputedRef<T>
export default function useLightMask( export default function useLightMap(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
x: RefOrComputed<number>, x: RefOrComputed<number>,
y: RefOrComputed<number>, y: RefOrComputed<number>,
@ -43,29 +43,16 @@ export default function useLightMask(
} }
function drawPlayerLight(sizeMul:number) { function drawPlayerLight(sizeMul:number) {
const t = time.value
const playerLight = ctx.createRadialGradient( const playerLight = ctx.createRadialGradient(
playerX - tx.value, playerY - ty.value, 0, playerX - tx.value, playerY - ty.value, 0,
playerX - tx.value, playerY - ty.value, playerLightSize * sizeMul playerX - tx.value, playerY - ty.value, playerLightSize * sizeMul
) )
// Add color stops for a light around the player // Add color stops: white in the center to transparent white
// Night time, lets tone down the light a bit playerLight.addColorStop(0.0, "#FFFF");
if (t > 900 || t < 100) { playerLight.addColorStop(0.4, "#FFFF");
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(0.7, "#FFFA");
playerLight.addColorStop(1, "#FFF0"); playerLight.addColorStop(1, "#FFF1");
}
// Set the fill style and draw a rectangle // Set the fill style and draw a rectangle
ctx.fillStyle = playerLight; ctx.fillStyle = playerLight;
@ -106,7 +93,7 @@ export default function useLightMask(
return function update() { return function update() {
// first, throw the world in complete darkness // first, throw the world in complete darkness
ctx.fillStyle = '#000000' ctx.fillStyle = '#000'
ctx.fillRect(0, 0, W, H) ctx.fillRect(0, 0, W, H)
// second, find and bring light into the world // second, find and bring light into the world

View file

@ -1,58 +0,0 @@
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)
})
})
})

View file

@ -1,59 +1,30 @@
import { ref, computed } from 'vue' 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() { export default function useTime() {
// the day is split in 1000 parts, so we start in the morning // the day is split in 1000 parts, so we start in the morning
const time = ref(250) const time = ref(230)
function updateTime() { function updateTime() {
time.value = (time.value + 0.1) % 1000 time.value = (time.value + 0.1) % 1000
} }
const timeOfDay = computed(() => calcTimeOfDay(time.value)) const timeOfDay = computed(() => {
const clock = computed(() => renderClock(time.value)) 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}`
})
return { time, updateTime, timeOfDay, clock } return { time, updateTime, timeOfDay, clock }
} }