Skip to content

Instantly share code, notes, and snippets.

@kbseah
Last active January 11, 2017 19:05
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 kbseah/b912cb1e140367e6200e06c8c31f644d to your computer and use it in GitHub Desktop.
Save kbseah/b912cb1e140367e6200e06c8c31f644d to your computer and use it in GitHub Desktop.
Pulfrich effect - Animated explanation

Pulfrich effect - Animated explanation

The Pulfrich effect is an optical illusion where objects moving in a single plane also appear to have depth, because the processing of the visual signal in one eye is delayed, e.g. by holding a filter in front of one eye to dim the light reaching that eye but not the other.

See my other Gist for more info.

What this animation shows

This animation illustrates how the time delay in one eye leads to the stereoscopic effect. This diagram shows the view from above looking vertically down at a viewer facing a pendulum (or a display screen) oscillating within the (flat) plane of motion.

The light reaching the right eye is dimmed by a filter, and so it takes a longer time to process the image. Because of this delay, the image of the pendulum perceived by the right eye (grey disk) lags behind the position seen by the left eye (blue disk).

Where the lines of sight cross is the apparent position of the pendulum in stereoscopic vision. Normally, they would cross in the same plane as the pendulum and the motion would appear flat. However, with the delay in one eye, the lines of sight sometimes cross in front and sometimes behind the actual plane of motion, so the motion now appears to have depth!

