From 3c8b2893100707ccd04dbd65d02cd308192bb5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20K=C3=B6hring?= Date: Tue, 25 Apr 2023 09:09:49 +0200 Subject: [PATCH] debugging highlights --- src/App.vue | 25 ++--- src/Background.vue | 35 +++++++ src/assets/field.css | 139 ++++++++++++++++++++++------ src/util/useBackground.ts | 186 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 338 insertions(+), 47 deletions(-) create mode 100644 src/Background.vue create mode 100644 src/util/useBackground.ts diff --git a/src/App.vue b/src/App.vue index da12eb4..4fd389f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -138,12 +138,14 @@ const move = (thisTick: number): void => { // do nothing when paused if (paused.value) { - lastTick = thisTick // reset tick, to avoid huge tickDelta + // reset tick, to avoid tickDelta and resulting character teleport + lastTick = thisTick return } const tickDelta = thisTick - lastTick lastTimeUpdate += tickDelta + // update in-game time every 60ms by 0.1 // then a day needs 10000 updates, and it takes about 10 minutes if (lastTimeUpdate > 60) { @@ -189,23 +191,6 @@ const move = (thisTick: number): void => { lastTick = thisTick } -function calcBrightness(level: number, row: number) { - const barrier = lightBarrier.value[row] - const barrierLeft = lightBarrier.value[row - 1] - const barrierRight = lightBarrier.value[row + 1] - - 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 - else if (delta < 0) delta = 0 - - if (deltaR > delta || deltaL > delta) delta = Math.max(deltaL, deltaR) - 1 - - return `sun-${delta}` -} - function selectTool(item: InventoryItem) { inventorySelection.value = item } @@ -228,7 +213,9 @@ onMounted(() => {
diff --git a/src/Background.vue b/src/Background.vue new file mode 100644 index 0000000..2ec213a --- /dev/null +++ b/src/Background.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/assets/field.css b/src/assets/field.css index 0c1cf74..7ced72b 100644 --- a/src/assets/field.css +++ b/src/assets/field.css @@ -1,6 +1,8 @@ -.block, #player { +.block, +#player { transition: filter .5s linear; } + .block { flex: 0 0 auto; width: var(--block-size); @@ -10,6 +12,7 @@ background-position: center; background-repeat: no-repeat; } + .block::after { content: ''; position: absolute; @@ -23,40 +26,120 @@ 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.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.treeCrown, +.block.treeLeaves { + background-image: url(/Tiles/leaves_transparent.png); +} + +.block.treeTrunk { + background-image: url(/Tiles/trunk_mid.png); +} + +.block.treeRoot { + background-image: url(/Tiles/trunk_bottom.png); +} + +.block.soil { + background-image: url(/Tiles/dirt.png); +} + +.block.soilGravel { + background-image: url(/Tiles/gravel_dirt.png); +} -.block.grass { background-image: url(/Tiles/dirt_grass.png); } +.block.stoneGravel { + background-image: url(/Tiles/gravel_stone.png); +} -.block.treeCrown, .block.treeLeaves { background-image: url(/Tiles/leaves_transparent.png); } -.block.treeTrunk { background-image: url(/Tiles/trunk_mid.png); } -.block.treeRoot { background-image: url(/Tiles/trunk_bottom.png); } +.block.stone { + background-image: url(/Tiles/stone.png); +} -.block.soil { background-image: url(/Tiles/dirt.png); } -.block.soilGravel { background-image: url(/Tiles/gravel_dirt.png); } -.block.stoneGravel { background-image: url(/Tiles/gravel_stone.png); } -.block.stone { background-image: url(/Tiles/stone.png); } -.block.bedrock { background-image: url(/Tiles/greystone.png); } -.block.cave { background-color: #000; } +.block.bedrock { + background-image: url(/Tiles/greystone.png); +} -.block.brickWall { background-image: url(/Tiles/brick_grey.png); } +.block.cave { + background-color: #000; +} -#field .block:hover { outline: 1px solid white; z-index: 10; } +.block.brickWall { + background-image: url(/Tiles/brick_grey.png); +} -.morning0 .block, .morning0 #player {filter: saturate(50%); } -.morning1 .block, .morning1 #player { filter: saturate(100%); } -.morning2 .block, .morning2 #player { filter: saturate(120%); } +#field .block:hover, +#field .block.block.highlight { + filter: brightness(1.2) grayscale(1.0); + outline: 1px solid white; + z-index: 10; +} -.evening0 .block, .evening0 #player { filter: saturate(90%); } -.evening1 .block, .evening1 #player { filter: saturate(70%); } -.evening2 .block, .evening2 #player { filter: saturate(50%); } +.morning0 .block, +.morning0 #player { + filter: saturate(50%); +} -.night .block, .night #player { filter: saturate(30%); } +.morning1 .block, +.morning1 #player { + filter: saturate(100%); +} + +.morning2 .block, +.morning2 #player { + filter: saturate(120%); +} + +.evening0 .block, +.evening0 #player { + filter: saturate(90%); +} + +.evening1 .block, +.evening1 #player { + filter: saturate(70%); +} + +.evening2 .block, +.evening2 #player { + filter: saturate(50%); +} + +.night .block, +.night #player { + filter: saturate(30%); +} #blocks { position: absolute; @@ -76,4 +159,4 @@ height: calc(100% + var(--block-size) * 2); mix-blend-mode: multiply; pointer-events: none; -} +} \ No newline at end of file diff --git a/src/util/useBackground.ts b/src/util/useBackground.ts new file mode 100644 index 0000000..3a0a587 --- /dev/null +++ b/src/util/useBackground.ts @@ -0,0 +1,186 @@ + +/** + * hsl - creates hsl color string from h, s and l values + * @param h: number - hue + * @param s: number - saturation + * @param l: number - lightness + */ +function hsl(h: number, s: number, l: number): string { + return `hsl(${h}, ${s}%, ${l}%)` +} + +/** + * render godrays + * @param ctx: CanvasRenderingContext2D - where to draw + * @param cx: number - x-axis center of the "sun" + * @param cy: number - y-axis center of the "sun" + * @param sunY: number - the position (height) of the "sun" in the sky + * @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) { + const w = ctx.canvas.width + const h = ctx.canvas.height + + const emissionGradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, r) + ctx.fillStyle = emissionGradient + + // Now we addColorStops. This needs to be a dark gradient because our + // godrays effect will basically overlay it on top of itself many many times, + // so anything lighter will result in lots of white. + // If you're not space-bound you can add another stop or two, maybe fade out to black, + // but this actually looks good enough. + + // a black "gradient" means no emission, so we fade to black as transition to night or day + let emissionStrength = 1.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) + + 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. + ctx.fillRect(0, 0, w, h) + // And set the fillstyle to black, we'll use it to paint our occlusion (mountains). + ctx.fillStyle = '#000' + + return emissionStrength +} + +/** + * calculate mountain height + * 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) +} + +/** + * render mountains + * @param ctx: CanvasRenderingContext2D - where to draw + * @param grCtx: CanvasRenderingContext2D - for drawing mountain shadows on the godray canvas + * @param frame: number - current frame (position on the x axis) + * @param sunY: number - position (height) of the "sun" in the sky + * @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) { + const w = ctx.canvas.width + const h = ctx.canvas.height + const grDiv = w / grCtx.canvas.width + + for (let i = 0; i < layers; i++) { + // Set the main canvas fillStyle to a shade of green-brown with variable lightness + // depending on sunY and depth + ctx.fillStyle = sunY > -60 + ? hsl(5, 23, 33*emissionStrength - i*6*emissionStrength) + : hsl(220 - i*40, 23, 33-i*6) + + for (let x = w; x--;) { + // Ok, I don't really remember the details here, basically the (frame+frame*i*i) makes the + // near mountains move faster than the far ones. We divide by large numbers because our + // mountains repeat at position 1/7*Math.PI*2 or something like that... + const pos = (frame * 2 * i**2) / 1000 + x / 2000 + // Make further mountains more jagged, adds a bit of realism and also makes the godrays + // look nicer. + const roughness = i / 19 - .5 + // 128 is the middle, i * 25 moves the nearer mountains lower on the screen. + let y = 128 + i * 25 + calcMountainHeight(pos, roughness) * 45 + // Paint a 1px-wide rectangle from the mountain's top to below the bottom of the canvas. + ctx.fillRect(x, y, 1, h) + // Paint the same thing in black on the godrays emission canvas, which is 1/4 the size, + // and move it one pixel down (otherwise there can be a tiny underlit space between the + // mountains and the sky). + grCtx.fillRect(x/grDiv, y/grDiv+1, 1, h) + } + } +} + +/** + * render sky + * @param ctx: CanvasRenderingContext2D - where to draw + * @param sunY: number - the position (height) of the "sun" in the sky + */ +function renderSky(ctx: CanvasRenderingContext2D, sunY: number) { + const w = ctx.canvas.width + const h = ctx.canvas.height + + const skyGradient = ctx.createLinearGradient(0, 0, 0, h) + const skyHue = 360 + sunY // hue from blue to red, depending on the suns position + const skySaturation = 100 + sunY // less saturation at day so that the red fades away + const skyLightness = Math.min(sunY * -1 - 10, 55) // darker at night + + const skyHSLTop = `hsl(220, 70%, ${skyLightness}%)` + const skyHSLBottom = `hsl(${skyHue}, ${skySaturation}%, ${skyLightness}%)` + skyGradient.addColorStop(0, skyHSLTop) + skyGradient.addColorStop(.7, skyHSLBottom) + + ctx.fillStyle = skyGradient + ctx.fillRect(0, 0, w, h) +} + +/** + * useBackground + * @param canvasEl: HTMLCanvasElement - the canvas to draw the background on. + * @param w: number - the (pixel) width of the canvas. The element itself can have a different width. + * @param h: number - the (pixel) height of the canvas. The element itself can have a different height. + * @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) { + 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 grCtx = grCanvasEl.getContext('2d') + if (grCtx === null) return // like, how old is your browser? + + grCanvasEl.width = grW + grCanvasEl.height = grH + + const sunCenterX = grCanvasEl.width / 2 + const sunCenterY = grCanvasEl.height / 2 + + /** + * draw one frame of the background + * @param frame: number - the position on the x axis, to calculate the paralax background + * @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) + const emissionStrength = renderGodrays(grCtx, sunCenterX, sunCenterY, sunY) + renderSky(ctx, sunY) + renderMountains(ctx, grCtx, frame, sunY, mountainLayers, emissionStrength) + + // The godrays are generated by adding up RGB values, gCt is the bane of all js golfers - + // globalCompositeOperation. Set it to 'lighter' on both canvases. + ctx.globalCompositeOperation = grCtx.globalCompositeOperation = 'lighter' + + // NOW - let's light this m**f** up! We'll make several passes over our emission canvas, + // each time adding an enlarged copy of it to itself so at the first pass we get 2 copies, then 4, + // then 8, then 16 etc... We square our scale factor at each iteration. + for (let scaleFactor = 1.07; scaleFactor < 5; scaleFactor *= scaleFactor) { + // The x, y, width and height arguments for drawImage keep the light source in the same + // spot on the enlarged copy. It basically boils down to multiplying a 2D matrix by itself. + // There's probably a better way to do this, but I couldn't figure it out. + // For reference, here's an AS3 version (where BitmapData:draw takes a matrix argument): + // https://github.com/yonatan/volumetrics/blob/d3849027213e9499742cc4dfd2838c6032f4d9d3/src/org/zozuar/volumetrics/EffectContainer.as#L208-L209 + grCtx.drawImage( + grCanvasEl, + (grW - grW * scaleFactor) / 2, + (grH - grH * scaleFactor) / 2 - sunY * scaleFactor + sunY, + grW * scaleFactor, + grH * scaleFactor + ) + } + + // Draw godrays to output canvas (whose globalCompositeOperation is already set to 'lighter'). + ctx.drawImage(grCanvasEl, 0, 0, w, h); + } +}