|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Scrolytelling the Penrose triangle</title> |
|
<meta name="description" content="Step by step animated explanations on how to draw a Penrose triangle (and some derivatives), using scrolytelling with 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; |
|
font-size: larger; |
|
} |
|
|
|
#wip { |
|
display: none; |
|
position: absolute; |
|
top: 400px; |
|
left: 330px; |
|
font-size: 40px; |
|
text-align: center; |
|
} |
|
|
|
#container { |
|
position: relative; |
|
z-index: 100; |
|
} |
|
#sticky { |
|
position: absolute; |
|
top: 0; |
|
right: 0; |
|
width: 50%; |
|
height: 100%; |
|
z-index: 50; |
|
} |
|
#story-container { |
|
height: 100vh; |
|
overflow-y: scroll; |
|
} |
|
|
|
.panel { |
|
height: 100vh; |
|
position: relative; |
|
#border-top: dotted 1px; |
|
} |
|
.panel:last-child { |
|
margin-bottom: 50vh; |
|
} |
|
|
|
.vertical-positioner { |
|
position: absolute; |
|
width:50%; |
|
} |
|
.vertical-positioner.center { |
|
top: 25%; |
|
} |
|
.vertical-positioner.top { |
|
top: 5%; |
|
} |
|
.vertical-positioner.bottom { |
|
bottom: 5%; |
|
} |
|
|
|
.panel p { |
|
padding-left: 20px; |
|
padding-right: 20px; |
|
} |
|
.panel p.title { |
|
width: 100vw; |
|
} |
|
|
|
.emphasis { |
|
font-size: 200%; |
|
font-weight: bold; |
|
color: darkgrey; |
|
} |
|
|
|
.emphasis.step-number { |
|
font-size: 400%; |
|
} |
|
|
|
.penrose-triangle { |
|
fill: transparent; |
|
stroke: grey; |
|
stroke-width: 2px; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<div id="sticky"> |
|
</div> |
|
|
|
<div id="story-container"> |
|
<div id="story"> |
|
<div class="panel"> |
|
<div class="vertical-positioner center"> |
|
<p class="title"><span class="emphasis">how to draw a Penrose Triangle</span> |
|
<br/>, one of the most famous impossible geometry.</p> |
|
<p> </p> |
|
<p><em>Let's get scrolling!</em></p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p><span class="emphasis step-number">1</span> Draw a triangle</p> |
|
<p><em>It will be the inner part of the Penrose Triangle.</em></p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p><span class="emphasis step-number">2</span> Extend a line off of each corner.</p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p><span class="emphasis step-number">3</span> Draw another line off each of thoose extensions, parralel to the nearest side, and that extends to the top of the corner.</p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p><span class="emphasis step-number">4</span> Draw a short angle that lines up with the opposite side.</p> |
|
<p><em>The size should be the same as in the previous steps.</em></p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p><span class="emphasis step-number">5</span> Connect the lines, and ... |
|
<br/><em>here it is !</em></p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p>So now let's <span class="emphasis">play</span> a little bit</p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p>For instance, we can <span class="emphasis">reduce</span> or <span class="emphasis">enlarge</span> the inner triangle ...</p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p>... or we can make it <span class="emphasis">thinner</span> or <span class="emphasis">thicker</span></p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p>... or we can use a <span class="emphasis">rectangular section</span>, on one direction or another</p> |
|
</div> |
|
</div> |
|
<div class="panel"> |
|
<div class="vertical-positioner bottom"> |
|
<p>... or we can even make an <span class="emphasis">asymetric</span> Penrose triangle</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="wip"> |
|
Work in progress ... |
|
</div> |
|
|
|
<script> |
|
//begin: layout conf |
|
const windowWidth = window.innerWidth, |
|
panelHeight = window.innerHeight, |
|
svgWidth = 960/2, |
|
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: utils |
|
const cos60 = Math.cos(Math.PI/3); |
|
const sin60 = Math.sin(-Math.PI/3); //minus because svg y axe goes to bottom |
|
const orthoY = function(obliqueY) { |
|
return [obliqueY*cos60, obliqueY*sin60]; |
|
}; |
|
const orthoCoord = function(obliqueCoord){ |
|
const ortho = orthoY(obliqueCoord[1]); |
|
return [obliqueCoord[0]+ortho[0], ortho[1]]; |
|
}; |
|
const orthoCoords = function(obliqueCoords) { |
|
return obliqueCoords.map(function(obliqueCoord) { |
|
return orthoCoord(obliqueCoord); |
|
}) |
|
}; |
|
const liner = function (d) { |
|
let path = "M"+d.orthoStart; |
|
d.orthoDeltas.forEach(function(orthoDelta) { |
|
path += "l"+orthoDelta; |
|
}) |
|
return path; |
|
}; |
|
const nothingToDo = function () {}; |
|
const createAnimConf = function () { |
|
return { |
|
start: 0, |
|
lengh: 0, |
|
end: 0, |
|
animate: nothingToDo, |
|
onEnter: {fromPrev: nothingToDo, fromNext: nothingToDo}, |
|
onExit: {toPrev: nothingToDo, toNext: nothingToDo} |
|
}; |
|
}; |
|
const computeLeftLShapeCPs = function (ptCPs) { |
|
return ptCPs.map(function(pt){ return pt.leftLShapePath; }); |
|
}; |
|
const computeRightLShapeCPs = function (ptCPs) { |
|
return ptCPs.map(function(pt){ return pt.rightLShapePath; }); |
|
}; |
|
const computeBottomLShapeCPs = function (ptCPs) { |
|
return ptCPs.map(function(pt){ return pt.bottomLShapePath; }); |
|
}; |
|
//end: utils |
|
|
|
//begin: data conf |
|
let ptInnerWidth, ptExtent1, ptExtent2; |
|
const defaultIW = 25, |
|
defaultExt1 = defaultIW, |
|
defaultExt2 = defaultIW; |
|
//predefined Penrose Triangles' data used for scales/interpolations |
|
let pTData = computePTData(2*defaultIW, 2*defaultExt1, 2*defaultExt2), |
|
pTMiniData = computePTData(defaultIW, defaultExt1, defaultExt2), |
|
pTWithoutIWData = computePTData(0, defaultExt1, defaultExt2), |
|
pTLargerIWData = computePTData(4*defaultIW, defaultExt1, defaultExt2), |
|
pTThickerData = computePTData(4*defaultIW, 2*defaultExt1, 2*defaultExt2), |
|
pTThinnerData = computePTData(4*defaultIW, defaultExt1/2, defaultExt2/2), |
|
pTRect0Data = computePTData(4*defaultIW, defaultExt1, 2*defaultExt2), |
|
pTRect1Data = computePTData(4*defaultIW, 2*defaultExt1, defaultExt2), |
|
pTAsymetricData = computeAsymetricPTData(4*defaultIW, defaultIW/2, 2*defaultIW, 4*defaultIW); |
|
|
|
//end: data conf |
|
|
|
//begin: reusable d3 selections |
|
let svg, drawingArea, penroseTriangle; |
|
const storyContainer = d3.select('#story-container'), |
|
story = d3.select('#story'); |
|
//end reusable d3 selections |
|
initLayout(); |
|
|
|
//begin: scroll managment |
|
var scrollTop = 0, |
|
newScrollTop = 0, |
|
animationIndex = 0, |
|
newAnimationIndex = 0; |
|
storyContainer.on("scroll.scroller", function() { |
|
newScrollTop = storyContainer.node().scrollTop; |
|
}); |
|
const animConfs = []; |
|
//end: scroll management |
|
|
|
//begin: story-telling main loop |
|
window.requestAnimationFrame(function refresh() { |
|
if (scrollTop !== newScrollTop) { |
|
scrollTop = newScrollTop; |
|
|
|
//begin: retrieves which animation is being played |
|
for (var i=0; i<animConfs.length; i++){ |
|
if (scrollTop < animConfs[i].end) { |
|
newAnimationIndex = i; |
|
break; |
|
} |
|
} |
|
//end: retrieves which animation is being played |
|
|
|
if (animationIndex !== newAnimationIndex) { |
|
handleExitEnterOfAnimations(); |
|
animationIndex = newAnimationIndex; |
|
} |
|
animConfs[animationIndex].animate(); |
|
} |
|
|
|
window.requestAnimationFrame(refresh); |
|
}); |
|
|
|
function handleExitEnterOfAnimations() { |
|
if (animationIndex < newAnimationIndex) { |
|
animConfs[animationIndex].onExit.toNext(); |
|
animConfs[newAnimationIndex].onEnter.fromPrev(); |
|
} else { |
|
animConfs[animationIndex].onExit.toPrev(); |
|
animConfs[newAnimationIndex].onEnter.fromNext(); |
|
} |
|
} |
|
//end: story-telling main loop |
|
|
|
//begin: first animation (slide to top) conf. and utils |
|
animConfs.push(createAnimConf()); |
|
animConfs[0].length = panelHeight; |
|
animConfs[0].end = animConfs[0].start + animConfs[0].length; |
|
animConfs[0].animate = function() { |
|
let vPos = height/2 - scrollTop; |
|
penroseTriangle.attr("transform", "translate("+[width/2, vPos]+")"); |
|
}; |
|
animConfs[0].onExit.toNext = function() { |
|
//reposition the Penrose Triangle at center of the screen |
|
penroseTriangle.attr("transform", "translate("+[width/2, height/2]+")"); |
|
}; |
|
//end: first animation (slide to top) conf. and utils |
|
|
|
//begin: second animation (Penrose Triangle construction) conf. and utils |
|
//2nd animation uses the strokDashoffset trick to make pathes appearing as they were hand drawn |
|
animConfs.push(createAnimConf()); |
|
animConfs[1].start = animConfs[0].end; |
|
animConfs[1].length = 5*panelHeight; |
|
animConfs[1].end = animConfs[1].start + animConfs[1].length; |
|
animConfs[1].onEnter.fromPrev = function () { |
|
//on exit, set style so that the Penrose Triangle appears to be hand drawn |
|
penroseTriangle.style("stroke-dasharray", lShapeLength); |
|
} |
|
animConfs[1].onEnter.fromNext = animConfs[1].onEnter.fromPrev; |
|
animConfs[1].animate = function() { |
|
// cf. computeStrokeDashoffsetScale() for better understanding. |
|
penroseTriangle.style("stroke-dashoffset", strokeDashoffsetScale(scrollTop)); |
|
}; |
|
animConfs[1].onExit.toNext = function () { |
|
//on exit, reset style so that the Penrose Triangle is completly drawn |
|
penroseTriangle.style("stroke-dasharray", 0); |
|
penroseTriangle.style("stroke-dashoffset", 0); |
|
} |
|
animConfs[1].onExit.toPrev = animConfs[1].onExit.toNext; |
|
const lShapeLength = penroseTriangle.select(".l-shape").node().getTotalLength(); |
|
const strokeDashoffsetScale = computeStrokeDashoffsetScale(); |
|
//end: second animation (Penrose Triangle construction) conf. and utils |
|
|
|
//begin: third animation (show mini Penrose Triangle) conf. and utils |
|
//3rd animation animates the 'd' attribute of pathes |
|
animConfs.push(createAnimConf()); |
|
animConfs[2].start = animConfs[1].end; |
|
animConfs[2].length = panelHeight; |
|
animConfs[2].end = animConfs[2].start + animConfs[2].length; |
|
animConfs[2].animate = function() { |
|
// cf. computePTScale for better understanding. |
|
penroseTriangle.select(".l-shape:nth-child(1)") |
|
.attr("d", ptScale.leftLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(2)") |
|
.attr("d", ptScale.rightLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(3)") |
|
.attr("d", ptScale.bottomLShapeScale(scrollTop)); |
|
}; |
|
const ptScale = computePTScale(); |
|
//end: third animation (changing inner width) conf. and utils |
|
|
|
//begin: fourth animation (changing inner width) conf. and utils |
|
//4th animation animates the 'd' attribute of pathes |
|
animConfs.push(createAnimConf()); |
|
animConfs[3].start = animConfs[2].end; |
|
animConfs[3].length = panelHeight; |
|
animConfs[3].end = animConfs[3].start + animConfs[3].length; |
|
animConfs[3].animate = function() { |
|
// cf. computeIWScale for better understanding. |
|
penroseTriangle.select(".l-shape:nth-child(1)") |
|
.attr("d", iwScale.leftLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(2)") |
|
.attr("d", iwScale.rightLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(3)") |
|
.attr("d", iwScale.bottomLShapeScale(scrollTop)); |
|
}; |
|
const iwScale = computeIWScale(); |
|
//end: fourth animation (changing inner width) conf. and utils |
|
|
|
//begin: fifth animation (changing thickness) conf. and utils |
|
//5th animation animates the 'd' attribute of pathes |
|
animConfs.push(createAnimConf()); |
|
animConfs[4].start = animConfs[3].end; |
|
animConfs[4].length = panelHeight; |
|
animConfs[4].end = animConfs[4].start + animConfs[4].length; |
|
animConfs[4].animate = function() { |
|
// cf. computeThicknessScale for better understanding. |
|
penroseTriangle.select(".l-shape:nth-child(1)") |
|
.attr("d", thicknessScale.leftLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(2)") |
|
.attr("d", thicknessScale.rightLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(3)") |
|
.attr("d", thicknessScale.bottomLShapeScale(scrollTop)); |
|
}; |
|
const thicknessScale = computeThicknessScale(); |
|
//end: fifth animation (changing thickness) conf. and utils |
|
|
|
//begin: sixth animation (rectangle sections) conf. and utils |
|
//6th animation animates the 'd' attribute of pathes |
|
animConfs.push(createAnimConf()); |
|
animConfs[5].start = animConfs[4].end; |
|
animConfs[5].length = panelHeight; |
|
animConfs[5].end = animConfs[5].start + animConfs[5].length; |
|
animConfs[5].animate = function() { |
|
// cf. computeSectionScale for better understanding. |
|
penroseTriangle.select(".l-shape:nth-child(1)") |
|
.attr("d", sectionScale.leftLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(2)") |
|
.attr("d", sectionScale.rightLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(3)") |
|
.attr("d", sectionScale.bottomLShapeScale(scrollTop)); |
|
}; |
|
const sectionScale = computeSectionScale(); |
|
//end: sixth animation (rectangle sections) conf. and utils |
|
|
|
//begin: seventh animation (asymetric) conf. and utils |
|
//7th animation animates the 'd' attribute of pathes |
|
animConfs.push(createAnimConf()); |
|
animConfs[6].start = animConfs[5].end; |
|
animConfs[6].length = 1.5*panelHeight; |
|
animConfs[6].end = animConfs[6].start + animConfs[6].length; |
|
animConfs[6].animate = function() { |
|
// cf. computeAssymetricScale for better understanding. |
|
penroseTriangle.select(".l-shape:nth-child(1)") |
|
.attr("d", asymetricScale.leftLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(2)") |
|
.attr("d", asymetricScale.rightLShapeScale(scrollTop)); |
|
penroseTriangle.select(".l-shape:nth-child(3)") |
|
.attr("d", asymetricScale.bottomLShapeScale(scrollTop)); |
|
}; |
|
const asymetricScale = computeAsymetricScale(); |
|
//end: sixth animation (asymetric) conf. and utils |
|
|
|
///////////////////////////////// |
|
// scroll -> animation mapping // |
|
///////////////////////////////// |
|
|
|
function computeStrokeDashoffsetScale() { |
|
// 2nd animation consists on smoothly drawing the Penrose Triangle. It is composed of 3 identical L-shapes (each rotated by 120°, see ASCII-art near the end of the script). Each L-shape is composed of 5 segments, hence the animation is compsed of 5 steps. |
|
// Each step (i) waits the text to reach the viewport's center, and then (ii) animates the drawing of the corresponding segment. |
|
|
|
// on one hand we have 5 panels to scroll (or 10 half-panels, as each step is divided into 2 sub-steps (wait text, draw segment)) |
|
const anim1CPs = [ |
|
animConfs[1].start, // wait text to reach viewport's center |
|
animConfs[1].start+0.5*panelHeight, // draw 1st segment |
|
animConfs[1].start+1*panelHeight, // wait text |
|
animConfs[1].start+1.5*panelHeight, // draw 2nd segment |
|
animConfs[1].start+2*panelHeight, // wait text |
|
animConfs[1].start+2.5*panelHeight, // draw 3rd segment |
|
animConfs[1].start+3*panelHeight, // wait text |
|
animConfs[1].start+3.5*panelHeight, // draw 4th segment |
|
animConfs[1].start+4*panelHeight, // wait text |
|
animConfs[1].start+4.5*panelHeight, // draw 5th segment |
|
animConfs[1].end // extra space to position last text at center |
|
] |
|
// on the other hand, the L-shape we have to draw is composed of 5 segments of different sizes |
|
const strokeDashoffsetCPs = [ |
|
lShapeLength, // wait text to appear, shape complitely hidden |
|
lShapeLength*11/12, // 1st segment is size 1 |
|
lShapeLength*11/12, // wait text |
|
lShapeLength*10/12, // 2nd segment is size 1 |
|
lShapeLength*10/12, // wait text to appear |
|
lShapeLength*6/12, // 3rd segment is size 4 |
|
lShapeLength*6/12, // wait text to appear |
|
lShapeLength*5/12, // 4th segment is size 1 |
|
lShapeLength*5/12, // wait text to appear |
|
0, // 5th segment is size 5, shape complitely drawn |
|
0 // extra space to position last text at center |
|
] |
|
|
|
return d3.scaleLinear() |
|
.domain(anim1CPs) |
|
.range(strokeDashoffsetCPs); |
|
} |
|
|
|
function computePTScale() { |
|
// 3rd animation consists on smoothly set the Penrose Triangle's inner width 'iw' to 0, then to twice the default size, and go back to the default size |
|
const animCPs = [ |
|
animConfs[2].start, // wait text to reach viewport's center |
|
animConfs[2].start+.5*panelHeight, // show mini Penrose triangle |
|
animConfs[2].end // leave mini Penrose triangle |
|
]; |
|
const ptCPs = [ |
|
pTData, |
|
pTMiniData, |
|
pTMiniData |
|
] |
|
|
|
const leftLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeLeftLShapeCPs(ptCPs)); |
|
const rightLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeRightLShapeCPs(ptCPs)); |
|
const bottomLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeBottomLShapeCPs(ptCPs)); |
|
|
|
return { |
|
leftLShapeScale: leftLShapeScale, |
|
rightLShapeScale: rightLShapeScale, |
|
bottomLShapeScale: bottomLShapeScale |
|
}; |
|
} |
|
|
|
function computeIWScale() { |
|
// 4th animation consists on smoothly set the Penrose Triangle's inner width 'iw' to 0, then to twice the default size, and go back to the default size |
|
const animCPs = [ |
|
animConfs[3].start, // wait text to reach viewport's center |
|
animConfs[3].start+.5*panelHeight, // reduce 'iw' to 0 |
|
animConfs[3].start+.75*panelHeight, // set 'iw' larger |
|
animConfs[3].end // leave 'iw' larger size |
|
]; |
|
const ptCPs = [ |
|
pTMiniData, |
|
pTWithoutIWData, |
|
pTLargerIWData, |
|
pTLargerIWData |
|
]; |
|
|
|
const leftLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeLeftLShapeCPs(ptCPs)); |
|
const rightLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeRightLShapeCPs(ptCPs)); |
|
const bottomLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeBottomLShapeCPs(ptCPs)); |
|
|
|
return { |
|
leftLShapeScale: leftLShapeScale, |
|
rightLShapeScale: rightLShapeScale, |
|
bottomLShapeScale: bottomLShapeScale |
|
}; |
|
} |
|
|
|
function computeThicknessScale() { |
|
// 5th animation consists on smoothly set the Penrose Triangle thiner, then bolder, and go back to its default thickness |
|
const animCPs = [ |
|
animConfs[4].start, // wait text to reach viewport's center |
|
animConfs[4].start+.5*panelHeight, // bolder |
|
animConfs[4].start+.75*panelHeight, // thinner |
|
animConfs[4].end // leave thinner |
|
]; |
|
const ptCPs = [ |
|
pTLargerIWData, |
|
pTThinnerData, |
|
pTThickerData, |
|
pTThickerData |
|
]; |
|
|
|
const leftLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeLeftLShapeCPs(ptCPs)); |
|
const rightLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeRightLShapeCPs(ptCPs)); |
|
const bottomLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeBottomLShapeCPs(ptCPs)); |
|
|
|
return { |
|
leftLShapeScale: leftLShapeScale, |
|
rightLShapeScale: rightLShapeScale, |
|
bottomLShapeScale: bottomLShapeScale |
|
}; |
|
} |
|
|
|
function computeSectionScale() { |
|
// 6th animation consists on smoothly set the Penrose Triangle thiner, then bolder, and go back to its default thickness |
|
const animCPs = [ |
|
animConfs[5].start, // wait text to reach viewport's center |
|
animConfs[5].start+.5*panelHeight, // bolder |
|
animConfs[5].start+.75*panelHeight, // thinner |
|
animConfs[5].end // leave thinner |
|
]; |
|
const ptCPs = [ |
|
pTThickerData, |
|
pTRect0Data, |
|
pTRect1Data, |
|
pTRect1Data |
|
]; |
|
|
|
const leftLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeLeftLShapeCPs(ptCPs)); |
|
const rightLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeRightLShapeCPs(ptCPs)); |
|
const bottomLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeBottomLShapeCPs(ptCPs)); |
|
|
|
return { |
|
leftLShapeScale: leftLShapeScale, |
|
rightLShapeScale: rightLShapeScale, |
|
bottomLShapeScale: bottomLShapeScale |
|
}; |
|
} |
|
|
|
function computeAsymetricScale() { |
|
// 7th animation consists on smoothly set the Penrose Triangle thiner, then bolder, and go back to its default thickness |
|
const animCPs = [ |
|
animConfs[6].start, // wait text to reach viewport's center |
|
animConfs[6].start+.25*panelHeight, // asymetric Penrose triangle |
|
animConfs[6].end // leave asymetric Penrose triangle |
|
]; |
|
const ptCPs = [ |
|
pTRect1Data, |
|
pTAsymetricData, |
|
pTAsymetricData |
|
]; |
|
|
|
const leftLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeLeftLShapeCPs(ptCPs)); |
|
const rightLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeRightLShapeCPs(ptCPs)); |
|
const bottomLShapeScale = d3.scaleLinear() |
|
.domain(animCPs) |
|
.range(computeBottomLShapeCPs(ptCPs)); |
|
|
|
return { |
|
leftLShapeScale: leftLShapeScale, |
|
rightLShapeScale: rightLShapeScale, |
|
bottomLShapeScale: bottomLShapeScale |
|
}; |
|
} |
|
|
|
function computePTData(iw, ext1, ext2) { |
|
/* |
|
We consider Penrose Triangles where : |
|
1- inner width 'iw' is the same along the 3 axes |
|
2- extension 'ext1' is the same along the 3 axes |
|
3- extension 'ext2' is the same along the 3 axes |
|
4- inner width 'iw', extensions 'ext1' and 'ext2' may be of different size |
|
For convention : |
|
* oblique x axis 'ox' goes to the right |
|
* oblique y axis 'oy' goes to the top, in a oblique fashion |
|
* [0,0] is at the center of the inner triangle (not shown below) |
|
|
|
left L-shape |
|
/ ext2 |
|
oy / ______ p4 ______ p3 |
|
/_____ / /\ / / |
|
ox / / \ / / |
|
/ /....\ / / |
|
/ /. .\ / / |
|
/ / . . \ / / |
|
/ /...... \ / / |
|
/ / /\ \ / / |
|
/ / ext1/ \ \ / / |
|
/ / / \ \ / / |
|
/ / /\ \ \ / / |
|
/ / iw/ \iw \ \ / / |
|
/...../_____/____\ \ \ / /_____._____ |
|
/ ext1 iw \ \ \ / p2 p1 p0 |
|
/ \ext1 \ \ / |
|
/______________________\ \ \ p5 / |
|
\ . \ / |
|
ext2\ . \ /ext2 |
|
\_______________________._____\/ |
|
*/ |
|
|
|
//begin: lengths between points |
|
let p0p1 = iw; |
|
let p1p2 = ext1; |
|
let p2p3 = iw+ext1+ext1+ext2; |
|
let p3p4 = ext2; |
|
let p4p5 = p2p3+ext1; |
|
//end: lengths between points |
|
|
|
//leftLShape encodes information on the L shape with the left most line |
|
let leftLShape = {}; |
|
leftLShape.obliqueStart = [3*iw/4,-iw/2]; //p0 |
|
leftLShape.obliqueDeltas = [ |
|
[-p0p1, 0], |
|
[-p1p2, 0], |
|
[0, p2p3], |
|
[-p3p4, 0], |
|
[0, -p4p5] |
|
]; |
|
leftLShape.orthoStart = orthoCoord(leftLShape.obliqueStart); |
|
leftLShape.orthoDeltas = orthoCoords(leftLShape.obliqueDeltas); |
|
|
|
//rightLShape encodes information on the inverted-L shape with the right most line |
|
let rightLShape = {}; |
|
rightLShape.obliqueStart = [-iw/4,-iw/2]; |
|
rightLShape.obliqueDeltas = [ |
|
[0, p0p1], |
|
[0, p1p2], |
|
[p2p3, -p2p3], |
|
[0, p3p4], |
|
[-p4p5, p4p5] |
|
]; |
|
rightLShape.orthoStart = orthoCoord(rightLShape.obliqueStart); |
|
rightLShape.orthoDeltas = orthoCoords(rightLShape.obliqueDeltas); |
|
|
|
//bottomLshape encodes information on the inverted-L shape with the bottom most line |
|
let bottomLShape = {}; |
|
bottomLShape.obliqueStart = [-iw/4, iw/2]; |
|
bottomLShape.obliqueDeltas = [ |
|
[p0p1, -p0p1], |
|
[p1p2, -p1p2], |
|
[-p2p3, 0], |
|
[p3p4, -p3p4], |
|
[p4p5, 0] |
|
]; |
|
bottomLShape.orthoStart = orthoCoord(bottomLShape.obliqueStart); |
|
bottomLShape.orthoDeltas = orthoCoords(bottomLShape.obliqueDeltas); |
|
|
|
return { |
|
leftLShape: leftLShape, |
|
rightLShape: rightLShape, |
|
bottomLShape: bottomLShape, |
|
leftLShapePath: liner(leftLShape), |
|
rightLShapePath: liner(rightLShape), |
|
bottomLShapePath: liner(bottomLShape) |
|
} |
|
} |
|
|
|
function computeAsymetricPTData(iw, ext1, ext2, ext3) { |
|
/* |
|
We consider Penrose Triangles where : |
|
1- inner width 'iw' is the same along the 3 axes |
|
2- inner width 'iw', extensions 'ext1' and 'ext2' and 'ext3' may be of different size |
|
3- in order to make appealing Penrose triangle, there exist some constaints between one L-shape's height and another L-shape's width, in a circular way |
|
For convention : |
|
* oblique x axis 'ox' goes to the right |
|
* oblique y axis 'oy' goes to the top, in a oblique fashion |
|
* [0,0] is at the center of the inner triangle (not shown below) |
|
|
|
|
|
/ ext2 |
|
oy / ___________ |
|
/_____ / /\ |
|
ox / / \ |
|
/ / \ |
|
/ / \ |
|
/ / \ |
|
/ / \ |
|
/ / \ |
|
/ / \ |
|
/ / \ |
|
/ / \ |
|
/ / \ |
|
/ / \ |
|
/ / /\ \ |
|
/ / / \ \ |
|
/ / ext2/ \ \ |
|
/ / / \ \ |
|
/ / / \ \ |
|
/ / / \ \ |
|
/ / /\ \ \ |
|
/ / iw/ \iw \ \ |
|
/ /_____/____\ \ \ |
|
/ ext1 iw \ \ \ |
|
/ \ \ \ |
|
/ \ \ \ |
|
/ \ext3 \ / |
|
/ \ \ / |
|
/ \ \ / |
|
/ \ \ /ext3 |
|
/ \ \ / |
|
/_______________________________________\ \ / |
|
\ \ / |
|
ext1\ \ / |
|
\____________________________________________________\/ |
|
*/ |
|
|
|
//begin: lengths between points |
|
let left_p0p1 = iw; |
|
let left_p1p2 = ext1; |
|
let left_p2p3 = iw+ext1+ext2+ext3; |
|
let left_p3p4 = ext2; |
|
let left_p4p5 = left_p2p3+ext3; |
|
|
|
let right_p0p1 = iw; |
|
let right_p1p2 = ext2; |
|
let right_p2p3 = iw+ext1+ext2+ext3; |
|
let right_p3p4 = ext3; |
|
let right_p4p5 = right_p2p3+ext1; |
|
|
|
let bottom_p0p1 = iw; |
|
let bottom_p1p2 = ext3; |
|
let bottom_p2p3 = iw+ext1+ext2+ext3; |
|
let bottom_p3p4 = ext1; |
|
let bottom_p4p5 = bottom_p2p3+ext2; |
|
//end: lengths between points |
|
|
|
//leftLShape encodes information on the L shape with the left most line |
|
let leftLShape = {}; |
|
leftLShape.obliqueStart = [3*iw/4,-iw/2]; //p0 |
|
leftLShape.obliqueDeltas = [ |
|
[-left_p0p1, 0], |
|
[-left_p1p2, 0], |
|
[0, left_p2p3], |
|
[-left_p3p4, 0], |
|
[0, -left_p4p5] |
|
]; |
|
leftLShape.orthoStart = orthoCoord(leftLShape.obliqueStart); |
|
leftLShape.orthoDeltas = orthoCoords(leftLShape.obliqueDeltas); |
|
|
|
//rightLShape encodes information on the inverted-L shape with the right most line |
|
let rightLShape = {}; |
|
rightLShape.obliqueStart = [-iw/4,-iw/2]; |
|
rightLShape.obliqueDeltas = [ |
|
[0, right_p0p1], |
|
[0, right_p1p2], |
|
[right_p2p3, -right_p2p3], |
|
[0, right_p3p4], |
|
[-right_p4p5, right_p4p5] |
|
]; |
|
rightLShape.orthoStart = orthoCoord(rightLShape.obliqueStart); |
|
rightLShape.orthoDeltas = orthoCoords(rightLShape.obliqueDeltas); |
|
|
|
//bottomLshape encodes information on the inverted-L shape with the bottom most line |
|
let bottomLShape = {}; |
|
bottomLShape.obliqueStart = [-iw/4, iw/2]; |
|
bottomLShape.obliqueDeltas = [ |
|
[bottom_p0p1, -bottom_p0p1], |
|
[bottom_p1p2, -bottom_p1p2], |
|
[-bottom_p2p3, 0], |
|
[bottom_p3p4, -bottom_p3p4], |
|
[bottom_p4p5, 0] |
|
]; |
|
bottomLShape.orthoStart = orthoCoord(bottomLShape.obliqueStart); |
|
bottomLShape.orthoDeltas = orthoCoords(bottomLShape.obliqueDeltas); |
|
|
|
return { |
|
leftLShape: leftLShape, |
|
rightLShape: rightLShape, |
|
bottomLShape: bottomLShape, |
|
leftLShapePath: liner(leftLShape), |
|
rightLShapePath: liner(rightLShape), |
|
bottomLShapePath: liner(bottomLShape) |
|
} |
|
} |
|
|
|
function initLayout() { |
|
svg = d3.select("#sticky").append("svg") |
|
.attr("width", 960) |
|
.attr("height", 500) |
|
|
|
drawingArea = svg.append("g") |
|
.classed("drawing-area", true) |
|
.attr("transform", "translate("+[margins.top, margins.left]+")"); |
|
|
|
penroseTriangle = drawingArea |
|
.classed("penrose-triangle", true) |
|
.attr("transform", "translate("+[width/2, height/2]+")"); |
|
|
|
penroseTriangle |
|
.selectAll(".l-shape") |
|
.data([pTData.leftLShape, pTData.rightLShape, pTData.bottomLShape]) |
|
.enter() |
|
.append("path") |
|
.classed("l-shape", true) |
|
.attr("d", function(d){ return liner(d); }); |
|
} |
|
</script> |
|
</body> |