圆、球面、四维球面在二维空间的投影

423 阅读1分钟

先简化一下定义:

  • 圆:在AB坐标系中,a * a + b * b = 1
  • 球面:在ABC坐标系中,a * a + b * b + c * c = 1
  • 四维球面:在ABCD坐标系中,a * a + b * b + c * c + d * d = 1
  • 点投射:多维空间的点投射到二维XY的绘制平面,故每个维的投射关系,都对应一个[x,y]。
  • 脑补:绘制平台是二维的,加一个动态变化,由人类来脑补出多维的形象。例如,以下三维球面上的随机点,一旦变化,人类可以很容易脑补出一个蛋壳;但是如果不变化,人类不大容易脑补出蛋壳。

1662714159552.gif

以下代码效果,看下是否能脑补出四维空间的球面是个什么形象。

直接上代码。

<!DOCTYPE html>
<html>
<head> 
<meta charset="utf-8"> 
<title>圆-球面-四维球面</title> 
<style>
.ctn {width:400px;padding:10px;display:inline-block}
.point-count, .axis, .axisDelta {width:200px}
.deataBtn {background-color:#88FFCC}
</style>
</head>
<body>

<fieldset>
<legend>青色按钮,可以长按。</legend>

<div class="ctn">
二维(圆)<br> 
<input class="point-count" value=100> <input type=button value="重新生成点" onclick="refreshPoints(0)">
<input type=button value="重绘" onclick="repaint(0)"> 
<input type=button value="重置" onclick="resetInputs(0)"><br>
<input class="axis" value="[[1,0],[0,1]]" onchange="repaint(0)">
<input class="axisDelta" value="[[0.01,-0.01],[0.01,-0.01]]" onchange="repaint(0)" title="【坐标轴变化步距】"> <input class="deataBtn" type=button value="坐标轴+" onmousedown="changeAxis(0,1);"> <input class="deataBtn" type=button value="坐标轴-" onmousedown="changeAxis(0,-1);"><br>
<input class="topPoint" value="[0]" onchange="repaint(0)">球冠顶点<br>
<input class="topPointDelta" value="[0.05]" onchange="repaint(0)" title="顶点偏移步距"> <input class="deataBtn" type=button value="顶点+" onmousedown="changeTopPoint(0,1);"> <input class="deataBtn" type=button value="顶点-" onmousedown="changeTopPoint(0,-1);"> <br>
<input class="radian" value="3.1416" onchange="repaint(0)">球冠弧度<br>
<input class="radianDelta" value="0.05" onchange="repaint(0)" title="弧度变化步距"> <input class="deataBtn" type=button value="弧度+" onmousedown="changeRadian(0,1);"> <input class="deataBtn" type=button value="弧度-" onmousedown="changeRadian(0,-1);"><br>
<canvas width="400" height="400" style="border:1px solid #ccc;"></canvas>
</div>

<div class="ctn">
三维(球面)<br>
<input class="point-count" value=2000> <input type=button value="重新生成点" onclick="refreshPoints(1)">
<input type=button value="重绘" onclick="repaint(1)"> 
<input type=button value="重置" onclick="resetInputs(1)"><br>
<input class="axis" value="[[1,0],[0,1],[0.5,0.5]]" onchange="repaint(1)">
<input class="axisDelta" value="[[-0.001,0.001],[0.001,-0.001],[0.01,-0.01]]" onchange="repaint(1)" title="【坐标轴变化步距】"> <input class="deataBtn" type=button value="坐标轴+" onmousedown="changeAxis(1,1);"> <input class="deataBtn" type=button value="坐标轴-" onmousedown="changeAxis(1,-1);"><br>
<input class="topPoint" value="[0,0]" onchange="repaint(1)">球冠顶点<br>
<input class="topPointDelta" value="[0.05,0.01]" onchange="repaint(1)" title="顶点偏移步距"> <input class="deataBtn" type=button value="顶点+" onmousedown="changeTopPoint(1,1);"> <input class="deataBtn" type=button value="顶点-" onmousedown="changeTopPoint(1,-1);"> <br>
<input class="radian" value="3.1416" onchange="repaint(1)">球冠弧度<br>
<input class="radianDelta" value="0.05" onchange="repaint(1)" title="弧度变化步距"> <input class="deataBtn" type=button value="弧度+" onmousedown="changeRadian(1,1);"> <input class="deataBtn" type=button value="弧度-" onmousedown="changeRadian(1,-1);"><br>
<canvas width="400" height="400" style="border:1px solid #ccc;"></canvas>
</div>

<div class="ctn">
四维球面<br>
<input class="point-count" value=40000> <input type=button value="重新生成点" onclick="refreshPoints(2)">
<input type=button value="重绘" onclick="repaint(2)"> 
<input type=button value="重置" onclick="resetInputs(2)"><br>
<input class="axis" value="[[1,0],[0,1],[0.5,0.5],[-0.5,0.2]]" onchange="repaint(2)">
<input class="axisDelta" value="[[0,0.01],[0.001,0],[-0.01,0.001],[0.001,-0.005]]" onchange="repaint(2)" title="【坐标轴变化步距】"> <input class="deataBtn" type=button value="坐标轴+" onmousedown="changeAxis(2,1);"> <input class="deataBtn" type=button value="坐标轴-" onmousedown="changeAxis(2,-1);"><br>
<input class="topPoint" value="[0,0,0]" onchange="repaint(2)">球冠顶点<br>
<input class="topPointDelta" value="[0.05,0.01,0.03]" onchange="repaint(2)" title="顶点偏移步距"> <input class="deataBtn" type=button value="顶点+" onmousedown="changeTopPoint(2,1);"> <input class="deataBtn" type=button value="顶点-" onmousedown="changeTopPoint(2,-1);"> <br>
<input class="radian" value="3.1416" onchange="repaint(2)">球冠弧度<br>
<input class="radianDelta" value="0.05" onchange="repaint(2)" title="弧度变化步距"> <input class="deataBtn" type=button value="弧度+" onmousedown="changeRadian(2,1);"> <input class="deataBtn" type=button value="弧度-" onmousedown="changeRadian(2,-1);"><br>
<canvas width="400" height="400" style="border:1px solid #ccc;"></canvas>
</div>

</fieldset>

<fieldset>
<legend>变化对比</legend>

坐标轴变化:<input class="deataBtn" type=button value="坐标轴+" onmousedown="changeAxis(0,1);changeAxis(1,1);changeAxis(2,1)"> <input class="deataBtn" type=button value="坐标轴-" onmousedown="changeAxis(0,-1);changeAxis(1,-1);changeAxis(2,-1)"><br>
顶点变化:<input class="deataBtn" type=button value="顶点+" onmousedown="changeTopPoint(0,1);changeTopPoint(1,1);changeTopPoint(2,1)"> <input class="deataBtn" type=button value="顶点-" onmousedown="changeTopPoint(0,-1);changeTopPoint(1,-1);changeTopPoint(2,-1)"><br>
弧度变化:<input class="deataBtn" type=button value="弧度+" onmousedown="changeRadian(0,1);changeRadian(1,1);changeRadian(2,1)"> <input class="deataBtn" type=button value="弧度-" onmousedown="changeRadian(0,-1);changeRadian(1,-1);changeRadian(2,-1)">
</fieldset>
<script>

var points=[[],[],[]];


function refreshPoints(sectionIndex){	/*生成随机点*/
	var axis = JSON.parse(document.querySelectorAll('.axis')[sectionIndex].value);
	points[sectionIndex]=[];
	var num = Math.min(document.querySelectorAll('.point-count')[sectionIndex].value|0,100000);
	while (num-->0) {
		var point=[],
			left=1;
		for(var i=0;i<axis.length-1;i++){
			point[i] = left * (2 * Math.random() - 1);
			left = Math.pow(left*left - point[i]*point[i],0.5)
		}
		point.push(Math.random()>0.5?left:-left);
		point.sort(()=>Math.random()-0.5);
		points[sectionIndex].push(point);
	}
	repaint(sectionIndex);
}

function repaint(sectionIndex){
	var c=document.querySelectorAll("canvas")[sectionIndex];
	var ctx=c.getContext("2d");
	ctx.clearRect(0,0,c.width,c.height);
	ctx.fillStyle = 'rgb(255,0,0)';

	var axis = JSON.parse(document.querySelectorAll('.axis')[sectionIndex].value); //坐标轴
	var topPointR = JSON.parse(document.querySelectorAll('.topPoint')[sectionIndex].value);//顶点
	var topPoint = [];
	var leftR=1;
	for(var i=0;i<topPointR.length;i++) {
		topPoint[i] = leftR * Math.cos(topPointR[i]);
		leftR *= Math.sin(topPointR[i])
	}
	topPoint.push(leftR);
	var okDis = 2* Math.cos(Math.PI/2 - JSON.parse(document.querySelectorAll('.radian')[sectionIndex].value)/4);//

	/*画点*/
	var num = points[sectionIndex].length;
	var p1=[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; //[1,0,0,0,0,0,0,0]点为红色,其他点离他越近,越接近红色。给点着色,方便观察

	while (num-->0) {
		var point = points[sectionIndex][num];
		var coord = [0,0];
		var dis = 0;
		var dis2Top = 0;
		for(var i=0;i<axis.length;i++){
			coord[0] += point[i]*axis[i][0];
			coord[1] += point[i]*axis[i][1];
			dis += Math.pow(point[i]-p1[i],2);
			dis2Top += Math.pow(point[i]-topPoint[i],2);
		}

		if(Math.pow(dis2Top,0.5)> okDis) continue;//球冠范围外的点,不用绘制
		ctx.fillStyle = 'rgb(' + (dis<=1.8 ? parseInt(255*(2-dis)/2):0) + ',15,' + (dis>2.2 ? parseInt(255*(dis-2)/2):0) + ')';
		/*画点*/
		ctx.fillRect(coord[0]*100 + 200 , coord[1]*100 + 200 - 2, 1, 1);
	}
}

function resetInputs(sectionIndex) {
	document.querySelectorAll('.ctn')[sectionIndex].querySelectorAll('input').forEach(el=>el.value=el.defaultValue);
	repaint(sectionIndex);
}

function changeAxis(sectionIndex,dir){
	var v = JSON.parse(document.querySelectorAll('.axis')[sectionIndex].value);
	var delta = JSON.parse(document.querySelectorAll('.axisDelta')[sectionIndex].value);
	for(var i=0;i<v.length;i++){
		for(var j=0;j<v[i].length;j++){
			v[i][j] = normalizeNum(v[i][j] + delta[i][j]*dir);
		}
	}
	document.querySelectorAll('.axis')[sectionIndex].value = JSON.stringify(v);
	repaint(sectionIndex);
	window.setTimeout(function(){ if(documentMouseDown) changeAxis(sectionIndex,dir)},30)
}

function changeTopPoint(sectionIndex,dir){
	var v = JSON.parse(document.querySelectorAll('.topPoint')[sectionIndex].value);
	var delta = JSON.parse(document.querySelectorAll('.topPointDelta')[sectionIndex].value);
	for(var i=0;i<v.length;i++){
		v[i] = normalizeNum(v[i] + delta[i]*dir);
	}
	document.querySelectorAll('.topPoint')[sectionIndex].value = JSON.stringify(v);
	repaint(sectionIndex);
	window.setTimeout(function(){ if(documentMouseDown) changeTopPoint(sectionIndex,dir)},30)
}

function changeRadian(sectionIndex,dir){
	var v = JSON.parse(document.querySelectorAll('.radian')[sectionIndex].value);
	var delta = JSON.parse(document.querySelectorAll('.radianDelta')[sectionIndex].value);
	v = normalizeNum(v + delta*dir);
	v = Math.max(0,v);
	v = Math.min(2*Math.PI,v);
	document.querySelectorAll('.radian')[sectionIndex].value = JSON.stringify(v);
	repaint(sectionIndex);
	window.setTimeout(function(){ if(documentMouseDown) changeRadian(sectionIndex,dir)},30)
}

function normalizeNum(num){
	return num.toFixed(10)*1;
}

var documentMouseDown=false;
document.onmousedown = function(){
	documentMouseDown=true;
}
document.onmouseup = function(){
	documentMouseDown=false;
}

refreshPoints(0);
refreshPoints(1);
refreshPoints(2);

</script>

</body>
</html>