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.
xxxxxxxxxx
<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>
https://d3js.org/d3-timer.v1.min.js