angle = (height = 400, showBeta = false, showTheta = true, initialTheta = pi / 4) => {
let currentTheta = initialTheta;
const svg = DOM.svg(width, height);
const canvas = d3.select(svg);
canvas.style('border', '1px solid rgba(0,0,0,0.05)')
.style('font-family', 'monospace');
const origin = {x: width/2, y: height/2}
const rayLength = width > height / 3 ? height / 3 : width / 3;
const scaleX = d3.scaleLinear()
.domain([-1, 1])
.range([origin.x - rayLength, origin.x + rayLength]);
const scaleY = d3.scaleLinear()
.domain([1, -1])
.range([origin.y - rayLength, origin.y + rayLength]);
const arcTransform = `translate(${origin.x}, ${origin.y}), rotate(90)`
/* Helpers */
// Convert radians to arc units
// @see https://github.com/d3/d3-shape#arc_endAngle
const angle2Arc = (angle) => - angle;
// Calculate Radius (Ray) Endpoint Coordinates
const radiusEndpoint = (angle) => [scaleX(Math.cos(angle)), scaleY(Math.sin(angle))];
// Calculate Radius (Ray) Coordinates
const radiusCoords = (angle) => [[origin.x, origin.y ], radiusEndpoint(angle)];
/* Variables */
const arcStartAngle = 0;
/* Elements */
// Create the visualization group element
const vis = canvas.append('g').attr('class', 'visualization');
// Draw the initial point / vertex
const point = vis.append('circle')
.style("fill", 'black')
.attr("cx", origin.x)
.attr("cy", origin.y)
.attr("r", 2);
// Arcs
// Angle Arc Generator
const arcGen = d3.arc()
.outerRadius(39)
.innerRadius(40)
.startAngle(arcStartAngle)
// Theta angle arc
const thetaArc = vis.append('path')
.attr('class', 'arc arc-theta')
.attr('fill', 'transparent')
.attr('stroke', showTheta ? colors.thetaArc : 'transparent')
.attr('transform', arcTransform)
// Beta angle arc
const betaArc = vis.append('path')
.attr('class', 'arc arc-beta')
.attr('fill', 'transparent')
.attr('stroke', showBeta ? colors.betaArc : 'transparent')
.attr('transform', arcTransform);
// Rays
const rayGen = d3.line();
// Static Ray
const staticRay = vis.append('path')
.attr('class', 'ray ray--static')
.attr('stroke', 'black')
.attr('marker-end', (d) => "url(#arrow)")
.attr('d', rayGen(radiusCoords(arcStartAngle)));
// Dynamic Ray
const dynamicRay = vis.append('path')
.attr('class', 'ray ray--dynamic')
.attr("stroke", "black")
.attr('marker-end', (d) => "url(#arrow)");
/* Annotations */
// Create Annotations Group
const annotations = canvas.append('g')
.attr('class', 'annotations')
// Angle Labels
const labelArcGen = d3.arc()
.outerRadius(55)
.innerRadius(55)
.startAngle(arcStartAngle)
// Theta label
const thetaLabel = annotations.append("text")
.attr('class', 'annotation annotation-theta')
.attr('fill', showTheta ? colors.thetaArc : 'transparent')
.attr('dx','-4')
.attr('dy','4')
.text('θ');
// Beta label
const betaLabel = annotations.append("text")
.attr('class', 'annotation annotation-beta')
.attr('fill', showBeta ? colors.betaArc : 'transparent')
.attr('dx','-4')
.attr('dy','4')
.text('β');
// For debugging
/*
const labelArc = vis.append('path')
.attr('class', 'arc arc-label')
.attr('fill', 'transparent')
.attr('stroke', 'blue')
.attr('transform', arcTransform);
const labelPoint = vis.append('circle')
.style("fill", 'red')
.attr("r", 2);
*/
// Point label
annotations.append('text')
.attr('class', 'annotation annotation-point')
.attr('dx', -5)
.attr('dy', 20)
.attr('transform', `translate(${origin.x}, ${origin.y})`)
.text('O')
.attr('fill','black')
// Add an arrow-head marker to the ray
canvas.append("svg:defs").append("svg:marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
//.attr('refX', 0) //Adjust the arrow head position along the line
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
/* Methods */
const updatePositions = (angle) => {
// Arcs
// Theta Arc
thetaArc.attr('d', arcGen.endAngle(angle2Arc(angle)));
// Beta Arc
betaArc.attr('d', arcGen.endAngle(angle2Arc(angle) + 2 * pi ));
// Ray
dynamicRay.attr('d', rayGen(radiusCoords(angle)));
// Labels
// Theta Label
const thetaLabelCentroid = labelArcGen.endAngle(angle2Arc(angle) + pi).centroid();
thetaLabel.attr('transform', `translate(${thetaLabelCentroid[0] + origin.x}, ${thetaLabelCentroid[1] + origin.y})`)
// Beta Label
const betaLabelCentroid = labelArcGen.endAngle(angle2Arc(angle) + 3 * pi ).centroid();
betaLabel.attr('transform', `translate(${betaLabelCentroid[0] + origin.x}, ${betaLabelCentroid[1] + origin.y})`)
// For Debugging
//labelArc.attr('d', labelArcGen.endAngle(angle2Arc(angle)));
//labelPoint.attr('cx', thetaLabelCentroid[0] + origin.x).attr('cy', thetaLabelCentroid[1] + origin.y);
} // END updatePositions
const handleMouse = (x,y) => {
// Get the current angle using the arctangent
// Calculate the arctangent
const a = Math.atan2( y - origin.y, x - origin.x);
// Calculate the actual angle
const angle = a > 0 ? 2 * pi - a : - a;
// Set the external theta angle
currentTheta = angle;
svg.value = currentTheta;
svg.dispatchEvent(new CustomEvent('input'));
// Update the visualization
updatePositions(angle);
}
// Mouse position listener
canvas.on("mousemove", function() {
const mouse = d3.mouse(this);
// Update element positions
handleMouse(mouse[0], mouse[1]);
});
/* Initialization */
updatePositions(currentTheta);
svg.value = currentTheta;
return svg;
}