Skip to content

Instantly share code, notes, and snippets.

@sirisian
Created April 24, 2025 23:41
Show Gist options
  • Save sirisian/4abcf9fe0666c3a545f4e41c9512809d to your computer and use it in GitHub Desktop.
Save sirisian/4abcf9fe0666c3a545f4e41c9512809d to your computer and use it in GitHub Desktop.
roguelike map gen
<!doctype html>
<html lang="en">
<head>
<title>Roguelike</title>
<script language="javascript" src="Vector234.js"></script>
<script type="text/javascript">
"use strict";
var cellSize = 24;
var fps = 0;
var canvas = null;
var context = null;
var images = [];
const mapSize = 512;
const map = new Uint32Array(mapSize * mapSize);
var mousePosition = new Vector3();
var mousePositionStart = new Vector3();
var mouseDown = false;
var renderImageData;
let cameraPosition = new Vector2(0, 0);
let position = new Vector2(256, 256);
let velocity = new Vector2();
let entities = [];
async function KeyPress(...letters) {
return new Promise(resolve => {
const keyDown = e => {
if (letters.includes(e.key)) {
resolve(e.key);
document.removeEventListener('keydown', keyDown, true);
}
};
document.addEventListener('keydown', keyDown, true);
});
}
// Helper noise functions
function Interpolate(a, b, x) {
let ft = x * 3.1415927;
let f = (1 - Math.cos(ft)) * 0.5;
return a * (1 - f) + b * f;
}
async function ValueNoise(size, startOctave, endOctave, persistence, smoothAmount, postprocess = null) {
const data = new Float32Array(new SharedArrayBuffer(size**2 * 4));
// We're storing the random data samples in a quadtree
// octave 0 is the whole area
// octave 1 is the area divided by 4
// octave n is the previous octave with each area divided by 4
if (startOctave < 0 || endOctave < 0) {
throw new Error('Negative octaves have no meaning');
}
// It is not useful for an octave level to go to a detail level smaller than one pixel. This is usually a user error or they entered the wrong size
if (endOctave > Math.log2(size)) {
throw new Error('Octaves cannot be smaller than one pixel');
}
// We need 4 points to do bilinear interpolation from for the noise generation for each octave.
// This is the summation of 2**(i + 1) - 2**i + 1 which represents the
// number of corners per depth of a quadtree. So depth zero has 4 and depth one has 9.
const nodeCount = 1 / 3 * (3 * (endOctave + 1) + 3 * 2**((endOctave + 1) + 2) + 2**(2 * (endOctave + 1) + 2) - 4) -
1 / 3 * (3 * startOctave + 3 * 2**(startOctave + 2) + 2**(2 * startOctave + 2) - 4);
const randomTree = new Float32Array(new SharedArrayBuffer(nodeCount * 4));
for (let i = 0; i < randomTree.length; ++i) {
randomTree[i] = Math.random();
}
// Make it tileable
for (let i = startOctave; i <= endOctave; ++i) {
const octaveSize = 2**(i + 1) - 2**i + 1;
const indexOffset = 1 / 3 * (3 * i + 3 * 2**(i + 2) + 2**(2 * i + 2) - 4) -
1 / 3 * (3 * startOctave + 3 * 2**(startOctave + 2) + 2**(2 * startOctave + 2) - 4);
for(let y = 0; y < octaveSize; ++y) {
randomTree[indexOffset + y * octaveSize] = randomTree[indexOffset + y * octaveSize + octaveSize - 1];
}
for(let x = 0; x < octaveSize; ++x) {
randomTree[indexOffset + x] = randomTree[indexOffset + (octaveSize - 1) * octaveSize + x];
}
}
let workers = [];
for (let workerIndex = 0; workerIndex < 16; ++workerIndex) {
workers.push(new Promise(resolve => {
var blobURL = URL.createObjectURL(new Blob([Interpolate.toString(), '(', function() {
onmessage = e => {
const { workerIndex, randomTree, data, size, startOctave, endOctave, persistence } = e.data;
let xOffset = (workerIndex % 4) * size / 4;
let yOffset = Math.floor(workerIndex / 4) * size / 4;
for(let y = yOffset; y < yOffset + size / 4; ++y) {
for(let x = xOffset; x < xOffset + size / 4; ++x) {
data[y * size + x] = 0;
for (let i = startOctave; i <= endOctave; ++i) {
const cellSize = size / (2**i);
const integerX = Math.floor(x / cellSize);
const integerY = Math.floor(y / cellSize);
const indexOffset = 1 / 3 * (3 * i + 3 * 2**(i + 2) + 2**(2 * i + 2) - 4) -
1 / 3 * (3 * startOctave + 3 * 2**(startOctave + 2) + 2**(2 * startOctave + 2) - 4);
const fractionalX = (x - integerX * cellSize) / cellSize;
const fractionalY = (y - integerY * cellSize) / cellSize;
const octaveSize = 2**(i + 1) - 2**i + 1;
const i1 = Interpolate(randomTree[indexOffset + integerY * octaveSize + integerX],
randomTree[indexOffset + integerY * octaveSize + integerX + 1],
fractionalX);
const i2 = Interpolate(randomTree[indexOffset + (integerY + 1) * octaveSize + integerX],
randomTree[indexOffset + (integerY + 1) * octaveSize + integerX + 1],
fractionalX);
data[y * size + x] += Interpolate(i1, i2, fractionalY) * persistence**(i - startOctave);
}
}
}
postMessage('done');
};
}.toString(), ')()'], { type: 'application/javascript' }));
const worker = new Worker(blobURL);
URL.revokeObjectURL(blobURL);
worker.postMessage({ workerIndex, randomTree, data, size, startOctave, endOctave, persistence }); // Starts the computation
worker.onmessage = e => {
worker.terminate();
resolve();
};
}));
}
await Promise.all(workers);
await Smooth(data, size, smoothAmount);
await Normalize(data, size, 0, 1);
if (postprocess) {
await postprocess(data);
}
return data;
}
async function Smooth(data, size, amount) {
return new Promise(resolve => {
var blobURL = URL.createObjectURL(new Blob(['(', function() {
onmessage = e => {
const { data, size, amount } = e.data;
for (let i = 0; i < amount; ++i) {
for (let y = 0; y < size; ++y) {
for(let x = 0; x < size; ++x) {
const xMinus1 = x == 0 ? size - 1 : x - 1;
const yMinus1 = y == 0 ? size - 1 : y - 1;
const xPlus1 = (x + 1) % size;
const yPlus1 = (y + 1) % size;
const corners = (data[yMinus1 * size + xMinus1] +
data[yMinus1 * size + xPlus1] +
data[yPlus1 * size + xPlus1] +
data[yPlus1 * size + xMinus1]) / 16.0;
const sides = (data[y * size + xMinus1] +
data[y * size + xPlus1] +
data[yMinus1 * size + x] +
data[yPlus1 * size + x]) / 8.0;
const center = data[y * size + x] / 4.0;
data[y * size + x] = corners + sides + center;
}
}
}
postMessage('done');
};
}.toString(), ')()'], { type: 'application/javascript' }));
const worker = new Worker(blobURL);
URL.revokeObjectURL(blobURL);
worker.postMessage({ data, size, amount }); // Starts the computation
worker.onmessage = e => {
worker.terminate();
resolve();
};
});
}
async function Normalize(data, size, minimum, maximum) {
return new Promise(resolve => {
var blobURL = URL.createObjectURL(new Blob(['(', function() {
onmessage = e => {
const { data, size, minimum, maximum } = e.data;
let min = Number.MAX_VALUE;
let max = -Number.MAX_VALUE;
// Calculate min and max range used to normalize with
for (let y = 0; y < size; ++y) {
for(let x = 0; x < size; ++x) {
min = Math.min(min, data[y * size + x]);
max = Math.max(max, data[y * size + x]);
}
}
// Normalize the range to 0 to 1
for (let y = 0; y < size; ++y) {
for(let x = 0; x < size; ++x) {
data[y * size + x] = (data[y * size + x] - min) / (max - min) * (maximum - minimum) + minimum;
}
}
postMessage('done');
};
}.toString(), ')()'], { type: 'application/javascript' }));
const worker = new Worker(blobURL);
URL.revokeObjectURL(blobURL);
worker.postMessage({ data, size, minimum, maximum }); // Starts the computation
worker.onmessage = e => {
worker.terminate();
resolve();
};
});
}
const textureNameToTextureViews = new Map();
const textures = [];
let textureIndexSequence = 0;
const textureDetailsToIndex = new Map();
const textureIndexToDetails = new Map();
const textureIndexToImage = new Map();
function AlphaBlend(bottomRed, bottomGreen, bottomBlue, bottomAlpha, topRed, topGreen, topBlue, topAlpha) {
topAlpha /= 255;
bottomAlpha /= 255;
return [
topRed * topAlpha + bottomRed * bottomAlpha * (1 - topAlpha),
topGreen * topAlpha + bottomGreen * bottomAlpha * (1 - topAlpha),
topBlue * topAlpha + bottomBlue * bottomAlpha * (1 - topAlpha),
topAlpha * 255 + bottomAlpha * 255 * (1 - topAlpha)
];
}
function RegisterTextureIndex(name, red, green, blue, alphaModifier = 1, backgroundRed = 0, backgroundGreen = 0, backgroundBlue = 0, backgroundAlpha = 0, fillRed = 0, fillGreen = 0, fillBlue = 0, fillAlpha = 0) {
let textureDetails = { name, red, green, blue, backgroundRed, backgroundGreen, backgroundBlue };
let key = JSON.stringify(textureDetails);
let textureIndex = textureDetailsToIndex.get(key);
if (textureIndex == undefined) {
textureIndex = textureIndexSequence++;
textureDetailsToIndex.set(key, textureIndex);
// Create a unique set of texture
const originalImageLines = textureNameToTextureViews.get(name);
const imageLines = [];
for (let y = 0; y < cellSize; ++y) {
const originalImageLine = originalImageLines[y];
const imageLine = new Uint8ClampedArray(cellSize * 4);
for (let x = 0; x < originalImageLine.length; ++x) {
const alpha = originalImageLine[x * 4 + 3];
if (originalImageLine[x * 4] == 0 && originalImageLine[x * 4 + 1] == 0 && originalImageLine[x * 4 + 2] == 0) {
// Outside pixels or opaque colored pixels, background blended with the color on top
const [r, g, b, a] = AlphaBlend(backgroundRed, backgroundGreen, backgroundBlue, backgroundAlpha, red, green, blue, alpha * alphaModifier);
imageLine[x * 4] = Math.round(r);
imageLine[x * 4 + 1] = Math.round(g);
imageLine[x * 4 + 2] = Math.round(b);
imageLine[x * 4 + 3] = Math.round(a);
} else if (originalImageLine[x * 4] == 0 && originalImageLine[x * 4 + 1] == 255 && originalImageLine[x * 4 + 2] == 0) {
// Inside pixels, background blended fill blended with the color on top
const [r, g, b, a] = AlphaBlend(backgroundRed, backgroundGreen, backgroundBlue, backgroundAlpha, fillRed, fillGreen, fillBlue, fillAlpha);
const [r2, g2, b2, a2] = AlphaBlend(r, g, b, a, red, green, blue, alpha * alphaModifier);
imageLine[x * 4] = Math.round(r2);
imageLine[x * 4 + 1] = Math.round(g2);
imageLine[x * 4 + 2] = Math.round(b2);
imageLine[x * 4 + 3] = Math.round(a2);
}
}
imageLines.push(imageLine);
}
textures.push(imageLines);
textureIndexToDetails.set(textureIndex, textureDetails);
// Turn the pixel data into an image that can be used with drawImage
const imageCanvas = document.createElement('canvas');
imageCanvas.width = cellSize;
imageCanvas.height = cellSize;
const imageCanvasContext = imageCanvas.getContext('2d');
const imageData = imageCanvasContext.getImageData(0, 0, cellSize, cellSize);
const pixels = imageData.data;
for (let y = 0; y < imageLines.length; ++y) {
const imageLine = imageLines[y];
pixels.set(imageLine, y * cellSize * 4);
}
imageCanvasContext.putImageData(imageData, 0, 0);
const image = new Image();
image.src = imageCanvas.toDataURL();
textureIndexToImage.set(textureIndex, image);
}
return textureIndex;
}
function GetTextureIndex(name, red, green, blue) {
let textureDetails = { name, red, green, blue };
let key = JSON.stringify(textureDetails);
return textureDetailsToIndex.get(key);
}
function SetGrid(x, y, name, red, green, blue, alphaModifier = 1, backgroundRed = 0, backgroundGreen = 0, backgroundBlue = 0, backgroundAlpha = 0, fillRed = 0, fillGreen = 0, fillBlue = 0, fillAlpha = 0) {
const textureIndex = RegisterTextureIndex(name, red, green, blue, alphaModifier, backgroundRed, backgroundGreen, backgroundBlue, backgroundAlpha, fillRed, fillGreen, fillBlue, fillAlpha);
map[y * mapSize + x] = textureIndex;
}
function GetGrid(x, y) {
const textureIndex = map[y * mapSize + x];
return textureIndexToDetails.get(textureIndex);
}
function WindowResize() {
canvas.width = document.body.offsetWidth;
canvas.height = document.body.offsetHeight;
renderImageData = context.createImageData(canvas.width, canvas.height);
}
function FloodFill(x, y, pixels) {
const isTargetColor = (x, y) => {
if (x < 0 || x >= cellSize || y < 0 || y >= cellSize) {
return false;
}
const pixelIndex = (y * cellSize + x) * 4;
return pixels[pixelIndex] == 0 && pixels[pixelIndex + 1] == 0 && pixels[pixelIndex + 2] == 0 && pixels[pixelIndex + 3] == 0;
};
if (!isTargetColor(x, y)) {
return;
}
const setColor = (x, y) => {
const pixelIndex = (y * cellSize + x) * 4;
pixels[pixelIndex] = 255;
pixels[pixelIndex + 1] = 0;
pixels[pixelIndex + 2] = 0;
};
setColor(x, y);
let queue = [{ x, y }];
while (queue.length != 0) {
const n = queue.shift();
if (isTargetColor(n.x - 1, n.y)) {
setColor(n.x - 1, n.y);
queue.push({ x: n.x - 1, y: n.y });
}
if (isTargetColor(n.x + 1, n.y)) {
setColor(n.x + 1, n.y);
queue.push({ x: n.x + 1, y: n.y });
}
if (isTargetColor(n.x, n.y - 1)) {
setColor(n.x, n.y - 1);
queue.push({ x: n.x, y: n.y - 1 });
}
if (isTargetColor(n.x, n.y + 1)) {
setColor(n.x, n.y + 1);
queue.push({ x: n.x, y: n.y + 1 });
}
}
}
// Grows a color into aliased pixels by 1 pixel. The algorithm is the dilation algorithm (from morphology) in two passes
function Grow(pixels, red, green) {
let pixelsChanged = false;
const isTargetColor = (x, y) => {
if (x < 0 || x >= cellSize || y < 0 || y >= cellSize) {
return false;
}
const pixelIndex = (y * cellSize + x) * 4;
return pixels[pixelIndex] == 0 && pixels[pixelIndex + 1] == 0 && pixels[pixelIndex + 2] == 0 && pixels[pixelIndex + 3] != 255;
};
// Set the pixels that are grown to blue temporarily
const setColor = (x, y) => {
const pixelIndex = (y * cellSize + x) * 4;
pixels[pixelIndex] = 0;
pixels[pixelIndex + 1] = 0;
pixels[pixelIndex + 2] = 255;
pixelsChanged = true;
};
for (let y = 0; y < cellSize; ++y) {
for (let x = 0; x < cellSize; ++x) {
const pixelIndex = (y * cellSize + x) * 4;
if (pixels[pixelIndex] == red && pixels[pixelIndex + 1] == green) {
if (isTargetColor(x - 1, y)) {
setColor(x - 1, y);
}
if (isTargetColor(x + 1, y)) {
setColor(x + 1, y);
}
if (isTargetColor(x, y - 1)) {
setColor(x, y - 1);
}
if (isTargetColor(x, y + 1)) {
setColor(x, y + 1);
}
}
}
}
// Set all the blue pixels to the input color
for (let y = 0; y < cellSize; ++y) {
for (let x = 0; x < cellSize; ++x) {
const pixelIndex = (y * cellSize + x) * 4;
if (pixels[pixelIndex + 2] == 255) {
pixels[pixelIndex] = red;
pixels[pixelIndex + 1] = green;
pixels[pixelIndex + 2] = 0;
}
}
}
return pixelsChanged;
}
function CanMoveTo(position, direction, tileType) {
const newPosition = position.Add(direction);
if (newPosition.x > 0 && newPosition.x < mapSize && newPosition.y > 0 && newPosition.y < mapSize) {
const cell = GetGrid(newPosition.x, newPosition.y);
if (cell.name == tileType) {
return true;
}
}
return false;
}
async function Update() {
const key = await KeyPress('a', 's', 'w', 'd');
velocity.x = (key == 'd' ? 1 : 0) - (key == 'a' ? 1 : 0);
velocity.y = (key == 's' ? 1 : 0) - (key == 'w' ? 1 : 0);
if (CanMoveTo(position, velocity, 'border-none')) {
position.ThisAdd(velocity);
UpdateCamera();
// Update entities
const directions = [
new Vector2(1, 0),
new Vector2(-1, 0),
new Vector2(0, 1),
new Vector2(0, -1)
];
for (const entity of entities) {
if (entity.hasOwnProperty('water')) {
if (Math.random() < 0.25) {
const direction = directions[Math.floor(Math.random() * 4)];
if (CanMoveTo(entity.position, direction, 'waves')) {
entity.position.ThisAdd(direction);
}
}
} else {
if (Math.random() < 0.25) {
const direction = directions[Math.floor(Math.random() * 4)];
if (CanMoveTo(entity.position, direction, 'border-none')) {
entity.position.ThisAdd(direction);
}
}
}
}
}
Update();
}
Update();
function UpdateCamera() {
const radius = canvas.height / 2;
const maxZoom = 1000;
const offsetX = Math.min((mousePosition.x - canvas.width / 2) / radius, 1) * maxZoom;
const offsetY = Math.min((mousePosition.y - canvas.height / 2) / radius, 1) * maxZoom;
cameraPosition.x = Math.max(0, Math.min(mapSize * cellSize - canvas.width, position.x * cellSize + cellSize / 2 - canvas.width / 2 + offsetX));
cameraPosition.y = Math.max(0, Math.min(mapSize * cellSize - canvas.height, position.y * cellSize + cellSize / 2 - canvas.height / 2 + offsetY));
}
function NoiseToCanvas(size, data) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const context = canvas.getContext('2d');
const imageData = context.getImageData(0, 0, size, size);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = data[i / 4] * 255;
pixels[i + 1] = data[i / 4] * 255;
pixels[i + 2] = data[i / 4] * 255;
pixels[i + 3] = 255;
}
context.putImageData(imageData, 0, 0);
return canvas;
}
function NoiseToBiomeCanvas(size, temperatureNoise, humidityNoise, altitudeNoise) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const context = canvas.getContext('2d');
for (let y = 0; y < size; ++y) {
for (let x = 0; x < size; ++x) {
const index = y * size + x;
const [biome, temperature, humidity, altitude] = LookupBiome(temperatureNoise[index], humidityNoise[index], altitudeNoise[index]);
context.fillStyle = BiomeToColor[biome];
context.fillRect(x, y, 1, 1);
}
}
return canvas;
}
let playerTextureIndex;
let temperatureCanvas;
let humidityCanvas;
let altitudeCanvas;
let biomeCanvas;
const seaLevel = 0.33;
let biomeLookup;
const Biome = Object.freeze({
Tundra: 0,
BorealForest: 1,
WoodlandOrShrubland: 2,
TemperateGrasslandOrColdDesert: 3,
TemperateRainforest: 4,
TemperateSeasonableForest: 5,
TropicalRainforest: 6,
TropicalSeasonalForestOrSavanna: 7,
SubtropicalDesert: 8,
Impossible: 255
});
const BiomeToColor = Object.freeze({
[Biome.Tundra]: '#94A9AD',
[Biome.BorealForest]: '#5B9051',
[Biome.WoodlandOrShrubland]: '#B47D01',
[Biome.TemperateGrasslandOrColdDesert]: '#937F2C',
[Biome.TemperateRainforest]: '#02536D',
[Biome.TemperateSeasonableForest]: '#288AA1',
[Biome.TropicalRainforest]: '#01522C',
[Biome.TropicalSeasonalForestOrSavanna]: '#98A722',
[Biome.SubtropicalDesert]: '#C97234'
});
const ColorToBiome = Object.freeze(Object.assign({}, ...Object.entries(BiomeToColor).map(([a,b]) => ({ [b]: a }))));
let biomeColors;
let biomeColorsWidth = 20;
// Whittaker biome-types
// https://en.wikipedia.org/wiki/Biome#/media/File:Climate_influence_on_terrestrial_biome.svg
function LookupBiome(temperature, humidity, altitude) {
// Temperature (0 - 1) is mapped to -10 C to 35 C average annual temperature, a range of 45 C.
// Humidity (0 - 1) is mapped to 0 to 450 cm annual precipitation
// Altitude (0 - 1) is mapped starting at seaLevel to 1,000 meters
// Weight temperature so the average is somewhat warm
temperature = temperature**0.4;
// Temperature decreases with altitude, 9.8 C per 1,000 meters, we just use 10 C here
const temperatureDrop = (Math.max(0, altitude - seaLevel) / (1 - seaLevel) * 10) / 45; // Divide by 45 C, the temperature range
temperature = Math.max(0, temperature - temperatureDrop);
const x = Math.round(temperature * 99);
let y = 99 - Math.round(humidity * 99);
let biome = biomeLookup[y * 100 + x];
// If the biome is impossible, the temperature does not allow for this humidity, clamp the humidity by search down for a maximum humidity
while (biome == Biome.Impossible) {
y++;
biome = biomeLookup[y * 100 + x];
}
return [biome, temperature, humidity, altitude];
}
function Interpolate3(a, b, x) {
let ft = x * 3.1415927;
let f = (1 - Math.cos(ft)) * 0.5;
const oneMinusF = 1 - f;
return [
a[0] * oneMinusF + b[0] * f,
a[1] * oneMinusF + b[1] * f,
a[2] * oneMinusF + b[2] * f
];
}
function InterpolateColor(topLeft, topRight, bottomLeft, bottomRight, fractionalX, fractionalY) {
const i1 = Interpolate3(topLeft, topRight, fractionalX);
const i2 = Interpolate3(bottomLeft, bottomRight, fractionalX);
return Interpolate3(i1, i2, fractionalY);
}
window.addEventListener('resize', WindowResize);
window.onload = async () => {
canvas = document.querySelector('canvas');
context = canvas.getContext('2d');
WindowResize();
biomeLookup = await new Promise(resolve => {
const biomeLookupImage = new Image();
biomeLookupImage.onload = () => {
const imageCanvas = document.createElement('canvas');
const imageContext = imageCanvas.getContext('2d');
imageContext.drawImage(biomeLookupImage, 0, 0);
const imageData = imageContext.getImageData(0, 0, biomeLookupImage.width, biomeLookupImage.height);
const pixels = imageData.data;
const lookup = new Uint8Array(biomeLookupImage.width * biomeLookupImage.height);
for (let y = 0; y < biomeLookupImage.height; ++y) {
for (let x = 0; x < biomeLookupImage.width; ++x) {
const index = (y * biomeLookupImage.width + x) * 4;
const [red, green, blue] = [pixels[index], pixels[index + 1], pixels[index + 2]];
const color = `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`.toUpperCase();
lookup[y * biomeLookupImage.width + x] = ColorToBiome[color];
}
}
resolve(lookup);
};
biomeLookupImage.src = 'border/biomelookup.png';
});
const temperatureNoise = await ValueNoise(mapSize, 2, 3, 0.5, 30);
const humidityNoise = await ValueNoise(mapSize, 2, 3, 0.5, 30);
const altitudeNoise = await ValueNoise(mapSize, 4, 7, 0.5, 20);
// For debug:
temperatureCanvas = NoiseToCanvas(mapSize, temperatureNoise);
humidityCanvas = NoiseToCanvas(mapSize, humidityNoise);
altitudeCanvas = NoiseToCanvas(mapSize, altitudeNoise);
biomeCanvas = NoiseToBiomeCanvas(mapSize, temperatureNoise, humidityNoise, altitudeNoise);
document.addEventListener('keydown', e => {
switch(e.which) {
}
}, true);
document.addEventListener('keyup', e => {
switch(e.which) {
}
}, true);
canvas.addEventListener('pointerdown', e => {
e.preventDefault();
if (e.which == 1) {
const position = new Vector3(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop);
mousePositionStart = new Vector3(mousePosition.x, mousePosition.y);
mouseDown = true;
}
}, true);
canvas.addEventListener('pointerup', e => {
e.preventDefault();
if (e.which == 1) {
mouseDown = false;
}
}, true);
canvas.addEventListener('pointermove', e => {
mousePosition.x = e.pageX;
mousePosition.y = e.pageY;
if (mouseDown) {
mousePositionStart.x = mousePosition.x;
mousePositionStart.y = mousePosition.y;
}
UpdateCamera();
}, true);
const imageCanvas = document.createElement('canvas');
imageCanvas.width = cellSize;
imageCanvas.height = cellSize;
const imageContext = imageCanvas.getContext('2d');
images = await Promise.all([
'waves.png',
'image-filter-hdr.png',
'border-none.png',
'border-left.png',
'border-right.png',
'border-top.png',
'border-bottom.png',
'border-horizontal.png',
'border-vertical.png',
'border-inside.png',
'border-outside.png',
'pine-tree.png',
'palm-tree.png',
'tree.png',
'emoticon-happy-outline.png',
'pig.png',
'sheep.png',
'emoticon-devil-outline.png',
'snowman.png',
'cactus.png',
'cat.png',
'penguin.png',
'cloud.png',
'fish.png'
].map((imagePath, index) => new Promise(resolve => {
const image = new Image();
image.onload = () => {
imageContext.clearRect(0, 0, cellSize, cellSize);
imageContext.drawImage(image, 0, 0);
const imageData = imageContext.getImageData(0, 0, cellSize, cellSize);
const pixels = imageData.data;
// Flood fill the corners and turn any (0, 0, 0, 0) pixels to (255, 0, 0, 0). This turns the outside red segmenting it from the inside. This does not include aliased pixels which may still be included later.
FloodFill(0, 0, pixels);
FloodFill(cellSize - 1, 0, pixels);
FloodFill(cellSize - 1, cellSize - 1, pixels);
FloodFill(0, cellSize - 1, pixels);
// Mark any pixel still (0, 0, 0, 0) as an inside pixel
for (let i = 0; i < pixels.length; i += 4) {
if (pixels[i] == 0 && pixels[i + 1] == 0 && pixels[i + 2] == 0 && pixels[i + 3] == 0) {
pixels[i] = 0;
pixels[i + 1] = 255;
pixels[i + 2] = 0;
}
}
// Take turns growing the outside and inside into the aliased pixels until no more pixels are found
// Note the | and not ||, we don't want to short-circuit
while (Grow(pixels, 255, 0) | Grow(pixels, 0, 255)) {
// Grow (255, 0, 0) 1 pixel in all directions into aliased pixels
// Grow (0, 255, 0) 1 pixel in all directions into aliased pixels
}
// Set the outside pixels back to (0, 0, 0)
for (let i = 0; i < pixels.length; i += 4) {
if (pixels[i] == 255 && pixels[i + 1] == 0 && pixels[i + 2] == 0) {
pixels[i] = 0;
pixels[i + 1] = 0;
pixels[i + 2] = 0;
}
}
imageContext.putImageData(imageData, 0, 0);
const imageViews = [];
for (let y = 0; y < cellSize; ++y) {
imageViews.push(new Uint8ClampedArray(pixels.buffer, y * cellSize * 4, cellSize * 4));
}
textureNameToTextureViews.set(imagePath.split('.')[0], imageViews);
resolve(imageViews);
};
image.src = `border/${imagePath}`;
})));
biomeColors = await new Promise(resolve => {
const image = new Image();
image.onload = () => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
resolve(context.getImageData(0, 0, image.width, image.height).data);
};
image.src = 'border/biomecolors2.png';
});
playerTextureIndex = RegisterTextureIndex('emoticon-happy-outline', 0, 0, 0, 1, 0, 0, 0, 0, 255, 255, 255, 255);
for (let y = 0; y < mapSize; ++y) {
for (let x = 0; x < mapSize; ++x) {
const index = y * mapSize + x;
const [biome, temperature, humidity, altitude] = LookupBiome(temperatureNoise[index], humidityNoise[index], altitudeNoise[index]);
let pineTree = 0;
let palmTree = 0;
let cactus = 0;
let snowman = 0;
let tree = 0;
let bush = 0;
let pig = 0;
let sheep = 0;
let cat = 0;
let penguin = 0;
let enemy = 0;
switch (biome) {
case Biome.Tundra:
pineTree = 0.01;
snowman = 0.001;
penguin = 0.01;
enemy = 0.001;
break;
case Biome.BorealForest:
pineTree = 0.05;
break;
case Biome.WoodlandOrShrubland:
tree = 0.05;
bush = 0.01;
pig = 0.01;
sheep = 0.01;
break;
case Biome.TemperateGrasslandOrColdDesert:
tree = 0.01;
break;
case Biome.TemperateRainforest:
tree = 0.2;
cat = 0.005;
break;
case Biome.TemperateSeasonableForest:
tree = 0.15;
cat = 0.005;
break;
case Biome.TropicalRainforest:
tree = 0.15;
palmTree = 0.15;
break;
case Biome.TropicalSeasonalForestOrSavanna:
tree = 0.1;
bush = 0.005;
break;
case Biome.SubtropicalDesert:
cactus = 0.05;
enemy = 0.001;
break;
}
const randomColorOffset = Math.floor(Math.random() * 10);
const biomeColorIndex = (biome * biomeColorsWidth + randomColorOffset) * 4;
const [backgroundRed, backgroundGreen, backgroundBlue] = [biomeColors[biomeColorIndex], biomeColors[biomeColorIndex + 1], biomeColors[biomeColorIndex + 2]];
if (altitude < seaLevel) {
if (biome == Biome.Tundra) {
if (altitude < seaLevel / 3) {
SetGrid(x, y, 'waves', 128, 128, 200, 1, 150, 150, 255, 255);
} else if (altitude < seaLevel * 2 / 3) {
SetGrid(x, y, 'waves', 128, 180, 255, 1, 150, 150, 255, 255);
} else {
SetGrid(x, y, 'waves', 128, Math.round(Math.random() * 32 + 180), 255, 1, 150, 150, 255, 255);
}
} else {
if (altitude < seaLevel / 3) {
SetGrid(x, y, 'waves', 0, 0, 200, 1, 0, 0, 255, 255);
} else if (altitude < seaLevel * 2 / 3) {
SetGrid(x, y, 'waves', 0, 64, 255, 1, 0, 0, 255, 255);
} else {
SetGrid(x, y, 'waves', 0, Math.round(Math.random() * 32 + 116), 255, 1, 0, 0, 255, 255);
}
}
} else if (altitude < 0.75) {
SetGrid(x, y, 'border-none', 0, 0, 0, 0.2, backgroundRed, backgroundGreen, backgroundBlue, 255);
} else if (altitude < 0.90) {
SetGrid(x, y, 'image-filter-hdr', 140, 105, 0, 1, backgroundRed, backgroundGreen, backgroundBlue, 255);
} else {
SetGrid(x, y, 'image-filter-hdr', 255, 255, 255, 1, backgroundRed, backgroundGreen, backgroundBlue, 255);
}
let cell = GetGrid(x, y);
if (cell.name == 'border-none') {
if (Math.random() < pineTree) {
SetGrid(x, y, 'pine-tree', 0, 100, 0, 1, backgroundRed, backgroundGreen, backgroundBlue, 255);
} else if (Math.random() < palmTree) {
SetGrid(x, y, 'palm-tree', 0, 130, 0, 1, backgroundRed, backgroundGreen, backgroundBlue, 255);
} else if (Math.random() < tree) {
SetGrid(x, y, 'tree', 0, 150, 0, 1, backgroundRed, backgroundGreen, backgroundBlue, 255);
} else if (Math.random() < snowman) {
SetGrid(x, y, 'snowman', 250, 250, 250, 1, backgroundRed, backgroundGreen, backgroundBlue, 255);
} else if (Math.random() < cactus) {
SetGrid(x, y, 'cactus', 0, 150, 0, 1, backgroundRed, backgroundGreen, backgroundBlue, 255);
} else if (Math.random() < bush) {
SetGrid(x, y, 'cloud', 0, 150, 0, 1, backgroundRed, backgroundGreen, backgroundBlue, 255);
}
}
cell = GetGrid(x, y);
if (cell.name == 'border-none') {
if (Math.random() < pig) {
entities.push({ position: new Vector2(x, y), textureIndex: RegisterTextureIndex('pig', 255, 150, 255, 1, 0, 0, 0, 0, 255, 255, 255, 255) });
} else if (Math.random() < sheep) {
entities.push({ position: new Vector2(x, y), textureIndex: RegisterTextureIndex('sheep', 0, 0, 0, 1, 0, 0, 0, 0, 255, 255, 255, 255) });
} else if (Math.random() < cat) {
entities.push({ position: new Vector2(x, y), textureIndex: RegisterTextureIndex('cat', 211, 137, 80, 1, 0, 0, 0, 0, 255, 255, 255, 255) });
} else if (Math.random() < penguin) {
entities.push({ position: new Vector2(x, y), textureIndex: RegisterTextureIndex('penguin', 0, 0, 0, 1, 0, 0, 0, 0, 255, 255, 255, 255) });
} else if (Math.random() < enemy) {
entities.push({ position: new Vector2(x, y), textureIndex: RegisterTextureIndex('emoticon-devil-outline', 255, 0, 0, 1, 0, 0, 0, 0, 255, 255, 255, 255) });
}
} else if (cell.name == 'waves') {
if (biome != Biome.Tundra && Math.random() < 0.01) {
entities.push({ water: true, position: new Vector2(x, y), textureIndex: RegisterTextureIndex('fish', 0, 30 + Math.floor(Math.random() * 20), 100, 1, 0, 0, 0, 0, 255, 255, 255, 255) });
}
}
}
}
// Move the player outside of objects naively
const moveOutDirection = new Vector2(1, 0);
while (!CanMoveTo(position, new Vector2(0, 0), 'border-none') && position.x < mapSize) {
position.ThisAdd(moveOutDirection);
}
requestAnimationFrame(Render);
};
setInterval(() => {
//console.log(fps);
fps = 0;
}, 1000);
function DrawImage(textureIndex, x, y, xRounded, yRounded) {
if (x * cellSize < xRounded + canvas.width &&
x * cellSize + cellSize > xRounded &&
y * cellSize < yRounded + canvas.height &&
y * cellSize + cellSize > yRounded)
{
const startX = x * cellSize < xRounded ? (xRounded % cellSize) * 4 : 0;
const endX = (x * cellSize + cellSize > xRounded + canvas.width ? ((xRounded + canvas.width - 1) % cellSize) : cellSize) * 4;
const startY = y * cellSize < yRounded ? yRounded % cellSize : 0;
const endY = y * cellSize + cellSize > yRounded + canvas.height ? (yRounded + canvas.height - 1) % cellSize : cellSize;
const offsetX = x * cellSize < xRounded ? xRounded % cellSize : 0;
const cameraX = x * cellSize - xRounded;
const cameraY = y * cellSize - yRounded;
const image = textures[textureIndex];
if (startX == 0 && endX == cellSize * 4) {
for (let imageY = startY; imageY < endY; ++imageY) {
renderImageData.data.set(image[imageY], ((cameraY + imageY) * canvas.width + cameraX) * 4);
}
} else {
for (let imageY = startY; imageY < endY; ++imageY) {
renderImageData.data.set(image[imageY].subarray(startX, endX), ((cameraY + imageY) * canvas.width + cameraX + offsetX) * 4);
}
}
}
}
function DrawImage2(textureIndex, x, y, xRounded, yRounded) {
if (x * cellSize < xRounded + canvas.width &&
x * cellSize + cellSize > xRounded &&
y * cellSize < yRounded + canvas.height &&
y * cellSize + cellSize > yRounded) {
context.drawImage(textureIndexToImage.get(textureIndex), x * cellSize - xRounded, y * cellSize - yRounded);
}
}
function Render() {
fps++;
context.clearRect(0, 0, canvas.width, canvas.height);
const xRounded = Math.round(cameraPosition.x);
const yRounded = Math.round(cameraPosition.y);
const xModCellSize = xRounded % cellSize == 0 ? 0 : cellSize - (xRounded % cellSize);
const yModCellSize = yRounded % cellSize == 0 ? 0 : cellSize - (yRounded % cellSize);
const minX = Math.floor(xRounded / cellSize);
const maxX = Math.floor((xRounded + canvas.width - 1) / cellSize);
const minY = Math.floor(yRounded / cellSize);
const maxY = Math.floor((yRounded + canvas.height - 1) / cellSize);
// Top Left
{
const x = minX;
const y = minY;
const startX = (xRounded % cellSize) * 4;
const startY = yRounded % cellSize;
const image = textures[map[y * mapSize + x]];
for (let imageY = startY; imageY < cellSize; ++imageY) {
renderImageData.data.set(image[imageY].subarray(startX, cellSize * 4), ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded + (xRounded % cellSize)) * 4);
}
}
// Top Right
{
const x = maxX;
const endX = ((xRounded + canvas.width - 1) % cellSize) * 4;
const y = minY;
const startY = yRounded % cellSize;
const image = textures[map[y * mapSize + x]];
for (let imageY = startY; imageY < cellSize; ++imageY) {
renderImageData.data.set(image[imageY].subarray(0, endX), ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded) * 4);
}
}
// Bottom Left
{
const x = minX;
const startX = (xRounded % cellSize) * 4;
const y = maxY;
const endY = (yRounded + canvas.height - 1) % cellSize;
const image = textures[map[y * mapSize + x]];
for (let imageY = 0; imageY < endY; ++imageY) {
renderImageData.data.set(image[imageY].subarray(startX, cellSize * 4), ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded + (xRounded % cellSize)) * 4);
}
}
// Bottom Right
{
const x = maxX;
const endX = ((xRounded + canvas.width - 1) % cellSize) * 4;
const y = maxY;
const endY = (yRounded + canvas.height - 1) % cellSize;
const image = textures[map[y * mapSize + x]];
for (let imageY = 0; imageY < endY; ++imageY) {
renderImageData.data.set(image[imageY].subarray(0, endX), ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded) * 4);
}
}
// Left
{
const x = minX;
const startX = (xRounded % cellSize) * 4;
for (let y = minY + 1; y < maxY; ++y) {
const image = textures[map[y * mapSize + x]];
for (let imageY = 0; imageY < cellSize; ++imageY) {
renderImageData.data.set(image[imageY].subarray(startX, cellSize * 4), ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded + (xRounded % cellSize)) * 4);
}
}
}
// Right
{
const x = maxX;
const endX = ((xRounded + canvas.width - 1) % cellSize) * 4;
for (let y = minY + 1; y < maxY; ++y) {
const image = textures[map[y * mapSize + x]];
for (let imageY = 0; imageY < cellSize; ++imageY) {
renderImageData.data.set(image[imageY].subarray(0, endX), ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded) * 4);
}
}
}
// Top
{
const y = minY;
const startY = yRounded % cellSize;
for (let x = minX + 1; x < maxX; ++x) {
const image = textures[map[y * mapSize + x]];
for (let imageY = startY; imageY < cellSize; ++imageY) {
renderImageData.data.set(image[imageY], ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded) * 4);
}
}
}
// Bottom
{
const y = maxY;
const endY = (yRounded + canvas.height - 1) % cellSize;
for (let x = minX + 1; x < maxX; ++x) {
const image = textures[map[y * mapSize + x]];
for (let imageY = 0; imageY < endY; ++imageY) {
renderImageData.data.set(image[imageY], ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded) * 4);
}
}
}
// Center
for (let y = minY + 1; y < maxY; ++y) {
for (let x = minX + 1; x < maxX; ++x) {
const image = textures[map[y * mapSize + x]];
for (let imageY = 0; imageY < cellSize; ++imageY) {
renderImageData.data.set(image[imageY], ((y * cellSize - yRounded + imageY) * canvas.width + x * cellSize - xRounded) * 4);
}
}
}
//DrawImage(playerTextureIndex, position.x, position.y, xRounded, yRounded);
context.putImageData(renderImageData, 0, 0);
for (const entity of entities) {
DrawImage2(entity.textureIndex, entity.position.x, entity.position.y, xRounded, yRounded);
}
DrawImage2(playerTextureIndex, position.x, position.y, xRounded, yRounded);
context.drawImage(temperatureCanvas, 0, 0, 256, 256);
context.drawImage(humidityCanvas, mapSize / 2, 0, 256, 256);
context.drawImage(altitudeCanvas, mapSize, 0, 256, 256);
context.drawImage(biomeCanvas, 0, mapSize / 2, 256, 256);
requestAnimationFrame(Render);
}
</script>
<style>
*
{
margin: 0;
padding: 0;
}
html
{
width: 100%;
height: 100%;
}
body
{
width: 100%;
height: 100%;
background-color: #eee;
}
canvas
{
}
</style>
</head>
<body>
<canvas id="canvas" width="0" height="0"></canvas>
</body>
</html>
"use strict";
class Vector2
{
constructor(x = 0, y = 0)
{
this.x = x;
this.y = y;
}
Add(v)
{
return new Vector2(this.x + v.x, this.y + v.y);
}
ThisAdd(v)
{
this.x += v.x;
this.y += v.y;
return this;
}
Subtract(v)
{
return new Vector2(this.x - v.x, this.y - v.y);
}
ThisSubtract(v)
{
this.x -= v.x;
this.y -= v.y;
return this;
}
Multiply(s)
{
return new Vector2(this.x * s, this.y * s);
}
ThisMultiply(s)
{
this.x *= s;
this.y *= s;
return this;
}
VMultiply(v)
{
return new Vector2(this.x * v.x, this.y * v.y);
}
Divide(s)
{
return new Vector2(this.x / s, this.y / s);
}
ThisDivide(s)
{
this.x /= s;
this.y /= s;
return this;
}
Length()
{
return Math.sqrt(this.x * this.x + this.y * this.y);
}
LengthSquared()
{
return this.x * this.x + this.y * this.y;
}
Normal()
{
return new Vector2(-this.y, this.x);
}
ThisNormal()
{
var x = this.x;
this.x = -this.y
this.y = x;
return this;
}
Normalize()
{
var length = this.Length();
if(length != 0)
{
return new Vector2(this.x / length, this.y / length);
}
return new Vector2(0, 0);
}
ThisNormalize()
{
var length = this.Length();
if (length != 0)
{
this.x /= length;
this.y /= length;
}
return this;
}
Negate()
{
return new Vector2(-this.x, -this.y);
}
ThisNegate()
{
this.x = -this.x;
this.y = -this.y;
return this;
}
Project(v)
{
return v.Multiply((this.x * v.x + this.y * v.y) / (v.x * v.x + v.y * v.y));
}
ThisProject(v)
{
var temp = (this.x * v.x + this.y * v.y) / (v.x * v.x + v.y * v.y);
this.x = v.x * temp;
this.y = v.y * temp;
return this;
}
Clone()
{
return new Vector2(this.x, this.y);
}
Set(v)
{
this.x = v.x;
this.y = v.y;
}
static Compare(v1, v2)
{
return Math.abs(v1.x - v2.x) < 0.0001 && Math.abs(v1.y - v2.y) < 0.0001;
}
static Dot(v1, v2)
{
return v1.x * v2.x + v1.y * v2.y;
}
static Cross(v1, v2)
{
return v1.x * v2.y - v1.y * v2.x;
}
static Interpolate(v1, v2, p)
{
return new Vector2
(
(v2.x - v1.x) * p + v1.x,
(v2.y - v1.y) * p + v1.y
);
}
}
class Vector3
{
constructor(x = 0, y = 0, z = 0)
{
this.x = x;
this.y = y;
this.z = z;
}
Add(v)
{
return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z);
}
ThisAdd(v)
{
this.x += v.x;
this.y += v.y;
this.z += v.z;
return this;
}
Subtract(v)
{
return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z);
}
ThisSubtract(v)
{
this.x -= v.x;
this.y -= v.y;
this.z -= v.z;
return this;
}
Multiply(s)
{
return new Vector3(this.x * s, this.y * s, this.z * s);
}
ThisMultiply(s)
{
this.x *= s;
this.y *= s;
this.z *= s;
return this;
}
VMultiply(v)
{
return new Vector3(this.x * v.x, this.y * v.y, this.z * v.z);
}
Divide(s)
{
return new Vector3(this.x / s, this.y / s, this.z / s);
}
ThisDivide(s)
{
this.x /= s;
this.y /= s;
this.z /= s;
return this;
}
Length()
{
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
LengthSquared()
{
return this.x * this.x + this.y * this.y + this.z * this.z;
}
Normalize()
{
var length = this.Length();
if(length != 0)
{
return new Vector3(this.x / length, this.y / length, this.z / length);
}
return new Vector3(0, 0, 0);
}
ThisNormalize()
{
var length = this.Length();
if (length != 0)
{
this.x /= length;
this.y /= length;
this.z /= length;
}
return this;
}
Negate()
{
return new Vector3(-this.x, -this.y, -this.z);
}
ThisNegate()
{
this.x = -this.x;
this.y = -this.y;
this.z = -this.z;
return this;
}
Clone()
{
return new Vector3(this.x, this.y, this.z);
}
static Compare(v1, v2)
{
return Math.abs(v1.x - v2.x) < 0.0001 && Math.abs(v1.y - v2.y) < 0.0001 && Math.abs(v1.z - v2.z) < 0.0001;
}
static Dot(v1, v2)
{
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
static Cross(v1, v2)
{
return new Vector3(v1.y * v2.z - v2.y * v1.z, v1.z * v2.x - v2.z * v1.x, v1.x * v2.y - v2.x * v1.y);
}
static Interpolate(v1, v2, p)
{
return new Vector3
(
(v2.x - v1.x) * p + v1.x,
(v2.y - v1.y) * p + v1.y,
(v2.z - v1.z) * p + v1.z
);
}
}
class Vector4
{
constructor(x = 0, y = 0, z = 0, w = 0)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
Add(v)
{
return new Vector4(this.x + v.x, this.y + v.y, this.z + v.z, this.w + v.w);
}
ThisAdd(v)
{
this.x += v.x;
this.y += v.y;
this.z += v.z;
this.w += v.w;
return this;
}
Subtract(v)
{
return new Vector4(this.x - v.x, this.y - v.y, this.z - v.z, this.w - v.w);
}
ThisSubtract(v)
{
this.x -= v.x;
this.y -= v.y;
this.z -= v.z;
this.w -= v.w;
return this;
}
Multiply(s)
{
return new Vector4(this.x * s, this.y * s, this.z * s, this.w * s);
}
ThisMultiply(s)
{
this.x *= s;
this.y *= s;
this.z *= s;
this.w *= s;
return this;
}
Divide(s)
{
return new Vector4(this.x / s, this.y / s, this.z / s, this.w / s);
}
ThisDivide(s)
{
this.x /= s;
this.y /= s;
this.z /= s;
this.w /= s;
return this;
}
Length()
{
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
LengthSquared()
{
return this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w;
}
Normalize()
{
var length = this.Length();
if(length != 0)
{
return new Vector4(this.x / length, this.y / length, this.z / length, this.w / length);
}
return new Vector4();
}
ThisNormalize()
{
var length = this.Length();
if (length != 0)
{
this.x /= length;
this.y /= length;
this.z /= length;
this.w /= length;
}
return this;
}
Negate()
{
return new Vector4(-this.x, -this.y, -this.z, -this.w);
}
ThisNegate()
{
this.x = -this.x;
this.y = -this.y;
this.z = -this.z;
this.w = -this.w;
return this;
}
Clone()
{
return new Vector4(this.x, this.y, this.z, this.w);
}
static Compare(v1, v2)
{
return Math.abs(v1.x - v2.x) < 0.0001 && Math.abs(v1.y - v2.y) < 0.0001 && Math.abs(v1.z - v2.z) < 0.0001 && Math.abs(v1.w - v2.w) < 0.0001;
}
static Dot(v1, v2)
{
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z + v1.w * v2.w;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment