diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,22 @@
-# Revision history for reani
+# Revision history for reanimate
 
+## 0.1.6.0 -- 2019-09-14
+
+* Test suite.
+* Improved Windows support.
+* Automated builds on Linux, Mac, and Windows.
+* Automated builds with lts-13, lts-12, and lts-11.
+* Monadic language for composing animations (Reanimate.Scene).
+* Support for Povray.
+* Viewer: Incrementally generate videos with increasing fps.
+* 'check' command to verify external dependencies.
+* Built-in data for color theory (cone sensitivity etc).
+* Built-in colormaps.
+* Raster image support.
+* Reorganized signals.
+* Color interpolation (rgb, hsv, lab, xyz).
+* Improved performance of multi-threaded renderer.
+
 ## 0.1.5.0 -- 2019-07-07
 
 * Basic driver for live previewing and rendering.
@@ -7,6 +24,6 @@
   transformations, etc).
 * Automated CI builds (azure).
 
-## 0.1.0.0 -- YYYY-mm-dd
+## 0.1.0.0 -- 2019-03-08
 
 * First version. Released on an unsuspecting world.
diff --git a/data/CIExyz.csv b/data/CIExyz.csv
new file mode 100644
--- /dev/null
+++ b/data/CIExyz.csv
@@ -0,0 +1,441 @@
+390,0.16638,0.01830,0.81532
+391,0.16635,0.01846,0.81519
+392,0.16629,0.01858,0.81513
+393,0.16620,0.01867,0.81513
+394,0.16609,0.01872,0.81519
+395,0.16595,0.01874,0.81531
+396,0.16579,0.01872,0.81548
+397,0.16561,0.01867,0.81572
+398,0.16542,0.01857,0.81601
+399,0.16521,0.01844,0.81635
+400,0.16499,0.01827,0.81673
+401,0.16477,0.01807,0.81716
+402,0.16455,0.01784,0.81761
+403,0.16433,0.01761,0.81806
+404,0.16412,0.01738,0.81849
+405,0.16393,0.01718,0.81888
+406,0.16376,0.01702,0.81922
+407,0.16359,0.01688,0.81953
+408,0.16341,0.01676,0.81983
+409,0.16320,0.01664,0.82015
+410,0.16296,0.01653,0.82052
+411,0.16266,0.01640,0.82094
+412,0.16233,0.01627,0.82140
+413,0.16198,0.01614,0.82188
+414,0.16162,0.01603,0.82235
+415,0.16126,0.01594,0.82280
+416,0.16093,0.01587,0.82320
+417,0.16060,0.01583,0.82357
+418,0.16028,0.01582,0.82390
+419,0.15994,0.01584,0.82422
+420,0.15958,0.01589,0.82453
+421,0.15920,0.01597,0.82483
+422,0.15879,0.01608,0.82513
+423,0.15836,0.01622,0.82542
+424,0.15793,0.01637,0.82570
+425,0.15750,0.01653,0.82596
+426,0.15709,0.01671,0.82620
+427,0.15669,0.01690,0.82641
+428,0.15628,0.01712,0.82660
+429,0.15586,0.01738,0.82677
+430,0.15540,0.01767,0.82692
+431,0.15491,0.01802,0.82706
+432,0.15439,0.01841,0.82720
+433,0.15384,0.01883,0.82733
+434,0.15329,0.01926,0.82745
+435,0.15276,0.01968,0.82756
+436,0.15225,0.02008,0.82767
+437,0.15178,0.02047,0.82775
+438,0.15131,0.02086,0.82782
+439,0.15085,0.02128,0.82787
+440,0.15036,0.02173,0.82790
+441,0.14985,0.02225,0.82791
+442,0.14929,0.02282,0.82789
+443,0.14871,0.02343,0.82785
+444,0.14811,0.02409,0.82780
+445,0.14749,0.02478,0.82773
+446,0.14687,0.02550,0.82763
+447,0.14624,0.02625,0.82751
+448,0.14560,0.02706,0.82734
+449,0.14493,0.02795,0.82712
+450,0.14423,0.02895,0.82682
+451,0.14349,0.03007,0.82643
+452,0.14271,0.03133,0.82597
+453,0.14186,0.03271,0.82543
+454,0.14095,0.03422,0.82483
+455,0.13997,0.03584,0.82419
+456,0.13890,0.03759,0.82351
+457,0.13775,0.03945,0.82279
+458,0.13653,0.04145,0.82202
+459,0.13525,0.04359,0.82116
+460,0.13392,0.04588,0.82020
+461,0.13254,0.04834,0.81912
+462,0.13112,0.05098,0.81790
+463,0.12964,0.05384,0.81652
+464,0.12807,0.05695,0.81498
+465,0.12638,0.06036,0.81326
+466,0.12456,0.06410,0.81134
+467,0.12259,0.06822,0.80919
+468,0.12046,0.07275,0.80678
+469,0.11818,0.07773,0.80409
+470,0.11574,0.08320,0.80106
+471,0.11313,0.08922,0.79766
+472,0.11034,0.09582,0.79384
+473,0.10736,0.10307,0.78956
+474,0.10418,0.11103,0.78479
+475,0.10078,0.11975,0.77947
+476,0.09715,0.12930,0.77354
+477,0.09329,0.13971,0.76700
+478,0.08923,0.15097,0.75980
+479,0.08497,0.16308,0.75195
+480,0.08055,0.17601,0.74344
+481,0.07599,0.18973,0.73427
+482,0.07130,0.20429,0.72441
+483,0.06649,0.21974,0.71378
+484,0.06155,0.23615,0.70230
+485,0.05651,0.25359,0.68990
+486,0.05138,0.27212,0.67650
+487,0.04622,0.29169,0.66209
+488,0.04108,0.31224,0.64668
+489,0.03604,0.33365,0.63031
+490,0.03117,0.35580,0.61304
+491,0.02654,0.37853,0.59493
+492,0.02222,0.40175,0.57604
+493,0.01827,0.42531,0.55642
+494,0.01477,0.44909,0.53614
+495,0.01178,0.47294,0.51529
+496,0.00933,0.49673,0.49393
+497,0.00741,0.52049,0.47210
+498,0.00596,0.54424,0.44980
+499,0.00490,0.56805,0.42705
+500,0.00418,0.59194,0.40387
+501,0.00374,0.61593,0.38033
+502,0.00364,0.63984,0.35652
+503,0.00393,0.66346,0.33260
+504,0.00471,0.68656,0.30872
+505,0.00605,0.70890,0.28505
+506,0.00802,0.73018,0.26180
+507,0.01073,0.74993,0.23934
+508,0.01424,0.76771,0.21805
+509,0.01860,0.78319,0.19821
+510,0.02382,0.79618,0.18001
+511,0.02983,0.80664,0.16352
+512,0.03645,0.81498,0.14857
+513,0.04345,0.82161,0.13495
+514,0.05062,0.82692,0.12246
+515,0.05780,0.83125,0.11095
+516,0.06485,0.83484,0.10031
+517,0.07183,0.83765,0.09052
+518,0.07882,0.83959,0.08158
+519,0.08593,0.84060,0.07347
+520,0.09322,0.84063,0.06615
+521,0.10076,0.83968,0.05957
+522,0.10849,0.83786,0.05365
+523,0.11636,0.83533,0.04831
+524,0.12431,0.83221,0.04349
+525,0.13227,0.82861,0.03912
+526,0.14020,0.82463,0.03517
+527,0.14806,0.82034,0.03160
+528,0.15582,0.81580,0.02838
+529,0.16344,0.81106,0.02550
+530,0.17090,0.80617,0.02292
+531,0.17820,0.80119,0.02062
+532,0.18536,0.79609,0.01855
+533,0.19247,0.79084,0.01668
+534,0.19958,0.78542,0.01500
+535,0.20674,0.77980,0.01346
+536,0.21399,0.77394,0.01207
+537,0.22130,0.76790,0.01080
+538,0.22862,0.76172,0.00966
+539,0.23592,0.75544,0.00864
+540,0.24316,0.74912,0.00773
+541,0.25030,0.74278,0.00692
+542,0.25736,0.73644,0.00620
+543,0.26434,0.73010,0.00556
+544,0.27125,0.72376,0.00499
+545,0.27809,0.71742,0.00448
+546,0.28489,0.71108,0.00403
+547,0.29164,0.70474,0.00362
+548,0.29834,0.69841,0.00326
+549,0.30499,0.69208,0.00293
+550,0.31161,0.68576,0.00263
+551,0.31821,0.67944,0.00236
+552,0.32481,0.67307,0.00211
+553,0.33148,0.66663,0.00189
+554,0.33825,0.66006,0.00170
+555,0.34516,0.65332,0.00152
+556,0.35222,0.64642,0.00136
+557,0.35937,0.63941,0.00123
+558,0.36653,0.63237,0.00110
+559,0.37363,0.62538,0.00099
+560,0.38061,0.61851,0.00089
+561,0.38743,0.61177,0.00080
+562,0.39416,0.60512,0.00072
+563,0.40087,0.59849,0.00064
+564,0.40763,0.59179,0.00058
+565,0.41450,0.58498,0.00052
+566,0.42152,0.57801,0.00047
+567,0.42864,0.57094,0.00042
+568,0.43579,0.56383,0.00038
+569,0.44293,0.55672,0.00034
+570,0.45001,0.54968,0.00031
+571,0.45698,0.54274,0.00028
+572,0.46388,0.53587,0.00025
+573,0.47071,0.52906,0.00023
+574,0.47751,0.52229,0.00020
+575,0.48429,0.51553,0.00019
+576,0.49105,0.50878,0.00017
+577,0.49782,0.50203,0.00015
+578,0.50458,0.49529,0.00014
+579,0.51133,0.48854,0.00013
+580,0.51808,0.48181,0.00011
+581,0.52481,0.47509,0.00010
+582,0.53145,0.46846,0.00009
+583,0.53795,0.46197,0.00008
+584,0.54425,0.45567,0.00008
+585,0.55031,0.44962,0.00007
+586,0.55610,0.44384,0.00006
+587,0.56167,0.43828,0.00006
+588,0.56707,0.43288,0.00005
+589,0.57236,0.42759,0.00005
+590,0.57757,0.42238,0.00004
+591,0.58273,0.41723,0.00004
+592,0.58783,0.41214,0.00004
+593,0.59283,0.40714,0.00003
+594,0.59772,0.40225,0.00003
+595,0.60249,0.39748,0.00003
+596,0.60712,0.39285,0.00003
+597,0.61163,0.38834,0.00002
+598,0.61604,0.38394,0.00002
+599,0.62034,0.37963,0.00002
+600,0.62457,0.37541,0.00002
+601,0.62871,0.37127,0.00002
+602,0.63276,0.36723,0.00002
+603,0.63668,0.36330,0.00002
+604,0.64046,0.35952,0.00001
+605,0.64409,0.35589,0.00001
+606,0.64756,0.35243,0.00001
+607,0.65088,0.34911,0.00001
+608,0.65405,0.34594,0.00001
+609,0.65708,0.34291,0.00001
+610,0.66000,0.33999,0.00001
+611,0.66280,0.33719,0.00001
+612,0.66548,0.33451,0.00001
+613,0.66807,0.33193,0.00001
+614,0.67055,0.32945,0.00001
+615,0.67293,0.32706,0.00001
+616,0.67523,0.32477,0.00000
+617,0.67744,0.32256,0.00000
+618,0.67958,0.32042,0.00000
+619,0.68166,0.31834,0.00000
+620,0.68369,0.31631,0.00000
+621,0.68567,0.31433,0.00000
+622,0.68758,0.31242,0.00000
+623,0.68940,0.31060,0.00000
+624,0.69111,0.30889,0.00000
+625,0.69269,0.30731,0.00000
+626,0.69415,0.30585,0.00000
+627,0.69549,0.30451,0.00000
+628,0.69675,0.30325,0.00000
+629,0.69793,0.30207,0.00000
+630,0.69907,0.30093,0.00000
+631,0.70017,0.29983,0.00000
+632,0.70124,0.29876,0.00000
+633,0.70228,0.29772,0.00000
+634,0.70329,0.29671,0.00000
+635,0.70426,0.29574,0.00000
+636,0.70520,0.29480,0.00000
+637,0.70612,0.29388,0.00000
+638,0.70703,0.29297,0.00000
+639,0.70795,0.29205,0.00000
+640,0.70887,0.29113,0.00000
+641,0.70980,0.29020,0.00000
+642,0.71071,0.28929,0.00000
+643,0.71157,0.28843,0.00000
+644,0.71236,0.28764,0.00000
+645,0.71304,0.28696,0.00000
+646,0.71361,0.28639,0.00000
+647,0.71410,0.28590,0.00000
+648,0.71452,0.28548,0.00000
+649,0.71490,0.28510,0.00000
+650,0.71528,0.28472,0.00000
+651,0.71566,0.28434,0.00000
+652,0.71605,0.28395,0.00000
+653,0.71645,0.28355,0.00000
+654,0.71685,0.28315,0.00000
+655,0.71725,0.28275,0.00000
+656,0.71764,0.28236,0.00000
+657,0.71802,0.28198,0.00000
+658,0.71840,0.28160,0.00000
+659,0.71876,0.28124,0.00000
+660,0.71912,0.28088,0.00000
+661,0.71946,0.28054,0.00000
+662,0.71978,0.28022,0.00000
+663,0.72009,0.27991,0.00000
+664,0.72037,0.27963,0.00000
+665,0.72062,0.27938,0.00000
+666,0.72084,0.27916,0.00000
+667,0.72104,0.27896,0.00000
+668,0.72122,0.27878,0.00000
+669,0.72139,0.27861,0.00000
+670,0.72154,0.27846,0.00000
+671,0.72169,0.27831,0.00000
+672,0.72182,0.27818,0.00000
+673,0.72195,0.27805,0.00000
+674,0.72208,0.27792,0.00000
+675,0.72219,0.27781,0.00000
+676,0.72230,0.27770,0.00000
+677,0.72239,0.27761,0.00000
+678,0.72248,0.27752,0.00000
+679,0.72257,0.27743,0.00000
+680,0.72265,0.27735,0.00000
+681,0.72272,0.27728,0.00000
+682,0.72279,0.27721,0.00000
+683,0.72285,0.27715,0.00000
+684,0.72291,0.27709,0.00000
+685,0.72296,0.27704,0.00000
+686,0.72300,0.27700,0.00000
+687,0.72304,0.27696,0.00000
+688,0.72308,0.27692,0.00000
+689,0.72311,0.27689,0.00000
+690,0.72314,0.27686,0.00000
+691,0.72317,0.27683,0.00000
+692,0.72320,0.27680,0.00000
+693,0.72323,0.27677,0.00000
+694,0.72325,0.27675,0.00000
+695,0.72327,0.27673,0.00000
+696,0.72328,0.27672,0.00000
+697,0.72329,0.27671,0.00000
+698,0.72329,0.27671,0.00000
+699,0.72329,0.27671,0.00000
+700,0.72329,0.27671,0.00000
+701,0.72329,0.27671,0.00000
+702,0.72329,0.27671,0.00000
+703,0.72329,0.27671,0.00000
+704,0.72329,0.27671,0.00000
+705,0.72329,0.27671,0.00000
+706,0.72329,0.27671,0.00000
+707,0.72328,0.27672,0.00000
+708,0.72327,0.27673,0.00000
+709,0.72325,0.27675,0.00000
+710,0.72323,0.27677,0.00000
+711,0.72320,0.27680,0.00000
+712,0.72318,0.27682,0.00000
+713,0.72314,0.27686,0.00000
+714,0.72311,0.27689,0.00000
+715,0.72308,0.27692,0.00000
+716,0.72305,0.27695,0.00000
+717,0.72301,0.27699,0.00000
+718,0.72298,0.27702,0.00000
+719,0.72295,0.27705,0.00000
+720,0.72292,0.27708,0.00000
+721,0.72288,0.27712,0.00000
+722,0.72284,0.27716,0.00000
+723,0.72280,0.27720,0.00000
+724,0.72276,0.27724,0.00000
+725,0.72272,0.27728,0.00000
+726,0.72268,0.27732,0.00000
+727,0.72264,0.27736,0.00000
+728,0.72259,0.27741,0.00000
+729,0.72255,0.27745,0.00000
+730,0.72251,0.27749,0.00000
+731,0.72246,0.27754,0.00000
+732,0.72242,0.27758,0.00000
+733,0.72237,0.27763,0.00000
+734,0.72233,0.27767,0.00000
+735,0.72228,0.27772,0.00000
+736,0.72222,0.27778,0.00000
+737,0.72217,0.27783,0.00000
+738,0.72211,0.27789,0.00000
+739,0.72204,0.27796,0.00000
+740,0.72198,0.27802,0.00000
+741,0.72191,0.27809,0.00000
+742,0.72184,0.27816,0.00000
+743,0.72177,0.27823,0.00000
+744,0.72169,0.27831,0.00000
+745,0.72162,0.27838,0.00000
+746,0.72155,0.27845,0.00000
+747,0.72148,0.27852,0.00000
+748,0.72141,0.27859,0.00000
+749,0.72134,0.27866,0.00000
+750,0.72127,0.27873,0.00000
+751,0.72120,0.27880,0.00000
+752,0.72112,0.27888,0.00000
+753,0.72105,0.27895,0.00000
+754,0.72098,0.27902,0.00000
+755,0.72091,0.27909,0.00000
+756,0.72083,0.27917,0.00000
+757,0.72075,0.27925,0.00000
+758,0.72068,0.27932,0.00000
+759,0.72060,0.27940,0.00000
+760,0.72052,0.27948,0.00000
+761,0.72044,0.27956,0.00000
+762,0.72037,0.27963,0.00000
+763,0.72030,0.27970,0.00000
+764,0.72022,0.27978,0.00000
+765,0.72015,0.27985,0.00000
+766,0.72008,0.27992,0.00000
+767,0.72001,0.27999,0.00000
+768,0.71994,0.28006,0.00000
+769,0.71987,0.28013,0.00000
+770,0.71980,0.28020,0.00000
+771,0.71973,0.28027,0.00000
+772,0.71966,0.28034,0.00000
+773,0.71958,0.28042,0.00000
+774,0.71951,0.28049,0.00000
+775,0.71943,0.28057,0.00000
+776,0.71936,0.28064,0.00000
+777,0.71928,0.28072,0.00000
+778,0.71920,0.28080,0.00000
+779,0.71912,0.28088,0.00000
+780,0.71904,0.28096,0.00000
+781,0.71895,0.28105,0.00000
+782,0.71887,0.28113,0.00000
+783,0.71879,0.28121,0.00000
+784,0.71870,0.28130,0.00000
+785,0.71862,0.28138,0.00000
+786,0.71853,0.28147,0.00000
+787,0.71845,0.28155,0.00000
+788,0.71836,0.28164,0.00000
+789,0.71828,0.28172,0.00000
+790,0.71819,0.28181,0.00000
+791,0.71810,0.28190,0.00000
+792,0.71802,0.28198,0.00000
+793,0.71793,0.28207,0.00000
+794,0.71783,0.28217,0.00000
+795,0.71774,0.28226,0.00000
+796,0.71764,0.28236,0.00000
+797,0.71753,0.28247,0.00000
+798,0.71743,0.28257,0.00000
+799,0.71732,0.28268,0.00000
+800,0.71721,0.28279,0.00000
+801,0.71710,0.28290,0.00000
+802,0.71699,0.28301,0.00000
+803,0.71688,0.28312,0.00000
+804,0.71677,0.28323,0.00000
+805,0.71666,0.28334,0.00000
+806,0.71655,0.28345,0.00000
+807,0.71644,0.28356,0.00000
+808,0.71633,0.28367,0.00000
+809,0.71622,0.28378,0.00000
+810,0.71611,0.28389,0.00000
+811,0.71599,0.28401,0.00000
+812,0.71588,0.28412,0.00000
+813,0.71577,0.28423,0.00000
+814,0.71566,0.28434,0.00000
+815,0.71555,0.28445,0.00000
+816,0.71544,0.28456,0.00000
+817,0.71533,0.28467,0.00000
+818,0.71522,0.28478,0.00000
+819,0.71512,0.28488,0.00000
+820,0.71502,0.28498,0.00000
+821,0.71492,0.28508,0.00000
+822,0.71482,0.28518,0.00000
+823,0.71473,0.28527,0.00000
+824,0.71464,0.28536,0.00000
+825,0.71455,0.28545,0.00000
+826,0.71447,0.28553,0.00000
+827,0.71439,0.28561,0.00000
+828,0.71431,0.28569,0.00000
+829,0.71424,0.28576,0.00000
+830,0.71417,0.28583,0.00000
diff --git a/data/cone_sensitivity_lms.csv b/data/cone_sensitivity_lms.csv
new file mode 100644
--- /dev/null
+++ b/data/cone_sensitivity_lms.csv
@@ -0,0 +1,441 @@
+390,  4.15003E-04,  3.68349E-04,  9.54729E-03
+391,  5.02650E-04,  4.48015E-04,  1.14794E-02
+392,  6.07367E-04,  5.43965E-04,  1.37986E-02
+393,  7.31850E-04,  6.58983E-04,  1.65746E-02
+394,  8.79012E-04,  7.96121E-04,  1.98869E-02
+395,  1.05192E-03,  9.58658E-04,  2.38250E-02
+396,  1.25373E-03,  1.15002E-03,  2.84877E-02
+397,  1.48756E-03,  1.37367E-03,  3.39832E-02
+398,  1.75633E-03,  1.63296E-03,  4.04274E-02
+399,  2.06261E-03,  1.93089E-03,  4.79417E-02
+400,  2.40836E-03,  2.26991E-03,  5.66498E-02
+401,  2.79522E-03,  2.65210E-03,  6.66757E-02
+402,  3.22640E-03,  3.08110E-03,  7.81479E-02
+403,  3.70617E-03,  3.56156E-03,  9.11925E-02
+404,  4.23972E-03,  4.09900E-03,  1.05926E-01
+405,  4.83339E-03,  4.70010E-03,  1.22451E-01
+406,  5.49335E-03,  5.37186E-03,  1.40844E-01
+407,  6.21933E-03,  6.11757E-03,  1.61140E-01
+408,  7.00631E-03,  6.93795E-03,  1.83325E-01
+409,  7.84503E-03,  7.83144E-03,  2.07327E-01
+410,  8.72127E-03,  8.79369E-03,  2.33008E-01
+411,  9.61879E-03,  9.81865E-03,  2.60183E-01
+412,  1.05324E-02,  1.09044E-02,  2.88723E-01
+413,  1.14620E-02,  1.20509E-02,  3.18512E-01
+414,  1.24105E-02,  1.32582E-02,  3.49431E-01
+415,  1.33837E-02,  1.45277E-02,  3.81363E-01
+416,  1.43870E-02,  1.58600E-02,  4.14141E-01
+417,  1.54116E-02,  1.72496E-02,  4.47350E-01
+418,  1.64424E-02,  1.86878E-02,  4.80439E-01
+419,  1.74614E-02,  2.01638E-02,  5.12767E-01
+420,  1.84480E-02,  2.16649E-02,  5.43618E-01
+421,  1.93852E-02,  2.31801E-02,  5.72399E-01
+422,  2.02811E-02,  2.47139E-02,  5.99284E-01
+423,  2.11545E-02,  2.62785E-02,  6.24786E-01
+424,  2.20286E-02,  2.78905E-02,  6.49576E-01
+425,  2.29317E-02,  2.95714E-02,  6.74474E-01
+426,  2.38896E-02,  3.13438E-02,  7.00186E-01
+427,  2.49026E-02,  3.32151E-02,  7.26460E-01
+428,  2.59631E-02,  3.51889E-02,  7.52726E-01
+429,  2.70619E-02,  3.72683E-02,  7.78333E-01
+430,  2.81877E-02,  3.94566E-02,  8.02555E-01
+431,  2.93303E-02,  4.17546E-02,  8.24818E-01
+432,  3.04898E-02,  4.41544E-02,  8.45422E-01
+433,  3.16694E-02,  4.66432E-02,  8.64961E-01
+434,  3.28731E-02,  4.92050E-02,  8.84100E-01
+435,  3.41054E-02,  5.18199E-02,  9.03573E-01
+436,  3.53670E-02,  5.44645E-02,  9.23844E-01
+437,  3.66400E-02,  5.71131E-02,  9.44055E-01
+438,  3.78988E-02,  5.97369E-02,  9.62920E-01
+439,  3.91148E-02,  6.23038E-02,  9.79057E-01
+440,  4.02563E-02,  6.47782E-02,  9.91020E-01
+441,  4.12989E-02,  6.71329E-02,  9.97765E-01
+442,  4.22582E-02,  6.93844E-02,  9.99982E-01
+443,  4.31627E-02,  7.15658E-02,  9.98861E-01
+444,  4.40444E-02,  7.37164E-02,  9.95628E-01
+445,  4.49380E-02,  7.58812E-02,  9.91515E-01
+446,  4.58729E-02,  7.80980E-02,  9.87377E-01
+447,  4.68459E-02,  8.03538E-02,  9.82619E-01
+448,  4.78446E-02,  8.26198E-02,  9.76301E-01
+449,  4.88555E-02,  8.48644E-02,  9.67513E-01
+450,  4.98639E-02,  8.70524E-02,  9.55393E-01
+451,  5.08618E-02,  8.91640E-02,  9.39499E-01
+452,  5.18731E-02,  9.12523E-02,  9.20807E-01
+453,  5.29317E-02,  9.33950E-02,  9.00592E-01
+454,  5.40746E-02,  9.56775E-02,  8.80040E-01
+455,  5.53418E-02,  9.81934E-02,  8.60240E-01
+456,  5.67734E-02,  1.01031E-01,  8.42031E-01
+457,  5.83973E-02,  1.04229E-01,  8.25572E-01
+458,  6.02409E-02,  1.07814E-01,  8.10860E-01
+459,  6.23353E-02,  1.11817E-01,  7.97902E-01
+460,  6.47164E-02,  1.16272E-01,  7.86704E-01
+461,  6.74131E-02,  1.21204E-01,  7.77119E-01
+462,  7.04040E-02,  1.26573E-01,  7.68365E-01
+463,  7.36489E-02,  1.32311E-01,  7.59538E-01
+464,  7.70978E-02,  1.38334E-01,  7.49777E-01
+465,  8.06894E-02,  1.44541E-01,  7.38268E-01
+466,  8.43613E-02,  1.50828E-01,  7.24389E-01
+467,  8.80904E-02,  1.57146E-01,  7.08113E-01
+468,  9.18630E-02,  1.63457E-01,  6.89572E-01
+469,  9.56637E-02,  1.69721E-01,  6.68927E-01
+470,  9.94755E-02,  1.75893E-01,  6.46359E-01
+471,  1.03286E-01,  1.81938E-01,  6.22112E-01
+472,  1.07103E-01,  1.87872E-01,  5.96591E-01
+473,  1.10947E-01,  1.93731E-01,  5.70216E-01
+474,  1.14838E-01,  1.99557E-01,  5.43373E-01
+475,  1.18802E-01,  2.05398E-01,  5.16411E-01
+476,  1.22863E-01,  2.11302E-01,  4.89647E-01
+477,  1.27026E-01,  2.17283E-01,  4.63406E-01
+478,  1.31293E-01,  2.23347E-01,  4.37965E-01
+479,  1.35666E-01,  2.29501E-01,  4.13549E-01
+480,  1.40145E-01,  2.35754E-01,  3.90333E-01
+481,  1.44731E-01,  2.42108E-01,  3.68394E-01
+482,  1.49415E-01,  2.48545E-01,  3.47582E-01
+483,  1.54189E-01,  2.55037E-01,  3.27724E-01
+484,  1.59038E-01,  2.61554E-01,  3.08676E-01
+485,  1.63952E-01,  2.68063E-01,  2.90322E-01
+486,  1.68932E-01,  2.74561E-01,  2.72626E-01
+487,  1.74063E-01,  2.81180E-01,  2.55772E-01
+488,  1.79457E-01,  2.88097E-01,  2.39945E-01
+489,  1.85241E-01,  2.95508E-01,  2.25280E-01
+490,  1.91556E-01,  3.03630E-01,  2.11867E-01
+491,  1.98532E-01,  3.12649E-01,  1.99719E-01
+492,  2.06182E-01,  3.22565E-01,  1.88674E-01
+493,  2.14485E-01,  3.33319E-01,  1.78557E-01
+494,  2.23411E-01,  3.44844E-01,  1.69217E-01
+495,  2.32926E-01,  3.57061E-01,  1.60526E-01
+496,  2.42992E-01,  3.69895E-01,  1.52368E-01
+497,  2.53616E-01,  3.83355E-01,  1.44620E-01
+498,  2.64810E-01,  3.97467E-01,  1.37173E-01
+499,  2.76587E-01,  4.12260E-01,  1.29937E-01
+500,  2.88959E-01,  4.27764E-01,  1.22839E-01
+501,  3.01934E-01,  4.44001E-01,  1.15834E-01
+502,  3.15508E-01,  4.60947E-01,  1.08922E-01
+503,  3.29673E-01,  4.78562E-01,  1.02118E-01
+504,  3.44416E-01,  4.96795E-01,  9.54374E-02
+505,  3.59716E-01,  5.15587E-01,  8.88965E-02
+506,  3.75550E-01,  5.34868E-01,  8.25343E-02
+507,  3.91895E-01,  5.54575E-01,  7.64613E-02
+508,  4.08722E-01,  5.74640E-01,  7.07769E-02
+509,  4.25998E-01,  5.94984E-01,  6.55491E-02
+510,  4.43683E-01,  6.15520E-01,  6.08210E-02
+511,  4.61731E-01,  6.36162E-01,  5.65919E-02
+512,  4.80094E-01,  6.56872E-01,  5.27659E-02
+513,  4.98717E-01,  6.77624E-01,  4.92440E-02
+514,  5.17539E-01,  6.98393E-01,  4.59470E-02
+515,  5.36494E-01,  7.19154E-01,  4.28123E-02
+516,  5.55493E-01,  7.39844E-01,  3.98014E-02
+517,  5.74386E-01,  7.60235E-01,  3.69227E-02
+518,  5.92995E-01,  7.80039E-01,  3.41909E-02
+519,  6.11124E-01,  7.98941E-01,  3.16158E-02
+520,  6.28561E-01,  8.16610E-01,  2.92033E-02
+521,  6.45139E-01,  8.32783E-01,  2.69536E-02
+522,  6.60907E-01,  8.47550E-01,  2.48575E-02
+523,  6.75993E-01,  8.61113E-01,  2.29047E-02
+524,  6.90542E-01,  8.73698E-01,  2.10855E-02
+525,  7.04720E-01,  8.85550E-01,  1.93912E-02
+526,  7.18662E-01,  8.96873E-01,  1.78145E-02
+527,  7.32323E-01,  9.07638E-01,  1.63514E-02
+528,  7.45606E-01,  9.17757E-01,  1.49980E-02
+529,  7.58410E-01,  9.27137E-01,  1.37497E-02
+530,  7.70630E-01,  9.35687E-01,  1.26013E-02
+531,  7.82208E-01,  9.43362E-01,  1.15466E-02
+532,  7.93290E-01,  9.50309E-01,  1.05766E-02
+533,  8.04084E-01,  9.56733E-01,  9.68268E-03
+534,  8.14813E-01,  9.62844E-01,  8.85746E-03
+535,  8.25711E-01,  9.68858E-01,  8.09453E-03
+536,  8.36943E-01,  9.74919E-01,  7.38890E-03
+537,  8.48349E-01,  9.80850E-01,  6.73799E-03
+538,  8.59676E-01,  9.86388E-01,  6.13947E-03
+539,  8.70656E-01,  9.91267E-01,  5.59073E-03
+540,  8.81011E-01,  9.95217E-01,  5.08900E-03
+541,  8.90496E-01,  9.98007E-01,  4.63120E-03
+542,  8.99052E-01,  9.99595E-01,  4.21366E-03
+543,  9.06669E-01,  9.99982E-01,  3.83286E-03
+544,  9.13341E-01,  9.99177E-01,  3.48559E-03
+545,  9.19067E-01,  9.97193E-01,  3.16893E-03
+546,  9.23898E-01,  9.94102E-01,  2.88021E-03
+547,  9.28099E-01,  9.90204E-01,  2.61698E-03
+548,  9.31995E-01,  9.85855E-01,  2.37701E-03
+549,  9.35916E-01,  9.81404E-01,  2.15829E-03
+550,  9.40198E-01,  9.77193E-01,  1.95896E-03
+551,  9.45076E-01,  9.73441E-01,  1.77737E-03
+552,  9.50367E-01,  9.69881E-01,  1.61218E-03
+553,  9.55775E-01,  9.66133E-01,  1.46215E-03
+554,  9.61000E-01,  9.61823E-01,  1.32606E-03
+555,  9.65733E-01,  9.56583E-01,  1.20277E-03
+556,  9.69744E-01,  9.50167E-01,  1.09118E-03
+557,  9.73133E-01,  9.42773E-01,  9.90135E-04
+558,  9.76086E-01,  9.34709E-01,  8.98564E-04
+559,  9.78793E-01,  9.26271E-01,  8.15524E-04
+560,  9.81445E-01,  9.17750E-01,  7.40174E-04
+561,  9.84186E-01,  9.09339E-01,  6.71772E-04
+562,  9.86965E-01,  9.00895E-01,  6.09693E-04
+563,  9.89678E-01,  8.92197E-01,  5.53372E-04
+564,  9.92220E-01,  8.83035E-01,  5.02292E-04
+565,  9.94486E-01,  8.73205E-01,  4.55979E-04
+566,  9.96386E-01,  8.62565E-01,  4.13997E-04
+567,  9.97895E-01,  8.51173E-01,  3.75940E-04
+568,  9.99004E-01,  8.39132E-01,  3.41439E-04
+569,  9.99706E-01,  8.26545E-01,  3.10160E-04
+570,  9.99993E-01,  8.13509E-01,  2.81800E-04
+571,  9.99837E-01,  8.00082E-01,  2.56084E-04
+572,  9.99123E-01,  7.86166E-01,  2.32764E-04
+573,  9.97719E-01,  7.71635E-01,  2.11616E-04
+574,  9.95491E-01,  7.56376E-01,  1.92436E-04
+575,  9.92310E-01,  7.40291E-01,  1.75039E-04
+576,  9.88146E-01,  7.23369E-01,  1.59257E-04
+577,  9.83345E-01,  7.05890E-01,  1.44940E-04
+578,  9.78342E-01,  6.88184E-01,  1.31948E-04
+579,  9.73564E-01,  6.70554E-01,  1.20157E-04
+580,  9.69429E-01,  6.53274E-01,  1.09454E-04
+581,  9.66211E-01,  6.36524E-01,  9.97367E-05
+582,  9.63630E-01,  6.20217E-01,  9.09125E-05
+583,  9.61273E-01,  6.04211E-01,  8.28976E-05
+584,  9.58732E-01,  5.88375E-01,  7.56160E-05
+585,  9.55602E-01,  5.72597E-01,  6.89991E-05
+586,  9.51569E-01,  5.56783E-01,  6.29846E-05
+587,  9.46654E-01,  5.40896E-01,  5.75163E-05
+588,  9.40962E-01,  5.24912E-01,  5.25432E-05
+589,  9.34600E-01,  5.08817E-01,  4.80192E-05
+590,  9.27673E-01,  4.92599E-01,  4.39024E-05
+591,  9.20264E-01,  4.76268E-01,  4.01551E-05
+592,  9.12391E-01,  4.59893E-01,  3.67431E-05
+593,  9.04050E-01,  4.43551E-01,  3.36353E-05
+594,  8.95243E-01,  4.27314E-01,  3.08037E-05
+595,  8.85969E-01,  4.11246E-01,  2.82228E-05
+596,  8.76242E-01,  3.95396E-01,  2.58697E-05
+597,  8.66117E-01,  3.79782E-01,  2.37235E-05
+598,  8.55658E-01,  3.64410E-01,  2.17653E-05
+599,  8.44926E-01,  3.49289E-01,  1.99779E-05
+600,  8.33982E-01,  3.34429E-01,  1.83459E-05
+601,  8.22859E-01,  3.19843E-01,  1.68551E-05
+602,  8.11491E-01,  3.05564E-01,  1.54929E-05
+603,  7.99794E-01,  2.91625E-01,  1.42476E-05
+604,  7.87689E-01,  2.78053E-01,  1.31087E-05
+605,  7.75103E-01,  2.64872E-01,  1.20667E-05
+606,  7.61996E-01,  2.52099E-01,  1.11131E-05
+607,  7.48425E-01,  2.39747E-01,  1.02399E-05
+608,  7.34470E-01,  2.27822E-01,  9.43999E-06
+609,  7.20208E-01,  2.16330E-01,  8.70695E-06
+610,  7.05713E-01,  2.05273E-01,  8.03488E-06
+611,  6.91044E-01,  1.94650E-01,  7.41844E-06
+612,  6.76212E-01,  1.84448E-01,  6.85279E-06
+613,  6.61220E-01,  1.74654E-01,  6.33352E-06
+614,  6.46072E-01,  1.65256E-01,  5.85662E-06
+615,  6.30773E-01,  1.56243E-01,  5.41843E-06
+616,  6.15349E-01,  1.47602E-01,
+617,  5.99888E-01,  1.39329E-01,
+618,  5.84489E-01,  1.31416E-01,
+619,  5.69240E-01,  1.23856E-01,
+620,  5.54224E-01,  1.16641E-01,
+621,  5.39469E-01,  1.09766E-01,
+622,  5.24827E-01,  1.03226E-01,
+623,  5.10124E-01,  9.70205E-02,
+624,  4.95206E-01,  9.11430E-02,
+625,  4.79941E-01,  8.55872E-02,
+626,  4.64270E-01,  8.03428E-02,
+627,  4.48338E-01,  7.53913E-02,
+628,  4.32329E-01,  7.07136E-02,
+629,  4.16406E-01,  6.62924E-02,
+630,  4.00711E-01,  6.21120E-02,
+631,  3.85355E-01,  5.81595E-02,
+632,  3.70377E-01,  5.44276E-02,
+633,  3.55793E-01,  5.09098E-02,
+634,  3.41618E-01,  4.75991E-02,
+635,  3.27864E-01,  4.44879E-02,
+636,  3.14541E-01,  4.15659E-02,
+637,  3.01662E-01,  3.88152E-02,
+638,  2.89239E-01,  3.62181E-02,
+639,  2.77278E-01,  3.37599E-02,
+640,  2.65784E-01,  3.14282E-02,
+641,  2.54740E-01,  2.92176E-02,
+642,  2.44054E-01,  2.71403E-02,
+643,  2.33634E-01,  2.52084E-02,
+644,  2.23399E-01,  2.34286E-02,
+645,  2.13284E-01,  2.18037E-02,
+646,  2.03257E-01,  2.03285E-02,
+647,  1.93370E-01,  1.89779E-02,
+648,  1.83688E-01,  1.77272E-02,
+649,  1.74263E-01,  1.65560E-02,
+650,  1.65141E-01,  1.54480E-02,
+651,  1.56354E-01,  1.43924E-02,
+652,  1.47916E-01,  1.33896E-02,
+653,  1.39834E-01,  1.24414E-02,
+654,  1.32111E-01,  1.15488E-02,
+655,  1.24749E-01,  1.07120E-02,
+656,  1.17744E-01,  9.92994E-03,
+657,  1.11081E-01,  9.20057E-03,
+658,  1.04747E-01,  8.52126E-03,
+659,  9.87277E-02,  7.88945E-03,
+660,  9.30085E-02,  7.30255E-03,
+661,  8.75769E-02,  6.75820E-03,
+662,  8.24219E-02,  6.25474E-03,
+663,  7.75328E-02,  5.79045E-03,
+664,  7.28989E-02,  5.36347E-03,
+665,  6.85100E-02,  4.97179E-03,
+666,  6.43554E-02,  4.61300E-03,
+667,  6.04242E-02,  4.28339E-03,
+668,  5.67057E-02,  3.97940E-03,
+669,  5.31896E-02,  3.69803E-03,
+670,  4.98661E-02,  3.43667E-03,
+671,  4.67258E-02,  3.19326E-03,
+672,  4.37598E-02,  2.96653E-03,
+673,  4.09594E-02,  2.75543E-03,
+674,  3.83165E-02,  2.55896E-03,
+675,  3.58233E-02,  2.37617E-03,
+676,  3.34725E-02,  2.20618E-03,
+677,  3.12583E-02,  2.04809E-03,
+678,  2.91751E-02,  1.90110E-03,
+679,  2.72173E-02,  1.76442E-03,
+680,  2.53790E-02,  1.63734E-03,
+681,  2.36541E-02,  1.51916E-03,
+682,  2.20336E-02,  1.40913E-03,
+683,  2.05092E-02,  1.30655E-03,
+684,  1.90735E-02,  1.21078E-03,
+685,  1.77201E-02,  1.12128E-03,
+686,  1.64451E-02,  1.03766E-03,
+687,  1.52510E-02,  9.59892E-04,
+688,  1.41404E-02,  8.87951E-04,
+689,  1.31137E-02,  8.21729E-04,
+690,  1.21701E-02,  7.61051E-04,
+691,  1.13063E-02,  7.05623E-04,
+692,  1.05131E-02,  6.54874E-04,
+693,  9.78127E-03,  6.08240E-04,
+694,  9.10300E-03,  5.65240E-04,
+695,  8.47170E-03,  5.25457E-04,
+696,  7.88227E-03,  4.88550E-04,
+697,  7.33213E-03,  4.54277E-04,
+698,  6.81928E-03,  4.22436E-04,
+699,  6.34173E-03,  3.92839E-04,
+700,  5.89749E-03,  3.65317E-04,
+701,  5.48444E-03,  3.39710E-04,
+702,  5.09978E-03,  3.15856E-04,
+703,  4.74086E-03,  2.93607E-04,
+704,  4.40538E-03,  2.72834E-04,
+705,  4.09129E-03,  2.53417E-04,
+706,  3.79703E-03,  2.35265E-04,
+707,  3.52187E-03,  2.18331E-04,
+708,  3.26520E-03,  2.02574E-04,
+709,  3.02632E-03,  1.87948E-04,
+710,  2.80447E-03,  1.74402E-04,
+711,  2.59882E-03,  1.61878E-04,
+712,  2.40849E-03,  1.50305E-04,
+713,  2.23259E-03,  1.39612E-04,
+714,  2.07024E-03,  1.29734E-04,
+715,  1.92058E-03,  1.20608E-04,
+716,  1.78269E-03,  1.12176E-04,
+717,  1.65540E-03,  1.04373E-04,
+718,  1.53762E-03,  9.71383E-05,
+719,  1.42839E-03,  9.04202E-05,
+720,  1.32687E-03,  8.41716E-05,
+721,  1.23238E-03,  7.83538E-05,
+722,  1.14456E-03,  7.29425E-05,
+723,  1.06308E-03,  6.79164E-05,
+724,  9.87592E-04,  6.32542E-05,
+725,  9.17777E-04,  5.89349E-05,
+726,  8.53264E-04,  5.49360E-05,
+727,  7.93589E-04,  5.12291E-05,
+728,  7.38306E-04,  4.77872E-05,
+729,  6.87018E-04,  4.45862E-05,
+730,  6.39373E-04,  4.16049E-05,
+731,  5.95057E-04,  3.88245E-05,
+732,  5.53797E-04,  3.62302E-05,
+733,  5.15350E-04,  3.38086E-05,
+734,  4.79496E-04,  3.15474E-05,
+735,  4.46035E-04,  2.94354E-05,
+736,  4.14809E-04,  2.74634E-05,
+737,  3.85749E-04,  2.56268E-05,
+738,  3.58792E-04,  2.39217E-05,
+739,  3.33861E-04,  2.23432E-05,
+740,  3.10869E-04,  2.08860E-05,
+741,  2.89699E-04,  1.95424E-05,
+742,  2.70145E-04,  1.82990E-05,
+743,  2.52007E-04,  1.71423E-05,
+744,  2.35117E-04,  1.60611E-05,
+745,  2.19329E-04,  1.50458E-05,
+746,  2.04535E-04,  1.40893E-05,
+747,  1.90692E-04,  1.31899E-05,
+748,  1.77771E-04,  1.23462E-05,
+749,  1.65736E-04,  1.15569E-05,
+750,  1.54549E-04,  1.08200E-05,
+751,  1.44167E-04,  1.01334E-05,
+752,  1.34528E-04,  9.49367E-06,
+753,  1.25574E-04,  8.89736E-06,
+754,  1.17251E-04,  8.34135E-06,
+755,  1.09508E-04,  7.82271E-06,
+756,  1.02300E-04,  7.33865E-06,
+757,  9.55828E-05,  6.88612E-06,
+758,  8.93161E-05,  6.46228E-06,
+759,  8.34631E-05,  6.06462E-06,
+760,  7.79912E-05,  5.69093E-06,
+761,  7.28730E-05,  5.33942E-06,
+762,  6.80921E-05,  5.00929E-06,
+763,  6.36342E-05,  4.69984E-06,
+764,  5.94841E-05,  4.41034E-06,
+765,  5.56264E-05,  4.13998E-06,
+766,  5.20430E-05,  3.88772E-06,
+767,  4.87063E-05,  3.65186E-06,
+768,  4.55900E-05,  3.43070E-06,
+769,  4.26710E-05,  3.22278E-06,
+770,  3.99295E-05,  3.02683E-06,
+771,  3.73509E-05,  2.84192E-06,
+772,  3.49326E-05,  2.66795E-06,
+773,  3.26730E-05,  2.50491E-06,
+774,  3.05690E-05,  2.35267E-06,
+775,  2.86163E-05,  2.21100E-06,
+776,  2.68077E-05,  2.07946E-06,
+777,  2.51286E-05,  1.95700E-06,
+778,  2.35645E-05,  1.84258E-06,
+779,  2.21026E-05,  1.73528E-06,
+780,  2.07321E-05,  1.63433E-06,
+781,  1.94444E-05,  1.53910E-06,
+782,  1.82351E-05,  1.44932E-06,
+783,  1.71008E-05,  1.36478E-06,
+784,  1.60380E-05,  1.28526E-06,
+785,  1.50432E-05,  1.21054E-06,
+786,  1.41127E-05,  1.14038E-06,
+787,  1.32420E-05,  1.07446E-06,
+788,  1.24263E-05,  1.01247E-06,
+789,  1.16618E-05,  9.54130E-07,
+790,  1.09446E-05,  8.99170E-07,
+791,  1.02716E-05,  8.47379E-07,
+792,  9.64036E-06,  7.98635E-07,
+793,  9.04897E-06,  7.52832E-07,
+794,  8.49535E-06,  7.09857E-07,
+795,  7.97750E-06,  6.69594E-07,
+796,  7.49341E-06,  6.31907E-07,
+797,  7.04079E-06,  5.96604E-07,
+798,  6.61744E-06,  5.63495E-07,
+799,  6.22133E-06,  5.32408E-07,
+800,  5.85057E-06,  5.03187E-07,
+801,  5.50333E-06,  4.75686E-07,
+802,  5.17756E-06,  4.49753E-07,
+803,  4.87135E-06,  4.25249E-07,
+804,  4.58300E-06,  4.02050E-07,
+805,  4.31102E-06,  3.80046E-07,
+806,  4.05422E-06,  3.59156E-07,
+807,  3.81213E-06,  3.39356E-07,
+808,  3.58438E-06,  3.20633E-07,
+809,  3.37053E-06,  3.02966E-07,
+810,  3.17009E-06,  2.86329E-07,
+811,  2.98248E-06,  2.70687E-07,
+812,  2.80691E-06,  2.55980E-07,
+813,  2.64257E-06,  2.42147E-07,
+814,  2.48873E-06,  2.29130E-07,
+815,  2.34468E-06,  2.16878E-07,
+816,  2.20974E-06,  2.05338E-07,
+817,  2.08315E-06,  1.94449E-07,
+818,  1.96419E-06,  1.84155E-07,
+819,  1.85222E-06,  1.74407E-07,
+820,  1.74666E-06,  1.65158E-07,
+821,  1.64705E-06,  1.56373E-07,
+822,  1.55307E-06,  1.48031E-07,
+823,  1.46448E-06,  1.40117E-07,
+824,  1.38100E-06,  1.32615E-07,
+825,  1.30241E-06,  1.25508E-07,
+826,  1.22844E-06,  1.18781E-07,
+827,  1.15888E-06,  1.12416E-07,
+828,  1.09348E-06,  1.06398E-07,
+829,  1.03203E-06,  1.00711E-07,
+830,  9.74306E-07,  9.53411E-08,
diff --git a/reanimate.cabal b/reanimate.cabal
--- a/reanimate.cabal
+++ b/reanimate.cabal
@@ -2,7 +2,7 @@
 --  see http://haskell.org/cabal/users-guide/
 
 name:                reanimate
