Interactive demonstration of the Curvature Blindness Illusion.
Last active
December 16, 2017 05:07
-
-
Save akngs/6e8d225d9922bbcb5783d890c45e8430 to your computer and use it in GitHub Desktop.
Curvature Blindness Illusion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>Curvature Blindness Illusion</title> | |
<style> | |
html { | |
font-size: 14px; | |
font-family: sans-serif; | |
} | |
body { | |
margin: 0 auto; | |
max-width: 40em; | |
padding: 1em; | |
} | |
svg { | |
margin: 1em 0; | |
border: 1px solid black; | |
} | |
form { | |
column-count: 2; | |
} | |
form .row { | |
padding: 0.2em 0; | |
} | |
form label { | |
display: inline-block; | |
width: 7em; | |
} | |
form input[type="range"] { | |
width: 7em; | |
} | |
</style> | |
</head> | |
<body> | |
<p> | |
Some parts of lines below are perceived as zigzags while there are only wavy lines. | |
This phenomenon is called <a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5703117/">Curvature Blindness Illusion</a>. | |
</p> | |
<p> | |
The illusion disappears if the background color is <a href="#" onclick="javascript:update({color0: 0.3, color1: 0.7, bgSteps: 2, unitPeriod: 30, amp: 0.2, nWaves: 10}); return false;">black or white</a>. | |
The illusion also disappears when there is <a href="#" onclick="javascript:update({color0: 0.3, color1: 0.3, bgSteps: 3, unitPeriod: 30, amp: 0.2, nWaves: 10}); return false;">no luminance contrast polarity</a> | |
or <a href="#" onclick="javascript:update({color0: 0.3, color1: 0.7, bgSteps: 3, unitPeriod: 30, amp: 0.7, nWaves: 10}); return false;">the curves are too steep</a>. | |
</p> | |
<p> | |
<a href="#" onclick="javascript:update({color0: 0.3, color1: 0.7, bgSteps: 3, unitPeriod: 30, amp: 0.2, nWaves: 10}); return false;">Click here to reset parameters</a> and see the illusion again. | |
</p> | |
<form> | |
<div class="row"> | |
<span class="pair color0"> | |
<label for="color0">Wave color 0:</label> | |
<input type="range" id="color0" value="0.3" min="0" max="1" step="0.05"> | |
<span class="value">0</span> | |
</span> | |
</div> | |
<div class="row"> | |
<span class="pair color1"> | |
<label for="color1">Wave color 1:</label> | |
<input type="range" id="color1" value="0.7" min="0" max="1" step="0.05"> | |
<span class="value">0</span> | |
</span> | |
</div> | |
<div class="row"> | |
<span class="pair bgSteps"> | |
<label for="bgSteps">BG steps:</label> | |
<input type="range" id="bgSteps" value="3" min="2" max="10" step="1"> | |
<span class="value">0</span> | |
</span> | |
</div> | |
<div class="row"> | |
<span class="pair unitPeriod"> | |
<label for="unitPeriod">Wave length:</label> | |
<input type="range" id="unitPeriod" value="30" min="10" max="100" step="1"> | |
<span class="value">0</span> | |
</span> | |
</div> | |
<div class="row"> | |
<span class="pair amp"> | |
<label for="amp">Amplitude:</label> | |
<input type="range" id="amp" value="0.2" min="0.1" max="2.0" step="0.1"> | |
<span class="value">0</span> | |
</span> | |
</div> | |
<div class="row"> | |
<span class="pair nWaves"> | |
<label for="nWaves"># of waves:</label> | |
<input type="range" id="nWaves" value="10" min="3" max="20" step="1"> | |
<span class="value">0</span> | |
</span> | |
</div> | |
</form> | |
<svg></svg> | |
<p>Programmed by <a href="https://twitter.com/alankang">@alankang</a></p> | |
<script src="//d3js.org/d3.v4.min.js"></script> | |
<script> | |
var W = 520, | |
H = 200, | |
pi = Math.PI, | |
sin = Math.sin, | |
qs = document.querySelector.bind(document), | |
root = d3.select('svg').attr('width', W).attr('height', H); | |
root.append('g').attr('class', 'bgs'); | |
root.append('g').attr('class', 'waves'); | |
d3.selectAll('input').on('change', update); | |
update(); | |
function update(params) { | |
// Update params | |
var paramTypes = { | |
'color0': String, | |
'color1': String, | |
'bgSteps': Number, | |
'unitPeriod': Number, | |
'amp': Number, | |
'nWaves': Number | |
}; | |
if(params) { | |
for(var key in paramTypes) { | |
qs('#' + key).value = paramTypes[key](params[key]); | |
} | |
} else { | |
params = {}; | |
for(var key in paramTypes) { | |
params[key] = paramTypes[key](qs('#' + key).value); | |
} | |
} | |
for(var key in paramTypes) { | |
qs('.' + key + ' .value').innerHTML = params[key]; | |
} | |
var color = d3.scaleLinear() | |
.interpolate(d3.interpolateLab) | |
.domain([0, 1]) | |
.range([d3.lab(100, 0, 0), d3.lab(0, 0, 0)]); | |
// Background | |
var bgBand = d3.scaleBand() | |
.domain(d3.range(params.bgSteps)) | |
.range([W * -0.2, W * 1.2]); | |
var bgSel = root.select('g.bgs') | |
.style('transform-origin', (W / 2) + 'px ' + (H / 2) + 'px') | |
.style('transform', 'rotate(45deg)') | |
.selectAll('rect').data(d3.range(params.bgSteps)); | |
bgSel.enter() | |
.append('rect') | |
.merge(bgSel) | |
.attr('x', bgBand) | |
.attr('y', H * -2) | |
.attr('height', H * 4) | |
.attr('width', bgBand.bandwidth()) | |
.attr('fill', function(d) {return color(d / (params.bgSteps - 1));}); | |
bgSel.exit().remove(); | |
// Waves | |
var samplesPerPeriod = 11; | |
var fragmentDef = d3.line() | |
.curve(d3.curveCardinal) | |
.x(function(d) {return d[0];}) | |
.y(function(d) {return d[1];}); | |
var y = d3.scaleLinear() | |
.domain([0, params.nWaves - 1]) | |
.range([params.amp * params.unitPeriod + 10, H - (params.amp * params.unitPeriod + 10)]); | |
var waveSel = root.select('g.waves').selectAll('.wave').data(d3.range(params.nWaves)); | |
waveSel.enter() | |
.append('g') | |
.attr('class', 'wave') | |
.merge(waveSel) | |
.style('transform', function(d) {return 'translate(0, ' + y(d) + 'px)';}) | |
.each(function(wave) { | |
var phase = wave % 2 ? 0 : pi / 2; | |
// Dots for a single sine wave | |
var dots = d3.range(samplesPerPeriod + 1).map(function(i) { | |
var x = i / samplesPerPeriod * params.unitPeriod; | |
var theta = phase + i / samplesPerPeriod * pi * 2; | |
return [x, -params.amp * params.unitPeriod * 0.5 * sin(theta)]; | |
}); | |
// Break a sine wave into two pieces | |
var heads = dots.slice(0, Math.floor(dots.length / 2)) | |
var tails = dots.slice(Math.floor(dots.length / 2) - 1); | |
var fragmentHead = fragmentDef(heads); | |
var fragmentTail = fragmentDef(tails); | |
// Repeat pieces to make a long wave | |
var fragmentRepeat = Math.ceil(W / params.unitPeriod + 1) * 2; | |
var fragments = d3.range(fragmentRepeat).map(function(i) { | |
return i % 2 ? fragmentHead : fragmentTail | |
}); | |
// Render | |
var fragmentSel = d3.select(this).selectAll('.fragment').data(fragments); | |
fragmentSel.enter() | |
.append('path') | |
.attr('class', function(d, i) {return 'fragment ' + (i % 2 ? 'head' : 'tail');}) | |
.merge(fragmentSel) | |
.style('transform', function(d, i) {return 'translate(' + (Math.floor(i / 2) * params.unitPeriod) + 'px, 0)';}) | |
.attr('d', function(d) {return d;}) | |
.attr('stroke', function(d, i) {return color(i % 2 === 0 ? params.color0 : params.color1);}) | |
.attr('stroke-width', 2) | |
.attr('fill', 'none'); | |
fragmentSel.exit().remove(); | |
}); | |
waveSel.exit().remove(); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment