|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
|
|
<style> |
|
|
|
label { |
|
display: inline-block; |
|
width: 5em; |
|
font-family: sans-serif; |
|
font-size: 1.1em; |
|
} |
|
legend { |
|
display: inline-block; |
|
width: 20em; |
|
font-family: sans-serif; |
|
font-size: 1.1em; |
|
} |
|
span { |
|
font-size: 1.1em; |
|
} |
|
|
|
input[type="range"] { |
|
width: 20em; |
|
} |
|
input[type="number"] { |
|
width: 4em; |
|
} |
|
|
|
canvas, svg { |
|
position: absolute; |
|
top: 5; |
|
left: 0; |
|
} |
|
|
|
#redrawButton { |
|
position: absolute; |
|
left: 100px; |
|
top: 40px; |
|
} |
|
|
|
#initialParams { |
|
position: absolute; |
|
left: 450px; |
|
top: 0px; |
|
} |
|
|
|
.axisLabel { |
|
font-family: sans-serif; |
|
font-size: 20; |
|
} |
|
</style> |
|
|
|
<div> |
|
<div id="initialParams"> |
|
<legend>Initial conditions:</legend> |
|
x: |
|
<input type="number" id="xi" value="1.5"><br> |
|
y: |
|
<input type="number" id="yi" value="3.2"><br> |
|
z: |
|
<input type="number" id="zi" value="0.4"><br> |
|
<button id="redrawButton">Redraw</button> |
|
</div> |
|
<div> |
|
<label>Z rotation:</label> |
|
<input type="range" min="0" max="360" id="theta" value=30> |
|
<span id="theta-value">30</span> |
|
</div> |
|
<div> |
|
<label>X rotation:</label> |
|
<input type="range" min="0" max="360" id="phi" value=10> |
|
<span id="phi-value">10</span> |
|
</div> |
|
<div> |
|
<label>Scale:</label> |
|
<input type="range" id="scale" min="0.01" max="5" step="0.01" value=0.9> |
|
<span id="scale-value">0.9</span> |
|
</div> |
|
</div> |
|
<div> |
|
<canvas width="960" height="500"></canvas> |
|
<svg width="960" height="500"></svg> |
|
</div> |
|
|
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
|
|
//canvas linear gradient |
|
const canvas = document.querySelector("canvas"), |
|
context = canvas.getContext("2d"); |
|
|
|
const canvasGradient = context.createLinearGradient(80, 0, 960, 0); |
|
canvasGradient.addColorStop(0, '#EDCA3A'); |
|
canvasGradient.addColorStop(0.25, '#F25754'); |
|
canvasGradient.addColorStop(0.50, '#1FBAD6'); |
|
canvasGradient.addColorStop(0.75, '#E6B0F1'); |
|
canvasGradient.addColorStop(1, '#8BC53F'); |
|
|
|
//Append marker in svg defs |
|
const svg = d3.select("svg") |
|
const defs = svg.append("defs"); |
|
const marker = defs.append('marker') |
|
.attr("id", "arrow") |
|
.attr("refX", 12) |
|
.attr("refY", 6) |
|
.attr("markerUnits", 'userSpaceOnUse') |
|
.attr("markerWidth", 12) |
|
.attr("markerHeight", 18) |
|
.attr("orient", 'auto') |
|
.append('path') |
|
.attr("d", 'M 0 0 12 6 0 12 3 6'); |
|
|
|
//initial conditions and parameters for Rossler system |
|
let t = 0.001, |
|
iter = 0 |
|
|
|
const dequanliParameters = { |
|
a: 40, |
|
c: 11.0/6.0, |
|
d: 0.16, |
|
e: 0.65, |
|
k: 55, |
|
f: 20 |
|
} |
|
|
|
//initial angles parameters |
|
let theta = +d3.select("#theta-value").text(), |
|
phi = +d3.select("#phi-value").text(), |
|
scaleFactor = +d3.select("#scale-value").text(), |
|
x = +d3.select("#xi")._groups[0][0].value, |
|
y = +d3.select("#yi")._groups[0][0].value, |
|
z = +d3.select("#zi")._groups[0][0].value |
|
|
|
let rotTheta = theta * - Math.PI / 180, //z axis rotation (20deg) |
|
rotPhi = phi * Math.PI / 180, //y axis rotation (20deg) |
|
initialPoint; |
|
|
|
const dx = 480, |
|
dy = 250, |
|
centerOrigin = {x: 0, y: 0, z: 0}, //rotation origin |
|
currentProjection = orthoProjectXZ //add other projections later? |
|
|
|
//To store spatial coordinates |
|
const Vertex = function(x, y, z) { |
|
this.x = parseFloat(x); |
|
this.y = parseFloat(y); |
|
this.z = parseFloat(z); |
|
}; |
|
|
|
const Vertex2D = function(x, y) { |
|
this.x = parseFloat(x); |
|
this.y = parseFloat(y); |
|
}; |
|
|
|
function projectPoint(d){ |
|
//rotate point in space and return current chosen projection |
|
let rotatedPoint = rotateVertex(d, initialPoint, rotTheta, rotPhi) |
|
//initial drawn point will be drawn at (dx,dy), and rotation will be done around it |
|
rotatedPoint.x = rotatedPoint.x - initialPoint.x |
|
rotatedPoint.y = rotatedPoint.y - initialPoint.y |
|
rotatedPoint.z = rotatedPoint.z - initialPoint.z |
|
|
|
let projectedPoint = currentProjection(rotatedPoint, dx, dy, scaleFactor) |
|
return projectedPoint |
|
} |
|
|
|
//path generator on the projected data |
|
const lineTransform = d3.line() |
|
.x(d => projectPoint(d).x) |
|
.y(d => projectPoint(d).y) |
|
.curve(d3.curveCardinal) |
|
.context(context); |
|
|
|
const lineGeneratorAxes = d3.line() |
|
.x(d => d.x) |
|
.y(d => d.y) |
|
.curve(d3.curveBasis) |
|
|
|
// svg group element for the axes |
|
const gTrackMain = svg.append("g") |
|
.attr("class", "gTrack") |
|
.attr("transform", |
|
`translate(80, 150)`) |
|
|
|
function drawScene() { |
|
//initial dataPoint |
|
let dataPoint = new Vertex(x, y, z) |
|
//initial transformed point, to set initial drawn point to zero |
|
initialPoint = rotateVertex(new Vertex(x, y, z), new Vertex(x, y, z), rotTheta, rotPhi) |
|
|
|
//store the iterations of the solution of Rossler |
|
let dataMain = [] |
|
dataMain.push(new Vertex(x, y, z)) |
|
|
|
drawAxes() |
|
|
|
let timeInterval = d3.timer(function() { |
|
dequanli(drawMain) |
|
}); |
|
|
|
///////////////////////////////////////// |
|
function dequanli(callback) { |
|
//update dataPoint position |
|
dataPoint.x += t * (dequanliParameters.a * (dataPoint.y - dataPoint.x) + dequanliParameters.d * dataPoint.x * dataPoint.z) |
|
dataPoint.y += t * (dequanliParameters.k * dataPoint.x + dequanliParameters.f * dataPoint.y - dataPoint.x * dataPoint.z) |
|
dataPoint.z += t * (dequanliParameters.c * dataPoint.z + dataPoint.x * dataPoint.y - dequanliParameters.e * Math.pow(dataPoint.x, 2)) |
|
|
|
callback(new Vertex(dataPoint.x, dataPoint.y, dataPoint.z)); |
|
} |
|
//callback |
|
function drawMain(point) { |
|
//add latest point projection to data |
|
dataMain.push(point) |
|
|
|
// let projectedPoint = currentProjection(point, dx, dy, scaleFactor) |
|
let projectedPoint = projectPoint(point) |
|
|
|
//update visualization of dot and dotTrack |
|
context.clearRect(0, 0, 960, 500); |
|
context.beginPath(); |
|
lineTransform(dataMain); |
|
context.lineWidth = 0.5; |
|
context.strokeStyle = canvasGradient; |
|
context.stroke(); |
|
//dot |
|
context.beginPath(); |
|
context.arc(projectedPoint.x, projectedPoint.y, 3, 0, 2*Math.PI); |
|
context.fillStyle = 'rgba(225,225,225,0.3)'; |
|
context.fill() |
|
context.strokeStyle = 'gray'; |
|
context.stroke(); |
|
} |
|
} |
|
|
|
function drawAxes(){ |
|
|
|
const axisLabels = ['x', 'y', 'z'] |
|
|
|
//axes vertex and projection of axes |
|
let axeCenter = rotateVertex(new Vertex(0, 0, 0), centerOrigin, rotTheta, rotPhi) |
|
let axeX = rotateVertex(new Vertex(50, axeCenter.y, axeCenter.z), centerOrigin, rotTheta, rotPhi) |
|
let axeY = rotateVertex(new Vertex(axeCenter.x, 50, axeCenter.z), centerOrigin, rotTheta, rotPhi) |
|
let axeZ = rotateVertex(new Vertex(axeCenter.x, axeCenter.y, 50), centerOrigin, rotTheta, rotPhi) |
|
|
|
|
|
let axeSystemMain = [ |
|
[currentProjection(axeCenter), currentProjection(axeX)], //x axis line |
|
[currentProjection(axeCenter), currentProjection(axeY)], //y axis line |
|
[currentProjection(axeCenter), currentProjection(axeZ)] //z axis line |
|
] |
|
|
|
//add paths for axes |
|
const axes = gTrackMain.selectAll(".axisPath") |
|
.data(axeSystemMain) |
|
|
|
axes.exit().remove() |
|
|
|
const axesEnter = axes.enter() |
|
.append("path") |
|
.attr("class", "axisPath") |
|
.attr("d", d => lineGeneratorAxes(d)) |
|
.attr("stroke", "black") |
|
.attr("marker-end", "url(#arrow)") |
|
.merge(axes) |
|
.attr("d", d => lineGeneratorAxes(d)) |
|
|
|
const axesText = gTrackMain.selectAll(".axisLabel") |
|
.data(axeSystemMain) |
|
|
|
axesText.exit().remove() |
|
|
|
const axesTextEnter = axesText.enter() |
|
.append("text") |
|
.attr("class", "axisLabel") |
|
.attr("x", d => d[1].x + 5) |
|
.attr("y", d => d[1].y - 2) |
|
.text((d,i) => axisLabels[i]) |
|
.merge(axesText) |
|
.attr("x", d => d[1].x + 5) |
|
.attr("y", d => d[1].y - 2) |
|
} |
|
|
|
//orthographic projections for each axe |
|
function orthoProjectXZ(M, dx=0, dy=0, scale = 1) { |
|
return new Vertex2D(dx + scale * M.x, dy + scale * - M.z); |
|
} |
|
function orthoProjectXY(M, dx=0, dy=0, scale = 1) { |
|
return new Vertex2D(dx + scale * M.x, dy + scale * - M.y); |
|
} |
|
function orthoProjectYZ(M, dx=0, dy=0, scale = 1) { |
|
return new Vertex2D(dx + scale * M.z, dy + scale * - M.y); |
|
} |
|
|
|
// Rotate a vertice |
|
function rotateVertex(M, center, theta, phi) { |
|
// Rotation matrix coefficients |
|
const ct = Math.cos(theta); |
|
const st = Math.sin(theta); |
|
const cp = Math.cos(phi); |
|
const sp = Math.sin(phi); |
|
|
|
// Rotation |
|
const x = M.x - center.x; |
|
const y = M.y - center.y; |
|
const z = M.z - center.z; |
|
|
|
return new Vertex( |
|
ct * x - st * cp * y + st * sp * z + center.x, |
|
st * x + ct * cp * y - ct * sp * z + center.y, |
|
sp * y + cp * z + center.z, |
|
) |
|
} |
|
|
|
d3.select("#theta") |
|
.on("input", function () { |
|
//update theta and displayed value |
|
theta = +this.value |
|
d3.select("#theta-value").text(theta) |
|
rotTheta = theta * - Math.PI / 180 //update rotTheta |
|
drawAxes() |
|
}); |
|
|
|
d3.select("#phi") |
|
.on("input", function () { |
|
//update phi and displayed value |
|
phi = +this.value |
|
d3.select("#phi-value").text(phi) |
|
rotPhi = phi * Math.PI / 180 //update rotPhi |
|
drawAxes() |
|
}); |
|
|
|
d3.select("#scale") |
|
.on("input", function () { |
|
//update phi and displayed value |
|
scaleFactor = +this.value |
|
d3.select("#scale-value").text(scaleFactor) |
|
drawAxes() |
|
}); |
|
|
|
d3.select('#redrawButton') |
|
.on('click', function() { |
|
x = +d3.select("#xi")._groups[0][0].value |
|
y = +d3.select("#yi")._groups[0][0].value |
|
z = +d3.select("#zi")._groups[0][0].value |
|
initialPoint = rotateVertex(new Vertex(x, y, z), new Vertex(x, y, z), rotTheta, rotPhi) |
|
drawScene() |
|
}) |
|
|
|
drawScene() |
|
|
|
|
|
</script> |