Skip to content

Instantly share code, notes, and snippets.

@dsaffo
Last active June 25, 2023 20:42
Show Gist options
  • Save dsaffo/02e285a64149618cfe8429a323f9c91a to your computer and use it in GitHub Desktop.
Save dsaffo/02e285a64149618cfe8429a323f9c91a to your computer and use it in GitHub Desktop.
Planarity Party

VisConnect Planarity Party

This VisConnect example is adapted from this web game. The only changes are adding the PeerJS and VisConnect script tags, and replacing d3.drag() with vc.drag(). Click the visconnect logo on the bottom right and send the coppied URL to a friend. Once they open the link all your interactions will be synchronized!

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Planarity Party</title>
<link href="planarity.css" rel="stylesheet">
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/peerjs@1.0.4/dist/peerjs.min.js"></script>
<script src="https://unpkg.com/visconnect@latest/visconnect-bundle.js"></script>
</head>
<body collaboration>
<p>Can you untangle the graph? See if you can position the vertices so that no two lines cross.
<p><b>Level <span id="levelindicator"></span></b>. Number of line crossings detected: <span id="count">0</span>
<p style="font-style: italic">
<span id="move-count">0 moves</span>.
<button onclick="generate()" id="nextlevel" disabled="disabled">Next Level</button>
</p>
<div id="vis"></div>
<div id="force"></div>
<!--<input type="hidden" value="8" id="nodes" min="4">-->
<!--<p><label for="nodes">Number of vertices:</label>
<input type="number" value="8" id="nodes" min="4">
<button id="generate" class="first last">Generate new, random graph</button>-->
<!--<p><label for="intersections"><input type="checkbox" id="intersections"> Highlight non-intersecting lines</label>.
<p>Don't worry, the game only generates solvable graphs! These are known as <a href="http://en.wikipedia.org/wiki/Planar_graph">planar graphs</a>.
<h2>Further Reading</h2>
<ul>
<li><a href="http://www.planarity.net/">John Tantalo's original version</a> of the puzzle.
<li>Information about the <a href="http://en.wikipedia.org/wiki/Planarity">Planarity</a> game on Wikipedia.
<li><a href="http://en.wikipedia.org/wiki/Planar_graph">Planar graph</a> on Wikipedia.
</ul>
<p><a href="http://mbostock.github.com/d3/">Built with D3</a>.-->
</body>
</html>
<p class="copyright">Adapted from <a href="https://www.jasondavies.com/planarity/">Jason Davies</a>.
<script src="planarity.js"></script>
body {
font-size: 1em;
font-weight: 400;
word-spacing: normal;
letter-spacing: normal;
text-transform: none;
font-family: Verdana, Myriad Web, Syntax, sans-serif;
font-size-adjust: .58;
color: #000;
background: #FFF;
line-height: 1.58em;
border-top: 0;
border-left: 0;
border-bottom: 0;
border-right: 0;
width: auto;
margin: 20px 50px;
padding: 0
}
circle {
fill: #fc0;
cursor: move;
stroke: #000;
stroke-width: 1.5px;
}
circle.intersection {
fill: #0cf;
}
line {
stroke: #fc0;
stroke-width: 1.5px;
}
line.intersection {
stroke: #000;
}
#vis svg {
border: 2px solid #ccc;
border-radius: 7px;
}
var w = 760,
h = 300,
p = 15,
x = d3.scaleLinear().range([0, w]),
y = d3.scaleLinear().range([0, h]),
start,
format = d3.format(",.1f"),
moves = 0,
highlightIntersections = false,
count = 0, // intersections
graph;
let level = 0;
graph = scramble(planarGraph(8));
d3.select("#vis").selectAll("*").remove();
var vis = d3.select("#vis").append("svg")
.attr("width", w + p * 2)
.attr("height", h + p * 2)
.append("g")
.attr("transform", "translate(" + [p, p] + ")");
const lines = vis.append("g");
const nodes = vis.append("g");
const counter = d3.select("#count");
const moveCounter = d3.select("#move-count");
const timer = d3.select("#timer");
const force = d3.select("#force").append("svg")
.attr("width", w + p * 2)
.attr("height", h + p * 2)
.append("g")
.attr("transform", "translate(" + [p, p] + ")");
const forceLines = force.append("g");
const forceNodes = force.append("g");
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter( (w + p * 2)/2, (h + p * 2)/2));
d3.select("#generate").on("click", generate);
d3.select("#intersections").on("change", function() {
highlightIntersections = this.checked;
update();
});
generate();
d3.timer(function() {
if (count) timer.text(format((+new Date - start) / 1000));
});
function generate() {
if(intersections(graph.links) !== 0 && level !== 0) {
return;
}
level++;
seed = level;
moveCounter.text("0 moves");
document.getElementById('levelindicator').innerText = level;
moves = 0;
start = +new Date;
lastCount = null;
forceLines.selectAll("line").remove();
forceNodes.selectAll("circle").remove();
graph = scramble(planarGraph(level + 4));
simulation.restart();
simulation.alpha(1);
update();
}
function update() {
count = intersections(graph.links);
counter.text(count ? count + "." : "0! Well done!");
const nextLevelButton = document.getElementById('nextlevel');
const forceDiv = document.getElementById('force');
if(count === 0) {
nextLevelButton.removeAttribute('disabled');
forceDiv.style.display = 'inline';
} else {
nextLevelButton.setAttribute('disabled', '');
forceDiv.style.display = 'none';
}
const line = lines.selectAll("line")
.data(graph.links);
const lineEnter = line.enter().append("line");
line.exit().remove();
lineEnter.merge(line).attr("x1", function(d) { return x(d[0][0]); })
.attr("y1", function(d) { return y(d[0][1]); })
.attr("x2", function(d) { return x(d[1][0]); })
.attr("y2", function(d) { return y(d[1][1]); })
.classed("intersection", highlightIntersections ? function(d) { return d.intersection; } : true);
const node = nodes.selectAll("circle")
.data(graph.nodes);
const nodeEnter = node.enter().append("circle");
nodeEnter
.attr("r", p - 1)
.call(vc.drag()
.on("drag", function(d) {
// Jitter to prevent coincident nodes.
const pos = vc.mouse(this);
d[0] = x.invert(pos[0]);
d[1] = y.invert(pos[1]);
update();
})
.on("end", function() {
moveCounter.text(++moves + " move" + (moves !== 1 ? "s" : ""));
}));
node.exit().remove();
nodeEnter.merge(node).attr("cx", function(d) { return x(d[0]); })
.attr("cy", function(d) { return y(d[1]); })
.classed("intersection", highlightIntersections ?
function(d) { return d.intersection; } : count);
const forceLine = forceLines.selectAll("line")
.data(graph.links)
.enter().append("line");
const forceNode = forceNodes.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()
.append("circle")
.attr("r", 2);
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
forceLine
.attr("x1", function(d) { return d[0].x; })
.attr("y1", function(d) { return d[0].y; })
.attr("x2", function(d) { return d[1].x; })
.attr("y2", function(d) { return d[1].y; });
forceNode
.attr("r", 16)
.style("fill", "#efefef")
.style("stroke", "#424242")
.style("stroke-width", "1px")
.attr("cx", function (d) { return d.x+5; })
.attr("cy", function(d) { return d.y-3; });
}
}
// Scramble the node positions.
function scramble(graph) {
if (graph.nodes.length < 4) return graph;
do {
graph.nodes.forEach(function(node) {
node[0] = vc.random();
node[1] = vc.random();
});
} while (!intersections(graph.links));
return graph;
}
// Generates a random planar graph with *n* nodes.
function planarGraph(n) {
var points = [],
links = [],
i = -1,
j;
while (++i < n) points[i] = [vc.random(), vc.random()];
i = -1; while (++i < n) {
addPlanarLink([points[i], points[~~(vc.random() * n)]], links);
}
i = -1; while (++i < n) {
j = i; while (++j < n) addPlanarLink([points[i], points[j]], links);
}
//console.log(links)
return {nodes: points, links: links};
}
// Adds a link if it doesn't intersect with anything.
function addPlanarLink(link, links) {
if (!links.some(function(to) { return intersect(link, to); })) {
links.push(link);
}
}
// Counts the number of intersections for a given array of links.
function intersections(links) {
var n = links.length,
i = -1,
j,
x,
count = 0;
// Reset flags.
while (++i < n) {
(x = links[i]).intersection = false;
x[0].intersection = false;
x[1].intersection = false;
}
i = -1; while (++i < n) {
x = links[i];
j = i; while (++j < n) {
if (intersect(x, links[j])) {
x.intersection =
x[0].intersection =
x[1].intersection =
links[j].intersection =
links[j][0].intersection =
links[j][1].intersection = true;
count++;
}
}
}
return count;
}
// Returns true if two line segments intersect.
// Based on http://stackoverflow.com/a/565282/64009
function intersect(a, b) {
// Check if the segments are exactly the same (or just reversed).
if (a[0] === b[0] && a[1] === b[1] || a[0] === b[1] && a[1] === b[0]) return true;
// Represent the segments as p + tr and q + us, where t and u are scalar
// parameters.
var p = a[0],
r = [a[1][0] - p[0], a[1][1] - p[1]],
q = b[0],
s = [b[1][0] - q[0], b[1][1] - q[1]];
// Solve p + tr = q + us to find an intersection point.
// First, cross both sides with s:
// (p + tr) × s = (q + us) × s
// We know that s × s = 0, so this can be rewritten as:
// t(r × s) = (q − p) × s
// Then solve for t to get:
// t = (q − p) × s / (r × s)
// Similarly, for u we get:
// u = (q − p) × r / (r × s)
var rxs = cross(r, s),
q_p = [q[0] - p[0], q[1] - p[1]],
t = cross(q_p, s) / rxs,
u = cross(q_p, r) / rxs,
epsilon = 1e-6;
return t > epsilon && t < 1 - epsilon && u > epsilon && u < 1 - epsilon;
}
function cross(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
@dsaffo
Copy link
Author

dsaffo commented Jul 22, 2020

party

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment