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 }
}