Click on the buttons to observe the following:

  • Toggle the filter on and off
  • Speeding up the pendulum increases the magnitude of the effect
  • Darkening the filter increases the time delay and hence also the magnitude of the effect
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="author" content="Brandon Seah">
<head>
</head>
<body>
<div>
<button onclick="toggleOffset()">Toggle Filter</button>
<button onclick="changeSpeed(2)">Speed up pendulum</button>
<button onclick="changeSpeed(0.5)">Slow down pendulum</button>
<button onclick="changeOffset(1.5)">Darken filter</button>
<button onclick="changeOffset(0.75)">Lighten filter</button>
<svg class="stereo"></svg>
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// DEMO 1.5 - Stereoscopic lines of sight
// Define chart dimensions
var height=400;
var width=800;
// Starting parameters
var timeStereo=0;
var speedParamStereo=0.05;
var initX=0; // Should be either zero or width
var planeY=100; // Position of image plane
var eyesPlaneY=350; // Position of eyes plane
var eyeLeftX = 300;
var eyeRightX = 500;
// Render chart and background
var chartStereo = d3.select(".stereo")
.attr("height",height)
.attr("width",width)
.append("g");
// Background color light yellow
chartStereo.append("rect")
.attr("height",height)
.attr("width",width)
.attr("fill","#feffe7");
// Line to represent plane of pendulum
chartStereo.append("path")
.attr("d","M0 "+planeY+" H "+width)
.attr("fill","transparent")
.attr("stroke","black");
// Text label for pendulum plane
chartStereo.append("text")
.attr("x",10)
.attr("y",planeY-10)
.text("Actual plane of motion")
.attr("font-family","sans-serif")
.attr("font-size","18px")
.attr("fill","black");
// Pendulum
chartStereo.append("circle")
.attr("class","pend1")
.attr("r",10)
.attr("cx",initX)
.attr("cy",planeY)
.attr("fill","blue")
.attr("opacity",1);
// "shadow" pendulum with delay
chartStereo.append("circle")
.attr("class","pend2")
.attr("r",10)
.attr("cx",initX)
.attr("cy",planeY)
.attr("fill","grey")
.attr("opacity",0.5);
// Line to represent plane of eyes
chartStereo.append("path")
.attr("d","M0 "+eyesPlaneY+" H "+width)
.attr("fill","transparent")
.attr("stroke","black");
// Text label for eyes plane
chartStereo.append("text")
.attr("x",10)
.attr("y",eyesPlaneY+25)
.text("Position of eyes")
.attr("font-family","sans-serif")
.attr("font-size","18px")
.attr("fill","black");
// Left eye sightline
chartStereo.append("path")
.attr("class","leftEye")
.attr("d","M"+eyeLeftX+" "+eyesPlaneY+" L"+initX+" "+planeY)
.attr("fill","transparent")
.attr("stroke","blue");
// Right eye sightline
chartStereo.append("path")
.attr("class","rightEye")
.attr("d","M"+eyeRightX+" "+eyesPlaneY+" L"+initX+" "+planeY)
.attr("fill","transparent")
.attr("stroke","grey");
// Left eye pic
chartStereo.append("circle")
.attr("class","eye")
.attr("cx",eyeLeftX)
.attr("cy",eyesPlaneY+(height-eyesPlaneY)/2)
.attr("r",(height-eyesPlaneY)/2)
.attr("stroke","black")
.attr("fill","white");
// Right eye pic
chartStereo.append("circle")
.attr("class","eye")
.attr("cx",eyeRightX)
.attr("cy",eyesPlaneY+(height-eyesPlaneY)/2)
.attr("r",(height-eyesPlaneY)/2)
.attr("stroke","black")
.attr("fill","white");
// Right eye label
chartStereo.append("text")
.attr("class","filterText")
.attr("x",eyeRightX+30)
.attr("y",eyesPlaneY+25)
.text("Right eye sees delayed image")
.attr("font-family","sans-serif")
.attr("font-size","14px")
.attr("fill","black");
// Filter pic
filterOpacity=0.5;
chartStereo.append("rect")
.attr("class","filter")
.attr("x",eyeRightX-50)
.attr("y",eyesPlaneY-20)
.attr("height",5)
.attr("width",100)
.attr("fill","black")
.attr("opacity",filterOpacity);
// Filter label
chartStereo.append("text")
.attr("class","filterText")
.attr("x",eyeRightX+60)
.attr("y",eyesPlaneY-12)
.text("Filter")
.attr("font-family","sans-serif")
.attr("font-size","14px")
.attr("fill","black");
// Visual offset introduced by the filter
var offsetReset = 2;
var offset=offsetReset;
// Function to update image in each step
function updateStereo() {
// Calculate new positions of the pendulum and its "shadow"
var newPend1X = width*(1+Math.cos(timeStereo/Math.PI))/2;
var newPend2X = width*(1+Math.cos((timeStereo/Math.PI)-offset*speedParamStereo))/2;
chartStereo.select(".pend1")
.attr("cx",newPend1X);
chartStereo.select(".pend2")
.attr("cx",newPend2X);
// Calculate endpoints of sightlines with similar triangles
var newPendEnd1X = eyeLeftX - (newPend1X-eyeLeftX)*(eyesPlaneY/(planeY-eyesPlaneY));
var newPendEnd2X = eyeRightX - (newPend2X-eyeRightX)*(eyesPlaneY/(planeY-eyesPlaneY));
chartStereo.select(".leftEye")
.attr("d","M"+eyeLeftX+" "+eyesPlaneY+" L"+newPendEnd1X+" "+0);
chartStereo.select(".rightEye")
.attr("d","M"+eyeRightX+" "+eyesPlaneY+" L"+newPendEnd2X+" "+0);
}
function changeSpeed(factor) {
speedParamStereo = speedParamStereo*factor;
}
function changeOffset(factor) {
offsetReset = offsetReset*factor;
offset = offsetReset;
// Change opacity of filter - need a function which has horizontal asymptote at 1,
// range (0,1) and domain (0,Inf).
filterOpacity = filterOpacity + (2/Math.PI)*(Math.atan(Math.log(factor)));
chartStereo.selectAll(".filter")
.attr("opacity",filterOpacity);
}
// Function to change visual offset
function toggleOffset() {
if (offset > 0) {
// Reposition delayed pendulum
offset = 0;
// Recolor delayed pendulum and sightline
chartStereo.select(".pend2")
.attr("fill","blue");
chartStereo.select(".rightEye")
.attr("stroke","blue");
chartStereo.selectAll(".filter")
.attr("display","none");
chartStereo.selectAll(".filterText")
.attr("display","none");
} else if (offset == 0) {
// Return delayed virtual pendulum
offset = offsetReset;
// Recolor delayed pendulum and sightline to grey
chartStereo.select(".pend2")
.attr("fill","grey");
chartStereo.select(".rightEye")
.attr("stroke","grey");
chartStereo.selectAll(".filter")
.attr("display","unset");
chartStereo.selectAll(".filterText")
.attr("display","unset");
}
}
// Timer function to render animation
setInterval( function() {
timeStereo = timeStereo + speedParamStereo;
return updateStereo();
}, 10);
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment