Skip to content

Instantly share code, notes, and snippets.

@agware
Last active February 7, 2017 23:01
Show Gist options
  • Save agware/0245f2f4b833fa5fc1cb1e720b1d0682 to your computer and use it in GitHub Desktop.
Save agware/0245f2f4b833fa5fc1cb1e720b1d0682 to your computer and use it in GitHub Desktop.
Hexagons Spiralling
license: gpl-3.0
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hexagons</title>
<style>
line {
stroke: #000;
stroke-width: 2px;
stroke-opacity: 1;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="spiral.js" type="text/javascript"></script>
<script src="twist.js" type="text/javascript"></script>
<script src="utils.js" type="text/javascript"></script>
<script>
const width = 960;
const height = 500;
const length = 150;
const iterations = 12;
const duration = 800;
const tileAcross = 5;
const tileDown = 3;
let svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.attr('overflow', 'hidden');
let x = Math.round(length*Math.cos(30*Math.PI/180)*1000)/1000;
let y = Math.round(length*Math.sin(30*Math.PI/180)*1000)/1000;
let ySection = Math.round(2*y*Math.sin(30*Math.PI/180)*1000)/1000;
let g = svg.append('g');
let hexagon = [
{'x1': x, 'y1': -y, 'x2': 0, 'y2': -length},
{'x1': 0, 'y1': -length, 'x2': -x, 'y2': -y},
{'x1': -x, 'y1': -y, 'x2': -x, 'y2': y},
{'x1': -x, 'y1': y, 'x2': 0, 'y2': length},
{'x1': 0, 'y1': length, 'x2': x, 'y2': y},
{'x1': x, 'y1': y, 'x2': x, 'y2': -y}
];
let lineData = [];
for (let count = 0; count < tileAcross*tileDown; count++) {
let xOffset = 2*(count % tileAcross)*x - (Math.floor(count/tileAcross) % 2)*x;
let yOffset = Math.floor(count/tileAcross)*(2*y + ySection);
let container = g.append('g')
.attr('id', 'container' + count)
.classed('container', true);
container.append('g').selectAll('line')
.data(hexagon)
.enter().append('line')
.attr('x1', function (d) {return xOffset + d.x1; })
.attr('y1', function (d) {return yOffset + d.y1; })
.attr('x2', function (d) {return xOffset + d.x2; })
.attr('y2', function (d) {return yOffset + d.y2; });
spiral(container, iterations);
lineData.push(ripMultiGLineData(d3.select('#container' + count)));
}
let start = Date.now() - (duration+100);
d3.timer(animate);
function animate() {
let t = (Date.now() - start)/(duration+200);
if(t > 1) {
for (let i = 0; i < d3.selectAll('.container').size(); i++) {
twist(d3.select('#container' + i), duration, lineData[i]);
}
start = Date.now();
}
}
</script>
</body>
</html>
function spiral (container, iterations) {
// Spiral based on intersections of lines
let foundation = container.select('g:nth-child(1)');
let length = getLength(foundation, iterations);
for (let i = 0; i < iterations; i++) {
let gaps = getGaps(container, length);
generateSpiral(container, gaps);
}
}
function getLength(foundation, iterations) {
let lineNum = foundation.selectAll('line').size();
let ret = [];
for (let i = 0; i < lineNum; i++) {
let rise = foundation.select('line:nth-child(' + (i+1) + ')').attr('y2') - foundation.select('line:nth-child(' + (i+1) + ')').attr('y1');
let run = foundation.select('line:nth-child(' + (i+1) + ')').attr('x2') - foundation.select('line:nth-child(' + (i+1) + ')').attr('x1');
run = run ? run : 0.00001;
let length = Math.round(Math.sqrt(Math.pow(run, 2) + Math.pow(rise, 2))*1000)/1000;
let subLength = length/((5/lineNum)*iterations);
ret.push(subLength);
}
return ret;
}
function getGaps(container, length) {
let layerNum = container.selectAll('g').size();
let layer = container.select('g:nth-child(' + layerNum + ')');
let lineNum = layer.selectAll('line').size();
let ret = [];
for (let i = 0; i < lineNum; i++) {
let rise = layer.select('line:nth-child(' + (i+1) + ')').attr('y2') - layer.select('line:nth-child(' + (i+1) + ')').attr('y1');
let run = layer.select('line:nth-child(' + (i+1) + ')').attr('x2') - layer.select('line:nth-child(' + (i+1) + ')').attr('x1');
run = run ? run : 0.00001;
let angle = Math.round(Math.atan(rise/run)*1000)/1000;
let x = Math.abs(length[i]*Math.cos(angle)) * (run < 0 ? -1 : 1);
let y = Math.abs(length[i]*Math.sin(angle)) * (rise < 0 ? -1 : 1);
ret.push({'x': Math.round(x*1000)/1000, 'y': Math.round(y*1000)/1000});
}
return ret;
}
function generateSpiral (g, gaps) {
let layerNum = g.selectAll('g').size();
let layer = g.select('g:nth-child(' + layerNum + ')');
let newLayer = g.append('g');
let lineNum = layer.selectAll('line').size();
for (let j = 0; j < lineNum; j++) {
let intersect = findLineIntersection(layer.select('line:nth-child(' + (j+1) + ')'), layer.select('line:nth-child(' + ((j+(lineNum-1))%(lineNum)+1) + ')'));
let intersect2 = findLineIntersection(layer.select('line:nth-child(' + (j+1) + ')'), layer.select('line:nth-child(' + ((j+1)%(lineNum)+1) + ')'));
let x1 = intersect.x + gaps[j].x;
let y1 = intersect.y + gaps[j].y;
let x2 = intersect2.x + gaps[(j+1)%lineNum].x;
let y2 = intersect2.y + gaps[(j+1)%lineNum].y;
newLayer.append('line')
.attr('x1', x1)
.attr('y1', y1)
.attr('x2', x2)
.attr('y2', y2);
}
}
function twist (container, duration, lineData) {
setTwist(container, lineData, duration);
setTimeout(function() {revertTwist(container, lineData)}, duration+100);
}
function setTwist (container, lineData, duration) {
let layer = container.select('g:nth-child(1)');
let linesPerLayer = layer.selectAll('line').size();
let lineNum = lineData.length;
for (let i = 0; i < (lineNum-linesPerLayer); i++) {
let nextLine = (i + linesPerLayer);
let layer = Math.floor(i/linesPerLayer);
let layerG = container.select('g:nth-child(' + (layer+1) + ')');
layerG.select('line:nth-child(' + ((i%linesPerLayer)+1) + ')')
.transition()
.duration(duration)
.attr('x1', lineData[nextLine].x1)
.attr('y1', lineData[nextLine].y1)
.attr('x2', lineData[nextLine].x2)
.attr('y2', lineData[nextLine].y2);
}
container.append('g')
.attr('id', 'tempLines')
.selectAll('line')
.data(d3.range(linesPerLayer))
.enter().append('line')
.attr('x1', function (d) {return lineData[d].x1; })
.attr('y1', function (d) {return lineData[d].y1; })
.attr('x2', function (d) {return lineData[d].x2; })
.attr('y2', function (d) {return lineData[d].y2; });
}
function revertTwist (container, lineData) {
d3.select('#tempLines').remove();
let layer = container.select('g:nth-child(1)');
let linesPerLayer = layer.selectAll('line').size();
let lineNum = lineData.length;
for (let i = 0; i < lineNum; i++) {
let layer = Math.floor(i/linesPerLayer);
let layerG = container.select('g:nth-child(' + (layer+1) + ')');
layerG.select('line:nth-child(' + ((i%linesPerLayer)+1) + ')')
.attr('x1', lineData[i].x1)
.attr('y1', lineData[i].y1)
.attr('x2', lineData[i].x2)
.attr('y2', lineData[i].y2);
}
}
function addLine(points, step, i) {
g.append('line')
.attr('x1', points.x1)
.attr('y1', points.y1)
.attr('x2', points.x1)
.attr('y2', points.y1)
.attr('id', 'step' + step + 'Line' + i)
.classed('construction', true)
.transition()
.duration(duration)
.attr('x2', points.x2)
.attr('y2', points.y2);
}
function addStaticLine(points, step, i) {
g.append('line')
.attr('x1', points.x1)
.attr('y1', points.y1)
.attr('x2', points.x2)
.attr('y2', points.y2)
.attr('id', 'step' + step + 'Line' + i)
.classed('construction', true);
}
function findCircleIntersection(line, circle) {
let circleFormula = getCircleFormula(circle);
let lineFormula = getLineFormula(line);
// 1 + m^2
let a = 1 + Math.pow(lineFormula.m, 2);
// 2mc - 2mk - 2h
let b = 2*lineFormula.m*lineFormula.c - 2*lineFormula.m*circleFormula.k - 2*circleFormula.h;
// h^2 + c^2 + k^2 - r^2 - 2ck
let c = Math.pow(circleFormula.h, 2) + Math.pow(lineFormula.c, 2) + Math.pow(circleFormula.k, 2) - Math.pow(circleFormula.r, 2) - 2*lineFormula.c*circleFormula.k;
let discriminant = Math.pow(b, 2) - 4*a*c;
let x1 = (-b + Math.sqrt(discriminant))/(2*a);
let x2 = (-b - Math.sqrt(discriminant))/(2*a);
let x = checkIfInBound(x1, x2, line);
let y = lineFormula.m*x + lineFormula.c;
return {'x': x, 'y': y};
}
function findLineIntersection (line1, line2) {
let formula1 = getLineFormula(line1);
let formula2 = getLineFormula(line2);
let x = (formula2.c - formula1.c)/(formula1.m - formula2.m);
let y = formula1.m*x + formula1.c;
return {'x': x, 'y': y};
}
function extendLine(point1, point2, extension) {
let x1 = point1.x;
let x2 = point2.x;
let y1 = point1.y;
let y2 = point2.y;
let rise = y2-y1;
let run = x2-x1;
run = run ? run : -0.000001;
let m = Math.round(rise*1000/run)/1000;
let angle = Math.round(Math.atan(m)*1000)/1000;
let xShift = Math.round(extension*Math.cos(angle)*1000)/1000;
let yShift = Math.round(extension*Math.sin(angle)*1000)/1000;
xShift = xShift*(run > 0 ? -1 : 1);
yShift = yShift*(run > 0 ? -1 : 1);
return {'x1': x1 + xShift, 'y1': y1 + yShift, 'x2': x2 - xShift, 'y2': y2 - yShift};
}
function ripLineData (g) {
let lineNum = g.selectAll('line').size();
let ret = [];
for(let i = 0; i < lineNum; i++) {
let line = g.select('line:nth-child(' + (i+1) + ')');
let data = {'x1': line.attr('x1'), 'y1': line.attr('y1'), 'x2': line.attr('x2'), 'y2': line.attr('y2')}
ret.push(data);
}
return ret;
}
function ripMultiGLineData (container) {
let layer = container.select('g:nth-child(1)');
let linesPerLayer = layer.selectAll('line').size();
let lineNum = container.selectAll('line').size();
let lineData = [];
for (let i = 0; i < lineNum; i++) {
let layer = Math.floor(i/linesPerLayer);
let layerG = container.select('g:nth-child(' + (layer+1) + ')');
let line = layerG.select('line:nth-child(' + ((i%linesPerLayer)+1) + ')');
let x1 = line.attr('x1');
let y1 = line.attr('y1');
let x2 = line.attr('x2');
let y2 = line.attr('y2');
lineData.push({'x1': x1, 'y1': y1, 'x2': x2, 'y2': y2});
}
return lineData;
}
function generateUsingLineData (container, lineData) {
let layer = container.append('g');
layer.selectAll('line')
.data(lineData)
.enter().append('line')
.attr('x1', function(d) {return d.x1})
.attr('y1', function(d) {return d.y1})
.attr('x2', function(d) {return d.x2})
.attr('y2', function(d) {return d.y2})
.classed('final', true);
}
function transitionUsingLineData (g, lineData) {
// ToDo: Hide/generate lines based on need
let lineNum = g.selectAll('line').size();
let lineNumNeeded = lineData.length;
for (let i = 0; i < lineNumNeeded; i++) {
let line = g.select('line:nth-child(' + (i+1) + ')');
let data = lineData[i];
line
.transition()
.duration(2000)
.attr('x1', data.x1)
.attr('y1', data.y1)
.attr('x2', data.x2)
.attr('y2', data.y2);
}
}
function getLineFormula (line) {
let x1 = line.attr('x1');
let x2 = line.attr('x2');
let y1 = line.attr('y1');
let y2 = line.attr('y2');
let rise = y2-y1;
let run = x2-x1;
run = run ? run : -0.000001;
let m = Math.round(rise*1000/run)/1000;
let c = Math.round((y1 - m*x1)*1000)/1000;
return {'m': m, 'c': c};
}
function getCircleFormula(circle) {
let h = circle.attr('cx');
h = (h == null) ? 0 : h;
let k = circle.attr('cy');
k = (k == null) ? 0 : k;
let r = circle.attr('r');
return {'h': h, 'k': k, 'r': r};
}
function checkIfInBound(x1, x2, line) {
let xBound1 = line.attr('x1');
let xBound2 = line.attr('x2');
let x;
if (Math.min(xBound1, xBound2) <= x1 && Math.max(xBound1, xBound2) >= x1) {
x = x1;
} else {
x = x2;
}
return x;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment