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);  } | 
