I wrote a short python script to generate optical illusion buttons that can’t be read very well unless you tilt the screen or image to a very low angle, almost edge-on, yielding images like this:

If you’re doing it right, you should see the phrase, “NPR COOL DAD ROCK.”
The code is below, to generate an 18-character-or-less button. There are two things to be aware of: the only two dependencies are PIL and NumPy, and if you’re not on a Debian-based Linux system, you may need to adjust a line of code near the top to find the font you want. Try experimenting with parameters and fonts, some definitely work better than others. Narrow fonts tend to look pretty good. I’d love to optimize for speed, but it only takes about 3 or 4 seconds to run on my machine. This should work with either Python 2 or 3 (tested with 3.5). This repo is now hosted on GitHub here.
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw
from PIL import ImageOps
from numpy import sqrt
illusion_text = "NPR COOL DAD ROCK"
text_color = (0, 0, 0)
background_color = (255, 255, 255)
img_side = 1024
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-ExtraLight.ttf"
charmax = 18
font_size_guess = 7*(30-len(illusion_text))
crop_width_x = 12
crop_width_y = 3
num_rotations = 6
darkness_threshold = 108
if len(illusion_text) <= charmax:
pass
else:
print('WARNING: Text too long. Exiting.')
raise SystemExit(0)
img_size = (img_side, img_side)
img_size_text = (img_side, img_side)
raw_img = Image.new("RGB", img_size_text, background_color)
img = Image.new("RGBA", img_size, background_color)
circle_img = Image.new("RGBA", img_size, background_color)
full_image = Image.new("RGBA", img_size, background_color)
draw = ImageDraw.Draw(raw_img)
# step through font sizes to find optimal font for box
for font_trial in range(font_size_guess-30, font_size_guess+30):
possible_font = ImageFont.truetype(font_path, font_trial)
raw_img = Image.new("RGB", img_size_text, background_color)
draw = ImageDraw.Draw(raw_img)
draw.text((crop_width_x, crop_width_y), illusion_text, text_color, font=possible_font)
# find bounding box of text by inversion
inverted = ImageOps.invert(raw_img)
possible_boundingbox = (inverted.getbbox()[0] - crop_width_x, \
inverted.getbbox()[1] - crop_width_y, \
inverted.getbbox()[2] + crop_width_x, \
inverted.getbbox()[3] + crop_width_y)
if possible_boundingbox[2] - possible_boundingbox[0] < img_side-2*crop_width_x:
boundingbox = possible_boundingbox
font_size = font_trial
else:
break
raw_img = Image.new("RGB", img_size_text, background_color)
draw = ImageDraw.Draw(raw_img)
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-ExtraLight.ttf", font_size)
draw.text((crop_width_x, crop_width_y), illusion_text, text_color, font=font)
inverted = ImageOps.invert(raw_img)
raw_img = raw_img.crop(boundingbox)
scaled_img = raw_img.resize((img_side, img_side), Image.BICUBIC)
img.paste(scaled_img, (0, 0))
# map points in the square image to points in a circle
# turn light grey and white to alpha channel. Blacken dark grays.
pixdata = img.load()
for y in range(img.size[1]):
for x in range(img.size[0]):
if pixdata[x, y] == (255, 255, 255, 255):
pixdata[x, y] = (255, 255, 255, 0)
elif pixdata[x, y][1] >= darkness_threshold:
pixdata[x, y] = (255, 255, 255, 0)
elif pixdata[x, y][1] <= darkness_threshold:
pixdata[x, y] = (0, 0, 0, 255)
circle_img.paste(img, (0, 0))
pixdata2 = circle_img.load()
for x in range(img_side):
Ysize = 2 * sqrt((img_side / 2) ** 2 - (x - (img_side / 2)) ** 2)
for y in range(img_side):
Yoffset = int((img_side-Ysize)/2.)
Y = Yoffset + int((Ysize/img_side)*y)
pixdata2[x, Y] = pixdata[x, y]
if sqrt((x-img_side/2)**2 + (y-img_side/2)**2) >= img_side/2 - 2:
pixdata2[x, y] = (255, 255, 255, 0)
for i in range(num_rotations):
this_circle = circle_img.rotate(i*180/num_rotations)
full_image.paste(this_circle, (0, 0), this_circle)
pixdata = full_image.load()
for y in range(full_image.size[1]):
for x in range(full_image.size[0]):
if pixdata[x, y] == (255, 255, 255, 0):
pixdata[x, y] = (255, 255, 255, 255)
full_image.save(illusion_text+".png")