// images:
// https://stuepfnick.macbay.net/js/tiles/test/assets/buch-outdoor-transparent.png
// https://stuepfnick.macbay.net/js/tiles/test/assets/cloudtest.png
// https://stuepfnick.macbay.net/js/tiles/test/config.js
/*
* All important constants in one file
* © Created by Stuepfnick
*/
// Config object as constant with frozen properties
const Config = Object.freeze ({
// Tiles Source image URL
tileMapSource: 'https://i.sstatic.net/kP1lm.png',
tileMapColumns: 6,
// Tile Size and Spacing in Pixels
tileWidth: 16,
tileHeight: 16,
tileSpacing: 0,
// MapSize in Tiles
mapWidth: 64,
mapHeight: 64,
groundColor: '#344A61',
/*
* 1–––2
* | |
* 4–––8
*/
marchingIndices: [
[ // Dirt + 36
9, // 0000 - 0
50, // 0001 - 1
49, // 0010 - 2
[37, 38, 39, 40], // 0011 - 3 (2+1)
44, // 0100 - 4
[42, 48, 54, 60], // 0101 - 5 (4+1)
[56, 61], // 0110 - 6 (4+2)
36, // 0111 - 7 (4+2+1)
43, // 1000 - 8 (8)
[55, 62], // 1001 - 9 (8+1)
[47, 53, 59, 65], // 1010 - 10 (8+2)
41, // 1011 - 11 (8+2+1)
[67, 68, 69, 70], // 1100 - 12 (8+4)
66, // 1101 - 13 (8+4+1)
71, // 1110 - 14 (8+4+2)
[45, 46, 45, 51, 45, 52, 45, 57, 45, 58, 45, 63, 45, 64, 45, 45, 45]// 1111 - 15 (8+4+2+1 )
],
[ // Grass
9, // 0000 - 0
14, // 0001 - 1
13, // 0010 - 2
[1, 2, 3, 4], // 0011 - 3 (2+1)
8, // 0100 - 4
[6, 12, 18, 24], // 0101 - 5 (4+1)
[20, 25], // 0110 - 6 (4+2)
0, // 0111 - 7 (4+2+1)
7, // 1000 - 8 (8)
[19, 26], // 1001 - 9 (8+1)
[11, 17, 23, 29], // 1010 - 10 (8+2)
5, // 1011 - 11 (8+2+1)
[31, 32, 33, 34], // 1100 - 12 (8+4)
30, // 1101 - 13 (8+4+1)
35, // 1110 - 14 (8+4+2)
[15, 10, 15, 16, 15, 21, 15, 22, 15, 27, 15, 28, 15, 15, 15, 15, 15]// 1111 - 15 (8+4+2+1 )
]
]
});
// view-source:https://stuepfnick.macbay.net/js/tiles/test/input.js
/*
* All important main routines for Tiles!
* © Created by Stuepfnick
*/
var isMouseDown, dataSet = false;
var data = 0;
var scrollContainer = {};//document.getElementById('scroll-container');
var drawCheckbox = {};
initInput = function() {
//var canvas = document.getElementById('demo');
scrollContainer = document.getElementById('scroll-container');
drawCheckbox = document.getElementById('drawCheckbox');
//Touch Handler
canvas.addEventListener('touchstart', touchHandler);
canvas.addEventListener('touchmove', touchHandler);
canvas.addEventListener('touchend', mouseUp);
canvas.addEventListener('touchcancel', mouseUp);
//Mouse Handler
canvas.addEventListener('mousedown', mouseDown);
canvas.addEventListener('mousemove', mouseHandler);
window.addEventListener('mouseup', mouseUp)
scrollContainer.addEventListener('wheel', wheelHandler);
scrollContainer.addEventListener('scroll', scrollHandler);
};
touchHandler = function(e) {
if (drawCheckbox.checked) {
if (e.touches) {
e.preventDefault();
var touchX = e.touches[0].pageX - canvas.offsetLeft + scrollContainer.scrollLeft;
var touchY = e.touches[0].pageY - canvas.offsetTop + scrollContainer.scrollTop;
coordinatesHandler(touchX, touchY);
};
}
};
mouseHandler = function(e) {
if (isMouseDown) {
coordinatesHandler(e.offsetX, e.offsetY);
}
};
mouseDown = function(e) {
isMouseDown = true;
mouseHandler(e);
};
mouseUp = function(e) {
isMouseDown = false;
dataSet = false;
};
coordinatesHandler = function(x, y) {
var tileX = Math.round(x / tiles.width);
var tileY = Math.round(y / tiles.height);
let numberOfLayers = Config.marchingIndices.length;
if (!dataSet) {
data = map.getRaw(tileX, tileY) + 1;
if (data > numberOfLayers) {
data = 0;
}
dataSet = true;
};
if (data != map.getRaw(tileX, tileY)) {
map.setRaw(data, tileX, tileY);
Game.regenerateMap(tileX - 1, tileY - 1, tileX, tileY, numberOfLayers);
};
};
wheelHandler = function(e) {
e.preventDefault();
scrollContainer.scrollLeft += e.deltaX;
scrollContainer.scrollTop += e.deltaY;
};
scrollHandler = function(e) {
//console.log(scrollContainer.scrollLeft + ', ' + scrollContainer.scrollTop)
};
// view-source:https://stuepfnick.macbay.net/js/tiles/test/tiles.js
/*
* All important main routines for Tiles!
* © Created by Stuepfnick
*/
// Game Object (will move some of the values below in it eventually)
var Game = {}
// var Game = {
// canvas: {},
// ctx: {},
// …
// };
// Temp: Some global objects and values, will move some of them to the Game object eventually
var canvas = {}; // will become reference to Main Canvas
var ctx = {}; // wil become referene to '2d' Context of Main Canvas
var lastTimeStamp = 0; // need a permanent variable to store the last time stamp
var speed = 60|0; // Speed in pixels per second?
// Temp: For testing purposes only: moving coordinateX and bool for the moving direction
var reverse = false, cX = 0;
var clouds = {};
// Generate map object and fill in some values from Config
var map = {
cols: Config.mapWidth,
rows: Config.mapHeight,
rawData: [],
getRaw: function (col, row) {
return this.rawData[row * this.cols + col]
},
setRaw: function (data, col, row) {
// Get index using col & raw
let idx = (row * this.cols + col)|0; // force to int
// Check if data is boolean and in bounds of rawData
if (typeof(data) === 'number' && idx < this.rawData.length) {
this.rawData[idx] = data;
} else { // Log the error
console.log('Error: map.setRaw(' + data + ', ' + col + ', ' + row + ')');
};
},
layers: [],
getTile: function (layer, col, row) {
return this.layers[layer][row * this.cols + col]
},
setTile: function (layer, tile, col, row) {
// Get index using col & raw
let idx = (row * this.cols + col)|0; // force to int
// Check if data is number and in bounds of rawData
if (typeof(tile) === 'number' && idx < this.layers[layer].length) {
this.layers[layer][idx] = tile|0; //force to int
} else { // Log the error
console.log('Error: map.setTile(' + tile + ', ' + col + ', ' + row + ')');
};
}
};
// Generate tiles object and fill in some values from Config
var tiles = {
atlas: {},
width: Config.tileWidth,
height: Config.tileHeight,
spacing: Config.tileSpacing,
cols: Config.tileMapColumns,
ground: Config.groundIndex,
marching: Config.marchingIndices
};
// Here everything starts
window.onload = function() {
console.log('window.onload');
clouds = new Image();
clouds.src = 'https://i.sstatic.net/RKfb4.png'
// Create and load the Tile Atlas
tiles.atlas = new Image();
tiles.atlas.src = Config.tileMapSource;
// Need to wait for the image to fully load, before Game can be initialized and map can be drawn
tiles.atlas.onload = function() {
Game.init();
};
};
// After everything is loaded, initalize the Game
Game.init = function() {
console.log('Game.init');
let numberOfLayers = Config.marchingIndices.length;
console.log('layers: ' + numberOfLayers);
// Get Canvas and Context
canvas = document.getElementById('myCanvas');
ctx = canvas.getContext('2d');
// Generate Raw Data & Map
this.generateRawData(numberOfLayers);
this.generateMap(numberOfLayers);
initInput();
initStorage();
// After Map Generation is finished, begin draw loop
// CAUTION: needs to be bound to the Game object (.bind(Game)), as it is not called as a regular function!
console.log('Begin draw …');
// requestAnimationFrame lets the browser handle the timer for the draw loop, so this is recommended over a timer
requestAnimationFrame(this.draw);
};
// Main draw loop
Game.draw = function(timeStamp) {
// Calculate delta from timeStamp and lastTimeStamp
var delta = (timeStamp - lastTimeStamp) / 1000.0;
// Clear the Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw the Tile Atlas itself for testing purposes
//ctx.drawImage(tiles.atlas, cX, 30)
// Draw the Offscreen Canvas, which contains the map
ctx.drawImage(this.offCanvas, 0, 0);//-cX, 0);
// draw clouds
drawClouds = function(targetX, targetY) {
ctx.drawImage(
clouds, // source image
0, // source X
0, // source Y
1024, // source Width
1024, // source Heigth
targetX, // target X
targetY, // target Y
2048, // target width
2048 // target heigth
);
}
let cloudX = cX - scrollContainer.scrollLeft * 2;
let cloudY = - scrollContainer.scrollTop * 2;
drawClouds(cloudX, cloudY);
drawClouds(cX < 0 ? cloudX + 2048 : cloudX - 2048, cloudY);
// temp: A simple query to move the clouds
cX += delta * speed;
if (cX > 2048) {
cX = -2048;
}
// Store the actual timeStamp in lastTimeStamp
lastTimeStamp = timeStamp;
// Request next animation frame from the Browser with calling this method again …
requestAnimationFrame(Game.draw);
// Bind the whole function to the Game object, so it is not needed to bind
}.bind(Game);
// Function to generate a boolean array that contains the raw data needed for the Marching Cubes
Game.generateRawData = function(numberOfLayers = Config.marchingIndices.length) {
// Needs to have 1 extra column and row! (so I use <= instead of <)
for (let y = 0; y <= map.rows; y++) {
for (let x = 0; x <= map.cols; x++) {
// Add a random Boolean to rawData
map.rawData.push(((Math.random()* numberOfLayers) + 0.5 | 0));
}
}
};
// Function to Generate Map Array and draw it (temporary for testing purposes)
Game.generateMap = function(numberOfLayers = Config.marchingIndices.length) {
// Create an Offscreen Canvas with the Size of the Map and get the '2d' Context
this.offCanvas = createOffscreenCanvas(map.cols * tiles.width, map.rows * tiles.height);
var tCtx = this.offCanvas.getContext('2d');
console.log('Game.generateMap offCanvas created: ' + this.offCanvas);
// Temporary
tCtx.fillStyle = Config.groundColor;
tCtx.fillRect(0, 0, map.cols * tiles.width, map.rows * tiles.height);
// Loop for creating a map with the needed Size and …
for (let layer = 0; layer < numberOfLayers; layer++) {
map.layers.push([]);
for (let y = 0; y < map.rows; y++) {
for (let x = 0; x < map.cols; x++) {
// Append the tile to the layer array of the Map*/
map.layers[layer].push(this.drawMarchingTile(tCtx, layer, x, y));
}
}
}
console.log('map generation finished!');
};
// Not simply a redraw, also sets the tiles (NOTE: should add a pure redraw too)
Game.regenerateMap = function(startX = 0, startY = 0, endX = map.cols - 1, endY = map.rows - 1, numberOfLayers = Config.marchingIndices.length) {
// Get Context from Offscreen Canvas
var tCtx = this.offCanvas.getContext('2d');
// Check if parameters are there and numbers (otherwise autofill -> can be done in function signature)
// if (typeof(numberOfLayers) !== 'number') {numberOfLayers = Config.marchingIndices.length;};
// if (typeof(startX) !== 'number') {startX = 0;};
// if (typeof(startY) !== 'number') {startY = 0;};
// if (typeof(endX) !== 'number') {endX = map.cols-1;};
// if (typeof(endY) !== 'number') {endY = map.rows-1;};
// Cap the values and set to int, although map.setTile does a check itself
startX = Math.max(startX|0, 0);
startY = Math.max(startY|0, 0);
endX = Math.min(endX|0, map.cols - 1);
endY = Math.min(endY|0, map.rows - 1);
console.log('regenerateMap: ' + startX + '-' + (endX) + ', ' + startY + '-' + (endY));
// Redraw Background color
let rwidth = (endX - startX + 1) * tiles.width;
let rheight = (endY - startY + 1) * tiles.height;
//tCtx.fillStyle = Config.groundColor;
tCtx.fillRect(startX * tiles.width, startY * tiles.height, rwidth, rheight);
// Loop through the part of the map that needs to be replaced
for (let layer = 0; layer < numberOfLayers; layer++) {
for (let y = startY; y <= endY; y++) {
for (let x = startX; x <= endX; x++) {
// set the tile at x, y
map.setTile(layer, this.drawMarchingTile(tCtx, layer, x, y), x, y);
}
}
}
};
Game.drawMarchingTile = function(tCtx, layer, x, y) {
// Pass the values of rawData to marching Squares and get appropriate index, if same layer
let mIndex = this.mSquares(map.getRaw(x, y) == layer, map.getRaw(x + 1 , y) == layer, map.getRaw(x, y + 1) == layer, map.getRaw(x+1, y+1) == layer);
// Set the tile number to result of MS using tiles.marching Index
let tile = tiles.marching[layer][mIndex];
// if tile is an array, get a random one
if (typeof(tile) === 'object' && Array.isArray(tile)) {
tile = tile[(Math.random() * (tile.length - 1)) + 0.5|0];
};
// if tile is no number, fill in -1, so it draws nothing (transparent) at that place of the map
if (typeof(tile) !== 'number') {
tile = -1;
}
// force tile to int
tile |= 0;
// calculate sourceX and sourceY, dependent on tileSize and Spacing
let sX = tiles.spacing + (tiles.width + tiles.spacing) * (tile % tiles.cols);
let sY = tiles.spacing + (tiles.height + tiles.spacing) * Math.floor(tile/tiles.cols);
// If the actual tile is something (>=0) draw a tile
if (tile >= 0) {
// Actually draw the tile
tCtx.drawImage(
tiles.atlas, // source image
sX, // source X
sY, // source Y
tiles.width, // source Width
tiles.height, // source Heigth
x * tiles.width, // target X
y * tiles.height, // target Y
tiles.width, // target width
tiles.height // target heigth
);
}
return tile;
};
Game.mSquares = function(up_left, up_right, down_left, down_right) {
/* We label the corners with those values:
*
* 1–––2
* | |
* 4–––8
*/
let state = 0|0;
// Determine which state we are in
if (up_left){state |= 1;}
if (up_right){state |= 2;}
if (down_left){state |= 4;}
if (down_right){state |= 8;}
return state;
};
// Function for creating an Offscreen Canvas with a variable size
createOffscreenCanvas = function(width, height) {
console.log('createOffscreenCanvas');
var c = document.createElement('canvas');
c.width = width;
c.height = height;
return c;
};
// view-source:https://stuepfnick.macbay.net/js/tiles/test/storage.js
/*
* All important main routines for Tiles!
* © Created by Stuepfnick
*/
var textarea = {};
var outButton = {};
var inButton = {};
initStorage = function() {
textarea = document.getElementById('mapstorage');
outButton = document.getElementById('exportButton');
inButton = document.getElementById('importButton');
outButton.onclick = exportMap;
inButton.onclick = importMap;
}
const exportMap = function() {
textarea.value = JSON.stringify(map.rawData);
}
const importMap = function() {
map.rawData = JSON.parse(textarea.value);
Game.regenerateMap();
}
* {
padding: 0;
margin: 0;
text-align: center;
color: #7df;
background: #222;
}
canvas {
background: #000;
border:1px solid #000;
}
#scroll-container {
width: 800px;
height: 600px;
overflow: auto;
margin: auto;
}
<div id="scroll-container">
<canvas id="myCanvas" width="1024" height="1024">
Your browser does not support the HTML canvas tag.
</canvas>
</div>
<div id="scroll-container">
<canvas id="myCanvas" width="1024" height="1024">
Your browser does not support the HTML canvas tag.
</canvas>
</div>
<input type="checkbox" id = "drawCheckbox" value = "draw">
<label for="drawCheckbox">draw</label>
<p>
<textarea id = "mapstorage" rows="4" cols="50">
</textarea>
</p>
<input id="exportButton" type="button" value="export"/>
<input id="importButton" type="button" value="import"/>