diff --git a/src/App.vue b/src/App.vue index 788cd73..fe1ad38 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,8 +1,9 @@ diff --git a/src/assets/field.css b/src/assets/field.css index ea85618..f824ed2 100644 --- a/src/assets/field.css +++ b/src/assets/field.css @@ -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; +} diff --git a/src/assets/mask.png b/src/assets/mask.png deleted file mode 100644 index 9a04caf..0000000 Binary files a/src/assets/mask.png and /dev/null differ diff --git a/src/util/useBackground.ts b/src/util/useBackground.ts index 962a20d..d8f6b00 100644 --- a/src/util/useBackground.ts +++ b/src/util/useBackground.ts @@ -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) diff --git a/src/util/useLightMap.ts b/src/util/useLightMask.ts similarity index 83% rename from src/util/useLightMap.ts rename to src/util/useLightMask.ts index 16bcf15..4c3a23e 100644 --- a/src/util/useLightMap.ts +++ b/src/util/useLightMask.ts @@ -3,7 +3,7 @@ import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def' type RefOrComputed = Ref | ComputedRef -export default function useLightMap( +export default function useLightMask( ctx: CanvasRenderingContext2D, x: RefOrComputed, y: RefOrComputed, @@ -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 diff --git a/src/util/useTime.test.ts b/src/util/useTime.test.ts new file mode 100644 index 0000000..a2e05ee --- /dev/null +++ b/src/util/useTime.test.ts @@ -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) + }) + }) +}) diff --git a/src/util/useTime.ts b/src/util/useTime.ts index b817fc3..70c94cc 100644 --- a/src/util/useTime.ts +++ b/src/util/useTime.ts @@ -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 } }