|
<!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> |