commit d2f693ccf5990ebc3d1cdc84ba7a681df561e4b2 Author: Evert Prants Date: Wed Jul 31 21:32:08 2019 +0300 Initial commit diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..4b15448 --- /dev/null +++ b/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": [ + [ + "@babel/plugin-transform-runtime", + { + "corejs": false, + "helpers": true, + "regenerator": true, + "useESModules": false + } + ] + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a0a4de8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.js] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99d9404 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/node_modules/ +/dist/ +/package-lock.json +/**/dna.txt +*.db +*.sqlite3 diff --git a/index.html b/index.html new file mode 100644 index 0000000..2f806c9 --- /dev/null +++ b/index.html @@ -0,0 +1,10 @@ + + + + + + bechunked + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..a8c3af0 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "bechunked", + "version": "0.0.0", + "description": "testing canvas stuff", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "serve": "node ./serve.js", + "build": "webpack -p", + "watch": "webpack -w --mode=development" + }, + "keywords": [], + "private": true, + "author": "Evert \"Diamond\" Prants ", + "license": "MIT", + "devDependencies": { + "@babel/core": "^7.1.6", + "@babel/plugin-transform-runtime": "^7.1.0", + "@babel/preset-env": "^7.1.6", + "babel-loader": "^8.0.4", + "copy-webpack-plugin": "^5.0.4", + "express": "^4.16.4", + "html-webpack-plugin": "^3.2.0", + "standard": "^12.0.1", + "webpack": "^4.26.0", + "webpack-command": "^0.4.2" + }, + "dependencies": { + "@babel/runtime": "^7.1.5", + "simplex-noise": "^2.4.0" + } +} diff --git a/serve.js b/serve.js new file mode 100755 index 0000000..6ac160f --- /dev/null +++ b/serve.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node +const express = require('express') +const path = require('path') +const app = express() + +app.set('env', 'development') +app.use('/', express.static(path.join(__dirname, 'dist'))) + +app.listen(3000, function () { + console.log('server listening on 0.0.0.0:3000') +}) diff --git a/src/image.js b/src/image.js new file mode 100644 index 0000000..710b324 --- /dev/null +++ b/src/image.js @@ -0,0 +1,63 @@ +import Resource from './resource' + +let imageCache = {} + +class Image { + constructor (file, img) { + this.file = file + this.img = img + } + + static async load (file) { + if (imageCache[file]) return imageCache[file] + let img = await Resource.loadImage(file) + let imgCl = new Image(file, img) + imageCache[file] = imgCl + return imgCl + } + + get width () { + return this.img.width + } + + get height () { + return this.img.height + } + + draw (ctx, x, y, w, h) { + ctx.drawImage(this.img, x, y, w, h) + } +} + +class TileMap extends Image { + constructor (file, img, xaspect, yaspect) { + super(file, img) + this.xaspect = xaspect + this.yaspect = yaspect + this.xcount = this.width / xaspect + this.ycount = this.height / yaspect + this.tiles = [] + + for (let y = 0; y < this.ycount; y++) { + for (let x = 0; x < this.xcount; x++) { + this.tiles.push({ x, y }) + } + } + } + + draw (ctx, index, x, y, w, h) { + let box = this.tiles[index] + if (!box) return + ctx.drawImage(this.img, box.x * this.xaspect, box.y * this.yaspect, this.xaspect, this.yaspect, x, y, w, h) + } + + static async load (file, x, y) { + if (imageCache[file]) return imageCache[file] + let img = await Resource.loadImage(file, true) + let imgCl = new TileMap(file, img, x, y) + imageCache[file] = imgCl + return imgCl + } +} + +export { Image, TileMap } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..8711cc6 --- /dev/null +++ b/src/index.js @@ -0,0 +1,271 @@ +/* global requestAnimationFrame */ +import Simplex from 'simplex-noise' +import Input from './input' + +class BetterNoise extends Simplex { + // amplitude - Controls the amount the height changes. The higher, the taller the hills. + // persistence - Controls details, value in [0,1]. Higher increases grain, lower increases smoothness. + // octaves - Number of noise layers + // period - Distance above which we start to see similarities. The higher, the longer "hills" will be on a terrain. + // lacunarity - Controls period change across octaves. 2 is usually a good value to address all detail levels. + constructor (seed, amplitude = 15, persistence = 0.4, octaves = 5, period = 80, lacunarity = 2) { + super(seed) + this.seed = seed + + this.amplitude = amplitude + this.period = period + this.lacunarity = lacunarity + this.octaves = octaves + this.persistence = persistence + } + + getNoise (zx, zy) { + let x = zx / this.period + let y = zy / this.period + + let amp = 1.0 + let max = 1.0 + let sum = this.noise2D(x, y) + + let i = 0 + while (++i < this.octaves) { + x *= this.lacunarity + y *= this.lacunarity + amp *= this.persistence + max += amp + sum += this.noise2D(x, y) * amp + } + + return (sum / max) * this.amplitude + } +} + +const canvas = document.createElement('canvas') +const ctx = canvas.getContext('2d') +document.body.appendChild(canvas) + +canvas.width = window.innerWidth +canvas.height = window.innerHeight + +const ChunkSize = 16 +const TileSize = 32 +const TotalSize = ChunkSize * TileSize + +class Tile { + constructor (name, color = '#000', image, size = TileSize) { + this.name = name + this.size = size + this.color = color + this.image = image + } + + // Render tile at precise render coordinates on the canvas, no relativity + draw (r, x, y, s = 1) { + let scaledSize = this.size / s + if (this.image) { + if (this.image.tiles && this.index != null) { + return this.image.draw(r, this.index, x, y, scaledSize, scaledSize) + } + + if (this.image.src) { + return r.drawImage(this.image, x, y, scaledSize, scaledSize) + } + + return this.image.draw(r, x, y, scaledSize, scaledSize) + } + + r.fillStyle = this.color + r.fillRect(x, y, scaledSize, scaledSize) + } +} + +class Chunk { + constructor (x, y) { + this.x = x + this.y = y + this.tiles = {} + } + + getTile (x, y) { + if (x > ChunkSize || y > ChunkSize || x < 0 || y < 0) throw new Error('Out Of Bounds!') + return this.getTilei(Chunk.ptoi(x, y)) + } + + getTilei (i) { + if (i < 0 || i > ChunkSize * ChunkSize) return null + return this.tiles[i] + } + + setTile (x, y, j) { + if (x > ChunkSize || y > ChunkSize || x < 0 || y < 0) throw new Error('Out Of Bounds!') + this.setTilei(Chunk.ptoi(x, y), j) + } + + setTilei (i, j) { + if (i < 0 || i > ChunkSize * ChunkSize) throw new Error('Out Of Bounds!') + this.tiles[i] = j + } + + // Index -> 2D Position + static itop (i) { + return { x: Math.floor(i % ChunkSize), y: Math.floor(i / ChunkSize) } + } + + // 2D Position -> Index + static ptoi (x, y) { + return x + y * ChunkSize + } + + // @r : canvas context + // @c : chunk + // @o : offset + // @s : scale + static render (r, c, o = { x: 0, y: 0 }, s = 1) { + if (!c) return null + for (let i in c.tiles) { + let tile = c.tiles[i] + if (tile == null || !(tile instanceof Tile)) continue + let pos = Chunk.itop(i) + tile.draw(r, + Math.floor(((pos.x * TileSize / s) + (c.x * TotalSize / s)) + o.x), + Math.floor(((pos.y * TileSize / s) + (c.y * TotalSize / s)) + o.y), + s + ) + } + } +} + +class ChunkWorld { + constructor () { + this.chunks = [] + } + + static generateTerrain (c) { + for (let i = 0; i < 16; i++) { + for (let j = 0; j < 16; j++) { + let h = noise.getNoise(i + (c.x * ChunkSize), j + (c.y * ChunkSize)) + let tile = 0 + if (h > noise.amplitude * 0.5) { + tile = 3 + } else if (h > noise.amplitude * 0.3) { + tile = 2 + } else if (h > noise.amplitude * 0.2) { + tile = 1 + } + c.setTile(i, j, tiles[tile]) + } + } + return c + } + + static getPositionsInViewport (pos, scale) { + // Calculate how many chunks could theoretically fit in this viewport + let swidth = Math.ceil(canvas.width / (TotalSize / scale)) + 1 + let sheight = Math.ceil(canvas.height / (TotalSize / scale)) + 1 + let positions = [] + + // Loop through the theoretical positions + for (let px = 0; px < swidth; px++) { + for (let py = 0; py < sheight; py++) { + let cx = (px * (TotalSize / scale)) - pos.x + let cy = (py * (TotalSize / scale)) - pos.y + + positions.push({ + x: Math.floor(cx / (TotalSize / scale)), + y: Math.floor(cy / (TotalSize / scale)) + }) + } + } + + return positions + } + + update (pos, scale) { + let positions = ChunkWorld.getPositionsInViewport(pos, scale) + for (let p in positions) { + let cpos = positions[p] + let found = false + for (let i in this.chunks) { + let chunk = this.chunks[i] + if (chunk.x == cpos.x && chunk.y == cpos.y) { + found = true + break + } + } + + if (!found) { + let chunk = new Chunk(cpos.x, cpos.y) + ChunkWorld.generateTerrain(chunk) + this.chunks.push(chunk) + break + } + } + } + + render (r, o, s) { + for (let i in this.chunks) { + let chunk = this.chunks[i] + let scaledSize = TotalSize / s + // Convert to chunk-space coordinates + let localX = Math.floor(cursor.x / scaledSize) * -1 + let localY = Math.floor(cursor.y / scaledSize) * -1 + let aw = Math.ceil(canvas.width / scaledSize) + 1 + let ah = Math.ceil(canvas.height / scaledSize) + + if (chunk.x <= localX + aw && chunk.x + 1 >= localX && + chunk.y <= localY + ah && chunk.y + 1 >= localY) { + Chunk.render(r, chunk, o, s) + } + } + } +} + +let world = new ChunkWorld() +let input = new Input(canvas) +let noise = new BetterNoise(1, /*amplitude = */15, /*persistence = */0.4, /*octaves = */5, /*period = */120, /*lacunarity = */2) +let zoom = 2 + +let tiles = [ + new Tile('water', '#028fdb'), + new Tile('sand', '#cec673'), + new Tile('grass', '#007c1f'), + new Tile('stone', '#515151') +] + +let cursor = {x: 0, y: 0} + +function render () { + ctx.fillStyle = '#00aaff' + ctx.fillRect(0, 0, canvas.width, canvas.height) + world.render(ctx, cursor, zoom) +} + +function update (dt) { + if (input.isDown('w')) { + cursor.y += 6 + } else if (input.isDown('s')) { + cursor.y -= 6 + } + + if (input.isDown('a')) { + cursor.x += 6 + } else if (input.isDown('d')) { + cursor.x -= 6 + } + + world.update(cursor, zoom) + input.update() +} + +function gameLoop () { + requestAnimationFrame(gameLoop) + update() + render() +} + +window.addEventListener('resize', function () { + canvas.width = window.innerWidth + canvas.height = window.innerHeight +}) + +gameLoop() diff --git a/src/input.js b/src/input.js new file mode 100644 index 0000000..e6f0417 --- /dev/null +++ b/src/input.js @@ -0,0 +1,222 @@ + +const specialKeyMap = { + 'backspace': 8, + 'tab': 9, + 'enter': 13, + 'shift': 16, + 'ctrl': 17, + 'alt': 18, + 'pausebreak': 19, + 'capslock': 20, + 'escape': 27, + 'pgup': 33, + 'pgdown': 34, + 'end': 35, + 'home': 36, + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, + 'insert': 45, + 'delete': 46, + 'left-window': 91, + 'right-window': 92, + 'select': 93, + 'numpad0': 96, + 'numpad1': 97, + 'numpad2': 98, + 'numpad3': 99, + 'numpad4': 100, + 'numpad5': 101, + 'numpad6': 102, + 'numpad7': 103, + 'numpad8': 104, + 'numpad9': 105, + 'multiply': 106, + 'add': 107, + 'subtract': 109, + 'decimal': 110, + 'divide': 111, + 'f1': 112, + 'f2': 113, + 'f3': 114, + 'f4': 115, + 'f5': 116, + 'f6': 117, + 'f7': 118, + 'f8': 119, + 'f9': 120, + 'f10': 121, + 'f11': 122, + 'f12': 123, + 'numlock': 144, + 'scrolllock': 145, + 'semi-colon': 186, + 'equals': 187, + 'comma': 188, + 'dash': 189, + 'period': 190, + 'fwdslash': 191, + 'grave': 192, + 'open-bracket': 219, + 'bkslash': 220, + 'close-braket': 221, + 'single-quote': 222 +} + +class Input { + constructor (canvas) { + this.keyList = {} + this.previousKeyList = {} + this.canvas = canvas + + this.mouse = { + pos: { x: 0, y: 0 }, + frame: { x: 0, y: 0 }, + previous: { x: 0, y: 0 } + } + + window.addEventListener('keydown', (e) => this.keyDown(e), false) + window.addEventListener('keyup', (e) => this.keyUp(e), false) + + canvas.addEventListener('mousemove', (e) => { + let x + let y + + if (e.changedTouches) { + let touch = e.changedTouches[0] + if (touch) { + e.pageX = touch.pageX + e.pageY = touch.pageY + } + } + + if (e.pageX || e.pageY) { + x = e.pageX + y = e.pageY + } else { + x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft + y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop + } + + x -= canvas.offsetLeft + y -= canvas.offsetTop + + this.mouse.frame.x = x + this.mouse.frame.y = y + }, false) + + canvas.addEventListener('mousedown', (e) => { + e.preventDefault() + this.mouse['btn' + e.button] = true + }) + + canvas.addEventListener('mouseup', (e) => { + e.preventDefault() + this.mouse['btn' + e.button] = false + }) + + canvas.addEventListener('contextmenu', (e) => { + e.preventDefault() + }) + } + + get mousePos () { + return this.mouse.pos + } + + get mouseMoved () { + return (this.mouse.pos.x !== this.mouse.previous.x || + this.mouse.pos.y !== this.mouse.previous.y) + } + + get mouseOffset () { + return { + x: this.mouse.previous.x - this.mouse.pos.x, + y: this.mouse.previous.y - this.mouse.pos.y + } + } + + toggleKey (keyCode, on) { + // Find key in special key list + let key = null + for (let k in specialKeyMap) { + let val = specialKeyMap[k] + if (keyCode === val) { + key = k + break + } + } + + // Use fromCharCode + if (!key) { + key = String.fromCharCode(keyCode).toLowerCase() + } + + this.keyList[key] = (on === true) + } + + keyDown (e) { + let keycode + + if (window.event) { + keycode = window.event.keyCode + } else if (e) { + keycode = e.which + } + + this.toggleKey(keycode, true) + } + + keyUp (e) { + let keycode + + if (window.event) { + keycode = window.event.keyCode + } else if (e) { + keycode = e.which + } + + this.toggleKey(keycode, false) + } + + down (key) { + return this.keyList[key] != null ? this.keyList[key] : false + } + + downLast (key) { + return this.previousKeyList[key] != null ? this.previousKeyList[key] : false + } + + isDown (key) { + return this.down(key) && this.downLast(key) + } + + isUp (key) { + return !this.isDown(key) + } + + isPressed (key) { + return this.down(key) === true && this.downLast(key) === false + } + + update () { + this.previousKeyList = {} + for (let k in this.keyList) { + if (this.keyList[k] === true) { + this.previousKeyList[k] = true + } + } + + // Mouse positions in the previous frame + this.mouse.previous.x = this.mouse.pos.x + this.mouse.previous.y = this.mouse.pos.y + + // Mouse positions in the current frame + // Convert to OpenGL coordinate system + this.mouse.pos.x = this.mouse.frame.x / this.canvas.width * 2 - 1 + this.mouse.pos.y = this.mouse.frame.y / this.canvas.height * 2 - 1 + } +} + +export default Input diff --git a/src/resource.js b/src/resource.js new file mode 100644 index 0000000..b10211c --- /dev/null +++ b/src/resource.js @@ -0,0 +1,101 @@ +/* global XMLHttpRequest, Image */ + +let imgCache = {} + +function powerOfTwo (n) { + return n && (n & (n - 1)) === 0 +} + +function GET (url, istext) { + return new Promise((resolve, reject) => { + var xmlHttp = new XMLHttpRequest() + + xmlHttp.onreadystatechange = function () { + if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { + resolve(xmlHttp.responseText) + } else if (xmlHttp.readyState === 4 && xmlHttp.status >= 400) { + let err = new Error(xmlHttp.status) + err.request = xmlHttp + reject(err) + } + } + + xmlHttp.open('GET', url, true) + istext && (xmlHttp.responseType = 'text') + xmlHttp.send(null) + }) +} + +function smartGET (data) { + if (typeof data === 'string') { + data = { + url: data, + type: 'text' + } + } + + if (!data.type) data.type = 'text' + let istext = (data.type !== 'image' && data.type !== 'file') + + let url = data.url + if (!url) throw new Error('URL is required!') + + if (data.type === 'json') { + return new Promise((resolve, reject) => { + GET(url).then((dtext) => { + try { + let jsonp = JSON.parse(dtext) + return resolve(jsonp) + } catch (e) { + reject(e) + } + }, reject) + }) + } + + return GET(data.url, istext) +} + +function loadImage (url, nowarn = false) { + url = '/assets/images/' + url + + // Ensure we don't load a texture multiple times + if (imgCache[url]) return new Promise((resolve, reject) => resolve(imgCache[url])) + + return new Promise((resolve, reject) => { + let img = new Image() + + img.onload = function () { + // Friendly warnings + if ((!powerOfTwo(img.width) || !powerOfTwo(img.height)) && !nowarn) { + console.warn(`warn: image ${url} does not have dimensions that are powers of two. 16x16 to 128x128 recommended.`) + } + + if ((img.width / img.height) !== 1 && !nowarn) { + console.warn(`warn: image ${url} does not have an aspect ratio of 1 to 1.`) + } + + imgCache[url] = img + resolve(img) + } + + img.onerror = function (e) { + reject(e) + } + + img.src = url + }) +} + +function imageToSampler (img) { + let canvas = document.createElement('canvas') + let ctx = canvas.getContext('2d') + canvas.width = img.width + canvas.height = img.height + ctx.drawImage(img, 0, 0, img.width, img.height) + return function (x, y) { + return ctx.getImageData(x, y, 1, 1).data + } +} + +export default { GET: smartGET, imageToSampler, loadImage } diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..4325d1a --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,32 @@ +const path = require('path') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const CopyWebpackPlugin = require('copy-webpack-plugin') + +module.exports = (env) => { + return { + entry: './src/index.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'app.js' + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /(node_modules)/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'] + } + } + } + ] + }, + devtool: env.mode === 'development' ? 'inline-source-map' : '', + plugins: [ + new HtmlWebpackPlugin({ template: 'index.html' }), + new CopyWebpackPlugin([ { from: 'static' } ]) + ] + } +}