Skip to content

Instantly share code, notes, and snippets.

@akngs
Last active December 16, 2017 05:07
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 akngs/6e8d225d9922bbcb5783d890c45e8430 to your computer and use it in GitHub Desktop.
Save akngs/6e8d225d9922bbcb5783d890c45e8430 to your computer and use it in GitHub Desktop.
Curvature Blindness Illusion
license: gpl-3.0
<!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