Skip to content

Instantly share code, notes, and snippets.

@gcalmettes
Last active May 2, 2021 20:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gcalmettes/b470179e1707700463d236525a0c3613 to your computer and use it in GitHub Desktop.
Save gcalmettes/b470179e1707700463d236525a0c3613 to your computer and use it in GitHub Desktop.
Visual exploration of the DeQuan Li attractor

Real-time orthographic projection of the Dequan Li attractor.

The attractor shape can be explored while the attractor is live-rendered by selecting different sets of angles to rotate the scene along the X and Z axes. Initials values of x,y,z (initial data point) can be chosen as well.

The rendering of the attractor is done by canvas, while the axis are rendered by svg.

Visualization of other attractors:

<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment