Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active July 4, 2019 12:23
Show Gist options
  • Save Kcnarf/bab49f708c556fbf3d1dd4be2f20e333 to your computer and use it in GitHub Desktop.
Save Kcnarf/bab49f708c556fbf3d1dd4be2f20e333 to your computer and use it in GitHub Desktop.
Vis. variations on Recamán sequence
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Vis. variations on Recamán sequence</title>
<meta name="description" content="Some visual variations on the Recamán sequence using D3.js">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
.recaman {
fill: none;
stroke: grey;
}
.recaman.main {
animation: dash;
animation-duration: 10s;
animation-timing-function: ease;
animation-fill-mode: both;
}
@keyframes dash {
to {
stroke-dashoffset: 0;
}
}
.recaman.thumbnail {
fill: transparent;
stroke: lightGrey;
stroke-opacity: 0;
animation: thumbnail;
animation-duration: .25s;
animation-timing-function: ease;
animation-fill-mode: both;
}
.recaman.thumbnail:hover {
stroke: grey;
}
.thumbnails>:nth-child(1).recaman { animation-delay: 10.5s; }
.thumbnails>:nth-child(2).recaman { animation-delay: 10.75s; }
.thumbnails>:nth-child(3).recaman { animation-delay: 11s; }
.thumbnails>:nth-child(4).recaman { animation-delay: 11.25s; }
.thumbnails>:nth-child(5).recaman { animation-delay: 11.5s; }
.thumbnails>:nth-child(6).recaman { animation-delay: 11.75s; }
.thumbnails>:nth-child(7).recaman { animation-delay: 12s; }
@keyframes thumbnail {
80% {
stroke: grey;
stroke-opacity: 1;
}
100% {
stroke-opacity: 1;
cursor: pointer;
}
}
</style>
</head>
<body>
<script>
//begin: layout conf
const svgWidth = 960,
svgHeight = 500,
margins = {top: 10, right: 10, bottom:10, left:10},
width = svgWidth - margins.top - margins.bottom,
height = svgHeight - margins.left - margins.right;
//end: layout conf
//begin: data conf
const recamanSeqLength = 100,
stretchedRecamanSeqMax = 400,
seqScale = stretchedRecamanSeqMax/recamanSeqLength;
let recamanSeq,
stretchedRecamanSeq;
//end: data conf
//begin: reusable d3 selection
let svg, drawingArea,recamanPath, thumbnailContainer;
//end reusable d3 selection
initLayout();
recamanSeq = computeRecaman(recamanSeqLength);
stretchedRecamanSeq = recamanSeq.map((v)=>{ return v*seqScale; });
updateRecamanPath("circle");
function initLayout() {
svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
drawingArea = svg.append("g")
.classed("drawing-area", true)
.attr("transform", "translate("+[margins.top, margins.left]+")");
initThumbnails();
recamanPath = drawingArea.append("path")
.classed("recaman main", true)
.attr("transform", "translate("+[0, height/2]+")");
}
function initThumbnails () {
const recamanSeq = computeRecaman(6),
thumbnailSeq = recamanSeq.map( (v)=>{ return v*seqScale; } )
thumbnailHeight = 4.5*seqScale,
thumbnailData = [
{path: computeCirclePath(thumbnailSeq), type: "circle"},
{path: computeHexPath(thumbnailSeq), type: "hex"},
{path: computeLosangePath(thumbnailSeq), type: "losange"},
{path: computeSquarePath(thumbnailSeq), type: "square"},
{path: computeRoundedSquarePath(thumbnailSeq), type: "roundedSquare"},
{path: computeZedSquarePath(thumbnailSeq), type: "zedSquare"},
{path: computeRoundedPlusPath(thumbnailSeq), type: "roundedPlus"}
];
thumbnailContainer = drawingArea.append("g")
.classed("thumbnails", true)
.attr("transform", "translate("+[0, height/2]+")");
let thumbnails = thumbnailContainer.selectAll(".thumbnail")
.data(thumbnailData)
.enter()
.append("path")
.attr("d", (d)=>{ return d.path; })
.attr("transform", (d,i)=>{
return "translate("+[0, i*2*thumbnailHeight]+")";
})
.classed("recaman thumbnail", true)
.on("click", (d,i)=>{ selectRecamanPath(d,i); })
}
function computeRecaman(recamanSeqLength) {
let newRecamanSequence = [],
currentRecamanValue = 0,
nexRecamanValue,
i;
for (i=0; i<recamanSeqLength; i++){
nextRecamanValue = computeNextRecamanValue(currentRecamanValue, i, newRecamanSequence);
newRecamanSequence.push(nextRecamanValue);
currentRecamanValue = nextRecamanValue;
}
return newRecamanSequence;
}
function computeNextRecamanValue(currentRecamanValue, value, recaman) {
minus = currentRecamanValue - value;
if (minus<0) {
return currentRecamanValue + value;
} else if (recaman.includes(minus)) {
return currentRecamanValue + value;
} else {
return minus;
}
}
function updateRecamanPath(pathType, position) {
let newPath;
switch (pathType) {
default:
newPath = computeCirclePath(stretchedRecamanSeq);
break;
case "hex":
newPath = computeHexPath(stretchedRecamanSeq);
break;
case "losange":
newPath = computeLosangePath(stretchedRecamanSeq);
break;
case "square":
newPath = computeSquarePath(stretchedRecamanSeq);
break;
case "roundedSquare":
newPath = computeRoundedSquarePath(stretchedRecamanSeq);
break;
case "zedSquare":
newPath = computeZedSquarePath(stretchedRecamanSeq);
break;
case "roundedPlus":
newPath = computeRoundedPlusPath(stretchedRecamanSeq);
break;
}
if (!position) { position = 0};
recamanPath.attr("d", newPath);
let recamanPathLength = recamanPath.node().getTotalLength();
recamanPath.style("stroke-dasharray", recamanPathLength)
.style("stroke-dashoffset", recamanPathLength);
}
function selectRecamanPath(thumbnailDatum, i) {
const thumbnailHeight = 4.5*seqScale;
thumbnailContainer.transition()
.attr("transform", "translate("+[0, height/2-i*2*thumbnailHeight]+")")
.on('end', function(d,i){
updateRecamanPath(thumbnailDatum.type);
});
}
/////////////////////////////////////////////////////////////////////
//begin: function used to computed circle/hex/losange/square paths //
/////////////////////////////////////////////////////////////////////
function computeCirclePath(recaman) {
let recamanLength = recaman.length,
newPath = "M0,0",
lastValue = 0,
even = true,
currentValue,
halfDist,
absHalfDist,
sweepFlag,
i;
for (i=0; i<recamanLength; i++) {
currentValue = recaman[i];
halfDist = (currentValue - lastValue)/2;
absHalfDist = Math.abs(halfDist);
if (currentValue < lastValue) {
sweepFlag = even? 0 : 1;
} else {
sweepFlag = even? 1 : 0;
}
newPath += "A "+absHalfDist+" "+absHalfDist+" 0 0 "+sweepFlag+" "+[currentValue,0];
lastValue = currentValue;
even = !even;
}
return newPath;
}
function computeHexPath(recaman) {
return d3.line().curve(d3.curveLinear)(computeCPs(recaman, 2));
}
function computeLosangePath(recaman) {
return d3.line().curve(d3.curveLinear)(computeCPs(recaman, 1));
}
function computeSquarePath(recaman) {
return d3.line().curve(d3.curveLinear)(computeSquareCPs(recaman));
}
function computeRoundedSquarePath(recaman) {
return d3.line().curve(d3.curveBasis)(computeSquareCPs(recaman));
}
function computeZedSquarePath(recaman) {
return d3.line().curve(d3.curveCardinal)(computeSquareCPs(recaman));
}
function computeRoundedPlusPath(recaman) {
return d3.line().curve(d3.curveBasis)(computePlusCPs(recaman));
}
function computeCPs(recaman, intermediateCPCount) {
let recamanLength = recaman.length,
newCPs = [[0,0]],
lastValue = 0,
even = true,
currentValue,
intermediateCPsAngle,
intermediateCPsUnitPositions,
halfX,
halfDeltaX,
radius,
i,
x,
y;
if (intermediateCPCount<1) {
intermediateCPCount = 1;
}
if (intermediateCPCount>4) {
intermediateCPCount = 4;
}
intermediateCPsAngle = Math.PI/(intermediateCPCount+1);
intermediateCPsUnitPositions=[];
for (i=0; i<intermediateCPCount; i++){
intermediateCPsUnitPositions.push([
Math.cos((i+1)*intermediateCPsAngle),
Math.sin((i+1)*intermediateCPsAngle)
]);
}
for (i=0; i<recamanLength; i++) {
currentValue = recaman[i];
halfX = (lastValue+currentValue)/2;
halfDeltaX = -(currentValue - lastValue)/2;
radius = Math.abs(halfDeltaX);
intermediateCPsUnitPositions.forEach(function(intermediateCPUnitPosition){
x = halfX+intermediateCPUnitPosition[0]*halfDeltaX;
y = intermediateCPUnitPosition[1]*radius;
if (even) { y = -y; }
newCPs.push([x, y]);
})
newCPs.push([currentValue, 0]);
lastValue = currentValue;
even = !even;
}
return newCPs;
}
function computeSquareCPs(recaman) {
let recamanLength = recaman.length,
newCPs = [[0,0]],
lastValue = 0,
even = true,
currentValue,
absHalfDeltaX,
i,
y;
for (i=0; i<recamanLength; i++) {
currentValue = recaman[i];
absHalfDeltaX = Math.abs((currentValue - lastValue)/2);
y = even? -absHalfDeltaX : absHalfDeltaX;
newCPs.push([lastValue, y]);
newCPs.push([currentValue, y]);
newCPs.push([currentValue, 0]);
lastValue = currentValue;
even = !even;
}
return newCPs;
}
function computePlusCPs(recaman) {
let recamanLength = recaman.length,
newCPs = [[0,0]],
lastValue = 0,
even = true,
currentValue,
halfX,
quarterDeltaX,
absHalfDeltaX,
i,
y;
for (i=0; i<recamanLength; i++) {
currentValue = recaman[i];
halfX = (lastValue + currentValue)/2,
quarterDeltaX = -(lastValue - currentValue)/4,
absHalfDeltaX = Math.abs((currentValue - lastValue)/2);
y = even? -absHalfDeltaX : absHalfDeltaX;
newCPs.push([lastValue, y/2]);
newCPs.push([lastValue+quarterDeltaX, y/2]);
newCPs.push([lastValue+quarterDeltaX, y]);
newCPs.push([currentValue-quarterDeltaX, y]);
newCPs.push([currentValue-quarterDeltaX, y/2]);
newCPs.push([currentValue, y/2]);
newCPs.push([currentValue, 0]);
lastValue = currentValue;
even = !even;
}
return newCPs;
}
///////////////////////////////////////////////////////////////////
//end: function used to computed circle/hex/losange/square paths //
///////////////////////////////////////////////////////////////////
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment