Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@bmershon
Last active June 30, 2017 04:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmershon/231160f11b33ac780bd86b5a7e891576 to your computer and use it in GitHub Desktop.
Save bmershon/231160f11b33ac780bd86b5a7e891576 to your computer and use it in GitHub Desktop.
An Interesting Lattice
height: 960
border: no
license: MIT
<!DOCTYPE html>
<meta charset="utf-8">
<title>Reflections</title>
<style>
body {
cursor: crosshair;
}
.vertex {
pointer-events: none;
stroke: none;
}
.vertex.coprime {
stroke: #000;
stroke-width: 1px;
}
.vertex.invalid {
fill-opacity: 0.1;
stroke-opacity: 0.3;
}
.node {
fill: #fff;
fill-opacity: 0;
stroke-width: 0;
}
.node.active {
stroke-width: 1px;
stroke: #ccc;
stroke-dasharray: 5, 5;
}
.node.terminal {
stroke-width: 1px;
stroke: #000;
}
.edge {
stroke: #ccc;
stroke-width: 1px;
pointer-events: none;
}
.edge.reflection {
stroke: #333;
stroke-width: 1px;
}
.beam {
stroke-width: 1.5px;
stroke: rgba(240, 0, 0, 0.8);
}
.extension {
stroke-width: 1px;
stroke: rgba(240, 0, 0, 0.8);
stroke-dasharray: 5, 5;
}
text {
fill: #aaa;
font-size: 16px;
font-family: monospace;
pointer-events: none;
}
text.valid {
fill: #000;
}
</style>
<svg width="960" height="960"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="intersection.debug.js"></script>
<script>
// 2017, Brooks Mershon
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
margin = { top: 20, left: 20, right: 20, bottom: 20};
// skewX(30) rotate(240)
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + margin.top + ")");
// The number of dots in the last row.
var N = 25;
var side = (width - margin.left - margin.right) / N;
var height = side * Math.sqrt(3) / 2;
var emitter = new Vertex(0, 0, 0, 0, 0);
var vertices = [ emitter ];
// Hash table for looking up vertices.
var table = { 0: { 0: vertices[0] }};
var edges = [];
var beam = {
source: vertices[0],
target: vertices[0]
};
var extendedBeam = {
source: vertices[0],
target: vertices[0]
};
// Create layout of vertices and edges.
previousRow = vertices.slice(0, 1);
for (let i = 1; i < N; i++) {
let row = new Array(i + 1);
for (let j = 0; j < i + 1; j++) {
let label,
newVertex,
newEdge;
// Compute position.
let x = -side * i / 2 + j * side;
let y = i * height;
// Infer label.
if (j === 0) { // First vertex in row.
let t = previousRow[0];
label = (t.label + 1) % 3;
newVertex = new Vertex(x, y, label, i, j);
edges.push(new Edge(t, newVertex, 1));
} else { // Insect two other vertices of the triangle to infer vertex label.
let p = row[j - 1];
let q = previousRow[j - 1];
let r = previousRow[j];
label = inferLabel(p.label, q.label);
newVertex = new Vertex(x, y, label, i, j);
edges.push(
new Edge(p, newVertex, 1),
new Edge(q, newVertex, 1));
if (j < i) {
edges.push(new Edge(r, newVertex, 1));
}
}
if (!table[i]) {
table[i] = {};
}
table[i][j] = newVertex;
row[j] = newVertex;
}
// Keep auxiliary information for the next row to infer colorings using the
// previously created row of vertices.
previousRow = row;
vertices = vertices.concat(row);
}
// Create a vertex.
function Vertex(x, y, label, row, index) {
this.x = x;
this.y = y;
this.label = label;
this.row = row;
this.index = index;
}
// Create an edge.
function Edge(a, b, label) {
this.a = a;
this.b = b;
this.label = label;
}
// Returns the implied coloring if the other two vertices of a triangle are
// colored x and y. The colors are 0, 1, or 2.
function inferLabel(x, y) {
return ((3 - (x + y) % 3) % 3);
}
var vertexColor = d3.scaleOrdinal()
.domain([0, 2])
.range(["#EB9394", "#96D096", "#8FBBDA"]);
var laser = g.append("line")
.datum(beam)
.classed("beam", true)
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
var extension = g.append("line")
.datum(extendedBeam)
.classed("extension", true)
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
var line = g.selectAll(".edge")
.data(edges)
.enter().append("line")
.attr("x1", (d) => d.a.x)
.attr("y1", (d) => d.a.y)
.attr("x2", (d) => d.b.x)
.attr("y2", (d) => d.b.y)
.classed("edge", true);
var circle = g.selectAll(".vertex")
.data(vertices);
var circleGroup = circle.enter().append("g");
var node = circleGroup.append("circle")
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.classed("node", true)
.attr("r", side * 0.49)
.on("mouseover", inspect)
.on("mouseleave", function(d, i) {
d3.select(this).classed("active", false);
d3.select(this).classed("terminal", false);
});
var vertex = circleGroup.append("circle")
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.attr("r", Math.min(0.1 * side, 5))
.classed("vertex", true)
.classed("coprime", (d) => coprime(d.row, d.index))
.attr("fill", (d, i) => vertexColor(d.label));
function inspect(d, i) {
let reflections = 0;
let terminal = d;
let factor = gcd(terminal.row, terminal.index);
if (factor > 1) {
terminal = table[terminal.row / factor][terminal.index / factor];
}
beam.target = terminal;
extendedBeam.source = beam.target;
extendedBeam.target = d;
d3.select(this).classed((d === terminal && d !== emitter)
? "terminal"
: "active", true);
vertex
.classed("invalid", (vertex) => {
return d.label === 0 && d === terminal
&& ((!coprime(vertex.row, vertex.index)
|| vertex.row !== d.row
|| vertex.label > 0));
});
// Horrible intersection test against all edges.
line.each(function(edge, i) {
let include = false;
if (edge.a.row < terminal.row && edge.b.row < terminal.row
&& emitter !== edge.a && emitter !== edge.b
&& terminal !== edge.a && terminal !== edge.b) {
include = intersection(0, 0, terminal.x, terminal.y,
edge.a.x, edge.a.y, edge.b.x, edge.b.y);
if (include) reflections++;
}
d3.select(this).classed("reflection", include);
});
laser
.datum(beam)
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
extension
.datum(extendedBeam)
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
}
// Returns the greatest common denominator.
function gcd(a, b) {
if (!b) {
return a;
}
return gcd(b, a % b);
};
function coprime(a, b) {
return gcd(a, b) === 1;
}
</script>
// Code snippet used from https://gist.github.com/Joncom/e8e8d18ebe7fe55c3894.
// Returns true if an intersection exists and false otherwise.
function intersection(p0_x, p0_y, p1_x, p1_y, p2_x, p2_y, p3_x, p3_y) {
let s1_x, s1_y, s2_x, s2_y;
s1_x = p1_x - p0_x;
s1_y = p1_y - p0_y;
s2_x = p3_x - p2_x;
s2_y = p3_y - p2_y;
let s, t;
s = (-s1_y * (p0_x - p2_x) + s1_x * (p0_y - p2_y)) / (-s2_x * s1_y + s1_x * s2_y);
t = ( s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / (-s2_x * s1_y + s1_x * s2_y);
return (s >= 0 && s <= 1 && t >= 0 && t <= 1);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment