use canvas as light map
This commit is contained in:
parent
3a9d2044e1
commit
3b7ee96f62
6 changed files with 184 additions and 12 deletions
40
src/App.vue
40
src/App.vue
|
@ -9,12 +9,16 @@ import createLevel from './level'
|
||||||
import useTime from './util/useTime'
|
import useTime from './util/useTime'
|
||||||
import useInput from './util/useInput'
|
import useInput from './util/useInput'
|
||||||
import usePlayer from './util/usePlayer'
|
import usePlayer from './util/usePlayer'
|
||||||
|
import useLightMap from './util/useLightMap'
|
||||||
|
|
||||||
const { updateTime, timeOfDay, clock } = useTime()
|
const { updateTime, time, clock } = useTime()
|
||||||
const { player, direction, dx, dy } = usePlayer()
|
const { player, direction, dx, dy } = 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 lightMapEl = ref<HTMLCanvasElement | undefined>(undefined)
|
||||||
|
let lightMap: ReturnType<typeof useLightMap>
|
||||||
|
|
||||||
player.inventory.push(
|
player.inventory.push(
|
||||||
{ name: 'Shovel', type: 'tool', icon: 'shovel', quality: 'bronze', amount: 1 },
|
{ name: 'Shovel', type: 'tool', icon: 'shovel', quality: 'bronze', amount: 1 },
|
||||||
{ name: 'Sword', type: 'weapon', icon: 'sword', quality: 'bronze', amount: 2 },
|
{ name: 'Sword', type: 'weapon', icon: 'sword', quality: 'bronze', amount: 2 },
|
||||||
|
@ -67,19 +71,18 @@ const blocked = computed(() => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
const damagedBlocks = ref([])
|
||||||
|
|
||||||
function dig(blockX: number, blockY: number, oldBlockType: BlockType) {
|
function dig(blockX: number, blockY: number, oldBlockType: BlockType) {
|
||||||
// § 4 ArbZG
|
// § 4 ArbZG
|
||||||
if (paused.value) return
|
if (paused.value) return
|
||||||
|
|
||||||
// TODO: temporary filter
|
// TODO: temporary filter
|
||||||
if (oldBlockType === 'air' || oldBlockType === 'cave') return
|
if (oldBlockType === 'air' || oldBlockType === 'cave') return
|
||||||
|
// when we finally dig that block
|
||||||
|
level.change({ type: 'exchange', x: floorX.value + blockX, y: floorY.value + blockY, newType: 'air' })
|
||||||
|
|
||||||
level.change({
|
|
||||||
type: 'exchange',
|
|
||||||
x: floorX.value + blockX,
|
|
||||||
y: floorY.value + blockY,
|
|
||||||
newType: 'air'
|
|
||||||
})
|
|
||||||
// This feels like cheating, but it makes Vue recalculate floorX
|
// This feels like cheating, but it makes Vue recalculate floorX
|
||||||
// which then recalculates the blocks, so that the changes are
|
// which then recalculates the blocks, so that the changes are
|
||||||
// applied. Otherwise, they wouldn't be visible before moving
|
// applied. Otherwise, they wouldn't be visible before moving
|
||||||
|
@ -133,39 +136,54 @@ const move = (thisTick: number): void => {
|
||||||
|
|
||||||
walking.value = !!dx_
|
walking.value = !!dx_
|
||||||
|
|
||||||
x.value += dx_ * movementMultiplier
|
if (dy_ <= 0) x.value += dx_ * movementMultiplier
|
||||||
|
|
||||||
if (dy_ < 0 || arriving.value) {
|
if (dy_ < 0 || arriving.value) {
|
||||||
y.value += dy_ * movementMultiplier
|
y.value += dy_ * movementMultiplier
|
||||||
} else {
|
} else {
|
||||||
y.value += dy_ * fallMultiplier
|
y.value += dy_ * fallMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lightMap.draw(floorX.value, floorY.value, tx.value, ty.value, time.value)
|
||||||
lastTick = thisTick
|
lastTick = thisTick
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcBrightness(level: number, row: number) {
|
function calcBrightness(level: number, row: number) {
|
||||||
const barrier = lightBarrier.value[row]
|
const barrier = lightBarrier.value[row]
|
||||||
|
const barrierLeft = lightBarrier.value[row - 1]
|
||||||
|
const barrierRight = lightBarrier.value[row + 1]
|
||||||
|
|
||||||
let delta = barrier - level - (floorY.value - 3)
|
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
|
if (delta > 3) delta = 3
|
||||||
else if (delta < 0) delta = 0
|
else if (delta < 0) delta = 0
|
||||||
|
|
||||||
|
if (deltaR > delta || deltaL > delta) delta = Math.max(deltaL, deltaR) - 1
|
||||||
|
|
||||||
return `sun-${delta}`
|
return `sun-${delta}`
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const canvas = lightMapEl.value!
|
||||||
|
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
||||||
|
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
|
||||||
|
lightMap = useLightMap(ctx)
|
||||||
lastTick = performance.now()
|
lastTick = performance.now()
|
||||||
move(lastTick)
|
move(lastTick)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="field" :class="timeOfDay">
|
<div id="field">
|
||||||
|
|
||||||
<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, calcBrightness(y, x)]"
|
:class="['block', block.type]"
|
||||||
@click="dig(x, y, block.type)"
|
@click="dig(x, y, block.type)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -185,8 +203,8 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<canvas id="light-mask" ref="lightMapEl" :style="{transform: `translate(${tx}px, ${ty}px)`}" />
|
||||||
<div id="beam" v-if="arriving"></div>
|
<div id="beam" v-if="arriving"></div>
|
||||||
|
|
||||||
<div id="level-indicator">
|
<div id="level-indicator">
|
||||||
x:{{ floorX }}, y:{{ floorY }}
|
x:{{ floorX }}, y:{{ floorY }}
|
||||||
<template v-if="paused">(PAUSED)</template>
|
<template v-if="paused">(PAUSED)</template>
|
||||||
|
|
|
@ -7,6 +7,26 @@
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
.block::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: var(--block-size);
|
||||||
|
height: var(--block-size);
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: url(/break.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position-y: center;
|
||||||
|
background-position-x: calc(var(--block-size) * -7);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.damage-0::after { background-position-x: 0px; }
|
||||||
|
.block.damage-1::after { background-position-x: calc(var(--block-size) * -1); }
|
||||||
|
.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.grass { background-image: url(/Tiles/dirt_grass.png); }
|
||||||
|
|
||||||
|
@ -70,3 +90,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
BIN
src/assets/mask.png
Normal file
BIN
src/assets/mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
80
src/light-mask.vue
Normal file
80
src/light-mask.vue
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
|
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
tx: number,
|
||||||
|
ty: number,
|
||||||
|
lightBarrier: number[],
|
||||||
|
time: number,
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
// TODO: use OffscreenCanvas and a WebWorker?
|
||||||
|
const lightMapEl = ref<HTMLCanvasElement | undefined>(undefined)
|
||||||
|
const W = ((STAGE_WIDTH + 2) * BLOCK_SIZE) / 2
|
||||||
|
const H = ((STAGE_HEIGHT + 2) * BLOCK_SIZE) / 2
|
||||||
|
|
||||||
|
const playerX = (W - BLOCK_SIZE) / 4
|
||||||
|
const playerY = H / 4 - BLOCK_SIZE / 2
|
||||||
|
const playerLightSize = BLOCK_SIZE / 3
|
||||||
|
|
||||||
|
function drawPlayerLight(ctx: CanvasRenderingContext2D) {
|
||||||
|
|
||||||
|
const playerLight = ctx.createRadialGradient(
|
||||||
|
playerX - props.tx / 4, playerY - props.ty / 4, 0,
|
||||||
|
playerX - props.tx / 4, playerY - props.ty / 4, playerLightSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add three color stops
|
||||||
|
playerLight.addColorStop(0.0, "#FFFF");
|
||||||
|
playerLight.addColorStop(1, "#F0F0");
|
||||||
|
|
||||||
|
// Set the fill style and draw a rectangle
|
||||||
|
ctx.fillStyle = playerLight;
|
||||||
|
ctx.fillRect(0, 0, W, H)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const canvas = lightMapEl.value
|
||||||
|
const ctx = canvas?.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
watch(props, () => {
|
||||||
|
const t = props.time
|
||||||
|
|
||||||
|
if (t > 900 || t < 100) {
|
||||||
|
ctx.fillStyle = `hsl(0, 0%, 20%)`
|
||||||
|
} else if (t < 250) {
|
||||||
|
const s = Math.round((t - 100) / 1.5) // 0-100%
|
||||||
|
const l = Math.round((t - 100) / 1.875) + 20 // 20-100%
|
||||||
|
ctx.fillStyle = `hsl(0, ${s}%, ${l}%)`
|
||||||
|
// } else if (t < 700) {
|
||||||
|
// ctx.fillStyle = `hsl(0, ${}%, ${}%)`
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = `hsl(0, 0%, 100%)`
|
||||||
|
}
|
||||||
|
ctx.fillRect(0, 0, W, H)
|
||||||
|
drawPlayerLight(ctx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas ref="lightMapEl" :style="{transform: `translate(${tx}px, ${ty}px)`}" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: -64px;
|
||||||
|
left: -64px;
|
||||||
|
width: calc(100% + 128px);
|
||||||
|
height: calc(100% + 128px);
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
items: InventoryItem[]
|
items: InventoryItem[]
|
||||||
shown: boolean
|
shown: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'selection', value: InventoryItem | null): void
|
(event: 'selection', value: InventoryItem | null): void
|
||||||
}>()
|
}>()
|
||||||
|
|
44
src/util/useLightMap.ts
Normal file
44
src/util/useLightMap.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
|
||||||
|
|
||||||
|
export default function useLightMap(ctx: CanvasRenderingContext2D) {
|
||||||
|
const W = ((STAGE_WIDTH + 2) * BLOCK_SIZE)
|
||||||
|
const H = ((STAGE_HEIGHT + 2) * BLOCK_SIZE)
|
||||||
|
|
||||||
|
const playerX = (W - BLOCK_SIZE) / 2 + BLOCK_SIZE / 4
|
||||||
|
const playerY = H / 2 - BLOCK_SIZE / 2
|
||||||
|
const playerLightSize = BLOCK_SIZE * 1.8
|
||||||
|
|
||||||
|
function drawPlayerLight(tx: number, ty: number) {
|
||||||
|
const playerLight = ctx.createRadialGradient(
|
||||||
|
playerX - tx, playerY - ty, 0,
|
||||||
|
playerX - tx, playerY - ty, playerLightSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add three color stops
|
||||||
|
playerLight.addColorStop(0.0, "#FFCF");
|
||||||
|
playerLight.addColorStop(1, "#FFC0");
|
||||||
|
|
||||||
|
// Set the fill style and draw a rectangle
|
||||||
|
ctx.fillStyle = playerLight;
|
||||||
|
ctx.fillRect(0, 0, W, H)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: support light barrier
|
||||||
|
function draw(x:number, y:number, tx:number, ty:number, time:number) {
|
||||||
|
if (time > 900 || time < 100) {
|
||||||
|
ctx.fillStyle = `hsl(0, 0%, 20%)`
|
||||||
|
} else if (time < 250) {
|
||||||
|
const s = Math.round((time - 100) / 1.5) // 0-100%
|
||||||
|
const l = Math.round((time - 100) / 1.875) + 20 // 20-100%
|
||||||
|
ctx.fillStyle = `hsl(0, ${s}%, ${l}%)`
|
||||||
|
// } else if (t < 700) {
|
||||||
|
// ctx.fillStyle = `hsl(0, ${}%, ${}%)`
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = `hsl(0, 0%, 100%)`
|
||||||
|
}
|
||||||
|
ctx.fillRect(0, 0, W, H)
|
||||||
|
drawPlayerLight(tx, ty)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { draw }
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue