diff options
author | sanine <sanine.not@pm.me> | 2023-03-31 17:17:27 -0500 |
---|---|---|
committer | sanine <sanine.not@pm.me> | 2023-03-31 17:17:27 -0500 |
commit | 673e9c13aea6cd1b11f5ca3e1f6edd474bbb1a19 (patch) | |
tree | 5764ff2cfd6b8dea3b615a1bb2f1dc1c078b07e7 | |
parent | 8aa6645f2311de78f74b35f804cc45c7fcf38f57 (diff) |
implement nice map grid
-rw-r--r-- | src/Canvas.js | 127 | ||||
-rw-r--r-- | src/Geometry.js | 209 | ||||
-rw-r--r-- | src/MapView.js | 62 | ||||
-rw-r--r-- | src/Terrain.js | 206 | ||||
-rw-r--r-- | src/main.js | 50 |
5 files changed, 300 insertions, 354 deletions
diff --git a/src/Canvas.js b/src/Canvas.js index 6dff7bd..f3db3e0 100644 --- a/src/Canvas.js +++ b/src/Canvas.js @@ -1,142 +1,25 @@ -import { clamp } from './modules/Util.js'; - class Canvas { - constructor(rootId) { - const root = document.getElementById(rootId); - + constructor(parentElement) { this.element = document.createElement('canvas'); this.context = this.element.getContext('2d'); - - /* state variables */ - this.scale = 1; - this.zoom = 1; - const ZOOM_SPEED = 1.2; - - this.mouse = { - screenPos: { x: 0, y: 0 }, - drawingPos: { x: 0, y: 0 }, - }; - this.pan = { x: 0, y: 0 }; - this.panning = false; - - /* callbacks */ - this.onDraw = null; - this.onMouseMove = null; - this.onMouseDown = null; - this.onMouseUp = null; - - /* register event listeners */ - - /* mouse movement */ - this.element.addEventListener('mousemove', e => { - this.mouse.screenPos.x = e.offsetX; - this.mouse.screenPos.y = e.offsetY; - - const [xd, yd] = this.screenToDrawingPos(e.offsetX, e.offsetY); - - /* compute movement */ - const dx = e.movementX / (this.zoom * this.scale); - const dy = e.movementY / (this.zoom * this.scale); - - /* pan? */ - if (this.panning) { - this.pan.x += dx; - this.pan.y += dy; - this.setTransform(); - } - - this.mouse.drawingPos.x = xd; - this.mouse.drawingPos.y = yd; - - if (this.onMouseMove) this.onMouseMove(this.mousePos); - }); - - /* clicking */ - this.element.addEventListener('mousedown', e => { - e.preventDefault(); - if (e.button === 1) this.panning = true; - if (this.onMouseDown) this.onMouseDown(e); - }); - this.element.addEventListener('mouseup', e => { - if (e.button === 1) this.panning = false; - if (this.onMouseUp) this.onMouseUp(e); - }); - - /* mouse leave */ - this.element.addEventListener('mouseleave', e => { - this.panning = false; - }); - - /* mouse wheel */ - this.element.addEventListener('wheel', e => { - if (this.panning) return; // don't zoom and pan simultaneously - - const delta = e.deltaY < 0 ? ZOOM_SPEED : 1/ZOOM_SPEED; - const alpha = (1/delta) - 1; - - /* zoom in */ - this.zoom *= delta; - this.zoom = clamp(this.zoom, 1, 1000); - this.setTransform(); - - /* pan to keep mouse in the same place */ - const [mouseX, mouseY] = this.screenToDrawingPos(this.mouse.screenPos.x, this.mouse.screenPos.y); - this.pan.x += mouseX - this.mouse.drawingPos.x; - this.pan.y += mouseY - this.mouse.drawingPos.y; - this.setTransform(); - this.draw(); - }); - - /* finalize setup */ + parentElement.appendChild(this.element); this.fillWindow(); window.addEventListener('resize', () => this.fillWindow()); - root.appendChild(this.element); - } - - setTransform() { - /* clamp pan */ - const xMax = this.pixelsToUnits(this.element.width) - 1; - const yMax = this.pixelsToUnits(this.element.height) - 1; - this.pan.x = clamp(this.pan.x, xMax, 0); - this.pan.y = clamp(this.pan.y, yMax, 0); - - this.context.setTransform(1, 0, 0, 1, 0, 0); - this.context.scale(this.zoom * this.scale, this.zoom * this.scale); - this.context.translate(this.pan.x, this.pan.y); } fillWindow() { const width = window.innerWidth; const height = window.innerHeight; - this.scale = Math.max(width, height); + const scale = Math.min(width, height); this.element.width = width; this.element.height = height; - this.setTransform(); + this.context.setTransform(0.4*scale, 0, 0, -0.4*scale, 0.5*scale, 0.5*scale); this.draw(); } - pixelsToUnits(value) { - return value / this.zoom / this.scale; - } - - unitsToPixels(value) { - return value * this.zoom * this.scale; - } - - screenToDrawingPos(xs, ys) { - const matrix = this.context.getTransform(); - matrix.invertSelf(); - - /* compute drawing-space position */ - const x = matrix.a*xs + matrix.b*ys + matrix.e; - const y = matrix.c*xs + matrix.d*ys + matrix.f; - - return [ x, y ]; - } - draw() { - this.context.clearRect(0, 0, 1, 1); + this.context.clearRect(-1, -1, 2, 2); if (this.onDraw) this.onDraw(this.context); } } diff --git a/src/Geometry.js b/src/Geometry.js new file mode 100644 index 0000000..23ef480 --- /dev/null +++ b/src/Geometry.js @@ -0,0 +1,209 @@ +'use strict'; + + +export class Mat3 { + constructor(arr = null) { + if (arr == null) { + this.identity(); + } + else { + this.arr = arr; + } + } + + i(row, col) { + const r = row-1 + const c = col-1 + const index = (3*r)+c + return this.arr[index] + } + + identity() { + this.arr = [ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1, + ]; + return this; + } + + + mul(other) { + const out = [] + out[0] = (this.i(1,1)*other.i(1,1)) + (this.i(1,2)*other.i(2,1)) + (this.i(1,3)*other.i(3,1)); + out[1] = (this.i(1,1)*other.i(1,2)) + (this.i(1,2)*other.i(2,2)) + (this.i(1,3)*other.i(3,2)); + out[2] = (this.i(1,1)*other.i(1,3)) + (this.i(1,2)*other.i(2,3)) + (this.i(1,3)*other.i(3,3)); + + out[3] = (this.i(2,1)*other.i(1,1)) + (this.i(2,2)*other.i(2,1)) + (this.i(2,3)*other.i(3,1)); + out[4] = (this.i(2,1)*other.i(1,2)) + (this.i(2,2)*other.i(2,2)) + (this.i(2,3)*other.i(3,2)); + out[5] = (this.i(2,1)*other.i(1,3)) + (this.i(2,2)*other.i(2,3)) + (this.i(2,3)*other.i(3,3)); + + out[6] = (this.i(3,1)*other.i(1,1)) + (this.i(3,2)*other.i(2,1)) + (this.i(3,3)*other.i(3,1)); + out[7] = (this.i(3,1)*other.i(1,2)) + (this.i(3,2)*other.i(2,2)) + (this.i(3,3)*other.i(3,2)); + out[8] = (this.i(3,1)*other.i(1,3)) + (this.i(3,2)*other.i(2,3)) + (this.i(3,3)*other.i(3,3)); + + this.arr = out + return this + } + + mulv(vec) { + const x = (this.i(1,1)*vec.x) + (this.i(1,2)*vec.y) + (this.i(1,3)*vec.z); + const y = (this.i(2,1)*vec.x) + (this.i(2,2)*vec.y) + (this.i(2,3)*vec.z); + const z = (this.i(3,1)*vec.x) + (this.i(3,2)*vec.y) + (this.i(3,3)*vec.z); + return new Vec3(x, y, z); + } + + rotation(axis, angle) { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + const mcos = 1-cos; + const msin = 1-sin; + + this.arr[0] = cos + (axis.x * axis.x * mcos); + this.arr[1] = (axis.x * axis.y * mcos) - (axis.z * sin); + this.arr[2] = (axis.x * axis.z * mcos) + (axis.y * sin); + + this.arr[3] = (axis.y * axis.x * mcos) + (axis.z * sin); + this.arr[4] = cos + (axis.y * axis.y * mcos); + this.arr[5] = (axis.y * axis.z * mcos) - (axis.x * sin); + + + this.arr[6] = (axis.z * axis.x * mcos) - (axis.y * sin); + this.arr[7] = (axis.z * axis.y * mcos) + (axis.x * sin); + this.arr[8] = cos + (axis.z * axis.z * mcos); + + return this + } +} + + +export class Point { + constructor(latitude, longitude) { + this.lat = latitude; + this.long = longitude; + } + + normal() { + const x = Math.cos(this.lat) * Math.cos(this.long) + const y = Math.cos(this.lat) * Math.sin(this.long) + const z = Math.sin(this.lat) + return new Vec3(x, y, z) + } +} + + +export class Vec3 { + constructor(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + } + + normalize() { + const len2 = (this.x*this.x) + (this.y*this.y) + (this.z*this.z); + const len = Math.sqrt(len2); + this.x = this.x/len; + this.y = this.y/len; + this.z = this.z/len; + return this; + } + + point() { + const latitude = Math.asin(this.z); + const longitute = + Math.sign(this.this.y) * Math.acos( + this.x / Math.sqrt( (this.x*this.x) + (this.y*this.y) ) + ) + return new Point(latitude, longitude) + } + + dot(vec) { + return (this.x * vec.x) + (this.y * vec.y) + (this.z * vec.z); + } + + cross(vec) { + const x = (this.y*vec.z) - (this.z*vec.y); + const y = (this.z*vec.x) - (this.x*vec.z); + const z = (this.x*vec.y) - (this.y*vec.x); + return new Vec3(x, y, z); + } + + transform(matrix) { + return matrix.mulv(this); + } + + render(ct, view) { + const viewNorm = view.mulv(this); + if (viewNorm.z >= -0.01) { + ct.beginPath() + ct.arc(viewNorm.x, viewNorm.y, 0.01, 0, 2*Math.PI); + ct.fill(); + } + } +} + + +export class Shape { + constructor(normals) { + this.normals = normals; + let avgx = 0; + let avgy = 0; + let avgz = 0; + for (let normal of normals) { + avgx += normal.x; + avgy += normal.y; + avgz += normal.z; + } + avgx /= normals.length; + avgy /= normals.length; + avgz /= normals.length; + this.center = new Vec3(avgx, avgy, avgz).normalize(); + this.extent = 0 + for (let normal of normals) { + this.extent = Math.max(this.extent, normal.dot(this.center)); + } + } + + translate(circle, angle) { + const transform = new Mat3().rotation(circle, angle); + for (let i=0; i<this.normals.length; i++) { + this.normals[i] = transform.mulv(this.normals[i]); + } + this.center = transform.mulv(this.normals[i]); + } + + getNextRenderPoint(ct, start, view) { + for (let i=start; i<this.normals.length; i++) { + const v = view.mulv(this.normals[i]); + if (v.z >= -0.01) { + if (i != start) { + ct.stroke(); + ct.beginPath(); + ct.moveTo(v.x, v.y); + } + return [ i+1, v ]; + } + } + + return [null, null]; + } + + render(ct, view) { + ct.beginPath(); + let i = 0; + let v; + [ i, v ] = this.getNextRenderPoint(ct, i, view); + if (v == null) { + // no renderable points + return; + } + + + ct.moveTo(v.x, v.y); + while(v != null) { + ct.lineTo(v.x, v.y); + [ i, v ] = this.getNextRenderPoint(ct, i, view); + } + ct.stroke(); + } +} diff --git a/src/MapView.js b/src/MapView.js new file mode 100644 index 0000000..a08d6b7 --- /dev/null +++ b/src/MapView.js @@ -0,0 +1,62 @@ +import Canvas from './Canvas.js'; +import { Mat3, Vec3, Point, Shape } from './Geometry.js'; + + +const yAxis = new Vec3(0, 1, 0); +const zAxis = new Vec3(0, 0, 1); + + +function radians(degrees) { + return degrees * (Math.PI/180); +} + +function Circle(p0, axis, subdiv) { + const rotationMatrix = new Mat3().rotation(axis, 2*Math.PI/(subdiv-1)); + const points = [p0]; + for (let i=0; i<subdiv-1; i++) { + const point = rotationMatrix.mulv(points[i]); + points.push(point); + } + return new Shape(points); +} + +export class MapGrid { + constructor(latDiv, longDiv, circleDiv=50) { + // create latitudes + this.latitudes = []; + // equator + this.latitudes.push(Circle( + new Point(0, radians(0)).normal(), yAxis, circleDiv + )); + for (let angle=latDiv; angle<90; angle += latDiv) { + // positive (north) + this.latitudes.push(Circle( + new Point(0, radians(angle)).normal(), yAxis, circleDiv + )); + // negative (south) + this.latitudes.push(Circle( + new Point(0, radians(-angle)).normal(), yAxis, circleDiv + )); + } + + // create longitudes + this.longitudes = []; + for (let angle=0; angle<180; angle += longDiv) { + console.log(angle); + const point = new Point(radians(angle), 0).normal(); + const axis = point.cross(yAxis).normalize(); + this.longitudes.push(Circle( + point, axis, circleDiv + )); + } + } + + render(ct, view) { + for (let latitude of this.latitudes) { + latitude.render(ct, view); + } + for (let longitude of this.longitudes) { + longitude.render(ct, view); + } + } +} diff --git a/src/Terrain.js b/src/Terrain.js deleted file mode 100644 index 291a4fc..0000000 --- a/src/Terrain.js +++ /dev/null @@ -1,206 +0,0 @@ -'use strict'; - -import Voronoi from './3rdparty/rhill-voronoi-core.js'; - -import { lerp, clamp, useAverage } from './modules/Util.js'; -import { dist, AABB, QuadTree } from './modules/Geometry.js'; - - -/* from here on up, we always assume that points live in the range [(0,0), (1,1)) */ - -function lloydRelax(point_set, density) { - /* setup quadtree and averages */ - let tree = new QuadTree(1,1); - let averages = {}; - for (let i=0; i<point_set.length; i++) { - const point = point_set[i]; - point.index = i; - tree.insert(point); - - let [avg, append] = useAverage(); - const cent_x = { avg, append }; - [avg, append] = useAverage(); - const cent_y = { avg, append }; - averages[i] = { cent_x, cent_y }; - } - - /* compute average centroids */ - for (let x=0; x<1; x += 1/density) { - for (let y=0; y<1; y += 1/density) { - const point = { x, y }; - const closest = tree.closest(point); - const { cent_x, cent_y } = averages[closest.index]; - cent_x.append(point.x); - cent_y.append(point.y); - } - } - - /* return centroid points */ - const result = []; - for (let i=0; i<point_set.length; i++) { - const point = { x: averages[i].cent_x.avg(), y: averages[i].cent_y.avg() }; - result.push(point); - } - return result; -} - - -class Terrain { - constructor() { - const N_SEED_POINTS = 2**12; - const N_RELAX_ITERATIONS = 1; - const RELAX_DENSITY = 400; - const randomPoint = () => ({x: Math.random(), y: Math.random()}); - - this.min_height = 0; - this.max_height = 0; - - let seed_points = []; - for (let i=0; i<N_SEED_POINTS; i++) seed_points.push(randomPoint()); - - for (let i=0; i<N_RELAX_ITERATIONS; i++) - lloydRelax(seed_points, RELAX_DENSITY); - - const v = new Voronoi(); - this.voronoi = v.compute(seed_points, {xl: 0, xr: 1, yt: 0, yb: 1}); - - this.tree = new QuadTree(1,1); - let index = 0; - for (let v of this.voronoi.vertices) { - v.height = v.y; - v.index = index; - index += 1; - this.tree.insert(v); - } - - this.graph = {}; - for (let e of this.voronoi.edges) { - if (!this.graph[e.va.index]) this.graph[e.va.index] = new Set(); - if (!this.graph[e.vb.index]) this.graph[e.vb.index] = new Set(); - - this.graph[e.va.index].add(e.vb.index); - this.graph[e.vb.index].add(e.va.index); - } - } - - - applyBrush(x, y, f, strength, radius) { - const region = new AABB(x-radius, y-radius, 2*radius, 2*radius); - const points = this.tree.root.getPointsInRegion(region); - - const dist2 = (a, b) => (a.x - b.x)**2 + (a.y - b.y)**2; - - const sigma = radius/3; - const beta = 1/(2*sigma*sigma); - const center = { x, y }; - const power = pt => Math.exp(-beta * dist2(pt, center)); - - for (let pt of points) f(pt, strength * power(pt)); - } - - - getNeighbors(point) { - const indices = Array.from(this.graph[point.index]); - return indices.map( index => this.voronoi.vertices[index] ); - } - - - renderGrid(ct) { - ct.save(); - ct.lineWidth = 0.001; - for (let edge of this.voronoi.edges) { - ct.fillStyle = `hsl(${edge.va.height}, 100%, 50%)`; - ct.beginPath(); - ct.arc(edge.va.x, edge.va.y, 0.0005, 0, 2*Math.PI); - ct.closePath(); - ct.fill(); - - /*ct.beginPath(); - ct.moveTo(edge.va.x, edge.va.y); - ct.lineTo(edge.vb.x, edge.vb.y); - ct.closePath(); - ct.stroke(); - */ - } - ct.restore(); - } - - render(ct) { - function rgb(r, g, b) { - this.r = r; this.g = g; this.b = b; - } - const renderRgb = rgb => `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; - const lerpColors = (a, b, alpha) => new rgb( - lerp(a.r, b.r, alpha), - lerp(a.g, b.g, alpha), - lerp(a.b, b.b, alpha) - ); - const getBucket = (value, min, max, numBuckets) => { - const delta = max - min; - const step = delta/numBuckets; - return clamp(Math.floor((value-min)/step), 0, numBuckets-1); - } - const getColor = (value, min, max, colors) => { - if (value < min) return colors[0]; - if (value >= max) return colors[colors.length-1]; - - const step = (max - min)/(colors.length - 1); - - const index = getBucket(value, min, max, colors.length-1); - const alpha = (value - (index*step) - min)/step; - const color = lerpColors( - colors[index], colors[index+1], - alpha - ); - if (color.r <= 0) - console.log(value, index, alpha, color); - return color; - } - - const colors = [ - /* ocean */ - new rgb(31, 59, 68), - new rgb(68, 130, 149), - - /* land */ - new rgb(229, 199, 169), - new rgb(198, 133, 67), - new rgb(117, 89, 61), - - /* mountain */ - new rgb(82, 74, 64), - new rgb(82, 74, 64), - new rgb(255, 255, 255), - ]; - ct.save(); - - for (let cell of this.voronoi.cells) { - ct.beginPath(); - - let height = 0; - let count = 1; - for (let edge of cell.halfedges) { - const p0 = edge.getStartpoint(); - const p1 = edge.getEndpoint(); - height += p0.height; - count += 1; - ct.lineTo(p0.x, p0.y); - ct.lineTo(p1.x, p1.y); - } - ct.closePath(); - height /= count; - height = 10 * Math.floor(height/10); - const color = getColor(height, 0, 100, colors); - if (color.r <=0) console.log(color); - ct.fillStyle = renderRgb(color); - ct.strokeStyle = renderRgb(color); - ct.stroke(); - ct.fill(); - } - - ct.restore(); - } -} - -export { lloydRelax }; -export default Terrain; diff --git a/src/main.js b/src/main.js index f73a17b..a86a38e 100644 --- a/src/main.js +++ b/src/main.js @@ -1,39 +1,37 @@ -import Terrain from './Terrain.js'; import Canvas from './Canvas.js'; -import Brush from './Brush.js'; -import BrushSelector from './BrushSelector.js'; +import { Mat3, Vec3, Point, Shape } from './Geometry.js'; +import { MapGrid } from './MapView.js'; const $ = id => document.getElementById(id) window.onload = () => { - const canvas = new Canvas('root'); - const terrain = new Terrain(); - const selector = new BrushSelector('root', canvas, terrain); + const canvas = new Canvas($('root')); - let brushing = false; + const xaxis = new Vec3(1, 0, 0); + const yaxis = new Vec3(0, 1, 0); + const zaxis = new Vec3(0, 0, 1); - canvas.onMouseDown = e => { - if (e.button == 0) brushing = true; - }; - canvas.onMouseUp = e => { - if (e.button == 0) brushing = false; - }; + const grid = new MapGrid(30, 90); - const pos = canvas.mouse.drawingPos; - canvas.onMouseMove = () => { - if (brushing) selector.apply(); - canvas.draw(); - }; - - canvas.onDraw = ct => { - terrain.render(ct); + let view = new Mat3(); + let angle = 0; - ct.strokeStyle = '#fff'; - ct.lineWidth = canvas.pixelsToUnits(1); - ct.beginPath(); - ct.arc(pos.x, pos.y, canvas.pixelsToUnits(selector.radiusSlider.value), 0, 2*Math.PI); - ct.closePath(); + canvas.onDraw = ct => { + ct.fillStyle = "#01162B" + ct.fillRect(-10,-10, 100, 100); + ct.lineWidth = 0.005; + ct.strokeStyle = "#E7305D"; + ct.fillStyle = "blue"; + ct.arc(0, 0, 1, 0, 2*Math.PI); ct.stroke(); + + grid.render(ct, view); }; canvas.draw(); + + setInterval(() => { + angle = angle + 0.01*Math.PI; + view.rotation(xaxis, angle); + canvas.draw(); + }, 50); } |