Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active January 25, 2019 15:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kcnarf/56a2e78c6df01d6e17556c21cfe583af to your computer and use it in GitHub Desktop.
Save Kcnarf/56a2e78c6df01d6e17556c21cfe583af to your computer and use it in GitHub Desktop.
Popple spiral illusion
license: gpl-3.0

This block is a recreation that attemps to recreate an optical illusion I'm particularly fan of, called the popple illusion. This particular illusion disturbs perception of lines. Here, concentric rings no longer appear as rings (at least, for me !). It works also for 3, 2, or even 1 ring(s).

More on this optical illusion at Akiyoshi Kitaoka's dedicated web site's page.

Acknowledgments to:

<meta charset="utf-8">
<style>
#under-construction {
display: none;
position: absolute;
top: 200px;
left: 300px;
font-size: 40px;
}
#flexer {
display: flex;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
}
.relativer {
position: relative;
}
.cropped-blurred-points, #transparency, #pattern {
position: absolute;
top: 0;
left: 0;
}
.cropped-blurred-points {
transform-origin: 50% 50%;
#animation: rotating 10s linear infinite;
}
.title {
position: absolute;
top: 50%;
left: 50%;
color: #aaa;
}
.title div {
transform: translate(-50%, -50%);
text-align: center;
}
#explanation .title div {
line-height: 0.7em;
transform: translate(-50%, calc(-50% - 12px));
}
@keyframes rotating {
from{
transform: rotate(0deg);
}
to{
transform: rotate(360deg);
}
}
.hidden {
display: none;
}
</style>
<body>
<div id="flexer">
<div id="explanation" class="relativer">
<canvas class="background"></canvas>
<canvas id="transparency"></canvas>
<canvas id="pattern"></canvas>
<div class="title"><div>4 rings<br/>+<br/>2 patterns</div></div>
</div>
<div id="spiral" class="relativer">
<canvas class="background"></canvas>
<canvas class="cropped-blurred-points"></canvas>
<div class="title"><div>spiral</div></div>
</div>
<div id="zigzag" class="relativer">
<canvas class="background"></canvas>
<canvas class="cropped-blurred-points"></canvas>
<div class="title"><div>zigzag</div></div>
</div>
<div id="cardioid" class="relativer">
<canvas class="background"></canvas>
<canvas class="cropped-blurred-points"></canvas>
<div class="title"><div>cardioid</div></div>
</div>
<div id="random" class="relativer">
<canvas class="background"></canvas>
<canvas class="cropped-blurred-points"></canvas>
<div class="title"><div>random</div></div>
</div>
<div id="updated-random" class="relativer">
<canvas class="background"></canvas>
<canvas class="cropped-blurred-points"></canvas>
<div class="title"><div>updated<br/>random</div></div>
</div>
</div>
<canvas id="points" class="hidden"></canvas>
<canvas id="blurred-points" class="hidden"></canvas>
<div id="under-construction">
UNDER CONSTRUCTION
</div>
<script src="https://d3js.org/d3-timer.v1.min.js"></script>
<script>
const _PI = Math.PI,
_2PI = 2*Math.PI,
cos = Math.cos,
sin = Math.sin,
random = Math.random;
//begin: display conf.
const size = 250,
halfSize = size/2,
pointsPerRing = 72, // multipple of 4
spaceBetweenRings = halfSize/7,
spaceFromOuter = 1.5*spaceBetweenRings;
let ringNumber = 4;
//end: display conf.
const illusionTypes = ["spiral", "zigzag", "cardioid", "random", "updated-random"];
//begin: reusable DOM elements
const transparencyCanvas = document.querySelector("#transparency"),
pointsCanvas = document.querySelector("#points"),
blurredPointsCanvas = document.querySelector("#blurred-points");
//end: reusable DOM elements
initLayout();
drawStaticCanvases();
d3.interval(function(elapsed) {
makeIllusion("updated-random");
}, 1000);
function initLayout() {
document.querySelectorAll("canvas").forEach(function(c){
c.width = size;
c.height = size;
});
}
function drawStaticCanvases() {
makeBackgrounds();
makeTransparency();
makePattern();
illusionTypes.forEach(it=>makeIllusion(it));
}
function makeBackgrounds() {
let backgroundContext;
document.querySelectorAll(".background").forEach(function(c){
backgroundContext = c.getContext("2d");
backgroundContext.clearRect(0,0,size,size);
backgroundContext.fillStyle = "grey";
backgroundContext.filter = "blur(2px)";
backgroundContext.translate(halfSize, halfSize);
backgroundContext.beginPath();
backgroundContext.arc(0, 0, halfSize-5, 0, _2PI);
backgroundContext.fill();
backgroundContext.resetTransform();
})
}
function makePattern() {
const patternCanvas = document.querySelector("#pattern")
patternContext = patternCanvas.getContext("2d"),
blur = 1,
pointRadius = 2;
patternContext.clearRect(0,0,size,size);
patternContext.filter = "blur("+blur+"px)";
for (let p=0; p<2; p++) {
patternContext.translate(halfSize+(-13+17*p)*pointRadius, halfSize+20);
drawPoint(patternContext, 0, 0, pointRadius, "black");
patternContext.translate(3*pointRadius, 0);
if (p<1) {
drawPoint(patternContext, pointRadius, _PI/2, pointRadius, "black");
drawPoint(patternContext, pointRadius, -_PI/2, pointRadius, "white");
} else {
drawPoint(patternContext, pointRadius, _PI/2, pointRadius, "white");
drawPoint(patternContext, pointRadius, -_PI/2, pointRadius, "black");
}
patternContext.translate(3*pointRadius, 0);
drawPoint(patternContext, 0, 0, pointRadius, "white");
patternContext.translate(3*pointRadius, 0);
if (p<1) {
drawPoint(patternContext, pointRadius, _PI/2, pointRadius, "white");
drawPoint(patternContext, pointRadius, -_PI/2, pointRadius, "black");
} else {
drawPoint(patternContext, pointRadius, _PI/2, pointRadius, "black");
drawPoint(patternContext, pointRadius, -_PI/2, pointRadius, "white");
}
patternContext.resetTransform();
}
}
function makeTransparency() {
const transparencyContext = transparencyCanvas.getContext("2d"),
lineWidth = halfSize/20/2,
blur = lineWidth/2;
let ringRadius;
transparencyContext.clearRect(0,0,size,size);
//begin: draw greyscale 'mate' image
transparencyContext.fillRect(0,0,size,size);
transparencyContext.lineWidth = lineWidth;
transparencyContext.strokeStyle = "white";
transparencyContext.filter = "blur("+blur+"px)";
transparencyContext.translate(halfSize, halfSize); // position at canvas' center
transparencyContext.rotate(-_PI/2);
for (let r=0; r<ringNumber; r++) {
ringRadius = halfSize-spaceFromOuter-r*spaceBetweenRings;
transparencyContext.beginPath();
transparencyContext.arc(0, 0, ringRadius, 0, _2PI);
//transparencyContext.arc(0, 0, ringRadius, 0, _2PI*(0.5+0.5*random()));
transparencyContext.stroke();
transparencyContext.stroke(); //twice for saturation
}
//end: draw greyscale 'mate' image
//begin: update alpha channel
const imageData = transparencyContext.getImageData(0, 0, size, size),
pixels = imageData.data;
let pixel = 0;
for(pixel=0 ; pixel<size*size*4; pixel+=4) {
pixels[pixel+3]=pixels[pixel];
}
transparencyContext.putImageData(imageData, 0, 0);
//end: update alpha channel
transparencyContext.resetTransform();
}
function makeIllusion(illusionType){
switch (illusionType) {
case "spiral":
makeSpiralPoints();
break;
case "zigzag":
makeZigZagPoints();
break;
case "cardioid":
makeCardioidPoints();
break;
default:
makeRandomPoints();
break;
}
makeBlurredPoints();
makeCroppedBlurredPointsCanvas(
document.querySelector("#"+illusionType+" .cropped-blurred-points")
);
}
function makeSpiralPoints() {
const pointsContext = pointsCanvas.getContext("2d");
let ringRadius, pointRadius, ringRadiusPlus, ringRadiusMinus;
pointsContext.clearRect(0,0,size,size);
pointsContext.translate(halfSize, halfSize); // position at canvas' center
pointsContext.rotate(-_PI/2);
for (let r=0; r<ringNumber; r++) {
ringRadius = halfSize-spaceFromOuter-r*spaceBetweenRings;
pointRadius = ringRadius/pointsPerRing*2;
ringRadiusPlus = ringRadius+1.5*pointRadius;
ringRadiusMinus = ringRadius-1.5*pointRadius;
for (let p=0, j=0; p<pointsPerRing; p++, j+=_2PI/pointsPerRing) {
if (p%4 === 0) {
drawPoint(pointsContext, ringRadius, j, pointRadius, "black");
}
else if (p%4 === 1) {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "white");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "black");
}
else if (p%4 === 2) {
drawPoint(pointsContext, ringRadius, j, pointRadius, "white");
}
else /*(p%4 === 3)*/ {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "black");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "white");
}
}
}
pointsContext.resetTransform();
}
function makeZigZagPoints() {
const pointsContext = pointsCanvas.getContext("2d");
let ringRadius, pointRadius, ringRadiusPlus, ringRadiusMinus;
pointsContext.clearRect(0,0,size,size);
pointsContext.translate(halfSize, halfSize); // position at canvas' center
pointsContext.rotate(-_PI/2);
for (let r=0; r<ringNumber; r++) {
ringRadius = halfSize-spaceFromOuter-r*spaceBetweenRings;
pointRadius = ringRadius/pointsPerRing*2;
ringRadiusPlus = ringRadius+1.5*pointRadius;
ringRadiusMinus = ringRadius-1.5*pointRadius;
for (let p=0, j=0; p<pointsPerRing; p++, j+=_2PI/pointsPerRing) {
if (p%4 === 0) {
drawPoint(pointsContext, ringRadius, j, pointRadius, "black");
}
else if (p%4 === 1) {
if (r%2) {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "white");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "black");
} else {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "black");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "white");
}
}
else if (p%4 === 2) {
drawPoint(pointsContext, ringRadius, j, pointRadius, "white");
}
else /*(p%4 === 3)*/ {
if (r%2) {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "black");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "white");
} else {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "white");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "black");
}
}
}
}
pointsContext.resetTransform();
}
function makeCardioidPoints() {
const pointsContext = pointsCanvas.getContext("2d");
let ringRadius, pointRadius, ringRadiusPlus, ringRadiusMinus;
pointsContext.clearRect(0,0,size,size);
pointsContext.translate(halfSize, halfSize); // position at canvas' center
pointsContext.rotate(-_PI/2);
for (let r=0; r<ringNumber; r++) {
ringRadius = halfSize-spaceFromOuter-r*spaceBetweenRings;
pointRadius = ringRadius/pointsPerRing*2;
ringRadiusPlus = ringRadius+1.5*pointRadius;
ringRadiusMinus = ringRadius-1.5*pointRadius;
for (let p=0, j=0; p<pointsPerRing; p++, j+=_2PI/pointsPerRing) {
if (p%4 === 0) {
drawPoint(pointsContext, ringRadius, j, pointRadius, "black");
}
else if (p%4 === 1) {
if (p<pointsPerRing/2) {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "white");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "black");
} else {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "black");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "white");
}
}
else if (p%4 === 2) {
drawPoint(pointsContext, ringRadius, j, pointRadius, "white");
}
else /*(p%4 === 3)*/ {
if (p<pointsPerRing/2) {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "black");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "white");
} else {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "white");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "black");
}
}
}
}
pointsContext.resetTransform();
}
function makeRandomPoints() {
const pointsContext = pointsCanvas.getContext("2d");
let ringRadius, pointRadius, ringRadiusPlus, ringRadiusMinus, slope;
pointsContext.clearRect(0,0,size,size);
pointsContext.translate(halfSize, halfSize); // position at canvas' center
pointsContext.rotate(-_PI/2);
for (let r=0; r<ringNumber; r++) {
ringRadius = halfSize-spaceFromOuter-r*spaceBetweenRings;
pointRadius = ringRadius/pointsPerRing*2;
ringRadiusPlus = ringRadius+1.5*pointRadius;
ringRadiusMinus = ringRadius-1.5*pointRadius;
for (let p=0, j=0; p<pointsPerRing; p++, j+=_2PI/pointsPerRing) {
if (p%16 === 0) {
//lower modulos make more heratic illusion
slope = (random()>0.5);
}
if (p%4 === 0) {
drawPoint(pointsContext, ringRadius, j, pointRadius, "black");
}
else if (p%4 === 1) {
if (slope) {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "white");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "black");
} else {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "black");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "white");
}
}
else if (p%4 === 2) {
drawPoint(pointsContext, ringRadius, j, pointRadius, "white");
}
else {
if (slope) {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "black");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "white");
} else {
drawPoint(pointsContext, ringRadiusPlus, j, pointRadius, "white");
drawPoint(pointsContext, ringRadiusMinus, j, pointRadius, "black");
}
}
}
}
pointsContext.resetTransform();
}
function drawPoint(context, distance, angle, radius, color) {
context.beginPath();
context.fillStyle = color;
context.arc(distance*cos(angle), distance*sin(angle), radius, 0, _2PI);
context.fill();
}
function makeBlurredPoints() {
const blurredPointsContext = blurredPointsCanvas.getContext("2d"),
imageData = blurredPointsContext.createImageData(size, size),
lineWidth = halfSize/20,
blur = lineWidth/5;
blurredPointsContext.clearRect(0,0,size,size);
blurredPointsContext.filter = "blur("+blur+"px)";
blurredPointsContext.drawImage(pointsCanvas, 0, 0);
blurredPointsContext.drawImage(pointsCanvas, 0, 0); //twice for saturation
}
function makeCroppedBlurredPointsCanvas(canvas) {
const context = canvas.getContext("2d");
context.clearRect(0,0,size,size);
//begin: use 'blurred-points' to color pixels of the 'transparency' canvas
context.clearRect(0, 0, size, size);
context.globalCompositeOperation = "source-over";
context.drawImage(blurredPointsCanvas, 0, 0);
context.globalCompositeOperation = "destination-in";
context.drawImage(transparencyCanvas, 0, 0);
//begin: use 'blurred-points' to color pixels of the 'transparency' canvas
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment