Published
Edited
Jun 1, 2022
2 forks
Importers
74 stars
Insert cell
Insert cell
Insert cell
attitude = require("attitude@0.2")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
v = attitude({ axis: [10, 50], angle: 12 }).vector() // export to vector representation
Insert cell
attitude().vector(v) // import from vector representation
Insert cell
Insert cell
Insert cell
Insert cell
attitude().inverse()
Insert cell
Insert cell
attitude().power(0.25)
Insert cell
Insert cell
// Euler angles must but composed in the correct order
attitude([0, 0, 10]).compose(attitude([0, -35, 0]).compose(attitude([40, 0, 0])))
.angles()
Insert cell
attitude([40, 0, 0])
.compose(attitude([0, -35, 0]))
.compose(attitude([0, 0, 10]))
.angles()
Insert cell
Insert cell
frac = attitude([40, 0, 0])
.compose(attitude([0, 10, 0]).inverse())
.angles()
Insert cell
Insert cell
A = [-90, 0]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drag(projection) {
let p,
a = attitude();

function dragstarted({x,y}) {
a.angles(projection.rotate());
p = projection.rotate([0, 0]).invert([x, y]);
d3.select(this).style("cursor", "grabbing");
}

function dragged({x,y}) {
const q = projection.rotate([0, 0]).invert([x, y]);
projection.rotate(
attitude()
.arc(p, q)
.compose(a)
.angles()
);
}

function dragended() {
d3.select(this).style("cursor", "grab");
}

return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
function antipode(pole) {
return [pole[0] - 180, -pole[1]];
}
Insert cell
Insert cell
Insert cell
world = d3.json(
"https://unpkg.com/visionscarto-world-atlas@0.0.6/world/110m_land.geojson"
)
Insert cell
function show(attitude, options = {}) {
const m = 10;
const height = Math.min(318, width - 100),
context = DOM.context2d(width, height),
projection = d3
.geoOrthographic()
.fitExtent([[m, m], [height - m, height - m]], { type: "Sphere" })
.rotate(attitude.angles()),
path = d3.geoPath(projection, context);

function globe() {
context.fillStyle = "#777";
context.strokeStyle = "#000";

context.beginPath();
context.lineWidth = 0.25;
path(d3.geoGraticule10());
context.lineWidth = 0.5;
context.stroke();

context.beginPath();
path(world);
context.fill();

context.beginPath();
path({ type: "Sphere" });
context.lineWidth = 1;
context.stroke();
}

function render() {
context.save();
context.clearRect(0, 0, width, height);
globe();
if (options.axisAngle) draw_axis_angle(context, projection);

attitude.angles(projection.rotate());

const B = options.A ? attitude(A) : null;
if (B) {
context.beginPath();
path({ type: "MultiPoint", coordinates: [A, B] });
context.fillStyle = "blue";
context.fill();
}

context.fillStyle = "#000";
context.translate(height, 20);
if (options.euler) {
const r = projection.rotate();
context.fillText(`Euler Angles`, 0, 0);
context.fillText(`λ = ${+r[0].toFixed(2)}°`, 0, 20);
context.fillText(`φ = ${+r[1].toFixed(2)}°`, 0, 40);
context.fillText(`γ = ${+r[2].toFixed(2)}°`, 0, 60);
}

if (B) {
context.fillText(`A`, 0, 0);
context.fillText(`lon = ${+A[0].toFixed(2)}°`, 0, 20);
context.fillText(`lat = ${+A[1].toFixed(2)}°`, 0, 40);
context.fillText(`B`, 0, 70);
context.fillText(`lon = ${+B[0].toFixed(2)}°`, 0, 90);
context.fillText(`lat = ${+B[1].toFixed(2)}°`, 0, 110);
} else if (options.axisAngle) {
const axis = attitude.axis();
context.fillText(`Axis`, 0, 0);
context.fillText(`lon = ${+axis[0].toFixed(2)}°`, 0, 20);
context.fillText(`lat = ${+axis[1].toFixed(2)}°`, 0, 40);
context.fillText(`angle = ${+attitude.angle().toFixed(2)}°`, 0, 60);
}

if (options.matrix) {
const m = attitude.matrix();
context.fillText(`Matrix`, 0, 0);
for (let i = 0; i < 3; i++)
for (let j = 0; j < 3; j++) {
const a = +m[i][j].toFixed(5);
context.fillText(a, j * 65 - 3 * (a < 0), 20 + i * 20);
}
}

if (options.quaternion) {
const q = attitude.versor();
context.fillText(`Versor`, 0, 0);
for (let j = 0; j < 4; j++) {
const a = +q[j].toFixed(5);
context.fillText(a, j * 65 - 3 * (a < 0), 20);
}
}

context.restore();
}

if (options.drag)
d3.select(context.canvas)
.style("cursor", "grab")
.call(
drag(projection, options)
.on("drag.render", render)
.on("end.render", render)
);

return Object.assign(
d3
.select(context.canvas)
.call(render)
.node(),
{ projection, render }
);
}
Insert cell
function draw_axis_angle(context, projection) {
const a = attitude().angles(projection.rotate());
const path = d3.geoPath(projection, context);

context.save();

const axisPts = [a.axis(), antipode(a.axis())];

context.save();
context.globalCompositeOperation = "destination-over";
context.fillStyle = "red";
context.beginPath();
const p = projection(axisPts[0]);
context.moveTo(...p);
context.arc(...p, 2, 0, 2 * pi);
const q = projection(axisPts[1]);
context.moveTo(...q);
context.arc(...q, 2, 0, 2 * pi);
path({ type: "MultiPoint", coordinates: axisPts });
context.fill();

context.strokeStyle = "red";
context.beginPath();
let s = -.1;
context.moveTo((1 - s) * p[0] + s * q[0], (1 - s) * p[1] + s * q[1]);
context.lineTo(...p);
context.stroke();

context.beginPath();
context.moveTo(...p);
s = 1.;
context.lineTo((1 - s) * p[0] + s * q[0], (1 - s) * p[1] + s * q[1]);
context.setLineDash([2, 6]);
context.stroke();

context.restore();

const angle = a.angle();
if (angle) {
const k = 65; // parallel
const [a0, a1] = false ? [-angle, 0] : [0, angle];
const pts = [
...d3.range(a0, a1, Math.sign(angle)).map(a => [a, k]),
[a1, k],
[a1 - 2 * Math.sign(angle), k + 1.5],
[a1, k],
[a1 - 2.5 * Math.sign(angle), k - 1.5],
[a1, k]
];
//return pts;
const axis = a.axis();
context.beginPath();
path({
type: "LineString",
coordinates: pts.map(d3.geoRotation([-axis[0], 90 - axis[1]]).invert)
});
path({
type: "LineString",
coordinates: pts
.map(d => [-d[0], d[1]])
.map(d3.geoRotation([-axis[0], 90 - axis[1], 180]).invert)
});
context.lineWidth = 1;
context.strokeStyle = "red";
context.stroke();
}

context.restore();
}
Insert cell
pi = Math.PI
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more