This toy had a generic set of lights and I’ve wanted it to be able to blink morse code messages, be the right colors, and pretend someone ate a power dot since I got it. 47 RGBW LEDs later and I had something that met my expectations.
I played around with the patterns quite a bit to add some character to the ghost, and found it really useful. I’ve included it in case anyone can find something they want to ask about or can use. It’s kinda messy and I feel like I’d do a better job rewriting it with what I know now, but it seems to work.
/* The eye layout prioritized wiring ease over regularity, and the following tables
are eye 'gestures' based on pixel indexes rather than position.
left eye |right eye
left down mid up right left up mid down right */
lookLeft = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0 ]
lookRight = [0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]
lookCenter = [0.25, 0.5, 1, 0.5, 0.25, 0.25, 0.5, 1, 0.5, 0.25 ]
lookUp = [0, 0, 0, 1, 0, 0, 1, 0, 0, 0 ]
lookDown = [0, 1, 0, 0, 0, 0, 0, 0, 1, 0 ]
halfBlink = [0.25, 0, 1, 0, 0.25, 0.25, 0, 1, 0, 0.25 ]
quarterBlink = [0, 0, 0.5, 0, 0, 0, 0, 0.5, 0, 0 ]
shut = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
/* Eye animations are a list of gestures and the time to take to transition to them. */
/* The first entry is a number to report current animation */
blinkSymbol = .8114
blink = [blinkSymbol,[halfBlink,0.001],[quarterBlink,0.001],[shut,0.001],[quarterBlink,0.001],[halfBlink,0.001],[lookCenter,0.001],[lookCenter,0.1]]
eyerollSymbol = 0.3731
eyeroll = [eyerollSymbol,[lookLeft,0.01],[lookUp,0.005],[lookRight,0.005],[lookDown,0.005],
[lookLeft,0.005],[lookUp,0.005],[lookRight,0.005],[lookDown,0.005],
[lookLeft,0.005],[lookCenter,0.005],[lookCenter,0.1]]
shiftySymbol = 0.5177
shifty = [shiftySymbol,[lookLeft,0.005],[lookLeft,0.025],[halfBlink,0.005],[lookRight,0.005],[lookRight,0.025],[lookCenter,0.005],[lookCenter,0.1]]
napSymbol = 0.4420
nap = [napSymbol,[halfBlink,0.1],[quarterBlink,0.001],[shut,0.001],[shut,0.001],
[quarterBlink,0.001],[halfBlink,0.001],[lookCenter,0.005],[halfBlink,0.005],[quarterBlink,0.1],[shut,0.001],[shut,0.01],
[quarterBlink,0.001],[shut,0.1],[shut,1],
[quarterBlink,0.001],[halfBlink,0.001],[lookCenter,0.001],
[lookCenter,0.01],[lookLeft,0.005],[lookLeft,0.05],[halfBlink,0.005],[lookRight,0.005],[lookRight,0.05],[lookCenter,0.005],[lookCenter,0.1],]
// Power Dot animations involve the ghost and the eyes so it's pretty irregular
powerDotSymbol = .2033
powerDot = [powerDotSymbol, [lookRight,0.05],[lookRight,0.05],[lookRight,0.045],[lookRight,0.045],[lookRight,0.04],[lookRight,0.04],
[lookRight,0.035],[lookRight,0.035],[lookRight,0.03],[lookRight,0.03],[lookRight,0.025],[lookRight,0.025],
[lookRight,0.02],[lookRight,0.02],[lookRight,0.015],[lookRight,0.015],[lookRight,0.01],[lookRight,0.01]]
// Go Home animations involve the ghost and the eyes so it's pretty irregular
goHomeSymbol = 0.4033
goHome = [goHomeSymbol, [lookRight,0.001],[lookRight,0.05],[lookUp,0.001],[lookUp,0.02],[lookLeft,0.001],[lookLeft,0.1],[lookUp,0.001],
[lookUp,0.05],[lookRight,0.001],[lookRight,0.02],[lookDown,0.001],[lookDown,0.02]]
/* Blinking morse code uses a fast blink, with the shut period scaled to the morseRate */
morseRate = 0.01
xRate = morseRate/10
ditSymbol = 0.217
dit = [ditSymbol,[halfBlink,xRate],[quarterBlink,xRate],[shut,xRate],[shut,morseRate-3*xRate],
[quarterBlink,xRate],[halfBlink,xRate],[lookCenter,xRate],[lookCenter,morseRate-3*xRate]]
dahSymbol = 0.444
dah = [dahSymbol,[halfBlink,xRate],[quarterBlink,xRate],[shut,xRate],[shut,3*morseRate-3*xRate],
[quarterBlink,xRate],[halfBlink,xRate],[lookCenter,xRate],[lookCenter,morseRate-3*xRate]]
// letter endings have a longer space
ltrEndSymbol = 0
ltrEnd = [ltrEndSymbol,[lookCenter,morseRate*2]]
_A = [dit,dah,ltrEnd]
_B = [dah,dit,dit,dit,ltrEnd]
_C = [dah,dit,dah,dit,ltrEnd]
_D = [dah,dit,dit,ltrEnd]
_E = [dit,ltrEnd]
_F = [dit,dit,dah,dit,ltrEnd]
_H = [dit,dit,dit,dit,ltrEnd]
_I = [dit,dit,ltrEnd]
_K = [dah,dit,dah,ltrEnd]
_L = [dit,dah,dit,dit,ltrEnd]
_M = [dah,dah,ltrEnd]
_N = [dah,dit,ltrEnd]
_O = [dah,dah,dah,ltrEnd]
_P = [dit,dah,dah,dit,ltrEnd]
_R = [dit,dah,dit,ltrEnd]
_S = [dit,dit,dit,ltrEnd]
_T = [dah,ltrEnd]
_U = [dit,dit,dah,ltrEnd]
_V = [dit,dit,dit,dah,ltrEnd]
_W = [dit,dah,dah,ltrEnd]
_X = [dah,dit,dit,dah,ltrEnd]
_Y = [dah,dit,dah,dah,ltrEnd]
_Z = [dah,dah,dit,dit,ltrEnd]
wrdEnd = [ltrEnd,ltrEnd]
// Track current action for eye animation
export var actionIdx = 1
var action = dah
from = lookCenter
to = lookCenter
rate = 0.1
var morseWord = HI
var wordIdx = 0
var morseWordLength = morseWord.length
var morseLetter = _A
var actionLength = wrdEnd.length
function chooseNewMorseLetter(){
morseIdx = 0
wordIdx ++
if(wordIdx>=morseWord.length){
morseLetter = wrdEnd
} else {
morseLetter = morseWord[wordIdx]
}
}
// Morse words
HI = [wrdEnd,_H,_I,wrdEnd]
HOWSYOURMORSECODE = [wrdEnd,_I,wrdEnd,_S,_P,_E,_A,_K,wrdEnd,_M,_O,_R,_S,_E,wrdEnd,_C,_O,_D,_E,wrdEnd,_S,_T,_O,_P,wrdEnd]
HAPPYBIRTHDAY = [wrdEnd,_H,_A,_P,_P,_Y,wrdEnd,_B,_I,_R,_T,_H,_D,_A,_Y,wrdEnd]
HAPPYFATHERSDAY = [wrdEnd,_H,_A,_P,_P,_Y,wrdEnd,_F,_A,_T,_H,_E,_R,_S,wrdEnd,_D,_A,_Y,wrdEnd]
HAPPYMOTHERSDAY = [wrdEnd,_H,_A,_P,_P,_Y,wrdEnd,_M,_O,_T,_H,_E,_R,_S,wrdEnd,_D,_A,_Y,wrdEnd]
HELPIMTRAPPEDINAMAZE = [wrdEnd,_H,_E,_L,_P,wrdEnd,_I,_M,wrdEnd,_T,_R,_A,_P,_P,_E,_D,wrdEnd,_I,_N,wrdEnd,_A,wrdEnd,_M,_A,_Z,_E,wrdEnd]
FORDADLOVEDREW = [wrdEnd,_F,_O,_R,wrdEnd,_D,_A,_D,wrdEnd,_L,_O,_V,_E,wrdEnd,_D,_R,_E,_W,wrdEnd]
BUILTWITHPIXELBLAZE = [wrdEnd,_B,_U,_I,_L,_T,wrdEnd,_W,_I,_T,_H,wrdEnd,_P,_I,_X,_E,_L,_B,_L,_A,_Z,_E,wrdEnd]
function chooseNewAnimation(){
morseWordLength = morseWord.length
if(wordIdx >= morseWord.length) {
/* Advance to random eye animation */
actionIdx = 1
chance = random(1)
if(chance<0.05){ // Start to signal a word of morse code
if ((clockMonth()==6) && (clockWeekday()==1) && (clockDay()>=15) && (clockDay()<=22)) {
morseWord = HAPPYFATHERSDAY
} if ((clockMonth()==5) && (clockWeekday()==1) && (clockDay()>=8) && (clockDay()<=13)) {
morseWord = HAPPYMOTHERSDAY
} else {
chance = random(1)
if(chance<0.2){
morseWord = HOWSYOURMORSECODE
} else if (chance<0.4) {
morseWord = HELPIMTRAPPEDINAMAZE
} else if (chance<0.6) {
morseWord = FORDADLOVEDREW
} else if (chance<0.8) {
morseWord = BUILTWITHPIXELBLAZE
} else {
morseWord = HI
}
}
wordIdx = 0
morseLetter = morseWord[0]
morseIdx = 0
action = morseLetter[0]
} else if (chance<0.08) {
action = powerDot
} else if(chance<0.12){
action = nap
} else if (chance<0.30) {
action = shifty
} else if(chance<0.50){
action = eyeroll
} else if(chance<0.57){
action = dit
} else if(chance<0.62){
action = dah
} else {
action = blink
}
} else { //continue ongoing morse word
morseIdx++
if(morseIdx>=morseLetter.length){
chooseNewMorseLetter()
}
action = morseLetter[morseIdx]
}
}
function advanceAnimation(){
from = to
actionIdx = actionIdx + 1
actionLength = action.length
if(actionIdx>=action.length){
chooseNewAnimation()
}
actionLength = action.length
if(action.length<=actionIdx){actionIdx=1}
entry = action[actionIdx]
to = entry[0]
rate = entry[1]
// Special case to catch power dot
if(action == powerDot && random(1)>0.96){triggerGoHome()}
}
var t1
var maxvar = 32767
var hour
export function beforeRender(delta) {
tCont = time(.5)
actionTime += delta/rate
if (actionTime < 0) {
advanceAnimation()
actionTime = 0
}
t1 = actionTime/maxvar
computeBrightness()
}
function eyes(index) {
return index<10
}
//convert HSV to RGB
//output sets r,g,b globals
function hsv2rgb(hh, ss, vv) {
var h = mod(hh, 1)
var s = clamp(ss, 0, 1)
var v = clamp(vv, 0, 1)
var i = floor(h * 6)
var f = h * 6 - i
var p = v * (1 - s)
var q = v * (1 - (s * f))
var t = v * (1 - (s * (1 - f)))
if (i == 0) {
r = v; g = t; b = p
} else if (i == 1) {
r = q; g = v; b = p
} else if (i == 2) {
r = p; g = v; b = t
} else if (i == 3) {
r = p; g = q; b = v
} else if (i == 4) {
r = t; g = p; b = v
} else if (i == 5) {
r = v; g = p; b = q
}
}
var rSel = 1
var gSel = 0
var bSel = 0
export function rgbPickerColor(r, g, b){
rSel = r
gSel = g
bSel = b
}
// Debugging buttons to test animations
export function triggerBlink(){
action = blink
actionIdx = 0
rate=0.01
}
export function triggerShifty(){
action = shifty
actionIdx = 0
rate=0.01
}
export function triggerNap(){
action = nap
actionIdx = 0
rate=0.01
}
export function triggerEyeRoll(){
action = eyeroll
actionIdx = 0
rate=0.01
}
export function triggerDit(){
action = dit
actionIdx = 0
rate=0.01
}
export function triggerDah(){
action = dah
actionIdx = 0
rate=0.01
}
export function triggerPowerDot(){
action = powerDot
actionIdx = 0
rate=0.01
}
export function triggerGoHome(){
action = goHome
actionIdx = 0
rate = 0.01
chance = random(1)
if(chance>0.75){pink()}
else if (chance>0.5){lightBlue()}
else if (chance>0.25){red()}
else {yellow()}
rSel = r; gSel = g; bSel = b
}
export function inputNumberMorseRate(v){
morseRate = v
}
var brightnessOverride = 0
export function toggleFullBrightness(on){
brightnessOverride = on
}
export function showNumberAction(){return action[0]}
export function showNumberBrightness(){return brightness}
second = -1
function computeBrightness() {
if (second != clockSecond()) {
if (brightnessOverride) {
brightness = 1
} else {
second = clockSecond()
nightBrightness = 0.05
hour = clockHour() + clockMinute()/60 + second/3600
brightness = nightBrightness + clamp((1-nightBrightness)*(8-abs(12-hour))/4,0,1-nightBrightness)
}
}
}
/* Scaled RGB applies a brightness effect to reduce brightness at night */
function sRGB(r,g,b) {
rgb(r*brightness,g*brightness,b*brightness)
}
function white() {
r = g = b = 1
}
function blue() {
r = g = 0 ; b = .75
}
function lightBlue() {
r = 0; g = 1; b = 0.8
}
function yellow() {
r = 1 ; g = 0.7 ; b = 0
}
function green() {
r = b = 0 ; g = 1
}
function red() {
b = g = 0 ; r = 1
}
function pink() {
r = 1 ; g = 0.6 ; b = 0.75
}
export function render3D(index,x,y,z) {
if (action==powerDot) {
if (actionIdx % 2) {
blue()
} else {
white()
}
} else if (action == goHome){
} else if (clockMonth() == 6){
hsv2rgb(x/6+tCont+y/4,1,1)
} else if ((clockMonth()==2) && clockDay() == 14) {
pink()
} else if (((clockMonth()==12) && (clockDay()>=24)) || ((clockMonth()==1) && (clockDay()<=6))){
t = (x/10+y/10+tCont)%1
if (t<0.25) {red()}
else if (t<0.5) {white()}
else if (t<0.75) {green()}
else {white()}
} else if ((clockMonth()==7) && (clockDay()==4)){
t = (x/10+y/5+tCont)%1
if (t<0.25) {red()}
else if (t<0.5) {white()}
else if (t<0.75) {blue()}
else {white()}
} else {
r = rSel ; g = gSel ; b = bSel
}
// Apply a lazy gamma effect
g = g*g*g
b = b*b*b
if (eyes(index)) {
if (action == powerDot){
if(actionIdx%2) {
v = to[index]
sRGB(v+max(0,r/4-v),v+max(0,g/4-v),v+max(0,b/4-v))
}
else {
v = to[index]
sRGB(v+max(0,r/4-v),max(0,g/4-v),max(0,b/4-v))
}
} else {
v = mix(from[index],to[index],t1)
// a shut eye should be the background color not black
// the eye LEDs are about half as far from the plane so we reduce their brightness
sRGB(v+max(0,r/4-v),v+max(0,g/4-v),v+max(0,b/4-v))
}
}
else
{
// The bottom walks if we're not napping or signalling a message
if(y>0.75 && action!=nap && action!=dit && action!=dah && action!= ltrEnd){
r*=wave(index/8+150*tCont)
g*=wave(index/8+150*tCont)
b*=wave(index/8+150*tCont)
}
sRGB(r,g,b)
}
}