-version:             0.1.5.0
+version:             0.1.6.0
 -- synopsis:
 -- description:
 license:             PublicDomain
@@ -24,9 +24,11 @@
 data-files:           viewer/build/*.js
                       viewer/build/*.html
                       viewer/build/static/js/2.772a56e7.chunk.js
-                      viewer/build/static/js/main.db22f45d.chunk.js
+                      viewer/build/static/js/main.c36ecb4a.chunk.js
                       viewer/build/static/js/runtime~main.9eb600ee.js
-                      viewer/build/static/css/main.6efe09fd.chunk.css
+                      viewer/build/static/css/main.f7ad3e9b.chunk.css
+                      data/CIExyz.csv
+                      data/cone_sensitivity_lms.csv
 
 Source-Repository head
     Type:      git
@@ -37,6 +39,7 @@
   default-language:   Haskell2010
   default-extensions: PackageImports
   exposed-modules:    Reanimate.Monad
+                      Reanimate.Signal
                       Reanimate.Render
                       Reanimate.Examples
                       Reanimate.Combinators
@@ -47,30 +50,38 @@
                       Reanimate.Driver
                       Reanimate.Misc
                       Reanimate.Morph
+                      Reanimate.Raster
+                      Reanimate.ColorMap
+                      Reanimate.ColorSpace
+                      Reanimate.Interpolate
+                      Reanimate.Memo
+                      Reanimate.Scene
+                      Reanimate.Povray
   other-modules:      Reanimate.Svg.NamedColors
                       Reanimate.Cache
                       Paths_reanimate
   build-depends:       base >=4.10 && <4.13,
                        time, text, filepath, process, directory,
-                       containers, reanimate-svg >= 0.7.0.0, xml, bytestring, lens, linear, mtl, matrix,
+                       containers, reanimate-svg >= 0.9.1.1, xml, bytestring, lens, linear, mtl, matrix,
                        JuicyPixels, attoparsec, parallel, diagrams, diagrams-svg,
                        diagrams-core, diagrams-lib, diagrams-contrib,
                        svg-builder, matrices, cubicbezier, palette, websockets,
-                       hashable, fsnotify, open-browser
-
-Flag server
-  Description: Enable rendering server
-  Default:     False
+                       hashable, fsnotify, open-browser, random-shuffle, base64-bytestring,
+                       vector, colour, cassava, ansi-wl-pprint, here, temporary
 
-executable reanimate-server
-  if flag(server)
-    buildable:        True
-  else
-    buildable:        False
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
   default-language:   Haskell2010
-  hs-source-dirs:     server, src
-  main-is:            Main.hs
-  other-modules:      Reanimate.Misc
-                      Cache
-  build-depends:      base >=4.10 && <4.13, text, websockets, process, filepath, directory,
-                      containers, hashable, time
+  other-modules:
+    UnitTests
+  hs-source-dirs: test
+  build-depends:
+    base,
+    directory,
+    filepath,
+    bytestring, process,
+    reanimate,
+    QuickCheck >= 2.1.0,
+    tasty, tasty-golden, tasty-hunit,
+    ansi-wl-pprint
diff --git a/server/Cache.hs b/server/Cache.hs
deleted file mode 100644
--- a/server/Cache.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-module Cache (lookupCache, insertCache) where
-
-import Control.Concurrent
-import Control.Exception
-import qualified Data.IntMap as M
-import System.Directory
-import System.FilePath
-import qualified Data.Text as T
-import qualified Data.Text.IO as T
-import Data.Text (Text)
-import Data.Hashable
-
-valueFilePath :: Text -> IO FilePath
-valueFilePath key = do
-  tmp <- getTemporaryDirectory
-  return $ tmp </> "reanimate" ++ show (hash key) <.> "svgs"
-
-
-insertCache :: Text -> [Text] -> IO ()
-insertCache key value = do
-  cacheFile <- valueFilePath key
-  T.writeFile cacheFile (T.unlines value)
-
-lookupCache :: Text -> IO (Maybe [Text])
-lookupCache key = do
-    cacheFile <- valueFilePath key
-    do svgs <- T.lines <$> T.readFile cacheFile
-       evaluate (svgs)
-       return (Just svgs)
-     `catch` \SomeException{} -> return Nothing
diff --git a/server/Main.hs b/server/Main.hs
deleted file mode 100644
--- a/server/Main.hs
+++ /dev/null
@@ -1,117 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-module Main where
-
-import Control.Concurrent
-import Control.Exception
-import Control.Monad
-import Data.Monoid
-import Data.Time
-import Network.WebSockets
-import Control.Monad.Fix
-import qualified Data.Text as T
-import qualified Data.Text.IO as T
-import qualified Data.Map as Map
-
-import Cache
-import Reanimate.Misc
-
-main :: IO ()
-main = do
-  runServerWith "127.0.0.1" 9161 opts $ \pending -> do
-  conn <- acceptRequest pending
-  thread <- newEmptyMVar
-  forkPingThread conn 30
-  forever $ do
-    msg <- receiveData conn :: IO T.Text
-    stopWorker conn thread
-    putMVar thread =<< forkIO (generateResponse conn msg >> tryTakeMVar thread >> return ())
-  where
-    opts = defaultConnectionOptions
-      { connectionCompressionOptions = PermessageDeflateCompression defaultPermessageDeflate }
-
-stopWorker conn mvar = do
-  mbTid <- tryTakeMVar mvar
-  case mbTid of
-    Nothing -> return ()
-    Just tid -> do
-      putStrLn "Interrupt"
-      killThread tid
-      sendTextData conn $ T.pack $ "Error" ++ "Reset"
-
-generateResponse conn msg = do
-  mbCached <- lookupCache msg
-  case mbCached of
-    Just svgs -> do
-      putStrLn "Returning cached svg."
-      sendTextDatas conn (T.pack "Success!" : svgs)
-      sendTextData conn (T.pack "Done")
-    Nothing ->
-      withTempFile ".exe" $ \tmpExecutable ->
-      withTempFile ".hs" $ \tmpSource ->
-      withTempDir $ \tmpDir -> do
-        T.writeFile tmpSource $ T.unlines
-          ["{-# LANGUAGE Arrows, OverloadedStrings, CPP #-}"
-          ,"module Main where"
-          ,"import Reanimate.Monad"
-          ,"import           Reanimate.Combinators"
-          ,"import           Reanimate.LaTeX"
-          ,"import           Codec.Picture.Types"
-          ,"import           Reanimate.Svg"
-          ,"import           Reanimate.Render"
-          ,"import           Data.Monoid"
-          ,"import           Graphics.Svg as S"
-          ,"main = renderSvgs animation " <> T.pack (show tmpDir)
-          ,"#line 1 \"animation.hs\""
-          ] <> msg
-        putStrLn $ "Compiling program:\n" ++ T.unpack msg
-        sendTextData conn (T.pack "Compiling")
-        ret <- timeIt "compile" $
-                runCmd_ "stack" $ ["ghc", "--"] ++ ghcOptions ++ [tmpSource, "-o", tmpExecutable]
-        case ret of
-          Left err -> do
-            sendTextData conn $ T.pack $ "Error" ++ unlines (drop 3 (lines err))
-          Right{} -> do
-            queue <- newChan
-            tid <- forkIO $ forever $ sendTextData conn =<< readChan queue
-            sendTextData conn (T.pack "Rendering")
-            flip onException (killThread tid) $
-              timeIt "render" $ withTimeout queue conn 60 $ do
-              getFrame <- runCmdLazy tmpExecutable ["+RTS", "-N", "-M50M", "-RTS"]
-              flip fix [] $ \loop acc -> do
-                frame <- getFrame
-                case frame of
-                  Left "" -> do
-                    writeChan queue (T.pack "Done")
-                    insertCache msg (reverse acc)
-                  Left err -> do
-                    _ <- getChanContents queue
-                    writeChan queue $ T.pack $ "Error" ++ err
-                  Right frame -> do
-                    writeChan queue frame
-                    loop (frame : acc)
-
-ghcOptions :: [String]
-ghcOptions = ["-rtsopts", "--make", "-threaded", "-O"]
-
-withTimeout queue conn t action = do
-  finished <- newEmptyMVar
-  worker <- forkIO (action >> putMVar finished ())
-  timer <- forkIO $ do
-    threadDelay (10^6 * t)
-    putMVar finished ()
-    putStrLn "Timeout"
-    killThread worker
-    _ <- getChanContents queue
-    writeChan queue $ T.pack $ "Error" ++ "Timeout"
-
-  takeMVar finished `onException` do
-    killThread worker
-    killThread timer
-
-timeIt :: String -> IO a -> IO a
-timeIt label fn = do
-  t1 <- getCurrentTime
-  a <- fn
-  t2 <- getCurrentTime
-  putStrLn $ label ++ ": " ++ show (diffUTCTime t2 t1)
-  return a
diff --git a/src/Reanimate/Cache.hs b/src/Reanimate/Cache.hs
--- a/src/Reanimate/Cache.hs
+++ b/src/Reanimate/Cache.hs
@@ -2,6 +2,7 @@
   ( cacheMem
   , cacheDisk
   , cacheDiskSvg
+  , cacheDiskKey
   , cacheDiskLines
   ) where
 
@@ -21,6 +22,7 @@
 import           System.Directory
 import           System.FilePath
 import           System.IO.Unsafe
+import           System.IO
 
 -- Memory cache and disk cache
 
@@ -34,19 +36,20 @@
       then do
         inp <- T.readFile path
         case parse inp of
-          Nothing -> do
-            let tmp = path <.> "tmp"
-            new <- gen key
-            T.writeFile tmp (render new)
-            renameFile tmp path
-            return new
+          Nothing -> genCache root path
           Just val -> pure val
-      else do
-        let tmp = path <.> "tmp"
-        new <- gen key
-        T.writeFile tmp (render new)
-        renameFile tmp path
-        return new
+      else genCache root path
+  where
+    genCache root path = do
+      (tmpPath, tmpHandle) <- openTempFile root (show (hash key))
+      new <- gen key
+      T.hPutStr tmpHandle (render new)
+      hClose tmpHandle
+      renameFile tmpPath path
+      return new
+
+cacheDiskKey :: Text -> IO Tree -> IO Tree
+cacheDiskKey key gen = cacheDiskSvg (const gen) key
 
 cacheDiskSvg :: (Text -> IO Tree) -> (Text -> IO Tree)
 cacheDiskSvg = cacheDisk parse render
diff --git a/src/Reanimate/ColorMap.hs b/src/Reanimate/ColorMap.hs
new file mode 100644
--- /dev/null
+++ b/src/Reanimate/ColorMap.hs
@@ -0,0 +1,475 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Reanimate.ColorMap
+  ( turbo
+  , viridis
+  , magma
+  , inferno
+  , plasma
+  , sinebow
+  , parula
+  , cividis
+  , jet
+  , hsv
+  , greyscale
+  ) where
+
+import Data.Text (Text)
+import Data.Vector (Vector)
+import qualified Data.Text as T
+import qualified Data.Vector as V
+import Codec.Picture
+import Data.Char
+import Data.Bits
+import qualified Data.Colour.RGBSpace.HSV as HSV
+import           Data.Colour.RGBSpace
+
+-- | Given a number t in the range [0,1], returns the corresponding color from
+--   the “turbo” color scheme by Anton Mikhailov.
+turbo :: Double -> PixelRGB8
+turbo t = PixelRGB8 red green blue
+  where
+    red   = trunc (round (34.61 + t * (1172.33 - t * (10793.56 - t * (33300.12 - t * (38394.49 - t * 14825.05))))))
+    green = trunc (round (23.31 + t * (557.33 + t * (1225.33 - t * (3574.96 - t * (1073.77 + t * 707.56))))))
+    blue  = trunc (round (27.2 + t * (3211.1 - t * (15327.97 - t * (27814 - t * (22569.18 - t * 6838.66))))))
+    trunc :: Integer -> Pixel8
+    trunc = fromIntegral . min 255 . max 0
+
+-- | Given a number t in the range [0,1], returns the corresponding color from
+--   the “viridis” perceptually-uniform color scheme designed by van der Walt,
+--   Smith and Firing for matplotlib, represented as an RGB string.
+viridis :: Double -> PixelRGB8
+viridis = ramp (colors
+  "44015444025645045745055946075a46085c460a5d460b5e470d60470e614710634711644713\
+  \6548146748166848176948186a481a6c481b6d481c6e481d6f481f7048207148217348237448\
+  \2475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f\
+  \463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142\
+  \874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b\
+  \518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d\
+  \355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b\
+  \8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a\
+  \778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e\
+  \25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f\
+  \8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e\
+  \9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a685\
+  \22a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db2\
+  \7d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340\
+  \bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c765\
+  \5ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d0\
+  \5477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195\
+  \d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2b\
+  \b8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e2\
+  \19dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8\
+  \e621fbe723fde725")
+
+-- | Given a number t in the range [0,1], returns the corresponding color from
+--   the “magma” perceptually-uniform color scheme designed by van der Walt and
+--   Smith for matplotlib, represented as an RGB string.
+magma :: Double -> PixelRGB8
+magma = ramp (colors
+  "00000401000501010601010802010902020b02020d03030f0303120404140504160605180605\
+  \1a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d3414\
+  \0e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e221150241253\
+  \25125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f\
+  \6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f\
+  \127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980\
+  \641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621\
+  \817822817922827b23827c23827e24828025828125818326818426818627818827818928818b\
+  \29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7f\
+  \a02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb336\
+  \7ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c8\
+  \3e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476a\
+  \dc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb57\
+  \60ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf6\
+  \6c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835f\
+  \fb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b\
+  \6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afe\
+  \b47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8d\
+  \fecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2\
+  \a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fc\
+  \f9bbfcfbbdfcfdbf")
+
+-- | Given a number t in the range [0,1], returns the corresponding color from
+--   the “inferno” perceptually-uniform color scheme designed by van der Walt
+--   and Smith for matplotlib, represented as an RGB string.
+inferno :: Double -> PixelRGB8
+inferno = ramp (colors
+  "00000401000501010601010802010a02020c02020e0302100403120403140504170604190705\
+  \1b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b3716\
+  \0b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b55\
+  \2b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a\
+  \67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d55\
+  \0f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e\
+  \6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e\
+  \6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f246990256892\
+  \25689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60\
+  \a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b935\
+  \56ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb\
+  \4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3c\
+  \db503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee860\
+  \2de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2\
+  \741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890c\
+  \f98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca1\
+  \08fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfb\
+  \ba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13d\
+  \f7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea\
+  \69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9\
+  \fc9dfafda1fcffa4")
+
+-- | Given a number t in the range [0,1], returns the corresponding color from
+--   the “plasma” perceptually-uniform color scheme designed by van der Walt and
+--   Smith for matplotlib, represented as an RGB string.
+plasma :: Double -> PixelRGB8
+plasma = ramp (colors
+  "0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05\
+  \932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41\
+  \049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a4\
+  \5601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900\
+  \a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d\
+  \03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca4\
+  \8f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a\
+  \9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b0\
+  \2991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786\
+  \be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca45\
+  \7acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5\
+  \546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263\
+  \e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e971\
+  \58e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1\
+  \814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143\
+  \f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca3\
+  \38fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efe\
+  \b72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26\
+  \fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df\
+  \25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1\
+  \f525f0f724f0f921")
+
+-- | Given a number t in the range [0,1], returns the corresponding color from
+--   the “sinebow” color scheme by Jim Bumgardner and Charlie Loyd.
+sinebow :: Double -> PixelRGB8
+sinebow t = PixelRGB8 r g b
+  where
+    pi_1_3 = pi / 3
+    pi_2_3 = pi * 2 / 3
+    x = (0.5 - t) * pi
+    r = round $ 255 * sin(x)^2
+    g = round $ 255 * sin(x+pi_1_3)^2
+    b = round $ 255 * sin(x+pi_2_3)^2
+
+-- | Given a number t in the range [0,1], returns the corresponding color from
+--   the “cividis” color vision deficiency-optimized color scheme designed by
+--   Nuñez, Anderton, and Renslow, represented as an RGB string.
+cividis :: Double -> PixelRGB8
+cividis t = PixelRGB8 red green blue
+  where
+    red   = trunc $ round(-4.54 - t * (35.34 - t * (2381.73 - t * (6402.7 - t * (7024.72 - t * 2710.57)))))
+    green = trunc $ round(32.49 + t * (170.73 + t * (52.82 - t * (131.46 - t * (176.58 - t * 67.37)))))
+    blue  = trunc $ round(81.24 + t * (442.36 - t * (2482.43 - t * (6167.24 - t * (6614.94 - t * 2475.67)))))
+    trunc :: Integer -> Pixel8
+    trunc = fromIntegral . min 255 . max 0
+
+-- | Jet colormap. Used to be the default in matlab. Obsolete.
+jet :: Double -> PixelRGB8
+jet t = PixelRGB8 red green blue
+  where
+    red   = trunc $ min (4*t - 1.5) (-4*t + 4.5)
+    green = trunc $ min (4*t - 0.5) (-4*t + 3.5)
+    blue  = trunc $ min (4*t + 0.5) (-4*t + 2.5)
+    trunc :: Double -> Pixel8
+    trunc = fromIntegral . min 255 . max 0 . round . (*) 255
+
+-- | hsv colormap. Goes from 0 degrees to 360 degrees.
+hsv :: Double -> PixelRGB8
+hsv t = PixelRGB8 (round $ r*255) (round $ g*255) (round $ b*255)
+  where
+    RGB r g b = HSV.hsv (t * 360) 1 1
+
+-- | Matlab hsv colormap. Goes from 0 degrees to 330 degrees.
+hsvMatlab :: Double -> PixelRGB8
+hsvMatlab t = PixelRGB8 (round $ r*255) (round $ g*255) (round $ b*255)
+  where
+    RGB r g b = HSV.hsv (t * 330) 1 1
+
+greyscale :: Double -> PixelRGB8
+greyscale t = PixelRGB8 v v v
+  where
+    v = round $ t * 255
+
+parula :: Double -> PixelRGB8
+parula = ramp vec
+  where
+    vec = V.fromList $ pixels colors
+    pixels [] = []
+    pixels (r:g:b:xs) =
+      PixelRGB8 (round $ r*255) (round $ g*255) (round $ b*255) :
+      pixels xs
+    colors =
+       [0.2081, 0.1663, 0.5292
+       ,0.2091, 0.1721, 0.5411
+       ,0.2101, 0.1779, 0.5530
+       ,0.2109, 0.1837, 0.5650
+       ,0.2116, 0.1895, 0.5771
+       ,0.2121, 0.1954, 0.5892
+       ,0.2124, 0.2013, 0.6013
+       ,0.2125, 0.2072, 0.6135
+       ,0.2123, 0.2132, 0.6258
+       ,0.2118, 0.2192, 0.6381
+       ,0.2111, 0.2253, 0.6505
+       ,0.2099, 0.2315, 0.6629
+       ,0.2084, 0.2377, 0.6753
+       ,0.2063, 0.2440, 0.6878
+       ,0.2038, 0.2503, 0.7003
+       ,0.2006, 0.2568, 0.7129
+       ,0.1968, 0.2632, 0.7255
+       ,0.1921, 0.2698, 0.7381
+       ,0.1867, 0.2764, 0.7507
+       ,0.1802, 0.2832, 0.7634
+       ,0.1728, 0.2902, 0.7762
+       ,0.1641, 0.2975, 0.7890
+       ,0.1541, 0.3052, 0.8017
+       ,0.1427, 0.3132, 0.8145
+       ,0.1295, 0.3217, 0.8269
+       ,0.1147, 0.3306, 0.8387
+       ,0.0986, 0.3397, 0.8495
+       ,0.0816, 0.3486, 0.8588
+       ,0.0646, 0.3572, 0.8664
+       ,0.0482, 0.3651, 0.8722
+       ,0.0329, 0.3724, 0.8765
+       ,0.0213, 0.3792, 0.8796
+       ,0.0136, 0.3853, 0.8815
+       ,0.0086, 0.3911, 0.8827
+       ,0.0060, 0.3965, 0.8833
+       ,0.0051, 0.4017, 0.8834
+       ,0.0054, 0.4066, 0.8831
+       ,0.0067, 0.4113, 0.8825
+       ,0.0089, 0.4159, 0.8816
+       ,0.0116, 0.4203, 0.8805
+       ,0.0148, 0.4246, 0.8793
+       ,0.0184, 0.4288, 0.8779
+       ,0.0223, 0.4329, 0.8763
+       ,0.0264, 0.4370, 0.8747
+       ,0.0306, 0.4410, 0.8729
+       ,0.0349, 0.4449, 0.8711
+       ,0.0394, 0.4488, 0.8692
+       ,0.0437, 0.4526, 0.8672
+       ,0.0477, 0.4564, 0.8652
+       ,0.0514, 0.4602, 0.8632
+       ,0.0549, 0.4640, 0.8611
+       ,0.0582, 0.4677, 0.8589
+       ,0.0612, 0.4714, 0.8568
+       ,0.0640, 0.4751, 0.8546
+       ,0.0666, 0.4788, 0.8525
+       ,0.0689, 0.4825, 0.8503
+       ,0.0710, 0.4862, 0.8481
+       ,0.0729, 0.4899, 0.8460
+       ,0.0746, 0.4937, 0.8439
+       ,0.0761, 0.4974, 0.8418
+       ,0.0773, 0.5012, 0.8398
+       ,0.0782, 0.5051, 0.8378
+       ,0.0789, 0.5089, 0.8359
+       ,0.0794, 0.5129, 0.8341
+       ,0.0795, 0.5169, 0.8324
+       ,0.0793, 0.5210, 0.8308
+       ,0.0788, 0.5251, 0.8293
+       ,0.0778, 0.5295, 0.8280
+       ,0.0764, 0.5339, 0.8270
+       ,0.0746, 0.5384, 0.8261
+       ,0.0724, 0.5431, 0.8253
+       ,0.0698, 0.5479, 0.8247
+       ,0.0668, 0.5527, 0.8243
+       ,0.0636, 0.5577, 0.8239
+       ,0.0600, 0.5627, 0.8237
+       ,0.0562, 0.5677, 0.8234
+       ,0.0523, 0.5727, 0.8231
+       ,0.0484, 0.5777, 0.8228
+       ,0.0445, 0.5826, 0.8223
+       ,0.0408, 0.5874, 0.8217
+       ,0.0372, 0.5922, 0.8209
+       ,0.0342, 0.5968, 0.8198
+       ,0.0317, 0.6012, 0.8186
+       ,0.0296, 0.6055, 0.8171
+       ,0.0279, 0.6097, 0.8154
+       ,0.0265, 0.6137, 0.8135
+       ,0.0255, 0.6176, 0.8114
+       ,0.0248, 0.6214, 0.8091
+       ,0.0243, 0.6250, 0.8066
+       ,0.0239, 0.6285, 0.8039
+       ,0.0237, 0.6319, 0.8010
+       ,0.0235, 0.6352, 0.7980
+       ,0.0233, 0.6384, 0.7948
+       ,0.0231, 0.6415, 0.7916
+       ,0.0230, 0.6445, 0.7881
+       ,0.0229, 0.6474, 0.7846
+       ,0.0227, 0.6503, 0.7810
+       ,0.0227, 0.6531, 0.7773
+       ,0.0232, 0.6558, 0.7735
+       ,0.0238, 0.6585, 0.7696
+       ,0.0246, 0.6611, 0.7656
+       ,0.0263, 0.6637, 0.7615
+       ,0.0282, 0.6663, 0.7574
+       ,0.0306, 0.6688, 0.7532
+       ,0.0338, 0.6712, 0.7490
+       ,0.0373, 0.6737, 0.7446
+       ,0.0418, 0.6761, 0.7402
+       ,0.0467, 0.6784, 0.7358
+       ,0.0516, 0.6808, 0.7313
+       ,0.0574, 0.6831, 0.7267
+       ,0.0629, 0.6854, 0.7221
+       ,0.0692, 0.6877, 0.7173
+       ,0.0755, 0.6899, 0.7126
+       ,0.0820, 0.6921, 0.7078
+       ,0.0889, 0.6943, 0.7029
+       ,0.0956, 0.6965, 0.6979
+       ,0.1031, 0.6986, 0.6929
+       ,0.1104, 0.7007, 0.6878
+       ,0.1180, 0.7028, 0.6827
+       ,0.1258, 0.7049, 0.6775
+       ,0.1335, 0.7069, 0.6723
+       ,0.1418, 0.7089, 0.6669
+       ,0.1499, 0.7109, 0.6616
+       ,0.1585, 0.7129, 0.6561
+       ,0.1671, 0.7148, 0.6507
+       ,0.1758, 0.7168, 0.6451
+       ,0.1849, 0.7186, 0.6395
+       ,0.1938, 0.7205, 0.6338
+       ,0.2033, 0.7223, 0.6281
+       ,0.2128, 0.7241, 0.6223
+       ,0.2224, 0.7259, 0.6165
+       ,0.2324, 0.7275, 0.6107
+       ,0.2423, 0.7292, 0.6048
+       ,0.2527, 0.7308, 0.5988
+       ,0.2631, 0.7324, 0.5929
+       ,0.2735, 0.7339, 0.5869
+       ,0.2845, 0.7354, 0.5809
+       ,0.2953, 0.7368, 0.5749
+       ,0.3064, 0.7381, 0.5689
+       ,0.3177, 0.7394, 0.5630
+       ,0.3289, 0.7406, 0.5570
+       ,0.3405, 0.7417, 0.5512
+       ,0.3520, 0.7428, 0.5453
+       ,0.3635, 0.7438, 0.5396
+       ,0.3753, 0.7446, 0.5339
+       ,0.3869, 0.7454, 0.5283
+       ,0.3986, 0.7461, 0.5229
+       ,0.4103, 0.7467, 0.5175
+       ,0.4218, 0.7473, 0.5123
+       ,0.4334, 0.7477, 0.5072
+       ,0.4447, 0.7482, 0.5021
+       ,0.4561, 0.7485, 0.4972
+       ,0.4672, 0.7487, 0.4924
+       ,0.4783, 0.7489, 0.4877
+       ,0.4892, 0.7491, 0.4831
+       ,0.5000, 0.7491, 0.4786
+       ,0.5106, 0.7492, 0.4741
+       ,0.5212, 0.7492, 0.4698
+       ,0.5315, 0.7491, 0.4655
+       ,0.5418, 0.7490, 0.4613
+       ,0.5519, 0.7489, 0.4571
+       ,0.5619, 0.7487, 0.4531
+       ,0.5718, 0.7485, 0.4490
+       ,0.5816, 0.7482, 0.4451
+       ,0.5913, 0.7479, 0.4412
+       ,0.6009, 0.7476, 0.4374
+       ,0.6103, 0.7473, 0.4335
+       ,0.6197, 0.7469, 0.4298
+       ,0.6290, 0.7465, 0.4261
+       ,0.6382, 0.7460, 0.4224
+       ,0.6473, 0.7456, 0.4188
+       ,0.6564, 0.7451, 0.4152
+       ,0.6653, 0.7446, 0.4116
+       ,0.6742, 0.7441, 0.4081
+       ,0.6830, 0.7435, 0.4046
+       ,0.6918, 0.7430, 0.4011
+       ,0.7004, 0.7424, 0.3976
+       ,0.7091, 0.7418, 0.3942
+       ,0.7176, 0.7412, 0.3908
+       ,0.7261, 0.7405, 0.3874
+       ,0.7346, 0.7399, 0.3840
+       ,0.7430, 0.7392, 0.3806
+       ,0.7513, 0.7385, 0.3773
+       ,0.7596, 0.7378, 0.3739
+       ,0.7679, 0.7372, 0.3706
+       ,0.7761, 0.7364, 0.3673
+       ,0.7843, 0.7357, 0.3639
+       ,0.7924, 0.7350, 0.3606
+       ,0.8005, 0.7343, 0.3573
+       ,0.8085, 0.7336, 0.3539
+       ,0.8166, 0.7329, 0.3506
+       ,0.8246, 0.7322, 0.3472
+       ,0.8325, 0.7315, 0.3438
+       ,0.8405, 0.7308, 0.3404
+       ,0.8484, 0.7301, 0.3370
+       ,0.8563, 0.7294, 0.3336
+       ,0.8642, 0.7288, 0.3300
+       ,0.8720, 0.7282, 0.3265
+       ,0.8798, 0.7276, 0.3229
+       ,0.8877, 0.7271, 0.3193
+       ,0.8954, 0.7266, 0.3156
+       ,0.9032, 0.7262, 0.3117
+       ,0.9110, 0.7259, 0.3078
+       ,0.9187, 0.7256, 0.3038
+       ,0.9264, 0.7256, 0.2996
+       ,0.9341, 0.7256, 0.2953
+       ,0.9417, 0.7259, 0.2907
+       ,0.9493, 0.7264, 0.2859
+       ,0.9567, 0.7273, 0.2808
+       ,0.9639, 0.7285, 0.2754
+       ,0.9708, 0.7303, 0.2696
+       ,0.9773, 0.7326, 0.2634
+       ,0.9831, 0.7355, 0.2570
+       ,0.9882, 0.7390, 0.2504
+       ,0.9922, 0.7431, 0.2437
+       ,0.9952, 0.7476, 0.2373
+       ,0.9973, 0.7524, 0.2310
+       ,0.9986, 0.7573, 0.2251
+       ,0.9991, 0.7624, 0.2195
+       ,0.9990, 0.7675, 0.2141
+       ,0.9985, 0.7726, 0.2090
+       ,0.9976, 0.7778, 0.2042
+       ,0.9964, 0.7829, 0.1995
+       ,0.9950, 0.7880, 0.1949
+       ,0.9933, 0.7931, 0.1905
+       ,0.9914, 0.7981, 0.1863
+       ,0.9894, 0.8032, 0.1821
+       ,0.9873, 0.8083, 0.1780
+       ,0.9851, 0.8133, 0.1740
+       ,0.9828, 0.8184, 0.1700
+       ,0.9805, 0.8235, 0.1661
+       ,0.9782, 0.8286, 0.1622
+       ,0.9759, 0.8337, 0.1583
+       ,0.9736, 0.8389, 0.1544
+       ,0.9713, 0.8441, 0.1505
+       ,0.9692, 0.8494, 0.1465
+       ,0.9672, 0.8548, 0.1425
+       ,0.9654, 0.8603, 0.1385
+       ,0.9638, 0.8659, 0.1343
+       ,0.9623, 0.8716, 0.1301
+       ,0.9611, 0.8774, 0.1258
+       ,0.9600, 0.8834, 0.1215
+       ,0.9593, 0.8895, 0.1171
+       ,0.9588, 0.8958, 0.1126
+       ,0.9586, 0.9022, 0.1082
+       ,0.9587, 0.9088, 0.1036
+       ,0.9591, 0.9155, 0.0990
+       ,0.9599, 0.9225, 0.0944
+       ,0.9610, 0.9296, 0.0897
+       ,0.9624, 0.9368, 0.0850
+       ,0.9641, 0.9443, 0.0802
+       ,0.9662, 0.9518, 0.0753
+       ,0.9685, 0.9595, 0.0703
+       ,0.9710, 0.9673, 0.0651
+       ,0.9736, 0.9752, 0.0597
+       ,0.9763, 0.9831, 0.0538]
+
+--------------------------------------------------------------------------------
+-- Helpers
+
+colors :: Text -> Vector PixelRGB8
+colors = V.fromList . map (toColor . map (fromIntegral . digitToInt) . T.unpack) . T.chunksOf 6
+  where
+    toColor [r1,r2,g1,g2,b1,b2] =
+      PixelRGB8 (r1 `shiftL` 4 + r2) (g1 `shiftL` 4 + g2) (b1 `shiftL` 4+b2)
+
+ramp :: Vector PixelRGB8 -> Double -> PixelRGB8
+ramp v = \t -> v V.! (max 0 $ min (len-1) $ round $ t * (len'-1))
+  where
+    len = V.length v
+    len' = fromIntegral len
diff --git a/src/Reanimate/ColorSpace.hs b/src/Reanimate/ColorSpace.hs
new file mode 100644
--- /dev/null
+++ b/src/Reanimate/ColorSpace.hs
@@ -0,0 +1,72 @@
+module Reanimate.ColorSpace where
+
+import qualified Data.ByteString.Lazy as BS
+import           Data.Csv
+import           Data.Map             (Map)
+import qualified Data.Map             as Map
+import           Data.Maybe
+import qualified Data.Vector          as V
+import Data.Colour.CIE
+import Data.Colour.CIE.Illuminant
+import           Paths_reanimate
+import           System.IO.Unsafe
+
+import           Reanimate.Svg
+import           Graphics.SvgTree (Number(..), Tree)
+
+type Nanometer = Integer
+
+
+lightXYZCoordinates :: Map Nanometer (Double, Double, Double)
+lightXYZCoordinates = unsafePerformIO $ do
+  dat <- BS.readFile =<< getDataFileName "data/CIExyz.csv"
+  case decode NoHeader dat of
+    Left err -> error err
+    Right vec -> return $ Map.fromList
+      [ (nm, (x,y,z)) | (nm,x,y,z) <- V.toList vec, nm <= 700 ]
+
+renderXYZCoordinates :: Tree
+renderXYZCoordinates =
+  withFillOpacity 0 $
+  mkLinePath $
+  [ (x, 1-y)
+  | (ang, (x,y,_z)) <- Map.toList lightXYZCoordinates
+  ]
+
+lightLABCoordinates :: Map Nanometer (Double, Double, Double)
+lightLABCoordinates = Map.map fn lightXYZCoordinates
+  where
+    fn (x,y,z) = cieLABView d65 (cieXYZ x y z)
+
+renderLABCoordinates :: Tree
+renderLABCoordinates =
+  withFillOpacity 0 $
+  mkLinePath $
+  [ (a/350, (1-b)/150)
+  | (ang, (_l,a,b)) <- Map.toList lightLABCoordinates
+  ]
+
+-- (Long, Medium, Short)
+coneSensitivity :: Map Nanometer (Double, Double, Double)
+coneSensitivity = unsafePerformIO $ do
+  dat <- BS.readFile =<< getDataFileName "data/cone_sensitivity_lms.csv"
+  case decode NoHeader dat of
+    Left err -> error err
+    Right vec -> return $ Map.fromList
+      [ (nm, (l,m,fromMaybe 0 s)) | (nm,l,m,s) <- V.toList vec ]
+
+renderSensitivity :: Tree
+renderSensitivity = mkGroup
+  [ withStrokeColor "blue"  $ draw (\(l,m,s) -> s)
+  , withStrokeColor "green" $ draw (\(l,m,s) -> m)
+  , withStrokeColor "red"   $ draw (\(l,m,s) -> l)
+  ]
+  where
+    draw fn = withFillOpacity 0 $ mkLinePath
+      [ (fromIntegral (ang-minAng) / maxAng * width, fn val * height)
+      | (ang, val) <- Map.toList coneSensitivity
+      ]
+    width = 100
+    height = -50
+    maxAng = fromIntegral $ fst (Map.findMax coneSensitivity) - minAng
+    minAng = fst (Map.findMin coneSensitivity)
diff --git a/src/Reanimate/Driver.hs b/src/Reanimate/Driver.hs
--- a/src/Reanimate/Driver.hs
+++ b/src/Reanimate/Driver.hs
@@ -1,25 +1,35 @@
+{-# LANGUAGE ScopedTypeVariables #-}
 module Reanimate.Driver ( reanimate ) where
 
-import           Control.Concurrent (MVar, forkIO, killThread, modifyMVar_,
-                                     newEmptyMVar, putMVar)
-import           Control.Exception  (finally)
-import           Control.Monad.Fix  (fix)
-import qualified Data.Text          as T
+import           Control.Concurrent           (MVar, forkIO, forkOS, killThread,
+                                               modifyMVar_, newEmptyMVar,
+                                               putMVar, takeMVar)
+import           Control.Exception            (SomeException, finally, handle)
+import           Control.Monad
+import           Control.Monad.Fix            (fix)
+import           Data.Maybe
+import qualified Data.Text                    as T
+import qualified Data.Text.Read               as T
+import           Data.Version
+import           GHC.Environment              (getFullArgs)
 import           Network.WebSockets
-import           System.Directory   (findFile, listDirectory)
-import           System.Environment (getArgs, getProgName)
+import           Paths_reanimate
+import           Reanimate.Misc               (runCmdLazy, runCmd_)
+import           Reanimate.Monad              (Animation)
+import           Reanimate.Render             (render, renderSnippets,
+                                               renderSvgs)
+import           System.Directory             (doesFileExist, findExecutable,
+                                               findFile, listDirectory, withCurrentDirectory)
+import           System.Environment           (getArgs, getProgName)
+import           System.Exit
 import           System.FilePath
 import           System.FSNotify
-import           System.IO          (BufferMode (..), hPutStrLn, hSetBuffering,
-                                     stderr, stdin)
-
-import           Data.Maybe
-import           Paths_reanimate
-import           Reanimate.Misc     (runCmdLazy, runCmd_, withTempDir,
-                                     withTempFile)
-import           Reanimate.Monad    (Animation)
-import           Reanimate.Render   (renderSvgs, render)
-import           Web.Browser        (openBrowser)
+import           System.IO
+import           System.IO.Temp
+import           Text.ParserCombinators.ReadP
+import qualified Text.PrettyPrint.ANSI.Leijen as Doc
+import           Text.Printf
+import           Web.Browser                  (openBrowser)
 
 opts = defaultConnectionOptions
   { connectionCompressionOptions = PermessageDeflateCompression defaultPermessageDeflate }
@@ -31,60 +41,75 @@
   hSetBuffering stdin NoBuffering
   case args of
     ["once"] -> renderSvgs animation
+    ["snippets"] -> renderSnippets animation
+    ["check"] -> checkEnvironment
     ["render", target] ->
       render animation target
-    _ -> withTempDir $ \tmpDir -> do
+    _ -> do
+      self <- findOwnSource
       url <- getDataFileName "viewer/build/index.html"
       putStrLn "Opening browser..."
       bSucc <- openBrowser url
       if bSucc
           then putStrLn "Browser opened."
           else hPutStrLn stderr $ "Failed to open browser. Manually visit: " ++ url
+      putStrLn "Listening..."
       runServerWith "127.0.0.1" 9161 opts $ \pending -> do
-        putStrLn "Server pending..."
-        prog <- getProgName
-        lst <- listDirectory "."
-        mbSelf <- findFile ("." : lst) prog
-        blocker <- newEmptyMVar :: IO (MVar ())
-        case mbSelf of
-          Nothing -> do
-            hPutStrLn stderr "Failed to find own source code."
-          Just self -> do
-            conn <- acceptRequest pending
-            slave <- newEmptyMVar
-            let handler = modifyMVar_ slave $ \tid -> do
-                  sendTextData conn (T.pack "Compiling")
-                  putStrLn "Killing and respawning..."
-                  killThread tid
-                  tid <- forkIO $ withTempFile ".exe" $ \tmpExecutable -> do
-                    ret <- runCmd_ "stack" $ ["ghc", "--"] ++ ghcOptions tmpDir ++ [self, "-o", tmpExecutable]
-                    case ret of
-                      Left err ->
-                        sendTextData conn $ T.pack $ "Error" ++ unlines (drop 3 (lines err))
-                      Right{} -> do
-                        getFrame <- runCmdLazy tmpExecutable ["once", "+RTS", "-N", "-M200M", "-RTS"]
-                        flip fix [] $ \loop acc -> do
-                          frame <- getFrame
-                          case frame of
-                            Left "" -> do
-                              sendTextData conn (T.pack "Done")
-                              -- insertCache msg (reverse acc)
-                            Left err -> do
-                              -- _ <- getChanContents queue
-                              sendTextData conn $ T.pack $ "Error" ++ err
-                            Right frame -> do
-                              sendTextData conn frame
-                              loop (frame : acc)
-                  return tid
-            putStrLn "Found self. Listening..."
-            stop <- watchFile watch self handler
-            putMVar slave =<< forkIO (return ())
-            let loop = do
-                  fps <- receiveData conn :: IO T.Text
-                  handler
-                  loop
-            loop `finally` stop
+        putStrLn "New connection received."
+        conn <- acceptRequest pending
+        slave <- newEmptyMVar
+        let handler = modifyMVar_ slave $ \tid -> do
 
+              putStrLn "Reloading code..."
+              killThread tid
+              tid <- forkOS $ slaveHandler conn self
+              return tid
+            killSlave = do
+              tid <- takeMVar slave
+              killThread tid
+        stop <- watchFile watch self handler
+        putMVar slave =<< forkIO (return ())
+        let loop = do
+              fps <- receiveData conn :: IO T.Text
+              handler
+              loop
+        loop `finally` (stop >> killSlave)
+
+slaveHandler conn self =
+  withCurrentDirectory (takeDirectory self) $
+  withSystemTempDirectory "reanimate" $ \tmpDir ->
+  withTempFile tmpDir "reanimate.exe" $ \tmpExecutable handle -> do
+    hClose handle
+    sendTextData conn (T.pack "Compiling")
+    ret <- runCmd_ "stack" $ ["ghc", "--"] ++ ghcOptions tmpDir ++ [takeFileName self, "-o", tmpExecutable]
+    case ret of
+      Left err ->
+        sendTextData conn $ T.pack $ "Error" ++ unlines (drop 3 (lines err))
+      Right{} -> do
+        getFrame <- runCmdLazy tmpExecutable ["once", "+RTS", "-N", "-M1G", "-RTS"]
+        (frameCount,_) <- expectFrame =<< getFrame
+        sendTextData conn (T.pack $ show frameCount)
+        fix $ \loop -> do
+          (frameIdx, frame) <- expectFrame =<< getFrame
+          sendTextData conn (T.pack $ show frameIdx)
+          sendTextData conn frame
+          loop
+  where
+    expectFrame (Left "") = do
+      sendTextData conn (T.pack "Done")
+      exitWith ExitSuccess
+    expectFrame (Left err) = do
+      sendTextData conn $ T.pack $ "Error" ++ err
+      exitWith (ExitFailure 1)
+    expectFrame (Right frame) =
+      case T.decimal frame of
+        Left err -> do
+          hPutStrLn stderr (T.unpack frame)
+          hPutStrLn stderr $ "expectFrame: " ++ err
+          sendTextData conn $ T.pack $ "Error" ++ err
+          exitWith (ExitFailure 1)
+        Right (frameNumber, rest) -> pure (frameNumber, rest)
+
 watchFile watch file action = watchDir watch (takeDirectory file) check (const action)
   where
     check event = takeFileName (eventPath event) == takeFileName file
@@ -93,3 +118,157 @@
 ghcOptions tmpDir =
     ["-rtsopts", "--make", "-threaded", "-O2"] ++
     ["-odir", tmpDir, "-hidir", tmpDir]
+
+-- FIXME: Gracefully disable code reloading if source is missing.
+findOwnSource :: IO FilePath
+findOwnSource = do
+  fullArgs <- getFullArgs
+  let stackSource = last fullArgs
+  exist <- doesFileExist stackSource
+  if exist
+    then return stackSource
+    else do
+      prog <- getProgName
+      lst <- listDirectory "."
+      mbSelf <- findFile ("." : lst) prog
+      case mbSelf of
+        Nothing -> do
+          hPutStrLn stderr "Failed to find own source code."
+          exitFailure
+        Just self -> pure self
+
+
+--------------------------------------------------------------------------
+-- Check environment
+
+checkEnvironment :: IO ()
+checkEnvironment = do
+    putStrLn "reanimate checks:"
+    runCheck "Has ffmpeg" hasFFmpeg
+    runCheck "Has LaTeX" hasLaTeX
+    runCheck "Has XeLaTeX" hasXeLaTeX
+    runCheck "Has dvisvgm" hasDvisvgm
+    runCheck "Has povray" hasPovray
+    forM_ latexPackages $ \pkg ->
+      runCheck ("Has LaTeX package '"++ pkg ++ "'") $ hasTeXPackage "latex" $
+        "{"++pkg++"}"
+    forM_ xelatexPackages $ \pkg ->
+      runCheck ("Has XeLaTeX package '"++ pkg ++ "'") $ hasTeXPackage "xelatex" $
+        "{"++pkg++"}"
+  where
+    latexPackages =
+      ["babel"
+      ,"amsmath"
+      ,"amssymb"
+      ,"dsfont"
+      ,"setspace"
+      ,"relsize"
+      ,"textcomp"
+      ,"mathrsfs"
+      ,"calligra"
+      ,"wasysym"
+      ,"ragged2e"
+      ,"physics"
+      ,"xcolor"
+      ,"textcomp"
+      ,"xfrac"
+      ,"microtype"]
+    xelatexPackages =
+      ["ctex"]
+    runCheck msg fn = do
+      printf "  %-35s" (msg ++ ":")
+      val <- fn
+      case val of
+        Left err -> print $ Doc.red $ Doc.text err
+        Right ok -> print $ Doc.green $ Doc.text ok
+
+-- latex, dvisvgm, xelatex
+
+hasLaTeX :: IO (Either String String)
+hasLaTeX = hasProgram "latex"
+
+hasXeLaTeX :: IO (Either String String)
+hasXeLaTeX = hasProgram "xelatex"
+
+hasDvisvgm :: IO (Either String String)
+hasDvisvgm = hasProgram "dvisvgm"
+
+hasPovray :: IO (Either String String)
+hasPovray = hasProgram "povray"
+
+hasFFmpeg :: IO (Either String String)
+hasFFmpeg = do
+  mbVersion <- ffmpegVersion
+  return $ case mbVersion of
+    Nothing                   -> Left "no"
+    Just vs | vs < minVersion -> Left "too old"
+            | otherwise       -> Right (showVersion vs)
+  where
+    minVersion = Version [4,1,3] []
+ffmpegVersion :: IO (Maybe Version)
+ffmpegVersion = do
+  mbPath <- findExecutable "ffmpeg"
+  case mbPath of
+    Nothing   -> return Nothing
+    Just path -> do
+      ret <- runCmd_ path ["-version"]
+      case ret of
+        Left{} -> return Nothing
+        Right out ->
+          case map (take 3 . words) $ take 1 $ lines out of
+            [["ffmpeg", "version", vs]] ->
+              return $ parseVS vs
+            _ -> return Nothing
+  where
+    parseVS vs = listToMaybe
+      [ v | (v, "") <- readP_to_S parseVersion vs ]
+
+
+hasTeXPackage :: FilePath -> String -> IO (Either String String)
+hasTeXPackage exec pkg = handle (\(e::SomeException) -> return $ Left "n/a") $
+    withSystemTempDirectory "reanimate" $ \tmp_dir -> withTempFile tmp_dir "test.tex" $ \tex_file tex_handle -> do
+      hPutStr tex_handle tex_document
+      hPutStr tex_handle $ "\\usepackage" ++ pkg ++ "\n"
+      hPutStr tex_handle "\\begin{document}\n"
+      hPutStr tex_handle "blah\n"
+      hPutStr tex_handle tex_epilogue
+      hClose tex_handle
+      ret <- runCmd_ exec ["-interaction=batchmode", "-halt-on-error", "-output-directory="++tmp_dir, tex_file]
+      return $ case ret of
+        Right{} -> Right "OK"
+        Left{}  -> Left "missing"
+  where
+    tex_document = "\\documentclass[preview]{standalone}\n"
+    tex_xelatex =
+      "\\usepackage[UTF8]{ctex}\n"
+
+    tex_prologue =
+      "\\usepackage[english]{babel}\n\
+      \\\usepackage{amsmath}\n\
+      \\\usepackage{amssymb}\n\
+      \\\usepackage{dsfont}\n\
+      \\\usepackage{setspace}\n\
+      \\\usepackage{relsize}\n\
+      \\\usepackage{textcomp}\n\
+      \\\usepackage{mathrsfs}\n\
+      \\\usepackage{calligra}\n\
+      \\\usepackage{wasysym}\n\
+      \\\usepackage{ragged2e}\n\
+      \\\usepackage{physics}\n\
+      \\\usepackage{xcolor}\n\
+      \\\usepackage{textcomp}\n\
+      \\\usepackage{xfrac}\n\
+      \\\usepackage{microtype}\n\
+      \\\linespread{1}\n\
+      \\\begin{document}\n"
+
+    tex_epilogue =
+      "\n\
+      \\\end{document}"
+
+hasProgram :: String -> IO (Either String String)
+hasProgram exec = do
+  mbPath <- findExecutable exec
+  return $ case mbPath of
+    Nothing   -> Left $ "'" ++ exec ++ "'' not found"
+    Just path -> Right path
diff --git a/src/Reanimate/Examples.hs b/src/Reanimate/Examples.hs
--- a/src/Reanimate/Examples.hs
+++ b/src/Reanimate/Examples.hs
@@ -22,6 +22,7 @@
 import           Reanimate.LaTeX
 import           Reanimate.Monad
 import           Reanimate.Svg
+import           Reanimate.Signal
 
 import qualified Data.Colour.Palette.BrewerSet as D
 import qualified Diagrams.Backend.SVG          as D
@@ -490,25 +491,9 @@
 -}
 
 
-latex_draw :: Animation
-latex_draw =
-    bg `sim` (autoReverse $ drawText `andThen` fillText)
-  where
-    bg = mkAnimation 0 $ emit (mkBackground "black")
-    msg = "\\sum_{k=1}^\\infty {1 \\over k^2} = {\\pi^2 \\over 6}"
-    glyphs = center $ latexAlign msg
-    fillText = mkAnimation 1 $ do
-      s <- signal 0 1
-      emit $ scale 5 $ withFillColor "white" $ withFillOpacity s glyphs
-    drawText = mkAnimation 2 $ do
-      s <- signal 0 1
-      emit $ scale 5 $
-        withStrokeColor "white" $ withFillOpacity 0 $ withStrokeWidth (Num 0.1) $
-          partialSvg s glyphs
-
 morph_wave :: Animation
 morph_wave = autoReverse $ mkAnimation 2.5 $ do
-    morph <- signal 0 1
+    morph <- getSignal signalLinear
     emit $ mkBackground "black"
     emit $ withStrokeColor "white" $ translate (-320/2) (-180/2) $ mkGroup
       [ translate 30 50  $ mkLinePath wave1
@@ -523,7 +508,7 @@
 
 morph_wave_circle :: Animation
 morph_wave_circle = autoReverse $ mkAnimation 2.5 $ do
-    idx <- signal 0 1
+    idx <- getSignal signalLinear
     emit $ mkBackground "black"
     emit $ withStrokeColor "white" $ translate (-320/2) (-180/2) $ mkGroup
       [ translate 30 90 $ mkLinePath $ morphPath circle wave1 idx
@@ -551,110 +536,12 @@
 
 progressMeter :: Animation
 progressMeter = mkAnimation 3 $ do
-  h <- signal 0 100
+  h <- getSignal $ signalFromTo 0 100 signalLinear
   emit $ center $ mkGroup
     [ withStrokeColor "white" $ withStrokeWidth (Num 2) $ withFillOpacity 0 $
-        mkRect (Num 0, Num 0) (Num 30) (Num 100)
+        mkRect (Num 30) (Num 100)
     , withFillColor "white" $
-        mkRect (Num 0, Num 0) (Num 30) (Num h) ]
-
-
-bbox :: Animation
-bbox = bg `sim`
-    mapA (translate (-50) 0) bbox1 `sim`
-    mapA (translate 50 0) bbox2
-  where
-    bg = mkAnimation 0 $ emit $ mkBackground "black"
-
-bbox1 :: Animation
-bbox1 = mkAnimation 5 $ do
-    s <- signal 0 1
-    emit $ mkGroup
-      [ mkBoundingBox $ rotate (360*s) svg
-      , withFillColor "white" $ rotate (360*s) svg ]
-  where
-    svg = scale 3 $ center $ latexAlign "\\sum_{k=1}^\\infty"
-
-bbox2 :: Animation
-bbox2 = autoReverse $ mkAnimation 2.5 $ do
-  s <- signal 0 1
-  emit $ mkGroup
-    [ mkBoundingBox $ partialSvg s heartShape
-    , withStrokeColor "white" $ withFillOpacity 0 $ partialSvg s heartShape ]
-
-mkBoundingBox :: Tree -> Tree
-mkBoundingBox svg = withStrokeColor "red" $ withFillOpacity 0 $
-    mkRect (S.Num x, S.Num y) (S.Num w) (S.Num h)
-  where
-    (x, y, w, h) = boundingBox svg
-
-heartShape =
-    center $ rotateAroundCenter 225 $ mkPathString
-      "M0.0,40.0 v-40.0 h40.0\
-      \a20.0 20.0 90.0 0 1 0.0,40.0\
-      \a20.0 20.0 90.0 0 1 -40.0,0.0 Z"
-
-latex_color :: Animation
-latex_color = mkAnimation 1 $ do
-    emit $ mkBackground "black"
-    emit $ withStrokeWidth (Num 0.2) $
-      withStrokeColor "white" $
-      withSubglyphs [0] (withFillColor "blue") $
-      withSubglyphs [1] (withFillColor "yellow") $
-      withSubglyphs [2] (withFillColor "green") $
-      withSubglyphs [3] (withFillColor "red") $
-      withSubglyphs [4] (withFillColor "darkslategrey") $
-      svg
-  where
-    svg = scale 10 $ center $ latex "\\LaTeX"
-
-latex_basic :: Animation
-latex_basic = autoReverse $ mkAnimation 2 $ do
-    s <- signal 0 1
-    emit $ mkGroup
-      [ mkBackground "black"
-      , withStrokeColor "white" $ withFillOpacity 0 $ withStrokeWidth (Num 0.1) text
-      , withFillColor "white" $ withFillOpacity s text ]
-  where
-    text = scale 4 $ center $ latexAlign
-      "\\sum_{k=1}^\\infty {1 \\over k^2} = {\\pi^2 \\over 6}"
-
-valentine :: Animation
-valentine =
-    all_red `before`
-    ( background `sim`
-      (backgroundDelay `before`
-       foldr1 sim [ pause p `before` fallingLove x | (p, x) <- falling ]
-      ) `sim`
-      (heart_ani `before` heart_disappear) `sim`
-      (pause 5 `before` message ai)
-    )
-  where
-    falling = [(6.4, 0.09), (4.9, 0.12), (4.5, 0.88), (0.3, 0.43), (5.3, 0.93)
-              ,(0.1, 0.80), (1.1, 0.39), (2.3, 0.21), (2.9, 0.77), (3.4, 0.46)
-              ,(6.2, 0.19), (5.9, 0.53), (3.2, 0.14), (7.7, 0.99) ]
-    ai = center $ xelatex "爱"
-    all_red = mkAnimation 1 $ emit $ mkBackground "red"
-    background = mkAnimation 2 $ do
-      n <- round <$> signal 0 0xFF
-      emit $ mkBackgroundPixel $ PixelRGBA8 0xFF n n 0xFF
-    backgroundDelay = pause (duration background-1)
-    heart_ani = repeatAnimation 10 $ mkAnimation 1 $ do
-      n <- oscillate $ signalSCurve 2 0.9 1.1
-      mapF (scale n) $ drawHeart
-    heart_disappear = mkAnimation 3 $ do
-      n  <- signal 0.9 10
-      mapF (scale n) drawHeart
-    fallingLove xPos = mkAnimation 2 $ do
-      n <- signal (-100) 100
-      o <- oscillate $ signal (-1) 1
-      emit $ scale 2 $ withFillColor "red" $
-        translate ((xPos*2-1)*60) n $ rotate (45*o) ai
-    message txt = mkAnimation 1 $ do
-      o <- oscillate $ signal 0 1
-      n <- oscillate $ signalSCurve 2 0.9 1.1
-      emit $ scale n $ scale 2 $ withFillColor "white" $ withFillOpacity o txt
-    drawHeart = emit $ withFillColor "red" $ heartShape
+        mkRect (Num 30) (Num h) ]
 
 
 diaSize :: Animation
@@ -670,7 +557,7 @@
 
 wavyTree :: Animation
 wavyTree = mkAnimation 1 $ do
-    s <- oscillate $ signal 1 2
+    s <- oscillate $ getSignal $ signalFromTo 1 2 signalLinear
     emit $ mkBackground "white"
     emit $ translate (-320/2) (-180/2) (dSvg s)
   where
@@ -683,125 +570,3 @@
     tree s =
       D.lSystem gens (s/16 @@ turn) (D.symbols "F") rules
     rules = M.fromList [D.rule 'F' "FF-[->F+F+>F]+[+>F->F->F]"]
-
-tangentAndNormal :: Animation
-tangentAndNormal = mkAnimation 5 $ do
-    s <- oscillate $ signalSCurve 2 0 1
-    emit $ mkBackground "white"
-    emit $ translate (-320/2) (-180/2) $ renderDiagram $
-      withEnvelope (D.rect 320 180 :: SvgDiagram) $
-      D.scale 50 $ D.translate (V2 (-2) (-0.75)) $ dia s
-  where
-    dia param =
-        frame 0.5 $
-        strokeLocTrail spline
-        <> mconcat
-          [ tangentLine
-          , baselineText "tangent" # D.translate tangentVector
-          , normalLine
-          , topLeftText "normal" # D.translate (-normalVector)
-          , rightAngleSquare
-          ] # moveTo pt # D.fontSize large
-      where
-        pts = map p2 [(0,0), (1,1), (2,1), (3,0), (3.5,0)]
-
-        spline :: Located (Trail V2 Double)
-        spline = cubicSpline False pts
-
-        pt = atParam spline param
-        tangentVector ::  V2 Double
-        tangentVector = D.normalize $ tangentAtParam spline param
-        normalVector = D.normalize $ normalAtParam spline param
-
-        symmetricLine :: V2 Double -> SvgDiagram
-        symmetricLine v = fromOffsets [2 *^ v] # D.center
-        tangentLine :: SvgDiagram
-        tangentLine = symmetricLine tangentVector
-        normalLine = symmetricLine normalVector
-
-        rightAngleSquare :: SvgDiagram
-        rightAngleSquare = square 0.1 # alignBL # D.rotate (signedAngleBetween tangentVector unitX)
-
-
-drawSunflower :: Animation
-drawSunflower = mkAnimation 10 $ do
-    n <- signal 1 500
-    rot <- signal 0 45
-    emit $ mkBackground "black"
-    emit $ rotate rot $ translate (-320/2) (-180/2)
-      (dSvg $ round n)
-  where
-    cached = [ dSvg n | n <- [0..]]
-    dSvg n = renderDiagram $ withEnvelope (D.rect 320 180 :: SvgDiagram) $
-      D.scale 5 $ sunflower n
-
-    mkCoords :: [P2 Double]
-    mkCoords =[coord (fromIntegral i) | i <- [1..]]
-      where
-        coord m = p2 $ fromPolar (sqrt m) (2.4 * m)
-        fromPolar r theta = (r * cos theta, r * sin theta)
-
-    floret :: Double -> SvgDiagram
-    floret r = D.circle 0.6 # lw none # fc (colors !! n)
-      where
-        n = floor (1.4 * sqrt r) `mod` 10
-        colors = black : (reverse $ D.brewerSet D.YlOrBr 9)
-
-    sunflower :: Int ->  SvgDiagram
-    sunflower n = frame 4 $ position $ take n $ zip mkCoords florets
-      where
-        florets = [ floret (sqrt (fromIntegral i)) | i <- [1 ..]]
-
-mkFilter :: String -> [FilterElement] -> Filter
-mkFilter ident fe = defaultSvg & filterChildren .~ fe & attrId .~ Just ident
-
-gooEffect :: Animation
-gooEffect = mkAnimation 5 $ do
-  s <- oscillate $ signal 0 3
-  emit $ mkBackground "black"
-  emit $ FilterTree $ mkFilter "blur"
-    [FEGaussianBlur $ defaultSvg
-      & gaussianBlurStdDeviationX .~ Num dev
-      & filterResult .~ Just "blur"
-    ] & filterWidth .~ pure (Percent 3)
-      & filterX .~ pure (Percent (-1))
-      & filterHeight .~ pure (Percent 3)
-      & filterY .~ pure (Percent (-1))
-  emit $ FilterTree $ mkFilter "goo"
-    [FEGaussianBlur $ defaultSvg
-      & gaussianBlurStdDeviationX .~ Num dev
-      & filterResult .~ Just "blur"
-    ,FEColorMatrix $ defaultSvg
-      & colorMatrixType .~ Matrix
-      & colorMatrixValues .~ "1 0 0 0 0 \
-                             \0 1 0 0 0 \
-                             \0 0 1 0 0 \
-                             \0 0 0 " ++ show (sharpness*2) ++ " -" ++ show sharpness
-      & filterResult .~ pure "goo"
-    ,FEComposite $ defaultSvg
-      & compositeIn .~ pure SourceGraphic
-      & compositeIn2 .~ pure (SourceRef "goo")
-      & compositeOperator .~ CompositeAtop
-    ] & filterWidth .~ pure (Percent 3)
-      & filterX .~ pure (Percent (-1))
-      & filterHeight .~ pure (Percent 3)
-      & filterY .~ pure (Percent (-1))
-  emit $ translate 0 (-radius*2) $ withFillColor "red" $ mkGroup
-    [ translate (s*(-radius)) 0 circ
-    , translate (s*radius) 0 circ
-    ]
-  emit $ withFillColor "red" $ mkGroup
-    [ translate (s*(-radius)) 0 circ
-    , translate (s*radius) 0 circ
-    ]
-    & filterRef .~ pure (Ref "blur")
-  emit $ translate 0 (radius*2) $ withFillColor "red" $ set filterRef (pure $ Ref "goo")
-    $ mkGroup [ translate (s*(-radius)) 0 circ
-              , translate (s*radius) 0 circ ]
-  where
-    sharpness = 60
-    dev = 10
-    radius = 30
-    circ = CircleTree $ defaultSvg
-      & circleCenter .~ (Num 0, Num 0)
-      & circleRadius .~ Num radius
diff --git a/src/Reanimate/Interpolate.hs b/src/Reanimate/Interpolate.hs
new file mode 100644
--- /dev/null
+++ b/src/Reanimate/Interpolate.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE RecordWildCards #-}
+module Reanimate.Interpolate where
+
+import Codec.Picture
+import Data.Colour
+import Data.Colour.CIE
+import Data.Colour.CIE.Illuminant
+import Data.Colour.SRGB
+import Data.Colour.RGBSpace.HSV
+import Data.Colour.RGBSpace
+import Data.Fixed
+
+data ColorComponents = ColorComponents
+  { colorUnpack :: Colour Double -> (Double, Double, Double)
+  , colorPack :: Double -> Double -> Double -> Colour Double }
+
+rgbComponents :: ColorComponents
+rgbComponents = ColorComponents rgbUnpack sRGB
+  where
+    rgbUnpack :: Colour Double -> (Double, Double, Double)
+    rgbUnpack c =
+      case toSRGB c of
+        RGB r g b -> (r,g,b)
+
+hsvComponents :: ColorComponents
+hsvComponents = ColorComponents unpack pack
+  where
+    unpack = hsvView.toSRGB
+    pack a b c = uncurryRGB sRGB $ hsv a b c
+
+labComponents :: ColorComponents
+labComponents = ColorComponents unpack pack
+  where
+    unpack = cieLABView d65
+    pack = cieLAB d65
+
+xyzComponents :: ColorComponents
+xyzComponents = ColorComponents cieXYZView cieXYZ
+
+lchComponents :: ColorComponents
+lchComponents = ColorComponents unpack pack
+  where
+    toDeg,toRad :: Double -> Double
+    toRad deg = deg/180 * pi
+    toDeg rad = rad/pi * 180
+    unpack :: Colour Double -> (Double, Double, Double)
+    unpack color =
+      let (l,a,b) = cieLABView d65 color
+          c = sqrt (a*a + b*b)
+          h :: Double
+          h = (toDeg(atan2 b a) + 360) `mod'` 360
+          isZero = round (c*10000) == 0
+      in (l, c, if isZero then 0/0 else h)
+    pack l c h =
+      cieLAB d65 l (cos (toRad h) * c) (sin (toRad h) * c)
+
+interpolate :: ColorComponents -> Colour Double -> Colour Double -> (Double -> Colour Double)
+interpolate ColorComponents{..} from to = \d ->
+    colorPack (a1 + (a2-a1)*d) (b1 + (b2-b1)*d) (c1 + (c2-c1)*d)
+  where
+    (a1,b1,c1) = colorUnpack from
+    (a2,b2,c2) = colorUnpack to
+
+interpolateRGB8 :: ColorComponents -> PixelRGB8 -> PixelRGB8 -> (Double -> PixelRGB8)
+interpolateRGB8 comps from to = toRGB8 . interpolate comps (fromRGB8 from) (fromRGB8 to)
+
+toRGB8 :: Colour Double -> PixelRGB8
+toRGB8 c = PixelRGB8 r g b
+  where
+    RGB r g b = toSRGBBounded c
+
+fromRGB8 :: PixelRGB8 -> Colour Double
+fromRGB8 (PixelRGB8 r g b) = sRGB24 r g b
diff --git a/src/Reanimate/LaTeX.hs b/src/Reanimate/LaTeX.hs
--- a/src/Reanimate/LaTeX.hs
+++ b/src/Reanimate/LaTeX.hs
@@ -29,7 +29,7 @@
   ("% plain latex\n" <> tex)
 
 xelatex :: Text -> Tree
-xelatex tex = (unsafePerformIO . (cacheMem . cacheDiskSvg) latexToSVG)
+xelatex tex = (unsafePerformIO . (cacheMem . cacheDiskSvg) xelatexToSVG)
   ("% xelatex\n" <> tex)
 
 latexAlign :: Text -> Tree
diff --git a/src/Reanimate/Memo.hs b/src/Reanimate/Memo.hs
new file mode 100644
--- /dev/null
+++ b/src/Reanimate/Memo.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE RankNTypes, ExistentialQuantification, BangPatterns #-}
+module Reanimate.Memo
+  ( Key(..)
+  , memo
+  ) where
+
+import Control.Monad.ST
+import Data.STRef
+import System.Mem.StableName
+import Debug.Trace
+import System.IO.Unsafe
+import Control.Monad.ST.Unsafe
+import Data.IORef
+import qualified Data.Map as Map
+import Data.Dynamic
+import Data.Typeable
+import Data.Maybe
+
+data DynamicName = forall a. DynamicName !(StableName a) | forall a. (Eq a, Ord a, Typeable a) => DynamicKey a
+instance Eq DynamicName where
+  DynamicName a == DynamicName b = eqStableName a b
+  DynamicKey a == DynamicKey b =
+    case cast a of
+      Nothing -> False
+      Just a' -> a'==b
+  _ == _ = False
+
+instance Ord DynamicName where
+  DynamicName a `compare` DynamicName b =
+    hashStableName a `compare` hashStableName b
+  DynamicName{} `compare` _ = LT
+  DynamicKey a `compare` DynamicKey b =
+    case cast a of
+      Nothing -> typeOf a `compare` typeOf b
+      Just a' -> a' `compare` b
+  _ `compare` _ = GT
+
+data CacheMap = CacheMap !(Map.Map DynamicName CacheMap) !(Map.Map DynamicName Dynamic)
+
+emptyCacheMap :: CacheMap
+emptyCacheMap = CacheMap Map.empty Map.empty
+
+cacheMapLookup :: [DynamicName] -> CacheMap -> Maybe Dynamic
+cacheMapLookup [] _ = Nothing
+cacheMapLookup [k] (CacheMap _ vals) = Map.lookup k vals
+cacheMapLookup (k:ks) (CacheMap sub _) =
+  cacheMapLookup ks =<< Map.lookup k sub
+
+cacheMapInsert :: [DynamicName] -> Dynamic -> CacheMap -> CacheMap
+cacheMapInsert [] _ m = m
+cacheMapInsert [k] v (CacheMap sub vals) = CacheMap sub (Map.insert k v vals)
+cacheMapInsert (k:ks) v (CacheMap sub vals) =
+  CacheMap (Map.alter fn k sub) vals
+  where
+    fn = Just . cacheMapInsert ks v . fromMaybe emptyCacheMap
+
+{-# NOINLINE cacheMap #-}
+cacheMap :: IORef CacheMap
+cacheMap = unsafePerformIO (newIORef emptyCacheMap)
+
+data Key = forall a. Key !a | forall a. (Typeable a, Eq a, Ord a) => KeyPrim !a
+
+fromKey :: Key -> IO DynamicName
+fromKey (Key val) = DynamicName <$> makeStableName val
+fromKey (KeyPrim val) = pure (DynamicKey val)
+
+memo :: Typeable a => [Key] -> a -> a
+memo !k v = unsafePerformIO $ do
+  keys <- mapM fromKey k
+  atomicModifyIORef' cacheMap $ \m ->
+    case fromDynamic =<< cacheMapLookup keys m of
+      Just v' -> (m, v')
+      Nothing -> (cacheMapInsert keys (toDyn v) m, v)
diff --git a/src/Reanimate/Misc.hs b/src/Reanimate/Misc.hs
--- a/src/Reanimate/Misc.hs
+++ b/src/Reanimate/Misc.hs
@@ -69,7 +69,7 @@
 withTempDir :: (FilePath -> IO a) -> IO a
 withTempDir action = do
   dir <- getTemporaryDirectory
-  (path, handle) <- openTempFile dir "reanimate-XXXXXX"
+  (path, handle) <- openTempFile dir "reanimate"
   hClose handle
   removeFile path
   createDirectory (dir </> path)
@@ -78,6 +78,6 @@
 withTempFile :: String -> (FilePath -> IO a) -> IO a
 withTempFile ext action = do
   dir <- getTemporaryDirectory
-  (path, handle) <- openTempFile dir ("reanimate-XXXXXX" <.> ext)
+  (path, handle) <- openTempFile dir ("reanimate" <.> ext)
   hClose handle
   action path -- `finally` removeFile path
diff --git a/src/Reanimate/Monad.hs b/src/Reanimate/Monad.hs
--- a/src/Reanimate/Monad.hs
+++ b/src/Reanimate/Monad.hs
@@ -14,6 +14,7 @@
                                         Tree, Tree (..), xmlOfDocument, xmlOfTree)
 import           Graphics.SvgTree.Printer
 import           Reanimate.Svg
+import           Reanimate.Signal
 import           Text.XML.Light        (elContent)
 import           Text.XML.Light.Output
 
@@ -86,17 +87,8 @@
 andThen :: Animation -> Animation -> Animation
 andThen a b = a `sim` (pause (duration a) `before` b)
 
-signal :: Double -> Double -> Frame Double
-signal from to = Frame $ \d t -> pure $
-  from + (to-from)*(t/d)
-
-signalSCurve :: Double -> Double -> Double -> Frame Double
-signalSCurve steepness from to = do
-  s <- signal 0 1
-  let s' = if s < 0.5
-              then 0.5 * (2*s)**steepness
-              else 1-0.5 * (2 - 2*s)**steepness
-  pure $ from + (to-from)*s'
+getSignal :: Signal -> Frame Double
+getSignal s = Frame $ \d t -> pure $ s (t/d)
 
 frameAt :: Double -> Animation -> Tree
 frameAt t (Animation d (Frame f)) = mkGroup $ execState (f d (min d t)) id []
@@ -131,9 +123,23 @@
 pauseAtEnd :: Double -> Animation -> Animation
 pauseAtEnd p a = a `andThen` pause p
 
+pauseAtBeginning :: Double -> Animation -> Animation
+pauseAtBeginning d1 a =
+    Animation d1 (freezeFrame 0 a) `before` a
+
+pauseAround :: Double -> Double -> Animation -> Animation
+pauseAround start end = pauseAtEnd end . pauseAtBeginning start
+
+freezeFrame :: Double -> Animation -> Frame ()
+freezeFrame t (Animation d f) = Frame $ \_ _ -> unFrame f d t
+
 adjustSpeed :: Double -> Animation -> Animation
 adjustSpeed factor (Animation d fn) =
   Animation (d/factor) $ Frame $ \_dur t -> unFrame fn d (t*factor)
+
+setDuration :: Double -> Animation -> Animation
+setDuration newD (Animation d fn) =
+  Animation newD $ Frame $ \dur t -> unFrame fn dur t
 
 reverseAnimation :: Animation -> Animation
 reverseAnimation (Animation d fn) = Animation d $ Frame $ \_dur t ->
diff --git a/src/Reanimate/Povray.hs b/src/Reanimate/Povray.hs
new file mode 100644
--- /dev/null
+++ b/src/Reanimate/Povray.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+module Reanimate.Povray
+  ( povray
+  , povrayQuick
+  , povraySlow
+  ) where
+
+import           Codec.Picture.Png
+import           Control.Exception     (SomeException, handle)
+import qualified Data.ByteString       as B
+import qualified Data.ByteString.Lazy  as BL
+import           Data.IORef
+import           Data.Map              (Map)
+import qualified Data.Map              as Map
+import           Data.Monoid
+import           Reanimate.Cache
+import           Reanimate.Cache
+import           Reanimate.Memo
+import           Reanimate.Misc
+import           Reanimate.Raster
+import           Reanimate.Svg
+import           System.IO
+
+import           System.FilePath       (replaceExtension, takeFileName, (</>))
+import           System.IO.Unsafe      (unsafePerformIO)
+
+import           Control.Lens          (over, set, (%~), (&), (.~), (^.))
+import           Data.Text             (Text)
+import qualified Data.Text             as T
+import qualified Data.Text.IO          as T
+import           Graphics.SvgTree      (Document (..), Tree (..), defaultSvg,
+                                        elements, loadSvgFile, parseSvgFile,
+                                        xmlOfDocument)
+import           Text.XML.Light        (elContent)
+import           Text.XML.Light.Output (ppcContent, ppcElement, prettyConfigPP)
+
+povrayRaw :: [String] -> Text -> Tree
+povrayRaw args script =
+  memo [Key mkPovrayImage, KeyPrim args, KeyPrim script]
+  (unsafePerformIO $ mkPovrayImage args script)
+
+povray :: [String] -> Text -> Tree
+povray args = povrayRaw (["+H180","+W320", "+A"] ++ args)
+
+povrayQuick :: [String] -> Text -> Tree
+povrayQuick args = povrayRaw (["+H180","+W320"] ++ args)
+
+povraySlow :: [String] -> Text -> Tree
+povraySlow args = povrayRaw (["+H1440","+W2560", "+A"] ++ args)
+
+mkPovrayImage :: [String] -> Text -> IO Tree
+mkPovrayImage args script = cacheDiskKey key $ do
+  povray <- requireExecutable "povray"
+  withTempFile "pov" $ \pov_file -> do
+    let out = replaceExtension pov_file "png"
+    T.writeFile pov_file script
+    ret <- runCmd_ povray (args ++ ["-D","+UA", pov_file, "+o"++out])
+    case ret of
+      Left{} -> error "povray went wrong"
+      Right{} -> do
+        png <- B.readFile out
+        case decodePng png of
+          Left{}    -> error "bad image"
+          Right img -> return $ center $ scaleToSize 320 180 $ embedDynamicImage img
+  where
+    key = T.concat (script:map T.pack args)
diff --git a/src/Reanimate/Raster.hs b/src/Reanimate/Raster.hs
new file mode 100644
--- /dev/null
+++ b/src/Reanimate/Raster.hs
@@ -0,0 +1,37 @@
+module Reanimate.Raster
+  ( embedImage
+  , embedDynamicImage
+  ) where
+
+import Control.Lens
+import Codec.Picture
+import Codec.Picture.Png
+import Codec.Picture.Types
+import qualified Data.ByteString.Base64.Lazy as Base64
+import qualified Data.ByteString.Lazy.Char8 as LBS
+import Graphics.SvgTree (Tree(..), defaultSvg)
+import qualified Graphics.SvgTree as Svg
+
+-- XXX: Use Px instead of Num for width and height?
+{-# INLINE embedImage #-}
+embedImage :: PngSavable a => Image a -> Tree
+embedImage img =
+  ImageTree $ defaultSvg
+    & Svg.imageWidth .~ Svg.Num (fromIntegral $ imageWidth img)
+    & Svg.imageHeight .~ Svg.Num (fromIntegral $ imageHeight img)
+    & Svg.imageHref .~ ("data:image/png;base64," ++ imgData)
+  where
+    imgData = LBS.unpack $ Base64.encode (encodePng img)
+
+{-# INLINE embedDynamicImage #-}
+embedDynamicImage :: DynamicImage -> Tree
+embedDynamicImage img =
+  ImageTree $ defaultSvg
+    & Svg.imageWidth .~ Svg.Num (fromIntegral $ dynamicMap imageWidth img)
+    & Svg.imageHeight .~ Svg.Num (fromIntegral $ dynamicMap imageHeight img)
+    & Svg.imageHref .~ ("data:image/png;base64," ++ imgData)
+  where
+    imgData =
+      case encodeDynamicPng img of
+        Left err -> error err
+        Right dat -> LBS.unpack $ Base64.encode dat
diff --git a/src/Reanimate/Render.hs b/src/Reanimate/Render.hs
--- a/src/Reanimate/Render.hs
+++ b/src/Reanimate/Render.hs
@@ -1,16 +1,19 @@
 module Reanimate.Render
   ( render
   , renderSvgs
+  , renderSnippets
   ) where
 
 import           Control.Monad               (forM_)
 import           Control.Parallel.Strategies
+import           Control.Concurrent.QSemN
+import           Control.Concurrent
+import           Control.Exception
 import qualified Data.ByteString.Lazy.Char8  as BS
 import qualified Data.Text                   as T
 import qualified Data.Text.IO                as T
 import           Graphics.SvgTree            (Number (..))
 import           Reanimate.Diagrams
-import           Reanimate.Examples
 import           Reanimate.Misc
 import           Reanimate.Monad
 import           System.Directory            (renameFile)
@@ -21,17 +24,48 @@
 
 renderSvgs :: Animation ->  IO ()
 renderSvgs ani = do
-    let renderedFrames = map (T.concat . T.lines . T.pack . nthFrame) frames
-    mapM_ T.putStrLn (renderedFrames `using` parBuffer 16 rdeepseq)
+    print frameCount
+    lock <- newMVar ()
+
+    concurrentForM_ (frameOrder rate frameCount) $ \nth -> do
+      let -- frame = frameAt (recip (fromIntegral rate-1) * fromIntegral nth) ani
+          now = (duration ani / (fromIntegral frameCount-1)) * fromIntegral nth
+          frame = frameAt (if frameCount<=1 then 0 else now) ani
+          svg = renderSvg Nothing Nothing frame
+      evaluate (length svg)
+      withMVar lock $ \_ -> do
+        putStr (show nth)
+        T.putStrLn $ T.concat . T.lines . T.pack $ svg
+        hFlush stdout
   where
-    frames = [0..frameCount-1]
     rate = 60
-    nthFrame nth = renderSvg Nothing Nothing $ frameAt (recip (fromIntegral rate) * fromIntegral nth) ani
     frameCount = round (duration ani * fromIntegral rate) :: Int
-    nameTemplate :: String
-    nameTemplate = "render-%05d.svg"
 
+-- XXX: Merge with 'renderSvgs'
+renderSnippets :: Animation ->  IO ()
+renderSnippets ani = do
+    print frameCount
+    forM_ [0..frameCount-1] $ \nth -> do
+      let now = (duration ani / (fromIntegral frameCount-1)) * fromIntegral nth
+          frame = frameAt (if frameCount<=1 then 0 else now) ani
+          svg = renderSvg Nothing Nothing frame
+      putStr (show nth)
+      T.putStrLn $ T.concat . T.lines . T.pack $ svg
+  where
+    frameCount = 50
 
+frameOrder :: Int -> Int -> [Int]
+frameOrder fps nFrames = worker [] fps
+  where
+    worker seen 0 = []
+    worker seen nthFrame =
+      filterFrameList seen nthFrame nFrames ++
+      worker (nthFrame : seen) (nthFrame `div` 2)
+filterFrameList seen nthFrame nFrames =
+    filter (not.isSeen) $ [0, nthFrame .. nFrames-1]
+  where
+    isSeen x = any (\y -> x `mod` y == 0) seen
+
 data Format = RenderMp4 | RenderGif | RenderWebm | RenderBlank
 
 formatFPS :: Format -> Int
@@ -84,17 +118,18 @@
 
 ---------------------------------------------------------------------------------
 -- Helpers
--- XXX: Move to a different module and unify with helpers from LaTeX.
 
--- XXX: Use threads
 generateFrames ani width_ rate action = withTempDir $ \tmp -> do
+    done <- newMVar 0
     let frameName nth = tmp </> printf nameTemplate nth
         rendered = [ renderSvg width height $ nthFrame n | n <- frames]
                     `using` parBuffer 16 rdeepseq
-    forM_ (zip [0::Int ..] rendered) $ \(n, frame) -> do
-      writeFile (frameName n) frame
-      putStr $ "\r" ++ show (n+1) ++ "/" ++ show frameCount
-      hFlush stdout
+    concurrentForM_ frames $ \n -> do
+      writeFile (frameName n) $ renderSvg width height $ nthFrame n
+      modifyMVar_ done $ \nDone -> do
+        putStr $ "\r" ++ show (nDone+1) ++ "/" ++ show frameCount
+        hFlush stdout
+        return (nDone+1)
     putStrLn "\n"
     action (tmp </> nameTemplate)
   where
@@ -105,3 +140,12 @@
     frameCount = round (duration ani * fromIntegral rate) :: Int
     nameTemplate :: String
     nameTemplate = "render-%05d.svg"
+
+concurrentForM_ :: [a] -> (a -> IO ()) -> IO ()
+concurrentForM_ lst action = do
+  n <- getNumCapabilities
+  sem <- newQSemN n
+  forM_ lst $ \elt -> do
+    waitQSemN sem 1
+    forkIO (action elt `finally` signalQSemN sem 1)
+  waitQSemN sem n
diff --git a/src/Reanimate/Scene.hs b/src/Reanimate/Scene.hs
new file mode 100644
--- /dev/null
+++ b/src/Reanimate/Scene.hs
@@ -0,0 +1,134 @@
+{-# LANGUAGE RankNTypes   #-}
+module Reanimate.Scene where
+
+import           Control.Monad.ST
+import           Control.Monad.ST.Unsafe
+import           Data.IORef
+import           Data.Map                (Map)
+import qualified Data.Map                as Map
+import           Data.STRef
+import           Debug.Trace
+import           Data.Ord
+import           Data.List
+import           System.IO.Unsafe
+import           System.Mem.StableName
+
+import           Reanimate.Monad
+
+data World
+type ZIndex = Int
+
+-- (seq duration, par duration)
+-- [(Time, Animation, ZIndex)]
+-- Map Time [(Animation, ZIndex)]
+type Timeline = [(Time, Animation, ZIndex)]
+newtype Scene s a = M { unM :: Time -> ST s (a, Duration, Duration, Timeline) }
+
+unionTimeline :: Timeline -> Timeline -> Timeline
+unionTimeline = (++)
+
+emptyTimeline :: Timeline
+emptyTimeline = []
+
+instance Functor (Scene s) where
+  fmap f action = M $ \t -> do
+    (a, d1, d2, tl) <- unM action t
+    return (f a, d1, d2, tl)
+
+instance Applicative (Scene s) where
+  pure a = M $ \_ -> return (a, 0, 0, emptyTimeline)
+  f <*> g = M $ \t -> do
+    (f', s1, p1, tl1) <- unM f t
+    (g', s2, p2, tl2) <- unM g (t+s1)
+    return (f' g', s1+s2, max p1 (s1+p2), unionTimeline tl1 tl2)
+
+instance Monad (Scene s) where
+  return = pure
+  f >>= g = M $ \t -> do
+    (a, s1, p1, tl1) <- unM f t
+    (b, s2, p2, tl2) <- unM (g a) (t+s1)
+    return (b, s1+s2, max p1 (s1+p2), unionTimeline tl1 tl2)
+
+--data Frame a = Frame {unFrame :: Duration -> Time -> State ([Tree] -> [Tree]) a}
+sceneAnimation :: (forall s. Scene s a) -> Animation
+sceneAnimation action = Animation (max s p) $ Frame $ \d t ->
+  sequence_ $ map snd $ sortBy (comparing fst)
+    [ (z, unFrame frameGen dur (t-startT))
+    | (startT, Animation dur frameGen, z) <- tl
+    , startT < t
+    , startT+dur > t
+    ]
+  where
+    (_, s, p, tl) = runST (unM action 0)
+
+debug :: String -> Scene s ()
+debug msg = M $ \t -> trace msg (return ((), 0, 0, emptyTimeline))
+
+someaction :: Scene s ()
+someaction = debug "someaction"
+
+fork :: Scene s a -> Scene s a
+fork (M action) = M $ \t -> do
+  (a, s, p, tl) <- action t
+  return (a, 0, max s p, tl)
+
+play :: Animation -> Scene s ()
+play = playZ 0
+
+playZ :: ZIndex -> Animation -> Scene s ()
+playZ z ani = M $ \t -> do
+  let d = duration ani
+  return ((), d, 0, [(t, ani, z)])
+
+queryNow :: Scene s Time
+queryNow = M $ \t -> return (t, 0, 0, emptyTimeline)
+
+-- Wait until all forked and sequential animations have finished.
+waitAll :: Scene s a -> Scene s a
+waitAll (M action) = M $ \t -> do
+  (a, s, p, tl) <- action t
+  return (a, max s p, 0, tl)
+
+waitUntil :: Time -> Scene s ()
+waitUntil tNew = do
+  now <- queryNow
+  wait (max 0 (tNew - now))
+
+wait :: Duration -> Scene s ()
+wait d = M $ \t ->
+  return ((), d, 0, emptyTimeline)
+
+adjustZ :: (ZIndex -> ZIndex) -> Scene s a -> Scene s a
+adjustZ fn (M action) = M $ \t -> do
+  (a, s, p, tl) <- action t
+  return (a, s, p, [ (startT, ani, fn z) | (startT, ani, z) <- tl ])
+
+withSceneDuration :: Scene s a -> Scene s Duration
+withSceneDuration s = do
+  t1 <- queryNow
+  s
+  t2 <- queryNow
+  return (t2-t1)
+
+{-
+blackness
+show numbers
+fade in colormap
+slide to middle
+
+do fork $ play $ setDuration 5 showNumbers
+      # fadeOut 1 # fadeIn 1
+   wait 3
+
+   fork $ do
+    playZ (-1) $ setDuration 5 $ revealImage 0 0.5
+      # pauseAround 1 1
+    playZ (-1) $ setDuration 5 $ revealImage 0.5 1
+      # pauseAtEnd 1
+
+   play $ setDuration 5 $ colormap 0 0.5
+     # fadeIn 1 # pauseAround 1 1
+   play $ setDuration 5 $ colormap 0.5 1
+     # fadeOut 1 # pauseAtEnd 1
+
+-}
diff --git a/src/Reanimate/Signal.hs b/src/Reanimate/Signal.hs
new file mode 100644
--- /dev/null
+++ b/src/Reanimate/Signal.hs
@@ -0,0 +1,35 @@
+module Reanimate.Signal where
+
+type Signal = Double -> Double
+
+signalFromList :: [(Double, Signal)] -> Signal
+signalFromList fns t = worker 0 fns
+  where
+    worker _ [] = 0
+    worker now [(len, fn)] = fn (min 1 ((t-now) / min (1-now) len))
+    worker now ((len, fn):rest)
+      | now+len < t = worker (now+len) rest
+      | otherwise = fn ((t-now) / len)
+
+signalFlat :: Double -> Signal
+signalFlat x = const x
+
+signalLinear :: Signal
+signalLinear = id
+
+signalFromTo :: Double -> Double -> Signal -> Signal
+signalFromTo from to c t = from + (to-from)*(c t)
+
+signalReverse :: Signal -> Signal
+signalReverse fn t = fn (1-t)
+
+signalCurve :: Double -> Signal
+signalCurve steepness s =
+  if s < 0.5
+    then 0.5 * (2*s)**steepness
+    else 1-0.5 * (2 - 2*s)**steepness
+
+signalBell :: Double -> Signal
+signalBell steepness s
+  | s < 0.5   = signalCurve steepness (s/0.5)
+  | otherwise = signalCurve steepness (1-((s-0.5)/0.5))
diff --git a/src/Reanimate/Svg.hs b/src/Reanimate/Svg.hs
--- a/src/Reanimate/Svg.hs
+++ b/src/Reanimate/Svg.hs
@@ -2,7 +2,7 @@
 module Reanimate.Svg where
 
 import           Codec.Picture               (PixelRGBA8 (..))
-import           Codec.Picture.Types
+import           Codec.Picture.Types         ()
 import           Control.Arrow
 import           Control.Lens                (over, set, (%~), (&), (.~), (^.))
 import           Control.Monad.Fix
@@ -350,20 +350,26 @@
       CircleTree{}    -> error "CircleTree"
       PolyLineTree{}  -> error "PolyLineTree"
       EllipseTree{}   -> error "EllipseTree"
-      LineTree{}      -> error "LineTree"
+      LineTree line   -> map pointToRPoint [line^.linePoint1, line^.linePoint2]
       RectangleTree rect ->
-        case mapTuple (toUserUnit defaultDPI) (rect^.rectUpperLeftCorner) of
-          (Num x, Num y) -> [V2 x y] ++
+        case pointToRPoint (rect^.rectUpperLeftCorner) of
+          V2 x y -> [V2 x y] ++
             case mapTuple (fmap $ toUserUnit defaultDPI) (rect^.rectWidth, rect^.rectHeight) of
               (Just (Num w), Just (Num h)) -> [V2 (x+w) (y+h)]
               _                            -> []
-          _ -> []
       TextTree{}      -> []
-      ImageTree{}     -> []
+      ImageTree img   ->
+        case (img^.imageCornerUpperLeft, img^.imageWidth, img^.imageHeight) of
+          ((Num x, Num y), Num w, Num h) ->
+            [V2 x y, V2 (x+w) (y+h)]
+          _ -> []
       MeshGradientTree{} -> []
   where
     m = Transform.mkMatrix (t^.transform)
     mapTuple f = f *** f
+    pointToRPoint p =
+      case mapTuple (toUserUnit defaultDPI) p of
+        (Num x, Num y) -> (V2 x y)
 
 lowerTransformations :: Tree -> Tree
 lowerTransformations = worker Transform.identity
@@ -438,6 +444,12 @@
 scale :: Double -> Tree -> Tree
 scale a = withTransformations [Scale a Nothing]
 
+scaleToSize :: Double -> Double -> Tree -> Tree
+scaleToSize w h t =
+    scaleXY (w/w') (h/h') t
+  where
+    (_x, _y, w', h') = boundingBox t
+
 scaleXY :: Double -> Double -> Tree -> Tree
 scaleXY x y = withTransformations [Scale x (Just y)]
 
@@ -475,6 +487,16 @@
   where
     (x, y, w, h) = boundingBox t
 
+centerX :: Tree -> Tree
+centerX t = translate (-x-w/2) 0 t
+  where
+    (x, y, w, h) = boundingBox t
+
+centerY :: Tree -> Tree
+centerY t = translate 0 (-y-h/2) t
+  where
+    (x, y, w, h) = boundingBox t
+
 mkColor :: String -> Texture
 mkColor name =
   case Map.lookup name svgNamedColors of
@@ -496,21 +518,34 @@
 withFillOpacity :: Double -> Tree -> Tree
 withFillOpacity opacity = fillOpacity .~ Just (realToFrac opacity)
 
+withGroupOpacity :: Double -> Tree -> Tree
+withGroupOpacity opacity = groupOpacity .~ Just (realToFrac opacity)
+
 withStrokeWidth :: Number -> Tree -> Tree
 withStrokeWidth width = strokeWidth .~ pure width
 
 withClipPathRef :: ElementRef -> Tree -> Tree
 withClipPathRef ref = clipPathRef .~ pure ref
 
-mkRect :: Point -> Number -> Number -> Tree
-mkRect corner width height = RectangleTree $ defaultSvg
-  & rectUpperLeftCorner .~ corner
+withId :: String -> Tree -> Tree
+withId idTag = attrId .~ Just idTag
+
+mkRect :: Number -> Number -> Tree
+mkRect width height = center $ RectangleTree $ defaultSvg
+  & rectUpperLeftCorner .~ (Num 0, Num 0)
   & rectWidth .~ Just width
   & rectHeight .~ Just height
 
+mkCircle :: Number -> Tree
+mkCircle radius = CircleTree $ defaultSvg
+  & circleCenter .~ (Num 0, Num 0)
+  & circleRadius .~ radius
+
+-- XXX: This is broken.
 mkBoundingRect :: Tree -> Double -> Tree
 mkBoundingRect src margin =
-    mkRect (Num $ x-margin, Num $ y-margin) (Num $ w+margin*2) (Num $ h+margin*2)
+    translate (x-margin) (y-margin) $
+    mkRect (Num $ w+margin*2) (Num $ h+margin*2)
   where
     (x, y, w, h) = boundingBox src
 
@@ -523,6 +558,17 @@
 mkGroup forest = GroupTree $ defaultSvg
   & groupChildren .~ forest
 
+mkDefinitions :: [Tree] -> Tree
+mkDefinitions forest = DefinitionTree $ defaultSvg
+  & groupChildren .~ forest
+
+mkUse :: String -> Tree
+mkUse name = UseTree (defaultSvg & useName .~ name) Nothing
+
+mkClipPath :: String -> [Tree] -> Tree
+mkClipPath idTag forest = withId idTag $ ClipPathTree $ (defaultSvg
+  & clipPathContent .~ forest)
+
 mkPathString :: String -> Tree
 mkPathString = mkPathText . T.pack
 
@@ -541,11 +587,11 @@
            , LineTo OriginAbsolute [ V2 x y | (x, y) <- rest ] ]
 
 mkBackground :: String -> Tree
-mkBackground color = withFillColor color $ mkRect (Num $ -320/2, Num $ -180/2) (Percent 1) (Percent 1)
+mkBackground color = withFillColor color $ mkRect (Num 320) (Num 180)
 
 mkBackgroundPixel :: PixelRGBA8 -> Tree
 mkBackgroundPixel pixel =
-    withFillColorPixel pixel $ mkRect (Num $ -320/2, Num $ -180/2) (Percent 1) (Percent 1)
+    withFillColorPixel pixel $ mkRect (Num 320) (Num 180)
 
 withSubglyphs :: [Int] -> (Tree -> Tree) -> Tree -> Tree
 withSubglyphs target fn t = evalState (worker t) 0
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,12 @@
+module Main (main) where
+
+import           Test.Tasty
+
+import           UnitTests
+
+main :: IO ()
+main = do
+  tests1 <- unitTestFolder "examples/"
+  tests2 <- compileTestFolder "examples/"
+  tests3 <- compileVideoFolder "videos/"
+  defaultMain $ testGroup "tests" [tests1, tests2, tests3]
diff --git a/test/UnitTests.hs b/test/UnitTests.hs
new file mode 100644
--- /dev/null
+++ b/test/UnitTests.hs
@@ -0,0 +1,101 @@
+module UnitTests
+  ( unitTestFolder
+  , compileTestFolder
+  , compileVideoFolder
+  ) where
+
+import           Control.Monad        (forM)
+import qualified Data.ByteString.Lazy as LBS
+import           Data.Maybe           (catMaybes)
+import           Reanimate.Misc       (withTempDir, withTempFile)
+import           System.Directory
+import           System.Exit
+import           System.FilePath
+import           System.IO
+import           System.Process
+import           Test.Tasty
+import           Test.Tasty.Golden
+import           Test.Tasty.HUnit
+
+unitTestFolder :: FilePath -> IO TestTree
+unitTestFolder path = do
+  files <- getDirectoryContents path
+  return $ testGroup "animate"
+    [ goldenVsStringDiff file (\ref new -> ["diff", "--brief", ref, new]) fullPath (genGolden hsPath)
+    | file <- files
+    , let fullPath = path </> file
+          hsPath = replaceExtension fullPath "hs"
+    , takeExtension fullPath == ".golden"
+    ]
+
+genGolden :: FilePath -> IO LBS.ByteString
+genGolden path = withTempDir $ \tmpDir -> withTempFile ".exe" $ \tmpExecutable -> do
+  let ghcOpts = ["-rtsopts", "--make", "-O2"] ++
+                ["-odir", tmpDir, "-hidir", tmpDir, "-o", tmpExecutable]
+      runOpts = ["+RTS", "-M1G"]
+  -- XXX: Check for errors.
+  _ <- readProcessWithExitCode "stack" (["ghc","--", path] ++ ghcOpts) ""
+  -- ret <- runCmd_ "stack" $ ["ghc", "--"] ++ ghcOptions tmpDir ++ [self, "-o", tmpExecutable]
+  -- ["-rtsopts", "--make", "-threaded", "-O2"] ++
+  -- ["-odir", tmpDir, "-hidir", tmpDir]
+  (inh, outh, errh, pid) <- runInteractiveProcess tmpExecutable (["snippets"] ++ runOpts)
+    Nothing Nothing
+  hClose inh
+  hClose errh
+  LBS.hGetContents outh
+
+compileTestFolder :: FilePath -> IO TestTree
+compileTestFolder path = do
+  files <- getDirectoryContents path
+  return $ testGroup "compile"
+    [ testCase file $ do
+        (ret, _stdout, _stderr) <- readProcessWithExitCode "stack" (["ghc","--", fullPath] ++ ghcOpts) ""
+        case ret of
+          ExitFailure{} -> assertFailure "Failed to compile"
+          ExitSuccess   -> return ()
+    | file <- files
+    , let fullPath = path </> file
+    , takeExtension file == ".hs" || takeExtension file == ".lhs"
+    ]
+  where
+    ghcOpts = ["-fno-code", "-O0"]
+
+compileVideoFolder :: FilePath -> IO TestTree
+compileVideoFolder path = do
+  files <- getDirectoryContents path
+  return $ testGroup "videos"
+    [ testCase dir $ do
+        (ret, _stdout, _stderr) <- readProcessWithExitCode "stack" (["ghc","--", fullPath] ++ ghcOpts) ""
+        case ret of
+          ExitFailure{} -> assertFailure "Failed to compile"
+          ExitSuccess   -> return ()
+    | dir <- files
+    , let fullPath = path </> dir </> dir <.> "hs"
+    , dir /= "." && dir /= ".."
+    ]
+  where
+    ghcOpts = ["-fno-code", "-O0"]
+
+--------------------------------------------------------------------------------
+-- Helpers
+
+-- findAnExecutable :: [String] -> IO (Maybe FilePath)
+-- findAnExecutable [] = return Nothing
+-- findAnExecutable (x:xs) = do
+--   mbExec <- findExecutable x
+--   case mbExec of
+--     Just exec -> return (Just exec)
+--     Nothing   -> findAnExecutable xs
+--
+-- readFileOptional :: FilePath -> IO String
+-- readFileOptional path = do
+--   hasFile <- doesFileExist path
+--   if hasFile then readFile path else return ""
+--
+-- assertExitCode :: String -> ExitCode -> Assertion
+-- assertExitCode _ ExitSuccess = return ()
+-- assertExitCode msg (ExitFailure code) = assertFailure (msg ++ ", code: " ++ show code)
+--
+-- assertMaybe :: String -> Maybe a -> IO a
+-- assertMaybe _ (Just a)  = return a
+-- assertMaybe msg Nothing = assertFailure msg
diff --git a/viewer/build/index.html b/viewer/build/index.html
--- a/viewer/build/index.html
+++ b/viewer/build/index.html
@@ -1,1 +1,1 @@
-<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="./manifest.json"/><title>Reanimate Playground</title><link href="./static/css/main.6efe09fd.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],f=0,i=[];f<n.length;f++)t=n[f],p[t]&&i.push(p[t][0]),p[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);i.length;)i.shift()();return c.push.apply(c,u||[]),a()}function a(){for(var e,r=0;r<c.length;r++){for(var t=c[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==p[u]&&(n=!1)}n&&(c.splice(r--,1),e=f(f.s=t[0]))}return e}var t={},p={1:0},c=[];function f(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,f),r.l=!0,r.exports}f.m=l,f.c=t,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(r,e){if(1&e&&(r=f(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)f.d(t,n,function(e){return r[e]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="./";var r=window.webpackJsonp=window.webpackJsonp||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;a()}([])</script><script src="./static/js/2.772a56e7.chunk.js"></script><script src="./static/js/main.db22f45d.chunk.js"></script></body></html>
+<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="./manifest.json"/><title>Reanimate Playground</title><link href="./static/css/main.f7ad3e9b.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],f=0,i=[];f<n.length;f++)t=n[f],p[t]&&i.push(p[t][0]),p[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);i.length;)i.shift()();return c.push.apply(c,u||[]),a()}function a(){for(var e,r=0;r<c.length;r++){for(var t=c[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==p[u]&&(n=!1)}n&&(c.splice(r--,1),e=f(f.s=t[0]))}return e}var t={},p={1:0},c=[];function f(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,f),r.l=!0,r.exports}f.m=l,f.c=t,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(r,e){if(1&e&&(r=f(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)f.d(t,n,function(e){return r[e]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="./";var r=window.webpackJsonp=window.webpackJsonp||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;a()}([])</script><script src="./static/js/2.772a56e7.chunk.js"></script><script src="./static/js/main.c36ecb4a.chunk.js"></script></body></html>
diff --git a/viewer/build/service-worker.js b/viewer/build/service-worker.js
--- a/viewer/build/service-worker.js
+++ b/viewer/build/service-worker.js
@@ -14,7 +14,7 @@
 importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js");
 
 importScripts(
-  "./precache-manifest.15b8d497ab10704d87b84878b92d08cf.js"
+  "./precache-manifest.2267f062fb7f449c2505790eb4da5a8a.js"
 );
 
 workbox.clientsClaim();
diff --git a/viewer/build/static/css/main.6efe09fd.chunk.css b/viewer/build/static/css/main.6efe09fd.chunk.css
deleted file mode 100644
--- a/viewer/build/static/css/main.6efe09fd.chunk.css
+++ /dev/null
@@ -1,2 +0,0 @@
-body{margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{height:100vh;background-color:#282c34;color:#fff;display:grid;grid-template-columns:1fr;grid-template-rows:auto;grid-auto-flow:column;-webkit-align-items:center;align-items:center;overflow:hidden}.controls{text-align:center}.controls svg{padding-left:5px;padding-right:5px}.viewer{position:static}.viewer svg{max-width:100vw;max-height:100vh;margin-top:auto;margin-bottom:auto}div.messages{position:fixed;top:0;width:100%;background:#282c34}div.messages pre{margin:0}.home{color:#fff;text-align:center;float:right;margin-right:2em}
-/*# sourceMappingURL=main.6efe09fd.chunk.css.map */
diff --git a/viewer/build/static/css/main.f7ad3e9b.chunk.css b/viewer/build/static/css/main.f7ad3e9b.chunk.css
new file mode 100644
--- /dev/null
+++ b/viewer/build/static/css/main.f7ad3e9b.chunk.css
@@ -0,0 +1,2 @@
+body{margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{height:100vh;background-color:#282c34;color:#fff;display:grid;grid-template-columns:1fr;grid-template-rows:auto;grid-auto-flow:column;-webkit-align-items:center;align-items:center;overflow:hidden}.controls{text-align:center}.controls svg{padding-left:5px;padding-right:5px}.viewer{position:static}.viewer svg{max-width:100vw;max-height:100vh;margin-top:auto;margin-bottom:auto}div.messages{position:fixed;top:0;width:100%;background:#282c34}div.messages pre{margin:0}.home{color:#fff;text-align:center;float:right;margin-right:2em}
+/*# sourceMappingURL=main.f7ad3e9b.chunk.css.map */
diff --git a/viewer/build/static/js/main.c36ecb4a.chunk.js b/viewer/build/static/js/main.c36ecb4a.chunk.js
new file mode 100644
--- /dev/null
+++ b/viewer/build/static/js/main.c36ecb4a.chunk.js
@@ -0,0 +1,2 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{10:function(e,t,n){e.exports=n(18)},16:function(e,t,n){},17:function(e,t,n){},18:function(e,t,n){"use strict";n.r(t);var a=n(0),s=n.n(a),o=n(4),r=n.n(o),c=(n(16),n(2)),i=n(5),u=n(6),m=n(9),l=n(7),f=n(1),g=n(8),d=(n(17),function(e){function t(e){var n;Object(i.a)(this,t),(n=Object(m.a)(this,Object(l.a)(t).call(this,e))).connect=function(){var e=new WebSocket("ws://localhost:9161");e.onopen=function(t){n.setState(function(e){return Object(c.a)({},e,{message:"Connected."})}),e.send("60")},e.onclose=function(e){n.setState(function(e){return Object(c.a)({},e,{message:"Disconnected."})}),setTimeout(n.connect,1e3)},e.onmessage=function(e){if("Success!"===e.data)console.log("Success");else if("Compiling"===e.data)n.setState({message:"Compiling..."}),n.status="compiling",n.svgs=[],n.frame_count=0,n.next_frame=0;else if("Done"===e.data)n.setState({message:""}),console.log("Done"),n.status="done";else if(e.data.startsWith("Error"))console.log("Error",e.data.substring(5)),n.setState({message:e.data.substring(5)});else{var t=parseInt(e.data);if(isNaN(t)){var a=document.createElement("div");a.innerHTML=e.data,n.svgs[n.next_frame]=a;var s=0;n.svgs.forEach(function(e){return s++}),console.log("Received",n.next_frame,n.frame_count,s)}else"compiling"===n.status?(n.setState({message:"Rendering..."}),n.frame_count=t,n.status="rendering",n.start=Date.now(),n.svgs=[],n.svgs[n.frame_count-1]=void 0):"rendering"===n.status?n.next_frame=t:console.log("Bad state change: received number")}},n.setState(function(t){return Object(c.a)({},t,{socket:e,message:"Connecting..."})})},n.onLoad=function(e){setTimeout(function(){e.resize()},0)},n.state={},setTimeout(n.connect,0),n.svgs=[],n.start=Date.now(),n.status="",n.frame_count=0,n.next_frame=0;var a=Object(f.a)(n);return requestAnimationFrame(function e(){var t=Date.now(),s=a.frame_count,o=a.frame_count/60,r=Math.round((t-n.start)/1e3*60)%s,c=0;if(n.svgs.forEach(function(e){return c++}),s){if(a.svgs[r]){for(n.setState({message:r+"/"+a.frame_count+" "+Math.round(c/o)+" fps"});a.svg.firstChild;)a.svg.removeChild(a.svg.firstChild);a.svg.appendChild(a.svgs[r])}}else a.svg.innerText="";requestAnimationFrame(e)}),n}return Object(g.a)(t,e),Object(u.a)(t,[{key:"render",value:function(){var e=this,t=this.state.message;return s.a.createElement("div",{className:"App"},s.a.createElement("div",{className:"viewer"},s.a.createElement("div",{ref:function(t){return e.svg=t}}),s.a.createElement("div",{className:"messages"},s.a.createElement("pre",null,t))))}}]),t}(a.Component));Boolean("localhost"===window.location.hostname||"[::1]"===window.location.hostname||window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/));r.a.render(s.a.createElement(d,null),document.getElementById("root")),"serviceWorker"in navigator&&navigator.serviceWorker.ready.then(function(e){e.unregister()})}},[[10,1,2]]]);
+//# sourceMappingURL=main.c36ecb4a.chunk.js.map
diff --git a/viewer/build/static/js/main.db22f45d.chunk.js b/viewer/build/static/js/main.db22f45d.chunk.js
deleted file mode 100644
--- a/viewer/build/static/js/main.db22f45d.chunk.js
+++ /dev/null
@@ -1,2 +0,0 @@
-(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{10:function(e,n,t){e.exports=t(18)},16:function(e,n,t){},17:function(e,n,t){},18:function(e,n,t){"use strict";t.r(n);var s=t(0),a=t.n(s),o=t(4),r=t.n(o),i=(t(16),t(2)),c=t(5),l=t(6),m=t(9),g=t(7),u=t(1),d=t(8),v=(t(17),function(e){function n(e){var t;Object(c.a)(this,n),(t=Object(m.a)(this,Object(g.a)(n).call(this,e))).connect=function(){var e=new WebSocket("ws://localhost:9161");e.onopen=function(n){t.setState(function(e){return Object(i.a)({},e,{message:"Connected."})}),e.send("60")},e.onclose=function(e){t.setState(function(e){return Object(i.a)({},e,{message:"Disconnected."})}),setTimeout(t.connect,1e3)},e.onmessage=function(e){if("Success!"===e.data)console.log("Success");else if("Compiling"===e.data)t.setState({message:"Compiling..."});else if("Rendering"===e.data)t.setState({message:"Rendering..."}),t.nFrames_new=0,t.svgs_new=[];else if("Done"===e.data)t.setState({message:""}),console.log("Done"),t.nFrames=t.nFrames_new,t.svgs=t.svgs_new,t.nFrames_new=0,t.svgs_new=[],t.start=Date.now();else if(e.data.startsWith("Error"))console.log("Error"),t.setState({message:e.data.substring(5)});else{t.setState({message:"Rendering: ".concat(t.nFrames_new)}),t.nFrames_new++;var n=document.createElement("div");n.innerHTML=e.data,t.svgs_new.push(n)}},t.setState(function(n){return Object(i.a)({},n,{socket:e,message:"Connecting..."})})},t.onLoad=function(e){setTimeout(function(){e.resize()},0)},t.state={},setTimeout(t.connect,0),t.nFrames_new=0,t.svgs_new=[],t.nFrames=0,t.svgs=[],t.start=Date.now();var s=Object(u.a)(t);return requestAnimationFrame(function e(){var n=Date.now(),a=s.nFrames,o=Math.round((n-t.start)/1e3*60)%a;if(s.svgs_new.length){for(;s.svg.firstChild;)s.svg.removeChild(s.svg.firstChild);s.svg.appendChild(s.svgs_new[s.svgs_new.length-1])}else if(a){for(;s.svg.firstChild;)s.svg.removeChild(s.svg.firstChild);s.svg.appendChild(s.svgs[o])}else s.svg.innerText="";requestAnimationFrame(e)}),t}return Object(d.a)(n,e),Object(l.a)(n,[{key:"render",value:function(){var e=this,n=this.state.message;return a.a.createElement("div",{className:"App"},a.a.createElement("div",{className:"viewer"},a.a.createElement("div",{ref:function(n){return e.svg=n}}),a.a.createElement("div",{className:"messages"},a.a.createElement("pre",null,n))))}}]),n}(s.Component));Boolean("localhost"===window.location.hostname||"[::1]"===window.location.hostname||window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/));r.a.render(a.a.createElement(v,null),document.getElementById("root")),"serviceWorker"in navigator&&navigator.serviceWorker.ready.then(function(e){e.unregister()})}},[[10,1,2]]]);
-//# sourceMappingURL=main.db22f45d.chunk.js.map
