Skip to content

Instantly share code, notes, and snippets.

Last active January 5, 2019 15:38
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
What would you like to do?
# encoding: utf-8
This is an exploration of some of the calculations we can do
based on the size of a typographic detail in a shape, the distance
of the shape to the eye, the average amount of light that shines
on the shape, the amount of that reflected light that will be captured by the eye,
the aperture of the pupil and some other factors.
Where available, links to sources are included.
Erik van Blokland
February 2013
version 1.1
12 feb 2013: added an assumed value for paper reflectitivity
23 mar 2013: lowered the reflectivity of paper from 0.98 to 0.6
10 dec 2014: corrected the calculation of lightSphereSurface,
using the sphere surface area rather than volume. Oops.
15 jun 2018: update for python3
import time
import math
print("version", 1.4)
print("Exploration of light and shapes at reading sizes")
start = time.time()
# first: calculate the surface area of a shape.
# if you don't have RoboFont, or want to skipt this part
# for speed reasons, just comment out the section until shapeSurface = s
# and add your own value.
# step 1
g = CurrentGlyph()
if g is not None:
path = g.naked().getRepresentation("defconAppKit.NSBezierPath")
s = 0
xMin, yMin, xMax, yMax =
xMin = int(round(xMin))
yMin = int(round(yMin))
xMax= int(round(xMax))
yMax = int(round(yMax))
for x in range(xMin, xMax):
for y in range(yMin, yMax):
if path.containsPoint_((x,y)):
# we have no current glyph. Come on, make an effort.
print("No current glyph, assuming a shape is 100 by 100 units.")
s = 100 * 100
except NameError:
# we're probably not in RoboFont
# assume we're looking at a surface of 100 by 100 em units.
print("No current glyph, not even in RoboFont, assuming a shape is 100 by 100 units.")
s = 100 * 100
# this is the value we'll take as the surface area of the shape.
shapeSurface = s
print("surface of selected shape:", shapeSurface, "square units")
# step 2
# factor to convert points to mm
pt_mm = 0.352777778
# at 10pt
# (s*a/upm) * (s*a/upm)
# em : 1000 * 1000 = 1000000^2 u
# pt: 10pt * 10pt = 100pt^2
# assume we want to calculate the effects of the shape at 8pt, seen at 400 mm.
ptSize = 8
distance_mm = 200
size = ptSize * pt_mm
sqmm = (size**2 * shapeSurface) / 1e6
print("shape surface area in square millimeter", sqmm, "at %d pt"%ptSize)
# how many square meter is that shape then?
sqm = sqmm / 1e6
print("shape surface area in square meter", sqm)
# step 3
# Light conditions in an office
# 320–500 lux Office lighting[9][10][11]
# 1 lux = i lumen / m^2
#officeLux = 320
officeLux = 0.5*(500 + 320)
lux = officeLux * sqm
print("lux for this surface", lux, "assuming office lights at", officeLux, "lx")
# step 4
# Photons per lumen aren't easy to calculate. Different values are offered
# 3.8*10e15
photonsPerSecond = 3.8*10e15 * lux
# However:
# says: 1.3*10e16 is the number of photos per second per lumen of white light
photonsPerSecond = 1.3*10e16 * lux
# Either way, can we say they're both in the same ballpark? Not sure how large ballparks are.
# This is the total amount of light reflecting of our small surface. It scatters in all
# directions. Assuming the reading surface is flat, the light than scatters in a hemisphere.
print("Photons per second reflecting onto the surface", photonsPerSecond)
# step 5
# Assuming the paper is white and reflects most of the light that falls on it.
# Check: this factor is just an assumption. Could be much lower.
reflectedByPaper = 0.60
photonsPerSecond = photonsPerSecond * reflectedByPaper
# step 6
# So that's the total amount of light hitting that area.
# It scatters in all directions, right?
# The eye will sample a small section
# of half a sphere of reflected light
# hemisphere surface is 0.5 * 4/3 * pi r^3
lightSphereSurface = (4 * math.pi * distance_mm**2) * 0.5
print("Surface of the reflected hemisphere, r = eye distance", lightSphereSurface, "mm^2")
#pupil aperture, assume 4 mm, r=2mm
# Check: pupil aperture size should correspond with the lumen value picked earlier.
eyeAperture = 2*2*math.pi
print("Pupil aperture opening", eyeAperture, "mm2")
# section of reflected hemisphere actually reaching eye
reachedFraction = eyeAperture / lightSphereSurface
print("Fraction of the hemisphere of reflected light that reaches the pupil:", reachedFraction)
print("Photons per second reflected of the shape, captured by the eye:", reachedFraction * photonsPerSecond)
# the number of observed photons reflecting per second per font unit
photonsPerSecondPerFontUnit = (reachedFraction * photonsPerSecond) / shapeSurface
print("Photons per font unit per second, at %dpt, 400mm:"%ptSize, photonsPerSecondPerFontUnit)
#Seen from in front, the pupil of the eye looks very black – because only about 4% of
#incident light is reflected back. Of the light entering the pupil, about half is scattered
#or absorbed in the humours or the lens, so that only about half reaches the retina. Of that
#fraction, about 20% is absorbed by a photoreceptor, giving the eye an overall capture
#efficiency of 10%. This is considerably lower than that of nocturnal mammals, and much
#less than that of the huntsman spider. In a dark-adapted eye, about 7 photons absorbed
#in a few tens of ms in one photoreceptive field are required to be aware of a flash, so
#one could say that we need an absolute minimum of 70 photons to see. If a microwatt of
#light enters the eye, then the number of photons per second is 1μW/(energy per photon) = 1μWλ/hc = 2 X 1012 photons/s.
# so it is even less.
# assuming only 10% of incoming photos are actually absorbed by photoreceptors
print("Photons per second absorbed by photoreceptor, 'contributes to vision':", 0.1 * reachedFraction * photonsPerSecond)
print("Photons per second absorbed by photoreceptor, per font unit, 'contributes to vision':", 0.1 * reachedFraction * photonsPerSecondPerFontUnit)
#step 7
def pt_retina(pointSize, distance=400):
""" Postscript pointsize to projected size on retina, in mm. """
focalLength_in_eye_mm = 22.3
return focalLength_in_eye_mm * (pt_mm*pointSize)/distance
def pt_photoReceptors(pointSize, distance=400):
""" Postscript pointsize to number of photoreceptors"""
fovealDensity = math.sqrt(199000)
return pt_retina(pointSize, distance)*fovealDensity
# These functions want points
# Our test shape is 100 by 100 units. But if this calculation is run on an actual
# shape we can't really figure out the size, it might be wide or tall or whatever.
# If we take the root of the surface we assume the shape is square. It will at least
# give us an indication of the size.
ourShapeInPt = math.sqrt(shapeSurface) * 8.0/1000
print("pointsize to retina size, in mm", pt_retina(ourShapeInPt, distance_mm))
photoReceptorsReceiving = pt_photoReceptors(ourShapeInPt, distance_mm)**2
print("estimate of photoreceptors in that area", photoReceptorsReceiving)
print("Photons per second, per font unit, received by a single photoreceptor", photonsPerSecondPerFontUnit / photoReceptorsReceiving)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment