Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active December 7, 2019 10:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Kcnarf/476fe66949490c53f7085c24832612ca to your computer and use it in GitHub Desktop.
Save Kcnarf/476fe66949490c53f7085c24832612ca to your computer and use it in GitHub Desktop.
Scrollytelling the Penrose triangle
license: mit

This block experiments scrolly telling with D3.

I apply the technique described by tonyhschu in its Small Scroll-linked Animation Demo block.

The main loop is available through the refresh function (loc #246). The entire story is composed of severall animations/scenes, executed in sequence. Configurations of animations are available right after the main loop, each possibly defining onEnter, animate and onExit callbacks.

A tricky thing was to convert a linear vertical scrolling, to a non-linear animation. For instance, in the main animation (explaining how to hand drawn a Penrose triangle), it takes the same amount of vertical scroll to draw tiny segments (e.i. first, second and fourth segments) and longer ones (i.e. third and fifth ones). Hence, the animation must go faster than normal to draw longer segments. To do so, I use a d3.scaleLinear() with many values. Standard use case sets the domain and range both with 2 values (start and stop mapping). But one can set these props with EXTRA VALUES in order to set INTERMEDIATE MAPPINGS ! (see this tweet).

The use case is a step by step animated explanation on how to draw a Penrose triangle (and some derivatives, at the end of the story).

Acknowledgments to :

<!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>&nbsp;</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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment