]> git.hungrycats.org Git - xscreensaver/commitdiff
From https://www.jwz.org/xscreensaver/xscreensaver-6.10.1.tar.gz
authorZygo Blaxell <zblaxell@waya.furryterror.org>
Mon, 28 Apr 2025 20:49:39 +0000 (16:49 -0400)
committerZygo Blaxell <xss@mirrors.furryterror.org>
Mon, 28 Apr 2025 20:50:43 +0000 (16:50 -0400)
-rw-rw-r-- 1 zblaxell zblaxell 25081752 Apr 28 16:08 xscreensaver-6.10.1.tar.gz
05a7e1dab3bb0a1e663c9037baf79f84ac7ee498  xscreensaver-6.10.1.tar.gz

448 files changed:
Makefile.in
OSX/README
OSX/Randomizer.plist
OSX/SaverRunner.m
OSX/SaverRunner.plist
OSX/Updater.plist
OSX/XScreenSaver.plist
OSX/bindist.rtf
OSX/bindist2.webloc
OSX/iSaverRunner.plist
OSX/installer.rtf
OSX/ios-function-table.m
OSX/perl-minimize.pl [new file with mode: 0755]
OSX/tvSaverRunner.plist
OSX/update-info-plist.pl
OSX/updates.xml
OSX/xscreensaver.xcodeproj/project.pbxproj
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Abstractile.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/All Savers (OpenGL).xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/All Savers (XLockmore).xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/All Savers (XScreenSaver).xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/All Savers.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Anemone.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Anemotaxis.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Ant.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/AntInspect.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/AntMaze.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/AntSpotlight.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Apollonian.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Apple2-OSX.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Apple2-iOS.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Apple2.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Atlantis.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Attraction.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Atunnel.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/BSOD.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Barcode.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Beats.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/BinaryHorizon.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/BinaryRing.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Blaster.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/BlinkBox.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/BlitSpin.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/BlockTube.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Boing.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Bouboule.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/BouncingCow.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/BoxFit.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Boxed.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Braid.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Bubble3D.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Bubbles.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Bumps.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CCurve.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/COVID19.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CWaves.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Cage.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Carousel.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Celtic.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/ChompyTower.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Circuit.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Cityflow.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CloudLife.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CompanionCube.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Compass.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Coral.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Crackberg.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Critical.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Crumbler.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Crystal.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Cube21.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CubeStack.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CubeStorm.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CubeTwist.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Cubenetic.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CubicGrid.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/CuboctEversion.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Cynosure.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/DNAlogo.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/DangerBall.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/DecayScreen.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Deco.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/DeepStars.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Deluxe.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Demon.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Discoball.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Discrete.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Distort.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Drift.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Droste.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/DumpsterFire.xcscheme [new file with mode: 0644]
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/DymaxionMap.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Endgame.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/EnergyStream.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Engine.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Epicycle.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Eruption.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Esper.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/EtruscanVenus.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Euler2D.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Extrusion.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FadePlot.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Fiberlamp.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FilmLeader.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Fireworkx.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Flag.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Flame.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FlipFlop.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FlipScreen3D.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FlipText.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Flow.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FluidBalls.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FlyingToasters.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FontGlide.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Forest.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/FuzzyFlakes.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GFlux.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLBlur.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLCells.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLForestFire.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLHanoi.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLKnots.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLMatrix.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLPlanet.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLSchool.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLSlideshow.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLSnake.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GLText.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Galaxy.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Gears.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Geodesic.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GeodesicGears.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Gibson.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Gleidescope.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GlitchPEG.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Goop.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Grav.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/GravityWell.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Greynetic.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Halftone.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Halo.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Handsy.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Headroom.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Helix.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/HexTrail.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Hexadrop.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Hexstrut.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/HighVoltage.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Hilbert.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Hopalong.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/HopfFibration.xcscheme [new file with mode: 0644]
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Hydrostat.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/HyperBall.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/HyperCube.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Hypertorus.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Hypnowheel.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/IFS.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/IMSMap.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Interaggregate.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Interference.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Intermomentary.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/JigglyPuff.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Jigsaw.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Juggle.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Juggler3D.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Julia.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Kaleidescope.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Kaleidocycle.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Kallisti.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Klein.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Klondike.xcscheme [new file with mode: 0644]
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Kumppa.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/LCDscrub.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/LMorph.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Lament.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Laser.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Lavalite.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Lightning.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Lisa.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Lissie.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Lockward.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/MapScroller.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Marbling.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Maze.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Maze3D.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/MemScroller.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Menger.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/MetaBalls.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/MirrorBlob.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Moebius.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/MoebiusGears.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Moire.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Moire2.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Molecule.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Morph3D.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Mountain.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Munch.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Nakagin.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/NerveRot.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Noof.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/NoseGuy.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Obsolete.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Pacman.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/PaperCube.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Pedal.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Peepers.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Penetrate.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Penrose.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Petri.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Phosphor-OSX.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Phosphor-iOS.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Phosphor.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Photopile.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Piecewise.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Pinion.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Pipes.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/PlatonicFolding.xcscheme [new file with mode: 0644]
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Polyhedra.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Polyominoes.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Polytopes.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Pong.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/PopSquares.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/ProjectivePlane.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Providence.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Pulsar.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Pyro.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Qix.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/QuasiCrystal.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Queens.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/RDbomb.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/RandomXScreenSaver.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/RaverHoop.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/RazzleDazzle.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Ripples.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Rocks.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/RomanBoy.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Rorschach.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/RotZoomer.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Rotor.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Rubik.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/RubikBlocks.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/SBalls.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/SaverTester.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Scooter.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/ShadeBobs.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Sierpinski.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Sierpinski3D.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Skulloop.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/SkyTentacles.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/SlideScreen.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Slip.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Sonar.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/SpeedMine.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Sphere.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/SphereEversion.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Spheremonics.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Spiral.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/SplitFlap.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Splodesic.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Spotlight.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Sproingies.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Squiral.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Squirtorus.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Stairs.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/StarWars.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Starfish.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/StonerView.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Strange.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Substrate.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Superquadrics.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Surfaces.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Swirl.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/T3D.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Tangram.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Tessellimage.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/TestX11-iOS.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/TestX11.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Thornbird.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/TimeTunnel.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/TopBlock.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Triangle.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/TronBit.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Truchet.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Twang.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Unicrud.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/UnknownPleasures.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/VFeedback.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Vermiculate.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Vigilance.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Vines.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Voronoi.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Wander.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/WebCollage.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/WhirlWindWarp.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Whirlygig.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/WindupRobot.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Worm.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Wormhole.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XAnalogTV.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XFlame.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XJack.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XLyap.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XMatrix.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XRaySwarm.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XScreenSaver-iOS.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XScreenSaver-tvOS.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XScreenSaverUpdater.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/XSpirograph.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Zoom.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/enable_gc.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/images_png_h.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/jwxyz.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/m6502.h.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/m6502.xcscheme
OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/molecules.h.xcscheme
README
aclocal.m4
android/Makefile
android/build.gradle
android/gradle/wrapper/gradle-wrapper.properties
android/xscreensaver/build.gradle
android/xscreensaver/jni/Android.mk
android/xscreensaver/src/org/jwz/xscreensaver/Activity.java
android/xscreensaver/src/org/jwz/xscreensaver/jwxyz.java
configure
configure.ac
driver/XScreenSaver.ad.in
driver/XScreenSaver_ad.h
driver/auth.h
driver/dialog.c
driver/test-passwd.c
driver/xscreensaver-settings.man
driver/xscreensaver-systemd.c
hacks/Makefile.in
hacks/analogtv-cli.c
hacks/ansi-tty.c [new file with mode: 0644]
hacks/ansi-tty.h [new file with mode: 0644]
hacks/apple2-main.c
hacks/barcode.c
hacks/bsod.c
hacks/check-configs.pl
hacks/config/README
hacks/config/bsod.xml
hacks/config/dumpsterfire.xml [new file with mode: 0644]
hacks/config/hopffibration.xml [new file with mode: 0644]
hacks/config/klondike.xml [new file with mode: 0644]
hacks/config/platonicfolding.xml [new file with mode: 0644]
hacks/config/sonar.xml
hacks/ffmpeg-out.c
hacks/glx/Makefile.in
hacks/glx/cubocteversion.c
hacks/glx/dumpster.dxf [new file with mode: 0644]
hacks/glx/dumpster_model.c [new file with mode: 0644]
hacks/glx/dumpsterfire.c [new file with mode: 0644]
hacks/glx/flyingtoasters.c
hacks/glx/fps-gl.c
hacks/glx/glsl-utils.c
hacks/glx/glsl-utils.h
hacks/glx/glsnake.c
hacks/glx/gltrackball.c
hacks/glx/hopfanimations.c [new file with mode: 0644]
hacks/glx/hopfanimations.h [new file with mode: 0644]
hacks/glx/hopffibration.c [new file with mode: 0644]
hacks/glx/hopffibration.man [new file with mode: 0644]
hacks/glx/klondike-game.c [new file with mode: 0644]
hacks/glx/klondike-game.h [new file with mode: 0644]
hacks/glx/klondike.c [new file with mode: 0644]
hacks/glx/platonicfolding.c [new file with mode: 0644]
hacks/glx/platonicfolding.man [new file with mode: 0644]
hacks/glx/polyhedra.c
hacks/glx/sonar-icmp.c
hacks/glx/xlock-gl-utils.c
hacks/images/klondike/C2.png [new file with mode: 0644]
hacks/images/klondike/C3.png [new file with mode: 0644]
hacks/images/klondike/C4.png [new file with mode: 0644]
hacks/images/klondike/C5.png [new file with mode: 0644]
hacks/images/klondike/C6.png [new file with mode: 0644]
hacks/images/klondike/C7.png [new file with mode: 0644]
hacks/images/klondike/C8.png [new file with mode: 0644]
hacks/images/klondike/C9.png [new file with mode: 0644]
hacks/images/klondike/CA.png [new file with mode: 0644]
hacks/images/klondike/CJ.png [new file with mode: 0644]
hacks/images/klondike/CK.png [new file with mode: 0644]
hacks/images/klondike/CQ.png [new file with mode: 0644]
hacks/images/klondike/CT.png [new file with mode: 0644]
hacks/images/klondike/D2.png [new file with mode: 0644]
hacks/images/klondike/D3.png [new file with mode: 0644]
hacks/images/klondike/D4.png [new file with mode: 0644]
hacks/images/klondike/D5.png [new file with mode: 0644]
hacks/images/klondike/D6.png [new file with mode: 0644]
hacks/images/klondike/D7.png [new file with mode: 0644]
hacks/images/klondike/D8.png [new file with mode: 0644]
hacks/images/klondike/D9.png [new file with mode: 0644]
hacks/images/klondike/DA.png [new file with mode: 0644]
hacks/images/klondike/DJ.png [new file with mode: 0644]
hacks/images/klondike/DK.png [new file with mode: 0644]
hacks/images/klondike/DQ.png [new file with mode: 0644]
hacks/images/klondike/DT.png [new file with mode: 0644]
hacks/images/klondike/H2.png [new file with mode: 0644]
hacks/images/klondike/H3.png [new file with mode: 0644]
hacks/images/klondike/H4.png [new file with mode: 0644]
hacks/images/klondike/H5.png [new file with mode: 0644]
hacks/images/klondike/H6.png [new file with mode: 0644]
hacks/images/klondike/H7.png [new file with mode: 0644]
hacks/images/klondike/H8.png [new file with mode: 0644]
hacks/images/klondike/H9.png [new file with mode: 0644]
hacks/images/klondike/HA.png [new file with mode: 0644]
hacks/images/klondike/HJ.png [new file with mode: 0644]
hacks/images/klondike/HK.png [new file with mode: 0644]
hacks/images/klondike/HQ.png [new file with mode: 0644]
hacks/images/klondike/HT.png [new file with mode: 0644]
hacks/images/klondike/S2.png [new file with mode: 0644]
hacks/images/klondike/S3.png [new file with mode: 0644]
hacks/images/klondike/S4.png [new file with mode: 0644]
hacks/images/klondike/S5.png [new file with mode: 0644]
hacks/images/klondike/S6.png [new file with mode: 0644]
hacks/images/klondike/S7.png [new file with mode: 0644]
hacks/images/klondike/S8.png [new file with mode: 0644]
hacks/images/klondike/S9.png [new file with mode: 0644]
hacks/images/klondike/SA.png [new file with mode: 0644]
hacks/images/klondike/SJ.png [new file with mode: 0644]
hacks/images/klondike/SK.png [new file with mode: 0644]
hacks/images/klondike/SQ.png [new file with mode: 0644]
hacks/images/klondike/ST.png [new file with mode: 0644]
hacks/images/klondike/attribution.txt [new file with mode: 0644]
hacks/images/klondike/back.png [new file with mode: 0644]
hacks/images/klondike/back0.png [new file with mode: 0644]
hacks/phosphor.c
hacks/phosphor.man
hacks/screenhack.c
hacks/screenhackI.h
hacks/webcollage
hacks/xscreensaver-text
jwxyz/jwxyz.h
jwxyz/jwzgles.c
jwxyz/jwzglesI.h
po/POTFILES.in
utils/bin2c
utils/textclient-mobile.c
utils/textclient.c
utils/textclient.h
utils/version.h
utils/visual-gl.c
utils/xft.c
utils/xft.h
utils/xftwrap.c
xscreensaver.spec

index c713f1f5620b5f16b0489f7c3dff4f43f9b132d5..e5a2af2547220218e341fbed72fd13bec689a0f4 100644 (file)
@@ -1,4 +1,4 @@
-# Makefile.in --- xscreensaver, Copyright © 1999-2024 Jamie Zawinski.
+# Makefile.in --- xscreensaver, Copyright © 1999-2025 Jamie Zawinski.
 # the `../configure' script generates `Makefile' from this file.
 
 @SET_MAKE@
@@ -167,7 +167,7 @@ bump-version::
   MAJOR="$$1"; MINOR="$$2"; SUF="$$3";                                     \
   VERS="$$MAJOR.$$MINOR$$SUF" ;                                                    \
   if [ -z "$$SUF" ]; then                                                  \
-    MINOR=`echo $$MINOR + 1 | bc | sed 's/^\(.\)/0\1/'` ;                  \
+    MINOR=`echo $$MINOR + 1 | bc | sed 's/^\(.\)$$/0\1/'` ;                \
   else                                                                     \
     set - `echo $$SUF | sed 's/^\([^0-9]*\)/\1 /'` ;                       \
     AA="$$1"; BB="$$2";                                                            \
@@ -435,6 +435,7 @@ install_links::
        cwd=`pwd` ;                                                     \
        for d in $(SUBDIRS) ; do (                                      \
          cd $$d ;                                                      \
+         set -e ;                                                      \
          $(MAKE2) install -k INSTALL=true INSTALL_DATA=true            \
               INSTALL_DIRS=false SUID_FLAGS= 2>&- |                    \
          while read s ; do                                             \
@@ -442,6 +443,8 @@ install_links::
            if [ $$1 = true ]; then                                     \
              a="$$cwd/$$d/$$2" ;                                       \
              b="$$3" ;                                                 \
+             d2=`dirname "$$b"` ;                                      \
+             [ -d "$$d2" ] || mkdir -p "$$d2" ;                        \
              a=`echo "$$a" | sed -e 's@/\./@/@'                        \
                 -e 's@/[^/]*/\.\./@/@'` ;                              \
              echo "ln -sf $$a $$b" ;                                   \
@@ -452,7 +455,7 @@ install_links::
 
 
 cerebrum::
-       rsync -vax . 10.0.1.12:xscreensaver/ \
+       rsync -vax . 10.0.1.37:xscreensaver/ \
        --omit-dir-times \
        --delete-during \
        --exclude .git \
index 7bfd62550d625c83c05390505bde9ac711158140..ab027b0a9fef5bfee46209786a2c7101bf7e3009 100644 (file)
@@ -109,7 +109,7 @@ Building for older operating systems:
   8. Scheme / Profile / Info: Executable: SaverTester.app.
      Scheme / Run / Info: Executable: SaverTester.app.
   9: Scheme / Run / Arguments: set SELECTED_SAVER environment variable.
- 10: File / Add Files / the new .c and .xml.
+ 10: File / Add Files / [x] Reference In Place / the new .c and .xml.
      Add To Targets: the new target, and "XScreenSaver-iOS" and "-tvOS".
  11: Re-order the two files in the file list on the left.
  12: The files might not have moved. This means Xcode is gonna crash soon.
@@ -124,5 +124,5 @@ Building for older operating systems:
            ~/Library/Screen\ Savers/
  18: git add xscreensaver.xcodeproj/xcshareddata/xcschemes/*.xcscheme
  19: Create a man page from the XML with xml2man.pl, and update Makefile.in.
- 20: Upload a YouTube video: -record-animation 3600 -geom 1920x1080+128+64
+ 20: Upload a YouTube video: -record-animation 2min -geom 1920x1080+128+64
      ./upload-video.pl NAME
index 2c39711f2cc08e480ec42ac1f0beeced81388c5a..27ade40407dc22fb7b3de41457482bcc1b22b8b6 100644 (file)
@@ -17,7 +17,7 @@
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>LSMinimumSystemVersion</key>
        <string>${MACOSX_DEPLOYMENT_TARGET}</string>
        <key>NSPrincipalClass</key>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.entertainment</string>
        <key>CFBundleShortVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleLongVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleGetInfoString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSHumanReadableCopyright</key>
-       <string>6.09</string>
+       <string>6.10</string>
 </dict>
 </plist>
index 233e6267d0eae6da1b3ccf67305524a2d9302fc4..44da19f09f4da6c1070b33de097b78d2a3fa64a6 100644 (file)
@@ -1517,6 +1517,7 @@ FAIL:
 
 # ifndef HAVE_IPHONE
   int window_count = ([saverNames count] <= 1 ? 1 : 2);
+
   NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
                         retain];
   windows = a;
index 7f764c37daff58fa2b1dc534a3c43d31ff5f1023..97da30847b2782ff9b374afbcf9ab8d1afd2922b 100644 (file)
@@ -17,7 +17,7 @@
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>LSMinimumSystemVersion</key>
        <string>${MACOSX_DEPLOYMENT_TARGET}</string>
        <key>NSPrincipalClass</key>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.entertainment</string>
        <key>CFBundleShortVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleLongVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleGetInfoString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSHumanReadableCopyright</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSMainNibFile</key>
        <string>SaverRunner</string>
        <key>CFBundleIconFile</key>
index 5f6d8e7accb5ff910a45011fba0d19eef95f8374..7d1071e54a758423b7078ee9c101476daeabf3ec 100644 (file)
@@ -17,7 +17,7 @@
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>LSMinimumSystemVersion</key>
        <string>${MACOSX_DEPLOYMENT_TARGET}</string>
        <key>NSPrincipalClass</key>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.entertainment</string>
        <key>CFBundleShortVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleLongVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleGetInfoString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSHumanReadableCopyright</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSMainNibFile</key>
        <string>Updater</string>
        <key>CFBundleIconFile</key>
index d8269a00b938f19e2f99c5b0aaf1f7c8eaf886fb..e86aaa0e4ebe2eff0856590a75281ffdef399f53 100644 (file)
@@ -17,7 +17,7 @@
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>LSMinimumSystemVersion</key>
        <string>${MACOSX_DEPLOYMENT_TARGET}</string>
        <key>NSPrincipalClass</key>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.entertainment</string>
        <key>CFBundleShortVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleLongVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleGetInfoString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSHumanReadableCopyright</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSMainNibFile</key>
        <string>SaverRunner</string>
 </dict>
index 0e3d791bb66cde79267a87a7f1bc22982fc47ca8..036289680943a4fa4c3c68d09cd7f8d8a7aa4946 100644 (file)
 \b0 by Jamie Zawinski\
 and many others\
 \
-version 6.09\
-07-Jun-2024\
+version 6.10\
+27-Apr-2025\
 \
 {\field{\*\fldinst{HYPERLINK "https://www.jwz.org/xscreensaver/"}}{\fldrslt \cf2 \ul \ulc2 https://www.jwz.org/xscreensaver/}}\
 \pard\pardeftab720
 \cf0 \
 
-\b To install all 250+ screen savers:\
+\b To install all 260+ screen savers:\
 \pard\pardeftab720\li360
 
 \b0 \cf0 \
@@ -84,12 +84,13 @@ available there.\
 \pard\pardeftab720\li360
 
 \b0 \cf0 \
-XScreenSaver also runs on iOS and Android. The iOS version is available in the 
+XScreenSaver also runs on iOS and Android. Free downloads in the 
 {\field{\*\fldinst{HYPERLINK 
  "https://itunes.apple.com/app/xscreensaver/id539014593?mt=8"}}
-{\fldrslt \cf2 \ul \ulc2 iTunes App Store}} 
-and the Android version is available in the 
+{\fldrslt \cf2 \ul \ulc2 Apple Store}} 
+and on the 
 {\field{\*\fldinst{HYPERLINK 
- "https://play.google.com/store/apps/details?id=org.jwz.android.xscreensaver"}}
-{\fldrslt \cf2 \ul \ulc2 Google Play Store}}, and they're both free!
+ "https://www.jwz.org/xscreensaver/"}}
+{\fldrslt \cf2 \ul \ulc2 XScreenSaver web site}}, 
+respectively.
 }
index 4b87f8ca8a757f26d78fe0f5a5e73127a14c49d8..0ebfa919039c3b6095ed5ec48a256ea4aa23124c 100644 (file)
@@ -3,6 +3,6 @@
 <plist version="1.0">
 <dict>
        <key>URL</key>
-       <string>https://play.google.com/store/apps/details?id=org.jwz.android.xscreensaver</string>
+       <string>https://www.jwz.org/xscreensaver/download.html</string>
 </dict>
 </plist>
index e000aeb6ed6fe11a29cacaf816b1d292e08b587e..2495d6c2a2c8ceb584cb278ac270b76720ca8ad9 100644 (file)
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.entertainment</string>
        <key>CFBundleShortVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleLongVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleGetInfoString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSHumanReadableCopyright</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSMainNibFile</key>
        <string>iSaverRunner</string>
        <key>CFBundleDisplayName</key>
index 2ac081784515591fe47190900e8f0abea271045f..391ddd1937cbf2e561bbafdf9f58658f4420e775 100644 (file)
@@ -25,10 +25,10 @@ XScreenSaver also runs on iOS and Android. Free downloads in the
 {\field{\*\fldinst{HYPERLINK 
  "https://itunes.apple.com/app/xscreensaver/id539014593?mt=8"}}
 {\fldrslt \cf2 \ul \ulc2 Apple Store}} 
-and 
+and on the 
 {\field{\*\fldinst{HYPERLINK 
- "https://play.google.com/store/apps/details?id=org.jwz.android.xscreensaver"}}
-{\fldrslt \cf2 \ul \ulc2 Google Store}} 
+ "https://www.jwz.org/xscreensaver/"}}
+{\fldrslt \cf2 \ul \ulc2 XScreenSaver web site}}, 
 respectively.  Source code for all versions is available on the 
 {\field{\*\fldinst{HYPERLINK "https://www.jwz.org/xscreensaver/"}}{\fldrslt \cf2 \ul \ulc2 XScreenSaver web site}}. 
 }
index 12f14dea030389dae8847c6dc531e49e2c166bb2..716b91062b2da21fb289d04dc058aa0a54fa9fa8 100644 (file)
@@ -1,5 +1,5 @@
 /* Generated file, do not edit.
-   Created: Mon May  6 11:02:53 2024 by build-fntable.pl 1.14.
+   Created: Sun Apr 27 13:13:54 2025 by build-fntable.pl 1.14.
  */
 
 #import <Foundation/Foundation.h>
@@ -72,6 +72,7 @@ extern struct xscreensaver_function_table
  dnalogo_xscreensaver_function_table,
  drift_xscreensaver_function_table,
  droste_xscreensaver_function_table,
+ dumpsterfire_xscreensaver_function_table,
  dymaxionmap_xscreensaver_function_table,
  endgame_xscreensaver_function_table,
  energystream_xscreensaver_function_table,
@@ -126,6 +127,7 @@ extern struct xscreensaver_function_table
  highvoltage_xscreensaver_function_table,
  hilbert_xscreensaver_function_table,
  hopalong_xscreensaver_function_table,
+ hopffibration_xscreensaver_function_table,
  hydrostat_xscreensaver_function_table,
  hypertorus_xscreensaver_function_table,
  hypnowheel_xscreensaver_function_table,
@@ -142,6 +144,7 @@ extern struct xscreensaver_function_table
  kaleidocycle_xscreensaver_function_table,
  kallisti_xscreensaver_function_table,
  klein_xscreensaver_function_table,
+ klondike_xscreensaver_function_table,
  kumppa_xscreensaver_function_table,
  lament_xscreensaver_function_table,
  lavalite_xscreensaver_function_table,
@@ -178,6 +181,7 @@ extern struct xscreensaver_function_table
  piecewise_xscreensaver_function_table,
  pinion_xscreensaver_function_table,
  pipes_xscreensaver_function_table,
+ platonicfolding_xscreensaver_function_table,
  polyhedra_xscreensaver_function_table,
  polyominoes_xscreensaver_function_table,
  polytopes_xscreensaver_function_table,
@@ -329,6 +333,7 @@ NSDictionary *make_function_table_dict(void) {
        @"DNA Logo":    [NSValue valueWithPointer:&dnalogo_xscreensaver_function_table],
        @"Drift":       [NSValue valueWithPointer:&drift_xscreensaver_function_table],
        @"Droste":      [NSValue valueWithPointer:&droste_xscreensaver_function_table],
+       @"Dumpster Fire":       [NSValue valueWithPointer:&dumpsterfire_xscreensaver_function_table],
        @"Dymaxion Map":        [NSValue valueWithPointer:&dymaxionmap_xscreensaver_function_table],
        @"Endgame":     [NSValue valueWithPointer:&endgame_xscreensaver_function_table],
        @"Energy Stream":       [NSValue valueWithPointer:&energystream_xscreensaver_function_table],
@@ -383,6 +388,7 @@ NSDictionary *make_function_table_dict(void) {
        @"High Voltage":        [NSValue valueWithPointer:&highvoltage_xscreensaver_function_table],
        @"Hilbert":     [NSValue valueWithPointer:&hilbert_xscreensaver_function_table],
        @"Hopalong":    [NSValue valueWithPointer:&hopalong_xscreensaver_function_table],
+       @"Hopf Fibration":      [NSValue valueWithPointer:&hopffibration_xscreensaver_function_table],
        @"Hydrostat":   [NSValue valueWithPointer:&hydrostat_xscreensaver_function_table],
        @"Hypertorus":  [NSValue valueWithPointer:&hypertorus_xscreensaver_function_table],
        @"Hypnowheel":  [NSValue valueWithPointer:&hypnowheel_xscreensaver_function_table],
@@ -399,6 +405,7 @@ NSDictionary *make_function_table_dict(void) {
        @"Kaleidocycle":        [NSValue valueWithPointer:&kaleidocycle_xscreensaver_function_table],
        @"Kallisti":    [NSValue valueWithPointer:&kallisti_xscreensaver_function_table],
        @"Klein":       [NSValue valueWithPointer:&klein_xscreensaver_function_table],
+       @"Klondike":    [NSValue valueWithPointer:&klondike_xscreensaver_function_table],
        @"Kumppa":      [NSValue valueWithPointer:&kumppa_xscreensaver_function_table],
        @"Lament":      [NSValue valueWithPointer:&lament_xscreensaver_function_table],
        @"Lavalite":    [NSValue valueWithPointer:&lavalite_xscreensaver_function_table],
@@ -435,6 +442,7 @@ NSDictionary *make_function_table_dict(void) {
        @"Piecewise":   [NSValue valueWithPointer:&piecewise_xscreensaver_function_table],
        @"Pinion":      [NSValue valueWithPointer:&pinion_xscreensaver_function_table],
        @"Pipes":       [NSValue valueWithPointer:&pipes_xscreensaver_function_table],
+       @"Platonic Folding":    [NSValue valueWithPointer:&platonicfolding_xscreensaver_function_table],
        @"Polyhedra":   [NSValue valueWithPointer:&polyhedra_xscreensaver_function_table],
        @"Polyominoes": [NSValue valueWithPointer:&polyominoes_xscreensaver_function_table],
        @"Polytopes":   [NSValue valueWithPointer:&polytopes_xscreensaver_function_table],
diff --git a/OSX/perl-minimize.pl b/OSX/perl-minimize.pl
new file mode 100755 (executable)
index 0000000..bbfc5d3
--- /dev/null
@@ -0,0 +1,139 @@
+#!/opt/local/bin/perl -w
+# Copyright © 2025 Jamie Zawinski <jwz@jwz.org>
+#
+# Permission to use, copy, modify, distribute, and sell this software and its
+# documentation for any purpose is hereby granted without fee, provided that
+# the above copyright notice appear in all copies and that both that
+# copyright notice and this permission notice appear in supporting
+# documentation.  No representations are made about the suitability of this
+# software for any purpose.  It is provided "as is" without express or 
+# implied warranty.
+#
+# Created: 25-Feb-2025.
+
+require 5;
+use diagnostics;
+use strict;
+
+my $progname = $0; $progname =~ s@.*/@@g;
+my ($version) = ('$Revision: 1.2 $' =~ m/\s(\d[.\d]+)\s/s);
+
+my $verbose = 0;
+
+BEGIN { eval 'use Perl::Tidy;' }
+
+sub minimize($$) {
+  my ($in, $out) = @_;
+
+  if (!defined($Perl::Tidy::VERSION)) {
+    print STDERR "$progname: Perl::Tidy not installed, skipping...\n";
+    return;
+  }
+
+  open (my $inf, "<:utf8", $in) || error ("$in: $!");
+  local $/ = undef;  # read entire file
+  my $body = <$inf>;
+  close $inf;
+
+  my $body2;
+  my $err = Perl::Tidy::perltidy (
+    argv        => join (' ',
+                         '--mangle',
+                         '--delete-all-comments',
+                       ),
+    source      => \$body,
+    destination => \$body2,
+    );
+  error ($err) if $err;
+
+  # Find all functions and variables, and shorten them.
+  #
+  my @subs = ($body2 =~ m/ \b sub \s+ ([a-zA-Z\d_]+) \s* [\{\(] /gsx);
+  my @vars = ($body2 =~ m/ \b my \s* [\$#%@] ([a-zA-Z\d_]+) [\s;,=] /gsx);
+  my @args = ($body2 =~ m/ \b my \s* \( ( .*? ) \) /gsx);
+
+  foreach my $tt (@args) {
+    foreach my $t (split (/\s*,\s*/, $tt)) {
+      $t =~ s/^[\$#%@]//s;
+      $t =~ s/=.*//s;
+      push @vars, $t;
+    }
+  }
+
+  my %tokens;
+  my $i = 0;
+  foreach my $t (@vars) {
+    if (length($t) < 4) {
+      print STDERR "$progname: skip var $t\n" if ($verbose > 1);
+      next;
+    }
+    $tokens{$t} = sprintf ("V%X", $i++);
+  }
+
+  $i = 0;
+  foreach my $t (@subs) {
+    if (length($t) < 4) {
+      print STDERR "$progname: skip sub $t\n" if ($verbose > 1);
+      next;
+    }
+    next if (length($t) < 4);
+    $tokens{$t} = sprintf ("F%X", $i++);
+  }
+
+  foreach my $k (sort keys %tokens) {
+    my $v = $tokens{$k};
+    if ($v =~ m/^S/) {
+      $body2 =~ s/ ( [^a-zA-Z\d_] ) ( $k ) ( [^a-zA-Z\d_] ) /$1$v$3/gsx;
+    } else {
+      $body2 =~ s/ ( [\$#%@] \{? )  ( $k ) ( [^a-zA-Z\d_] ) /$1$v$3/gsx;
+    }
+    print STDERR "$progname: $k => $v\n" if ($verbose > 1);
+  }
+
+  $in =~ s@^.*/@@s;
+  my $tag = "# DO NOT EDIT -- auto-generated from \"$in\"\n\n";
+  $body2 =~ s/^([^\n]+\n)/$1$tag/s;
+
+  $body2 =~ s/\n\n+/\n/gs;  # Why is it double-spaced?
+
+  if ($out eq '-') {
+    print STDOUT $body2;
+  } else {
+    open (my $outf, ">:utf8", $out) || error ("$out: $!");
+    print $outf $body2;
+    close $outf;
+    print STDERR "$progname: wrote $out\n";
+  }
+}
+
+
+sub error($) {
+  my ($err) = @_;
+  print STDERR "$progname: $err\n";
+  exit 1;
+}
+
+sub usage() {
+  print STDERR "usage: $progname [--verbose] in.pl out.pl\n";
+  exit 1;
+}
+
+sub main() {
+  my ($in, $out);
+  while (@ARGV) {
+    $_ = shift @ARGV;
+    if (m/^--?verbose$/s) { $verbose++; }
+    elsif (m/^-v+$/s) { $verbose += length($_)-1; }
+#   elsif (m/^--?debug$/s) { $debug_p++; }
+    elsif (m/^-./s) { usage; }
+    elsif (!$in)  { $in  = $_; }
+    elsif (!$out) { $out = $_; }
+    else { usage; }
+  }
+
+  usage unless ($out);
+  minimize ($in, $out);
+}
+
+main();
+exit 0;
index 474b047ec412026a138b2246c36c1f4db944b8f9..9bc514e0b3495052f5a3c05f0b5e05597b433c64 100644 (file)
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>LSApplicationCategoryType</key>
        <string>public.app-category.entertainment</string>
        <key>CFBundleShortVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleLongVersionString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleGetInfoString</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>NSHumanReadableCopyright</key>
-       <string>6.09</string>
+       <string>6.10</string>
        <key>CFBundleDisplayName</key>
        <string>${PRODUCT_NAME}</string>
        <key>CFBundleIcons</key>
index ccb86a2cf710c84d2995b76f614b4fe297a4a04d..b8e09ef74434bbdbde856f7b01aa0af5f61572be 100755 (executable)
@@ -1,5 +1,5 @@
 #!/usr/bin/perl -w
-# Copyright © 2006-2023 Jamie Zawinski <jwz@jwz.org>
+# Copyright © 2006-2025 Jamie Zawinski <jwz@jwz.org>
 #
 # Permission to use, copy, modify, distribute, and sell this software and its
 # documentation for any purpose is hereby granted without fee, provided that
@@ -27,7 +27,7 @@ use IO::Compress::Gzip qw(gzip $GzipError);
 
 my ($exec_dir, $progname) = ($0 =~ m@^(.*?)/([^/]+)$@);
 
-my ($version) = ('$Revision: 1.58 $' =~ m/\s(\d[.\d]+)\s/s);
+my ($version) = ('$Revision: 1.60 $' =~ m/\s(\d[.\d]+)\s/s);
 
 $ENV{PATH} = "/usr/local/bin:$ENV{PATH}";   # for seticon
 $ENV{PATH} = "/opt/local/bin:$ENV{PATH}";   # for macports wget
@@ -306,6 +306,30 @@ sub compress_all_xml_files($) {
 }
 
 
+sub minimize_scripts($) {
+  my ($dir) = @_;
+  my $d2 = $dir . '/Contents/Resources';
+  $dir = $d2 if (-d $d2);
+  opendir (my $dirp, $dir) || error ("$dir: $!");
+  my @files = readdir ($dirp);
+  closedir $dirp;
+  foreach my $f (sort @files) {
+    next if ($f =~ m/^\./s);
+    next if ($f eq 'XScreenSaver');
+    $f = "$dir/$f";
+    next if ($f =~ m/\.(xml|png|jpg|ttf)$/s);
+    next if (-d $f);
+    next unless (-x $f);       # Assume executable files are Perl scripts
+    my @cmd = "$exec_dir/perl-minimize.pl";
+    push @cmd, '--verbose' if ($verbose > 2);
+    push @cmd, ($f, $f);
+    print STDERR "$progname: exec: " . join(' ', @cmd) . "\n"
+      if ($verbose > 1);
+    system (@cmd);
+  }
+}
+
+
 sub set_plist_key($$$$) {
   my ($filename, $body, $key, $val) = @_;
 
@@ -395,9 +419,6 @@ sub update($) {
   my $vers = $1;
   my ($ignore, $info_str, $name) = update_saver_xml ($app_dir, $vers);
 
-  # No, don't do this -- the iOS version reads the XML file in a few
-  # different places, and most of those places don't understand gzip.
-
   if ($app_name eq 'XScreenSaver') {
     compress_all_xml_files ($app_dir);
   } else {
@@ -450,6 +471,8 @@ sub update($) {
     }
   }
 
+  minimize_scripts ($app_dir);
+
   # MacOS 10.12: codesign says "resource fork, Finder information, or
   # similar detritus not allowed" if any bundle has an Icon\r file.
   # set_icon ($app_dir);
index 3279d9c0f66c2a1b3f6a56a1ff21260badc21e27..1971a9a15e7fea05a736e461ef651ce633ab4edb 100644 (file)
@@ -7,6 +7,17 @@
   <link>https://www.jwz.org/xscreensaver/updates.xml</link>
   <description>Updates to xscreensaver.</description>
   <language>en</language>
+  <item>
+   <title>Version 6.10</title>
+   <link>https://www.jwz.org/xscreensaver/xscreensaver-6.10.dmg</link>
+   <description><![CDATA[&bull; New hacks, `dumpsterfire', `hopffibration', `platonicfolding' and `klondike'. <BR>&bull; Rewrote the VT100 emulator for 'apple2' and 'phosphor'. Supports inverse and DEC Special Graphics. <BR>&bull; BSOD supports systemd and bitlocker. ]]></description>
+   <pubDate>Sun, 27 Apr 2025 17:04:18 -0700</pubDate>
+   <enclosure url="https://www.jwz.org/xscreensaver/xscreensaver-6.10.dmg"
+    sparkle:version="6.10"
+    sparkle:edSignature="7xGV6QfwYePmr/DRgisDoL7cuLoHelN9ftdzzUBxpvjtI8oVzWTjcO545qvbiLYAosgXdie8ZP9XUF9Yor9lAA=="
+    length="102575147"
+    type="application/octet-stream" />
+  </item>
   <item>
    <title>Version 6.09</title>
    <link>https://www.jwz.org/xscreensaver/xscreensaver-6.09.dmg</link>
     length="90634351"
     type="application/octet-stream" />
   </item>
-  <item>
-   <title>Version 6.07</title>
-   <link>https://www.jwz.org/xscreensaver/xscreensaver-6.07.dmg</link>
-   <description><![CDATA[&bull; New hacks, `droste', `skulloop', `papercube' and `cubocteversion'. <BR>&bull; `xscreensaver-settings' was sometimes turning off the DPMS checkbox. <BR>&bull; Log pid of caller of `deactivate' command, to give a hint about who is preventing the screen from blanking. <BR>&bull; recanim uses libffmpeg. <BR>&bull; Updates to `sphereeversion'. <BR>&bull; Added some new map sources to `mapscroller'. <BR>&bull; Worked around a macOS 13.4 bug where multi-head systems would fail to launch savers on some or all screens. <BR>&bull; Minimum compiler target is now ISO C99 instead of ANSI C89. Didn't want to rush into it. <BR>&bull; macOS, Android: Better looking thunbmail images. <BR>&bull; Various other minor bug fixes.]]></description>
-   <pubDate>Tue, 29 Aug 2023 18:06:48 -0700</pubDate>
-   <enclosure url="https://www.jwz.org/xscreensaver/xscreensaver-6.07.dmg"
-    sparkle:version="6.07"
-    sparkle:edSignature="HSb5FJ/VaOJ/V3s+8EVrelYhf1/vswYWBWFDrKq1PLeAd8fhOQ9BvS4bdzxSbnwL361lSoIKkip61eQIay7mAQ=="
-    length="88624232"
-    type="application/octet-stream" />
-  </item>
  </channel>
 </rss>
index d0ea840fa3a92be849ef4cd7ce3968b6f25ac0af..7d6d4b60320c2414b345b90db0f3c81daa00e7e1 100644 (file)
                                AFF449FE22754B5600DB8EDB /* PBXTargetDependency */,
                                AF3938381D0FBF5300205406 /* PBXTargetDependency */,
                                AF777A3F09B660B500EA3033 /* PBXTargetDependency */,
+                               AFCB48BB2DBC0C3E006296D3 /* PBXTargetDependency */,
                                AFEC23EB1CB6ED0800DE138F /* PBXTargetDependency */,
                                AF777A3D09B660B500EA3033 /* PBXTargetDependency */,
                                AFACE8911CC8365F008B24CD /* PBXTargetDependency */,
                                AF1B0FC51D7AB5740011DBE4 /* PBXTargetDependency */,
                                AF0BF6ED29456D50000D9473 /* PBXTargetDependency */,
                                AF42CF712BE8A56100675AC7 /* PBXTargetDependency */,
+                               AFA82E402DB1ECC600BEEE3E /* PBXTargetDependency */,
                                AF4F10F0143450C300E34F3F /* PBXTargetDependency */,
                                AFC0E8C91CDC6125008CAFAC /* PBXTargetDependency */,
                                AF777A1709B660B300EA3033 /* PBXTargetDependency */,
                                AFBFE7421786407000432B21 /* PBXTargetDependency */,
                                AFC644742AE47A1000D589B9 /* PBXTargetDependency */,
                                AF777A1109B660B300EA3033 /* PBXTargetDependency */,
+                               AF2D86B82DB2105300B4F248 /* PBXTargetDependency */,
                                AF777A0F09B660B200EA3033 /* PBXTargetDependency */,
                                AF777A0D09B660B200EA3033 /* PBXTargetDependency */,
                                AF4FD6FF0CE7A4F9005EE58E /* PBXTargetDependency */,
                                AFD51B350F063B7800471C02 /* PBXTargetDependency */,
                                AF7779FF09B660B200EA3033 /* PBXTargetDependency */,
                                AF7779FD09B660B100EA3033 /* PBXTargetDependency */,
+                               AF2D86942DB2032D00B4F248 /* PBXTargetDependency */,
                                AF7779FB09B660B100EA3033 /* PBXTargetDependency */,
                                AF7779F909B660B100EA3033 /* PBXTargetDependency */,
                                AFFAB33519158F1E0020F021 /* PBXTargetDependency */,
                AF2D0D40241D7D7F0001D8B8 /* etruscanvenus.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D0D3F241D7D7F0001D8B8 /* etruscanvenus.c */; };
                AF2D0D41241D7D7F0001D8B8 /* etruscanvenus.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D0D3F241D7D7F0001D8B8 /* etruscanvenus.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
                AF2D29E6270BE727000B8588 /* tvLaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AF2D29E5270BE726000B8588 /* tvLaunchScreen.storyboard */; };
+               AF2D867A2DB1FCC900B4F248 /* XScreenSaverSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = AF9CC7A0099580E70075E99B /* XScreenSaverSubclass.m */; };
+               AF2D867C2DB1FCC900B4F248 /* libjwxyz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AF4808C1098C3B6C00FB32B8 /* libjwxyz.a */; };
+               AF2D867D2DB1FCC900B4F248 /* ScreenSaver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF976ED30989BF59001F8B92 /* ScreenSaver.framework */; };
+               AF2D867E2DB1FCC900B4F248 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF2C31E515C0F7FE007A6896 /* QuartzCore.framework */; };
+               AF2D867F2DB1FCC900B4F248 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
+               AF2D86802DB1FCC900B4F248 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF48112B0990A2C700FB32B8 /* Carbon.framework */; };
+               AF2D86812DB1FCC900B4F248 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE0BC611A6B0D6200C098BF /* OpenGL.framework */; };
+               AF2D86822DB1FCC900B4F248 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = AF78369617DB9F25003B9FC0 /* libz.dylib */; };
+               AF2D868B2DB1FDB900B4F248 /* platonicfolding.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86892DB1FDB900B4F248 /* platonicfolding.c */; };
+               AF2D868C2DB1FDB900B4F248 /* platonicfolding.xml in Resources */ = {isa = PBXBuildFile; fileRef = AF2D868A2DB1FDB900B4F248 /* platonicfolding.xml */; };
+               AF2D868D2DB1FDB900B4F248 /* platonicfolding.xml in Resources */ = {isa = PBXBuildFile; fileRef = AF2D868A2DB1FDB900B4F248 /* platonicfolding.xml */; };
+               AF2D868E2DB1FDB900B4F248 /* platonicfolding.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86892DB1FDB900B4F248 /* platonicfolding.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AF2D868F2DB1FDB900B4F248 /* platonicfolding.xml in Resources */ = {isa = PBXBuildFile; fileRef = AF2D868A2DB1FDB900B4F248 /* platonicfolding.xml */; };
+               AF2D86902DB1FDB900B4F248 /* platonicfolding.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86892DB1FDB900B4F248 /* platonicfolding.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AF2D86912DB2013200B4F248 /* xftwrap.c in Sources */ = {isa = PBXBuildFile; fileRef = AFBD953C2C504ADC000DA52A /* xftwrap.c */; };
+               AF2D86922DB2014100B4F248 /* xftwrap.c in Sources */ = {isa = PBXBuildFile; fileRef = AFBD953C2C504ADC000DA52A /* xftwrap.c */; };
+               AF2D869C2DB20F2100B4F248 /* XScreenSaverSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = AF9CC7A0099580E70075E99B /* XScreenSaverSubclass.m */; };
+               AF2D869E2DB20F2100B4F248 /* libjwxyz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AF4808C1098C3B6C00FB32B8 /* libjwxyz.a */; };
+               AF2D869F2DB20F2100B4F248 /* ScreenSaver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF976ED30989BF59001F8B92 /* ScreenSaver.framework */; };
+               AF2D86A02DB20F2100B4F248 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF2C31E515C0F7FE007A6896 /* QuartzCore.framework */; };
+               AF2D86A12DB20F2100B4F248 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
+               AF2D86A22DB20F2100B4F248 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF48112B0990A2C700FB32B8 /* Carbon.framework */; };
+               AF2D86A32DB20F2100B4F248 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE0BC611A6B0D6200C098BF /* OpenGL.framework */; };
+               AF2D86A42DB20F2100B4F248 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = AF78369617DB9F25003B9FC0 /* libz.dylib */; };
+               AF2D86AE2DB2100A00B4F248 /* klondike.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86AB2DB2100A00B4F248 /* klondike.c */; };
+               AF2D86AF2DB2100A00B4F248 /* klondike-game.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86AD2DB2100A00B4F248 /* klondike-game.c */; };
+               AF2D86B02DB2100A00B4F248 /* klondike.xml in Resources */ = {isa = PBXBuildFile; fileRef = AF2D86AC2DB2100A00B4F248 /* klondike.xml */; };
+               AF2D86B12DB2100A00B4F248 /* klondike.xml in Resources */ = {isa = PBXBuildFile; fileRef = AF2D86AC2DB2100A00B4F248 /* klondike.xml */; };
+               AF2D86B22DB2100A00B4F248 /* klondike.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86AB2DB2100A00B4F248 /* klondike.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AF2D86B32DB2100A00B4F248 /* klondike-game.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86AD2DB2100A00B4F248 /* klondike-game.c */; };
+               AF2D86B42DB2100A00B4F248 /* klondike.xml in Resources */ = {isa = PBXBuildFile; fileRef = AF2D86AC2DB2100A00B4F248 /* klondike.xml */; };
+               AF2D86B52DB2100A00B4F248 /* klondike.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86AB2DB2100A00B4F248 /* klondike.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AF2D86B62DB2100A00B4F248 /* klondike-game.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D86AD2DB2100A00B4F248 /* klondike-game.c */; };
                AF2D8F321CEBA10300198014 /* jwxyz-timers.c in Sources */ = {isa = PBXBuildFile; fileRef = AF2D8F301CEBA10300198014 /* jwxyz-timers.c */; };
                AF2D8F331CEBA10300198014 /* jwxyz-timers.h in Headers */ = {isa = PBXBuildFile; fileRef = AF2D8F311CEBA10300198014 /* jwxyz-timers.h */; };
                AF32D9E70F3AD0B40080F535 /* XScreenSaverSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = AF9CC7A0099580E70075E99B /* XScreenSaverSubclass.m */; };
                AF5C9B121A0CCF4E00B0147A /* cityflow.xml in Resources */ = {isa = PBXBuildFile; fileRef = AF5C9B0F1A0CCF4E00B0147A /* cityflow.xml */; };
                AF5C9B131A0CCF4E00B0147A /* cityflow.c in Sources */ = {isa = PBXBuildFile; fileRef = AF5C9B101A0CCF4E00B0147A /* cityflow.c */; };
                AF5C9B141A0CCF4E00B0147A /* cityflow.c in Sources */ = {isa = PBXBuildFile; fileRef = AF5C9B101A0CCF4E00B0147A /* cityflow.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AF5D4E3C2D6409AE009BE57E /* ansi-tty.c in Sources */ = {isa = PBXBuildFile; fileRef = AF5D4E3B2D6409AE009BE57E /* ansi-tty.c */; };
+               AF5D4E3D2D6409AE009BE57E /* ansi-tty.c in Sources */ = {isa = PBXBuildFile; fileRef = AF5D4E3B2D6409AE009BE57E /* ansi-tty.c */; };
+               AF5D4E3E2D6409AE009BE57E /* ansi-tty.c in Sources */ = {isa = PBXBuildFile; fileRef = AF5D4E3B2D6409AE009BE57E /* ansi-tty.c */; };
+               AF5D4E3F2D6409AE009BE57E /* ansi-tty.c in Sources */ = {isa = PBXBuildFile; fileRef = AF5D4E3B2D6409AE009BE57E /* ansi-tty.c */; };
                AF5ECEB02116B1A400069433 /* XScreenSaverSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = AF9CC7A0099580E70075E99B /* XScreenSaverSubclass.m */; };
                AF5ECEB12116B1A400069433 /* analogtv.c in Sources */ = {isa = PBXBuildFile; fileRef = AF9D4CFA09B5AC94006E59CF /* analogtv.c */; };
                AF5ECEB42116B1A400069433 /* libjwxyz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AF4808C1098C3B6C00FB32B8 /* libjwxyz.a */; };
                AFA6AAFF20999950006D2685 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = AF78369617DB9F25003B9FC0 /* libz.dylib */; };
                AFA6AB0D20999A60006D2685 /* glitchpeg.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFA6AB0C20999A60006D2685 /* glitchpeg.xml */; };
                AFA6AB0F20999A7B006D2685 /* glitchpeg.c in Sources */ = {isa = PBXBuildFile; fileRef = AFA6AB0E20999A7B006D2685 /* glitchpeg.c */; };
+               AFA82E242DB1EB4B00BEEE3E /* XScreenSaverSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = AF9CC7A0099580E70075E99B /* XScreenSaverSubclass.m */; };
+               AFA82E262DB1EB4B00BEEE3E /* libjwxyz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AF4808C1098C3B6C00FB32B8 /* libjwxyz.a */; };
+               AFA82E272DB1EB4B00BEEE3E /* ScreenSaver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF976ED30989BF59001F8B92 /* ScreenSaver.framework */; };
+               AFA82E282DB1EB4B00BEEE3E /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF2C31E515C0F7FE007A6896 /* QuartzCore.framework */; };
+               AFA82E292DB1EB4B00BEEE3E /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
+               AFA82E2A2DB1EB4B00BEEE3E /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF48112B0990A2C700FB32B8 /* Carbon.framework */; };
+               AFA82E2B2DB1EB4B00BEEE3E /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE0BC611A6B0D6200C098BF /* OpenGL.framework */; };
+               AFA82E2C2DB1EB4B00BEEE3E /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = AF78369617DB9F25003B9FC0 /* libz.dylib */; };
+               AFA82E362DB1EC3B00BEEE3E /* hopffibration.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFA82E352DB1EC3B00BEEE3E /* hopffibration.xml */; };
+               AFA82E372DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c in Sources */ = {isa = PBXBuildFile; fileRef = AFA82E342DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AFA82E382DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c in Sources */ = {isa = PBXBuildFile; fileRef = AFA82E332DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AFA82E392DB1EC3B00BEEE3E /* hopffibration.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFA82E352DB1EC3B00BEEE3E /* hopffibration.xml */; };
+               AFA82E3A2DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c in Sources */ = {isa = PBXBuildFile; fileRef = AFA82E342DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c */; };
+               AFA82E3B2DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c in Sources */ = {isa = PBXBuildFile; fileRef = AFA82E332DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c */; };
+               AFA82E3C2DB1EC3B00BEEE3E /* hopffibration.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFA82E352DB1EC3B00BEEE3E /* hopffibration.xml */; };
+               AFA82E3D2DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c in Sources */ = {isa = PBXBuildFile; fileRef = AFA82E342DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AFA82E3E2DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c in Sources */ = {isa = PBXBuildFile; fileRef = AFA82E332DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
                AFAA6B451773F07800DE720C /* ios-function-table.m in Sources */ = {isa = PBXBuildFile; fileRef = AFAA6B441773F07700DE720C /* ios-function-table.m */; };
                AFAAE38E207D6343007A515C /* XScreenSaverSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = AF9CC7A0099580E70075E99B /* XScreenSaverSubclass.m */; };
                AFAAE390207D6343007A515C /* libjwxyz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AF4808C1098C3B6C00FB32B8 /* libjwxyz.a */; };
                AFB8A69B1782BA34004EDB85 /* kaleidocycle.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFB8A69A1782BA34004EDB85 /* kaleidocycle.xml */; };
                AFB8A69C1782BF6C004EDB85 /* kaleidocycle.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFB8A69A1782BA34004EDB85 /* kaleidocycle.xml */; };
                AFB8A69D1782BFA6004EDB85 /* kaleidocycle.c in Sources */ = {isa = PBXBuildFile; fileRef = AF7511141782B64300380EA1 /* kaleidocycle.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AFBD953D2C504AFC000DA52A /* xftwrap.c in Sources */ = {isa = PBXBuildFile; fileRef = AFBD953C2C504ADC000DA52A /* xftwrap.c */; };
                AFBE744019A7C6930018AA35 /* robot.c in Sources */ = {isa = PBXBuildFile; fileRef = AFBE743F19A7C6930018AA35 /* robot.c */; };
                AFBE744119A7C6EF0018AA35 /* robot.c in Sources */ = {isa = PBXBuildFile; fileRef = AFBE743F19A7C6930018AA35 /* robot.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
                AFBF893E0E41D930006A2D66 /* fps.c in Sources */ = {isa = PBXBuildFile; fileRef = AFBF893C0E41D930006A2D66 /* fps.c */; };
                AFC7592E158D8E8B00C5458E /* textclient.h in Headers */ = {isa = PBXBuildFile; fileRef = AFC7592C158D8E8B00C5458E /* textclient.h */; };
                AFC75930158D9A7A00C5458E /* textclient-ios.m in Sources */ = {isa = PBXBuildFile; fileRef = AFC7592F158D9A7A00C5458E /* textclient-ios.m */; };
                AFCA3EED25856B6200CBCF16 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFCA3EEC25856B6200CBCF16 /* IOKit.framework */; };
+               AFCB489F2DBC0958006296D3 /* XScreenSaverSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = AF9CC7A0099580E70075E99B /* XScreenSaverSubclass.m */; };
+               AFCB48A12DBC0958006296D3 /* libjwxyz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AF4808C1098C3B6C00FB32B8 /* libjwxyz.a */; };
+               AFCB48A22DBC0958006296D3 /* ScreenSaver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF976ED30989BF59001F8B92 /* ScreenSaver.framework */; };
+               AFCB48A32DBC0958006296D3 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF2C31E515C0F7FE007A6896 /* QuartzCore.framework */; };
+               AFCB48A42DBC0958006296D3 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
+               AFCB48A52DBC0958006296D3 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF48112B0990A2C700FB32B8 /* Carbon.framework */; };
+               AFCB48A62DBC0958006296D3 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE0BC611A6B0D6200C098BF /* OpenGL.framework */; };
+               AFCB48A72DBC0958006296D3 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = AF78369617DB9F25003B9FC0 /* libz.dylib */; };
+               AFCB48B12DBC0BDB006296D3 /* dumpster_model.c in Sources */ = {isa = PBXBuildFile; fileRef = AFCB48AE2DBC0BDB006296D3 /* dumpster_model.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AFCB48B22DBC0BDB006296D3 /* dumpsterfire.c in Sources */ = {isa = PBXBuildFile; fileRef = AFCB48AF2DBC0BDB006296D3 /* dumpsterfire.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AFCB48B32DBC0BDB006296D3 /* dumpsterfire.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFCB48B02DBC0BDB006296D3 /* dumpsterfire.xml */; };
+               AFCB48B42DBC0BDB006296D3 /* dumpsterfire.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFCB48B02DBC0BDB006296D3 /* dumpsterfire.xml */; };
+               AFCB48B52DBC0BDB006296D3 /* dumpster_model.c in Sources */ = {isa = PBXBuildFile; fileRef = AFCB48AE2DBC0BDB006296D3 /* dumpster_model.c */; };
+               AFCB48B62DBC0BDB006296D3 /* dumpsterfire.c in Sources */ = {isa = PBXBuildFile; fileRef = AFCB48AF2DBC0BDB006296D3 /* dumpsterfire.c */; };
+               AFCB48B72DBC0BDB006296D3 /* dumpster_model.c in Sources */ = {isa = PBXBuildFile; fileRef = AFCB48AE2DBC0BDB006296D3 /* dumpster_model.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AFCB48B82DBC0BDB006296D3 /* dumpsterfire.c in Sources */ = {isa = PBXBuildFile; fileRef = AFCB48AF2DBC0BDB006296D3 /* dumpsterfire.c */; settings = {COMPILER_FLAGS = "-DUSE_GL"; }; };
+               AFCB48B92DBC0BDB006296D3 /* dumpsterfire.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFCB48B02DBC0BDB006296D3 /* dumpsterfire.xml */; };
                AFCCCBB009BFE4B000353F4D /* rdbomb.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFCCCBAD09BFE4B000353F4D /* rdbomb.xml */; };
                AFCCCBB309BFE51900353F4D /* thornbird.xml in Resources */ = {isa = PBXBuildFile; fileRef = AFC259230988A469000655EE /* thornbird.xml */; };
                AFCE26332337332000BDCE10 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AFCE26322337332000BDCE10 /* LaunchScreen.storyboard */; };
                        remoteGlobalIDString = AF2D0D25241D7C870001D8B8;
                        remoteInfo = EtruscanVenus;
                };
+               AF2D86742DB1FCC900B4F248 /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = AF4808C0098C3B6C00FB32B8;
+                       remoteInfo = jwxyz;
+               };
+               AF2D86932DB2032D00B4F248 /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = AF2D86722DB1FCC900B4F248;
+                       remoteInfo = PlatonicFolding;
+               };
+               AF2D86972DB20F2100B4F248 /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = AF4808C0098C3B6C00FB32B8;
+                       remoteInfo = jwxyz;
+               };
+               AF2D86B72DB2105300B4F248 /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = AF2D86952DB20F2100B4F248;
+                       remoteInfo = Klondike;
+               };
                AF32D9E20F3AD0B40080F535 /* PBXContainerItemProxy */ = {
                        isa = PBXContainerItemProxy;
                        containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
                        remoteGlobalIDString = AFA6AAF020999950006D2685;
                        remoteInfo = GlitchPEG;
                };
+               AFA82E1F2DB1EB4B00BEEE3E /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = AF4808C0098C3B6C00FB32B8;
+                       remoteInfo = jwxyz;
+               };
+               AFA82E3F2DB1ECC600BEEE3E /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = AFA82E1D2DB1EB4B00BEEE3E;
+                       remoteInfo = HopfFibration;
+               };
                AFAAE389207D6343007A515C /* PBXContainerItemProxy */ = {
                        isa = PBXContainerItemProxy;
                        containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
                        remoteGlobalIDString = AF9771D60989DC4A001F8B92;
                        remoteInfo = SaverTester;
                };
+               AFCB489A2DBC0958006296D3 /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = AF4808C0098C3B6C00FB32B8;
+                       remoteInfo = jwxyz;
+               };
+               AFCB48BA2DBC0C3E006296D3 /* PBXContainerItemProxy */ = {
+                       isa = PBXContainerItemProxy;
+                       containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
+                       proxyType = 1;
+                       remoteGlobalIDString = AFCB48982DBC0958006296D3;
+                       remoteInfo = DumpsterFire;
+               };
                AFCF833D1AF5B515008BB7E1 /* PBXContainerItemProxy */ = {
                        isa = PBXContainerItemProxy;
                        containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */;
                AF2D0D3F241D7D7F0001D8B8 /* etruscanvenus.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = etruscanvenus.c; path = hacks/glx/etruscanvenus.c; sourceTree = "<group>"; };
                AF2D29E5270BE726000B8588 /* tvLaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = tvLaunchScreen.storyboard; sourceTree = "<group>"; };
                AF2D522513E954A0002AA818 /* SaverRunner.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = SaverRunner.icns; sourceTree = "<group>"; };
+               AF2D86872DB1FCC900B4F248 /* PlatonicFolding.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PlatonicFolding.saver; sourceTree = BUILT_PRODUCTS_DIR; };
+               AF2D86892DB1FDB900B4F248 /* platonicfolding.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = platonicfolding.c; path = ../hacks/glx/platonicfolding.c; sourceTree = SOURCE_ROOT; };
+               AF2D868A2DB1FDB900B4F248 /* platonicfolding.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = platonicfolding.xml; path = ../hacks/config/platonicfolding.xml; sourceTree = SOURCE_ROOT; };
+               AF2D86A92DB20F2100B4F248 /* Klondike.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Klondike.saver; sourceTree = BUILT_PRODUCTS_DIR; };
+               AF2D86AB2DB2100A00B4F248 /* klondike.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = klondike.c; path = ../hacks/glx/klondike.c; sourceTree = SOURCE_ROOT; };
+               AF2D86AC2DB2100A00B4F248 /* klondike.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = klondike.xml; path = ../hacks/config/klondike.xml; sourceTree = SOURCE_ROOT; };
+               AF2D86AD2DB2100A00B4F248 /* klondike-game.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = "klondike-game.c"; path = "../hacks/glx/klondike-game.c"; sourceTree = SOURCE_ROOT; };
                AF2D8F301CEBA10300198014 /* jwxyz-timers.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "jwxyz-timers.c"; path = "../jwxyz/jwxyz-timers.c"; sourceTree = "<group>"; };
                AF2D8F311CEBA10300198014 /* jwxyz-timers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "jwxyz-timers.h"; path = "../jwxyz/jwxyz-timers.h"; sourceTree = "<group>"; };
                AF32D9F40F3AD0B40080F535 /* RubikBlocks.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RubikBlocks.saver; sourceTree = BUILT_PRODUCTS_DIR; };
                AF5C9B0D1A0CCE6E00B0147A /* Cityflow.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Cityflow.saver; sourceTree = BUILT_PRODUCTS_DIR; };
                AF5C9B0F1A0CCF4E00B0147A /* cityflow.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = cityflow.xml; sourceTree = "<group>"; };
                AF5C9B101A0CCF4E00B0147A /* cityflow.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cityflow.c; path = hacks/glx/cityflow.c; sourceTree = "<group>"; };
+               AF5D4E3B2D6409AE009BE57E /* ansi-tty.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = "ansi-tty.c"; path = "hacks/ansi-tty.c"; sourceTree = "<group>"; };
                AF5ECEC02116B1A400069433 /* VFeedback.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VFeedback.saver; sourceTree = BUILT_PRODUCTS_DIR; };
                AF5ECEC22116B2CC00069433 /* vfeedback.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = vfeedback.c; path = hacks/vfeedback.c; sourceTree = "<group>"; };
                AF5ECEC52116B2FE00069433 /* vfeedback.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = vfeedback.xml; sourceTree = "<group>"; };
                AFA6AB0520999950006D2685 /* GlitchPEG.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GlitchPEG.saver; sourceTree = BUILT_PRODUCTS_DIR; };
                AFA6AB0C20999A60006D2685 /* glitchpeg.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = glitchpeg.xml; sourceTree = "<group>"; };
                AFA6AB0E20999A7B006D2685 /* glitchpeg.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = glitchpeg.c; path = hacks/glitchpeg.c; sourceTree = "<group>"; };
+               AFA82E312DB1EB4B00BEEE3E /* HopfFibration.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HopfFibration.saver; sourceTree = BUILT_PRODUCTS_DIR; };
+               AFA82E332DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hacks/glx/hopfanimations.c; sourceTree = "<group>"; };
+               AFA82E342DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hacks/glx/hopffibration.c; sourceTree = "<group>"; };
+               AFA82E352DB1EC3B00BEEE3E /* hopffibration.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = hopffibration.xml; sourceTree = "<group>"; };
                AFAA6B441773F07700DE720C /* ios-function-table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ios-function-table.m"; sourceTree = "<group>"; };
                AFAAE39C207D6343007A515C /* Maze3D.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Maze3D.saver; sourceTree = BUILT_PRODUCTS_DIR; };
                AFAAE39E207D6420007A515C /* maze3d.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = maze3d.c; path = hacks/glx/maze3d.c; sourceTree = "<group>"; };
                AFB5A0ED0981FF8B00871B16 /* usleep.c */ = {isa = PBXFileReference; fileEncoding = 12; lastKnownFileType = sourcecode.c.c; name = usleep.c; path = utils/usleep.c; sourceTree = "<group>"; };
                AFB5A0EE0981FF8B00871B16 /* usleep.h */ = {isa = PBXFileReference; fileEncoding = 12; lastKnownFileType = sourcecode.c.h; name = usleep.h; path = utils/usleep.h; sourceTree = "<group>"; };
                AFB8A69A1782BA34004EDB85 /* kaleidocycle.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = kaleidocycle.xml; sourceTree = "<group>"; };
+               AFBD953B2C504ADB000DA52A /* xftwrap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = xftwrap.h; path = utils/xftwrap.h; sourceTree = "<group>"; };
+               AFBD953C2C504ADC000DA52A /* xftwrap.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = xftwrap.c; path = utils/xftwrap.c; sourceTree = "<group>"; };
                AFBE743F19A7C6930018AA35 /* robot.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = robot.c; path = hacks/glx/robot.c; sourceTree = "<group>"; };
                AFBF893C0E41D930006A2D66 /* fps.c */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.c; name = fps.c; path = hacks/fps.c; sourceTree = "<group>"; };
                AFBF893D0E41D930006A2D66 /* fps.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; name = fps.h; path = hacks/fps.h; sourceTree = "<group>"; };
                AFC7592C158D8E8B00C5458E /* textclient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = textclient.h; path = utils/textclient.h; sourceTree = "<group>"; };
                AFC7592F158D9A7A00C5458E /* textclient-ios.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "textclient-ios.m"; path = "OSX/textclient-ios.m"; sourceTree = "<group>"; };
                AFCA3EEC25856B6200CBCF16 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
+               AFCB48AC2DBC0958006296D3 /* DumpsterFire.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DumpsterFire.saver; sourceTree = BUILT_PRODUCTS_DIR; };
+               AFCB48AE2DBC0BDB006296D3 /* dumpster_model.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = dumpster_model.c; path = ../hacks/glx/dumpster_model.c; sourceTree = SOURCE_ROOT; };
+               AFCB48AF2DBC0BDB006296D3 /* dumpsterfire.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = dumpsterfire.c; path = ../hacks/glx/dumpsterfire.c; sourceTree = SOURCE_ROOT; };
+               AFCB48B02DBC0BDB006296D3 /* dumpsterfire.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = dumpsterfire.xml; path = ../hacks/config/dumpsterfire.xml; sourceTree = SOURCE_ROOT; };
                AFCCCBAD09BFE4B000353F4D /* rdbomb.xml */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; path = rdbomb.xml; sourceTree = "<group>"; };
                AFCE26322337332000BDCE10 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
                AFCF83501AF5B515008BB7E1 /* SplitFlap.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SplitFlap.saver; sourceTree = BUILT_PRODUCTS_DIR; };
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AF2D867B2DB1FCC900B4F248 /* Frameworks */ = {
+                       isa = PBXFrameworksBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AF2D867C2DB1FCC900B4F248 /* libjwxyz.a in Frameworks */,
+                               AF2D867D2DB1FCC900B4F248 /* ScreenSaver.framework in Frameworks */,
+                               AF2D867E2DB1FCC900B4F248 /* QuartzCore.framework in Frameworks */,
+                               AF2D867F2DB1FCC900B4F248 /* Cocoa.framework in Frameworks */,
+                               AF2D86802DB1FCC900B4F248 /* Carbon.framework in Frameworks */,
+                               AF2D86812DB1FCC900B4F248 /* OpenGL.framework in Frameworks */,
+                               AF2D86822DB1FCC900B4F248 /* libz.dylib in Frameworks */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
+               AF2D869D2DB20F2100B4F248 /* Frameworks */ = {
+                       isa = PBXFrameworksBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AF2D869E2DB20F2100B4F248 /* libjwxyz.a in Frameworks */,
+                               AF2D869F2DB20F2100B4F248 /* ScreenSaver.framework in Frameworks */,
+                               AF2D86A02DB20F2100B4F248 /* QuartzCore.framework in Frameworks */,
+                               AF2D86A12DB20F2100B4F248 /* Cocoa.framework in Frameworks */,
+                               AF2D86A22DB20F2100B4F248 /* Carbon.framework in Frameworks */,
+                               AF2D86A32DB20F2100B4F248 /* OpenGL.framework in Frameworks */,
+                               AF2D86A42DB20F2100B4F248 /* libz.dylib in Frameworks */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AF32D9E80F3AD0B40080F535 /* Frameworks */ = {
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AFA82E252DB1EB4B00BEEE3E /* Frameworks */ = {
+                       isa = PBXFrameworksBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AFA82E262DB1EB4B00BEEE3E /* libjwxyz.a in Frameworks */,
+                               AFA82E272DB1EB4B00BEEE3E /* ScreenSaver.framework in Frameworks */,
+                               AFA82E282DB1EB4B00BEEE3E /* QuartzCore.framework in Frameworks */,
+                               AFA82E292DB1EB4B00BEEE3E /* Cocoa.framework in Frameworks */,
+                               AFA82E2A2DB1EB4B00BEEE3E /* Carbon.framework in Frameworks */,
+                               AFA82E2B2DB1EB4B00BEEE3E /* OpenGL.framework in Frameworks */,
+                               AFA82E2C2DB1EB4B00BEEE3E /* libz.dylib in Frameworks */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AFAAE38F207D6343007A515C /* Frameworks */ = {
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AFCB48A02DBC0958006296D3 /* Frameworks */ = {
+                       isa = PBXFrameworksBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AFCB48A12DBC0958006296D3 /* libjwxyz.a in Frameworks */,
+                               AFCB48A22DBC0958006296D3 /* ScreenSaver.framework in Frameworks */,
+                               AFCB48A32DBC0958006296D3 /* QuartzCore.framework in Frameworks */,
+                               AFCB48A42DBC0958006296D3 /* Cocoa.framework in Frameworks */,
+                               AFCB48A52DBC0958006296D3 /* Carbon.framework in Frameworks */,
+                               AFCB48A62DBC0958006296D3 /* OpenGL.framework in Frameworks */,
+                               AFCB48A72DBC0958006296D3 /* libz.dylib in Frameworks */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AFCF83431AF5B515008BB7E1 /* Frameworks */ = {
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                                AF70B7A32A8320A6007C1EB8 /* Skulloop.saver */,
                                AFC644652AE478FA00D589B9 /* Kallisti.saver */,
                                AF42CF622BE8A3B300675AC7 /* HighVoltage.saver */,
+                               AFA82E312DB1EB4B00BEEE3E /* HopfFibration.saver */,
+                               AF2D86872DB1FCC900B4F248 /* PlatonicFolding.saver */,
+                               AF2D86A92DB20F2100B4F248 /* Klondike.saver */,
+                               AFCB48AC2DBC0958006296D3 /* DumpsterFire.saver */,
                        );
                        name = Products;
                        path = ..;
                                AF0839AA09930C4900277BE9 /* dolphin.c */,
                                AF241F81107C38DF00046A84 /* dropshadow.c */,
                                AF241F82107C38DF00046A84 /* dropshadow.h */,
+                               AFCB48AF2DBC0BDB006296D3 /* dumpsterfire.c */,
+                               AFCB48AE2DBC0BDB006296D3 /* dumpster_model.c */,
                                AFEC23E41CB6EBC400DE138F /* dymaxionmap.c */,
                                AF4C300D208569A900BE1DEF /* dymaxionmap-coords.c */,
                                AF7778C109B65C6A00EA3033 /* e_textures.h */,
                                AF42CF652BE8A50E00675AC7 /* highvoltage_model.c */,
                                AF42CF662BE8A50E00675AC7 /* highvoltage.c */,
                                AF78D18A142DD96E002AAF77 /* hilbert.c */,
+                               AFA82E332DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c */,
+                               AFA82E342DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c */,
                                AFC0E8C21CDC60A9008CAFAC /* hydrostat.c */,
                                AFA55F59099362DF00F3E977 /* hypertorus.c */,
                                AF3C715D0D624C600030CC0D /* hypnowheel.c */,
                                AFC644672AE479BB00D589B9 /* kallisti_model.c */,
                                AFC644692AE479BC00D589B9 /* kallisti.c */,
                                AFA55F3F0993626E00F3E977 /* klein.c */,
+                               AF2D86AB2DB2100A00B4F248 /* klondike.c */,
+                               AF2D86AD2DB2100A00B4F248 /* klondike-game.c */,
                                AFA55A8E0993369100F3E977 /* lament.c */,
                                AFF1BA0E19A96D8B0016A88D /* lament_model.c */,
                                AFA55DDD09935DB600F3E977 /* lavalite.c */,
                                AFA561B309937DCC00F3E977 /* polyhedra.c */,
                                AFA561B409937DCC00F3E977 /* polyhedra.h */,
                                AFA560C3099371D500F3E977 /* polytopes.c */,
+                               AF2D86892DB1FDB900B4F248 /* platonicfolding.c */,
                                AFFAB33119158EA80020F021 /* projectiveplane.c */,
                                AFA5621C099384F600F3E977 /* providence.c */,
                                AFA55B3F09933EC600F3E977 /* pulsar.c */,
                                AFC258970988A468000655EE /* discrete.xml */,
                                AFC258980988A468000655EE /* distort.xml */,
                                AF77787909B6545E00EA3033 /* dnalogo.xml */,
+                               AFCB48B02DBC0BDB006296D3 /* dumpsterfire.xml */,
                                AFC258990988A468000655EE /* drift.xml */,
                                AF7F06112A50C18C00E35B45 /* droste.xml */,
                                AFEC23E51CB6EBDA00DE138F /* dymaxionmap.xml */,
                                AF42CF642BE8A50E00675AC7 /* highvoltage.xml */,
                                AF78D18E142DD99A002AAF77 /* hilbert.xml */,
                                AFC258C50988A468000655EE /* hopalong.xml */,
+                               AFA82E352DB1EC3B00BEEE3E /* hopffibration.xml */,
                                AFC258C60988A468000655EE /* hyperball.xml */,
                                AFC0E8C31CDC60A9008CAFAC /* hydrostat.xml */,
                                AFC258C70988A468000655EE /* hypercube.xml */,
                                AFB8A69A1782BA34004EDB85 /* kaleidocycle.xml */,
                                AFC644682AE479BB00D589B9 /* kallisti.xml */,
                                AFC258D40988A468000655EE /* klein.xml */,
+                               AF2D86AC2DB2100A00B4F248 /* klondike.xml */,
                                AFC258D50988A468000655EE /* kumppa.xml */,
                                AFC258D60988A468000655EE /* lament.xml */,
                                AFC258D70988A468000655EE /* laser.xml */,
                                AFC258F40988A469000655EE /* piecewise.xml */,
                                AFC258F50988A469000655EE /* pinion.xml */,
                                AFC258F60988A469000655EE /* pipes.xml */,
+                               AF2D868A2DB1FDB900B4F248 /* platonicfolding.xml */,
                                AFC258F70988A469000655EE /* polyhedra.xml */,
                                AFC258F80988A469000655EE /* polyominoes.xml */,
                                AFC258F90988A469000655EE /* polytopes.xml */,
                        children = (
                                AFDA11211934424D003D397F /* aligned_malloc.c */,
                                AFDA11221934424D003D397F /* aligned_malloc.h */,
+                               AF5D4E3B2D6409AE009BE57E /* ansi-tty.c */,
                                CE9289D119BD00E200961F22 /* async_netdb.c */,
                                CE9289D219BD00E300961F22 /* async_netdb.h */,
                                AF9D473609B52EE0006E59CF /* colorbars.c */,
                                AFA33BD00B0587EE002B0E7D /* webcollage-helper-cocoa.m */,
                                AFE943AF19DD54C1000A5E6D /* xft.c */,
                                AFE943B019DD54C1000A5E6D /* xft.h */,
+                               AFBD953C2C504ADC000DA52A /* xftwrap.c */,
+                               AFBD953B2C504ADB000DA52A /* xftwrap.h */,
                                AF480CBB098E37D600FB32B8 /* xlockmore.c */,
                                AF480C89098E346700FB32B8 /* xlockmore.h */,
                                AF480C8A098E34AB00FB32B8 /* xlockmoreI.h */,
                        productReference = AF2D0D3A241D7C870001D8B8 /* EtruscanVenus.saver */;
                        productType = "com.apple.product-type.bundle";
                };
+               AF2D86722DB1FCC900B4F248 /* PlatonicFolding */ = {
+                       isa = PBXNativeTarget;
+                       buildConfigurationList = AF2D86842DB1FCC900B4F248 /* Build configuration list for PBXNativeTarget "PlatonicFolding" */;
+                       buildPhases = (
+                               AF2D86752DB1FCC900B4F248 /* Resources */,
+                               AF2D86772DB1FCC900B4F248 /* Sources */,
+                               AF2D867B2DB1FCC900B4F248 /* Frameworks */,
+                               AF2D86832DB1FCC900B4F248 /* Run Update Info Plist */,
+                       );
+                       buildRules = (
+                       );
+                       dependencies = (
+                               AF2D86732DB1FCC900B4F248 /* PBXTargetDependency */,
+                       );
+                       name = PlatonicFolding;
+                       productName = DangerBall;
+                       productReference = AF2D86872DB1FCC900B4F248 /* PlatonicFolding.saver */;
+                       productType = "com.apple.product-type.bundle";
+               };
+               AF2D86952DB20F2100B4F248 /* Klondike */ = {
+                       isa = PBXNativeTarget;
+                       buildConfigurationList = AF2D86A62DB20F2100B4F248 /* Build configuration list for PBXNativeTarget "Klondike" */;
+                       buildPhases = (
+                               AF2D86982DB20F2100B4F248 /* Resources */,
+                               AF2D869A2DB20F2100B4F248 /* Sources */,
+                               AF2D869D2DB20F2100B4F248 /* Frameworks */,
+                               AF2D86A52DB20F2100B4F248 /* Run Update Info Plist */,
+                       );
+                       buildRules = (
+                       );
+                       dependencies = (
+                               AF2D86962DB20F2100B4F248 /* PBXTargetDependency */,
+                       );
+                       name = Klondike;
+                       productName = DangerBall;
+                       productReference = AF2D86A92DB20F2100B4F248 /* Klondike.saver */;
+                       productType = "com.apple.product-type.bundle";
+               };
                AF32D9E00F3AD0B40080F535 /* RubikBlocks */ = {
                        isa = PBXNativeTarget;
                        buildConfigurationList = AF32D9F10F3AD0B40080F535 /* Build configuration list for PBXNativeTarget "RubikBlocks" */;
                        productReference = AFA6AB0520999950006D2685 /* GlitchPEG.saver */;
                        productType = "com.apple.product-type.bundle";
                };
+               AFA82E1D2DB1EB4B00BEEE3E /* HopfFibration */ = {
+                       isa = PBXNativeTarget;
+                       buildConfigurationList = AFA82E2E2DB1EB4B00BEEE3E /* Build configuration list for PBXNativeTarget "HopfFibration" */;
+                       buildPhases = (
+                               AFA82E202DB1EB4B00BEEE3E /* Resources */,
+                               AFA82E222DB1EB4B00BEEE3E /* Sources */,
+                               AFA82E252DB1EB4B00BEEE3E /* Frameworks */,
+                               AFA82E2D2DB1EB4B00BEEE3E /* Run Update Info Plist */,
+                       );
+                       buildRules = (
+                       );
+                       dependencies = (
+                               AFA82E1E2DB1EB4B00BEEE3E /* PBXTargetDependency */,
+                       );
+                       name = HopfFibration;
+                       productName = DangerBall;
+                       productReference = AFA82E312DB1EB4B00BEEE3E /* HopfFibration.saver */;
+                       productType = "com.apple.product-type.bundle";
+               };
                AFAAE387207D6343007A515C /* Maze3D */ = {
                        isa = PBXNativeTarget;
                        buildConfigurationList = AFAAE399207D6343007A515C /* Build configuration list for PBXNativeTarget "Maze3D" */;
                        productReference = AFC644652AE478FA00D589B9 /* Kallisti.saver */;
                        productType = "com.apple.product-type.bundle";
                };
+               AFCB48982DBC0958006296D3 /* DumpsterFire */ = {
+                       isa = PBXNativeTarget;
+                       buildConfigurationList = AFCB48A92DBC0958006296D3 /* Build configuration list for PBXNativeTarget "DumpsterFire" */;
+                       buildPhases = (
+                               AFCB489B2DBC0958006296D3 /* Resources */,
+                               AFCB489D2DBC0958006296D3 /* Sources */,
+                               AFCB48A02DBC0958006296D3 /* Frameworks */,
+                               AFCB48A82DBC0958006296D3 /* Run Update Info Plist */,
+                       );
+                       buildRules = (
+                       );
+                       dependencies = (
+                               AFCB48992DBC0958006296D3 /* PBXTargetDependency */,
+                       );
+                       name = DumpsterFire;
+                       productName = DangerBall;
+                       productReference = AFCB48AC2DBC0958006296D3 /* DumpsterFire.saver */;
+                       productType = "com.apple.product-type.bundle";
+               };
                AFCF833B1AF5B515008BB7E1 /* SplitFlap */ = {
                        isa = PBXNativeTarget;
                        buildConfigurationList = AFCF834D1AF5B515008BB7E1 /* Build configuration list for PBXNativeTarget "SplitFlap" */;
                        isa = PBXProject;
                        attributes = {
                                BuildIndependentTargetsInParallel = YES;
-                               LastUpgradeCheck = 1500;
+                               LastUpgradeCheck = 1620;
                                TargetAttributes = {
                                        AF08398F09930B6B00277BE9 = {
                                                DevelopmentTeam = 4627ATJELP;
                                AFF449E02275494400DB8EDB /* DeepStars */,
                                AF39381A1D0FBD6A00205406 /* Discoball */,
                                AF77786109B6536000EA3033 /* DNAlogo */,
+                               AFCB48982DBC0958006296D3 /* DumpsterFire */,
                                AFEC23CD1CB6EAE100DE138F /* DymaxionMap */,
                                AFACE8731CC83458008B24CD /* EnergyStream */,
                                AFA55E0D09935EDC00F3E977 /* Endgame */,
                                AF0BF6CD29456B2E000D9473 /* HexTrail */,
                                AF42CF4E2BE8A3B300675AC7 /* HighVoltage */,
                                AF78D175142DD8F3002AAF77 /* Hilbert */,
+                               AFA82E1D2DB1EB4B00BEEE3E /* HopfFibration */,
                                AFC0E8AB1CDC601A008CAFAC /* Hydrostat */,
                                AFA55F420993629000F3E977 /* Hypertorus */,
                                AF3C71450D624BF50030CC0D /* Hypnowheel */,
                                AF7510FF1782B5B900380EA1 /* Kaleidocycle */,
                                AFC644512AE478FA00D589B9 /* Kallisti */,
                                AFA55F2A0993622F00F3E977 /* Klein */,
+                               AF2D86952DB20F2100B4F248 /* Klondike */,
                                AFA55A790993364300F3E977 /* Lament */,
                                AFA55DC809935D7000F3E977 /* Lavalite */,
                                AF4FD6E60CE7A486005EE58E /* Lockward */,
                                AFD51B1B0F063B4A00471C02 /* Photopile */,
                                AFA5621F0993852500F3E977 /* Pinion */,
                                AF4812B30990D3D900FB32B8 /* Pipes */,
+                               AF2D86722DB1FCC900B4F248 /* PlatonicFolding */,
                                AFA5619D09937D7E00F3E977 /* Polyhedra */,
                                AFA560AE0993718D00F3E977 /* Polytopes */,
                                AFFAB31519158CE40020F021 /* ProjectivePlane */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AF2D86752DB1FCC900B4F248 /* Resources */ = {
+                       isa = PBXResourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AF2D868C2DB1FDB900B4F248 /* platonicfolding.xml in Resources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
+               AF2D86982DB20F2100B4F248 /* Resources */ = {
+                       isa = PBXResourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AF2D86B02DB2100A00B4F248 /* klondike.xml in Resources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AF32D9E30F3AD0B40080F535 /* Resources */ = {
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                                AF56892A26E7060500CCBA38 /* distort.xml in Resources */,
                                AF56892B26E7060500CCBA38 /* dnalogo.xml in Resources */,
                                AF7F06172A50C18C00E35B45 /* droste.xml in Resources */,
+                               AFCB48B92DBC0BDB006296D3 /* dumpsterfire.xml in Resources */,
                                AF56892C26E7060500CCBA38 /* dymaxionmap.xml in Resources */,
                                AF56892D26E7060500CCBA38 /* drift.xml in Resources */,
                                AF56892E26E7060500CCBA38 /* endgame.xml in Resources */,
                                AF42CF692BE8A50E00675AC7 /* highvoltage.xml in Resources */,
                                AF56895F26E7060500CCBA38 /* hilbert.xml in Resources */,
                                AF56896026E7060500CCBA38 /* hopalong.xml in Resources */,
+                               AFA82E362DB1EC3B00BEEE3E /* hopffibration.xml in Resources */,
                                AF56896126E7060500CCBA38 /* hydrostat.xml in Resources */,
                                AF56896226E7060500CCBA38 /* hypertorus.xml in Resources */,
                                AF56896326E7060500CCBA38 /* hypnowheel.xml in Resources */,
                                AF56896E26E7060500CCBA38 /* kaleidescope.xml in Resources */,
                                AFC6446F2AE479BC00D589B9 /* kallisti.xml in Resources */,
                                AF56896F26E7060500CCBA38 /* klein.xml in Resources */,
+                               AF2D86B12DB2100A00B4F248 /* klondike.xml in Resources */,
                                AF56897026E7060500CCBA38 /* kumppa.xml in Resources */,
                                AF56897126E7060500CCBA38 /* lament.xml in Resources */,
                                AF56897226E7060500CCBA38 /* lavalite.xml in Resources */,
                                AF56899026E7060500CCBA38 /* piecewise.xml in Resources */,
                                AF56899126E7060500CCBA38 /* pinion.xml in Resources */,
                                AF56899226E7060500CCBA38 /* pipes.xml in Resources */,
+                               AF2D868F2DB1FDB900B4F248 /* platonicfolding.xml in Resources */,
                                AF56899326E7060500CCBA38 /* polyhedra.xml in Resources */,
                                AF56899426E7060500CCBA38 /* polyominoes.xml in Resources */,
                                AF56899526E7060500CCBA38 /* polytopes.xml in Resources */,
                                AF918AE7158FC53D002B5D1E /* distort.xml in Resources */,
                                AFCF453815986A3000E6E8CC /* dnalogo.xml in Resources */,
                                AF7F06162A50C18C00E35B45 /* droste.xml in Resources */,
+                               AFCB48B32DBC0BDB006296D3 /* dumpsterfire.xml in Resources */,
                                AFEC23E81CB6EC6800DE138F /* dymaxionmap.xml in Resources */,
                                AF918AE9158FC53D002B5D1E /* drift.xml in Resources */,
                                AF918AEA158FC53D002B5D1E /* endgame.xml in Resources */,
                                AF42CF682BE8A50E00675AC7 /* highvoltage.xml in Resources */,
                                AF918B14158FC53D002B5D1E /* hilbert.xml in Resources */,
                                AF918B15158FC53D002B5D1E /* hopalong.xml in Resources */,
+                               AFA82E3C2DB1EC3B00BEEE3E /* hopffibration.xml in Resources */,
                                AFC0E8C71CDC60DE008CAFAC /* hydrostat.xml in Resources */,
                                AF918B18158FC53D002B5D1E /* hypertorus.xml in Resources */,
                                AF918B19158FC53D002B5D1E /* hypnowheel.xml in Resources */,
                                AF918B24158FC53D002B5D1E /* kaleidescope.xml in Resources */,
                                AFC6446E2AE479BC00D589B9 /* kallisti.xml in Resources */,
                                AF918B25158FC53D002B5D1E /* klein.xml in Resources */,
+                               AF2D86B42DB2100A00B4F248 /* klondike.xml in Resources */,
                                AF918B26158FC53D002B5D1E /* kumppa.xml in Resources */,
                                AF918B27158FC53D002B5D1E /* lament.xml in Resources */,
                                AF918B29158FC53D002B5D1E /* lavalite.xml in Resources */,
                                AF918B4A158FC53E002B5D1E /* piecewise.xml in Resources */,
                                AF918B4B158FC53E002B5D1E /* pinion.xml in Resources */,
                                AF918B4C158FC53E002B5D1E /* pipes.xml in Resources */,
+                               AF2D868D2DB1FDB900B4F248 /* platonicfolding.xml in Resources */,
                                AFCF4547159878D500E6E8CC /* polyhedra.xml in Resources */,
                                AF918B4E158FC53E002B5D1E /* polyominoes.xml in Resources */,
                                AF918B4F158FC53E002B5D1E /* polytopes.xml in Resources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AFA82E202DB1EB4B00BEEE3E /* Resources */ = {
+                       isa = PBXResourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AFA82E392DB1EC3B00BEEE3E /* hopffibration.xml in Resources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AFAAE38A207D6343007A515C /* Resources */ = {
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AFCB489B2DBC0958006296D3 /* Resources */ = {
+                       isa = PBXResourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AFCB48B42DBC0BDB006296D3 /* dumpsterfire.xml in Resources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AFCF833E1AF5B515008BB7E1 /* Resources */ = {
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                        shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX";
                        showEnvVarsInLog = 0;
                };
+               AF2D86832DB1FCC900B4F248 /* Run Update Info Plist */ = {
+                       isa = PBXShellScriptBuildPhase;
+                       alwaysOutOfDate = 1;
+                       buildActionMask = 2147483647;
+                       files = (
+                       );
+                       inputPaths = (
+                               "${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}",
+                       );
+                       name = "Run Update Info Plist";
+                       outputPaths = (
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+                       shellPath = /bin/sh;
+                       shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX";
+                       showEnvVarsInLog = 0;
+               };
+               AF2D86A52DB20F2100B4F248 /* Run Update Info Plist */ = {
+                       isa = PBXShellScriptBuildPhase;
+                       alwaysOutOfDate = 1;
+                       buildActionMask = 2147483647;
+                       files = (
+                       );
+                       inputPaths = (
+                               "${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}",
+                       );
+                       name = "Run Update Info Plist";
+                       outputPaths = (
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+                       shellPath = /bin/sh;
+                       shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX\n";
+                       showEnvVarsInLog = 0;
+               };
                AF32D9F00F3AD0B40080F535 /* Run Update Info Plist */ = {
                        isa = PBXShellScriptBuildPhase;
                        alwaysOutOfDate = 1;
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                        shellPath = /bin/sh;
-                       shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX";
+                       shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX\n";
                        showEnvVarsInLog = 0;
                };
                AFA3D69409C03B6200E4CFCA /* Run Update Info Plist */ = {
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                        shellPath = /bin/sh;
-                       shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX";
+                       shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX\n";
                        showEnvVarsInLog = 0;
                };
                AFA3D97309C03DD300E4CFCA /* Run Update Info Plist */ = {
                        shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX";
                        showEnvVarsInLog = 0;
                };
+               AFA82E2D2DB1EB4B00BEEE3E /* Run Update Info Plist */ = {
+                       isa = PBXShellScriptBuildPhase;
+                       alwaysOutOfDate = 1;
+                       buildActionMask = 2147483647;
+                       files = (
+                       );
+                       inputPaths = (
+                               "${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}",
+                       );
+                       name = "Run Update Info Plist";
+                       outputPaths = (
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+                       shellPath = /bin/sh;
+                       shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX";
+                       showEnvVarsInLog = 0;
+               };
                AFAAE398207D6343007A515C /* Run Update Info Plist */ = {
                        isa = PBXShellScriptBuildPhase;
                        alwaysOutOfDate = 1;
                        shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX";
                        showEnvVarsInLog = 0;
                };
+               AFCB48A82DBC0958006296D3 /* Run Update Info Plist */ = {
+                       isa = PBXShellScriptBuildPhase;
+                       alwaysOutOfDate = 1;
+                       buildActionMask = 2147483647;
+                       files = (
+                       );
+                       inputPaths = (
+                               "${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}",
+                       );
+                       name = "Run Update Info Plist";
+                       outputPaths = (
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+                       shellPath = /bin/sh;
+                       shellScript = "$SOURCE_ROOT/update-info-plist.pl -q $BUILT_PRODUCTS_DIR/$PRODUCT_NAME$WRAPPER_SUFFIX\n";
+                       showEnvVarsInLog = 0;
+               };
                AFCCCBB509C033DF00353F4D /* Run Update Info Plist */ = {
                        isa = PBXShellScriptBuildPhase;
                        alwaysOutOfDate = 1;
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AF2D86772DB1FCC900B4F248 /* Sources */ = {
+                       isa = PBXSourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AF2D867A2DB1FCC900B4F248 /* XScreenSaverSubclass.m in Sources */,
+                               AF2D868B2DB1FDB900B4F248 /* platonicfolding.c in Sources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
+               AF2D869A2DB20F2100B4F248 /* Sources */ = {
+                       isa = PBXSourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AF2D86AE2DB2100A00B4F248 /* klondike.c in Sources */,
+                               AF2D86AF2DB2100A00B4F248 /* klondike-game.c in Sources */,
+                               AF2D869C2DB20F2100B4F248 /* XScreenSaverSubclass.m in Sources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AF32D9E50F3AD0B40080F535 /* Sources */ = {
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                                AF568A6026E7060500CCBA38 /* wormhole.c in Sources */,
                                AF568A6126E7060500CCBA38 /* xanalogtv.c in Sources */,
                                AF568A6226E7060500CCBA38 /* xflame.c in Sources */,
+                               AF2D86912DB2013200B4F248 /* xftwrap.c in Sources */,
                                AF568A6326E7060500CCBA38 /* xjack.c in Sources */,
                                AF568A6426E7060500CCBA38 /* xlyap.c in Sources */,
                                AF568A6526E7060500CCBA38 /* xmatrix.c in Sources */,
                                AF568A9326E7060500CCBA38 /* deepstars.c in Sources */,
                                AF568A9426E7060500CCBA38 /* dnalogo.c in Sources */,
                                AF568A9526E7060500CCBA38 /* dolphin.c in Sources */,
+                               AFCB48B82DBC0BDB006296D3 /* dumpsterfire.c in Sources */,
+                               AFCB48B72DBC0BDB006296D3 /* dumpster_model.c in Sources */,
                                AF568A9626E7060500CCBA38 /* dymaxionmap.c in Sources */,
                                AF568A0C26E7060500CCBA38 /* dymaxionmap-coords.c in Sources */,
                                AF568A9726E7060500CCBA38 /* dropshadow.c in Sources */,
                                AF42CF6C2BE8A50E00675AC7 /* highvoltage_model.c in Sources */,
                                AF42CF6F2BE8A50E00675AC7 /* highvoltage.c in Sources */,
                                AF568AB926E7060500CCBA38 /* hilbert.c in Sources */,
+                               AFA82E372DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c in Sources */,
+                               AFA82E382DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c in Sources */,
                                AF568ABA26E7060500CCBA38 /* hydrostat.c in Sources */,
                                AF568ABB26E7060500CCBA38 /* hypertorus.c in Sources */,
                                AF568ABC26E7060500CCBA38 /* hypnowheel.c in Sources */,
                                AFC644722AE479BC00D589B9 /* kallisti.c in Sources */,
                                AFC6446C2AE479BC00D589B9 /* kallisti_model.c in Sources */,
                                AF568AC226E7060500CCBA38 /* klein.c in Sources */,
+                               AF2D86B22DB2100A00B4F248 /* klondike.c in Sources */,
+                               AF2D86B32DB2100A00B4F248 /* klondike-game.c in Sources */,
                                AF568AC326E7060500CCBA38 /* lament.c in Sources */,
                                AF568AC426E7060500CCBA38 /* lament_model.c in Sources */,
                                AF568AC526E7060500CCBA38 /* lavalite.c in Sources */,
                                AF568AD426E7060500CCBA38 /* pinion.c in Sources */,
                                AF568AD526E7060500CCBA38 /* pipeobjs.c in Sources */,
                                AF568AD626E7060500CCBA38 /* pipes.c in Sources */,
+                               AF2D86902DB1FDB900B4F248 /* platonicfolding.c in Sources */,
                                AF568AD726E7060500CCBA38 /* polyhedra-gl.c in Sources */,
                                AF568AD826E7060500CCBA38 /* polyhedra.c in Sources */,
                                AF568AD926E7060500CCBA38 /* polytopes.c in Sources */,
                                AF568B0A26E7060500CCBA38 /* toast2.c in Sources */,
                                AF568B0B26E7060500CCBA38 /* toaster.c in Sources */,
                                AF568B0C26E7060500CCBA38 /* toaster_base.c in Sources */,
+                               AF5D4E3E2D6409AE009BE57E /* ansi-tty.c in Sources */,
                                AF568B0D26E7060500CCBA38 /* toaster_handle.c in Sources */,
                                AF568B0E26E7060500CCBA38 /* toaster_handle2.c in Sources */,
                                AF568B0F26E7060500CCBA38 /* toaster_jet.c in Sources */,
                        files = (
                                AF7776EA09B63ABF00EA3033 /* XScreenSaverSubclass.m in Sources */,
                                AF77770409B63B5F00EA3033 /* phosphor.c in Sources */,
+                               AF5D4E3C2D6409AE009BE57E /* ansi-tty.c in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                                AF9189F5158FC35E002B5D1E /* wormhole.c in Sources */,
                                AF9189F6158FC35E002B5D1E /* xanalogtv.c in Sources */,
                                AF9189F7158FC35E002B5D1E /* xflame.c in Sources */,
+                               AF2D86922DB2014100B4F248 /* xftwrap.c in Sources */,
                                AF9189F8158FC35E002B5D1E /* xjack.c in Sources */,
                                AF9189F9158FC35E002B5D1E /* xlyap.c in Sources */,
                                AF9189FA158FC35E002B5D1E /* xmatrix.c in Sources */,
                                AF2C2A8A22754C31002112B9 /* deepstars.c in Sources */,
                                AFCF453715986A2100E6E8CC /* dnalogo.c in Sources */,
                                AF918A48158FC3BB002B5D1E /* dolphin.c in Sources */,
+                               AFCB48B22DBC0BDB006296D3 /* dumpsterfire.c in Sources */,
+                               AFCB48B12DBC0BDB006296D3 /* dumpster_model.c in Sources */,
                                AFEC23E91CB6EC7F00DE138F /* dymaxionmap.c in Sources */,
                                AF4C300F208569AA00BE1DEF /* dymaxionmap-coords.c in Sources */,
                                AF918A49158FC3BB002B5D1E /* dropshadow.c in Sources */,
                                AF62D6342180082100C57C42 /* handsy.c in Sources */,
                                AF96015D25759124007FA31B /* headroom.c in Sources */,
                                AF96015B25759124007FA31B /* headroom_model.c in Sources */,
-                               AF42CF6E2BE8A50E00675AC7 /* highvoltage.c in Sources */,
                                AF1B0FC31D7AB5500011DBE4 /* hexstrut.c in Sources */,
                                AF0BF6E829456CCC000D9473 /* hextrail.c in Sources */,
+                               AF42CF6E2BE8A50E00675AC7 /* highvoltage.c in Sources */,
                                AF42CF6B2BE8A50E00675AC7 /* highvoltage_model.c in Sources */,
                                AF918A6A158FC3E5002B5D1E /* hilbert.c in Sources */,
+                               AFA82E3D2DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c in Sources */,
+                               AFA82E3E2DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c in Sources */,
                                AFC0E8C41CDC60B0008CAFAC /* hydrostat.c in Sources */,
                                AF918A6B158FC3E5002B5D1E /* hypertorus.c in Sources */,
                                AF918A6C158FC3E5002B5D1E /* hypnowheel.c in Sources */,
                                AFC644712AE479BC00D589B9 /* kallisti.c in Sources */,
                                AFC6446B2AE479BC00D589B9 /* kallisti_model.c in Sources */,
                                AF918A70158FC417002B5D1E /* klein.c in Sources */,
+                               AF2D86B52DB2100A00B4F248 /* klondike.c in Sources */,
+                               AF2D86B62DB2100A00B4F248 /* klondike-game.c in Sources */,
                                AF918A71158FC417002B5D1E /* lament.c in Sources */,
                                AFF1BA1019A96D8B0016A88D /* lament_model.c in Sources */,
                                AF918A72158FC417002B5D1E /* lavalite.c in Sources */,
                                AF918A7D158FC417002B5D1E /* pinion.c in Sources */,
                                AF918A7E158FC417002B5D1E /* pipeobjs.c in Sources */,
                                AF918A7F158FC417002B5D1E /* pipes.c in Sources */,
+                               AF2D868E2DB1FDB900B4F248 /* platonicfolding.c in Sources */,
                                AFCF4545159878C300E6E8CC /* polyhedra-gl.c in Sources */,
                                AFCF4546159878C300E6E8CC /* polyhedra.c in Sources */,
                                AF918A82158FC417002B5D1E /* polytopes.c in Sources */,
                                AF918AA7158FC473002B5D1E /* toast2.c in Sources */,
                                AF918AA8158FC473002B5D1E /* toaster.c in Sources */,
                                AF918AA9158FC473002B5D1E /* toaster_base.c in Sources */,
+                               AF5D4E3D2D6409AE009BE57E /* ansi-tty.c in Sources */,
                                AF918AAA158FC473002B5D1E /* toaster_handle.c in Sources */,
                                AF918AAB158FC473002B5D1E /* toaster_handle2.c in Sources */,
                                AF918AAC158FC473002B5D1E /* toaster_jet.c in Sources */,
                                AF9D4DB609B5B71E006E59CF /* analogtv.c in Sources */,
                                AF9D4DC409B5B87D006E59CF /* bsod.c in Sources */,
                                AF9D4DD409B5B990006E59CF /* apple2.c in Sources */,
+                               AFBD953D2C504AFC000DA52A /* xftwrap.c in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                        buildActionMask = 2147483647;
                        files = (
                                AF9D4DF209B5BB19006E59CF /* XScreenSaverSubclass.m in Sources */,
+                               AF5D4E3F2D6409AE009BE57E /* ansi-tty.c in Sources */,
                                AF9D4DF309B5BB19006E59CF /* analogtv.c in Sources */,
                                AF9D4DF509B5BB19006E59CF /* apple2.c in Sources */,
                                AF9D4E0609B5BC9D006E59CF /* apple2-main.c in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AFA82E222DB1EB4B00BEEE3E /* Sources */ = {
+                       isa = PBXSourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AFA82E3A2DB1EC3B00BEEE3E /* hacks/glx/hopffibration.c in Sources */,
+                               AFA82E3B2DB1EC3B00BEEE3E /* hacks/glx/hopfanimations.c in Sources */,
+                               AFA82E242DB1EB4B00BEEE3E /* XScreenSaverSubclass.m in Sources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AFAAE38C207D6343007A515C /* Sources */ = {
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
+               AFCB489D2DBC0958006296D3 /* Sources */ = {
+                       isa = PBXSourcesBuildPhase;
+                       buildActionMask = 2147483647;
+                       files = (
+                               AFCB48B52DBC0BDB006296D3 /* dumpster_model.c in Sources */,
+                               AFCB48B62DBC0BDB006296D3 /* dumpsterfire.c in Sources */,
+                               AFCB489F2DBC0958006296D3 /* XScreenSaverSubclass.m in Sources */,
+                       );
+                       runOnlyForDeploymentPostprocessing = 0;
+               };
                AFCF83401AF5B515008BB7E1 /* Sources */ = {
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        target = AF2D0D25241D7C870001D8B8 /* EtruscanVenus */;
                        targetProxy = AF2D0D42241D7D9F0001D8B8 /* PBXContainerItemProxy */;
                };
+               AF2D86732DB1FCC900B4F248 /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = AF4808C0098C3B6C00FB32B8 /* jwxyz */;
+                       targetProxy = AF2D86742DB1FCC900B4F248 /* PBXContainerItemProxy */;
+               };
+               AF2D86942DB2032D00B4F248 /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = AF2D86722DB1FCC900B4F248 /* PlatonicFolding */;
+                       targetProxy = AF2D86932DB2032D00B4F248 /* PBXContainerItemProxy */;
+               };
+               AF2D86962DB20F2100B4F248 /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = AF4808C0098C3B6C00FB32B8 /* jwxyz */;
+                       targetProxy = AF2D86972DB20F2100B4F248 /* PBXContainerItemProxy */;
+               };
+               AF2D86B82DB2105300B4F248 /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = AF2D86952DB20F2100B4F248 /* Klondike */;
+                       targetProxy = AF2D86B72DB2105300B4F248 /* PBXContainerItemProxy */;
+               };
                AF32D9E10F3AD0B40080F535 /* PBXTargetDependency */ = {
                        isa = PBXTargetDependency;
                        target = AF4808C0098C3B6C00FB32B8 /* jwxyz */;
                        target = AFA6AAF020999950006D2685 /* GlitchPEG */;
                        targetProxy = AFA6AB1020999A9A006D2685 /* PBXContainerItemProxy */;
                };
+               AFA82E1E2DB1EB4B00BEEE3E /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = AF4808C0098C3B6C00FB32B8 /* jwxyz */;
+                       targetProxy = AFA82E1F2DB1EB4B00BEEE3E /* PBXContainerItemProxy */;
+               };
+               AFA82E402DB1ECC600BEEE3E /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = AFA82E1D2DB1EB4B00BEEE3E /* HopfFibration */;
+                       targetProxy = AFA82E3F2DB1ECC600BEEE3E /* PBXContainerItemProxy */;
+               };
                AFAAE388207D6343007A515C /* PBXTargetDependency */ = {
                        isa = PBXTargetDependency;
                        target = AF4808C0098C3B6C00FB32B8 /* jwxyz */;
                        target = AF9771D60989DC4A001F8B92 /* SaverTester */;
                        targetProxy = AFCAD5F80992DFE00009617A /* PBXContainerItemProxy */;
                };
+               AFCB48992DBC0958006296D3 /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = AF4808C0098C3B6C00FB32B8 /* jwxyz */;
+                       targetProxy = AFCB489A2DBC0958006296D3 /* PBXContainerItemProxy */;
+               };
+               AFCB48BB2DBC0C3E006296D3 /* PBXTargetDependency */ = {
+                       isa = PBXTargetDependency;
+                       target = AFCB48982DBC0958006296D3 /* DumpsterFire */;
+                       targetProxy = AFCB48BA2DBC0C3E006296D3 /* PBXContainerItemProxy */;
+               };
                AFCF833C1AF5B515008BB7E1 /* PBXTargetDependency */ = {
                        isa = PBXTargetDependency;
                        target = AF4808C0098C3B6C00FB32B8 /* jwxyz */;
                        };
                        name = Release;
                };
+               AF2D86852DB1FCC900B4F248 /* Debug */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = (
+                                       "USE_GL=1",
+                                       "$(GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS)",
+                               );
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                       };
+                       name = Debug;
+               };
+               AF2D86862DB1FCC900B4F248 /* Release */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = (
+                                       "USE_GL=1",
+                                       "$(GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS)",
+                               );
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                       };
+                       name = Release;
+               };
+               AF2D86A72DB20F2100B4F248 /* Debug */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = (
+                                       "USE_GL=1",
+                                       "$(GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS)",
+                               );
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                       };
+                       name = Debug;
+               };
+               AF2D86A82DB20F2100B4F248 /* Release */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = (
+                                       "USE_GL=1",
+                                       "$(GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS)",
+                               );
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                       };
+                       name = Release;
+               };
                AF32D9F20F3AD0B40080F535 /* Debug */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                        };
                        name = Release;
                };
+               AFA82E2F2DB1EB4B00BEEE3E /* Debug */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = (
+                                       "USE_GL=1",
+                                       "$(GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS)",
+                               );
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                       };
+                       name = Debug;
+               };
+               AFA82E302DB1EB4B00BEEE3E /* Release */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = (
+                                       "USE_GL=1",
+                                       "$(GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS)",
+                               );
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                       };
+                       name = Release;
+               };
                AFAAE39A207D6343007A515C /* Debug */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                        };
                        name = Release;
                };
+               AFCB48AA2DBC0958006296D3 /* Debug */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = (
+                                       "USE_GL=1",
+                                       "$(GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS)",
+                               );
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                       };
+                       name = Debug;
+               };
+               AFCB48AB2DBC0958006296D3 /* Release */ = {
+                       isa = XCBuildConfiguration;
+                       buildSettings = {
+                               GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = (
+                                       "USE_GL=1",
+                                       "$(GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS)",
+                               );
+                               PRODUCT_NAME = "$(TARGET_NAME)";
+                       };
+                       name = Release;
+               };
                AFCF834E1AF5B515008BB7E1 /* Debug */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
+               AF2D86842DB1FCC900B4F248 /* Build configuration list for PBXNativeTarget "PlatonicFolding" */ = {
+                       isa = XCConfigurationList;
+                       buildConfigurations = (
+                               AF2D86852DB1FCC900B4F248 /* Debug */,
+                               AF2D86862DB1FCC900B4F248 /* Release */,
+                       );
+                       defaultConfigurationIsVisible = 0;
+                       defaultConfigurationName = Release;
+               };
+               AF2D86A62DB20F2100B4F248 /* Build configuration list for PBXNativeTarget "Klondike" */ = {
+                       isa = XCConfigurationList;
+                       buildConfigurations = (
+                               AF2D86A72DB20F2100B4F248 /* Debug */,
+                               AF2D86A82DB20F2100B4F248 /* Release */,
+                       );
+                       defaultConfigurationIsVisible = 0;
+                       defaultConfigurationName = Release;
+               };
                AF32D9F10F3AD0B40080F535 /* Build configuration list for PBXNativeTarget "RubikBlocks" */ = {
                        isa = XCConfigurationList;
                        buildConfigurations = (
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
+               AFA82E2E2DB1EB4B00BEEE3E /* Build configuration list for PBXNativeTarget "HopfFibration" */ = {
+                       isa = XCConfigurationList;
+                       buildConfigurations = (
+                               AFA82E2F2DB1EB4B00BEEE3E /* Debug */,
+                               AFA82E302DB1EB4B00BEEE3E /* Release */,
+                       );
+                       defaultConfigurationIsVisible = 0;
+                       defaultConfigurationName = Release;
+               };
                AFAAE399207D6343007A515C /* Build configuration list for PBXNativeTarget "Maze3D" */ = {
                        isa = XCConfigurationList;
                        buildConfigurations = (
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
+               AFCB48A92DBC0958006296D3 /* Build configuration list for PBXNativeTarget "DumpsterFire" */ = {
+                       isa = XCConfigurationList;
+                       buildConfigurations = (
+                               AFCB48AA2DBC0958006296D3 /* Debug */,
+                               AFCB48AB2DBC0958006296D3 /* Release */,
+                       );
+                       defaultConfigurationIsVisible = 0;
+                       defaultConfigurationName = Release;
+               };
                AFCF834D1AF5B515008BB7E1 /* Build configuration list for PBXNativeTarget "SplitFlap" */ = {
                        isa = XCConfigurationList;
                        buildConfigurations = (
index 21949a56410c7e57b1a530dcb20d2ab5425496b2..e3c0158e5b557935509e1de40fb4a4b39b6be0b2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e77f00d4fac25016797db81bddbfcf695e8e7ded..4bf1a0d487ab6903db51c6bc990241d2538a44c8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index aa3aa8e5d29dd73dbdef598b0e8ff1ca93e49626..0c1519f40256b67335872d14dd782690bed52bc8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 9ab4b300799646a3773fcbfcd0ff9455d55277af..6c31ef752aeac9a217c29692dce77f53ac84db46 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c3c4f6a099127a33d3f8ac03aa9a59309ac90d8b..50b679abc0a2f87e2e35d91bb3a87ad3e1514679 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4a41e31a45075ff6c11e16b3c28ece3b8339df55..63fc7f85107ef304b4dce522ccbd1968c3ce3bd9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6f0277384bbe6dadfd06f7a3d6a0f953d7f5c07e..eea3bb497ca14282b7b3a1e023829daa00d96075 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 5f14be9ee6a4ae9ed18b40afacb4c4c3cdbf19dd..696033b18db36fd842775bb1cfd32b664188f910 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 25d3fa2eab98e1c12ca979f82a6b371e6a5cac6e..6d3700fd515744eda84758ccb517fa246fe2cba2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index a9d6b25ed75929b79db2b8bb9ab9d5b7d37ca205..e0073f48d852a6ceb720ab8d18e5de3c75d4a3f4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index fdd9da9dec38572c5b0a918b58d53a378bb75467..ce57e4fd7717c626891874ef445d643cff0ccd71 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 1fa61e6027bc5b3fd847d70298add489c0853b97..d537574391dad3aac42fbafd8a43ee8767d56d21 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 69de81cf96102ce52803fdad3a86e94743950d5e..ca2f589ba08e68581cc0bb0c1ba7be3d68e9a8b8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index cb88049e0b4e903a213a4b68a952a2579395a252..19f1d0a0a6bc4c2ac6820360fde7a26a2e589caf 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 4cf80208c6f115b29564210694fd43bbea117b44..bf9fdc13d2e2c56ff9450adcd1122df21509740e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ffc9723c1beb648844d93958df6f6fc7ed5ff1b3..eb4879f6e7028edb7e3499a98cc298f2ef9f498b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4cdf432d7dd0447afc77002d62d1921e801a2020..e658a38d94fd42156d8f3f26e840215144ad22da 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index fcee992dcc81538884045f5be4d2fbd52d196b64..ab56992988ef6022babc527f601c65fa29ec7c3b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index dfca4bf71c46e76b110df78abcb5c9b8c5e53d61..fc600b4ffeed617ce44d8896f0c4c10748c1eac2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 83d8165c41ebc7f5d4215ba074f5d5f0f6378711..54937092fc7e050ec3b614c4e706dd072f71fcf4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index cda2201728f915ffd90869a52e8493c409952b00..4c0806ca23c9fd02a799c3a48835b96c8e91fb81 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index dc2c6f004a8c025a61303d2af2ad2ae96d55d2af..5ca8e1d21cc4d176584cebfa3b3e63861f1795d5 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 0eccdec851e78a1493b9717cdea5e37cb39878e1..242b2927d00c23a9bae0204b72ace17c5d0e84aa 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 53839015db38e0984ae4eb4d935e619298447b98..ad48aa8753977a821651c729961826523baba285 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 9c0759a9db200cff619cc3f66efd2537bd7d2fdd..01ea21be7968b8153e6e60d21e9493059a8b5594 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4a67b3641e13409577ecc0a97acfe24c2fc33071..cda1bcc1ba4ed254686a29a159705663eef5e840 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 8a69c626de802ad6bee12eb32f03eaae80cdc042..69686ccecfc85b973df1c1fa5d7f2c5755eae7f2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 7389ca8ce5ba9b30d74e2504bfac4f846b0faa5a..5da6465b79087de4c506231e34ff023607572a2e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c1109de0d70abd8eb3636d36dedffdde8048843e..8e87199d2e9f2c72b73349df584898a8d70eeb92 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ecfe35ee43f1246583de1922cf6e2bb15238d314..8461ff82a9eb0a53ca6d9c2a662a86daf99dfa7d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e460653f1d7ccf09839fa0b077ff074b65b548e5..c40c72a728628d79daa73e4386494f274bba0e99 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 2acae77440cfa3c10d44ed0bc468c80e65132460..40ad543894760429d610b29cca315a4caa945d90 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 7e2b80b6a6b253530d4499c506c44bdf19209005..f6ca1a27854f2cdaf230d64fbf894d58f71ffd8f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index f3abafa34046013c77359b856c5a46c7e91a964d..0b4382ba4f41401d1dc4c895d91bde3d4f19099c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d1c4769b70b2f96c41ff79362c2c20c232c63437..f388f7db12ab8ddb5d29f04f640b9885f899a4d0 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e853227f67b1ceb9998da5dd16b1bfdf9b27e246..3d2e10ae9a949134478a4ac9f52ceffc67b1dc7e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c60f915310ded3e934f727e860e135290e8fb407..a58592073dea47f80997712b6b936f9791bf1b05 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index fa9356cb29d855de6eb358452bd9ad71bd0f3a77..0270e596bc2684c89cb2b577ddacb183c31429e5 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index fb666b1f10bf99bc8b26d274251bd3ba2dfec970..9571087f2f8807ec560ea53c151611c3087059a4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 236371dbdac851eb8498b28e8cc090b2c959236d..a30fd74abb50d8780339a3cbd5e6bb4fdfdd32ab 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 7338add85a0b4567c21fea7b6b3bec5a017ef57c..42b4e581d4ca36263bacb4ba552351b80b5f7915 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index b198bd5604df6e93754a5d6312e2ef401f2cf8eb..cf39cb3c8f7156da3c18ddf5773c6f4fb6ccfe63 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 99810affff4fa00c73cc590c0ccec6c8ae8314c9..f6334c467924f0fc27a7247c401b617d28811ad0 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 824a8f270cd88bb1d8a18e672b2540a5602a9d5f..c416da0752d8ec64aa56b66d0ad09558a73d5b96 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 225f091c0db5e58a39d36db720d07b82046edb29..a2128399beb83fba8ac23d6f803836d7fc2837d2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index ebf08e6d712ee1ed084d805f50572d642fb719f9..b322e302f68a0c171730419c899074dfe5621fcc 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 137470c8d7c53b6e489f7bee3b1befac452d8453..e4347747412cb563b2ee453ce06bd7c80346050d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 1fc01408f077a4133250c93f9ae9f73cf57237fe..c6b780171e532d983d4b7131cf931892b7d95d31 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 9e3a49d43dd89ada62a17c63ac8c4c32f5777aa6..7ceabdc982f18e340995a224124e0c75fe1b35b5 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 5561968d578f692a499161d0bd1054c0dbca9ece..3cc2dfd01e46b386513c96bb752c1a2778d89422 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 0b839bcdc50387b3839bfa651aeb2e9fe5c8d3e0..d418726e749f9231f72df80f734b20cd00ffe096 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 7397db62833830e93fe596fd8bbf3885603b3a8f..4d9e4db26db66d4e2beafda735a69676398f45d8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 176f43ac1fbd829ebd4ac081574d4f75601e3205..5511224855f1b4d49154f3a28ad3b42838742c79 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6188c4c8d4603fb1a6b4ab5080224387a13b1657..ca5f2266021eba0b1570f2386b6db70cee7c0c6b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 2eb5be61cf0513233ee02672ae2545c7471440d6..b8b4dcdb8c9704fb959a249708d37e87314f3dd3 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index bb6a7cddd732bec0d530be7dccdd48dc3d8143b8..d73ea5c01d7230e6d6a9cf0a4457aa0086420aba 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4ef62f5fba8032c9d3bfc8982c1ddf4f4add7d53..ac5e1cb91abc4e12da7efb5be0a561936ec2ba87 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 050cf77f5d869619c462f007ff544699b8e83c0f..8cf9feb6fa6f381a039604ea37de78843cf3aa36 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6670c3328a2760e649f66550e1419e5f2a5a28bb..0afb9dede1675b1922aaac7926eedd289103ab59 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4b133f553cd449839cfd6d30a8b96a0e31f79d97..3d6d4da1c8eb73b755bcbc30f3cc48d11ba9764a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.7">
    <BuildAction
       parallelizeBuildables = "YES"
index adbc160bba6f5458c4e0e2c166b55fc4cf13cb89..fafe256431c892d130f8f43230d52a54df5cf632 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c488cd3fce14120243a9a39594099b908edadea1..539a03a7b17a23a30c0db262916a69d25145ed4b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index f4f4a081824285987357dfc691ed13b9770dd892..19202b811bb7e0e3a4e7e743d1dc7e2de8ee3ef0 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 24bb9cf1f68537e2246515db3c058eb72dd709fd..eca398f6e858fe83ac635b2b89351a9816c5d317 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 15e2a05653d5ee2cd745759f702e3f00f5ae8ca3..4ed0516b74728fdf1d958c4e19daf773bf0cf18a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 1a91b10dc351ce1d224fa44b523bc03d5ee104ea..8dd1951ea6c708c70b3afe9b7d7b5126e2ee4ee8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 17e18be84c293afc2df6ee5ce2c64d59f1a251b9..4b48c3fa8ec00e4ee35ba2e226f94e0c7be3e64f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4627ce0c23a486fbace2c1447c58f5c26e634a76..ef5d0342db745257d52e66aa34031a004b05b531 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 85c2317cfcdfb668c9559c0ad570a9a92b6d2303..2ad267fd1764330284fffa7d772f0017e88b4bc2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index a847c8c5cdf7ffc4aeb0937b8d093c97d3ad5260..4e3204153a7d4d2451aa502c611363bf1245ad92 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ede0489aac7f702315f881cc9e8083819d1bad29..fc720c61556455007d6799aeeca0f11c0a8ab7ae 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 8a702a2f7cb0c24d528a6641d3eae4438965532f..68defefe1dd2fd2e8c9432eb57a33bbf4ccefbdb 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c321061503a08674e2dcfd91b10bd95eee4eb493..89348a1b87c3f4ca2599cc0c4c110924a4ddf02c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/DumpsterFire.xcscheme b/OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/DumpsterFire.xcscheme
new file mode 100644 (file)
index 0000000..668c39d
--- /dev/null
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1620"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "AFCB48982DBC0958006296D3"
+               BuildableName = "DumpsterFire.saver"
+               BlueprintName = "DumpsterFire"
+               ReferencedContainer = "container:xscreensaver.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AF9771D60989DC4A001F8B92"
+            BuildableName = "SaverTester.app"
+            BlueprintName = "SaverTester"
+            ReferencedContainer = "container:xscreensaver.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <EnvironmentVariables>
+         <EnvironmentVariable
+            key = "SELECTED_SAVER"
+            value = "DumpsterFire"
+            isEnabled = "YES">
+         </EnvironmentVariable>
+      </EnvironmentVariables>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AF9771D60989DC4A001F8B92"
+            BuildableName = "SaverTester.app"
+            BlueprintName = "SaverTester"
+            ReferencedContainer = "container:xscreensaver.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
index 0db594b303e13be19c4551dce819dc79fe4c78f6..c5a54d21cf683140706c5ebbd5d2d8de7a29ddef 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 29893985eb52ebd5f2a448b4fa91045ed0e3a667..0cdf1401d753e9fd8c1190727c0bf909084d5077 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 64ba0eec950c6b4613e44c47d87ca4f60854bb25..f396fb392a19a5a7f0401f9e1c46cce45e053723 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 111a1fa534071775c6b6669c7271f2238ce00079..77ceb11c0a2289d774a499af60add011c4a93b37 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 95cc763186d0d82ca753964c59f911ac9cfe5a05..09f3759dc48800240fd612f1d4e218470af72b10 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c840c5dd893e9a19583990f9c20d8e2bf2314347..2e44a9d380df41703a2527b52ab27c2fb66c9b02 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index feb47586b27475fc0bf68830b0d43ea72888f511..5eeec26072859280db225d02c621c948e56a3bcc 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 73169d4f66005796378ca0d1bc07ce65724f6cca..0978d5a8ea286b3a839dc32eb0a2e708bec68ce6 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index d34491215e86601a2447ddd17057c3229611b823..bab59a74aa009ee7d6bb8222e5b783ab3f41500a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index fb27800624537afbbe2e3da250812ebc6db47b33..0e740dc078ba68deba6aa77e5f1e61a1505e35bf 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ed250520cb404b262c2cd63d8d39d7fed48c9f15..d1fd1a9a0aad32886dd6bb05f019aa5bbd56a718 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 8ca1bd7a4ac65e0602ef61f782fa04c01052f0c8..77c7b38be3be8b10375521af7bb1f27289f85e2e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 78bc7f33b7b47cc5c647f061568cc88e7e70d4fc..4f10b437cdaf197574ceb3715d1c7b8b60bc97b9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index c267fb47363b62f0d57e7c16896077ceb414dc69..82d985c1888f2d63cfb10655dea62cdcac168b4b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d4e43ab98dcf9fcfe285c5fc505eab5eebba7f6a..102bfa306428c39304d5faaa1676072e352e6165 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 0e604d8f7d8ff76cb2b58c62a3056afdacaafd48..fbeb22e3c791f1657be7b04261440e3797999659 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 0e9ff6df2c4d08fdf663a7e856a3bfaad88f10bd..1ab02b37bf19398720a357362ee6760eb8df836a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 153090058d183f770e0a896cc896262e13f99ddd..e8c8818c8f838b8a209e4530afa1a6535a8af22d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d92307b74d4d29330cdeb357ba0f7eb5ab064a49..64c1b5b0ca2031c3f41558580eaa240f50651ca3 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 847c4a04fbb9559128bf562b7b192bc83f0603f3..0890de75de209576f00fbfb52256713dfef8cddc 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 87a625ff68609e55140924491a8d0e25c98bf9e5..220f8ecc797786455fe7eeca276fe54177279e35 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6870957661964b5e658211619304e9d8e8b5cf65..cb6a054bc465fc4e1bbd6f102e8e462bfa7cb7d5 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 651c35848e2d77988ef54d713955523c1f89d946..89f1b2625192a8aa8054689b1d3855b16a5cc9c4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index f005ecf94aaae2b75770281699496c45ced358b3..2acc74cab17d66d49bbdbc9f1e9257d2144e4cf5 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 73d1ac90a36343dc8279335745a0507b16a497ad..1d4d787b3f7ea7b3e53f142c4336c12490505492 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ee9d024ebc97a641c75716b0ffa469fd5d80bc62..ead2e90672b481ebd83156339e76205f21e7f346 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index dc366aac9e25843316763a2f53be961e2c8a3bcc..afc62afc02f22d83ad04588e1623d1a163037c0d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 8a0b0c0261304c3445eeb235dc2f5ca50d57e2b1..e2cab523843aa74edfd4c496eb570c0fd821e7b6 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index cc833ab02fff61558da9202345c66c942bfccfbf..83e68be1f37ed1d3b9518ce34f90a655bd23d07f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index b0f000876079df3eae1c4ca5f7df1ddac2ea7b4b..afb5c994e4a7bc3437c614c787c6179be23b700c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 165a893e29d5ca59b5ef7d37723f4cf1e21315ee..15e719a5a258687b0599610322ac6814f9bfadb9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 2282117e0febd6d00ba08cff2480e6a1b7752703..abdfcea5e84fd95c29631b3514f8b38bde033bb4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 58c6ba019fb1555ec2b058e2a75c62d6e4e1cdd8..044c46f904893e427350c5dae047a08c3eb2e20d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index a323b706718a3b7005c13e5cece2bef5c43868d6..516d0a42c0d3dea49cbf02e6599a384a3838a605 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 7972f38a3de35493a004833978e129f92d07a154..decb6d1c41ffbcff10c2a6ad8187b01c64494e19 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6278a54459bf8ef5fc5b5ea1c1de14c8cd596f24..07bff92659eb6e9ed2e7550c9cbdb63de58898a1 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 1dac26204a90c2f8543cc38e808e4182c8efd2a0..e943b4aab65541276c5200e353900dc3fee64b11 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 73e994870e9b65986598f65b9c834de751930510..63055eb8e92412d0d94c3d1235da8dcbb045820c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 8ff0314714c23e080ad725c76f72a98a96f08a82..ea4a60707cbb1946d4f5f69d07dfbbb861b360bb 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e59ca004310782ce3d0b103b278805158eab7bc5..a42d0b66629aa595c4a7359e98e1bb7779be2eda 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index ca0f293ae9ff9d8abe91f138439bef2ee787026e..d5c782632e3a85d839933717d3c86f3281eb0427 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index abab3cf44cbb63874cb76bbb7840230ae4e87d33..54e4fc7fa338ab7d8b4ca4e51c10ee79ef92009f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index d12228133328384619b2a2a0df11549bb5432350..c38e9d71ca60f756df7a802ffd539ef09755701e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c931e5e8d1a519ced84c047bc94816c827ce538b..3c7f6e4b10c16fb0e22836a8d2f4baf0a044724d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 3cb514f4ac581a1cdb6f0461e719ee147cfa2d32..d069232c414a5326414bbc2cbaac74175765f4e8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 13746e4c67515f4ce239dcb8e6b2951cd5fd4ee1..93f14d4a6dae04f6c4f86e258b10344bb143e7c3 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 8736dc6a99e87789d2faf10a8f89a738202d1553..5488c4a40b23ee3a16ec0964dd90120fdf50cc99 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 62c911d0d78bdd2291ee54924b6e1643928fefbc..53b5665958002b4d286dc7723d7ae780834f0044 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 10509960da30a64fffd4bc38b66270bda5817d60..7d28c9612755febb69d7de76e273f3f9daed78e9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 739c37521cfe507ee3dcba7e3738d578efd64dbd..b779569e9f3ec2dda7474560af941df77e859ea0 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 09d1bf78abbb441f6fac46e199ca6734d3150a20..c2f4aee77ec399020ba894b369ac555b3aa88189 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index b54c755aac8d2419d1b4b8d2a8f0e612296f563f..d053c6baf98ea0271220ac39f0450235a371f800 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 681f64d79b66a3cdb74ccf7206fba544061a888f..4970af7b75ff75d78819e4cde08f5efa70f880ec 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d51af35f458d97eced1cf597299ece41c0509a4a..d45a6d4435c74f3c72090219a412a209d05a4cf9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index ac3643079cd8599c6111b1bc0acd19b762b208b3..f1b915cc696f4e1514aa31c182b2f24e2b6b80dc 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 558e68a4b5fbf0962406ac380cd89d8b2bb5707f..18adc85778957f51ff9824c09a0e79d1b7f9a50e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index f2f3b14390a8ac67b0d32a89d529eb44c6f7eebf..4fd6aca647c33ad1d548064d563fc6a85dea414f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1530"
+   LastUpgradeVersion = "1620"
    version = "1.7">
    <BuildAction
       parallelizeBuildables = "YES"
index a0e2ab2a279e597e039503ed415f0991f6e79b3c..d066dc863a38d589dc5ac4c663262bcef3e66781 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c854a270c7045f9281a11ece6a00ae854afec072..84544ad8fe2a6248034a00a0c51cbf436eedbe78 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/HopfFibration.xcscheme b/OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/HopfFibration.xcscheme
new file mode 100644 (file)
index 0000000..11b0657
--- /dev/null
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1620"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "AFA82E1D2DB1EB4B00BEEE3E"
+               BuildableName = "HopfFibration.saver"
+               BlueprintName = "HopfFibration"
+               ReferencedContainer = "container:xscreensaver.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AF9771D60989DC4A001F8B92"
+            BuildableName = "SaverTester.app"
+            BlueprintName = "SaverTester"
+            ReferencedContainer = "container:xscreensaver.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <EnvironmentVariables>
+         <EnvironmentVariable
+            key = "SELECTED_SAVER"
+            value = "HopfFibration"
+            isEnabled = "YES">
+         </EnvironmentVariable>
+      </EnvironmentVariables>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AF9771D60989DC4A001F8B92"
+            BuildableName = "SaverTester.app"
+            BlueprintName = "SaverTester"
+            ReferencedContainer = "container:xscreensaver.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
index 514ac239bc35f6c5ef7814c5eac4d7059d4a2588..c81eb7caa3021ef3d92c054a4f46fadaab4ee8e6 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index cd0f5999936d922d128c141b770aec6afe5dd1c5..edf30ea729f4f42a3259ffd2016997fffe56d11e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4b1741eb0b1a96c8ea02b9987c97cd489bbd051a..43f96bd2b9e3476469ca3156ae8a4e0014425a3b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 2bd1a8944664ab7a7028240368527b4be2b02f49..b26211d7d5b9f97b65b1a4721bd438be95a17f7a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6509481f4f3278d10a77531164d160cedf3db7a9..fc126fa928da90845bc9335070d3d865d4d35b45 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index a8a62fd13671344279d6fefe308915961a1aeecc..22981a575944c035e3a64cba7ee72eff45ce6075 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index a338e684bacfa69d6dd4d3813fa282ac8f2b9c48..dba5068e0e705227a45734c676e11cd1d869e033 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e495ac870ea64299315c1ef6f912200aea27a536..486e7483a4018cae570e520a7114daa12f7ca865 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 857eee39598cd8819f5b762a09969523025c576c..e3d5aea9c0d57be019df67d509f46d79e324cb2e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index cd832e2c13b4195f67fc1deb167bc0e2f52a37f3..75b0ffc6ef7f7826848ca6fb017cdded4c92bfaf 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 3a8c919c1e54628f75c3732e3a622937f566b5ce..c55460b66bc6622d99d0f96232d51ace5c249b6d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 448f0222a45f9d6b5b042fc41961dd650895e932..5a84f0f0a62fde7fa45b755529bf3f8e080114de 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e86012cb56639fd4e5a6619738721c2d12346081..8bb8f532327a9133159ee491d02ad4bcd3ea6c45 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ee00d8ee7613874bc23c80449e2aa993ac0f7fef..394ad360bebcef4671f92dfd4de9c8b6043c2c2e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 694a78897735d9f92d6a79506a1a37d75f564325..d4ff4b8f6d6ac0fc04c426cf81cb0ab8141a9a4e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index cec32c12c1cc2d6aaa69c7ac25730c356f52f933..de0ea36a6d981a6a8a5ea224e73447ddfe2a9bca 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d9f8fe011ea0877dd93f9fb9516a926babbb25a0..b6686cbeb39b85d835add074cb8ad46b418d0c3c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 8bc02774a8cb7930136acb6b6cffff5b4e62519a..e020a77e8db614445325bc33028c8c83b21f4cc8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.7">
    <BuildAction
       parallelizeBuildables = "YES"
index c44a430867c42fef68c03ba6066aa22f6fe703b9..b98f665f534b4cb4c9b17b030eedc9ffa1df51a2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Klondike.xcscheme b/OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/Klondike.xcscheme
new file mode 100644 (file)
index 0000000..f56e5f7
--- /dev/null
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1620"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "AF2D86952DB20F2100B4F248"
+               BuildableName = "Klondike.saver"
+               BlueprintName = "Klondike"
+               ReferencedContainer = "container:xscreensaver.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AF9771D60989DC4A001F8B92"
+            BuildableName = "SaverTester.app"
+            BlueprintName = "SaverTester"
+            ReferencedContainer = "container:xscreensaver.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <EnvironmentVariables>
+         <EnvironmentVariable
+            key = "SELECTED_SAVER"
+            value = "Klondike"
+            isEnabled = "YES">
+         </EnvironmentVariable>
+      </EnvironmentVariables>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AF9771D60989DC4A001F8B92"
+            BuildableName = "SaverTester.app"
+            BlueprintName = "SaverTester"
+            ReferencedContainer = "container:xscreensaver.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
index 16b598158f52a847573c2e4d9a1678b381ee1402..dd3baae1872c5b261ff31e7a2bb74837946ec3b0 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 91f936f32f7c4bd1f7b069bf81c2c9439c5b0b39..5e1281d42b2e12ba0af503dc9e900f308125c32c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 5bff5964e0d45a71433e549bed373c65decc30ee..4a9f66e4d094592f64a07ac4f158b0ddbbc8e0f9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 19dbba0f0a0be623d0585f587e1429ce1689bf94..db1850335d2fe32ac672999de2441efa9a095118 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 212fdd042ff620ecbaa8dee7f4011114c421ec7b..fa007eb5c63548df9e8022be0967a42ee39cbbe1 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ac42d955770e60ebb47d1da327b6151be7532e0c..a79cbcbb7d5446bf2e6a6f478fee3920b2c7f67a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index a24af089929eea27006cab36a6ef0a57db04f5ce..8fc38d2279a1184c99b5989c3735393df1743d88 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e20234e22068c28774095f723be889bcc1c81e08..6d30692e9db54f0cab93de013bdd78c41838f02e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 7e69adc29834f179b4968797a4e10d201a7db101..ff6850b630f3835c050455c9a76ea08ad826305d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ef4eb87fe462ea8ee2b5e0757d97c067682856c7..71dd6e8d02042d5b4efea01266fb5d6e8cefad9e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 9b9ef9249edbcd2abf30c854b5ef98cccf7d4849..4062a79abd7386222fb57962332d26e62ee62202 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 876388015f9a30fb9b58ee17215914dc57faa578..8a82877fea7ef78958b0d836b47fe23f50c8d8b1 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index b7a6b398b7561e85ac6c623fda99cdcabe7b1bae..217d24f39b45afa02f7e182987f5117267317a20 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index b7886c9a73f6370f30a5dd5a72435f49bca651f5..ee28c9580525063223b1f3631a637c3003325b24 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c7f7044bb5093c05ddff14eef13b48b952256b3c..673d3e95046870fa22262429a7cc2451736d0099 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 68d448e0f355ff2153eb34973dc1c753d1b200a5..69606ec200cee452b413ea514ed36879a6edfc1d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ff3e49fc1b6164389d8aa18e1c1189c65fab6e6c..c37ff0a89045fed71eb380401f4c2698eb56b7ba 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 939299504f8f0a03a6696e6c8d28c8e8e5d4e186..c5533e15ae021d42629d1e85e006e7215dc1ca03 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index adcf4111241dbafeacfad2c82a6a1bf3b60a0ffe..8db94498883512ec16e76f9c838c59a8e765efec 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 57d8f0707d893c8d649ad19505eaa1a6a2941bcc..898c1152f2fd650ef7e34344ec3a0889032291df 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 2ca3afbbc87804b3d7516acc7a116477a98b7195..b87a015c77325d6926081b67c406523d3e113030 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 30d7d42160514070d37df475f388f79b6fa7a143..246e9e34648d399a6f82aa27c4af3a1d207d94c9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 506e2881f0e3ead894898a03850c7f1ddf9ac0f0..0370558bd67f72c8c163cace39f6c38d88f5ca3f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c198d5789fdf3140e82cce450e49caa8e845c5e9..cc4632100fe29a880cd963dddc9874270ecc9a0e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index bb9beb809d34660614e47b38f2b569c5c30e2edb..ca9a6c840cf72e02ede979607168f6cb48105b39 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index f563598798c32f63d05254d8a87cb0f6f6a4a1a0..1a638a2bd9f66e63d6d37b5b9ce134011a4c62b9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 28f50a4a34439c7dfddd7de722977b2399689fca..fbf20622155095c720f851cbf7df801e0e976252 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 443e6aa5990183e006751e0ab0985741fab1daeb..206aa547d86518dd02cc0e73091fb2870fdfa3cd 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 32bb070693b4dc25680e953c2861bfb567c6f6ab..4318213c9a6543c0bde8f920db804e6e1df2e7e0 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 736a7bdf675b72682406e427eeee68cd956fa193..e61967229a9b4297fbe54c262b2fa734ce9e2aaf 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e871fc2306f32c011ec910cde5974b513981320c..86459d6c6042208a814049fa7c301a31fe1565e4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 54af332f03be54a76616ba75b04e5202e229fa76..873db388917ddc1a0d5e6f637b1cc8c1c38250f4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 28c692629c05b1cf7a35815a80c9e5fa392ad482..53fe58d2b4b77297e89a1a113edc43a205b9e234 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 873afada3b0a9825cec8824b6c472e659fc56749..e8a9198e8ef07267121d9d98b785ba7104c437b7 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.7">
    <BuildAction
       parallelizeBuildables = "YES"
index a0d533dabdf46fdd223a1403e7ca77265d1e45b0..5a8aed94b4b9a36306f53aea13e439db9847cb51 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 498e643051b580ecbaadd4eee563baf5654888e2..5a0755d29482ff9b642abfef00d49416fcedaa13 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index aa8271b3fc41f362bad196a11515c5e3db3360bb..37ba6e1516b3a3c9c4c8a03368981edf18811aef 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 65e6f2be78ba4841bf7955e1a4674753b9859fd2..a67cfbed99186ee366fdef0b9e782453b65ba27c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 72668853cdc568945bdf1283c6689052083eced8..99b876088cad9fc7e66ffd96d3d5f87935753c32 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 53b30392b7bcc612b1b6628a13cb6ee760dd2cb1..48074f55037385869b5eb2a910d194992666942e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index d3e160e5c50467b2ee813920be155919a6bb0ed3..cf40ac4e814d4f040d8c4d2ea0d32f3512b3bb76 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index d3a7504f1f86186554a105a3769583c6d94bd9ad..a079422035d3df89799f5a6d60ea9f4ec2abbc8f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ef6dad2bdd99ca670c2a80f63d312fc944b2af9c..b606cd0bbdaa9f7d331dc6ff529a69b82aedeef9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 75e8694e3678305ec5d51747cd523dcb4330e525..e7c6ac3452c62921e9904afb1fb3d71e31e21c74 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 965edf71d0db69276cfe759759be225107f28e0a..feb2e09afa224679c499f4595a61d679e65a786d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e9c1e0ea2d3ef2321bffb5b46ccbf857cc8e7fab..94491aef9bffd6365e3f7f002cd087469ee23020 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/PlatonicFolding.xcscheme b/OSX/xscreensaver.xcodeproj/xcshareddata/xcschemes/PlatonicFolding.xcscheme
new file mode 100644 (file)
index 0000000..d30d67c
--- /dev/null
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1620"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "AF2D86722DB1FCC900B4F248"
+               BuildableName = "PlatonicFolding.saver"
+               BlueprintName = "PlatonicFolding"
+               ReferencedContainer = "container:xscreensaver.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AF9771D60989DC4A001F8B92"
+            BuildableName = "SaverTester.app"
+            BlueprintName = "SaverTester"
+            ReferencedContainer = "container:xscreensaver.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <EnvironmentVariables>
+         <EnvironmentVariable
+            key = "SELECTED_SAVER"
+            value = "PlatonicFolding"
+            isEnabled = "YES">
+         </EnvironmentVariable>
+      </EnvironmentVariables>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "AF9771D60989DC4A001F8B92"
+            BuildableName = "SaverTester.app"
+            BlueprintName = "SaverTester"
+            ReferencedContainer = "container:xscreensaver.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
index cd9ae25e922e690cc2359c8ffa30877cdf8289c1..792e885e5480a455c4d6800111a4273c48a3ac7a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 9d8348ffb90cd57d5943465292a9f4138b5d1c67..b42ce13882d2d673d30a12cca04d11b0e827a145 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 9c97a2a916b3294c29bcbec477d0f80b913c5616..959d5b96e4b6b089e1e57c5c780194a6f0d6d427 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "NO"
index 0446985b166de5de7f5dfa00f46931dd25ffb92a..83f080089c4e141010552326644074812dea972e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 390b0282a2b9cd8593ff8924d90855225fd1b8a9..7938d795f57cf66354566d56aadd8f0c2e667f64 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 533dd30f2906bbb5ccb12cb88bd68167ca829bdb..19b052b33d36a52d53a65051c255375a9fec2788 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index af560802d0480826a4b424caa3eb94ec12d6962f..22b61b38b7012cd55ebbc1850dbf53a325008617 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 967c6d387ecbfb85679a7a43b838cb11faa1c427..0187098a5e52673b8da630421a6af3c2631f8858 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 07b270d5a055a096141a808d0a34d740dbc7bbdf..5ad3e8ed3d019360aedd0127aa419bea45eb7fcf 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 96345ab949e4a8baec4ef3e87f86bc313577ed96..a759b6807e12577fe62ffa883762841e46ef1981 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 191acd597dfd9fe3d43d51e974e0a365e3428fa8..65342e89baa4569e6ec60cb8782392f8bc946658 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 1fcf82e5b2c3d47e9413a4e36fbdc31383d70129..5f33d2fc3bd41e0c12b5eb1ee06398e99e182a7e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 97f0809ec2d28e988f3254c9a36c22a172324748..d21d1778d59f0ae8c8a68e0800b42950dc1edea3 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 007b4e86c8bfaa891060152b7adffc1fbc97df3d..8cbf657f470c6a68f50c03a48543d7b65e94c068 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index ec9c11d927e3a381195bcd523699ed470c863d3b..e06f50dd44adda96eef6a80c3bf5ad14ece4e565 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index ee3a822a99b864f382f334f0f72c8e1088f87ecb..5541e4e6520e4ba522e43dca54ed7ddb9a4c5441 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 9911df232ec351b40973fa0b0007840c836a3d1b..754239abf231fceb285704c538a01d07dace9b21 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 59bfacbc0cc18d89b04f648bb592e1f0e2d16782..bd886f73ac608b8b8c9344acdef5f37aae1c0317 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 1ee00511f6e096a7e0f226921e2804da691ee4e6..1463f7959969351a100cb3d5d4ce5ae6a5395dff 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 672a42a911c68e687972b91e02cd5912981b9329..27f5979c428076a8ec6fe034c9617f1b8b655162 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d2c52dfd11daf34881a46006b3d4a42589585d57..0eb2aea2629ec563d1c06416db7e3e90b96dbc8f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index f5812a8bbdd5a39683e0798960750a92456ba026..b025915860805c3bacca73dcfb99059cdd3e23f8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 79aa92af9c908f7aa2ef9cc2f47f0d5fedc45f9f..134e4df78a7cf37596699604f6f6639b503d0885 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index b51e45f1da12196f470db5ef91a8c1c13f3941fe..e623e7e7666bdb19534f83d4a53358a6492322ec 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 29983b5f8394a44ae414ab6195e89b4a907b34f9..ce984b87e35f1e86d546bd72f290a302382a63ca 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 06ef9d9a7d052a5700f6a9abc320dc506c3b999d..f28e50e51d024ba374d146bb7230e61300afa40c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -25,7 +25,7 @@
    <TestAction
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       shouldUseLaunchSchemeArgsEnv = "YES">
       <MacroExpansion>
          <BuildableReference
index 656e67cfd78344e8c702e6e2080ae957458539fe..3aa5db5523b31a785281f4dac4440ab014b1eaed 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index a54299e6d758e46891b983bdf8a5d1a06c19d165..4a88479929134e986f798942f8afb77bcf73a707 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index a08b0d41fac7e177436952e10cbf7b5df346824a..3253dafa4bc1c39198fc3ae475ae82d74d6158f2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 5e66c59023ad99e76fd71837e330fc83a7824788..5130981ed7c34881f2f4e841dac034ac68a7ca2d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 83cf83d7d5944c3b9b87dd1edf585bccbb9a25fe..3f5bb3cbe6e96ab539c0fe9661ba9ff0aab2b2f3 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.7">
    <BuildAction
       parallelizeBuildables = "YES"
index ba67dbc316b7b85da34990ee6eb379a564ab1d51..e3116e5e17194f63da09de7dc6b2036ed6b933be 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 2b94827ccdb1968e185f65d3f7b07be18e91fc9e..0ab065cfcc4fcd5faca2ed4f256410312f779a72 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 9b9abee9d994d803ddcb6eed11b1855885b150b4..90de36d1a00390795be49a3c3ef07dbb981d402e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index aa18cb31bee577680cb494d0c16f9cc9790a2e20..82c24a7ac91fe600583a4ba68f5143a1164aafb6 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 53809e77b77cd71dbf827ea58afb64c584ebb6e2..8da3f8015e83e797cd5de6a59cc3563d37a7ebb8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index b624ed7a37d54f4d4942f136fd30c868eea9786c..3ef11af845b493376be8db101a5c571044e68ba7 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6cf01a3ee479d61da6609082640b534ce5b6c3b9..96fa9613afc2172f98c18901521fdb41a299d96e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 1d769c47a36be014b13d38774d196e1b28d44b93..48b851277186d88db16ec22a0f927c71a9421de2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 5b9a97d16b78f277d5803efffc4f3bd8df553d43..03ba4815ca7112e95e65add28d749200934f9ad2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index fd611d1a8f033b7139fce6bb1f691547e8d99187..250874a294afa6690051c308d4d81f12401c10ff 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6fd430c51625ac937785b9f8d0be9624d9d3e4f0..c28b9786c072773a855499e97c682f0d254c6fe8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 54a5a1e87d9204efa04296857920a1f5bf6c7ce8..5a189f823a1ac7f62e241c51d20e691de82f1059 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d5a2a4d7a491345d2e9b897bd24fb995872edb04..bc9d1ce839b78305a494247f3993d52afc3f566d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index f2738e6736eaa5a245588126b0aa2157c019be9d..89c930c96c0afec6bf4529edb54d79603881f53e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e33773126d3b220ff1fa4f930baa19d4f799ca99..30c89afa24647913933275c24a68ad3f21b9350b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 27a9fe9003194ab0bd5af3ef799fe38ed282712d..40d2b5db4b77c713212af9bf4b580a4ae6a20f85 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 77118f357c12658423f9c894a53b99e9433854f6..1c925bcbb3cec1212bb3d7cb41cc3ce5c6fa4566 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index b47e875db53e2304243e147b51ccbf43ff6fc260..6f9bfde6e1fa2bc3587843897ce6f4cea1f5d52b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index a9e6374c8c7d9b58cd1ce42b34e6b31f4a65f72e..90a5b452d7e30538532955d88265fc3bd838b34d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index f220d32793e9162909e091c2b4311e47f2c027ab..360ce7e5051026ed7c6e55d13f1a2a98bfa70653 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 303ed19646180b7baee9def720d3a3bfbc512156..e50fd950e191b5433bd792e257c5efc47a73d033 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 0be38b3e7d41b38a9a9fc4a5895b2b942a00833f..7889d3c0ee90c141917a1687abba7ce2dbb81439 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 35f7506d9e0bcd150350dad86bbdd45d40c9d586..8d164e6eea26dadb61e41864e263184a724c1db9 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 0348eeb9b5d0c3933b8c1ff2ec360b3e455bf242..3458b93a77b5daeda3b4aab07095d3145eefe7d2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 66713ec36f3958589683b545cca6ee8e20d3e994..ad5884eec6c9ae90db8fd9d55b80a39ace0d4379 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d0c89b44f44f2617e77e00752d8bdf4571e1e992..0d1774cb413d3742f5b248cac95909afbd939939 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index d614a42efe32e49d4780ee3efdf5b611e9f9d3d2..80bae844ada68304b3a36f241fd9f56c1b4e508e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 03a5e7196d81bd5ebc4db381a960d544575ad661..19b11a235708c8521ba2c1cd452227084073e24c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 8279409913e428973a00ae18c3b61b0a8d17196d..7685d679e97b741e3f38422690286d1a26b7e7ce 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index d75ad2f99d887160baa74f1b169c975f7be57dc9..1c9c9a94e569766f8d39c8c77ed8b55265eabf2b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index e82d873b42252b5be959d16b0d4e1819abc98ac6..edafd749262a36a83444b52e5db90febd6561803 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 866d567047420643a7a0bc771d8d1472ba9bbb80..cd5028c7ff2ae60a961b96d97db70eb4a77910cd 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 5e18f5662e5262ed1075556ae105867a7cc5104d..e747744095541b89d10b1a29382acdfccb0ee01e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c89c5554a3187f3340438700587e46cb74ae47a4..d2fea7e6856987f968ce2654da3356c8a38c7adf 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c4930070c82f89ceccbb345235696b424fadd8a3..76e3cda059d96da1eff6fcfe8b17738274910411 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index afa5b11090385ceb617e24999e7907532a494870..225ccbf5d35dd533a23668807c6b465761ac2085 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c7db5b0ac80b3c5c0fc8a95f23adb2b2efca9003..c81331734f93f921e02b50137f25278dc2bab149 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 621a52916fa37bd1406b761a6770d2f31823fafe..566b0ccc5ef8bd680ee8e0868fdef360e6c10f0d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index fcf28265db8a20034b7ac2f441e7d6a2325542d9..88188e26e30497adf9f9e1228c5177fd7c044d48 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 43fc618677159a9e2551a279758aee0d19093258..4f490b0e643d84b0046c2e89bd14cd648a7477e2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 09b0a57e538b6618c253052b283fcd8568238d3a..ccff8c97c405022bad70c9feaeae0e7d6a9f12cf 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 7234a599f7b4dd2c27a828c4a83bb539ac6e9fbe..9173c2cf1d108daa5fa5f2567b00f207b6429780 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 89efbd00790487050487c85cc36ffe1456f610ac..ec3df72a03de6ba8b3c1c009ac8c9c723a1d5f8e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index c04a132969bc97ce8ec25e8d3170a1c6bb49aaca..c919da53888522b98e5d48eda2012f98cb7aad8f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 6824f512e6f4687b14f20a5f2d97cbd0be77aba5..0d5083a3d8043e29ce7932b756b6e325e3f6347a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 32276cedf964c97bc7d3e3c516921985f95e9033..a95a535dc84be8b1cb480393227a6d9376a7d177 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 532a7ab6e344d12ede61e00ca8121c348408baad..d5b07f5377122cf0e1b8c440723f359592d50dc5 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 1400d44ae0cdadec9ddcf4c32a1775e6811b34d0..78be36befd93bb775f295acc1df45f8fdbe331ff 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index b96d40074309256ed83a8660df5b7a21d82946e2..45c4bf7249700b9ccbcec5a3fcd469f19bbae61b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 5daf7c1939b2e29b818c17a9080f94e0e6bfed72..e8cb57695925428e22200cc62ec3b07208358dee 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 90017a5f8b1b5d0921b16b091fcb193471459268..52853aa86adaecbae5f1778e88ada633393fc329 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 64f8cc1816b5c6020cde33977975cca3965598d2..1e2c756816762458f6092a81c4ae3596f3f3534c 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 944401d5d3bf7e04e93dc78a7fd569755ffe4a44..8f3f40b30a65e53b9aa766cd723fc9dd957bc79a 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4bb7a231f0e15bc75313a72e50701c0fa87f42d3..e868d0bab0230b6081f98335a07dc479e79028f2 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index de27cfe22432119ce8cbe7cd378f46e1653a0b3e..8f97c871cd5570104b7dd0fda95203fa29064e95 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 840a1cd730fbbfb64739d4ad4231906b12fe7882..e8c71bdf1a7eb85a66b16b8a22e85da8298bc28b 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ed2c0854af1df8d91318138aef4a88f7a7df6576..71cdbc2eaf1e1d523c6753bfae9b424e31f27f63 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 4b950c03aac34f8109ed0d7c40003fef631d0f45..736be986200a6cff6735db19d362f0fdbb7ea89d 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index a7b97b18804b637a566f944ccebe67c7714ad8a3..56ddb8c9c87f440d089934b17be206e844c628fb 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index bd54c35e2c0b5851f32c4977e08496c5b1bc03be..f75e838e6ce70eafa6f05a214b8cfe01f3037049 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index ff9686b69522ea8a7a0bebeea82ca5895b9e8b40..9f91a2252c7a62be928feead817d79a5ac27ef4e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 028f2915fd83facfd5a914cac5b6d45ae6e718b6..f120f8231d6b7da7afd747befb26d0adb3b53b8f 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 0e18d16338468320e7d5dd324a1b918b1759dee2..6d8e18dac8cef90a523e9f2d2127691b56028b4e 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
index 00500c128eee93083d18077fca1198b9a66011ea..7f702c0caef404a809d5621169197e95116c69ed 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4fc27860a9a4537b1add4ea7bdbf3741f723f7e1..b90368a09e1bbf696a94c22bf7952825d6825cda 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 4fd6aa52a05ce2313a7b515da9e413dfdbcde905..c75bef3d3493ef58db27482bacdc1cc6e47055b4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
index 2f62f5b47faaa97d977282555a471bb996a83eb5..e261b0ab1278d80637db234bc54403c26822a2c4 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1500"
+   LastUpgradeVersion = "1620"
    version = "1.8">
    <BuildAction
       parallelizeBuildables = "YES"
diff --git a/README b/README
index 2b6dc9636df49f522d8545a4f74f85f8a97a7cfc..69137d644b8fab4cb0ac1e646f71a7293b7aeb4a 100644 (file)
--- a/README
+++ b/README
@@ -38,7 +38,7 @@ To compile for a Unix system with X11:
     these libraries.  Append "-dev" or "-devel" to most of these:
 
         perl pkg-config gettext intltool libx11 libxext libxi libxt libxft
-        libxinerama libxrandr libxxf86vm libgl libglu libgle libgtk-3-0
+        libxinerama libxrandr libxxf86vm libgl libglu libgle libgtk-3
         libgdk-pixbuf2.0 libjpeg libxml2 libpam libsystemd elogind
 
     BSD systems might need gmake instead of make.
@@ -69,6 +69,14 @@ Interested in writing a new screen saver?
 Version History
 ===============================================================================
 
+6.10   * New hacks, `dumpsterfire', `hopffibration', `platonicfolding' and
+          `klondike'.
+       * Rewrote the VT100 emulator for 'apple2' and 'phosphor'.
+         Supports inverse and DEC Special Graphics.
+       * X11: `phosphor' scrolls fast again on "modern" Linux systems.
+       * BSOD supports systemd and bitlocker.
+       * Android: Fixed bug where photo access was not being requested.
+
 6.09   * New hacks,`kallisti' and `highvoltage'.
        * Formal-wear for `headroom'.
 
index c3a710b33f9e9f4eae57fbf80e10736700038ae3..02e15a036a9bc1ec4df28373f3b0744e0791af53 100644 (file)
@@ -1,6 +1,6 @@
-# generated automatically by aclocal 1.16.5 -*- Autoconf -*-
+# generated automatically by aclocal 1.17 -*- Autoconf -*-
 
-# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+# Copyright (C) 1996-2024 Free Software Foundation, Inc.
 
 # This file is free software; the Free Software Foundation
 # gives unlimited permission to copy and/or distribute it,
@@ -3456,7 +3456,7 @@ fi
 AC_SUBST([$1])dnl
 ])
 
-# Copyright (C) 2006-2021 Free Software Foundation, Inc.
+# Copyright (C) 2006-2024 Free Software Foundation, Inc.
 #
 # This file is free software; the Free Software Foundation
 # gives unlimited permission to copy and/or distribute it,
index 06af5a6669a728ba366a388d8a502bf017113995..f7aa6ad9bb383e62d969c39e2fdeadacaa7ebcf8 100644 (file)
@@ -122,6 +122,7 @@ export ANDROID_HACKS=               \
        dnalogo                 \
        drift                   \
        droste                  \
+       dumpsterfire            \
        dymaxionmap             \
        endgame                 \
        energystream            \
@@ -176,6 +177,7 @@ export ANDROID_HACKS=               \
        highvoltage             \
        hilbert                 \
        hopalong                \
+       hopffibration           \
        hypertorus              \
        hypnowheel              \
        ifs                     \
@@ -191,6 +193,7 @@ export ANDROID_HACKS=               \
        kaleidocycle            \
        kallisti                \
        klein                   \
+       klondike                \
        kumppa                  \
        lament                  \
        lavalite                \
@@ -224,6 +227,7 @@ export ANDROID_HACKS=               \
        piecewise               \
        pinion                  \
        pipes                   \
+       platonicfolding         \
        polyominoes             \
        polytopes               \
        pong                    \
index e29170c31c1c74abae962c66ff3b8098bac3da90..debdb728c025dbe262c7061dccd1ed9ecc6d7694 100644 (file)
@@ -5,7 +5,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:8.4.0'
+        classpath 'com.android.tools.build:gradle:8.9.1'
     }
 
 }
index f49017e1f623beb037bf96a9a522dac2f50bef76..622cf3b93afcecdee0fd8f6354f2e705e064362a 100644 (file)
@@ -3,5 +3,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-#distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-all.zip
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
index e7529c46f7a1db63a8efe711cab219e0afa28f73..1eef9163291cee5a2d8e38bf1b7ec5b556e6b9fc 100644 (file)
@@ -6,8 +6,8 @@ dependencies {
 }
 
 android {
-    compileSdkVersion 34
-    buildToolsVersion "34.0.0"
+    compileSdkVersion 36
+    buildToolsVersion "36.0.0"
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
index 905c261a28120c5086217136f8f1c93eae1468b8..28e0e1a163448a25fb58a78003674bbee9522acc 100644 (file)
@@ -57,6 +57,7 @@ LOCAL_SRC_FILES := \
 
 # Some savers occupy more than one source file:
 LOCAL_SRC_FILES += \
+    hacks/ansi-tty.c \
     hacks/glx/b_draw.c \
     hacks/glx/b_lockglue.c \
     hacks/glx/b_sphere.c \
@@ -71,6 +72,7 @@ LOCAL_SRC_FILES += \
     hacks/glx/cow_tail.c \
     hacks/glx/cow_udder.c \
     hacks/glx/dolphin.c \
+    hacks/glx/dumpster_model.c \
     hacks/glx/dymaxionmap-coords.c \
     hacks/glx/gllist.c \
     hacks/glx/glschool_alg.c \
@@ -78,8 +80,10 @@ LOCAL_SRC_FILES += \
     hacks/glx/handsy_model.c \
     hacks/glx/headroom_model.c \
     hacks/glx/highvoltage_model.c \
+    hacks/glx/hopfanimations.c \
     hacks/glx/involute.c \
     hacks/glx/kallisti_model.c \
+    hacks/glx/klondike-game.c \
     hacks/glx/lament_model.c \
     hacks/glx/pipeobjs.c \
     hacks/glx/quickhull.c \
@@ -184,6 +188,7 @@ LOCAL_SRC_FILES += \
     utils/usleep.c \
     utils/utf8wc.c \
     utils/xft.c \
+    utils/xftwrap.c \
     utils/xshm.c \
     utils/yarandom.c \
 
index bca473b3f6719f989f671c4549afe08272c0925b..115f807351fb98cbab2d833e23697774c8960a71 100644 (file)
@@ -19,6 +19,7 @@ package org.jwz.xscreensaver;
 
 import android.app.WallpaperManager;
 import android.content.Intent;
+import android.os.Build;
 import android.os.Bundle;
 import android.view.View;
 import android.provider.Settings;
@@ -80,8 +81,12 @@ public class Activity extends android.app.Activity
   }
 
   void checkPermission() {
-      // RES introduced in API 16
-      String permission = Manifest.permission.READ_EXTERNAL_STORAGE;
+      String permission = "";
+      if (Build.VERSION.SDK_INT >= 33) {
+          permission = Manifest.permission.READ_MEDIA_IMAGES;
+      } else {
+          permission = Manifest.permission.READ_EXTERNAL_STORAGE;
+      }
       if (permissionGranted(permission)) {
           withProceed();
       } else {
index 889040a4522b3715cdb6dc48f2f8af71fa6c5e6b..b9eb7dd9c3b56a228d1663c9676249bef4005a6b 100644 (file)
@@ -1,5 +1,5 @@
 /* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
- * xscreensaver, Copyright © 2016-2021 Jamie Zawinski <jwz@jwz.org>
+ * xscreensaver, Copyright © 2016-2024 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -38,6 +38,7 @@ import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.net.Uri;
+import android.os.Build;
 import android.view.GestureDetector;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -666,8 +667,13 @@ public class jwxyz
 
   public Object[] checkThenLoadRandomImage (int target_width, int target_height,
                                    boolean rotate_p) {
-      // RES introduced in API 16
-      String permission = Manifest.permission.READ_EXTERNAL_STORAGE;
+
+        String permission = "";
+        if (Build.VERSION.SDK_INT >= 33) {
+            permission = Manifest.permission.READ_MEDIA_IMAGES;
+        } else {
+            permission = Manifest.permission.READ_EXTERNAL_STORAGE;
+        }
 
         if (havePermission(permission)) {
             return loadRandomImage(target_width,target_height,rotate_p);
index 5bba409ae40302bd044426d1fa6e45447455febe..af5e09ee4820a32f0b212787e4279876077351ae 100755 (executable)
--- a/configure
+++ b/configure
@@ -20150,6 +20150,18 @@ have_swresample="$ok"
 # Check includes
 if test "$have_avutil" = yes; then
   ac_save_avutil_CPPFLAGS="$CPPFLAGS"
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libavutil includes" >&5
+printf %s "checking for libavutil includes... " >&6; }
+if test ${ac_cv_avutil_config_cflags+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) ac_cv_avutil_config_cflags=`$pkg_config --cflags libavutil` ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_avutil_config_cflags" >&5
+printf "%s\n" "$ac_cv_avutil_config_cflags" >&6; }
+  ac_avutil_config_cflags=$ac_cv_avutil_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_avutil_config_cflags"
   have_avutil=no
 
 
 if test "$have_avcodec" = yes; then
   ac_save_avcodec_CPPFLAGS="$CPPFLAGS"
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libavcodec includes" >&5
+printf %s "checking for libavcodec includes... " >&6; }
+if test ${ac_cv_avcodec_config_cflags+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) ac_cv_avcodec_config_cflags=`$pkg_config --cflags libavcodec` ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_avcodec_config_cflags" >&5
+printf "%s\n" "$ac_cv_avcodec_config_cflags" >&6; }
+  ac_avcodec_config_cflags=$ac_cv_avcodec_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_avcodec_config_cflags"
   have_avcodec=no
 
 
 if test "$have_avformat" = yes; then
   ac_save_avformat_CPPFLAGS="$CPPFLAGS"
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libavformat includes" >&5
+printf %s "checking for libavformat includes... " >&6; }
+if test ${ac_cv_avformat_config_cflags+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) ac_cv_avformat_config_cflags=`$pkg_config --cflags libavformat` ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_avformat_config_cflags" >&5
+printf "%s\n" "$ac_cv_avformat_config_cflags" >&6; }
+  ac_avformat_config_cflags=$ac_cv_avformat_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_avformat_config_cflags"
   have_avformat=no
 
 
 if test "$have_swscale" = yes; then
   ac_save_swscale_CPPFLAGS="$CPPFLAGS"
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libswscale includes" >&5
+printf %s "checking for libswscale includes... " >&6; }
+if test ${ac_cv_swscale_config_cflags+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) ac_cv_swscale_config_cflags=`$pkg_config --cflags libswscale` ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_swscale_config_cflags" >&5
+printf "%s\n" "$ac_cv_swscale_config_cflags" >&6; }
+  ac_swscale_config_cflags=$ac_cv_swscale_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_swscale_config_cflags"
   have_swscale=no
 
 
 if test "$have_swresample" = yes; then
   ac_save_swresample_CPPFLAGS="$CPPFLAGS"
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libswresample includes" >&5
+printf %s "checking for libswresample includes... " >&6; }
+if test ${ac_cv_swresample_config_cflags+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) ac_cv_swresample_config_cflags=`$pkg_config --cflags libswresample` ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_swresample_config_cflags" >&5
+printf "%s\n" "$ac_cv_swresample_config_cflags" >&6; }
+  ac_swresample_config_cflags=$ac_cv_swresample_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_swresample_config_cflags"
   have_swresample=no
 
@@ -20658,17 +20718,6 @@ if test "$have_avformat" = yes -a \
         "$have_swscale" = yes -a \
         "$have_swresample" = yes ; then
   have_ffmpeg=yes
-  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ffmpeg includes" >&5
-printf %s "checking for ffmpeg includes... " >&6; }
-if test ${ac_cv_ffmpeg_config_cflags+y}
-then :
-  printf %s "(cached) " >&6
-else case e in #(
-  e) ac_cv_ffmpeg_config_cflags=`$pkg_config --cflags $pkgs` ;;
-esac
-fi
-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_ffmpeg_config_cflags" >&5
-printf "%s\n" "$ac_cv_ffmpeg_config_cflags" >&6; }
   { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ffmpeg libs" >&5
 printf %s "checking for ffmpeg libs... " >&6; }
 if test ${ac_cv_ffmpeg_config_libs+y}
@@ -20684,7 +20733,13 @@ printf "%s\n" "$ac_cv_ffmpeg_config_libs" >&6; }
 
   FFMPEG_OBJS='$(FFMPEG_OBJS)'
 fi
-ac_ffmpeg_config_cflags=$ac_cv_ffmpeg_config_cflags
+ac_ffmpeg_config_cflags=" \
+  $ac_avutil_config_cflags \
+  $ac_avcodec_config_cflags \
+  $ac_avformat_config_cflags \
+  $ac_swscale_config_cflags \
+  $ac_swresample_config_cflags \
+"
 ac_ffmpeg_config_libs=$ac_cv_ffmpeg_config_libs
 FFMPEG_CFLAGS="$ac_ffmpeg_config_cflags"
 FFMPEG_LIBS="$ac_ffmpeg_config_libs"
index dc2ad2f5580ecff274b9bd0ff16dd6842a980eee..5e847061c8f2c27a526f4036e3c063dcfceb17a3 100644 (file)
@@ -4015,6 +4015,9 @@ have_swresample="$ok"
 # Check includes
 if test "$have_avutil" = yes; then
   ac_save_avutil_CPPFLAGS="$CPPFLAGS"
+  AC_CACHE_CHECK([for libavutil includes], ac_cv_avutil_config_cflags,
+                 [ac_cv_avutil_config_cflags=`$pkg_config --cflags libavutil`])
+  ac_avutil_config_cflags=$ac_cv_avutil_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_avutil_config_cflags"
   have_avutil=no
   AC_CHECK_X_HEADER(libavutil/avutil.h, [have_avutil=yes])
@@ -4023,6 +4026,9 @@ fi
 
 if test "$have_avcodec" = yes; then
   ac_save_avcodec_CPPFLAGS="$CPPFLAGS"
+  AC_CACHE_CHECK([for libavcodec includes], ac_cv_avcodec_config_cflags,
+                 [ac_cv_avcodec_config_cflags=`$pkg_config --cflags libavcodec`])
+  ac_avcodec_config_cflags=$ac_cv_avcodec_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_avcodec_config_cflags"
   have_avcodec=no
   AC_CHECK_X_HEADER(libavcodec/avcodec.h, [have_avcodec=yes])
@@ -4031,6 +4037,9 @@ fi
 
 if test "$have_avformat" = yes; then
   ac_save_avformat_CPPFLAGS="$CPPFLAGS"
+  AC_CACHE_CHECK([for libavformat includes], ac_cv_avformat_config_cflags,
+                 [ac_cv_avformat_config_cflags=`$pkg_config --cflags libavformat`])
+  ac_avformat_config_cflags=$ac_cv_avformat_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_avformat_config_cflags"
   have_avformat=no
   AC_CHECK_X_HEADER(libavformat/avformat.h, [have_avformat=yes])
@@ -4039,6 +4048,9 @@ fi
 
 if test "$have_swscale" = yes; then
   ac_save_swscale_CPPFLAGS="$CPPFLAGS"
+  AC_CACHE_CHECK([for libswscale includes], ac_cv_swscale_config_cflags,
+                 [ac_cv_swscale_config_cflags=`$pkg_config --cflags libswscale`])
+  ac_swscale_config_cflags=$ac_cv_swscale_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_swscale_config_cflags"
   have_swscale=no
   AC_CHECK_X_HEADER(libswscale/swscale.h, [have_swscale=yes])
@@ -4047,6 +4059,9 @@ fi
 
 if test "$have_swresample" = yes; then
   ac_save_swresample_CPPFLAGS="$CPPFLAGS"
+  AC_CACHE_CHECK([for libswresample includes], ac_cv_swresample_config_cflags,
+                 [ac_cv_swresample_config_cflags=`$pkg_config --cflags libswresample`])
+  ac_swresample_config_cflags=$ac_cv_swresample_config_cflags
   CPPFLAGS="$CPPFLAGS $ac_swresample_config_cflags"
   have_swresample=no
   AC_CHECK_X_HEADER(libswresample/swresample.h, [have_swresample=yes])
@@ -4114,14 +4129,18 @@ if test "$have_avformat" = yes -a \
         "$have_swscale" = yes -a \
         "$have_swresample" = yes ; then
   have_ffmpeg=yes
-  AC_CACHE_CHECK([for ffmpeg includes], ac_cv_ffmpeg_config_cflags,
-                 [ac_cv_ffmpeg_config_cflags=`$pkg_config --cflags $pkgs`])
   AC_CACHE_CHECK([for ffmpeg libs], ac_cv_ffmpeg_config_libs,
                   [ac_cv_ffmpeg_config_libs=`$pkg_config --libs $pkgs`])
   AC_DEFINE(HAVE_FFMPEG)
   FFMPEG_OBJS='$(FFMPEG_OBJS)'
 fi
-ac_ffmpeg_config_cflags=$ac_cv_ffmpeg_config_cflags
+ac_ffmpeg_config_cflags=" \
+  $ac_avutil_config_cflags \
+  $ac_avcodec_config_cflags \
+  $ac_avformat_config_cflags \
+  $ac_swscale_config_cflags \
+  $ac_swresample_config_cflags \
+"
 ac_ffmpeg_config_libs=$ac_cv_ffmpeg_config_libs
 FFMPEG_CFLAGS="$ac_ffmpeg_config_cflags"
 FFMPEG_LIBS="$ac_ffmpeg_config_libs"
index d3e89f5b316a7bc45197b29a654f9833a2bd59e8..eca70448db459a50acb3ebbfc2b9c23649665c08 100644 (file)
@@ -4,8 +4,8 @@
 !            a screen saver and locker for the X window system
 !                            by Jamie Zawinski
 !
-!                              version 6.09
-!                              07-Jun-2024
+!                              version 6.10
+!                              27-Apr-2025
 !
 ! See "man xscreensaver" for more info.  The latest version is always
 ! available at https://www.jwz.org/xscreensaver/
@@ -577,7 +577,11 @@ XScreenSaver.bourneShell:          /bin/sh
 @GL_KLUDGE@ GL:                                papercube --root                            \n\
 @GL_KLUDGE@ GL:                                skulloop --root                             \n\
 @GL_KLUDGE@ GL:                                highvoltage --root                          \n\
-@GL_KLUDGE@ GL:                                kallisti --root                             \n
+@GL_KLUDGE@ GL:                                kallisti --root                             \n\
+@GL_KLUDGE@ GL:                                klondike --root                             \n\
+@GL_KLUDGE@ GL:                                dumpsterfire --root                         \n\
+@GL_KLUDGE@ GL:                                hopffibration --root                        \n\
+@GL_KLUDGE@ GL:                                platonicfolding --root                      \n
 
 
 
@@ -618,6 +622,7 @@ XScreenSaver.bourneShell:           /bin/sh
 *hacks.decayscreen.name:    Decay Screen
 *hacks.deepstars.name:      Deep Stars
 *hacks.dnalogo.name:        DNA Logo
+*hacks.dumpsterfire.name:   Dumpster Fire
 *hacks.dymaxionmap.name:    Dymaxion Map
 *hacks.energystream.name:   Energy Stream
 *hacks.etruscanvenus.name:  Etruscan Venus
@@ -651,6 +656,7 @@ XScreenSaver.bourneShell:           /bin/sh
 *hacks.hexstrut.name:       Hex Strut
 *hacks.hextrail.name:       Hex Trail
 *hacks.highvoltage.name:    High Voltage
+*hacks.hopffibration.name:  Hopf Fibration
 *hacks.ifs.name:            IFS
 *hacks.imsmap.name:         IMS Map
 *hacks.jigglypuff.name:     Jiggly Puff
@@ -673,6 +679,7 @@ XScreenSaver.bourneShell:           /bin/sh
 *hacks.pacman.name:         Pac-Man
 *hacks.papercube.name:      Paper Cube
 *hacks.photopile.name:      Photo Pile
+*hacks.platonicfolding.name:Platonic Folding
 *hacks.popsquares.name:     Pop Squares
 *hacks.projectiveplane.name:Projective Plane
 *hacks.quasicrystal.name:   Quasi-Crystal
index c469a3ef95f80d8989865de65404b234051c6a6b..420a474a489433607ae34f321a4ce8ab22414b31 100644 (file)
   GL:                          papercube --root                            \\n\
   GL:                          skulloop --root                             \\n\
   GL:                          highvoltage --root                          \\n\
-  GL:                          kallisti --root                             \\n",
+  GL:                          kallisti --root                             \\n\
+  GL:                          klondike --root                             \\n\
+  GL:                          dumpsterfire --root                         \\n\
+  GL:                          hopffibration --root                        \\n\
+  GL:                          platonicfolding --root                      \\n",
 "*hacks.antinspect.name:     Ant Inspect",
 "*hacks.antmaze.name:        Ant Maze",
 "*hacks.antspotlight.name:   Ant Spotlight",
 "*hacks.decayscreen.name:    Decay Screen",
 "*hacks.deepstars.name:      Deep Stars",
 "*hacks.dnalogo.name:        DNA Logo",
+"*hacks.dumpsterfire.name:   Dumpster Fire",
 "*hacks.dymaxionmap.name:    Dymaxion Map",
 "*hacks.energystream.name:   Energy Stream",
 "*hacks.etruscanvenus.name:  Etruscan Venus",
 "*hacks.hexstrut.name:       Hex Strut",
 "*hacks.hextrail.name:       Hex Trail",
 "*hacks.highvoltage.name:    High Voltage",
+"*hacks.hopffibration.name:  Hopf Fibration",
 "*hacks.ifs.name:            IFS",
 "*hacks.imsmap.name:         IMS Map",
 "*hacks.jigglypuff.name:     Jiggly Puff",
 "*hacks.pacman.name:         Pac-Man",
 "*hacks.papercube.name:      Paper Cube",
 "*hacks.photopile.name:      Photo Pile",
+"*hacks.platonicfolding.name:Platonic Folding",
 "*hacks.popsquares.name:     Pop Squares",
 "*hacks.projectiveplane.name:Projective Plane",
 "*hacks.quasicrystal.name:   Quasi-Crystal",
index 80a49bae5f0830cee952b7408cb657747c0afba0..6580502dfd3ae52bfdfb1a41f8aeb2d76dd4390d 100644 (file)
@@ -1,5 +1,5 @@
 /* auth.h --- Providing authentication mechanisms.
- * Copyright © 1993-2022 Jamie Zawinski <jwz@jwz.org>
+ * Copyright © 1993-2024 Jamie Zawinski <jwz@jwz.org>
  * (c) 2007, Quest Software, Inc. All rights reserved.
  * This file is part of XScreenSaver.
  *
@@ -86,6 +86,7 @@ extern Bool xscreensaver_auth_conv (void *closure,
                                     auth_response **resp);
 extern void xscreensaver_auth_finished (void *closure, Bool authenticated_p);
 extern void xscreensaver_splash (void *root_widget, Bool disable_settings_p);
+extern void xscreensaver_auth_test_mode (void);  /* for test-passwd.c */
 
 #endif /* __XSCREENSAVER_AUTH_H__ */
 
index 417f862fecb793cd42e2bf9f24655815d172dd8a..5b5e868b865d9802dcabfa7b8123998b9b531f63 100644 (file)
@@ -2337,6 +2337,19 @@ thermo_timer (XtPointer closure, XtIntervalId *id)
 }
 
 
+/* This is horrible, but if we don't ignore XInput events when running
+   test-passwd.c, then every mouse is delivered twice (both XI and Xlib)
+   so we only see every other page, because every click is a double-click.
+   Only test_auth_conv() calls this to set this flag.
+ */
+static Bool test_mode_ignore_xi_events = False;
+void
+xscreensaver_auth_test_mode (void)
+{
+  test_mode_ignore_xi_events = True;
+}
+
+
 static void
 gui_main_loop (window_state *ws, Bool splash_p, Bool notification_p)
 {
@@ -2422,6 +2435,8 @@ gui_main_loop (window_state *ws, Bool splash_p, Bool notification_p)
           Bool ok =
             xinput_event_to_xlib (xev.xcookie.evtype, xev.xcookie.data, &ev2);
           XFreeEventData (ws->dpy, &xev.xcookie);
+          if (test_mode_ignore_xi_events)
+            ok = False;
           if (ok)
             xev = ev2;
         }
index 2c894ac1f26cc3c20fed8797ef3f920665f4dd25..8714de6a2ee7934b7e9187b496794472144b33f8 100644 (file)
@@ -34,6 +34,8 @@ test_auth_conv (void *closure,
   nmsgs = 0;
   msg = (auth_message *) calloc (100, sizeof(*msg));
 
+  xscreensaver_auth_test_mode();
+
 # define DIALOG()                                                       \
   fprintf (stderr, "\n%s: page %d\n", blurb(), page++);                 \
   xscreensaver_auth_conv (closure, nmsgs, msg, resp);                   \
@@ -93,7 +95,8 @@ test_auth_conv (void *closure,
 
 
   msg[nmsgs].type = AUTH_MSGTYPE_PROMPT_ECHO;
-  msg[nmsgs].msg = "1/1 Page Five visible text";
+  msg[nmsgs].msg = "1/1 Page Five visible text"
+    "over several lines, probably like three or four; y and g.";
   nmsgs++;
 
   msg[nmsgs].type = AUTH_MSGTYPE_PROMPT_ECHO;
index 6ecaa902f218bd56ca5f2d95d58bf5745384570a..d6c5231fc62bfdad56e4b0e7519968d61ba2ada5 100644 (file)
@@ -307,10 +307,10 @@ file, or it won't work.
 to get the default host and display number.
 .TP 8
 .B PATH
-to find the sub-programs to run.  However, note that the sub-programs
-actually launched by \fIxscreensaver-settings\fP for display in the
-inline preview pane, but are launched by the \fIxscreensaver\fP daemon
-when run full screen, so the \fB$PATH\fP setting in both processes matters.
+to find the sub-programs to run.  However, note that the sub-programs are
+launched by \fIxscreensaver-settings\fP for display in the inline preview
+pane, but by the \fIxscreensaver\fP daemon when run full screen, so the
+\fB$PATH\fP setting in both processes matters.
 .TP 8
 .B HOME
 for the directory in which to read and write the \fI.xscreensaver\fP file.
index 2e174b24654639bd136b9175401b96336d565913..e58825f15b4be320096f8313eac6b113ab95f7d5 100644 (file)
  *   - When the system is about to go to sleep (e.g., laptop lid closing)
  *     it locks the screen *before* the system goes to sleep, by running
  *     "xscreensaver-command -suspend".  And then when the system wakes
- *     up again, it runs "xscreensaver-command -deactivate" to force the
+ *     up again, it runs "xscreensaver-command --deactivate" to force the
  *     unlock dialog to appear immediately.
  *
  *   - When another process on the system asks for the screen saver to be
  *     inhibited (e.g. because a video is playing) this program periodically
- *     runs "xscreensaver-command -deactivate" to keep the display un-blanked.
+ *     runs "xscreensaver-command --deactivate" to keep the display un-blanked.
  *     It does this until the other program asks for it to stop.
  *
  * For this to work at all, you must either:
@@ -38,7 +38,7 @@
  *     A: Be running GNOME's "org.gnome.SessionManager" D-Bus service; or
  *     B: Be running KDE's "org.kde.Solid.PowerManagement.PolicyAgent" svc; or
  *     C: Prevent your desktop environment from running the
- *        "org.freedesktop.ScreenSaver "service.
+ *        "org.freedesktop.ScreenSaverservice.
  *
  *
  *****************************************************************************
@@ -47,7 +47,7 @@
  *
  *     For decades, the traditional way for a video player to temporarily
  *     inhibit the screen saver was to have a heartbeat command that ran
- *     "xscreensaver-command -deactivate" once a minute while the video was
+ *     "xscreensaver-command --deactivate" once a minute while the video was
  *     playing, and ceased when the video was paused or stopped.  The reason
  *     to do it as a heartbeat rather than a toggle is so that the player
  *     fails SAFE -- if the player exits abnormally, the heart stops beating,
@@ -84,7 +84,7 @@
  *
  *     To recap: because the existing video players decided to delete the
  *     single line of code that they already had -- the heartbeat call to
- *     "xscreensaver-command -deactivate" -- we had to respond by adding
+ *     "xscreensaver-command --deactivate" -- we had to respond by adding
  *     TWELVE HUNDRED LINES of complicated code that talks to a server that
  *     may not be running, and that may not allow us to connect, and that may
  *     not work properly anyway.
  *  Bad Chromium bug #1:
  *
  *     It inhibits when only audio is playing, and does so with the same
- *     message as for audio, so we can't tell them apart.  This means that,
+ *     message as for video, so we can't tell them apart.  This means that,
  *     unlike Firefox, playing audio-only in Chromium will prevent your
  *     screen from blanking.
  *
  *  Bad Chromium bug #2:
  *
  *     It inhibits when short looping videos are playing.  Many sites
- *     (including Twitter) auto-convert GIFs to looping MP4s to save
- *     bandwidth, so Chromium will inhibit any time such a GIF is visible.
+ *     auto-convert GIFs to looping MP4s to save bandwidth, so Chromium will
+ *     inhibit any time such a GIF is visible.
  *
  *     The proper way for Chrome to fix this would be to stop inhibiting once
  *     the video loops.  That way your multi-hour movie inhibits properly, but
- *     your looping GIF only inhibits for the first few seconds.
- *     
+ *     your looping GIF only inhibits for the first few seconds.  Other
+ *     reasonable choices might include: do not inhibit for silent or muted
+ *     videos; only inhibit for full-screen videos.
+ *
  *
  *****************************************************************************
  *
  *     I have not verified this:
  *
  *       ~/.mplayer/config:
- *       heartbeat-cmd="xscreensaver-command -deactivate >&- 2>&- &"
+ *       heartbeat-cmd="xscreensaver-command --deactivate >&- 2>&- &"
  *
  *
  *****************************************************************************
  *
  *     This setting prevents the screen from blanking, and has a long history
  *     of becoming turned on accidentally. Tries org.freedesktop.ScreenSaver
- *     and others before falling back to "xscreensaver-command -deactivate"
+ *     and others before falling back to "xscreensaver-command --deactivate"
  *     as a heartbeat.
  *
  *
  *     "org.gnome.SessionManager" with the same behavior, we should probably
  *     listen to "InhibitorAdded" on that as well.
  *
- *   - Run under valgrind to check for any memory leaks.
- *
  *   - Apparently the two different desktops have managed to come up with
  *     *three* different ways for dbus clients to ask the question, "is the
  *     screen currently blanked?"  We should probably also respond to these:
  *       /ScreenSaver org.freedesktop.ScreenSaver \
  *       UnInhibit u 1792821391
  *
- * https://github.com/mato/xscreensaver-systemd
- *
  *
  *****************************************************************************
  */
@@ -464,7 +462,7 @@ xscreensaver_command (const char *cmd)
 {
   char buf[1024];
   int rc;
-  sprintf (buf, "xscreensaver-command %.100s -%.100s",
+  sprintf (buf, "xscreensaver-command %.100s --%.100s",
            (verbose_p ? "--verbose" : "--quiet"),
            cmd);
   if (verbose_p)
index 44b745e08442186fbfd40a545add211854305962..743073ee818719250f78da64b8df768085b5dc58 100644 (file)
@@ -1,4 +1,4 @@
-# hacks/Makefile.in --- xscreensaver, Copyright © 1997-2024 Jamie Zawinski.
+# hacks/Makefile.in --- xscreensaver, Copyright © 1997-2025 Jamie Zawinski.
 # the `../configure' script generates `hacks/Makefile' from this file.
 
 @SET_MAKE@
@@ -51,6 +51,8 @@ X_PRE_LIBS    = @X_PRE_LIBS@
 X_EXTRA_LIBS   = @X_EXTRA_LIBS@
 XFT_LIBS       = @XFT_LIBS@
 
+FFMPEG_CFLAGS   = @FFMPEG_CFLAGS@
+
 # Note: see comment in ../driver/Makefile.in for explanation of X_LIBS, etc.
 #
 HACK_PRE       = $(LIBS) $(X_LIBS)
@@ -91,8 +93,8 @@ UTIL_OBJS     = $(UTILS_BIN)/alpha.o $(UTILS_BIN)/colors.o \
                  $(UTILS_BIN)/colorbars.o \
                  $(UTILS_BIN)/textclient.o $(UTILS_BIN)/aligned_malloc.o \
                  $(UTILS_BIN)/thread_util.o $(UTILS_BIN)/pow2.o \
-                 $(UTILS_BIN)/xft.o $(UTILS_BIN)/utf8wc.o \
-                 $(UTILS_BIN)/font-retry.o
+                 $(UTILS_BIN)/xft.o $(UTILS_BIN)/xftwrap.o \
+                 $(UTILS_BIN)/utf8wc.o $(UTILS_BIN)/font-retry.o
 
 SRCS           = xscreensaver-getimage.c \
                  attraction.c blitspin.c bouboule.c braid.c bubbles.c \
@@ -127,7 +129,7 @@ SRCS                = xscreensaver-getimage.c \
                  tessellimage.c delaunay.c recanim.c binaryring.c \
                  glitchpeg.c vfeedback.c scooter.c webcollage-cocoa.m \
                  webcollage-helper-cocoa.m testx11.c marbling.c \
-                 binaryhorizon.c droste.c ffmpeg-out.c
+                 binaryhorizon.c droste.c ffmpeg-out.c ansi-tty.c
 SCRIPTS                = xscreensaver-getimage-file xscreensaver-getimage-video \
                  xscreensaver-text vidwhacker webcollage
 
@@ -166,7 +168,7 @@ OBJS                = attraction.o blitspin.o bouboule.o braid.o bubbles.o \
                  asm6502.o abstractile.o lcdscrub.o hexadrop.o \
                  tessellimage.o delaunay.o recanim.o binaryring.o \
                  glitchpeg.o vfeedback.o scooter.o testx11.o marbling.o \
-                 binaryhorizon.o droste.o
+                 binaryhorizon.o droste.o ansi-tty.o
 
 EXES           = attraction blitspin bouboule braid decayscreen deco \
                  drift flame galaxy grav greynetic halo \
@@ -213,7 +215,7 @@ ANIM_OBJS   = recanim.o ffmpeg-out.o
 HDRS           = screenhack.h screenhackI.h fps.h fpsI.h xlockmore.h \
                  xlockmoreI.h automata.h bubbles.h ximage-loader.h \
                  apple2.h analogtv.h pacman.h pacman_ai.h pacman_level.h \
-                 asm6502.h delaunay.h recanim.h ffmpeg-out.h
+                 asm6502.h delaunay.h recanim.h ffmpeg-out.h ansi-tty.h
 MEN            = anemone.man apollonian.man attraction.man \
                  blaster.man blitspin.man bouboule.man braid.man bsod.man \
                  bumps.man ccurve.man compass.man coral.man \
@@ -400,9 +402,9 @@ distclean: clean
 
 # Adds all current dependencies to Makefile
 depend:
-       $(DEPEND) -s '# DO NOT DELETE: updated by make depend'              \
-       $(DEPEND_FLAGS) --                                                  \
-       $(INCLUDES) $(DEFS) $(DEPEND_DEFINES) $(CFLAGS) $(X_CFLAGS) --      \
+       $(DEPEND) -s '# DO NOT DELETE: updated by make depend'                          \
+       $(DEPEND_FLAGS) --                                                              \
+       $(INCLUDES) $(DEFS) $(DEPEND_DEFINES) $(CFLAGS) $(X_CFLAGS) $(FFMPEG_CFLAGS) -- \
        $(SRCS)
 
 # Adds some dependencies to Makefile.in -- not totally accurate, but pretty
@@ -410,24 +412,24 @@ depend:
 # to include only dependencies on files which are themselves a part of this
 # package.
 distdepend:: m6502.h
-       @echo updating dependencies in `pwd`/Makefile.in... ;               \
-       $(DEPEND) -w 0 -f -                                                 \
-       -s '# DO NOT DELETE: updated by make distdepend' $(DEPEND_FLAGS) -- \
-       $(INCLUDES_1) $(DEFS) $(DEPEND_DEFINES) $(CFLAGS) $(X_CFLAGS) --    \
-       $(SRCS) 2>/dev/null |                                               \
-       sort -d |                                                           \
-       (                                                                   \
-         awk '/^# .*Makefile.in ---/,/^# DO .*distdepend/' < Makefile.in ; \
-         sed -e '/^#.*/d'                                                  \
-             -e 's@ \./@ @g;s@ /[^ ]*@@g;/^.*:$$/d'                        \
-             -e 's@\.\./utils@$$(UTILS_SRC)@g'                             \
-             -e 's@ \([^$$]\)@ $$(srcdir)/\1@g'                            \
-             -e 's@ $$(srcdir)/\(.*config.h\)@ \1@g'                       \
-             -e 's@ $$(srcdir)/\(m6502.h\)@ \1@g'                          \
-             -e 's@ $$(srcdir)/\(images/gen/\)@ \1@g'                      \
-             -e 's@ $$(HACK_SRC)/\(images/gen/\)@ \1@g' ;                  \
-         echo ''                                                           \
-       ) > /tmp/distdepend.$$$$ &&                                         \
+       @echo updating dependencies in `pwd`/Makefile.in... ;                             \
+       $(DEPEND) -w 0 -f -                                                               \
+       -s '# DO NOT DELETE: updated by make distdepend' $(DEPEND_FLAGS) --               \
+       $(INCLUDES_1) $(DEFS) $(DEPEND_DEFINES) $(CFLAGS) $(X_CFLAGS) $(FFMPEG_CFLAGS) -- \
+       $(SRCS) 2>/dev/null |                                                             \
+       sort -d |                                                                         \
+       (                                                                                 \
+         awk '/^# .*Makefile.in ---/,/^# DO .*distdepend/' < Makefile.in ;               \
+         sed -e '/^#.*/d'                                                                \
+             -e 's@ \./@ @g;s@ /[^ ]*@@g;/^.*:$$/d'                                      \
+             -e 's@\.\./utils@$$(UTILS_SRC)@g'                                           \
+             -e 's@ \([^$$]\)@ $$(srcdir)/\1@g'                                          \
+             -e 's@ $$(srcdir)/\(.*config.h\)@ \1@g'                                     \
+             -e 's@ $$(srcdir)/\(m6502.h\)@ \1@g'                                        \
+             -e 's@ $$(srcdir)/\(images/gen/\)@ \1@g'                                    \
+             -e 's@ $$(HACK_SRC)/\(images/gen/\)@ \1@g' ;                                \
+         echo ''                                                                         \
+       ) > /tmp/distdepend.$$$$ &&                                                       \
        mv /tmp/distdepend.$$$$ Makefile.in
 
 TAGS: tags
@@ -456,7 +458,7 @@ check_men:
         fi
 
 validate_xml:
-       @cd $(srcdir) && ./check-configs.pl $(EXES)
+       @cd $(srcdir) && ./check-configs.pl --force $(EXES)
 
 munge_ad_file:
        @echo "Updating hack list in XScreenSaver.ad.in..." ; \
@@ -503,7 +505,7 @@ $(DRIVER_BIN)/prefs.o:
 
 
 # How we build object files in this directory.
-HACK_CFLAGS_BASE=$(INCLUDES) $(DEFS) $(CPPFLAGS) $(CFLAGS) $(X_CFLAGS)
+HACK_CFLAGS_BASE=$(INCLUDES) $(DEFS) $(CPPFLAGS) $(CFLAGS) $(X_CFLAGS) $(FFMPEG_CFLAGS)
 .c.o:
        $(CC) -c $(HACK_CFLAGS_BASE) $<
 
@@ -527,6 +529,7 @@ THRL                = $(THREAD_CFLAGS) $(THREAD_LIBS)
 ATV             = analogtv.o $(SHM) $(THRO)
 APPLE2          = apple2.o $(ATV)
 TEXT            = $(UTILS_BIN)/textclient.o
+TTY             = ansi-tty.o
 
 CC_HACK                = $(CC) $(LDFLAGS)
 
@@ -669,11 +672,12 @@ interference:  interference.o     $(HACK_OBJS) $(COL) $(SHM) $(THRO) $(DBE)
 truchet:        truchet.o      $(HACK_OBJS) $(COL)
        $(CC_HACK) -o $@ $@.o   $(HACK_OBJS) $(COL) $(HACK_LIBS)
 
-bsod:          bsod.o          $(HACK_OBJS) $(GRAB) $(APPLE2) $(PNG)
-       $(CC_HACK) -o $@ $@.o   $(HACK_OBJS) $(GRAB) $(APPLE2) $(PNG) $(PNG_LIBS) $(THRL)
+BSOD=$(HACK_OBJS) $(GRAB) $(APPLE2) $(PNG) $(UTILS_BIN)/xftwrap.o
+bsod:          bsod.o          $(BSOD)
+       $(CC_HACK) -o $@ $@.o   $(BSOD) $(PNG_LIBS) $(THRL)
 
-apple2:                apple2.o apple2-main.o        $(HACK_OBJS) $(ATV) $(GRAB) $(TEXT) $(PNG)
-       $(CC_HACK) -o $@ $@.o   apple2-main.o $(HACK_OBJS) $(ATV) $(GRAB) $(TEXT) $(PNG) $(PNG_LIBS) $(TEXT_LIBS) $(THRL)
+apple2:                apple2.o apple2-main.o        $(HACK_OBJS) $(ATV) $(GRAB) $(TEXT) $(PNG) $(TTY)
+       $(CC_HACK) -o $@ $@.o   apple2-main.o $(HACK_OBJS) $(ATV) $(GRAB) $(TEXT) $(PNG) $(PNG_LIBS) $(TEXT_LIBS) $(THRL) $(TTY)
 
 xanalogtv:     xanalogtv.o     $(HACK_OBJS) $(ATV) $(GRAB) $(PNG)
        $(CC_HACK) -o $@ $@.o   $(HACK_OBJS) $(ATV) $(GRAB) $(PNG) $(PNG_LIBS) $(HACK_LIBS) $(THRL)
@@ -728,8 +732,8 @@ spotlight:  spotlight.o     $(HACK_OBJS) $(GRAB)
 critical:      critical.o      $(HACK_OBJS) $(COL) $(ERASE)
        $(CC_HACK) -o $@ $@.o   $(HACK_OBJS) $(COL) $(ERASE) $(HACK_LIBS)
 
-phosphor:      phosphor.o      $(HACK_OBJS) $(TEXT) $(COL) $(PNG)
-       $(CC_HACK) -o $@ $@.o   $(HACK_OBJS) $(TEXT) $(COL) $(PNG) $(PNG_LIBS) $(TEXT_LIBS)
+phosphor:      phosphor.o      $(HACK_OBJS) $(TEXT) $(COL) $(PNG) $(TTY)
+       $(CC_HACK) -o $@ $@.o   $(HACK_OBJS) $(TEXT) $(COL) $(PNG) $(TTY) $(PNG_LIBS) $(TEXT_LIBS)
 
 xmatrix:       xmatrix.o       $(HACK_OBJS) $(TEXT) $(PNG)
        $(CC_HACK) -o $@ $@.o   $(HACK_OBJS) $(TEXT) $(PNG) $(PNG_LIBS) $(TEXT_LIBS)
@@ -1116,6 +1120,22 @@ anemotaxis.o: $(UTILS_SRC)/visual.h
 anemotaxis.o: $(UTILS_SRC)/xdbe.h
 anemotaxis.o: $(UTILS_SRC)/xft.h
 anemotaxis.o: $(UTILS_SRC)/yarandom.h
+ansi-tty.o: $(srcdir)/ansi-tty.h
+ansi-tty.o: ../config.h
+ansi-tty.o: $(srcdir)/fps.h
+ansi-tty.o: $(srcdir)/recanim.h
+ansi-tty.o: $(srcdir)/screenhackI.h
+ansi-tty.o: $(srcdir)/screenhack.h
+ansi-tty.o: $(UTILS_SRC)/colors.h
+ansi-tty.o: $(UTILS_SRC)/font-retry.h
+ansi-tty.o: $(UTILS_SRC)/grabclient.h
+ansi-tty.o: $(UTILS_SRC)/hsv.h
+ansi-tty.o: $(UTILS_SRC)/resources.h
+ansi-tty.o: $(UTILS_SRC)/usleep.h
+ansi-tty.o: $(UTILS_SRC)/utf8wc.h
+ansi-tty.o: $(UTILS_SRC)/visual.h
+ansi-tty.o: $(UTILS_SRC)/xft.h
+ansi-tty.o: $(UTILS_SRC)/yarandom.h
 ant.o: $(srcdir)/automata.h
 ant.o: ../config.h
 ant.o: $(srcdir)/fps.h
@@ -1150,6 +1170,7 @@ apollonian.o: $(UTILS_SRC)/yarandom.h
 apollonian.o: $(srcdir)/xlockmoreI.h
 apollonian.o: $(srcdir)/xlockmore.h
 apple2-main.o: $(srcdir)/analogtv.h
+apple2-main.o: $(srcdir)/ansi-tty.h
 apple2-main.o: $(srcdir)/apple2.h
 apple2-main.o: ../config.h
 apple2-main.o: $(srcdir)/fps.h
@@ -1360,6 +1381,7 @@ bsod.o: $(UTILS_SRC)/thread_util.h
 bsod.o: $(UTILS_SRC)/usleep.h
 bsod.o: $(UTILS_SRC)/visual.h
 bsod.o: $(UTILS_SRC)/xft.h
+bsod.o: $(UTILS_SRC)/xftwrap.h
 bsod.o: $(UTILS_SRC)/xshm.h
 bsod.o: $(UTILS_SRC)/yarandom.h
 bsod.o: $(srcdir)/ximage-loader.h
@@ -2672,6 +2694,7 @@ petri.o: $(UTILS_SRC)/usleep.h
 petri.o: $(UTILS_SRC)/visual.h
 petri.o: $(UTILS_SRC)/xft.h
 petri.o: $(UTILS_SRC)/yarandom.h
+phosphor.o: $(srcdir)/ansi-tty.h
 phosphor.o: ../config.h
 phosphor.o: $(srcdir)/fps.h
 phosphor.o: images/gen/6x10font_png.h
index 74da5d0ad63a6972401306f6e5b2bc7da52ec6e6..5caec0fbab6f9f022e94d37150792ab3a1e1f5fe 100644 (file)
@@ -1,4 +1,4 @@
-/* xanalogtv-cli, Copyright © 2018-2023 Jamie Zawinski <jwz@jwz.org>
+/* xanalogtv-cli, Copyright © 2018-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -996,10 +996,11 @@ analogtv_convert (const char **infiles, const char *outfile,
 static void
 usage(const char *err)
 {
-  if (err) fprintf (stderr, "%s: %s unknown\n", progname, err);
-  fprintf (stderr, "usage: %s [--verbose] [--duration secs] [--slideshow secs]"
-           " [--audio mp3-file] [--powerup] [--size WxH]"
-           " infile.png ... outfile.mp4\n",
+  if (err && *err) fprintf (stderr, "%s: %s unknown\n", progname, err);
+  fprintf (stderr,
+           "usage: %s [--verbose] [--duration secs] [--slideshow secs]\n"
+           "\t\t    [--audio mp3-file] [--powerup] [--size WxH]\n"
+           "\t\t    infile.png infile2.png ... outfile.mp4\n",
            progname);
   exit (1);
 }
@@ -1035,6 +1036,11 @@ main (int argc, char **argv)
        else if (!strcmp(argv[i], "-vvv")) verbose_p += 3;
        else if (!strcmp(argv[i], "-vvvv")) verbose_p += 4;
        else if (!strcmp(argv[i], "-vvvvv")) verbose_p += 5;
+       else if (!strcmp(argv[i], "-vvvvvv")) verbose_p += 6;
+       else if (!strcmp(argv[i], "-vvvvvvv")) verbose_p += 7;
+       else if (!strcmp(argv[i], "-vvvvvvvv")) verbose_p += 8;
+       else if (!strcmp(argv[i], "-vvvvvvvvv")) verbose_p += 9;
+       else if (!strcmp(argv[i], "-vvvvvvvvvv")) verbose_p += 10;
        else if (!strcmp(argv[i], "-duration") && argv[i+1])
          {
            char dummy;
@@ -1080,6 +1086,17 @@ main (int argc, char **argv)
   outfile = infiles[nfiles-1];
   infiles[--nfiles] = 0;
 
+  for (i = 0; i < nfiles; i++)
+    {
+      const char *f = infiles[i];
+      struct stat st;
+      if (stat (f, &st))
+        {
+          fprintf (stderr, "%s: %s does not exist\n", progname, f);
+          usage("");
+        }
+    }
+
   if (nfiles == 1)
     slideshow = duration;
 
diff --git a/hacks/ansi-tty.c b/hacks/ansi-tty.c
new file mode 100644 (file)
index 0000000..a91f364
--- /dev/null
@@ -0,0 +1,1930 @@
+/* xscreensaver, Copyright © 2025 Jamie Zawinski <jwz@jwz.org>
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or 
+ * implied warranty.
+ *
+ * An ANSI (VT100) terminal emulator.  Reads control codes and renders the
+ * screen into a character grid.  Other programs (apple2 and phosphor) then
+ * copy that layout to the screen while applying their own text styling.
+ *
+ *   https://en.wikipedia.org/wiki/ANSI_escape_code
+ *   https://vt100.net/docs/vt100-ug/chapter3.html
+ *   https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+ *   https://en.wikipedia.org/wiki/ISO/IEC_2022
+ *   https://invisible-island.net/vttest/
+ *   https://github.com/mattiase/wraptest
+ */
+
+#include "screenhack.h"
+#include "ansi-tty.h"
+#include "utf8wc.h"
+
+#define ESC  0x1B
+#define CSI "\x1B["
+
+# undef  UNDEF
+# define UNDEF -0xFFFF
+
+struct tty_state {
+  char buf[255], buf2[255];
+  int idx, idx2;
+  int awaiting_st;
+  int unicrud;
+  int auto_wrap_p;
+  int origin_relative_p;
+  int linefeed_p;
+
+  /* A fun quirk about ANSI / VT100 cursor addressing: When a character is
+     printed in column 80, the insertion point does not wrap to the next line
+     until the 81st character is printed, which will then appear in the
+     leftmost column of the following line.  In this case, the cursor blinks
+     atop the 80th character instead of after it. This means that a cursor at
+     position 80 is visually ambiguous with one at position 79.
+
+     Likewise, a character printed in the bottom right cell does not cause the
+     screen to scroll up by one line until the *next* character is printed.
+
+     However, this only applies to cursor coordinates caused by text insertion.
+     Inserting "X" at column 80 lets cursor.x go to 81; but if at column 70
+     and you do "move right by 20", the cursot ends up at 80, not 81.  Fun!
+
+     Details: https://github.com/mattiase/wraptest
+   */
+  int lcf;     /* Last Column Flag */
+
+  struct { int y1, y2; } scroll;
+  struct { int x,  y, lcf;
+           tty_flag flags; } saved;
+  tty_flag flags;
+  tty_flag g0, g1;
+  tty_color fg, bg;
+
+  char *tabs;  /* 1 in each column that has a tab stop set. */
+
+  FILE *log_file;
+};
+
+
+/* The DEC Special Graphics 8-bit character set used by vt100.
+   It is unclear to me if this was incorporated into ANSI.
+ */
+const unsigned long ansi_graphics_unicode[256] = {
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* 00-0F */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* 10-1F */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* 20-2F */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* 30-3F */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* 40-4F */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ' ',            /* 50-5F */
+  0x25C6,   /*  0x60 `    ◆  BLACK DIAMOND                            */
+  0x2592,   /*  0x61 a    ▒  MEDIUM SHADE                             */
+  0x2409,   /*  0x62 b    ␉  SYMBOL FOR HORIZONTAL TABULATION                 */
+  0x240C,   /*  0x63 c    ␌  SYMBOL FOR FORM FEED                     */
+  0x240D,   /*  0x64 d    ␍  SYMBOL FOR CARRIAGE RETURN               */
+  0x240A,   /*  0x65 e    ␊  SYMBOL FOR LINE FEED                     */
+  0x00B0,   /*  0x66 f    °  DEGREE SIGN                               */
+  0x00B1,   /*  0x67 g    ±  PLUS-MINUS SIGN                           */
+  0x2424,   /*  0x68 h    ␤  SYMBOL FOR NEWLINE                       */
+  0x240B,   /*  0x69 i    ␋  SYMBOL FOR VERTICAL TABULATION           */
+  0x2518,   /*  0x6A j    ┘  BOX DRAWINGS LIGHT UP AND LEFT           */
+  0x2510,   /*  0x6B k    ┐  BOX DRAWINGS LIGHT DOWN AND LEFT                 */
+  0x250C,   /*  0x6C l    ┌  BOX DRAWINGS LIGHT DOWN AND RIGHT                */
+  0x2514,   /*  0x6D m    └  BOX DRAWINGS LIGHT UP AND RIGHT          */
+  0x253C,   /*  0x6E n    ┼  BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL       */
+  0x23BA,   /*  0x6F o    ⎺  HORIZONTAL SCAN LINE-1                   */
+  0x23BB,   /*  0x70 p    ⎻  HORIZONTAL SCAN LINE-3                   */
+  0x2500,   /*  0x71 q    ─  BOX DRAWINGS LIGHT HORIZONTAL            */
+  0x23BC,   /*  0x72 r    ⎼  HORIZONTAL SCAN LINE-7                   */
+  0x23BD,   /*  0x73 s    ⎽  HORIZONTAL SCAN LINE-9                   */
+  0x251C,   /*  0x74 t    ├  BOX DRAWINGS LIGHT VERTICAL AND RIGHT    */
+  0x2524,   /*  0x75 u    ┤  BOX DRAWINGS LIGHT VERTICAL AND LEFT     */
+  0x2534,   /*  0x76 v    ┴  BOX DRAWINGS LIGHT UP AND HORIZONTAL     */
+  0x252C,   /*  0x77 w    ┬  BOX DRAWINGS LIGHT DOWN AND HORIZONTAL   */
+  0x2502,   /*  0x78 x    │  BOX DRAWINGS LIGHT VERTICAL              */
+  0x2264,   /*  0x79 y    ≤  LESS-THAN OR EQUAL TO                    */
+  0x2265,   /*  0x7A z    ≥  GREATER-THAN OR EQUAL TO                         */
+  0x03C0,   /*  0x7B {    π  GREEK SMALL LETTER PI                     */
+  0x2260,   /*  0x7C |    ≠  NOT EQUAL TO                             */
+  0x00A3,   /*  0x7D }    £  POUND SIGN                                */
+  0x00B7,   /*  0x7E ~    ·  MIDDLE DOT                                */
+  0,        /*  0x7F                                                    */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* 80-8F */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* 90-9F */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* A0-AF */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* B0-BF */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* C0-CF */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* D0-DF */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,              /* E0-EF */
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0               /* F0-FF */
+};
+
+
+static void
+set_color (ansi_tty *tty, int fg_p, int color)
+{
+  tty_state *st = tty->state;
+  tty_color *c = (fg_p ? &st->fg : &st->bg);
+  if (color < countof(tty->cmap))
+    {
+      *c = tty->cmap[color][fg_p ? 0 : 1];
+    }
+  else
+    {
+      c->r = (color >> 16) & 0xFF;
+      c->g = (color >> 8)  & 0xFF;
+      c->b = (color >> 0)  & 0xFF;
+    }
+}
+
+
+static void
+ansi_tty_default_tabs (ansi_tty *tty)
+{
+  int ts = 8;
+  int i;
+  char *tabs = tty->state->tabs;
+  for (i = 0; i < tty->width; i++)
+    tabs[i] = (i % ts) ? 0 : 1;
+}
+
+
+static void
+ansi_tty_reset (ansi_tty *tty)
+{
+  int odebug = tty->debug_p;
+  FILE *olog = tty->state->log_file;
+  char *otabs = tty->state->tabs;
+  memset (tty->grid, 0, tty->width * tty->height * sizeof (*tty->grid));
+  memset (tty->state, 0, sizeof(*tty->state));
+  tty->state->scroll.y1 = 0;
+  tty->state->scroll.y2 = tty->height;
+  tty->state->auto_wrap_p = True;
+  tty->state->fg = tty->cmap[2][0];    /* Green */
+  tty->state->bg = tty->cmap[0][1];    /* Black */
+  tty->debug_p = odebug;
+  tty->state->log_file = olog;
+  tty->state->tabs = otabs;
+  ansi_tty_default_tabs (tty);
+}
+
+
+ansi_tty *
+ansi_tty_init (int w, int h)
+{
+  ansi_tty *tty = (ansi_tty *) calloc (1, sizeof(*tty));
+  if (!tty) abort();
+  if (w <= 2 || h <= 2) abort();
+  tty->width  = w;
+  tty->height = h;
+  tty->grid   = (tty_char *) calloc (w * h, sizeof (*tty->grid));
+  tty->state  = (tty_state *) calloc (1, sizeof(*tty->state));
+  if (!tty->grid || !tty->state) abort();
+  tty->state->tabs = (char *) calloc (w, 1);
+  ansi_tty_default_tabs (tty);
+
+  tty->debug_p = 1;
+
+# if !defined(HAVE_MOBILE) && !defined(__OPTIMIZE__)
+  if (tty->debug_p >= 4)
+    {
+      const char *fn = "/tmp/ttylog.txt";
+      tty->state->log_file = fopen (fn, "wb");
+      if (!tty->state->log_file)
+        fprintf (stderr, "%s: unable to write %s\n", progname, fn);
+      else
+        fprintf (stderr, "%s: WARNING: logging to %s\n", progname, fn);
+
+      /* Note: if you're trying to do playback using this log file,
+         be sure to do "stty -onlcr ; cat FILE" or the shell will
+         turn \r\n into \r\r\n. */
+    }
+# endif
+
+  {
+    static const tty_color cmap[] = {          /*        VGA colors     */
+      { 0x00, 0x00, 0x00 },                    /* 30 40  Black          */
+      { 0xAA, 0x00, 0x00 },                    /* 31 41  Red            */
+      { 0x00, 0xAa, 0x00 },                    /* 32 42  Green          */
+      { 0xAA, 0x55, 0x00 },                    /* 33 43  Yellow         */
+      { 0x00, 0x00, 0xAA },                    /* 34 44  Blue           */
+      { 0xAA, 0x00, 0xAA },                    /* 35 45  Magenta        */
+      { 0x00, 0xAA, 0xAA },                    /* 36 46  Cyan           */
+      { 0xAA, 0xAA, 0xAA },                    /* 37 47  White          */
+      { 0x55, 0x55, 0x55 },                    /* 90 100 Bright Black   */
+      { 0xFF, 0x55, 0x55 },                    /* 91 101 Bright Red     */
+      { 0x55, 0xFF, 0x55 },                    /* 92 102 Bright Green   */
+      { 0xFF, 0xFF, 0x55 },                    /* 93 103 Bright Yellow  */
+      { 0x55, 0x55, 0xFF },                    /* 94 104 Bright Blue    */
+      { 0xFF, 0x55, 0xFF },                    /* 95 105 Bright Magenta */
+      { 0x55, 0xFF, 0xFF },                    /* 96 106 Bright Cyan    */
+      { 0xFF, 0xFF, 0xFF },                    /* 97 107 Bright White   */
+    };
+    int i;
+    for (i = 0; i < countof(cmap); i++)
+      {
+        tty->cmap[i][0] = cmap[i];
+        tty->cmap[i][1] = cmap[i];
+      }
+  }
+
+  ansi_tty_reset (tty);
+
+  if (tty->debug_p > 1)
+    fprintf (stderr, "%s: tty init %dx%d\n", progname, w, h);
+
+  return tty;
+}
+
+void
+ansi_tty_free (ansi_tty *tty)
+{
+  if (tty->state->log_file)
+    fclose (tty->state->log_file);
+  free (tty->state->tabs);
+  free (tty->state);
+  free (tty);
+}
+
+void ansi_tty_resize (ansi_tty *tty, int w, int h)
+{
+  tty_state *st = tty->state;
+  tty_char *grid2;
+  int x, y;
+  if (w <= 2 || h <= 2) abort();
+  grid2 = (tty_char *) calloc (w * h, sizeof (*grid2));
+
+  for (y = 0; y < tty->height; y++)
+    for (x = 0; x < tty->width; x++)
+      if (x < w && y < h)
+        grid2[y * w + x] = tty->grid[y * tty->width + x];
+
+  free (tty->grid);
+  tty->grid   = grid2;
+  tty->width  = w;
+  tty->height = h;
+
+  tty->state->tabs = (char *) realloc (tty->state->tabs, w);
+
+  if (tty->x >= w) tty->x = w-1;
+  if (st->scroll.y1 > tty->height) st->scroll.y1 = tty->height;
+  if (st->scroll.y2 > tty->height) st->scroll.y2 = tty->height;
+  if (tty->y >= st->scroll.y2) tty->y = st->scroll.y2 - 1;
+  if (tty->y >= st->scroll.y2) tty->y = st->scroll.y2 - 1;
+
+  if (tty->debug_p)
+    fprintf (stderr, "%s: tty resize %dx%d\n", progname, w, h);
+}
+
+
+/* Scroll lines up or down within the character grid.
+ */
+static void
+tty_scroll (ansi_tty *tty, int lines)
+{
+  tty_state *st = tty->state;
+  unsigned char *top = (unsigned char *)
+                       (tty->grid + tty->width * st->scroll.y1);
+  unsigned char *bot = (unsigned char *)
+                       (tty->grid + (tty->width * st->scroll.y2));
+  int total = bot - top;
+  int move  = lines * tty->width * sizeof(*tty->grid);
+
+  if ( lines >= st->scroll.y2 - st->scroll.y1 ||
+      -lines >= st->scroll.y2 - st->scroll.y1)
+    {
+      /* Scrolling more lines than exist is clearing. */
+      memset (top, 0, total);
+    }
+  else if (lines > 0)
+    {
+      memmove (top, top + move, total - move);
+      memset (top + total - move, 0, move);
+    }
+  else if (lines < 0)
+    {
+      move = -move;
+      memmove (top + move, top, total - move);
+      memset (top, 0, move);
+    }
+}
+
+
+/* Clear characters from the grid.
+   Start and end positions are inclusive.
+ */
+static void
+tty_erase (ansi_tty *tty, int x1, int y1, int x2, int y2)
+{
+  int from, to;
+  tty_char *ch, *end;
+
+  if (x1 < 0) x1 = 0;
+  if (x2 < 0) x2 = 0;
+  if (y1 < 0) y1 = 0;
+  if (y2 < 0) y2 = 0;
+  if (x1 > tty->width-1)  x1 = tty->width-1;
+  if (x2 > tty->width-1)  x2 = tty->width-1;
+  if (y1 > tty->height-1) y1 = tty->height-1;
+  if (y2 > tty->height-1) y2 = tty->height-1;
+
+  from = y1 * tty->width + x1;
+  to   = y2 * tty->width + x2;
+  if (from > to) abort();
+
+  ch  = tty->grid + from;
+  end = tty->grid + to;
+
+  while (ch <= end)
+    {
+# if 1
+      ch->c     = 0;
+      ch->flags = 0;
+      ch->fg    = tty->cmap[2][0];     /* Green */
+      ch->bg    = tty->cmap[0][1];     /* Black */
+# else
+      /* You might assume that turning on inverse and clearing to end of
+         line would give you an inverted line, but you would be wrong. */
+      ch->c     = tty->state->flags ? ' ' : 0;
+      ch->flags = tty->state->flags;
+      ch->fg    = tty->state->fg;
+      ch->bg    = tty->state->bg;
+# endif
+      ch++;
+    }
+}
+
+
+/* Print a line describing the non-command text that was printed,
+   once enough of it has buffered up to be legible. NULL means flush.
+ */
+static void
+tty_log_c (ansi_tty *tty, const char c)
+{
+  if (tty->debug_p > 2)
+    {
+      tty_state *st = tty->state;
+      char buf3[sizeof (st->buf2) * 4 + 100];
+      char *in, *out;
+
+      if (c)
+        {
+          st->buf2[st->idx2++] = c;
+          st->buf2[st->idx2] = 0;
+        }
+
+      if (((c >= ' ' && c <= '~') || c == '\r' || c == 0x08) &&
+          st->idx2 < sizeof(st->buf2) - 1)
+        return;
+
+      in = st->buf2;
+      out = buf3;
+      *out = 0;
+
+      while (*in) {
+        char c = *in++;
+        if (c >= ' ' && c <= '~')
+          {
+            *out++ = c;
+            *out = 0;
+          }
+        else
+          {
+            int pad = False;
+            if      (c == 0x08) strcat (out, "\\b");
+            else if (c == 0x09) strcat (out, "\\t");
+            else if (c == 0x0A) strcat (out, "\\n");
+            else if (c == 0x1B) strcat (out, "\\e");
+            else if (c == 0x0D) strcat (out, "\\r");
+            else
+              {
+                pad = True;
+                if (out > buf3 && out[-1] != ' ')
+                  {
+                    *out++ = ' ';
+                    *out = 0;
+                  }
+                sprintf (out, "\\x%02X", (unsigned char) c);
+              }
+
+            out += strlen (out);
+            if (*in && pad)
+              {
+                *out++ = ' ';
+                *out = 0;
+              }
+          }
+      }
+
+      if (out > buf3)
+        fprintf (stderr, "%s: txt: \"%s\"\n", progname, buf3);
+
+      st->idx2 = 0;
+      st->buf2[0] = 0;
+    }
+}
+
+
+/* Print a line describing the terminal command being executed.
+ */
+static void
+tty_log (ansi_tty *tty, const char *kind, int ok, const char *log,
+         int ac, const int *av, const char *cmd)
+{
+  if (tty->debug_p > (ok ? 1 : 0))
+    {
+      char buf[1024], *s = buf;
+      int i;
+
+      tty_log_c (tty, 0);  /* Flush */
+
+      if (ac) *s++ = ':';
+      *s = 0;
+
+      for (i = 0; i < ac; i++)
+        {
+          if (av[i] == UNDEF)
+            sprintf (s, " -");
+          else
+            sprintf (s, " %d", av[i]);
+          s += strlen(s);
+        }
+      if (!ok || tty->debug_p > 1)
+        {
+          int j = 0;
+          strcat (s, ": ");
+          s += strlen(s);
+          while (*cmd && j < 100)
+            {
+              if (*cmd == ESC)
+                strcat (s, "ESC ");
+              else if (*cmd < ' ' || *cmd > '~')
+                sprintf (s, "0x%02X", *cmd);
+              else
+                sprintf (s, "%c", *cmd);
+
+              s += strlen(s);
+              cmd++;
+              j++;
+            }
+        }
+
+      fprintf (stderr, "%s: %3s: %s%s%s\n", progname, kind,
+               (ok ? "" : "Unimplemented: "), log, buf);
+    }
+}
+
+
+/* Just gonna leave this here:
+
+       % infocmp -x vt100
+       Reconstructed via infocmp from file: /usr/share/terminfo/v/vt100
+       vt100|vt100-am|DEC VT100 (w/advanced video),
+       OTbs, am, mc5i, msgr, xenl, xon,
+       cols#80, it#8, lines#24, vt#3,
+       acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
+       bel=^G, blink=\E[5m$<2>, bold=\E[1m$<2>,
+       clear=\E[H\E[J$<50>, cr=\r, csr=\E[%i%p1%d;%p2%dr,
+       cub=\E[%p1%dD, cub1=^H, cud=\E[%p1%dB, cud1=\n,
+       cuf=\E[%p1%dC, cuf1=\E[C$<2>,
+       cup=\E[%i%p1%d;%p2%dH$<5>, cuu=\E[%p1%dA,
+       cuu1=\E[A$<2>, ed=\E[J$<50>, el=\E[K$<3>, el1=\E[1K$<3>,
+       enacs=\E(B\E)0, home=\E[H, ht=^I, hts=\EH, ind=\n, ka1=\EOq,
+       ka3=\EOs, kb2=\EOr, kbs=^H, kc1=\EOp, kc3=\EOn, kcub1=\EOD,
+       kcud1=\EOB, kcuf1=\EOC, kcuu1=\EOA, kent=\EOM, kf0=\EOy,
+       kf1=\EOP, kf10=\EOx, kf2=\EOQ, kf3=\EOR, kf4=\EOS, kf5=\EOt,
+       kf6=\EOu, kf7=\EOv, kf8=\EOl, kf9=\EOw, lf1=pf1, lf2=pf2,
+       lf3=pf3, lf4=pf4, mc0=\E[0i, mc4=\E[4i, mc5=\E[5i, rc=\E8,
+       rev=\E[7m$<2>, ri=\EM$<5>, rmacs=^O, rmam=\E[?7l,
+       rmkx=\E[?1l\E>, rmso=\E[m$<2>, rmul=\E[m$<2>,
+       rs2=\E<\E>\E[?3;4;5l\E[?7;8h\E[r, sc=\E7,
+       sgr=\E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t\016%e\017%;$<2>,
+       sgr0=\E[m\017$<2>, smacs=^N, smam=\E[?7h, smkx=\E[?1h\E=,
+       smso=\E[7m$<2>, smul=\E[4m$<2>, tbc=\E[3g,
+       u6=\E[%i%d;%dR, u7=\E[6n, u8=\E[?%[;0123456789]c, u9=\EZ,
+
+   Reformatted for legibility:
+
+       OTbs
+       auto_right_margin
+       prtr_silent
+       move_standout_mode
+       eat_newline_glitch
+       xon_xoff
+       cols#80
+       init_tabs#8
+       lines#24
+       virtual_terminal#3
+
+       acs_chars       = ``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~
+       bell                    = ^G
+       enter_blink_mode        = CSI 5 m
+       enter_bold_mode         = CSI 1 m
+       clear_screen            = CSI H
+                                 CSI J
+       carriage_return         = \r
+       change_scroll_region    = CSI <FROM> ; <TO> r
+       parm_left_cursor        = CSI <N> D
+       cursor_left             = ^H
+       parm_down_cursor        = CSI <N> B
+       cursor_down             = \n
+       parm_right_cursor       = CSI <N> C
+       cursor_right            = CSI C
+       cursor_address          = CSI <X> ; <Y> H
+       parm_up_cursor          = CSI <N> A
+       cursor_up               = CSI A
+       clr_eos                 = CSI J
+       clr_eol                 = CSI K
+       clr_bol                 = CSI 1 K
+       ena_acs                 = ESC ( B ESC ) 0
+       cursor_home             = CSI H
+       tab                     = ^I
+       set_tab                 = ESC H
+       scroll_forward          = \n
+       key_a1                  = ESC O q
+       key_a3                  = ESC O s
+       key_b2                  = ESC O r
+       key_backspace           = ^H
+       key_c1                  = ESC O p
+       key_c3                  = ESC O n
+       key_left                = ESC O D
+       key_down                = ESC O B
+       key_right               = ESC O C
+       key_up                  = ESC O A
+       key_enter               = ESC O M
+       key_f0                  = ESC O y
+       key_f1                  = ESC O P
+       key_f10                 = ESC O x
+       key_f2                  = ESC O Q
+       key_f3                  = ESC O R
+       key_f4                  = ESC O S
+       key_f5                  = ESC O t
+       key_f6                  = ESC O u
+       key_f7                  = ESC O v
+       key_f8                  = ESC O l
+       key_f9                  = ESC O w
+       lab_f1                  = pf1
+       lab_f2                  = pf2
+       lab_f3                  = pf3
+       lab_f4                  = pf4
+       print_screen            = CSI 0 i
+       prtr_off                = CSI 4 i
+       prtr_on                 = CSI 5 i
+       restore_cursor          = ESC 8
+       enter_reverse_mode      = CSI 7 m
+       scroll_reverse          = ESC M
+       exit_alt_charset_mode   = ^O
+       exit_am_mode            = CSI ? 7 l
+       keypad_local            = CSI ? 1 l ESC >
+       exit_standout_mode      = CSI m
+       exit_underline_mode     = CSI m
+       reset_2string           = ESC < ESC >
+                                 CSI ? 3 ; 4 ; 5 l
+                                 CSI ? 7 ; 8 h
+                                 CSI r
+       save_cursor             = ESC 7
+
+       set_attributes          = CSI 0
+                                 IF  %p1 | %p6  THEN  ; 1  ENDIF
+                                 IF  %p2        THEN  ; 4  ENDIF
+                                 IF  %p1 | %p3  THEN  ; 7  ENDIF
+                                 IF  %p4        THEN  ; 5  ENDIF
+                                 m
+                                 IF  %p9        THEN  \016
+                                 ELSE  \017                ENDIF
+
+       exit_attribute_mode     = CSI m \017
+       enter_alt_charset_mode  = ^N
+       enter_am_mode           = CSI ? 7 h
+       keypad_xmit             = CSI ? 1 h ESC =
+       enter_standout_mode     = CSI 7 m
+       enter_underline_mode    = CSI 4 m
+       clear_all_tabs          = CSI 3 g
+       user6                   = CSI %i %d ; %d R
+       user7                   = CSI 6 n
+       user8                   = CSI ? %[;0123456789] c
+       user9                   = ESC Z
+ */
+
+
+/* I find state machines soothing. If the thing you're writing is not
+   a state machine, it might be evil.
+     _______________________________________________________________
+    /                                                               \
+    |  Tech enthusiasts: My entire house is smart.                  |
+    |                                                               |
+    |  Tech workers: The only piece of technology in my house is a  |
+    |  printer and I keep a gun next to it so I can shoot it if it  |
+    |  makes a noise I don't recognize.          -- PPathole, 2019  |
+    \                                                               |
+    `---------------------------------------------------------------'
+ */
+void
+ansi_tty_print (ansi_tty *tty, unsigned long c)
+{
+  tty_state *st = tty->state;
+  int done = False;
+  int scrolled_p = False;
+
+  const char *kind = "?";
+  int av[255];
+  int ac = 0;
+  int i;
+
+  if (st->log_file)
+    {
+      fputc ((char) c, st->log_file);  /* Botches unicode */
+      fflush (st->log_file);
+    }
+
+# define LOG(S) tty_log (tty, kind, True,  (S), ac, av, st->buf)
+# define UND(S) tty_log (tty, kind, False, (S), ac, av, st->buf)
+
+
+  /* Parse the command sequences.
+
+     nF        20-2F    SPC - /        2+ bytes, [20-2F]+ 30-7E
+     Fp        30-3F    0-9:;<=>?      2 bytes
+     Fe        40-5F    @A-Zp\]^_      2 bytes, or CSI: ESC [ ... C
+     Fs        60-7E    `a-z{|}~       2 bytes
+   */
+
+  /* WTF, BS and others are allowed within command sequences! */
+  if (st->idx > 0 && c < ' ')
+    goto SELF_INSERT_INTERLUDE;
+
+  st->buf[st->idx++] = c;
+  st->buf[st->idx]   = 0;
+
+
+  /******************************************************************** ESC */
+
+
+  if (st->idx >= sizeof(st->buf) - 2)                   /* Buffer overflow */
+    {
+      kind = "?";
+      done = True;
+      UND ("OVERFLOW");
+    }
+  else if (st->awaiting_st)
+    {
+      kind = "ST0";
+      /* Some "Fe" commands swallow all bytes until an "ST" string terminator
+         command is received, so just keep going, filling the buffer. */
+    }
+  else if (c == ESC)                               /* Starting any command */
+    {
+      kind = "ESC";
+      if (st->idx > 1)
+        LOG ("Aborted by ESC");
+      st->idx = 1;
+    }
+
+
+  /********************************************************************* nF */
+
+
+  else if (st->idx == 2 &&                     /* nF escape sequence start */
+           st->buf[0] == ESC &&
+           c >= 0x20 &&                                /*    !"#$%&'()*+,-./       */
+           c <= 0x2F)
+    {
+      kind = "nFa";
+    }
+  else if (st->idx > 2 &&                      /* nF escape sequence cont. */
+           st->buf[0] == ESC  &&
+           st->buf[1] >= 0x20 &&               /*    !"#$%&'()*+,-./       */
+           st->buf[1] <= 0x2F &&
+           c >= 0x20 &&                                /*    !"#$%&'()*+,-./       */
+           c <= 0x2F)
+    {
+      kind = "nFb";
+    }
+  else if (st->idx > 2 &&                      /* nF escape sequence end   */
+           st->buf[0] == ESC  &&
+           st->buf[1] >= 0x20 &&               /*    !"#$%&'()*+,-./       */
+           st->buf[1] <= 0x2F &&
+           c >= 0x30 &&                                /*    0 - ~                 */
+           c <= 0x7E)
+    {
+      kind = "nF";
+      done = True;
+
+      switch (st->idx) {
+      case 2:                                  /* ESC ? */
+        kind = "nF2";
+        switch (st->buf[1]) {
+        case '`':      UND ("Disable manual input");                   break;
+        case 'a':      UND ("Interrupt");                              break;
+        case 'b':      UND ("Enable manual input");                    break;
+        case 'c':      LOG ("Reset");
+          ansi_tty_reset (tty);
+          break;
+        case 'd':      UND ("Coding method delimiter");                break;
+        case 'n':      UND ("Locking shift 1");                        break;
+        case 'o':      UND ("Locking shift 3");                        break;
+        case '|':      UND ("Locking shift 3R");                       break;
+        case '}':      UND ("Locking shift 2R");                       break;
+        case '~':      UND ("Locking shift 1R");                       break;
+        default:       UND ("Unknown");                                break;
+        }
+        break;
+
+      case 3:                                  /* ESC ? ? */
+        kind = "nF3";
+        switch (st->buf[1]) {
+        case '!':      UND ("C0-designate");                           break;
+        case '"':      UND ("C1-designate");                           break;
+
+        case '#':                              /* ESC # ? */
+          st->lcf = False;
+          switch (st->buf[2]) {
+          case '3':    UND ("Double Height Top");                      break;
+          case '4':    UND ("Double Height Bottom");                   break;
+          case '5':    UND ("Single Width");                           break;
+          case '6':    UND ("Double Width");                           break;
+          case '8':    LOG ("Screen Alignment");
+            {
+              tty_char  *ch = tty->grid;
+              tty_char *end = tty->grid + (tty->width * tty->height);
+              while (ch < end)
+                {
+                  ch->c = 'E';
+                  ch++;
+                }
+            }
+            break;
+          default:     UND ("Unknown #");                              break;
+          }
+          break;
+
+        case '$':      UND ("G0 designate");                           break;
+
+        case '%':                              /* ESC % ? */
+          switch (st->buf[2]) {
+          case '@':    UND ("Designate coding");                       break;
+          case 'B':    UND ("UTF-1");                                  break;
+          case 'G':    UND ("UTF-8");                                  break;
+          case '/':    UND ("UTF-32");                                 break;
+          default:     UND ("Unknown %");                              break;
+          }
+          break;
+        case '&':      UND ("Identify revised registration");          break;
+        case '\'':     UND ("Not used");                               break;
+
+        case '(':                              /* ESC ( ? */
+        case ')':                              /* ESC ( ? */
+          {
+            int g0_p = (st->buf[1] == '(');
+            tty_flag *gg = (g0_p ? &st->g0 : &st->g1);
+
+            /* This says which character set g0 and g1 indicate. */
+            *gg = 0;
+            switch (st->buf[2]) {
+            case '0':  LOG ("G0-designate 94-set: Line Drawing");
+              *gg = TTY_SYMBOLS;
+              break;
+
+              /* We could map these various fonts, particularly the
+                 line- and box-drawing font, to their corresponding
+                 Unicode code points.  But since neither Phosphor and
+                 Apple2 can display non-Latin1 characters, that
+                 wouldn't do us any good right now. */
+
+            case '1':  UND ("Gx-designate 94: Alt Char");              break;
+            case '2':  UND ("Gx-designate 94: Alt Special");           break;
+            case '5':  UND ("Gx-designate 94: Finnish");               break;
+            case '7':  UND ("Gx-designate 94: Swedish");               break;
+            case '9':  UND ("Gx-designate 94: French Canadian");       break;
+            case '<':  UND ("Gx-designate 94: DEC Supplemental");      break;
+            case '>':  UND ("Gx-designate 94: DEC Technical");         break;
+            case 'A':  UND ("Gx-designate 94: UK");                    break;
+            case 'B':  LOG ("Gx-designate 94: ASCII");                 break;
+            case 'C':  UND ("Gx-designate 94: Finnish");               break;
+            case 'H':  UND ("Gx-designate 94: Swedish");               break;
+            case 'I':  UND ("Gx-designate 94: JIS Katakana");          break;
+            case 'J':  UND ("Gx-designate 94: JIS Roman");             break;
+            case 'K':  UND ("Gx-designate 94: German");                break;
+            case 'Q':  UND ("Gx-designate 94: French Canadian");       break;
+            case 'R':  UND ("Gx-designate 94: French");                break;
+            case 'Y':  UND ("Gx-designate 94: Italian");               break;
+            case 'Z':  UND ("Gx-designate 94: Spanish");               break;
+            case 'f':  UND ("Gx-designate 94: French");                break;
+            default:   UND ("Unknown Gx");                             break;
+            }
+          }
+          break;
+                                               /* ESC # # */
+
+        case '*':      UND ("G2-designate 94");                        break;
+        case '+':      UND ("G3-designate 94");                        break;
+        case ',':      UND ("Not used");                               break;
+        case '-':      UND ("G1-designate 96");                        break;
+        case '.':      UND ("G2-designate 96");                        break;
+        case '/':      UND ("G3-designate 96");                        break;
+
+        case ' ':                              /* ESC SP # */
+          switch (st->buf[2]) {
+          case 'A':    UND ("G0 in GL, GR unused");                    break;
+          case 'B':    UND ("G0 and G1 to GL");                        break;
+          case 'C':    UND ("G0 in GL, G1 in GR, no lock");            break;
+          case 'D':    UND ("G0 in GL, G1 in GR 8-bit");               break;
+          case 'E':    UND ("Shift preserved");                        break;
+          case '"':    UND ("C1 controls esc");                        break;
+          case 'G':    UND ("C1 controls CR");                         break;
+          case 'H':    UND ("94-char graphical");                      break;
+          case 'I':    UND ("94-char and/or 96-char");                 break;
+          case 'J':    UND ("7-bit");                                  break;
+          case 'K':    UND ("8-bit");                                  break;
+          case 'L':    UND ("ISO/IEC 4873 1");                         break;
+          case 'M':    UND ("ISO/IEC 4873 2");                         break;
+          case 'N':    UND ("ISO/IEC 4873 3");                         break;
+          case 'P':    UND ("SI / LS0");                               break;
+          case 'R':    UND ("SO / LS1");                               break;
+          case 'S':    UND ("LS1R 8-bit");                             break;
+          case 'T':    UND ("LS2");                                    break;
+          case 'U':    UND ("LS2R 8-bit");                             break;
+          case 'V':    UND ("LS3");                                    break;
+          case 'W':    UND ("LS3R 8-bit");                             break;
+          case 'Z':    UND ("SS2");                                    break;
+          case '%':    UND ("Designate other");                        break;
+          case '[':    UND ("SS2");                                    break;
+          case '\\':   UND ("Single-shift GR");                        break;
+          default:     UND ("Unknown SP");                             break;
+          }
+          break;
+        default:       UND ("Unknown");                                break;
+        }
+        break;
+
+      case 4:                  /* ESC # # # */
+        kind = "nF4";
+        switch (st->buf[1]) {
+        case '$':              /* ESC $ # # */
+          switch (st->buf[2]) {
+          case '(':    UND ("G0 designate");                           break;
+          case ')':    UND ("G1-designate");                           break;
+          case '*':    UND ("G2-designate");                           break;
+          case '+':    UND ("G3-designate multi 94");                  break;
+          case ',':    UND ("Not used");                               break;
+          case '-':    UND ("G1-designate multi 96");                  break;
+          case '.':    UND ("G2-designate multi 96");                  break;
+          case '/':    UND ("G3-designate multi 96");                  break;
+          default:     UND ("Unknown $");                              break;
+          }
+        case '%':              /* ESC % # # */
+          switch (st->buf[2]) {
+          case '/':
+            switch (st->buf[3]) {
+            case 'I':  UND ("UTF-8");                                  break;
+            case 'L':  UND ("UTF-16");                                 break;
+            default:   UND ("Unknown %");                              break;
+            }
+            break;
+          default:     UND ("Unknown");                                break;
+          }
+          break;
+        default:       UND ("Unknown");                                break;
+        }
+        break;
+      default:         UND ("Unknown");                                break;
+      }
+    }
+
+
+  /********************************************************************* Fp */
+
+
+  else if (st->idx == 2 &&                           /* Fp escape sequence */
+           st->buf[0] == ESC &&
+           c >= 0x30 &&                                      /*    0-9:;<=>?       */
+           c <= 0x3F)
+    {
+      kind = "Fp";
+      done = True;
+
+      switch (c) {
+      case '7':                                LOG ("Save Cursor");
+        st->saved.flags = st->flags;
+        st->saved.lcf = st->lcf;
+        st->saved.x = tty->x;
+        st->saved.y = tty->y;
+        break;
+      case '8':                                LOG ("Restore Cursor");
+        st->flags = st->saved.flags;
+        st->lcf = st->saved.lcf;
+        tty->x = st->saved.x;
+        tty->y = st->saved.y;
+        break;
+      case '9':                                UND ("Forward Index");          break;
+      case '=':                             /* UND ("App Keypad");*/           break;
+      case '<':                                UND ("ANSI mode");              break;
+      case '>':                             /* UND ("Normal Keypad");*/        break;
+      default:                         UND ("Unknown");                break;
+      }
+    }
+  else if (st->idx >= 3 &&                     /* CSI: Sequence Introducer */
+           st->buf[0] == ESC &&                        /*      intermediate bytes  */
+           st->buf[1] == '[' &&                        /*      ESC [ ... C         */
+           ((c >= 0x30 && c <= 0x3F) ||                /* parameters    0–9:;<=>?  */
+            (c >= 0x20 && c <= 0x2F)))         /* others  !"#$%&'()*+,-./  */
+    {
+      kind = "CSIa";
+    }
+
+
+  /******************************************************************** CSI */
+
+
+  else if (st->idx >= 3 &&                     /* CSI: Sequence Introducer */
+           st->buf[0] == ESC &&                        /*      final byte          */
+           st->buf[1] == '[' &&                        /*      ESC [ ... C         */
+           c >= 0x40 &&                                /* cmd  @A–Z[\]^_`a–z{|}~   */
+           c <= 0x7E)
+    {
+      kind = "CSI";
+      done = True;
+
+      /* Parse the "80;24;365" args into 'av'. */
+      {
+        int any = False;
+        for (i = 0; i < countof(av); i++)
+          av[i] = UNDEF;
+        for (i = 2; i < st->idx - 1; i++)
+          {
+            if (st->buf[i] == ';' && ac < countof(av)-1)
+              {
+                if (ac >= countof(av) - 2)
+                  break;
+                ac++;
+                av[ac] = 0;
+                any = True;
+              }
+            else if (st->buf[i] >= '0' && st->buf[i] <= '9')
+              {
+                if (av[ac] == UNDEF)
+                  av[ac] = 0;
+                av[ac] = (av[ac] * 10) + (st->buf[i] - '0');
+                any = True;
+              }
+            else if (st->buf[i] >= 0x3C && st->buf[i] <= 0x3F)
+              ;  /* Characters "<=>?" indicate private commands. */
+            else if (st->buf[i] == '!')
+              ;  /* What's this? */
+            else
+              UND ("Weird parameters");
+          }
+        if (any) ac++;
+      }
+
+      switch (c) {
+      case '@':        st->lcf = False;                UND ("Shift left");     break;
+      case 'A':                                        LOG ("Cursor Up");
+        /* "CSI n SP A" means shift right n cols */
+        if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+        tty->y -= av[0];
+        st->lcf = False;
+        scrolled_p = True;
+        break;
+      case 'B':                                        LOG ("Cursor Down");
+        if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+        tty->y += av[0];
+        st->lcf = False;
+        scrolled_p = True;
+        break;
+      case 'C':                                        LOG ("Cursor Forward");
+        if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+        tty->x += av[0];
+        st->lcf = False;
+        scrolled_p = True;
+        break;
+      case 'D':                                        LOG ("Cursor Back");
+        if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+        tty->x -= av[0];
+        st->lcf = False;
+        scrolled_p = True;
+        break;
+      case 'E':                                        LOG ("Cursor Next Line");
+        if (av[0] == UNDEF) av[0] = 1;
+        tty->x = 0;
+        tty->y += av[0];
+        scrolled_p = True;
+        /* Set lcf to False here? */
+        break;
+      case 'F':                                        LOG ("Cursor Previous Line");
+        if (av[0] == UNDEF) av[0] = 1;
+        tty->x = 0;
+        tty->y -= av[0];
+        scrolled_p = True;
+        /* Set lcf to False here? */
+        break;
+      case 'G':                                        LOG ("Cursor Horizontal Abs");
+        if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+        tty->x = av[0]-1;
+        /* Set lcf to False here? */
+        break;
+      case 'H':                                        LOG ("Cursor Position");
+        if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+        if (av[1] == UNDEF || av[1] == 0) av[1] = 1;
+        tty->y = av[0]-1;
+        tty->x = av[1]-1;
+        if (st->origin_relative_p) tty->y += st->scroll.y1;
+        st->lcf = False;
+        break;
+
+      case 'I':                                        LOG ("Fwd Tab Stop");
+        {
+          int i;
+          if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+          /* Move forward N tab stops. */
+          /* Set lcf to False here? */
+          for (i = 0; i < av[0]; i++)
+            {
+              tty->x++;
+              while (tty->x < tty->width &&
+                     !st->tabs[tty->x])
+                tty->x++;
+            }
+        }
+       break;
+
+      case 'J':                                       /* Erase in Display */
+        st->lcf = False;
+        if (av[0] == UNDEF || av[0] == 0)
+          {                                    LOG ("Erase to end of screen");
+            tty_erase (tty,
+                       tty->x, tty->y,
+                       tty->width-1, tty->height-1);
+          }
+        else if (av[0] == 1)
+          {                                  LOG ("Erase to start of screen");
+            tty_erase (tty,
+                       0, 0,
+                       tty->x-1, tty->y);
+            }
+        else if (av[0] == 2 || av[0] == 3)
+          {                                    LOG ("Erase screen");
+            tty_erase (tty,
+                       0, 0,
+                       tty->width-1, tty->height-1);
+          }
+        /* CSI ? n J" are vt220 */
+        break;
+
+      case 'K':                                        /* Erase in Line */
+        st->lcf = False;
+        if (av[0] == UNDEF || av[0] == 0)
+          {                                    LOG ("Erase to end of line");
+            tty_erase (tty,
+                       tty->x, tty->y,
+                       tty->width-1, tty->y);
+          }
+        else if (av[0] == 1)
+            {                                  LOG ("Erase to start of line");
+              tty_erase (tty,
+                         0, tty->y,
+                         tty->x-1, tty->y);
+            }
+          else if (av[0] == 2 || av[0] == 3)
+            {                                  LOG ("Erase line");
+              tty_erase (tty,
+                         0, tty->y,
+                         tty->width-1, tty->y);
+            }
+        break;
+
+      case 'L':                                        UND ("Insert lines");   break;
+      case 'M':                                        UND ("Delete lines");   break;
+      case 'P':        st->lcf = False;                UND ("Delete chars");   break;
+      case 'Q':                                        UND ("Palette stack");  break;
+      case 'R':                                        UND ("Palette restore");break;
+
+      case 'S':                                        LOG ("Scroll Up");
+        if (av[0] == UNDEF) av[0] = 1;
+        tty_scroll (tty, av[0]);
+        scrolled_p = True;
+        /* Set lcf to False here? */
+        break;
+      case 'T':                                        LOG ("Scroll Down");
+        if (av[0] == UNDEF) av[0] = 1;
+        tty_scroll (tty, -av[0]);
+        scrolled_p = True;
+        /* Set lcf to False here? */
+        break;
+
+      case 'W':                                        LOG ("Reset tabs");
+        ansi_tty_default_tabs (tty);
+        break;
+
+      case 'X': st->lcf = False;               UND ("Erase chars");    break;
+      case 'Z':                                        LOG ("Back tab stop");
+        {
+          int i;
+          if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+          /* Move backward N tab stops. */
+          for (i = 0; i < av[0]; i++)
+            {
+              tty->x--;
+              while (tty->x >= 0 &&
+                     !st->tabs[tty->x])
+                tty->x--;
+            }
+        }
+        break;
+
+      /* Unused: [\]_ */
+
+      case '^':                                        UND ("Scroll down");    break;
+      case '`':                                        UND ("Position abs");   break;
+
+      case 'a':                                        UND ("Position rel");   break;
+      case 'b':                                        UND ("Repeat prev");    break;
+
+      case 'c':                                                         /* Reports */
+        switch (av[0]) {
+        case 0:                        LOG ("Report model");
+          if (tty->tty_send)
+            {
+              char buf[40];    /* Base model */
+              sprintf (buf, CSI "?%d;%dc", 1, 0);
+              (*tty->tty_send) (tty->closure, buf);
+            }
+          break;
+        default:               UND ("Unknown report request");         break;
+        break;
+        }
+        break;
+
+      case 'd':                                        UND ("Pos abs");        break;
+      case 'e':                                        UND ("Pos rel");        break;
+
+      case 'f':                                        LOG ("H/V Position");
+        if (av[0] == UNDEF || av[0] == 0) av[0] = 1;
+        if (av[1] == UNDEF || av[1] == 0) av[1] = 1;
+        tty->y = av[0]-1;
+        tty->x = av[1]-1;
+        st->lcf = False;
+        break;
+
+      case 'g':
+        if (av[0] == UNDEF) av[0] = 0;
+        switch (av[0]) {
+        case 0:                                        LOG ("Clear tab");
+          st->tabs[tty->x] = 0;
+          break;
+        case 1:                                        LOG ("Set tab");
+          st->tabs[tty->x] = 1;
+          break;
+        case 3:                                        LOG ("Clear all tabs");
+          memset (st->tabs, 0, tty->width);
+          break;
+        default:                               UND ("Unknown tab");    break;
+        }
+        break;
+
+      case 'h':                        /* On */
+      /* i,j,k are below */
+      case 'l':                        /* Off */
+        {
+          int on_p = (st->buf[st->idx-1] == 'h');
+          switch (av[0]) {
+          case 1:
+            if (on_p)
+              /* LOG ("Application cursor keys") */;
+            else
+              /* LOG ("Normal cursor keys") */;
+            break;
+          case 2:              UND ("USASCII sets G0-G3");             break;
+                               /* Or ANSI/VT52 Mode? */
+          case 3:              
+            st->lcf = False;
+            if (on_p)          UND ("132 Column Mode");
+            else               LOG ("80 Column Mode");
+            break;
+          case 4:              UND ("Smooth Scroll");                  break;
+          case 5:
+            if (on_p)
+              LOG ("Reverse Video on");
+            else
+              LOG ("Reverse Video off");
+            tty->inverse_p = on_p;
+            break;
+          case 6:
+            st->lcf = False;
+            if (on_p)
+              LOG ("Origin Mode on");
+            else
+              LOG ("Origin Mode off");
+            st->origin_relative_p = on_p;
+            tty->x = 0;
+            tty->y = on_p ? st->scroll.y1 : 0;
+            break;
+          case 7:
+            tty->state->auto_wrap_p = on_p;
+            if (on_p)
+              LOG ("Auto-wrap on");
+            else
+              LOG ("Auto-wrap off");
+            if (!on_p)
+              st->lcf = False;
+            break;
+          case 8:              UND ("Auto-Repeat Keys");               break;
+          case 9:              UND ("Send Mouse");                     break;
+                               /* Or Interlace? */
+          case 10:             UND ("Show toolbar");                   break;
+          case 12:             UND ("Start blinking");                 break;
+          case 13:             UND ("Start blinking");                 break;
+          case 14:             UND ("Enable XOR");                     break;
+          case 18:             UND ("Print Form Feed");                break;
+          case 19:             UND ("Print full screen");              break;
+
+          case 20:
+            if (on_p)          LOG ("Linefeed mode");
+            else               LOG ("Newline mode");
+            st->linefeed_p = on_p;
+            break;
+
+          case 25:             UND ("Show cursor");                    break;
+          case 30:             UND ("Show scrollbar");                 break;
+          case 35:             UND ("Enable font-shifting");           break;
+          case 38:             UND ("Enter Tektronix");                break;
+          case 40:             UND ("Allow 80/132");                   break;
+          case 41:             UND ("more");                           break;
+          case 42:             UND ("National replacement");           break;
+          case 43:             UND ("Graphic Expanded Print");         break;
+          case 44:             UND ("Graphic Print Color");            break;
+          case 45:             UND ("Graphic Print Color Syntax");     break;
+          case 46:             UND ("Graphic Print Background");       break;
+          case 47:             UND ("Graphic Rotated Print");          break;
+          case 66:          /* UND ("Application keypad"); */          break;
+          case 67:             UND ("Backarrow backspace");            break;
+          case 69:             UND ("Margin mode");                    break;
+          case 80:             UND ("Sixel Mode");                     break;
+          case 95:             UND ("Do not clear");                   break;
+          case 1000:           UND ("Send Mouse");                     break;
+          case 1001:           UND ("Hilite Mouse Tracking");          break;
+          case 1002:           UND ("Cell Motion Mouse");              break;
+          case 1003:           UND ("All Motion Mouse");               break;
+          case 1004:           UND ("Focus Events");                   break;
+          case 1005:           UND ("UTF-8 Mouse");                    break;
+          case 1006:           UND ("SGR Mouse");                      break;
+          case 1007:           UND ("Alternate Scroll");               break;
+          case 1010:           UND ("Scroll on output");               break;
+          case 1011:           UND ("Scroll on press");                break;
+          case 1014:           UND ("Fast scroll");                    break;
+          case 1015:           UND ("URXVT Mouse");                    break;
+          case 1016:           UND ("SGR Mouse");                      break;
+          case 1024:           LOG ("Enable/disable focus report");    break;
+          case 1034:           UND ("Meta key");                       break;
+          case 1035:           UND ("Alt modifiers");                  break;
+          case 1036:           UND ("ESC Meta");                       break;
+          case 1037:           UND ("DEL Delete");                     break;
+          case 1039:           UND ("ESC Alt");                        break;
+          case 1040:           UND ("selection when not highlighted"); break;
+          case 1041:           UND ("Use CLIPBOARD");                  break;
+          case 1042:           UND ("Enable Urgency");                 break;
+          case 1043:           UND ("Enable raising");                 break;
+          case 1044:           UND ("Reuse clipboard");                break;
+          case 1045:           UND ("Extended reverse-wraparound");    break;
+          case 1046:           UND ("Enable Alternate Screen");        break;
+          case 1047:           UND ("Alternate Screen Buffer");        break;
+          case 1048:           UND ("Save cursor");                    break;
+          case 1049:           UND ("Save cursor");                    break;
+          case 1050:           UND ("Terminfo Fn keys");               break;
+          case 1051:           UND ("Set Sun Fn keys");                break;
+          case 1052:           UND ("HP Fn keys");                     break;
+          case 1053:           UND ("SCO Fn keys");                    break;
+          case 1060:           UND ("Legacy keyboard emulation");      break;
+          case 1061:           UND ("VT220 keyboard emulation");       break;
+          case 2001:           UND ("Readline mouse 1");               break;
+          case 2002:           UND ("Readline mouse 2");               break;
+          case 2003:           UND ("Readline mouse 2");               break;
+          case 2004:           UND ("Bracketed paste");                break;
+          case 2005:           UND ("readline quoting");               break;
+          case 2006:           UND ("Readline newline paste");         break;
+          default:             UND ("Unknown GR H/L");                 break;
+          }
+        }
+        break;
+
+      case 'i':                                                           /* Ports */
+        if (av[0] == UNDEF) av[0] = 0;
+        switch (av[0]) {
+        case 0:                        UND ("Print screen");                   break;
+        case 1:                        UND ("Print cursor line");              break;
+        case 4:                        UND ("Aux Port Off");                   break;
+        case 5:                        UND ("Aux Port On");                    break;
+        case 10:               UND ("Print dpy");                      break;
+        case 11:               UND ("Print all");                      break;
+        default:               UND ("Unknown port request");           break;
+        }
+        break;
+
+      /* Unused: 'j', 'k'; 'l' is above. */
+
+      case 'm':                                        /* Select Graphic Rendition */
+        if (ac == 0)
+          av[ac++] = 0;  /* No args means "CSI 0m" */
+        for (i = 0; i < ac; i++) {
+          switch (av[i]) {
+          case UNDEF:
+          case 0:                              LOG ("Reset text props");
+            st->flags = 0;
+            break;
+          case 1:                              LOG ("Bold");
+            st->flags |= TTY_BOLD;
+            break;
+          case 2:                              LOG ("Dim");
+            st->flags |= TTY_DIM;
+            break;
+          case 3:                              LOG ("Italic");
+            st->flags |= TTY_ITALIC;
+            break;
+          case 4:                              LOG ("Underline");
+            st->flags |= TTY_UNDERLINE;
+            break;
+          case 5:                              LOG ("Blink");
+            st->flags |= TTY_BLINK;
+            break;
+          case 6:                              LOG ("Blink fast");
+            st->flags |= TTY_BLINK;
+            break;
+          case 7:                              LOG ("Invert");
+            st->flags |= TTY_INVERSE;
+            break;
+          case 8:                              UND ("Hide");           break;
+          case 9:                              UND ("Strike");         break;
+
+          case 10:                             LOG ("Primary font");
+            st->flags &= ~TTY_SYMBOLS;
+            break;
+          case 11:                             LOG ("Alternate font 1");
+            st->flags |= TTY_SYMBOLS;
+            break;
+          case 12:             UND ("Alternate font 2");               break;
+          case 13:             UND ("Alternate font 3");               break;
+          case 14:             UND ("Alternate font 4");               break;
+          case 15:             UND ("Alternate font 5");               break;
+          case 16:             UND ("Alternate font 6");               break;
+          case 17:             UND ("Alternate font 7");               break;
+          case 18:             UND ("Alternate font 8");               break;
+          case 19:             UND ("Alternate font 9");               break;
+          case 20:             UND ("Fraktur font");                   break;
+          case 21:             LOG ("Doubly underlined");
+            st->flags |= TTY_UNDERLINE;
+            break;
+          case 22:             LOG ("Not dim");
+            st->flags &= ~TTY_DIM;
+            break;
+          case 23:             LOG ("Not italic");
+            st->flags &= ~(TTY_ITALIC|TTY_BOLD);
+            break;
+          case 24:             LOG ("Not underlined");
+            st->flags &= ~TTY_UNDERLINE;
+            break;
+          case 25:             LOG ("No blink");
+            st->flags &= ~TTY_BLINK;
+            break;
+          case 26:             LOG ("Proportional spacing");           break;
+          case 27:             LOG ("Not reversed");
+            st->flags &= ~TTY_INVERSE;
+            break;
+          case 28:             LOG ("Reveal");                         break;
+          case 29:             LOG ("Not strike");                     break;
+
+          case 30:             LOG ("Set foreground color 1");
+            set_color (tty, True, 0);                                  break;
+          case 31:             LOG ("Set foreground color 2");
+            set_color (tty, True, 1);                                  break;
+          case 32:             LOG ("Set foreground color 3");
+            set_color (tty, True, 2);                                  break;
+          case 33:             LOG ("Set foreground color 4");
+            set_color (tty, True, 3);                                  break;
+          case 34:             LOG ("Set foreground color 5");
+            set_color (tty, True, 4);                                  break;
+          case 35:             LOG ("Set foreground color 6");
+            set_color (tty, True, 5);                                  break;
+          case 36:             LOG ("Set foreground color 7");
+            set_color (tty, True, 6);                                  break;
+          case 37:             LOG ("Set foreground color 8");
+            set_color (tty, True, 7);                                  break;
+
+          case 38:             LOG ("Set foreground color N");
+            if (av[i] == 5)
+              set_color (tty, True, av[i+1]);  /* 5;N */
+            else if (av[i] == 2)
+              {
+                st->fg.r = av[i+1];            /* 2;R;G;B */
+                st->fg.g = av[i+2];
+                st->fg.b = av[i+3];
+              }
+            i = 999;  /* stop processing */
+            break;
+
+          case 39:             LOG ("Default foreground color");
+            set_color (tty, True, 0);                                  break;
+
+          case 40:             LOG ("Set background color 1");
+            set_color (tty, False, 0);                                         break;
+          case 41:             LOG ("Set background color 2");
+            set_color (tty, False, 1);                                         break;
+          case 42:             LOG ("Set background color 3");
+            set_color (tty, False, 2);                                         break;
+          case 43:             LOG ("Set background color 4");
+            set_color (tty, False, 3);                                         break;
+          case 44:             LOG ("Set background color 5");
+            set_color (tty, False, 4);                                         break;
+          case 45:             LOG ("Set background color 6");
+            set_color (tty, False, 5);                                         break;
+          case 46:             LOG ("Set background color 7");
+            set_color (tty, False, 6);                                         break;
+          case 47:             LOG ("Set background color 8");
+            set_color (tty, False, 7);                                         break;
+
+          case 48:             LOG ("Set background color N");
+            if (av[i] == 5)
+              set_color (tty, False, av[i+1]); /* 5;N */
+            else if (av[i] == 2)
+              {
+                st->bg.r = av[i+1];            /* 2;R;G;B */
+                st->bg.g = av[i+2];
+                st->bg.b = av[i+3];
+              }
+            i = 999;  /* stop processing */
+            break;
+
+          case 49:             LOG ("Default background color");
+            set_color (tty, False, 0);                                         break;
+
+          case 50:             UND ("Fixed width");                    break;
+          case 51:             UND ("Framed");                         break;
+          case 52:             UND ("Encircled");                      break;
+          case 53:             UND ("Overlined");                      break;
+          case 54:             UND ("Not framed");                     break;
+          case 55:             UND ("Not overlined");                  break;
+          case 58:             UND ("Set underline color");            break;
+                               /*  5;n or 2;r;g;b */
+          case 59:             UND ("Default underline color");        break;
+          case 60:             UND ("Ideogram underline");             break;
+          case 61:             UND ("Ideogram double underline");      break;
+          case 62:             UND ("Ideogram overline");              break;
+          case 63:             UND ("Ideogram double overline");       break;
+          case 64:             UND ("Ideogram stress marking");        break;
+          case 65:             UND ("No ideogram");                    break;
+          case 73:             UND ("Superscript");                    break;
+          case 74:             UND ("Subscript");                      break;
+          case 75:             UND ("Not super/sub");                  break;
+          default:             UND ("Unknown");                        break;
+          break;
+          }
+        }
+        break;
+
+      case 'n':                                                         /* Reports */
+        switch (av[0]) {
+        case 5:                        LOG ("Report status");
+          if (tty->tty_send)
+            {                  /* Terminal ok */
+              const char *s = CSI "0n";
+              (*tty->tty_send) (tty->closure, s);
+            }
+          break;
+        case 6:                        LOG ("Report cursor position");
+          if (tty->tty_send)
+            {
+              char buf[40];
+              sprintf (buf, CSI "%d;%dR", tty->y+1, tty->x+1);
+              (*tty->tty_send) (tty->closure, buf);
+            }
+          break;
+        default:               UND ("Unknown report request");         break;
+        break;
+        }
+        break;
+
+      /* No 'o' */
+
+      case 'p':                        UND ("Pointer mode");                   break;
+      case 'q':                        UND ("LEDs");                           break;
+
+      case 'r':                        LOG ("Scroll Region");
+        if (av[0] == UNDEF) av[0] = 1;
+        if (av[1] == UNDEF) av[1] = tty->height;
+        /* Top and bottom lines, 1-based, inclusive.
+           So "3;3" means a single line at y=2. */
+        st->scroll.y1 = av[0] - 1;
+        st->scroll.y2 = av[1];
+        tty->x = 0;
+        tty->y = st->scroll.y1;
+        st->lcf = False;
+        break;
+
+      case 's':                        LOG ("Save Current Cursor Position");
+        st->saved.flags = st->flags;
+        st->saved.lcf = st->lcf;
+        st->saved.x = tty->x;
+        st->saved.y = tty->y;
+        break;
+
+      case 't':                        UND ("Window manip");                   break;
+
+      case 'u':                        LOG ("Restore Saved Cursor Position");
+        st->flags = st->saved.flags;
+        st->lcf = st->saved.lcf;
+        tty->x = st->saved.x;
+        tty->y = st->saved.y;
+        /* Set lcf to False here? */
+        break;
+
+      case 'v':                        UND ("Display extent");                 break;
+      case 'w':                        UND ("State report");                   break;
+      case 'x':                        UND ("Param report");                   break;
+
+      case 'y':                                                           /* Tests */
+        switch (av[0]) {
+        case 2:
+          if (av[1] == UNDEF) av[1] = 0;
+          switch (av[1]) {
+            case 0:            LOG ("Reset");
+              ansi_tty_reset (tty);
+              break;
+            case 1:            LOG ("Self-test");
+              if (tty->tty_send)
+                {
+                  char buf[40];
+                  sprintf (buf, CSI "%dn", 0);  /* Ready */
+                  (*tty->tty_send) (tty->closure, buf);
+                }
+              break;
+            case 2:            UND ("Data loopback test");             break;
+            case 4:            UND ("EIA modem test");                 break;
+            case 8:            UND ("Repeat tests forever");           break;
+          default:             UND ("Unknown test");                   break;
+          }
+          break;
+        default:               UND ("Unknown y");                      break;
+        break;
+        }
+        break;
+
+      case 'z':                        UND ("Erase rect");                     break;
+      case '{':                        UND ("Selective erase");                break;
+      case '|':                        UND ("Cols per page");                  break;
+      case '}':                        UND ("Insert cols");                    break;
+      case '~':                        UND ("Delete cols");                    break;
+      default:                 UND ("Unknown");                        break;
+      }
+    }
+
+
+  /********************************************************************* Fe */
+
+
+  else if (st->idx == 2 &&                           /* Fe escape sequence */
+           st->buf[0] == ESC &&
+           c >= 0x40 &&                                      /*    @A-Z[\]^_       */
+           c <= 0x5F)
+    {
+      kind = "Fe";
+      done = True;
+
+      switch (c) {                             /* These commands await ST  */
+      case 'P':                                        /*   Device Control String  */
+      case ']':                                        /*   System Command         */
+      case 'X':                                        /*   Start of String        */
+      case '^':                                        /*   Privacy Message        */
+      case '_':                                        /*   Application Program    */
+        st->awaiting_st = True;
+        break;
+      case '\\':                               /* ST: String Terminator    */
+        st->awaiting_st = False;
+        break;
+      case 'N': UND ("Single Shift Two"); break;
+      case 'O': UND ("Single Shift Three"); break;
+
+      case '[':                                /* CSI: Control Sequence Introducer */
+        done = False;
+        break;
+
+      case 'A':                                        LOG ("VT52 Cursor up");
+        tty->y--;
+        break;
+      case 'B':                                        LOG ("VT52 Cursor down");
+        tty->y++;
+        scrolled_p = True;
+        /* Set lcf to False here? */
+        break;
+      case 'C':                                        LOG ("VT52 Cursor right");
+        tty->x++;
+        scrolled_p = True;
+        /* Set lcf to False here? */
+        break;
+      case 'D':                                        LOG ("Cursor down");
+        goto DO_LF;                            /* VT52 Cursor left! */
+        break;
+      case 'E':                                        LOG ("Next line");
+        tty->x = 0;
+        goto DO_LF;
+        break;
+      case 'F':                                        LOG ("VT52 Graphics mode");
+        st->flags |= TTY_SYMBOLS;
+        break;
+      case 'G':                                        LOG ("VT52 Exit graphics");
+        st->flags &= ~TTY_SYMBOLS;
+        break;
+      case 'H':                                        LOG ("Set tab");
+        st->tabs[tty->x] = 1;
+       break;
+
+      case 'I':  /* VT52 */
+      case 'M':
+        st->lcf = False;
+        tty->y--;
+        if (tty->y < st->scroll.y1)
+          {
+            LOG ("Rev LF scroll");
+            tty->y = st->scroll.y1;
+            tty_scroll (tty, -1);
+            scrolled_p = True;
+          }
+        else
+          LOG ("Rev LF");
+        break;
+
+      case 'J':                                        LOG ("VT52 Erase to EOS");
+        tty_erase (tty,
+                   tty->x, tty->x,
+                   tty->width-1, tty->height-1);
+        break;
+      case 'K':                                        LOG ("VT52 Erase to EOL");
+        tty_erase (tty,
+                   tty->x, tty->y,
+                   tty->width-1, tty->y);
+        break;
+
+      case 'Y':                                        UND ("VT52 Direct cursor");
+        /* 4-byte sequence: "ESC Y y x" where x and y are \037 + N.
+           But the rest of the Fe commands are 2 bytes. */
+        break;
+
+      case 'Z':                                        LOG ("VT52 Identify");
+        if (tty->tty_send)
+          {
+            char buf[40];
+            sprintf (buf, "%c%c%c", ESC, '/', 'Z');
+            (*tty->tty_send) (tty->closure, buf);
+          }
+        break;
+
+      default:                                 UND ("Unknown");        break;
+      }
+    }
+
+
+  /********************************************************************* Fs */
+
+
+  else if (st->idx == 2 &&                           /* Fs escape sequence */
+           st->buf[0] == ESC &&
+           c >= 0x60 &&                                      /*    `a-z{|}~        */
+           c <= 0x7E)
+    {
+      kind = "Fs";
+      done = True;
+
+      switch (c) {
+      case '`':                                UND ("Disable manual input");   break;
+      case 'a':                                UND ("Interrupt");              break;
+      case 'b':                                UND ("Enable manual input");    break;
+      case 'c':                                LOG ("Full Reset");
+        ansi_tty_reset (tty);
+        break;
+      case 'd':                                UND ("Coding method delimiter");break;
+      case 'l':                                UND ("Memory Lock");            break;
+      case 'm':                                UND ("Memory Unlock");          break;
+      case 'n':                                UND ("G2 as GL");               break;
+      case 'o':                                UND ("G3 as GL");               break;
+      case '|':                                UND ("G3 as GR");               break;
+      case '~':                                UND ("G1 as GR");               break;
+      case '}':                                UND ("G2 as GR");               break;
+      default:                         UND ("Unknown");                break;
+      }
+    }
+
+  else if (st->idx > 0 &&
+           st->buf[0] == ESC)
+    {
+      /* An ESC followed by a character that matched none of the above.
+         We assume it is an unknown 2-byte sequence, which may not be true.
+       */
+      kind = "ESC";
+      done = True;
+      UND ("Unrecognized");
+    }
+
+
+  /******************************************************************* UTF8 */
+
+  /* Assemble UTF-8 multi-byte sequences into a 32 bit Unicode character.   */
+
+
+  else if (st->unicrud > 0)            /* Expect N more bytes of Unicode   */
+    {
+      kind = "UC";
+      st->unicrud--;
+      if (st->unicrud == 0)
+        {
+          /* Finished: replace c with the unichar. */
+          c = 0;
+          utf8_decode ((unsigned char *) st->buf, st->idx, &c);
+          done = True;
+          goto SELF_INSERT;
+        }
+    }
+  else if ((c & 0xE0) == 0xC0)         /* 110xxxxx: 11 bits, 2 bytes      */
+    {
+      kind = "UC2";
+      st->unicrud = 1;
+    }
+  else if ((c & 0xF0) == 0xE0)         /* 1110xxxx: 16 bits, 3 bytes      */
+    {
+      kind = "UC3";
+      st->unicrud = 2;
+    }
+  else if ((c & 0xF8) == 0xF0)         /* 11110xxx: 21 bits, 4 bytes      */
+    {
+      kind = "UC4";
+      st->unicrud = 3;
+    }
+  else if ((c & 0xFC) == 0xF8)         /* 111110xx: 26 bits, 5 bytes      */
+    {
+      kind = "UC5";
+      st->unicrud = 4;
+    }
+  else if ((c & 0xFE) == 0xFC)         /* 1111110x: 31 bits, 6 bytes      */
+    {
+      kind = "UC6";
+      st->unicrud = 5;
+    }
+
+
+  /******************************************************************** Co */
+
+
+  else                                 /* Co control codes: single chars. */
+    {
+    SELF_INSERT:
+      kind = "Co";
+      done = True;
+
+    SELF_INSERT_INTERLUDE:
+
+      switch (c) {
+      case 0:                                                  /* NOOP    */
+        break;
+
+      case 0x07:                                               /* BEL     */
+        tty_log_c (tty, c);
+        break;
+
+      case 0x08:                                               /* BS      */
+        tty_log_c (tty, c);
+        if (tty->x > 0)
+          tty->x--;
+        st->lcf = False;
+        break;
+
+      case 0x09:                                               /* HT: TAB */
+        {
+          /* Tabs are motion and do not alter what's under them. */
+          tty_log_c (tty, c);
+          tty->x++;
+          while (tty->x < tty->width && !st->tabs[tty->x])
+            tty->x++;
+          st->lcf = False;
+        }
+        break;
+
+      case 0x0A:                                               /* LF      */
+      case 0x0B:                                               /* VT      */
+      case 0x0C:                                               /* FF      */
+        tty_log_c (tty, c);
+      DO_LF:
+        if (st->linefeed_p)
+          tty->x = 0;
+        tty->y++;
+        st->lcf = False;
+        scrolled_p = True;
+        if (tty->y >= st->scroll.y2)
+          {
+            int n = tty->y - st->scroll.y2 + 1;
+            ac = 0;
+            av[ac++] = n;
+            LOG ("Scroll");
+            tty_scroll (tty, n);
+            tty->y = st->scroll.y2 - 1;
+          }
+        break;
+
+      case 0x1A:                                               /* SUB     */
+        tty_log_c (tty, c);
+        st->lcf = False;
+        break;
+
+      case 0x0D:                                               /* CR      */
+        tty_log_c (tty, c);
+        tty->x = 0;
+        st->lcf = False;
+        break;
+
+      case 0x0E:                                               /* SO      */
+        tty_log_c (tty, 0);
+        LOG ("SO font G1");
+        if (st->g1 & TTY_SYMBOLS)
+          st->flags |= TTY_SYMBOLS;
+        else
+          st->flags &= ~TTY_SYMBOLS;
+        break;
+
+      case 0x0F:                                               /* SI      */
+        tty_log_c (tty, 0);
+        LOG ("SI font G0");
+        if (st->g0 & TTY_SYMBOLS)
+          st->flags |= TTY_SYMBOLS;
+        else
+          st->flags &= ~TTY_SYMBOLS;
+        break;
+
+      default:                                                 /* Insert  */
+        tty_log_c (tty, c);
+        if (c >= ' ')
+          {
+            tty_char *ch;
+            if (tty->x >= tty->width - 1)
+              {
+                tty->x = tty->width - 1;
+                if (st->lcf == False)
+                  st->lcf = True;
+                else
+                  {
+                    st->lcf = False;
+                    if (tty->state->auto_wrap_p)
+                      {
+                        tty->x = 0;
+                        tty->y++;
+                        if (tty->y >= st->scroll.y2)
+                          {
+                            int n = tty->y - st->scroll.y2 + 1;
+                            ac = 0;
+                            av[ac++] = n;
+                            LOG ("Scroll wrap");
+                            tty_scroll (tty, n);
+                            tty->y = st->scroll.y2 - 1;
+                            scrolled_p = True;
+                          }
+                        else if (tty->debug_p > 2)
+                          LOG ("Wrap");
+                      }
+                  }
+              }
+            
+            if (tty->x >= tty->width) abort();
+            if (tty->y >= tty->height) abort();
+
+            ch = &tty->grid[tty->y * tty->width + tty->x];
+            ch->c     = c;
+            ch->flags = st->flags;
+            ch->fg    = st->fg;
+            ch->bg    = st->bg;
+            tty->x++;
+          }
+        break;
+      }
+    }
+
+  if (tty->x < 0)           tty->x = 0;
+  if (tty->x >= tty->width) tty->x = tty->width - 1;
+
+  /* Scrolling commands, or insertions that changed Y, clip the cursor to
+     the scrolling region. But cursor-positioning commands do not. */
+  if (scrolled_p)
+    {
+      if (tty->y <  st->scroll.y1) tty->y = st->scroll.y1;
+      if (tty->y >= st->scroll.y2) tty->y = st->scroll.y2 - 1;
+    }
+  else
+    {
+      if (tty->y <  0) tty->y = 0;
+      if (tty->y >= tty->height) tty->y = tty->height - 1;
+    }
+
+  if (done)
+    {
+      st->idx    = 0;
+      st->buf[0] = 0;
+    }
+}
+
diff --git a/hacks/ansi-tty.h b/hacks/ansi-tty.h
new file mode 100644 (file)
index 0000000..ff836e1
--- /dev/null
@@ -0,0 +1,63 @@
+/* xscreensaver, Copyright © 2025 Jamie Zawinski <jwz@jwz.org>
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or 
+ * implied warranty.
+ */
+
+#ifndef __XSCREENSAVER_ANSI_TTY_H__
+#define __XSCREENSAVER_ANSI_TTY_H__
+
+typedef enum {
+  TTY_BOLD      = 1,
+  TTY_ITALIC    = 2,
+  TTY_INVERSE   = 4,
+  TTY_DIM       = 8,
+  TTY_UNDERLINE = 16,
+  TTY_BLINK     = 32,
+  TTY_SYMBOLS   = 64,
+} tty_flag;
+
+typedef struct { unsigned char r, g, b; } tty_color;
+
+typedef struct {
+  unsigned long c;                     /* Unicode */
+  tty_flag flags;                      /* Bitmask */
+  tty_color fg, bg;
+} tty_char;
+
+typedef void (*tty_send_fn) (void *closure, const char *text);
+
+typedef struct tty_state tty_state;
+typedef struct {                       /* Do not change these: */
+
+  int x, y;                            /*   Cursor position */
+  int width, height;                   /*   Screen size */
+  int inverse_p;                       /*   Flip the sense of TTY_INVERSE */
+  tty_char *grid;                      /*   Characters */
+  tty_state *state;                    /*   Internal state */
+
+                                       /* You can change these: */
+
+  tty_send_fn tty_send;                        /*   Type characters to upstream */
+  void *closure;
+  tty_color cmap[16][2];               /*   Default indexed colors */
+
+  int debug_p;                         /*   1: log errors to stderr
+                                            2: and all commands
+                                            3: and all text
+                                            4: log to /tmp */
+} ansi_tty;
+
+extern ansi_tty *ansi_tty_init (int w, int h);
+extern void ansi_tty_resize (ansi_tty *, int w, int h);
+extern void ansi_tty_print (ansi_tty *, unsigned long c);
+extern void ansi_tty_free (ansi_tty *);
+
+extern const unsigned long ansi_graphics_unicode[256];
+
+#endif /* __XSCREENSAVER_ANSI_TTY_H__ */
index a87ea1a18c91c114be5b7071a59bd717e81010e4..ae517a236eabd55439698ef63a3aa32a550bf8d9 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 1998-2021 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 1998-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -16,6 +16,7 @@
 #include "screenhack.h"
 #include "apple2.h"
 #include "textclient.h"
+#include "ansi-tty.h"
 #include "utf8wc.h"
 
 #include <math.h>
@@ -778,19 +779,24 @@ static void slideshow_controller(apple2_sim_t *sim, int *stepno,
 
 #define NPAR 16
 
+#define COUNTDOWN_BANNER   "intermission    "
+#define COUNTDOWN_DURATION 18*60+30
+#undef COUNTDOWN_BANNER
+
 struct terminal_controller_data {
   Display *dpy;
   char curword[256];
   unsigned char lastc;
   double last_emit_time;
+  time_t launch_time;
   text_data *tc;
+  ansi_tty *tty;
 
   int escstate;
   int csiparam[NPAR];
   int curparam;
   int cursor_x, cursor_y;
   int saved_x,  saved_y;
-  int unicruds; char unicrud[7];
   union {
     struct {
       unsigned int bold : 1;
@@ -803,6 +809,8 @@ struct terminal_controller_data {
 
 };
 
+static void a2_tty_send (void *, const char *);
+
 
 /* The structure of closure linkage throughout this code is so amazingly
    baroque that I can't get to the 'struct state' from where I need it. */
@@ -817,6 +825,9 @@ terminal_closegen(struct terminal_controller_data *mine)
     textclient_close (mine->tc);
     mine->tc = 0;
   }
+
+  if (mine->tty)
+    ansi_tty_free (mine->tty);
 }
 
 static int
@@ -845,11 +856,55 @@ terminal_keypress_handler (Display *dpy, XEvent *event, void *data)
     (struct terminal_controller_data *) data;
   mine->dpy = dpy;
   if (event->xany.type == KeyPress && mine->tc)
-    return textclient_putc (mine->tc, &event->xkey);
+    return textclient_putc_event (mine->tc, &event->xkey);
   return 0;
 }
 
 
+#ifdef COUNTDOWN_BANNER
+static void
+a2_draw_banner (apple2_state_t *st, struct terminal_controller_data *mine)
+{
+  int x, y;
+  int w = 40;
+  int h = 3;
+  char buf[41], *s;
+  struct timeval tv;
+  double t;
+
+  for (y = 0; y < h; y++)
+    for (x = 0; x < w; x++)
+      st->textlines[y][x] = ' ' | 0xC0;
+
+# ifdef GETTIMEOFDAY_TWO_ARGS
+  gettimeofday (&tv, NULL);
+# else
+  gettimeofday (&tv);
+# endif
+
+  t = ((mine->launch_time + COUNTDOWN_DURATION)  - 
+       (tv.tv_sec + ((double) tv.tv_usec * 0.000001)));
+  if (t < 0) t = 0;
+  sprintf (buf, "%.30s %d:%02d:%02d.%1d",
+           COUNTDOWN_BANNER,
+           (int) (t/60/60),
+           (int) (t/60) % 60,
+           (int) t % 60,
+           (int) (t * 10) % 10);
+  x = (w - strlen (buf)) / 2;
+  y = 0;
+  s = buf;
+  while (*s && x < w) {
+    char c = *s++;
+    if (c >= 'a' && c <= 'z') c &= 0xDF;
+    c |= 0xC0;
+    st->textlines[y][x] = c;
+    x++;
+  }
+}
+#endif /* COUNTDOWN_BANNER */
+
+
 static void
 a2_ascii_printc (apple2_state_t *st, unsigned char c,
                  Bool bold_p, Bool blink_p, Bool rev_p,
@@ -867,7 +922,9 @@ a2_ascii_printc (apple2_state_t *st, unsigned char c,
     }
   else if (c >= 'A' && c <= 'Z')            /* invert upper-case chars */
     {
+# ifndef COUNTDOWN_BANNER
       c |= 0x80;
+# endif
     }
 
   if (bold_p)  c |= 0xc0;
@@ -886,434 +943,81 @@ a2_vt100_printc (apple2_sim_t *sim, struct terminal_controller_data *state,
                  unsigned char c)
 {
   apple2_state_t *st=sim->st;
-  int cols = SCREEN_COLS;
-  int rows = SCREEN_ROWS;
-
-  int i;
-  int start, end;
+  int w = SCREEN_COLS;
+  int h = SCREEN_ROWS;
+  int x, y;
+  ansi_tty *tty = state->tty;
+  ansi_tty_print (tty, c);
 
-  /* Mostly duplicated in phosphor.c */
+  /* We could optimize this to not re-draw characters that haven't changed,
+     but it seems plenty fast enough. */
 
-  switch (state->escstate)
-    {
-    case 0:
-      switch (c)
-        {
-        case 7: /* BEL */
-          /* Dummy case - we don't want the screensaver to beep */
-          /* #### But maybe this should flash the screen? */
-          break;
-        case 8: /* BS */
-          if (state->cursor_x > 0)
-            state->cursor_x--;
-          break;
-        case 9: /* HT */
-          if (state->cursor_x < cols - 8)
-            {
-              state->cursor_x = (state->cursor_x & ~7) + 8;
-            }
-          else
-            {
-              state->cursor_x = 0;
-              if (state->cursor_y < rows - 1)
-                state->cursor_y++;
-              else
-                a2_scroll (st);
-            }
-          break;
-        case 10: /* LF */
-# ifndef HAVE_FORKPTY
-          state->cursor_x = 0; /* No ptys on iPhone; assume CRLF. */
-# endif
-        case 11: /* VT */
-        case 12: /* FF */
-          if (state->cursor_y < rows - 1)
-            state->cursor_y++;
-          else
-            a2_scroll (st);
-          break;
-        case 13: /* CR */
-          state->cursor_x = 0;
-          break;
-        case 14: /* SO */
-        case 15: /* SI */
-          /* Dummy case - there is one and only one font. */
-          break;
-        case 24: /* CAN */
-        case 26: /* SUB */
-          /* Dummy case - these interrupt escape sequences, so
-             they don't do anything in this state */
-          break;
-        case 27: /* ESC */
-          state->escstate = 1;
-          break;
-        case 127: /* DEL */
-          /* Dummy case - this is supposed to be ignored */
-          break;
-        case 155: /* CSI */
-          state->escstate = 2;
-          for(i = 0; i < NPAR; i++)
-            state->csiparam[i] = 0;
-          state->curparam = 0;
-          break;
-        default:
-
-          /* states 102-106 are for UTF-8 decoding */
-
-          if ((c & 0xE0) == 0xC0) {        /* 110xxxxx - 11 bits, 2 bytes */
-            state->unicruds = 1;
-            state->unicrud[0] = c;
-            state->escstate = 102;
-            break;
-          } else if ((c & 0xF0) == 0xE0) { /* 1110xxxx - 16 bits, 3 bytes */
-            state->unicruds = 1;
-            state->unicrud[0] = c;
-            state->escstate = 103;
-            break;
-          } else if ((c & 0xF8) == 0xF0) { /* 11110xxx - 21 bits, 4 bytes */
-            state->unicruds = 1;
-            state->unicrud[0] = c;
-            state->escstate = 104;
-            break;
-          } else if ((c & 0xFC) == 0xF8) { /* 111110xx - 26 bits, 5 bytes */
-            state->unicruds = 1;
-            state->unicrud[0] = c;
-            state->escstate = 105;
-            break;
-          } else if ((c & 0xFE) == 0xFC) { /* 1111110x - 31 bits, 6 bytes */
-            state->unicruds = 1;
-            state->unicrud[0] = c;
-            state->escstate = 106;
-            break;
+  for (y = 0; y < h; y++)
+    for (x = 0; x < w; x++)
+      {
+        tty_char *tc = &tty->grid [tty->width * y + x];
+        unsigned char ascii = 0;
+        tty_flag flag = tc->flags;
+        int inv_p = flag & (TTY_INVERSE | TTY_BOLD);
+        if (tty->inverse_p) inv_p = !inv_p;
+
+        if (tc->c <= 128)
+          ascii = tc->c;
+        else
+          {
+            /* Convert non-ASCII Unicode to closest ASCII. */
+            char utf8[10];
+            int L = utf8_encode (tc->c, utf8, sizeof(utf8)-1);
+            utf8[L] = 0;
+            ascii = '?';
+            if (L)
+              {
+                char *s = utf8_to_latin1 (utf8, True);
+                if (s)
+                  {
+                    ascii = s[0];
+                    free (s);
+                  }
+              }
           }
 
-        PRINT:
-
-          /* If the cursor is in column 39 and we print a character, then
-             that character shows up in column 39, and the cursor is no longer
-             visible on the screen (it's in "column 40".)  If another character
-             is printed, then that character shows up in column 0, and the
-             cursor moves to column 1.
+        if (! ascii) ascii = ' ';
+
+        a2_goto (st, y, x);
+
+        if (flag & TTY_SYMBOLS)    /* Convert to the nearest ASCII */
+          switch (ascii) {
+          case 0x60: ascii = '*';  /* ◆ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x6A: ascii = 'J';  /* ┘ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x6B: ascii = 'T';  /* ┐ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x6C: ascii = 'r';  /* ┌ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x6D: ascii = 'L';  /* └ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x6E: ascii = '+';  /* ┼ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x6F: ascii = '-';  /* ⎺ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x70: ascii = '-';  /* ⎻ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x71: ascii = '=';  /* ─ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x72: ascii = '_';  /* ⎼ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x73: ascii = '_';  /* ⎽ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x74: ascii = 'F';  /* ├ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x75: ascii = '+';  /* ┤ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x76: ascii = '+';  /* ┴ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x77: ascii = 'T';  /* ┬ */  flag &= ~TTY_SYMBOLS; break;
+          case 0x78: ascii = '#';  /* │ */  flag &= ~TTY_SYMBOLS; break;
+          }
 
-             This is empirically what xterm and gnome-terminal do, so that must
-             be the right thing.  (In xterm, the cursor vanishes, whereas; in
-             gnome-terminal, the cursor overprints the character in col 39.)
-           */
-          if (state->cursor_x >= cols)
-            {
-              state->cursor_x = 0;
-              if (state->cursor_y >= rows - 1)
-                a2_scroll (st);
-              else
-                state->cursor_y++;
-            }
 
-          a2_goto(st, state->cursor_y, state->cursor_x);  /* clips range */
-          a2_ascii_printc (st, c,
-                           state->termattrib.bf.bold,
-                           state->termattrib.bf.blink,
-                           state->termattrib.bf.rev,
+        if (flag & TTY_SYMBOLS)
+          /* Draw unknown symbol font characters as a box. */
+          a2_ascii_printc (st, ' ', 0, 0, !inv_p, False);
+        else
+          a2_ascii_printc (st, ascii,
+                           0,
+                           flag & TTY_BLINK,
+                           inv_p,
                            False);
-          state->cursor_x++;
-
-          break;
-        }
-      break;
-    case 1:
-      switch (c)
-        {
-        case 24: /* CAN */
-        case 26: /* SUB */
-          state->escstate = 0;
-          break;
-        case 'c': /* Reset */
-          a2_cls(st);
-          state->escstate = 0;
-          break;
-        case 'D': /* Linefeed */
-          if (state->cursor_y < rows - 1)
-            state->cursor_y++;
-          else
-            a2_scroll (st);
-          state->escstate = 0;
-          break;
-        case 'E': /* Newline */
-          state->cursor_x = 0;
-          state->escstate = 0;
-          break;
-        case 'M': /* Reverse newline */
-          if (state->cursor_y > 0)
-            state->cursor_y--;
-          state->escstate = 0;
-          break;
-        case '7': /* Save state */
-          state->saved_x = state->cursor_x;
-          state->saved_y = state->cursor_y;
-          state->escstate = 0;
-          break;
-        case '8': /* Restore state */
-          state->cursor_x = state->saved_x;
-          state->cursor_y = state->saved_y;
-          state->escstate = 0;
-          break;
-        case '[': /* CSI */
-          state->escstate = 2;
-          for(i = 0; i < NPAR; i++)
-            state->csiparam[i] = 0;
-          state->curparam = 0;
-          break;
-        case '%': /* Select charset */
-          /* @: Select default (ISO 646 / ISO 8859-1)
-             G: Select UTF-8
-             8: Select UTF-8 (obsolete)
-
-             We can just ignore this and always process UTF-8, I think?
-             We must still catch the last byte, though.
-           */
-        case '(':
-        case ')':
-          /* I don't support different fonts either - see above
-             for SO and SI */
-          state->escstate = 3;
-          break;
-        default:
-          /* Escape sequences not supported:
-           * 
-           * H - Set tab stop
-           * Z - Terminal identification
-           * > - Keypad change
-           * = - Other keypad change
-           * ] - OS command
-           */
-          state->escstate = 0;
-          break;
-        }
-      break;
-    case 2:
-      switch (c)
-        {
-        case 24: /* CAN */
-        case 26: /* SUB */
-          state->escstate = 0;
-          break;
-        case '0': case '1': case '2': case '3': case '4':
-        case '5': case '6': case '7': case '8': case '9':
-          if (state->curparam < NPAR)
-            state->csiparam[state->curparam] =
-              (state->csiparam[state->curparam] * 10) + (c - '0');
-          break;
-        case ';':
-          state->csiparam[++state->curparam] = 0;
-          break;
-        case '[':
-          state->escstate = 3;
-          break;
-        case '@':
-          for (i = 0; i < state->csiparam[0]; i++)
-            {
-              if(++state->cursor_x > cols)
-                {
-                  state->cursor_x = 0;
-                  if (state->cursor_y < rows - 1)
-                    state->cursor_y++;
-                  else
-                    a2_scroll (st);
-                }
-            }
-          state->escstate = 0;
-          break;
-        case 'F':
-          state->cursor_x = 0;
-        case 'A':
-          if (state->csiparam[0] == 0)
-            state->csiparam[0] = 1;
-          if ((state->cursor_y -= state->csiparam[0]) < 0)
-            state->cursor_y = 0;
-          state->escstate = 0;
-          break;
-        case 'E':
-          state->cursor_x = 0;
-        case 'e':
-        case 'B':
-          if (state->csiparam[0] == 0)
-            state->csiparam[0] = 1;
-          if ((state->cursor_y += state->csiparam[0]) >= rows)
-            state->cursor_y = rows - 1;
-          state->escstate = 0;
-          break;
-        case 'a':
-        case 'C':
-          if (state->csiparam[0] == 0)
-            state->csiparam[0] = 1;
-          if ((state->cursor_x += state->csiparam[0]) >= cols)
-            state->cursor_x = cols - 1;
-          state->escstate = 0;
-          break;
-        case 'D':
-          if (state->csiparam[0] == 0)
-            state->csiparam[0] = 1;
-          if ((state->cursor_x -= state->csiparam[0]) < 0)
-            state->cursor_x = 0;
-          state->escstate = 0;
-          break;
-        case 'd':
-          if ((state->cursor_y = (state->csiparam[0] - 1)) >= rows)
-            state->cursor_y = rows - 1;
-          state->escstate = 0;
-          break;
-        case '`':
-        case 'G':
-          if ((state->cursor_x = (state->csiparam[0] - 1)) >= cols)
-            state->cursor_x = cols - 1;
-          state->escstate = 0;
-          break;
-        case 'f':
-        case 'H':
-          if ((state->cursor_y = (state->csiparam[0] - 1)) >= rows)
-            state->cursor_y = rows - 1;
-          if ((state->cursor_x = (state->csiparam[1] - 1)) >= cols)
-            state->cursor_x = cols - 1;
-          if(state->cursor_y < 0)
-            state->cursor_y = 0;
-          if(state->cursor_x < 0)
-            state->cursor_x = 0;
-          state->escstate = 0;
-          break;
-        case 'J':
-          start = 0;
-          end = rows * cols;
-          if (state->csiparam[0] == 0)
-            start = cols * state->cursor_y + state->cursor_x;
-          if (state->csiparam[0] == 1)
-            end = cols * state->cursor_y + state->cursor_x;
-
-          a2_goto(st, state->cursor_y, state->cursor_x);
-          for (i = start; i < end; i++)
-            {
-              a2_ascii_printc(st, ' ', False, False, False, False);
-            }
-          state->escstate = 0;
-          break;
-        case 'K':
-          start = 0;
-          end = cols;
-          if (state->csiparam[0] == 0)
-            start = state->cursor_x;
-          if (state->csiparam[1] == 1)
-            end = state->cursor_x;
-
-          a2_goto(st, state->cursor_y, state->cursor_x);
-          for (i = start; i < end; i++)
-            {
-              a2_ascii_printc(st, ' ', False, False, False, False);
-            }
-          state->escstate = 0;
-          break;
-        case 'm': /* Set attributes */
-          for (i = 0; i <= state->curparam; i++)
-            {
-              switch(state->csiparam[i])
-                {
-                case 0:
-                  state->termattrib.w = 0;
-                  break;
-                case 1:
-                  state->termattrib.bf.bold = 1;
-                  break;
-                case 5:
-                  state->termattrib.bf.blink = 1;
-                  break;
-                case 7:
-                  state->termattrib.bf.rev = 1;
-                  break;
-                case 21:
-                case 22:
-                  state->termattrib.bf.bold = 0;
-                  break;
-                case 25:
-                  state->termattrib.bf.blink = 0;
-                  break;
-                case 27:
-                  state->termattrib.bf.rev = 0;
-                  break;
-                }
-            }
-          state->escstate = 0;
-          break;
-        case 's': /* Save position */
-          state->saved_x = state->cursor_x;
-          state->saved_y = state->cursor_y;
-          state->escstate = 0;
-          break;
-        case 'u': /* Restore position */
-          state->cursor_x = state->saved_x;
-          state->cursor_y = state->saved_y;
-          state->escstate = 0;
-          break;
-        case '?': /* DEC Private modes */
-          if ((state->curparam != 0) || (state->csiparam[0] != 0))
-            state->escstate = 0;
-          break;
-        default:
-          /* Known unsupported CSIs:
-           *
-           * L - Insert blank lines
-           * M - Delete lines (I don't know what this means...)
-           * P - Delete characters
-           * X - Erase characters (difference with P being...?)
-           * c - Terminal identification
-           * g - Clear tab stop(s)
-           * h - Set mode (Mainly due to its complexity and lack of good
-           docs)
-           * l - Clear mode
-           * m - Set mode (Phosphor is, per defenition, green on black)
-           * n - Status report
-           * q - Set keyboard LEDs
-           * r - Set scrolling region (too exhausting - noone uses this,
-           right?)
-          */
-          state->escstate = 0;
-          break;
-        }
-      break;
-    case 3:
-      state->escstate = 0;
-      break;
-
-    case 102:
-    case 103:
-    case 104:
-    case 105:
-    case 106:
-      {
-        int total = state->escstate - 100;  /* see what I did there */
-        if (state->unicruds < total) {
-          /* Buffer more bytes of the UTF-8 sequence */
-          state->unicrud[state->unicruds++] = c;
-        }
-
-        if (state->unicruds >= total) {
-          /* Done! Convert it to ASCII and print that. */
-          char *s;
-          state->unicrud[state->unicruds] = 0;
-          s = utf8_to_latin1 ((const char *) state->unicrud, True);
-          state->unicruds = 0;
-          state->escstate = 0;
-          if (s) {
-            c = s[0];
-            free (s);
-            goto PRINT;
-          } else {
-            /* c = 0; */
-          }
-        }
       }
-      break;
 
-    default:
-      abort();
-    }
-  a2_goto(st, state->cursor_y, state->cursor_x);
+  a2_goto (st, tty->y, tty->x);
 }
 
 
@@ -1335,19 +1039,26 @@ terminal_controller(apple2_sim_t *sim, int *stepno, double *next_actiontime)
     sim->controller_data=calloc(sizeof(struct terminal_controller_data),1);
   mine=(struct terminal_controller_data *) sim->controller_data;
   mine->dpy = sim->dpy;
+  if (!mine->launch_time) mine->launch_time = time ((time_t *) 0);
 
   mine->fast_p           = global_fast_p;
 
   switch(*stepno) {
 
   case 0:
+# ifdef COUNTDOWN_BANNER
+    if (1)
+# else
     if (random()%2)
+# endif
       st->gr_mode |= A2_GR_FULL; /* Turn on color mode even through it's
                                     showing text */
     a2_cls(st);
+# ifndef COUNTDOWN_BANNER
     a2_goto(st,0,16);
     a2_prints(st, "APPLE ][");
     a2_goto(st,2,0);
+# endif
     mine->cursor_y = 2;
 
     if (! mine->tc) {
@@ -1358,6 +1069,12 @@ terminal_controller(apple2_sim_t *sim, int *stepno, double *next_actiontime)
                           0);
     }
 
+    if (!mine->tty) {
+      mine->tty = ansi_tty_init (SCREEN_COLS, SCREEN_ROWS);
+      mine->tty->closure  = mine;
+      mine->tty->tty_send = a2_tty_send;
+    }
+
     if (! mine->fast_p)
       *next_actiontime += 4.0;
     *stepno = 10;
@@ -1400,6 +1117,10 @@ terminal_controller(apple2_sim_t *sim, int *stepno, double *next_actiontime)
         else
           a2_ascii_printc (st, c, False, False, False, True);
       }
+
+# ifdef COUNTDOWN_BANNER
+      a2_draw_banner (st, mine);
+# endif
     }
     break;
 
@@ -1411,6 +1132,15 @@ terminal_controller(apple2_sim_t *sim, int *stepno, double *next_actiontime)
   }
 }
 
+static void
+a2_tty_send (void *closure, const char *text)
+{
+  struct terminal_controller_data *mine =
+    (struct terminal_controller_data *) closure;
+  textclient_puts (mine->tc, text);
+}
+
+
 struct basic_controller_data {
   int prog_line;
   int x,y,k;
index 29eb6e03f9734730027abe2647e14f46a7a39689..c42a74dc5bba4248b912aae043e2fd4af3c30f65 100644 (file)
@@ -105,6 +105,7 @@ static const char *words[] =
   "affluenza",
   "alertness",
   "Algeria",
+  "all your base",
   "antifa",
   "anxiety",
   "aorta",
@@ -121,12 +122,14 @@ static const char *words[] =
   "bells",
   "belly",
   "bird flu",
+  "Bitcorn",
   "bliss",
   "bogosity",
   "boobies",
   "boobs",
   "booty",
   "bread",
+  "bribe",
   "brogrammers",
   "bubba",
   "burrito",
@@ -167,14 +170,17 @@ static const char *words[] =
   "DNA Lounge",
   "doberman",
   "DOOM",
+  "doom loop",
   "dot com",
   "dreams",
   "drugs",
+  "Dunning-Krugerrands",
   "easy",
   "ebony",
   "election",
   "eloquence",
   "emergency",
+  "emolument",
   "eureka",
   "excommunication",
   "fat",
@@ -196,6 +202,8 @@ static const char *words[] =
   "goggles",
   "goobers",
   "gorilla",
+  "guillotine",
+  "H5N1",
   "halibut",
   "handmaid",
   "happiness",
@@ -206,6 +214,7 @@ static const char *words[] =
   "heroin",
   "heroine",
   "hope",
+  "horse paste",
   "hysteria",
   "icepick",
   "identity",
@@ -255,10 +264,14 @@ static const char *words[] =
   "nationalism",
   "nature",
   "neuron",
+  "NFTs",
   "noise",
   "nomenclature",
+  "NULL",
+  "null island",
   "nutria",
   "OBEY",
+  "ouroboros",
   "ocelot",
   "offspring",
   "overseer",
@@ -335,6 +348,7 @@ static const char *words[] =
   "television",
   "tenant",
   "tendril",
+  "teratoma",
   "terror",
   "terrorism",
   "terrorist",
@@ -343,10 +357,12 @@ static const char *words[] =
   "the unknown",
   "toast",
   "topography",
+  "tribble",
   "truism",
   "truthiness",
   "turgid",
   "twits",
+  "undef",
   "underbrush",
   "underling",
   "unguent",
@@ -386,6 +402,7 @@ static const char *words[] =
   "yellow",
   "yesterday",
   "your nose",
+  "Y2038",
   "Zanzibar",
   "zeal",
   "zebra",
index 37bfb614fb6cfa72af1e50b625b04e7e50e397b4..653d6a34e9354ab550c485db30565c41aab9a5e8 100644 (file)
@@ -37,6 +37,7 @@
 
 #include "screenhack.h"
 #include "xft.h"
+#include "xftwrap.h"
 #include "ximage-loader.h"
 #include "apple2.h"
 
@@ -1898,6 +1899,78 @@ windows_10_update (Display *dpy, Window window)
 }
 
 
+static struct bsod_state *
+windows_10_recovery (Display *dpy, Window window)
+{
+  struct bsod_state *bst =
+    make_bsod_state (dpy, window, "win10r", "Win10r");
+  const char *line1 = "Recovery";
+  const char *line2 = "It looks like Windows didn't load correctly";
+  const char *line3 = "If you'd like to restart and try again, choose "
+                     "\"Restart my PC\" below. Otherwise, choose "
+                     "\"See advanced repair options\" for troubleshooting "
+                     "tools and advanced options. If you don't know which "
+                     "option is right for you, contact someone you "
+                     "trust to help with this.";
+  const char *line4 = "  See advanced repair options  ";
+  const char *line5 = "  Restart my PC  ";
+  int line_height  = bst->fontA->ascent + bst->fontA->descent;
+  int line_heightB = bst->fontB->ascent + bst->fontB->descent;
+  int line_heightC = bst->fontC->ascent + bst->fontC->descent;
+  int y = line_heightC * 2;
+  int x, x2, x3;
+  XGlyphInfo ov;
+
+  XftTextExtentsUtf8 (bst->dpy, bst->fontB, (FcChar8 *) line2, strlen(line2),
+                      &ov);
+  x = (bst->xgwa.width - (ov.xOff * 1.7)) / 2;
+  if (x < 10) x = 10;
+  BSOD_MARGINS (bst, x, x);
+
+  BSOD_WORD_WRAP (bst);
+  BSOD_FONT (bst, 2);
+  BSOD_MOVETO (bst, 0, y);
+  BSOD_TEXT (bst, LEFT, line1);
+  BSOD_TEXT (bst, LEFT, "\n");
+
+  BSOD_FONT (bst, 1);
+  BSOD_TEXT (bst, LEFT, line2);
+  BSOD_TEXT (bst, LEFT, "\n");
+
+  BSOD_FONT (bst, 0);
+  BSOD_TEXT (bst, LEFT, line3);
+
+  XftTextExtentsUtf8 (bst->dpy, bst->font, (FcChar8 *) line5, strlen(line5),
+                      &ov);
+  x2 = bst->xgwa.width - x - ov.xOff;
+
+  if ( bst->xgwa.width >  bst->xgwa.height)
+    y += line_heightB * 2 + line_height * 7;
+  else
+    y += line_heightB * 3 + line_height * 9;
+
+  BSOD_TRUNCATE (bst);
+  BSOD_MOVETO (bst, x2, y);
+  BSOD_TEXT (bst, LEFT, line5);
+  BSOD_RECT (bst, False, x2, y - line_height, ov.xOff,
+             line_height + bst->fontA->descent * 2);
+
+  XftTextExtentsUtf8 (bst->dpy, bst->font, (FcChar8 *) line4, strlen(line4),
+                      &ov);
+  x3 = x2 - ov.xOff - line_height;
+
+  BSOD_TRUNCATE (bst);
+  BSOD_MOVETO (bst, x3, y);
+  BSOD_TEXT (bst, LEFT, line4);
+  BSOD_RECT (bst, False, x3, y - line_height, ov.xOff,
+             line_height + bst->fontA->descent * 2);
+
+
+  XClearWindow (dpy, window);
+  return bst;
+}
+
+
 static struct bsod_state *
 windows_10 (Display *dpy, Window window)
 {
@@ -1930,49 +2003,88 @@ windows_10 (Display *dpy, Window window)
     0xFF,0xFF,0xFF,0xFF,0xFF,0x01};
   Pixmap pixmap;
 
-  const char * const lines[] = {
-    ":(\n",
-    "\n",
-    "Your PC ran into a problem and needs to restart. We're just\n",
-    "collecting some error info, and then we'll restart for you.\n",
-    "\n",
-    "\n",
-    "\n",
-    "For more information about this issue and\n",
-    "possible fixes, visit\n",
-/*  "https://www.jwz.org/xscreensaver\n",*/
-    "http://youtu.be/-RjmN9RZyr4\n",
-    "\n",
-    "If you call a support person, give them this info:\n",
+  const char * const lines1[] = {
+    ":(",
+
+    "\n"
+    "Your PC ran into a problem and needs to restart. We're just"
+    " collecting some error info, and then we'll restart for you."
+    "\n\n\n",
+
+    "\n\n",
+
+    "For more information about this issue and possible fixes, visit\n"
+    "http://youtu.be/-RjmN9RZyr4\n\n"
+    "If you call a support person, give them this info:\n"
     "Stop code CRITICAL_PROCESS_DIED",
- };
-  int i, y = 0, y0 = 0;
-  int line_height0 = bst->fontB->ascent;
-  int line_height1 = bst->fontA->ascent + bst->fontA->descent;
-  int line_height2 = bst->fontC->ascent + bst->fontC->descent;
-  int line_height = line_height0;
-  int top, left0, left;
-  int stop = 60 + (random() % 39);
+  };
+  const char * const lines2[] = {
+    "-\\_(:/)_/-",   /* ¯\\_(ツ)_/¯   BSOD_TEXT() does not support UTF-8. */
 
-  if (!(random() % 7))
-    return windows_10_update (dpy, window);
+    "\n"
+    "Your PC ran into a ClownStrike and needs to restart 15 or more times."
+    "\n\n\n",
+
+    "\n\n",
+
+    "For more information about this issue and possible fixes, visit\n"
+    "http://youtu.be/-RjmN9RZyr4\n\n"
+    "If you call a support person, give them this info:\n"
+    "Stop code COMPLIANCE_LINE_ITEM_OK",
+  };
+  const char * const lines3[] = {
+    "x__x",
+
+    "\n"
+    "This place is a message... "
+      "and part of a system of messages... "
+      "pay attention to it! "
+      "\n\n"
+      "Sending this message was important to us. "
+      "We considered ourselves to be a powerful culture."
+      "\n\n",
+
+    "\nThis place is not a place of honor... "
+      "no highly esteemed deed is commemorated here... "
+      "nothing valued is here. "
+      "\n\n"
+      "What is here was dangerous and repulsive to us. "
+      "This message is a warning about danger. "
+      "\n",
+
+      "The danger is in a particular location... "
+      "it increases towards a center... "
+      "the center of danger is here... "
+      "of a particular size and shape, and below us. "
+      "\n\n"
+      "The danger is still present, in your time, as it was in ours. "
+      "The danger is to the body, and it can kill. "
+      "\n\n"
+      "This place is best shunned and left uninhabited."
+  };
+  int i, y = 0;
+  int top, left0, left, right, y1;
+  Bool clownp = !(random() % 10);
+  Bool honorp = !clownp && !(random() % 20);
+  const char * const * lines = (clownp ? lines2 : honorp ? lines3 : lines1);
+  int stop = 60 + (random() % 39) + (clownp ? 1300 : 0);
 
+  if (!(random() % 4))
+    return windows_10_recovery (dpy, window);
 
-  line_height1 *= 1.3;
-  line_height2 *= 1.5;
+  if (!(random() % 14))
+    return windows_10_update (dpy, window);
 
-  top = ((bst->xgwa.height - bst->yoff
-          - (line_height0 * 1 +
-             line_height1 * 6 +
-             line_height2 * 6))
-         / 2);
+  top = (bst->xgwa.height > 800 ? bst->fontB->ascent : 0);
 
   {
     XGlyphInfo ov;
-    const char *s = lines[2];
-
+    const char *s = lines1[1];
     XftTextExtentsUtf8 (bst->dpy, bst->font, (FcChar8 *) s, strlen(s), &ov);
-    left = left0 = (bst->xgwa.width - ov.xOff) / 2;
+    left = (bst->xgwa.width - (ov.xOff * 0.55)) / 2;
+    if (left < 10) left  = 10;
+    left0 = right = left;
+    BSOD_MARGINS (bst, left, right);
   }
 
   pixmap = XCreatePixmapFromBitmapData (dpy, window, (char *) qr_bits,
@@ -1993,39 +2105,59 @@ windows_10 (Display *dpy, Window window)
   bst->pixmap = pixmap;
 
   y = top;
-  line_height = line_height0;
   BSOD_FONT (bst, 1);
-  for (i = 0; i < countof(lines); i++)
+  for (i = 0; i < countof(lines1); i++)
     {
-      BSOD_MOVETO (bst, left, y);
-      BSOD_TEXT (bst, LEFT, lines[i]);
-      y += line_height;
+      int oy = y;
+      int fid = (i == 0 ? 1 : i >= 3 ? 2 : 0);
+      XftFont *font = (fid == 0 ? bst->font :
+                       fid == 1 ? bst->fontB : bst->fontC);
+      char *line;
+      XGlyphInfo ov;
+
+      line = xft_word_wrap (dpy, font, lines[i],
+                            bst->xgwa.width - left - right);
+      XftTextExtentsUtf8_multi (dpy, font, (FcChar8 *) line, strlen(line),
+                                &ov);
+
+      BSOD_MOVETO (bst, left, y + font->ascent);
+      BSOD_FONT (bst, fid);
+      BSOD_TEXT (bst, LEFT, line);
+
+      free (line);
+      line = 0;
+      y += ov.height + font->descent;
+
       if (i == 0)
         {
-          BSOD_FONT (bst, 0);
-          line_height = line_height1;
+          BSOD_MOVETO (bst, left, y);
         }
-      else if (i == 4)
+      else if (i == 1)
         {
-          y0 = y;
-          y += line_height / 2;
-          BSOD_PIXMAP (bst, 0, 0, qr_width, qr_height, left, y + line_height1);
-          BSOD_FONT (bst, 2);
-          line_height = line_height2;
-          left += qr_width + line_height2 / 2;
-# ifdef HAVE_MOBILE
-          y -= 14;
-# endif
+//          y += font->ascent + font->descent;
+          y1 = y;
+//          y += font->ascent + font->descent;
+        }
+      else if (i == 2)
+        {
+          left  += qr_width + font->ascent / 2;
+          if (bst->xgwa.width > bst->xgwa.height)
+            right += qr_width * 1.8;
+          BSOD_MARGINS (bst, left, right);
+        }
+      else if (i == 3)
+        {
+          BSOD_PIXMAP (bst, 0, 0, qr_width, qr_height, left0, oy);
         }
     }
 
-  left = left0;
+  BSOD_MARGINS (bst, left0, 0);
   BSOD_FONT (bst, 0);
   for (i = 0; i <= stop; i++)
     {
       char buf[100];
       sprintf (buf, "%d%% complete", i);
-      BSOD_MOVETO (bst, left, y0);
+      BSOD_MOVETO (bst, left0, y1);
       BSOD_TEXT (bst, LEFT, buf);
       BSOD_PAUSE (bst, 85000);
     }
@@ -2672,6 +2804,167 @@ windows_ransomware (Display *dpy, Window window)
 }
 
 
+static struct bsod_state *
+bitlocker (Display *dpy, Window window)
+{
+  struct bsod_state *bst =
+    make_bsod_state (dpy, window, "bitlocker", "Bitlocker");
+  int top = bst->fontB->ascent + bst->fontB->descent;
+  int left = top * 2;
+  int right = left + bst->fontB->ascent * 22;
+  const char *bottom = ("Press Enter to reboot and try again\n"
+                        "Press ESC for BitLocker recovery");
+  int which = random() % 5;
+
+  if (right > bst->xgwa.width) right = bst->xgwa.width;
+  BSOD_MARGINS (bst, left, bst->xgwa.width - right);
+  BSOD_WORD_WRAP (bst);
+  BSOD_FONT (bst, 1);
+  BSOD_MOVETO (bst, 0, top * 2);
+
+  switch (which) {
+    case 0:
+      BSOD_TEXT (bst, LEFT, "BitLocker\n\n");
+      BSOD_FONT (bst, 0);
+      BSOD_TEXT (bst, LEFT,
+                 "Plug in the USB drive that has the BitLocker key\n");
+      break;
+
+    case 1:
+      BSOD_TEXT (bst, LEFT, "BitLocker recovery\n\n");
+      BSOD_FONT (bst, 0);
+      BSOD_TEXT (bst, LEFT,
+                 "To recover this drive, plug in the USB drive that has "
+                 "the BitLocker recovery key\n\n");
+      BSOD_FONT (bst, 2);
+      BSOD_TEXT (bst, LEFT,
+                 "Bitlocker needs your recovery key to unlock your drive "
+                 "because Secure Boot policy has unexpectedly changed.\n"
+                 "For more information on how to retrieve this key, go to\n"
+                 "https://www.jwz.org/xscreensaver/ from another PC "
+                 "or mobile device.");
+      bottom = ("Press Enter to reboot and try again\n"
+                "Press Esc or the Windows key for more recovery options");
+      break;
+    case 2:
+      BSOD_TEXT (bst, LEFT, "BitLocker\n\n");
+      BSOD_FONT (bst, 0);
+      BSOD_TEXT (bst, LEFT,
+                 "Enter the PIN to unlock this drive\n");
+      BSOD_INVERT (bst);
+      BSOD_TRUNCATE (bst);
+      BSOD_TEXT (bst, LEFT,
+                 "                                                "
+                 "                                                ");
+      BSOD_INVERT (bst);
+      BSOD_WORD_WRAP (bst);
+      BSOD_TEXT (bst, LEFT,
+                 "\n\n\n"
+                 "Use the number keys or function keys F1-F10 "
+                 "(use F10 for 0)."
+                 "\n\n\n"
+                 "Press the Insert key to see the PIN as you type.");
+      break;
+
+    case 3:
+      BSOD_TEXT (bst, LEFT, "BitLocker recovery\n\n");
+      BSOD_FONT (bst, 0);
+      BSOD_TEXT (bst, LEFT,
+                 "Enter the recovery key for this drive\n");
+      BSOD_INVERT (bst);
+      BSOD_TRUNCATE (bst);
+      BSOD_TEXT (bst, LEFT,
+                 "                                                "
+                 "                                                ");
+      BSOD_INVERT (bst);
+      BSOD_WORD_WRAP (bst);
+
+      BSOD_FONT (bst, 2);
+      BSOD_TEXT (bst, LEFT,
+                 "\n\n\n"
+                 "Use the number keys or function keys F1-F10 "
+                 "(use F10 for 0).\n");
+      {
+        int which2;
+        char k[200];
+        sprintf (k, "Recovery key ID (to identify your key): "
+                 "%08X-%04X-%04X-%04X%08X\n\n",
+                 random() & 0xFFFFFFFF,
+                 random() & 0xFFFF,
+                 random() & 0xFFFF,
+                 random() & 0xFFFF,
+                 random() & 0xFFFFFFFF);
+        BSOD_TEXT (bst, LEFT, k);
+      
+        which2 = random() % 4;
+        switch (which2) {
+        case 0:
+          BSOD_TEXT (bst, LEFT,
+                     "Bitlocker needs your recovery key to unlock your "
+                     "drive because Secure Boot policy has unexpectedly "
+                     "changed."
+                     "\n\n");
+          break;
+        case 1:
+          BSOD_TEXT (bst, LEFT,
+                     "Bitlocker needs your recovery key to unlock your "
+                     "PC's configuration has changed. This may have happened "
+                     "because a disc or USB device ws inserted. Removing it "
+                     "and restarting your PC may fix this problem."
+                     "\n\n");
+          break;
+        case 3:
+          BSOD_TEXT (bst, LEFT,
+                     "Bitlocker needs your recovery key to unlock your drive "
+                     "because Secure Boot has been disabled. Either Secure "
+                     "Boot must be re-enabled, or BitLocker must be suspended "
+                     "for Windows to start normally."
+                     "\n\n");
+          break;
+        default:
+          break;
+        }
+      }
+
+      BSOD_TEXT (bst, LEFT,
+                 "Here's how to find your key:\n"
+                 "- Sign in on another device and go to: "
+                 "https://www.jwz.org/\n"
+                 "- For more information go to: "
+                 "https://www.jwz.org/xscreensaver/");
+      break;
+
+    case 4:
+      BSOD_TEXT (bst, LEFT, "Recovery\n\n");
+      BSOD_FONT (bst, 0);
+      BSOD_TEXT (bst, LEFT,
+                 "There are no more BitLocker recovery options on your PC"
+                 "\n\n");
+      BSOD_FONT (bst, 2);
+      BSOD_TEXT (bst, LEFT,
+                 "You'll need to use the recovery tools on your installation "
+                 "media. If you don't have any installation media (like a "
+                 "disc or USB device), contact your system administrator or "
+                 "PC manufacturer.");
+      bottom = ("Press Enter to try again\n"
+                "Press F8 for Startup Settings\n"
+                "Press Esc for UEFI Firmware Settings");
+      break;
+
+  default:
+    abort();
+  }
+
+  BSOD_FONT (bst, 0);
+  BSOD_MOVETO (bst, 0, bst->xgwa.height - top -
+               (bst->fontB->ascent + bst->fontB->descent) * 1);
+  BSOD_TEXT (bst, LEFT, bottom);
+
+  XClearWindow (dpy, window);
+  return bst;
+}
+
+
 /* As seen in Portal 2.  By jwz.
  */
 static struct bsod_state *
@@ -6734,6 +7027,33 @@ gnome (Display *dpy, Window window)
 }
 
 
+/* Linux kernel panic obscured by systemd, 2024.
+   https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/gpu/drm/drm_panic.c
+ */
+static struct bsod_state *
+systemd (Display *dpy, Window window)
+{
+  struct bsod_state *bst = make_bsod_state (dpy, window, "systemd", "Systemd");
+  int lh = (bst->font->ascent + bst->font->descent);
+  BSOD_MOVETO (bst, 0, lh);
+  BSOD_TEXT (bst, LEFT,
+             "     .--.        _\n"
+             "    |o_o |      | |\n"
+             "    |:_/ |      | |\n"
+             "   //   \\ \\     |_|\n"
+             "  (|     | )     _\n"
+             " /'\\_   _/`\\    (_)\n"
+             " \\___)=(___/\n");
+  BSOD_MOVETO (bst, 0, (bst->xgwa.height / 2) - lh);
+  BSOD_TEXT (bst, CENTER,
+             "KERNEL PANIC !\n"
+             "\n"
+             "Please reboot your computer.");
+  XClearWindow (dpy, window);
+  return bst;
+}
+
+
 /*****************************************************************************
  *****************************************************************************/
 
@@ -6746,6 +7066,7 @@ static const struct {
   { "NT",              windows_nt },
   { "Win2K",           windows_other },
   { "Win10",           windows_10 },
+  { "Bitlocker",       bitlocker },
   { "Ransomware",      windows_ransomware },
   { "Amiga",           amiga },
   { "Mac",             mac },
@@ -6781,6 +7102,7 @@ static const struct {
   { "Tivo",            tivo },
   { "Nintendo",                nintendo },
   { "Gnome",           gnome },
+  { "Systemd",         systemd },
 };
 
 
@@ -7076,6 +7398,7 @@ static const char *bsod_defaults [] = {
   "*doNT:                 True",
   "*doWin2K:              True",
   "*doWin10:              True",
+  "*doBitlocker:          True",
   "*doRansomware:         True",
   "*doAmiga:              True",
   "*doMac:                True",
@@ -7109,6 +7432,7 @@ static const char *bsod_defaults [] = {
   "*doTivo:               True",
   "*doNintendo:                   True",
   "*doGnome:              True",
+  "*doSystemd:            True",
 
   ".foreground:                   White",
   ".background:                   Black",
@@ -7130,6 +7454,12 @@ static const char *bsod_defaults [] = {
   ".win10.foreground:      White",
   ".win10.background:      #1070AA",
 
+  ".win10r.foreground:     White",
+  ".win10r.background:     #1070AA",
+
+  ".bitlocker.foreground:  White",
+  ".bitlocker.background:  #1070AA",
+
   ".ransomware.foreground:   White",
   ".ransomware.background:   #841212",
   ".ransomware.foreground2:  Black",      /* ransom note */
@@ -7246,6 +7576,9 @@ static const char *bsod_defaults [] = {
   ".gnome.background2:     #F0F0F0",
   ".gnome.foreground2:     #2E3436",
 
+  ".systemd.background:    #0000AA",
+  ".systemd.foreground:    White",
+
   "*dontClearRoot:         True",
 
   ANALOGTV_DEFAULTS
@@ -7272,9 +7605,19 @@ static const char *bsod_defaults [] = {
 
   ".win10.font:                Arial 24, Helvetica 24",
   ".win10.bigFont:     Arial 24, Helvetica 24",
-  ".win10.fontB:       Arial 36, Helvetica 36",
+  ".win10.fontB:       Arial 90, Helvetica 36",
   ".win10.fontC:       Arial 16, Helvetica 16",
 
+  ".win10r.font:       Arial 16, Helvetica 24",
+  ".win10r.bigFont:    Arial 16, Helvetica 24",
+  ".win10r.fontB:      Arial 28, Helvetica 36",
+  ".win10r.fontC:      Arial 50, Helvetica 16",
+
+  ".bitlocker.font:    Arial 24, Helvetica 24",
+  ".bitlocker.bigFont: Arial 24, Helvetica 24",
+  ".bitlocker.fontB:   Arial 36, Helvetica 36",
+  ".bitlocker.fontC:   Arial 18, Helvetica 18",
+
   /* "Arial" loads "ArialMT" but "Arial Bold" does not load "Arial-BoldMT"? */
   ".ransomware.font:    Arial 12, Helvetica 12",
   ".ransomware.bigFont: Arial 12, Helvetica 12",
@@ -7313,6 +7656,9 @@ static const char *bsod_defaults [] = {
   ".gnome.font:                Helvetica Bold 13",
   ".gnome.bigFont:     Helvetica Bold 13",
   ".gnome.fontB:       Helvetica 13",
+
+  ".systemd.font:      Classic Console 14",
+  ".systemd.bigFont:   Classic Console 14",
   0
 };
 
@@ -7328,6 +7674,8 @@ static const XrmOptionDescRec bsod_options [] = {
   { "-no-2k",          ".doWin2K",             XrmoptionNoArg,  "False" },
   { "-win10",          ".doWin10",             XrmoptionNoArg,  "True"  },
   { "-no-win10",       ".doWin10",             XrmoptionNoArg,  "False" },
+  { "-bitlocker",      ".doBitlocker",         XrmoptionNoArg,  "True"  },
+  { "-no-bitlocker",   ".doBitlocker",         XrmoptionNoArg,  "False" },
   { "-ransomware",     ".doRansomware",        XrmoptionNoArg,  "True"  },
   { "-no-ransomware",  ".doRansomware",        XrmoptionNoArg,  "False" },
   { "-amiga",          ".doAmiga",             XrmoptionNoArg,  "True"  },
@@ -7394,6 +7742,8 @@ static const XrmOptionDescRec bsod_options [] = {
   { "-no-nintendo",    ".doNintendo",          XrmoptionNoArg,  "False" },
   { "-gnome",          ".doGnome",             XrmoptionNoArg,  "True"  },
   { "-no-gnome",       ".doGnome",             XrmoptionNoArg,  "False" },
+  { "-systemd",                ".doSystemd",           XrmoptionNoArg,  "True"  },
+  { "-no-systemd",     ".doSystemd",           XrmoptionNoArg,  "False" },
   ANALOGTV_OPTIONS
   { 0, 0, 0, 0 }
 };
index bbcc15f93d5c31fdb77a096dd7fa2a9bf2792832..58d721709c16afe31d61cb2e4e80a2c92d379de2 100755 (executable)
@@ -1,5 +1,5 @@
 #!/usr/bin/perl -w
-# Copyright © 2008-2024 Jamie Zawinski <jwz@jwz.org>
+# Copyright © 2008-2025 Jamie Zawinski <jwz@jwz.org>
 #
 # Permission to use, copy, modify, distribute, and sell this software and its
 # documentation for any purpose is hereby granted without fee, provided that
@@ -21,7 +21,7 @@ use diagnostics;
 use strict;
 
 my $progname = $0; $progname =~ s@.*/@@g;
-my ($version) = ('$Revision: 1.44 $' =~ m/\s(\d[.\d]+)\s/s);
+my ($version) = ('$Revision: 1.49 $' =~ m/\s(\d[.\d]+)\s/s);
 
 my $verbose = 0;
 my $debug_p = 0;
@@ -671,21 +671,25 @@ sub munge_blurb($$$$) {
     }
   }
 
+  my $desc0 = "$desc";
+  utf8::decode($desc0);   # Pack UTF-8 into wide chars.
+
   my $desc1 = ("$name, version $vers.\n\n" .           # savername.xml
                $desc . "\n" .
                "\n" . 
                "From the XScreenSaver collection: " .
                "https://www.jwz.org/xscreensaver/\n" .
-               "Copyright \302\251 $year by $authors.\n");
+               "Copyright \x{A9} $year by $authors.\n");
 
   my $desc2 = ("$name $vers,\n" .                      # Info.plist
-               "\302\251 $year $authors.\n" .
+               "\x{A9} $year $authors.\n" .
                #"From the XScreenSaver collection:\n" .
                #"https://www.jwz.org/xscreensaver/\n" .
                "\n" .
                $desc .
                "\n");
-  utf8::decode($desc1);   # Pack UTF-8 into wide chars.
+
+  utf8::decode($desc1);   # Pack UTF-8 into wide chars (redundant?)
   utf8::decode($desc2);
 
   # unwrap lines, but only when it's obviously ok: leave blank lines,
@@ -752,6 +756,16 @@ sub build_android(@) {
     $saver_class =~ s/\]\[/2/gs;
     $saver_class =~ s/[-_\s]//gs;
 
+    # If the saver's title is not case-insensitively the same as its progname
+    # after removing spaces, the class name has to be the progname, or
+    # jwxyz_nativeInit is not able to find it in the function_table.
+    # (Cuboctahedron Eversion, Möbius, MöbiusGears, Moiré, Moiré2.)
+    #
+    if (lc($saver_class) ne lc($saver)) {
+      $saver_class = $saver;
+      $saver_class =~ s/^(.)/\U$1/s;
+    }
+
     my $settings = '';
 
     my $localize0 = sub($$) {
@@ -1137,6 +1151,8 @@ sub build_android(@) {
                    "android.permission.INTERNET\" />\n" .
                "  <uses-permission android:name=\"" .
                    "android.permission.READ_EXTERNAL_STORAGE\" />\n" .
+               "  <uses-permission android:name=\"" .
+                   "android.permission.READ_MEDIA_IMAGES\" />\n" .
 
                "  <application android:icon=\"\@drawable/thumbnail\"\n" .
                "    android:banner=\"\@drawable/thumbnail\"\n" .
@@ -1239,7 +1255,7 @@ sub error($) {
 }
 
 sub usage() {
-  print STDERR "usage: $progname [--verbose] [--debug]" .
+  print STDERR "usage: $progname [--verbose] [--debug] [--force]" .
     " [--build-android] files ...\n";
   exit 1;
 }
@@ -1248,11 +1264,13 @@ sub main() {
   binmode (STDOUT, ':utf8');
   binmode (STDERR, ':utf8');
 
+  my $force_p = 0;
   my $android_p = 0;
   my @files = ();
   while ($#ARGV >= 0) {
     $_ = shift @ARGV;
     if (m/^--?verbose$/) { $verbose++; }
+    elsif (m/^--?force$/) { $force_p++; }
     elsif (m/^-v+$/) { $verbose += length($_)-1; }
     elsif (m/^--?debug$/s) { $debug_p++; }
     elsif (m/^--?build-android$/s) { $android_p++; }
@@ -1261,7 +1279,7 @@ sub main() {
 #    else { usage; }
   }
 
-  usage unless ($#files >= 0);
+  usage unless ($#files >= 0 || $force_p);
   my $failures = 0;
   foreach my $file (@files) {
     $failures += check_config ($file, $android_p);
index 6b2c000fad9f25348944701cb8a86f98ef851172..3e68861ce0ca440fef198ddc794b0fcc3a725423 100644 (file)
@@ -4,8 +4,8 @@
             a screen saver and locker for the X window system
                             by Jamie Zawinski
 
-                              version 6.09
-                               07-Jun-2024
+                              version 6.10
+                               27-Apr-2025
 
                      https://www.jwz.org/xscreensaver/
 
index 3938147b4837ffcb566e3229ea4d653b4adf85b8..090e91823faed78b3589fc067bfb140e89368de9 100644 (file)
    <boolean id="nt"         _label="Windows NT"      arg-unset="--no-nt"/>
    <boolean id="2k"         _label="Windows 2000  "  arg-unset="--no-2k"/>
    <boolean id="win10"      _label="Windows 10    "  arg-unset="--no-win10"/>
-   <boolean id="vmwareArm"  _label="VMware ESXi-Arm" arg-unset="--no-vmwarearm"/>   
+   <boolean id="bitlocker"  _label="BitLocker"       arg-unset="--no-bitlocker"/>   
   </vgroup>
   <vgroup>
    <boolean id="msdos"      _label="MS-DOS"         arg-unset="--no-msdos"/>
    <boolean id="glados"     _label="GLaDOS"         arg-unset="--no-glados"/>
    <boolean id="amiga"      _label="AmigaDOS"       arg-unset="--no-amiga"/>
    <boolean id="android"    _label="Android"        arg-set="--android"/>
+   <boolean id="vmwareArm"  _label="VMware ESXi-Arm" arg-unset="--no-vmwarearm"/>   
   </vgroup>
   <vgroup>
    <boolean id="apple2"     _label="Apple ]["       arg-unset="--no-apple2"/>
@@ -67,6 +68,7 @@
    <boolean id="hpux"       _label="HPUX"           arg-unset="--no-hpux"/>
    <boolean id="tru64"      _label="Tru64"          arg-unset="--no-tru64"/>
    <boolean id="gnome"      _label="GNOME"          arg-unset="--no-gnome"/>
+   <boolean id="systemd"    _label="Systemd"        arg-unset="--no-systemd"/>
   </vgroup>
  </hgroup>
 
diff --git a/hacks/config/dumpsterfire.xml b/hacks/config/dumpsterfire.xml
new file mode 100644 (file)
index 0000000..193fe07
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<screensaver name="dumpsterfire" _label="Dumpster Fire" gl="yes">
+
+  <command arg="--root"/>
+
+  <video href="https://www.youtube.com/watch?v=9XZQ3xLS4O8"/>
+
+  <number id="delay" type="slider" arg="--delay %"
+          _label="Frame rate" _low-label="Low" _high-label="High"
+          low="0" high="100000" default="20000"
+          convert="invert"/>
+
+  <number id="speed" type="slider" arg="--speed %"
+          _label="Speed" _low-label="Slow" _high-label="Fast"
+          low="0.01" high="10.0" default="1.0"
+          convert="ratio"/>
+
+  <number id="density" type="slider" arg="--density %"
+          _label="Flame Density" _low-label="Low" _high-label="High"
+          low="0.25" high="4.0" default="1.0"
+          convert="ratio"/>
+
+  <hgroup>
+   <boolean id="wander" _label="Wander"    arg-unset="--no-wander"/>
+   <boolean id="spin"   _label="Spin"      arg-unset="--no-spin"/>
+   <boolean id="wire"   _label="Wireframe" arg-set="--wireframe"/>
+  </hgroup>
+
+  <boolean id="showfps" _label="Show frame rate" arg-set="--fps"/>
+
+  <xscreensaver-updater />
+
+  <_description>
+It's a dumpster. It's on fire. It's a metaphor for This Modern World.
+
+Written by Jamie Zawinski; 2025.
+  </_description>
+</screensaver>
diff --git a/hacks/config/hopffibration.xml b/hacks/config/hopffibration.xml
new file mode 100644 (file)
index 0000000..37d862d
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<screensaver name="hopffibration" _label="Hopf Fibration" gl="yes">
+
+  <video href="https://www.youtube.com/watch?v=llVdG2yjqnY"/>
+
+  <command arg="--root"/>
+
+  <hgroup>
+
+    <vgroup>
+     <boolean id="shadows" _label="Display shadows" arg-unset="--no-shadows"/>
+
+     <boolean id="basespace" _label="Display the base space" arg-unset="--no-base-space"/>
+
+     <boolean id="antialiasing" _label="Use anti-aliasing" arg-unset="--no-anti-aliasing"/>
+    </vgroup>
+
+    <vgroup>
+      <hgroup>
+        <select id="details">
+          <option id="coarse" _label="Coarse" arg-set="--details coarse"/>
+          <option id="medium" _label="Medium"/>
+          <option id="fine"   _label="Fine"   arg-set="--details fine"/>
+        </select>
+
+        <select id="projection">
+         <option id="perspective" _label="Perspective"/>
+         <option id="orthographic" _label="Orthographic"
+                 arg-set="--orthographic"/>
+        </select>
+      </hgroup>
+
+      <hgroup>
+        <boolean id="showfps" _label="Show frame rate" arg-set="--fps"/>
+
+        <number id="delay" type="slider" arg="--delay %"
+                _label="Frame rate" _low-label="Low" _high-label="High"
+                low="0" high="100000" default="20000"
+                convert="invert"/>
+      </hgroup>
+      <xscreensaver-updater />
+
+    </vgroup>
+  </hgroup>
+
+  <_description>
+The Hopf fibration of the 4d hypersphere S³.
+
+The Hopf fibration is based on the Hopf map, a many-to-one continuous
+function from S³ onto the the ordinary 3d sphere S² such that each
+distinct point of S² is mapped from a distinct great circle of S³.
+Hence, the inverse image of a point on S² corresponds to a great
+circle S¹ on S³. The sphere S² is called the base space, each S¹
+corresponding to a point on S² is called a fiber, and S³ is called the
+total space. The program displays various configurations of points on
+the base space and their fibers in the total space. The corresponding
+points and fibers are displayed in the same color.
+
+Any two fibers form a Hopf link. Each circle on S² creates a set of
+fibers that forms a Clifford torus on S³ (i.e., in 4d). Clifford tori
+are flat (in the same sense that the surface of a cylinder is
+flat). More generally, any closed curve on S² creates a torus-like
+surface on S³ that is flat. These surfaces are called Hopf tori or
+Bianchi-Pinkall flat tori. Look for the wave-like curve on S² in the
+animations to see a Hopf torus. A circular arc on S² creates a Hopf
+band on S³. The Hopf band is a Seifert surface of the Hopf link that
+forms the boundaries of the Hopf band.
+
+Inspired by Niles Johnson's visualization of the Hopf fibration
+(https://nilesjohnson.net/hopf.html).
+
+https://en.wikipedia.org/wiki/Hopf_fibration
+https://en.wikipedia.org/wiki/Hopf_link
+https://en.wikipedia.org/wiki/Clifford_torus
+https://en.wikipedia.org/wiki/Seifert_surface
+https://en.wikipedia.org/wiki/Dupin_cyclide
+https://en.wikipedia.org/wiki/3-sphere
+
+Written by Carsten Steger; 2025.
+  </_description>
+</screensaver>
diff --git a/hacks/config/klondike.xml b/hacks/config/klondike.xml
new file mode 100644 (file)
index 0000000..e74d221
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<screensaver name="klondike" _label="Klondike" gl="yes">
+
+  <command arg="--root"/>
+
+  <video href="https://www.youtube.com/watch?v=9F5YBnepT4Y"/>
+
+  <hgroup>
+   <vgroup>
+
+    <number id="delay" type="slider" arg="--delay %"
+              _label="Frame rate" _low-label="Low" _high-label="High"
+              low="0" high="100000" default="30000"
+              convert="invert"/>
+
+    <number id="animation_speed" type="slider" arg="--speed %"
+            _label="Animation Speed" _low-label="Fast" _high-label="Slow"
+            low="15" high="200" default="60"/>
+
+    <number id="camera_speed" type="slider" arg="--camera_speed %"
+            _label="Camera Speed" _low-label="Slow" _high-label="Fast"
+            low="10" high="100" default="50"/>
+
+   </vgroup>
+   <vgroup>
+
+    <select id="draw_count">
+      <option id="3"  _label="Deal 3 cards to waste pile"/>
+      <option id="1"  _label="Deal 1 card to waste pile" arg-set="--draw 1"/>
+    </select>
+
+    <hgroup>
+    <boolean id="sloppy" _label="Sloppy card placement" arg-unset="--no-sloppy"/>
+    </hgroup>
+
+    <hgroup>
+     <boolean id="showfps" _label="Show frame rate" arg-set="--fps"/>
+    </hgroup>
+
+    <xscreensaver-updater />
+   </vgroup>
+  </hgroup>
+
+  <_description>
+The screen saver plays solitaire.
+
+Written by Joshua Timmons; 2024.
+  </_description>
+</screensaver>
diff --git a/hacks/config/platonicfolding.xml b/hacks/config/platonicfolding.xml
new file mode 100644 (file)
index 0000000..5b59c49
--- /dev/null
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<screensaver name="platonicfolding" _label="Platonic Folding" gl="yes">
+
+  <video href="https://www.youtube.com/watch?v=c5gUsXvRkIs"/>
+
+  <command arg="--root"/>
+
+  <hgroup>
+
+    <boolean id="rotate" _label="Rotate" arg-unset="--no-rotate"/>
+
+    <select id="colors">
+      <option id="random" _label="Random coloration"/>
+      <option id="face"   _label="Face colors"  arg-set="--colors face"/>
+      <option id="earth"  _label="Earth colors" arg-set="--colors earth"/>
+    </select>
+
+    <select id="foldings">
+      <option id="random" _label="Random number of foldings"/>
+      <option id="1"      _label="1 folding"   arg-set="--foldings 1"/>
+      <option id="2"      _label="2 foldings"  arg-set="--foldings 2"/>
+      <option id="3"      _label="3 foldings"  arg-set="--foldings 3"/>
+      <option id="4"      _label="4 foldings"  arg-set="--foldings 4"/>
+      <option id="5"      _label="5 foldings"  arg-set="--foldings 5"/>
+      <option id="6"      _label="6 foldings"  arg-set="--foldings 6"/>
+      <option id="7"      _label="7 foldings"  arg-set="--foldings 7"/>
+      <option id="8"      _label="8 foldings"  arg-set="--foldings 8"/>
+      <option id="9"      _label="9 foldings"  arg-set="--foldings 9"/>
+      <option id="10"     _label="10 foldings" arg-set="--foldings 10"/>
+      <option id="11"     _label="11 foldings" arg-set="--foldings 11"/>
+      <option id="12"     _label="12 foldings" arg-set="--foldings 12"/>
+      <option id="13"     _label="13 foldings" arg-set="--foldings 13"/>
+      <option id="14"     _label="14 foldings" arg-set="--foldings 14"/>
+      <option id="15"     _label="15 foldings" arg-set="--foldings 15"/>
+      <option id="16"     _label="16 foldings" arg-set="--foldings 16"/>
+      <option id="17"     _label="17 foldings" arg-set="--foldings 17"/>
+      <option id="18"     _label="18 foldings" arg-set="--foldings 18"/>
+      <option id="19"     _label="19 foldings" arg-set="--foldings 19"/>
+      <option id="20"     _label="20 foldings" arg-set="--foldings 20"/>
+    </select>
+  </hgroup>
+
+  <hgroup>
+    <boolean id="showfps" _label="Show frame rate" arg-set="--fps"/>
+    <number id="delay" type="slider" arg="--delay %"
+            _label="Frame rate" _low-label="Low" _high-label="High"
+            low="0" high="100000" default="25000"
+            convert="invert"/>
+  </hgroup>
+  <xscreensaver-updater />
+
+  <_description>
+The unfolding and folding of the Platonic solids.
+
+For the five Platonic solids (the tetrahedron, cube, octahedron,
+dodecahedron, and icosahedron), all unfoldings of its faces are
+non-overlapping: they form a net. The tetrahedron has 16 unfoldings,
+of which two are essentially different (non-isomorphic), the cube and
+octahedron each have 384 unfoldings, of which eleven are
+non-isomorphic, and the dodecahedron and icosahedron each have
+5,184,000 unfoldings, of which 43,380 are non-isomorphic. This program
+displays randomly selected unfoldings for the five Platonic solids.
+The faces of the Platonic solids are either folded jointly or
+successively. Note that while it is guaranteed that the nets of the
+Platonic solids are non-overlapping, their faces may occasionally
+intersect during the unfolding and folding.
+
+https://en.wikipedia.org/wiki/Platonic_solid
+https://en.wikipedia.org/wiki/Net_(polyhedron)
+
+Written by Carsten Steger; 2025.
+  </_description>
+</screensaver>
index 74061061e2f664b32995b98cb313255f56beb2f5..db74a4f1d83aad223cc0e6c129f04a4bfda47a80 100644 (file)
@@ -23,8 +23,9 @@
      <option id="ssh" _label="Ping known SSH hosts"
   arg-set="--ping /etc/hosts,$HOME/.ssh/known_hosts,$HOME/.ssh/known_hosts2"/>
 
-     <option id="popular" _label="Ping Google, Facebook, etc."
-       arg-set="--ping google.com,facebook.com,twitter.com,yahoo.com,flickr.com,www.apple.com,wikipedia.org,linux.org,youtube.com,disqus.com,blogger.com,wordpress.com,tumblr.com,whitehouse.gov"/>
+     <!-- Most popular sites, as per Wikipedia 2025. -->
+     <option id="popular" _label="Ping popular web sites"
+       arg-set="--ping google.com,youtube.com,facebook.com,instagram.com,wikipedia.org,reddit.com,amazon.com,yandex.ru,baidu.com,tiktok.com,pornhub.com,apple.com,kernel.org,mastodon.social,nsa.gov"/>
 
      <option id="sim" _label="Simulation (don't ping)"
         arg-set="--ping simulation"/>
index 7520cb38a94c69f629961edb6e6953e50d2741b0..c3e94153a309233141e3b8e05105d6b995e0e332 100644 (file)
@@ -1,4 +1,4 @@
-/* ffmpeg-out, Copyright © 2023-2024 Jamie Zawinski <jwz@jwz.org>
+/* ffmpeg-out, Copyright © 2023-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
 # pragma GCC diagnostic ignored "-Wpragmas"
 # pragma GCC diagnostic ignored "-Wc99-extensions"
 # pragma GCC diagnostic ignored "-Wlong-long"
-  /* No "diagnostic pop" because some macrose use c99 features. */
+  /* No "diagnostic pop" because some macros use c99 features. */
 #endif
 
 #include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/avutil.h>
 #include <libswscale/swscale.h>
 #include <libswresample/swresample.h>
 
 # define HAVE_CH_LAYOUT
 #endif
 
+#if (LIBAVCODEC_VERSION_INT >= ((61<<16) | (12<<8) | 100))
+# define HAVE_AVCODEC_GET_SUPPORTED_CONFIG
+#endif
 
 struct av_stream {
-  AVCodec *codec;
+  const AVCodec *codec;
   AVStream *st;
   AVCodecContext *ctx;
   AVFrame *frame;
@@ -116,7 +121,7 @@ write_frame (AVFormatContext *oc, struct av_stream *ost)
 
 #ifdef HAVE_CH_LAYOUT
 
-AVChannelLayout
+static AVChannelLayout
 guess_channel_layout (int channels)
 {
   return (channels <= 1 ? (AVChannelLayout)AV_CHANNEL_LAYOUT_MONO : (AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO);
@@ -133,6 +138,85 @@ guess_channel_layout (int channels)
 #endif  /* !HAVE_CH_LAYOUT */
 
 
+#ifdef HAVE_AVCODEC_GET_SUPPORTED_CONFIG
+
+static const enum AVSampleFormat *
+get_sample_formats (struct av_stream *st)
+{
+  const enum AVSampleFormat *fmts;
+  av_check (avcodec_get_supported_config (st->ctx,
+                                          st->codec,
+                                          AV_CODEC_CONFIG_SAMPLE_FORMAT, 0,
+                                          (const void **) &fmts,
+                                          NULL));
+  return fmts;
+}
+
+static const int *
+get_sample_rates (struct av_stream *st)
+{
+  const int *rates;
+  av_check (avcodec_get_supported_config (st->ctx,
+                                          st->codec,
+                                          AV_CODEC_CONFIG_SAMPLE_RATE, 0,
+                                          (const void **) &rates,
+                                          NULL));
+  return rates;
+}
+
+/*
+static const AVChannelLayout *
+get_channel_layouts (struct av_stream *st)
+{
+  const AVChannelLayout *layouts;
+  av_check (avcodec_get_supported_config (st->ctx,
+                                          st->codec,
+                                          AV_CODEC_CONFIG_CHANNEL_LAYOUT, 0,
+                                          (const void **) &layouts,
+                                          NULL));
+  return layouts;
+}
+*/
+
+#else  /* !HAVE_AVCODEC_GET_SUPPORTED_CONFIG */
+
+static const enum AVSampleFormat *
+get_sample_formats (struct av_stream *st)
+{
+  return st->codec->sample_fmts;
+}
+
+static const int *
+get_sample_rates (struct av_stream *st)
+{
+  return st->codec->supported_samplerates;
+}
+
+# ifdef HAVE_CH_LAYOUT
+
+/*
+static const AVChannelLayout *
+get_channel_layouts (struct av_stream *st)
+{
+  return st->codec->ch_layouts;
+}
+*/
+
+# else  /* !HAVE_CH_LAYOUT */
+
+/*
+static const int *
+get_channel_layouts (struct av_stream *st)
+{
+  return st->codec->channel_layouts;
+}
+*/
+
+# endif  /* !HAVE_CH_LAYOUT */
+
+#endif  /* !HAVE_AVCODEC_GET_SUPPORTED_CONFIG */
+
+
 static void
 get_audio_frame (AVFormatContext *audio_fmt_ctx,
                  struct av_stream *audio_ist, AVPacket *audio_pkt,
@@ -291,6 +375,13 @@ ffmpeg_out_init (const char *outfile, const char *audiofile,
 
   const enum AVCodecID video_codec = AV_CODEC_ID_H264;
   const enum AVCodecID audio_codec = AV_CODEC_ID_AAC;
+  const enum AVSampleFormat *sample_fmts = NULL;
+  const int *sample_rates = NULL;
+# ifdef HAVE_CH_LAYOUT
+  const AVChannelLayout *channel_layouts = NULL;
+# else
+  const int *channel_layouts = NULL;
+# endif
   const enum AVPixelFormat pix_fmt = AV_PIX_FMT_YUV420P;
   const unsigned framerate = 30;
   int audio_bitrate = 96000;
@@ -356,19 +447,22 @@ ffmpeg_out_init (const char *outfile, const char *audiofile,
                                NULL));
 
       add_stream (&ffst->audio_ost, ffst->oc, audio_codec);
+
+      sample_fmts = get_sample_formats (&ffst->audio_ost);
       ffst->audio_ost.ctx->sample_fmt =
-        (ffst->audio_ost.codec->sample_fmts
-         ? ffst->audio_ost.codec->sample_fmts[0]
+        (sample_fmts && sample_fmts[0] != AV_SAMPLE_FMT_NONE
+         ? sample_fmts[0]
          : AV_SAMPLE_FMT_FLTP);
       ffst->audio_ost.ctx->bit_rate = audio_bitrate;
 
-      if (! ffst->audio_ost.codec->supported_samplerates)
+      sample_rates = get_sample_rates (&ffst->audio_ost);
+      if (! sample_rates || sample_rates[0] == 0)
         {
           ffst->audio_ost.ctx->sample_rate = ffst->audio_ist.ctx->sample_rate;
         }
       else
         {
-          const int *r = ffst->audio_ost.codec->supported_samplerates;
+          const int *r = sample_rates;
           int best = *r;
           ++r;
 
@@ -384,7 +478,7 @@ ffmpeg_out_init (const char *outfile, const char *audiofile,
         }
 
 # ifdef HAVE_CH_LAYOUT
-      if (! ffst->audio_ost.codec->ch_layouts)
+      if (! channel_layouts || ! av_channel_layout_check(&channel_layouts[0]))
         {
           if (! av_channel_layout_check(&ffst->audio_ist.ctx->ch_layout))
             {
@@ -400,7 +494,7 @@ ffmpeg_out_init (const char *outfile, const char *audiofile,
       else
         {
           /* XXX: This may or may not work. With AAC, it doesn't matter. */
-          const AVChannelLayout *c = ffst->audio_ost.codec->ch_layouts;
+          const AVChannelLayout *c = channel_layouts;
           uint64_t best_lost =
             av_popcount64 (ffst->audio_ost.ctx->ch_layout.u.mask);
           uint64_t best_added = 0;
@@ -427,7 +521,7 @@ ffmpeg_out_init (const char *outfile, const char *audiofile,
           av_channel_layout_from_mask (&ffst->audio_ost.ctx->ch_layout, best);
         }
 # else   /* !HAVE_CH_LAYOUT */
-      if (! ffst->audio_ost.codec->channel_layouts)
+      if (! channel_layouts || ! channel_layouts[0])
         {
           if (! ffst->audio_ist.ctx->channel_layout)
             {
@@ -443,7 +537,7 @@ ffmpeg_out_init (const char *outfile, const char *audiofile,
       else
         {
           /* XXX: This may or may not work. With AAC, it doesn't matter. */
-          const uint64_t *c = ffst->audio_ost.codec->channel_layouts;
+          const uint64_t *c = channel_layouts;
           unsigned int best_lost =
             av_popcount64 (ffst->audio_ost.ctx->channel_layout);
           unsigned int best_added = 0;
@@ -509,13 +603,13 @@ ffmpeg_out_init (const char *outfile, const char *audiofile,
       open_stream (&ffst->audio_ost, NULL);
 
       ffst->audio_ost.frame->format = ffst->audio_ost.ctx->sample_fmt;
-#ifdef HAVE_CH_LAYOUT
+# ifdef HAVE_CH_LAYOUT
       av_channel_layout_copy(&ffst->audio_ost.frame->ch_layout,
         &ffst->audio_ost.ctx->ch_layout);
-#else   /* !HAVE_CH_LAYOUT */
+# else   /* !HAVE_CH_LAYOUT */
       ffst->audio_ost.frame->channel_layout =
         ffst->audio_ost.ctx->channel_layout;
-#endif  /* !HAVE_CH_LAYOUT */
+# endif  /* !HAVE_CH_LAYOUT */
         ffst->audio_ost.frame->sample_rate = ffst->audio_ost.ctx->sample_rate;
       ffst->audio_ost.frame->nb_samples =
         (ffst->audio_ost.ctx->codec->capabilities &
index 99cc75bf75f0478c8d8190b72916bc5fa702b3af..8fee19b64f3fda14dfc0a17f34910d1b096fcf36 100644 (file)
@@ -1,4 +1,4 @@
-# hacks/glx/Makefile.in --- xscreensaver, Copyright © 1999-2024 Jamie Zawinski.
+# hacks/glx/Makefile.in --- xscreensaver, Copyright © 1999-2025 Jamie Zawinski.
 # the `../../configure' script generates `hacks/glx/Makefile' from this file.
 
 @SET_MAKE@
@@ -148,7 +148,9 @@ SRCS                = xscreensaver-gl-visual.c normals.c erase-gl.c fps-gl.c \
                  mapscroller.c squirtorus.c nakagin.c chompytower.c \
                  teeth_model.c hextrail.c papercube.c cubocteversion.c \
                  skulloop.c kallisti.c kallisti_model.c highvoltage.c \
-                 highvoltage_model.c
+                 highvoltage_model.c hopffibration.c hopfanimations.c \
+                 platonicfolding.c klondike.c klondike-game.c dumpsterfire.c \
+                 dumpster_model.c
 
 SCRIPTS                = mapscroller.pl
 
@@ -205,7 +207,8 @@ OBJS                = xscreensaver-gl-visual.o normals.o erase-gl.o fps-gl.o \
                  mapscroller.o squirtorus.o nakagin.o chompytower.o \
                  teeth_model.o hextrail.o papercube.o cubocteversion.o \
                  skulloop.o kallisti.o kallisti_model.o highvoltage.o \
-                 highvoltage_model.o
+                 highvoltage_model.o platonicfolding.o klondike.o \
+                 klondike-game.o dumpsterfire.o 
 
 GL_EXES                = cage gears moebius pipes sproingies stairs superquadrics \
                  morph3d rubik atlantis lament bubble3d glplanet pulsar \
@@ -229,7 +232,8 @@ GL_EXES             = cage gears moebius pipes sproingies stairs superquadrics \
                  maze3d handsy gravitywell deepstars gibson etruscanvenus \
                  sphereeversion covid19 headroom beats mapscroller \
                  squirtorus nakagin chompytower hextrail papercube \
-                 cubocteversion skulloop kallisti highvoltage
+                 cubocteversion skulloop kallisti highvoltage hopffibration \
+                 platonicfolding klondike dumpsterfire
 GLE_EXES       = extrusion
 SUID_EXES      = sonar
 SETCAP_EXES    = sonar
@@ -274,7 +278,8 @@ HDRS                = atlantis.h bubble3d.h buildlwo.h e_textures.h \
                  glschool_alg.h topblock.h involute.h teapot.h sonar.h \
                  dropshadow.h starwars.h teapot2.h dnapizza.h curlicue.h \
                  quickhull.h dymaxionmap-coords.h handsy_anim.h \
-                 glsl-utils.h mapcities.h sphereeversion.h
+                 glsl-utils.h mapcities.h sphereeversion.h hopfanimations.h \
+                 klondike-game.h
 GL_MEN         = xscreensaver-gl-visual.man \
                  atlantis.man boxed.man bubble3d.man cage.man circuit.man \
                  cubenetic.man dangerball.man engine.man extrusion.man \
@@ -309,7 +314,8 @@ GL_MEN              = xscreensaver-gl-visual.man \
                  sphereeversion.man covid19.man headroom.man beats.man \
                  mapscroller.man squirtorus.man nakagin.man chompytower.man \
                  hextrail.man papercube.man cubocteversion.man skulloop.man \
-                 kallisti.man highvoltage.man
+                 kallisti.man highvoltage.man hopffibration.man \
+                 platonicfolding.man
 MEN            = @GL_MEN@
 RETIRED_MEN    = glforestfire.man
 EXTRAS         = README Makefile.in dxf2gl.pl vrml2gl.pl wfront2gl.pl \
@@ -559,7 +565,7 @@ check_men:
         fi
 
 validate_xml:
-       @cd $(HACK_SRC) && ./check-configs.pl $(EXES) $(RETIRED_EXES)
+       @cd $(HACK_SRC) && ./check-configs.pl --force $(EXES) $(RETIRED_EXES)
 
 distdepend:: check_men validate_xml
 
@@ -988,6 +994,9 @@ sonar-icmp.o: sonar-icmp.c
        $(CC) -c $(HACK_CFLAGS_BASE) $(THREAD_CFLAGS) $<
 sonar:         sonar.o         $(SONAR_OBJS)
        $(CC_HACK) -o $@ $@.o   $(THREAD_CFLAGS) $(SONAR_OBJS) $(LIBCAP_LIBS) $(THREAD_LIBS) $(HACK_LIBS)
+       @if [ "$$USER" = jwz ]; then \
+         set -x ; sudo chown root $@ ; sudo chmod u+s $@ ; \
+       fi
 
 JIGSAW_OBJS=normals.o $(UTILS_BIN)/spline.o $(HACK_TRACK_GRAB_OBJS)
 jigsaw:                jigsaw.o        $(JIGSAW_OBJS)
@@ -1177,6 +1186,24 @@ HIGHVOLTAGE=highvoltage.o highvoltage_model.o gllist.o tube.o \
 highvoltage:                   $(HIGHVOLTAGE)
        $(CC_HACK) -o $@        $(HIGHVOLTAGE) $(HACK_LIBS)
 
+HOPFFIBRATION_OBJS=hopfanimations.o $(HACK_TRACK_OBJS)
+hopffibration: hopffibration.o $(HOPFFIBRATION_OBJS)
+       $(CC_HACK) -o $@ $@.o    $(HOPFFIBRATION_OBJS) $(HACK_LIBS)
+
+PLATONIC_OBJS=$(PNG) $(HACK_TRACK_OBJS)
+platonicfolding: platonicfolding.o $(PLATONIC_OBJS)
+       $(CC_HACK) -o $@ $@.o      $(PLATONIC_OBJS) $(HACK_LIBS) $(PNG_LIBS)
+
+KLONDIKE_OBJS=$(PNG) $(HACK_TRACK_OBJS) klondike-game.o
+klondike:      klondike.o $(KLONDIKE_OBJS)
+       $(CC_HACK) -o $@ $@.o   $(KLONDIKE_OBJS) $(HACK_LIBS) $(PNG_LIBS)
+
+dumpster_dxf::
+       ./dxf2gl.pl --smooth --layers --normalize dumpster.dxf dumpster_model.c
+DUMPSTERFIRE=dumpsterfire.o dumpster_model.o gllist.o $(HACK_TRACK_OBJS)
+dumpsterfire:                  $(DUMPSTERFIRE)
+       $(CC_HACK) -o $@        $(DUMPSTERFIRE) $(HACK_LIBS)
+
 
 ##############################################################################
 #
@@ -2052,6 +2079,25 @@ dropshadow.o: $(UTILS_SRC)/usleep.h
 dropshadow.o: $(UTILS_SRC)/visual.h
 dropshadow.o: $(UTILS_SRC)/xft.h
 dropshadow.o: $(UTILS_SRC)/yarandom.h
+dumpsterfire.o: ../../config.h
+dumpsterfire.o: $(HACK_SRC)/fps.h
+dumpsterfire.o: $(srcdir)/gllist.h
+dumpsterfire.o: $(srcdir)/gltrackball.h
+dumpsterfire.o: $(HACK_SRC)/recanim.h
+dumpsterfire.o: $(srcdir)/rotator.h
+dumpsterfire.o: $(HACK_SRC)/screenhackI.h
+dumpsterfire.o: $(UTILS_SRC)/colors.h
+dumpsterfire.o: $(UTILS_SRC)/erase.h
+dumpsterfire.o: $(UTILS_SRC)/font-retry.h
+dumpsterfire.o: $(UTILS_SRC)/grabclient.h
+dumpsterfire.o: $(UTILS_SRC)/hsv.h
+dumpsterfire.o: $(UTILS_SRC)/resources.h
+dumpsterfire.o: $(UTILS_SRC)/usleep.h
+dumpsterfire.o: $(UTILS_SRC)/visual.h
+dumpsterfire.o: $(UTILS_SRC)/xft.h
+dumpsterfire.o: $(UTILS_SRC)/yarandom.h
+dumpsterfire.o: $(HACK_SRC)/xlockmoreI.h
+dumpsterfire.o: $(HACK_SRC)/xlockmore.h
 dymaxionmap-coords.o: ../../config.h
 dymaxionmap-coords.o: $(srcdir)/dymaxionmap-coords.h
 dymaxionmap.o: ../../config.h
@@ -3117,6 +3163,26 @@ hilbert.o: $(UTILS_SRC)/xft.h
 hilbert.o: $(UTILS_SRC)/yarandom.h
 hilbert.o: $(HACK_SRC)/xlockmoreI.h
 hilbert.o: $(HACK_SRC)/xlockmore.h
+hopfanimations.o: $(srcdir)/hopfanimations.h
+hopffibration.o: ../../config.h
+hopffibration.o: $(HACK_SRC)/fps.h
+hopffibration.o: $(srcdir)/glsl-utils.h
+hopffibration.o: $(srcdir)/gltrackball.h
+hopffibration.o: $(srcdir)/hopfanimations.h
+hopffibration.o: $(HACK_SRC)/recanim.h
+hopffibration.o: $(HACK_SRC)/screenhackI.h
+hopffibration.o: $(UTILS_SRC)/colors.h
+hopffibration.o: $(UTILS_SRC)/erase.h
+hopffibration.o: $(UTILS_SRC)/font-retry.h
+hopffibration.o: $(UTILS_SRC)/grabclient.h
+hopffibration.o: $(UTILS_SRC)/hsv.h
+hopffibration.o: $(UTILS_SRC)/resources.h
+hopffibration.o: $(UTILS_SRC)/usleep.h
+hopffibration.o: $(UTILS_SRC)/visual.h
+hopffibration.o: $(UTILS_SRC)/xft.h
+hopffibration.o: $(UTILS_SRC)/yarandom.h
+hopffibration.o: $(HACK_SRC)/xlockmoreI.h
+hopffibration.o: $(HACK_SRC)/xlockmore.h
 hydrostat.o: ../../config.h
 hydrostat.o: $(HACK_SRC)/fps.h
 hydrostat.o: $(srcdir)/gltrackball.h
@@ -3321,6 +3387,96 @@ klein.o: $(UTILS_SRC)/xft.h
 klein.o: $(UTILS_SRC)/yarandom.h
 klein.o: $(HACK_SRC)/xlockmoreI.h
 klein.o: $(HACK_SRC)/xlockmore.h
+klondike-game.o: ../../config.h
+klondike-game.o: $(HACK_SRC)/fps.h
+klondike-game.o: $(srcdir)/gltrackball.h
+klondike-game.o: $(srcdir)/klondike-game.h
+klondike-game.o: $(HACK_SRC)/recanim.h
+klondike-game.o: $(HACK_SRC)/screenhackI.h
+klondike-game.o: $(UTILS_SRC)/colors.h
+klondike-game.o: $(UTILS_SRC)/erase.h
+klondike-game.o: $(UTILS_SRC)/font-retry.h
+klondike-game.o: $(UTILS_SRC)/grabclient.h
+klondike-game.o: $(UTILS_SRC)/hsv.h
+klondike-game.o: $(UTILS_SRC)/resources.h
+klondike-game.o: $(UTILS_SRC)/usleep.h
+klondike-game.o: $(UTILS_SRC)/visual.h
+klondike-game.o: $(UTILS_SRC)/xft.h
+klondike-game.o: $(UTILS_SRC)/yarandom.h
+klondike-game.o: $(HACK_SRC)/xlockmoreI.h
+klondike-game.o: $(HACK_SRC)/xlockmore.h
+klondike.o: ../../config.h
+klondike.o: $(HACK_SRC)/fps.h
+klondike.o: $(srcdir)/gltrackball.h
+klondike.o: ../images/gen/C2_png.h
+klondike.o: ../images/gen/C3_png.h
+klondike.o: ../images/gen/C4_png.h
+klondike.o: ../images/gen/C5_png.h
+klondike.o: ../images/gen/C6_png.h
+klondike.o: ../images/gen/C7_png.h
+klondike.o: ../images/gen/C8_png.h
+klondike.o: ../images/gen/C9_png.h
+klondike.o: ../images/gen/CA_png.h
+klondike.o: ../images/gen/CJ_png.h
+klondike.o: ../images/gen/CK_png.h
+klondike.o: ../images/gen/CQ_png.h
+klondike.o: ../images/gen/CT_png.h
+klondike.o: ../images/gen/D2_png.h
+klondike.o: ../images/gen/D3_png.h
+klondike.o: ../images/gen/D4_png.h
+klondike.o: ../images/gen/D5_png.h
+klondike.o: ../images/gen/D6_png.h
+klondike.o: ../images/gen/D7_png.h
+klondike.o: ../images/gen/D8_png.h
+klondike.o: ../images/gen/D9_png.h
+klondike.o: ../images/gen/DA_png.h
+klondike.o: ../images/gen/DJ_png.h
+klondike.o: ../images/gen/DK_png.h
+klondike.o: ../images/gen/DQ_png.h
+klondike.o: ../images/gen/DT_png.h
+klondike.o: ../images/gen/H2_png.h
+klondike.o: ../images/gen/H3_png.h
+klondike.o: ../images/gen/H4_png.h
+klondike.o: ../images/gen/H5_png.h
+klondike.o: ../images/gen/H6_png.h
+klondike.o: ../images/gen/H7_png.h
+klondike.o: ../images/gen/H8_png.h
+klondike.o: ../images/gen/H9_png.h
+klondike.o: ../images/gen/HA_png.h
+klondike.o: ../images/gen/HJ_png.h
+klondike.o: ../images/gen/HK_png.h
+klondike.o: ../images/gen/HQ_png.h
+klondike.o: ../images/gen/HT_png.h
+klondike.o: ../images/gen/S2_png.h
+klondike.o: ../images/gen/S3_png.h
+klondike.o: ../images/gen/S4_png.h
+klondike.o: ../images/gen/S5_png.h
+klondike.o: ../images/gen/S6_png.h
+klondike.o: ../images/gen/S7_png.h
+klondike.o: ../images/gen/S8_png.h
+klondike.o: ../images/gen/S9_png.h
+klondike.o: ../images/gen/SA_png.h
+klondike.o: ../images/gen/SJ_png.h
+klondike.o: ../images/gen/SK_png.h
+klondike.o: ../images/gen/SQ_png.h
+klondike.o: ../images/gen/ST_png.h
+klondike.o: ../images/gen/back_png.h
+klondike.o: $(srcdir)/klondike-game.h
+klondike.o: $(HACK_SRC)/recanim.h
+klondike.o: $(HACK_SRC)/screenhackI.h
+klondike.o: $(UTILS_SRC)/colors.h
+klondike.o: $(UTILS_SRC)/erase.h
+klondike.o: $(UTILS_SRC)/font-retry.h
+klondike.o: $(UTILS_SRC)/grabclient.h
+klondike.o: $(UTILS_SRC)/hsv.h
+klondike.o: $(UTILS_SRC)/resources.h
+klondike.o: $(UTILS_SRC)/usleep.h
+klondike.o: $(UTILS_SRC)/visual.h
+klondike.o: $(UTILS_SRC)/xft.h
+klondike.o: $(UTILS_SRC)/yarandom.h
+klondike.o: $(HACK_SRC)/ximage-loader.h
+klondike.o: $(HACK_SRC)/xlockmoreI.h
+klondike.o: $(HACK_SRC)/xlockmore.h
 lament_model.o: ../../config.h
 lament_model.o: $(HACK_SRC)/fps.h
 lament_model.o: $(srcdir)/gllist.h
@@ -3730,6 +3886,28 @@ pipes.o: $(UTILS_SRC)/xft.h
 pipes.o: $(UTILS_SRC)/yarandom.h
 pipes.o: $(HACK_SRC)/xlockmoreI.h
 pipes.o: $(HACK_SRC)/xlockmore.h
+platonicfolding.o: ../../config.h
+platonicfolding.o: $(HACK_SRC)/fps.h
+platonicfolding.o: $(srcdir)/glsl-utils.h
+platonicfolding.o: $(srcdir)/gltrackball.h
+platonicfolding.o: ../images/gen/earth_night_png.h
+platonicfolding.o: ../images/gen/earth_png.h
+platonicfolding.o: ../images/gen/earth_water_png.h
+platonicfolding.o: $(HACK_SRC)/recanim.h
+platonicfolding.o: $(HACK_SRC)/screenhackI.h
+platonicfolding.o: $(UTILS_SRC)/colors.h
+platonicfolding.o: $(UTILS_SRC)/erase.h
+platonicfolding.o: $(UTILS_SRC)/font-retry.h
+platonicfolding.o: $(UTILS_SRC)/grabclient.h
+platonicfolding.o: $(UTILS_SRC)/hsv.h
+platonicfolding.o: $(UTILS_SRC)/resources.h
+platonicfolding.o: $(UTILS_SRC)/usleep.h
+platonicfolding.o: $(UTILS_SRC)/visual.h
+platonicfolding.o: $(UTILS_SRC)/xft.h
+platonicfolding.o: $(UTILS_SRC)/yarandom.h
+platonicfolding.o: $(HACK_SRC)/ximage-loader.h
+platonicfolding.o: $(HACK_SRC)/xlockmoreI.h
+platonicfolding.o: $(HACK_SRC)/xlockmore.h
 polyhedra-gl.o: ../../config.h
 polyhedra-gl.o: $(HACK_SRC)/fps.h
 polyhedra-gl.o: $(srcdir)/gltrackball.h
index 4f2f7c4675853f8a8e7cc347ec71aa0986d6f93f..de4c1d10e500c0bfbdcfb5cbc1fc6c691de4ab92 100644 (file)
@@ -7,7 +7,7 @@
 static const char sccsid[] = "@(#)cubocteversion.c  1.1 23/03/02 xlockmore";
 #endif
 
-/* Copyright (c) 2023-2023 Carsten Steger <carsten@mirsanmir.org>. */
+/* Copyright (c) 2023-2025 Carsten Steger <carsten@mirsanmir.org>. */
 
 /*
  * Permission to use, copy, modify, and distribute this software and its
@@ -288,8 +288,9 @@ typedef struct {
   GLuint self_tri_num[NUMU*3*NUM_FACE+3*NUM_FACE];
   /* The number of triangle strips in the self-intersection tubes */
   GLuint self_tri_strips;
+  Bool use_shaders;
 #ifdef HAVE_GLSL
-  Bool use_shaders, use_mipmaps;
+  Bool use_mipmaps;
   /* The precomputed texture coordinates of the cuboctahedron */
   GLfloat tex[3*3*NUM_FACE];
   /* The textures of earth by day (index 0), earth by night (index 1), and
@@ -1268,7 +1269,8 @@ static const GLchar *shader_version_3_0 =
 static const GLchar *shader_version_3_0_es =
   "#version 300 es\n"
   "precision highp float;\n"
-  "precision highp int;\n";
+  "precision highp int;\n"
+  "precision highp sampler2D;\n";
 
 
 /* The vertex shader code is composed of code fragments that depend on
@@ -3341,7 +3343,8 @@ static int cuboctahedron_eversion_ff(ModeInfo *mi)
 
 #ifdef HAVE_GLSL
 
-/* Draw the sphere eversion using OpenGL's programmable functionality. */
+/* Draw the cuboctahedron eversion using OpenGL's programmable
+   functionality. */
 static int cuboctahedron_eversion_pf(ModeInfo *mi)
 {
   cubocteversionstruct *ce = &cubocteversion[MI_SCREEN(mi)];
@@ -4692,7 +4695,7 @@ static void init_glsl(ModeInfo *mi)
      1.0f,  1.0f,
   };
 
-  /* Determine whether to use shaders to render the sphere eversion. */
+  /* Determine whether to use shaders to render the cuboctahedron eversion. */
   ce->use_shaders = False;
   ce->use_mipmaps = False;
   ce->poly_shader_program = 0;
@@ -5507,8 +5510,7 @@ static void display_cubocteversion(ModeInfo *mi)
  */
 
 
-ENTRYPOINT void reshape_cubocteversion(ModeInfo *mi,
-                                              int width, int height)
+ENTRYPOINT void reshape_cubocteversion(ModeInfo *mi, int width, int height)
 {
   cubocteversionstruct *ce = &cubocteversion[MI_SCREEN(mi)];
   int y = 0;
@@ -5522,11 +5524,11 @@ ENTRYPOINT void reshape_cubocteversion(ModeInfo *mi,
 
   ce->WindW = (GLint)width;
   ce->WindH = (GLint)height;
-  ce->tex_scale[0] = 1.0f/(GLfloat)width;
-  ce->tex_scale[1] = 1.0f/(GLfloat)height;
   glViewport(0,y,width,height);
   ce->aspect = (GLfloat)width/(GLfloat)height;
 #ifdef HAVE_GLSL
+  ce->tex_scale[0] = 1.0f/(GLfloat)width;
+  ce->tex_scale[1] = 1.0f/(GLfloat)height;
   delete_dual_peeling_render_targets(mi);
   init_dual_peeling_render_targets(mi);
   delete_weighted_average_render_targets(mi);
diff --git a/hacks/glx/dumpster.dxf b/hacks/glx/dumpster.dxf
new file mode 100644 (file)
index 0000000..5584df1
--- /dev/null
@@ -0,0 +1,16278 @@
+ 0
+SECTION
+ 2
+ENTITIES
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+106.95687902348394
+30
+246.98357215579964
+11
+180.96357630756773
+21
+104.99428059828708
+31
+247.50944881889774
+12
+180.96357630756773
+22
+104.99428059828708
+32
+243.58425196850402
+13
+180.96357630756773
+23
+104.99428059828708
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+101.59496041099145
+30
+245.54685039370088
+11
+-180.96357619368754
+21
+101.06908374789336
+31
+243.58425196850402
+12
+180.96357630756773
+22
+101.06908374789336
+32
+243.58425196850402
+13
+180.96357630756773
+23
+101.06908374789336
+33
+243.58425196850402
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+101.06908374789336
+30
+243.58425196850402
+11
+180.96357630756773
+21
+101.59496041099145
+31
+245.54685039370088
+12
+-180.96357619368754
+22
+101.59496041099145
+32
+245.54685039370088
+13
+-180.96357619368754
+23
+101.59496041099145
+33
+245.54685039370088
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+101.59496041099145
+30
+245.54685039370088
+11
+-180.96357619368754
+21
+103.03168217309022
+31
+246.98357215579964
+12
+-180.96357619368754
+22
+101.59496041099145
+32
+245.54685039370088
+13
+-180.96357619368754
+23
+101.59496041099145
+33
+245.54685039370088
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+103.03168217309022
+30
+246.98357215579964
+11
+180.96357630756773
+21
+101.59496041099145
+31
+245.54685039370088
+12
+180.96357630756773
+22
+103.03168217309022
+32
+246.98357215579964
+13
+180.96357630756773
+23
+103.03168217309022
+33
+246.98357215579964
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+103.03168217309022
+30
+246.98357215579964
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+247.50944881889774
+12
+-180.96357619368754
+22
+103.03168217309022
+32
+246.98357215579964
+13
+-180.96357619368754
+23
+103.03168217309022
+33
+246.98357215579964
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+247.50944881889774
+11
+180.96357630756773
+21
+103.03168217309022
+31
+246.98357215579964
+12
+180.96357630756773
+22
+104.99428059828708
+32
+247.50944881889774
+13
+180.96357630756773
+23
+104.99428059828708
+33
+247.50944881889774
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.3936007855827
+30
+241.62165354330716
+11
+-180.96357619368754
+21
+106.95687902348394
+31
+240.1849317812084
+12
+-180.96357619368754
+22
+108.3936007855827
+32
+241.62165354330716
+13
+-180.96357619368754
+23
+108.3936007855827
+33
+241.62165354330716
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+106.95687902348394
+30
+240.1849317812084
+11
+180.96357630756773
+21
+108.3936007855827
+31
+241.62165354330716
+12
+180.96357630756773
+22
+106.95687902348394
+32
+240.1849317812084
+13
+180.96357630756773
+23
+106.95687902348394
+33
+240.1849317812084
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+247.50944881889774
+11
+-180.96357619368754
+21
+106.95687902348394
+31
+246.98357215579964
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+247.50944881889774
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+247.50944881889774
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+106.95687902348394
+30
+246.98357215579964
+11
+180.96357630756773
+21
+104.99428059828708
+31
+247.50944881889774
+12
+180.96357630756773
+22
+106.95687902348394
+32
+246.98357215579964
+13
+180.96357630756773
+23
+106.95687902348394
+33
+246.98357215579964
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+108.9194774486808
+30
+243.58425196850402
+11
+180.96357630756773
+21
+108.3936007855827
+31
+241.62165354330716
+12
+-180.96357619368754
+22
+108.3936007855827
+32
+241.62165354330716
+13
+-180.96357619368754
+23
+108.3936007855827
+33
+241.62165354330716
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.3936007855827
+30
+241.62165354330716
+11
+-180.96357619368754
+21
+108.9194774486808
+31
+243.58425196850402
+12
+180.96357630756773
+22
+108.9194774486808
+32
+243.58425196850402
+13
+180.96357630756773
+23
+108.9194774486808
+33
+243.58425196850402
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+106.95687902348394
+30
+246.98357215579964
+11
+-180.96357619368754
+21
+108.3936007855827
+31
+245.54685039370088
+12
+-180.96357619368754
+22
+106.95687902348394
+32
+246.98357215579964
+13
+-180.96357619368754
+23
+106.95687902348394
+33
+246.98357215579964
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+108.3936007855827
+30
+245.54685039370088
+11
+180.96357630756773
+21
+106.95687902348394
+31
+246.98357215579964
+12
+180.96357630756773
+22
+108.3936007855827
+32
+245.54685039370088
+13
+180.96357630756773
+23
+108.3936007855827
+33
+245.54685039370088
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+108.3936007855827
+30
+245.54685039370088
+11
+180.96357630756773
+21
+108.9194774486808
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+108.9194774486808
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+108.9194774486808
+33
+243.58425196850402
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.9194774486808
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+108.3936007855827
+31
+245.54685039370088
+12
+180.96357630756773
+22
+108.3936007855827
+32
+245.54685039370088
+13
+180.96357630756773
+23
+108.3936007855827
+33
+245.54685039370088
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+106.95687902348394
+30
+240.1849317812084
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+239.6590551181103
+12
+-180.96357619368754
+22
+106.95687902348394
+32
+240.1849317812084
+13
+-180.96357619368754
+23
+106.95687902348394
+33
+240.1849317812084
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+239.6590551181103
+11
+180.96357630756773
+21
+106.95687902348394
+31
+240.1849317812084
+12
+180.96357630756773
+22
+104.99428059828708
+32
+239.6590551181103
+13
+180.96357630756773
+23
+104.99428059828708
+33
+239.6590551181103
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+239.6590551181103
+11
+-180.96357619368754
+21
+103.03168217309022
+31
+240.1849317812084
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+239.6590551181103
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+239.6590551181103
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+103.03168217309022
+30
+240.1849317812084
+11
+180.96357630756773
+21
+104.99428059828708
+31
+239.6590551181103
+12
+180.96357630756773
+22
+103.03168217309022
+32
+240.1849317812084
+13
+180.96357630756773
+23
+103.03168217309022
+33
+240.1849317812084
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+103.03168217309022
+30
+240.1849317812084
+11
+-180.96357619368754
+21
+101.59496041099145
+31
+241.62165354330716
+12
+-180.96357619368754
+22
+103.03168217309022
+32
+240.1849317812084
+13
+-180.96357619368754
+23
+103.03168217309022
+33
+240.1849317812084
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+101.59496041099145
+30
+241.62165354330716
+11
+180.96357630756773
+21
+103.03168217309022
+31
+240.1849317812084
+12
+180.96357630756773
+22
+101.59496041099145
+32
+241.62165354330716
+13
+180.96357630756773
+23
+101.59496041099145
+33
+241.62165354330716
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+101.06908374789336
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+101.59496041099145
+31
+241.62165354330716
+12
+180.96357630756773
+22
+101.59496041099145
+32
+241.62165354330716
+13
+180.96357630756773
+23
+101.59496041099145
+33
+241.62165354330716
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+101.59496041099145
+30
+241.62165354330716
+11
+180.96357630756773
+21
+101.06908374789336
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+101.06908374789336
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+101.06908374789336
+33
+243.58425196850402
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+103.03168217309022
+31
+246.98357215579964
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+247.50944881889774
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+247.50944881889774
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.9194774486808
+30
+243.58425196850402
+11
+180.96357630756773
+21
+104.99428059828708
+31
+243.58425196850402
+12
+180.96357630756773
+22
+108.3936007855827
+32
+241.62165354330716
+13
+180.96357630756773
+23
+108.3936007855827
+33
+241.62165354330716
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+101.59496041099145
+31
+241.62165354330716
+12
+180.96357630756773
+22
+103.03168217309022
+32
+240.1849317812084
+13
+180.96357630756773
+23
+103.03168217309022
+33
+240.1849317812084
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+247.50944881889774
+11
+180.96357630756773
+21
+103.03168217309022
+31
+246.98357215579964
+12
+180.96357630756773
+22
+104.99428059828708
+32
+243.58425196850402
+13
+180.96357630756773
+23
+104.99428059828708
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.9194774486808
+30
+243.58425196850402
+11
+180.96357630756773
+21
+108.3936007855827
+31
+245.54685039370088
+12
+180.96357630756773
+22
+104.99428059828708
+32
+243.58425196850402
+13
+180.96357630756773
+23
+104.99428059828708
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+103.03168217309022
+31
+240.1849317812084
+12
+180.96357630756773
+22
+104.99428059828708
+32
+239.6590551181103
+13
+180.96357630756773
+23
+104.99428059828708
+33
+239.6590551181103
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.3936007855827
+30
+245.54685039370088
+11
+180.96357630756773
+21
+106.95687902348394
+31
+246.98357215579964
+12
+180.96357630756773
+22
+104.99428059828708
+32
+243.58425196850402
+13
+180.96357630756773
+23
+104.99428059828708
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+101.06908374789336
+31
+243.58425196850402
+12
+180.96357630756773
+22
+101.59496041099145
+32
+241.62165354330716
+13
+180.96357630756773
+23
+101.59496041099145
+33
+241.62165354330716
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+106.95687902348394
+30
+240.1849317812084
+11
+180.96357630756773
+21
+104.99428059828708
+31
+243.58425196850402
+12
+180.96357630756773
+22
+104.99428059828708
+32
+239.6590551181103
+13
+180.96357630756773
+23
+104.99428059828708
+33
+239.6590551181103
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+103.03168217309022
+31
+246.98357215579964
+12
+180.96357630756773
+22
+101.59496041099145
+32
+245.54685039370088
+13
+180.96357630756773
+23
+101.59496041099145
+33
+245.54685039370088
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+101.59496041099145
+31
+245.54685039370088
+12
+180.96357630756773
+22
+101.06908374789336
+32
+243.58425196850402
+13
+180.96357630756773
+23
+101.06908374789336
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.3936007855827
+30
+241.62165354330716
+11
+180.96357630756773
+21
+104.99428059828708
+31
+243.58425196850402
+12
+180.96357630756773
+22
+106.95687902348394
+32
+240.1849317812084
+13
+180.96357630756773
+23
+106.95687902348394
+33
+240.1849317812084
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+108.3936007855827
+30
+241.62165354330716
+11
+-180.96357619368754
+21
+106.95687902348394
+31
+240.1849317812084
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+239.6590551181103
+12
+180.96357630756773
+22
+104.99428059828708
+32
+239.6590551181103
+13
+180.96357630756773
+23
+104.99428059828708
+33
+239.6590551181103
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+239.6590551181103
+11
+180.96357630756773
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+243.58425196850402
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+104.99428059828708
+31
+243.58425196850402
+12
+180.96357630756773
+22
+104.99428059828708
+32
+247.50944881889774
+13
+180.96357630756773
+23
+104.99428059828708
+33
+247.50944881889774
+70
+13
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+104.99428059828708
+31
+247.50944881889774
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+247.50944881889774
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+247.50944881889774
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+101.06908374789336
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+101.59496041099145
+32
+245.54685039370088
+13
+-180.96357619368754
+23
+101.59496041099145
+33
+245.54685039370088
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+106.95687902348394
+30
+240.1849317812084
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+239.6590551181103
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+106.95687902348394
+31
+240.1849317812084
+12
+-180.96357619368754
+22
+106.95687902348394
+32
+240.1849317812084
+13
+-180.96357619368754
+23
+106.95687902348394
+33
+240.1849317812084
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+106.95687902348394
+30
+240.1849317812084
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+243.58425196850402
+12
+180.96357630756773
+22
+104.99428059828708
+32
+243.58425196850402
+13
+180.96357630756773
+23
+104.99428059828708
+33
+243.58425196850402
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+103.03168217309022
+32
+246.98357215579964
+13
+-180.96357619368754
+23
+103.03168217309022
+33
+246.98357215579964
+70
+13
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+103.03168217309022
+31
+246.98357215579964
+12
+180.96357630756773
+22
+103.03168217309022
+32
+246.98357215579964
+13
+180.96357630756773
+23
+103.03168217309022
+33
+246.98357215579964
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+106.95687902348394
+30
+246.98357215579964
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+247.50944881889774
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+247.50944881889774
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+101.59496041099145
+31
+245.54685039370088
+12
+-180.96357619368754
+22
+103.03168217309022
+32
+246.98357215579964
+13
+-180.96357619368754
+23
+103.03168217309022
+33
+246.98357215579964
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.3936007855827
+30
+241.62165354330716
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+108.3936007855827
+32
+241.62165354330716
+13
+-180.96357619368754
+23
+108.3936007855827
+33
+241.62165354330716
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+108.3936007855827
+31
+241.62165354330716
+12
+-180.96357619368754
+22
+101.59496041099145
+32
+245.54685039370088
+13
+-180.96357619368754
+23
+101.59496041099145
+33
+245.54685039370088
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+101.59496041099145
+30
+245.54685039370088
+11
+180.96357630756773
+21
+108.3936007855827
+31
+241.62165354330716
+12
+180.96357630756773
+22
+101.59496041099145
+32
+245.54685039370088
+13
+180.96357630756773
+23
+101.59496041099145
+33
+245.54685039370088
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+101.59496041099145
+30
+245.54685039370088
+11
+180.96357630756773
+21
+108.3936007855827
+31
+241.62165354330716
+12
+180.96357630756773
+22
+104.99428059828708
+32
+243.58425196850402
+13
+180.96357630756773
+23
+104.99428059828708
+33
+243.58425196850402
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+101.59496041099145
+31
+241.62165354330716
+12
+-180.96357619368754
+22
+101.06908374789336
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+101.06908374789336
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+108.9194774486808
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+108.3936007855827
+31
+241.62165354330716
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+101.06908374789336
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+101.06908374789336
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+101.06908374789336
+33
+243.58425196850402
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+101.06908374789336
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+108.9194774486808
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+108.9194774486808
+33
+243.58425196850402
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+108.9194774486808
+30
+243.58425196850402
+11
+180.96357630756773
+21
+101.06908374789336
+31
+243.58425196850402
+12
+180.96357630756773
+22
+108.9194774486808
+32
+243.58425196850402
+13
+180.96357630756773
+23
+108.9194774486808
+33
+243.58425196850402
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.9194774486808
+30
+243.58425196850402
+11
+180.96357630756773
+21
+101.06908374789336
+31
+243.58425196850402
+12
+180.96357630756773
+22
+104.99428059828708
+32
+243.58425196850402
+13
+180.96357630756773
+23
+104.99428059828708
+33
+243.58425196850402
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+108.3936007855827
+30
+245.54685039370088
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+106.95687902348394
+32
+246.98357215579964
+13
+-180.96357619368754
+23
+106.95687902348394
+33
+246.98357215579964
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+239.6590551181103
+11
+-180.96357619368754
+21
+103.03168217309022
+31
+240.1849317812084
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+243.58425196850402
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+103.03168217309022
+31
+240.1849317812084
+12
+180.96357630756773
+22
+103.03168217309022
+32
+240.1849317812084
+13
+180.96357630756773
+23
+103.03168217309022
+33
+240.1849317812084
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+103.03168217309022
+30
+240.1849317812084
+11
+180.96357630756773
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+104.99428059828708
+32
+243.58425196850402
+13
+-180.96357619368754
+23
+104.99428059828708
+33
+243.58425196850402
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+104.99428059828708
+31
+243.58425196850402
+12
+180.96357630756773
+22
+106.95687902348394
+32
+246.98357215579964
+13
+180.96357630756773
+23
+106.95687902348394
+33
+246.98357215579964
+70
+13
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+106.95687902348394
+31
+246.98357215579964
+12
+-180.96357619368754
+22
+106.95687902348394
+32
+246.98357215579964
+13
+-180.96357619368754
+23
+106.95687902348394
+33
+246.98357215579964
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+103.03168217309022
+31
+240.1849317812084
+12
+-180.96357619368754
+22
+101.59496041099145
+32
+241.62165354330716
+13
+-180.96357619368754
+23
+101.59496041099145
+33
+241.62165354330716
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+108.9194774486808
+30
+243.58425196850402
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+108.3936007855827
+32
+245.54685039370088
+13
+-180.96357619368754
+23
+108.3936007855827
+33
+245.54685039370088
+70
+0
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+108.3936007855827
+30
+245.54685039370088
+11
+-180.96357619368754
+21
+104.99428059828708
+31
+243.58425196850402
+12
+-180.96357619368754
+22
+108.3936007855827
+32
+245.54685039370088
+13
+-180.96357619368754
+23
+108.3936007855827
+33
+245.54685039370088
+70
+1
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+104.99428059828708
+30
+243.58425196850402
+11
+180.96357630756773
+21
+108.3936007855827
+31
+245.54685039370088
+12
+-180.96357619368754
+22
+101.59496041099145
+32
+241.62165354330716
+13
+-180.96357619368754
+23
+101.59496041099145
+33
+241.62165354330716
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+-180.96357619368754
+20
+101.59496041099145
+30
+241.62165354330716
+11
+180.96357630756773
+21
+108.3936007855827
+31
+245.54685039370088
+12
+180.96357630756773
+22
+101.59496041099145
+32
+241.62165354330716
+13
+180.96357630756773
+23
+101.59496041099145
+33
+241.62165354330716
+70
+3
+  0
+3DFACE
+ 8
+axle
+10
+180.96357630756773
+20
+101.59496041099145
+30
+241.62165354330716
+11
+180.96357630756773
+21
+108.3936007855827
+31
+245.54685039370088
+12
+180.96357630756773
+22
+104.99428059828708
+32
+243.58425196850402
+13
+180.96357630756773
+23
+104.99428059828708
+33
+243.58425196850402
+70
+1
+  0
+LINE
+ 8
+frame_half
+10
+192.98247766291428
+20
+-18.731889129101205
+30
+151.6787401574804
+11
+192.9824776628326
+21
+-18.729150113199566
+31
+151.6790750703387
+  0
+LINE
+ 8
+frame_half
+10
+174.56515482826853
+20
+-18.73188967799561
+30
+151.6787401574804
+11
+174.56515482818696
+21
+-18.72915066209397
+31
+151.6790750703387
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680488186044
+20
+98.36023622047261
+30
+0.3216681102362128
+11
+5.115907697472721e-13
+21
+-98.36024133728131
+31
+0.32163719118833317
+12
+5.115907697472721e-13
+22
+98.36023110366378
+32
+0.321666641223282
+13
+5.115907697472721e-13
+23
+98.36023110366378
+33
+0.321666641223282
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+-98.36024133728131
+30
+0.32163719118833317
+11
+171.68680488186044
+21
+98.36023622047261
+31
+0.3216681102362128
+12
+171.68681074475182
+22
+-98.36023622047243
+32
+0.32163976377951986
+13
+171.68681074475182
+23
+-98.36023622047243
+33
+0.32163976377951986
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+109.82597913515951
+30
+227.14655023375423
+11
+180.96357619368825
+21
+109.82598452844579
+31
+227.1425196850395
+12
+175.47334585487522
+22
+109.82598436481948
+32
+227.1425196820503
+13
+175.47334585487522
+23
+109.82598436481948
+33
+227.1425196820503
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357619368825
+20
+109.82598452844579
+30
+227.1425196850395
+11
+5.115907697472721e-13
+21
+109.82597913515951
+31
+227.14655023375423
+12
+180.96357619368825
+22
+109.82598452844579
+32
+236.60039370078752
+13
+180.96357619368825
+23
+109.82598452844579
+33
+236.60039370078752
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357619368825
+20
+109.82598452844579
+30
+236.60039370078752
+11
+5.115907697472721e-13
+21
+109.82597913515951
+31
+227.14655023375423
+12
+5.115907697472721e-13
+22
+109.82597913515954
+32
+236.60039370078752
+13
+5.115907697472721e-13
+23
+109.82597913515954
+33
+236.60039370078752
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357619368825
+20
+109.82598452844579
+30
+236.60039370078752
+11
+5.115907697472721e-13
+21
+109.82597913515954
+31
+236.60039370078752
+12
+171.68680454014486
+22
+109.82598425196863
+32
+236.60039370078752
+13
+171.68680454014486
+23
+109.82598425196863
+33
+236.60039370078752
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681074475182
+20
+-98.36023622047243
+30
+0.32163976377951986
+11
+164.33029144123975
+21
+-98.36023643971993
+31
+7.678158957059996
+12
+7.356519303512613
+22
+-98.36024111803383
+32
+7.678156604932067
+13
+7.356519303512613
+23
+-98.36024111803383
+33
+7.678156604932067
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029144123975
+20
+-98.36023643971993
+30
+7.678158957059996
+11
+171.68681074475182
+21
+-98.36023622047243
+31
+0.32163976377951986
+12
+171.68681074475182
+22
+-98.36023622047243
+32
+176.06259842519694
+13
+171.68681074475182
+23
+-98.36023622047243
+33
+176.06259842519694
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029144123975
+20
+-98.36023643971993
+30
+7.678158957059996
+11
+171.68681074475182
+21
+-98.36023622047243
+31
+176.06259842519694
+12
+164.33029144123975
+22
+-98.36023643971994
+32
+168.7061612498017
+13
+164.33029144123975
+23
+-98.36023643971994
+33
+168.7061612498017
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029144123975
+20
+-98.36023643971994
+30
+168.7061612498017
+11
+171.68681074475182
+21
+-98.36023622047243
+31
+176.06259842519694
+12
+7.356519303512613
+22
+-98.36024111803414
+32
+168.7079137133973
+13
+7.356519303512613
+23
+-98.36024111803414
+33
+168.7079137133973
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+-98.36024133728131
+30
+0.32163719118833317
+11
+7.356519303512613
+21
+-98.36024111803383
+31
+7.678156604932067
+12
+5.115907697472721e-13
+22
+-98.36024133728166
+32
+176.06451514594315
+13
+5.115907697472721e-13
+23
+-98.36024133728166
+33
+176.06451514594315
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519303512613
+20
+-98.36024111803383
+30
+7.678156604932067
+11
+5.115907697472721e-13
+21
+-98.36024133728131
+31
+0.32163719118833317
+12
+171.68681074475182
+22
+-98.36023622047243
+32
+0.32163976377951986
+13
+171.68681074475182
+23
+-98.36023622047243
+33
+0.32163976377951986
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+-98.36024133728166
+30
+176.06451514594315
+11
+7.356519303512613
+21
+-98.36024111803383
+31
+7.678156604932067
+12
+7.356519303512613
+22
+-98.36024111803414
+32
+168.7079137133973
+13
+7.356519303512613
+23
+-98.36024111803414
+33
+168.7079137133973
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+-98.36024133728166
+30
+176.06451514594315
+11
+7.356519303512613
+21
+-98.36024111803414
+31
+168.7079137133973
+12
+171.68681074475182
+22
+-98.36023622047243
+32
+176.06259842519694
+13
+171.68681074475182
+23
+-98.36023622047243
+33
+176.06259842519694
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+98.36023110366378
+30
+0.321666641223282
+11
+7.356519303512613
+21
+98.36023132291126
+31
+7.678186007680364
+12
+171.68680488186044
+22
+98.36023622047261
+32
+0.3216681102362128
+13
+171.68680488186044
+23
+98.36023622047261
+33
+0.3216681102362128
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519303512613
+20
+98.36023132291126
+30
+7.678186007680364
+11
+5.115907697472721e-13
+21
+98.36023110366378
+31
+0.321666641223282
+12
+5.115907697472721e-13
+22
+98.36023110366338
+32
+227.1465447984691
+13
+5.115907697472721e-13
+23
+98.36023110366338
+33
+227.1465447984691
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519303512613
+20
+98.36023132291126
+30
+7.678186007680364
+11
+5.115907697472721e-13
+21
+98.36023110366338
+31
+227.1465447984691
+12
+7.356519303512613
+22
+98.36023132291092
+32
+219.78985302292708
+13
+7.356519303512613
+23
+98.36023132291092
+33
+219.78985302292708
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519303512613
+20
+98.36023132291092
+30
+219.78985302292708
+11
+5.115907697472721e-13
+21
+98.36023110366338
+31
+227.1465447984691
+12
+171.68680488186044
+22
+98.36023622047261
+32
+227.1425196850395
+13
+171.68680488186044
+23
+98.36023622047261
+33
+227.1425196850395
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680488186044
+20
+98.36023622047261
+30
+0.3216681102362128
+11
+164.33028557834837
+21
+98.3602360012251
+31
+7.6781873508033405
+12
+171.68680488186044
+22
+98.36023622047261
+32
+227.1425196850395
+13
+171.68680488186044
+23
+98.36023622047261
+33
+227.1425196850395
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33028557834837
+20
+98.3602360012251
+30
+7.6781873508033405
+11
+171.68680488186044
+21
+98.36023622047261
+31
+0.3216681102362128
+12
+7.356519303512613
+22
+98.36023132291126
+32
+7.678186007680364
+13
+7.356519303512613
+23
+98.36023132291126
+33
+7.678186007680364
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680488186044
+20
+98.36023622047261
+30
+227.1425196850395
+11
+164.33028557834837
+21
+98.3602360012251
+31
+7.6781873508033405
+12
+164.33028557834837
+22
+98.3602360012251
+32
+219.78617284951383
+13
+164.33028557834837
+23
+98.3602360012251
+33
+219.78617284951383
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680488186044
+20
+98.36023622047261
+30
+227.1425196850395
+11
+164.33028557834837
+21
+98.3602360012251
+31
+219.78617284951383
+12
+7.356519303512613
+22
+98.36023132291092
+32
+219.78985302292708
+13
+7.356519303512613
+23
+98.36023132291092
+33
+219.78985302292708
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96358267716596
+20
+-107.71732255816846
+30
+189.86102362204736
+11
+175.47286961786034
+21
+-107.717322721809
+31
+176.06259842899448
+12
+180.96358267716596
+22
+-107.71732255816846
+32
+176.06259842519694
+13
+180.96358267716596
+23
+-107.71732255816846
+33
+176.06259842519694
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+175.47286961786034
+20
+-107.717322721809
+30
+176.06259842899448
+11
+180.96358267716596
+21
+-107.71732255816846
+31
+189.86102362204736
+12
+5.115907697472721e-13
+22
+-107.71732795145476
+32
+176.06451773417749
+13
+5.115907697472721e-13
+23
+-107.71732795145476
+33
+176.06451773417749
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+-107.71732795145476
+30
+176.06451773417749
+11
+180.96358267716596
+21
+-107.71732255816846
+31
+189.86102362204736
+12
+5.115907697472721e-13
+22
+-107.71732795145476
+32
+189.86102362204736
+13
+5.115907697472721e-13
+23
+-107.71732795145476
+33
+189.86102362204736
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96358267716596
+20
+-107.71732255816846
+30
+189.86102362204736
+11
+5.115907697472721e-13
+21
+-84.34409960499816
+31
+189.86102362204736
+12
+5.115907697472721e-13
+22
+-107.71732795145476
+32
+189.86102362204736
+13
+5.115907697472721e-13
+23
+-107.71732795145476
+33
+189.86102362204736
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+-84.34409960499816
+30
+189.86102362204736
+11
+180.96358267716596
+21
+-107.71732255816846
+31
+189.86102362204736
+12
+162.47775520891634
+22
+-84.34409476264783
+32
+189.86102362204736
+13
+162.47775520891634
+23
+-84.34409476264783
+33
+189.86102362204736
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+162.47775520891634
+20
+-84.34409476264783
+30
+189.86102362204736
+11
+180.96358267716596
+21
+-107.71732255816846
+31
+189.86102362204736
+12
+180.96358198056998
+22
+-84.34409421171179
+32
+189.86102362204736
+13
+180.96358198056998
+23
+-84.34409421171179
+33
+189.86102362204736
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+162.47775005936467
+20
+88.4413383082184
+30
+236.60039370078752
+11
+5.115907697472721e-13
+21
+109.82597913515954
+31
+236.60039370078752
+12
+5.115907697472721e-13
+22
+88.44133346586824
+32
+236.60039370078752
+13
+5.115907697472721e-13
+23
+88.44133346586824
+33
+236.60039370078752
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+109.82597913515954
+30
+236.60039370078752
+11
+162.47775005936467
+21
+88.4413383082184
+31
+236.60039370078752
+12
+171.68680454014486
+22
+109.82598425196863
+32
+236.60039370078752
+13
+171.68680454014486
+23
+109.82598425196863
+33
+236.60039370078752
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680454014486
+20
+109.82598425196863
+30
+236.60039370078752
+11
+162.47775005936467
+21
+88.4413383082184
+31
+236.60039370078752
+12
+180.96357683101826
+22
+88.44133885915446
+32
+236.60039370078752
+13
+180.96357683101826
+23
+88.44133885915446
+33
+236.60039370078752
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680454014486
+20
+109.82598425196863
+30
+236.60039370078752
+11
+180.96357683101826
+21
+88.44133885915446
+31
+236.60039370078752
+12
+180.96357619368825
+22
+109.82598452844579
+32
+236.60039370078752
+13
+180.96357619368825
+23
+109.82598452844579
+33
+236.60039370078752
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+162.47775520891634
+20
+-84.34409476264783
+30
+189.86102362204736
+11
+180.96357683101826
+21
+88.44133885915446
+31
+236.60039370078752
+12
+162.47775005936467
+22
+88.4413383082184
+32
+236.60039370078752
+13
+162.47775005936467
+23
+88.4413383082184
+33
+236.60039370078752
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357683101826
+20
+88.44133885915446
+30
+236.60039370078752
+11
+162.47775520891634
+21
+-84.34409476264783
+31
+189.86102362204736
+12
+180.96358198056998
+22
+-84.34409421171179
+32
+189.86102362204736
+13
+180.96358198056998
+23
+-84.34409421171179
+33
+189.86102362204736
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681074475182
+20
+-98.36023622047243
+30
+176.06259842519694
+11
+5.115907697472721e-13
+21
+-107.71732795145476
+31
+176.06451773417749
+12
+5.115907697472721e-13
+22
+-98.36024133728166
+32
+176.06451514594315
+13
+5.115907697472721e-13
+23
+-98.36024133728166
+33
+176.06451514594315
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+-107.71732795145476
+30
+176.06451773417749
+11
+171.68681074475182
+21
+-98.36023622047243
+31
+176.06259842519694
+12
+175.47286961786034
+22
+-107.717322721809
+32
+176.06259842899448
+13
+175.47286961786034
+23
+-107.717322721809
+33
+176.06259842899448
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+175.47286961786034
+20
+-107.717322721809
+30
+176.06259842899448
+11
+171.68681074475182
+21
+-98.36023622047243
+31
+176.06259842519694
+12
+180.96358239829522
+22
+-98.36023594399529
+32
+176.06259842519694
+13
+180.96358239829522
+23
+-98.36023594399529
+33
+176.06259842519694
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+175.47286961786034
+20
+-107.717322721809
+30
+176.06259842899448
+11
+180.96358239829522
+21
+-98.36023594399529
+31
+176.06259842519694
+12
+180.96358267716596
+22
+-107.71732255816846
+32
+176.06259842519694
+13
+180.96358267716596
+23
+-107.71732255816846
+33
+176.06259842519694
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+175.47334585487522
+20
+109.82598436481948
+30
+227.1425196820503
+11
+5.115907697472721e-13
+21
+98.36023110366338
+31
+227.1465447984691
+12
+5.115907697472721e-13
+22
+109.82597913515951
+32
+227.14655023375423
+13
+5.115907697472721e-13
+23
+109.82597913515951
+33
+227.14655023375423
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+5.115907697472721e-13
+20
+98.36023110366338
+30
+227.1465447984691
+11
+175.47334585487522
+21
+109.82598436481948
+31
+227.1425196820503
+12
+171.68680488186044
+22
+98.36023622047261
+32
+227.1425196850395
+13
+171.68680488186044
+23
+98.36023622047261
+33
+227.1425196850395
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680488186044
+20
+98.36023622047261
+30
+227.1425196850395
+11
+175.47334585487522
+21
+109.82598436481948
+31
+227.1425196820503
+12
+180.96357653540383
+22
+98.36023649694977
+32
+227.1425196850395
+13
+180.96357653540383
+23
+98.36023649694977
+33
+227.1425196850395
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357653540383
+20
+98.36023649694977
+30
+227.1425196850395
+11
+175.47334585487522
+21
+109.82598436481948
+31
+227.1425196820503
+12
+180.96357619368825
+22
+109.82598452844579
+32
+227.1425196850395
+13
+180.96357619368825
+23
+109.82598452844579
+33
+227.1425196850395
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357653540383
+20
+98.36023649694977
+30
+227.1425196850395
+11
+171.68681074475182
+21
+-98.36023622047243
+31
+176.06259842519694
+12
+171.68680488186044
+22
+98.36023622047261
+32
+227.1425196850395
+13
+171.68680488186044
+23
+98.36023622047261
+33
+227.1425196850395
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681074475182
+20
+-98.36023622047243
+30
+176.06259842519694
+11
+180.96357653540383
+21
+98.36023649694977
+31
+227.1425196850395
+12
+180.96358239829522
+22
+-98.36023594399529
+32
+176.06259842519694
+13
+180.96358239829522
+23
+-98.36023594399529
+33
+176.06259842519694
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357653540383
+20
+98.36023649694977
+30
+227.1425196850395
+11
+180.96358198056998
+21
+-84.34409421171179
+31
+189.86102362204736
+12
+180.96358239829522
+22
+-98.36023594399529
+32
+176.06259842519694
+13
+180.96358239829522
+23
+-98.36023594399529
+33
+176.06259842519694
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96358198056998
+20
+-84.34409421171179
+30
+189.86102362204736
+11
+180.96357653540383
+21
+98.36023649694977
+31
+227.1425196850395
+12
+180.96357683101826
+22
+88.44133885915446
+32
+236.60039370078752
+13
+180.96357683101826
+23
+88.44133885915446
+33
+236.60039370078752
+70
+1
+  0
+3DFACE
+ 8
+inside_half
+10
+162.47775520891634
+20
+-84.34409476264783
+30
+33.43464566929135
+11
+5.115907697472721e-13
+21
+88.44133346586825
+31
+80.17362204724412
+12
+5.115907697472721e-13
+22
+-84.34409960499816
+32
+33.43464566929135
+13
+5.115907697472721e-13
+23
+-84.34409960499816
+33
+33.43464566929135
+70
+1
+  0
+3DFACE
+ 8
+inside_half
+10
+5.115907697472721e-13
+20
+88.44133346586825
+30
+80.17362204724412
+11
+162.47775520891634
+21
+-84.34409476264783
+31
+33.43464566929135
+12
+162.47775005936467
+22
+88.44133830821842
+32
+80.17362204724412
+13
+162.47775005936467
+23
+88.44133830821842
+33
+80.17362204724412
+70
+1
+  0
+3DFACE
+ 8
+inside_half
+10
+162.47775005936467
+20
+88.44133830821842
+30
+236.60039370078752
+11
+5.115907697472721e-13
+21
+88.44133346586825
+31
+80.17362204724412
+12
+162.47775005936467
+22
+88.44133830821842
+32
+80.17362204724412
+13
+162.47775005936467
+23
+88.44133830821842
+33
+80.17362204724412
+70
+1
+  0
+3DFACE
+ 8
+inside_half
+10
+5.115907697472721e-13
+20
+88.44133346586825
+30
+80.17362204724412
+11
+162.47775005936467
+21
+88.44133830821842
+31
+236.60039370078752
+12
+5.115907697472721e-13
+22
+88.44133346586825
+32
+236.60039370078752
+13
+5.115907697472721e-13
+23
+88.44133346586825
+33
+236.60039370078752
+70
+1
+  0
+3DFACE
+ 8
+inside_half
+10
+5.115907697472721e-13
+20
+-84.34409960499816
+30
+189.86102362204736
+11
+162.47775520891634
+21
+-84.34409476264783
+31
+33.43464566929135
+12
+5.115907697472721e-13
+22
+-84.34409960499816
+32
+33.43464566929135
+13
+5.115907697472721e-13
+23
+-84.34409960499816
+33
+33.43464566929135
+70
+1
+  0
+3DFACE
+ 8
+inside_half
+10
+162.47775520891634
+20
+-84.34409476264783
+30
+33.43464566929135
+11
+5.115907697472721e-13
+21
+-84.34409960499816
+31
+189.86102362204736
+12
+162.47775520891634
+22
+-84.34409476264783
+32
+189.86102362204736
+13
+162.47775520891634
+23
+-84.34409476264783
+33
+189.86102362204736
+70
+1
+  0
+3DFACE
+ 8
+inside_half
+10
+162.47775005936467
+20
+88.44133830821842
+30
+236.60039370078752
+11
+162.47775520891634
+21
+-84.34409476264783
+31
+33.43464566929135
+12
+162.47775520891634
+22
+-84.34409476264783
+32
+189.86102362204736
+13
+162.47775520891634
+23
+-84.34409476264783
+33
+189.86102362204736
+70
+1
+  0
+3DFACE
+ 8
+inside_half
+10
+162.47775520891634
+20
+-84.34409476264783
+30
+33.43464566929135
+11
+162.47775005936467
+21
+88.44133830821842
+31
+236.60039370078752
+12
+162.47775005936467
+22
+88.44133830821842
+32
+80.17362204724412
+13
+162.47775005936467
+23
+88.44133830821842
+33
+80.17362204724412
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519303512613
+20
+-98.36024111803383
+30
+7.678156604932067
+11
+7.356519154496823
+21
+-93.36024111803414
+31
+168.7079137133973
+12
+7.356519303512613
+22
+-98.36024111803414
+32
+168.7079137133973
+13
+7.356519303512613
+23
+-98.36024111803414
+33
+168.7079137133973
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519154496823
+20
+-93.36024111803414
+30
+168.7079137133973
+11
+7.356519303512613
+21
+-98.36024111803383
+31
+7.678156604932067
+12
+7.356519154496823
+22
+-93.36024111803383
+32
+7.678156604932072
+13
+7.356519154496823
+23
+-93.36024111803383
+33
+7.678156604932072
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519303512613
+20
+-98.36024111803383
+30
+7.678156604932067
+11
+164.33029129222393
+21
+-93.36023643971993
+31
+7.678158957060002
+12
+7.356519154496823
+22
+-93.36024111803383
+32
+7.678156604932072
+13
+7.356519154496823
+23
+-93.36024111803383
+33
+7.678156604932072
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029129222393
+20
+-93.36023643971993
+30
+7.678158957060002
+11
+7.356519303512613
+21
+-98.36024111803383
+31
+7.678156604932067
+12
+164.33029144123975
+22
+-98.36023643971993
+32
+7.678158957059996
+13
+164.33029144123975
+23
+-98.36023643971993
+33
+7.678158957059996
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029129222393
+20
+-93.36023643971994
+30
+168.7061612498017
+11
+164.33029144123975
+21
+-98.36023643971993
+31
+7.678158957059996
+12
+164.33029144123975
+22
+-98.36023643971994
+32
+168.7061612498017
+13
+164.33029144123975
+23
+-98.36023643971994
+33
+168.7061612498017
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029144123975
+20
+-98.36023643971993
+30
+7.678158957059996
+11
+164.33029129222393
+21
+-93.36023643971994
+31
+168.7061612498017
+12
+164.33029129222393
+22
+-93.36023643971993
+32
+7.678158957060002
+13
+164.33029129222393
+23
+-93.36023643971993
+33
+7.678158957060002
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029129222393
+20
+-93.36023643971994
+30
+168.7061612498017
+11
+7.356519303512613
+21
+-98.36024111803414
+31
+168.7079137133973
+12
+7.356519154496823
+22
+-93.36024111803414
+32
+168.7079137133973
+13
+7.356519154496823
+23
+-93.36024111803414
+33
+168.7079137133973
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519303512613
+20
+-98.36024111803414
+30
+168.7079137133973
+11
+164.33029129222393
+21
+-93.36023643971994
+31
+168.7061612498017
+12
+164.33029144123975
+22
+-98.36023643971994
+32
+168.7061612498017
+13
+164.33029144123975
+23
+-98.36023643971994
+33
+168.7061612498017
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33028557834837
+20
+98.3602360012251
+30
+219.78617284951383
+11
+164.33028572736413
+21
+93.3602360012251
+31
+7.678187350803336
+12
+164.33028572736413
+22
+93.3602360012251
+32
+219.78617284951383
+13
+164.33028572736413
+23
+93.3602360012251
+33
+219.78617284951383
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33028572736413
+20
+93.3602360012251
+30
+7.678187350803336
+11
+164.33028557834837
+21
+98.3602360012251
+31
+219.78617284951383
+12
+164.33028557834837
+22
+98.3602360012251
+32
+7.6781873508033405
+13
+164.33028557834837
+23
+98.3602360012251
+33
+7.6781873508033405
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519452528403
+20
+93.36023132291126
+30
+7.67818600768036
+11
+164.33028557834837
+21
+98.3602360012251
+31
+7.6781873508033405
+12
+7.356519303512613
+22
+98.36023132291126
+32
+7.678186007680364
+13
+7.356519303512613
+23
+98.36023132291126
+33
+7.678186007680364
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33028557834837
+20
+98.3602360012251
+30
+7.6781873508033405
+11
+7.356519452528403
+21
+93.36023132291126
+31
+7.67818600768036
+12
+164.33028572736413
+22
+93.3602360012251
+32
+7.678187350803336
+13
+164.33028572736413
+23
+93.3602360012251
+33
+7.678187350803336
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519452528403
+20
+93.36023132291126
+30
+7.67818600768036
+11
+7.356519303512613
+21
+98.36023132291092
+31
+219.78985302292708
+12
+7.356519452528403
+22
+93.36023132291092
+32
+219.78985302292708
+13
+7.356519452528403
+23
+93.36023132291092
+33
+219.78985302292708
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519303512613
+20
+98.36023132291092
+30
+219.78985302292708
+11
+7.356519452528403
+21
+93.36023132291126
+31
+7.67818600768036
+12
+7.356519303512613
+22
+98.36023132291126
+32
+7.678186007680364
+13
+7.356519303512613
+23
+98.36023132291126
+33
+7.678186007680364
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33028557834837
+20
+98.3602360012251
+30
+219.78617284951383
+11
+7.356519452528403
+21
+93.36023132291092
+31
+219.78985302292708
+12
+7.356519303512613
+22
+98.36023132291092
+32
+219.78985302292708
+13
+7.356519303512613
+23
+98.36023132291092
+33
+219.78985302292708
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+7.356519452528403
+20
+93.36023132291092
+30
+219.78985302292708
+11
+164.33028557834837
+21
+98.3602360012251
+31
+219.78617284951383
+12
+164.33028572736413
+22
+93.3602360012251
+32
+219.78617284951383
+13
+164.33028572736413
+23
+93.3602360012251
+33
+219.78617284951383
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681074475182
+20
+-98.36023622047243
+30
+0.32163976377951986
+11
+171.68681053254642
+21
+-91.24000442486981
+31
+7.441872585372755
+12
+171.68681074475182
+22
+-98.36023622047243
+32
+176.06259842519694
+13
+171.68681074475182
+23
+-98.36023622047243
+33
+176.06259842519694
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681053254642
+20
+-91.24000442486981
+30
+7.441872585372755
+11
+171.68681074475182
+21
+-98.36023622047243
+31
+0.32163976377951986
+12
+171.68680488186044
+22
+98.36023622047261
+32
+0.3216681102362128
+13
+171.68680488186044
+23
+98.36023622047261
+33
+0.3216681102362128
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681053254642
+20
+-91.24000442486981
+30
+7.441872585372755
+11
+171.68680488186044
+21
+98.36023622047261
+31
+0.3216681102362128
+12
+171.68680509406585
+22
+91.24000442487
+32
+7.441898879848383
+13
+171.68680509406585
+23
+91.24000442487
+33
+7.441898879848383
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680509406585
+20
+91.24000442487
+30
+7.441898879848383
+11
+171.68680488186044
+21
+98.36023622047261
+31
+0.3216681102362128
+12
+171.68680509406585
+22
+91.24000442487
+32
+217.9373521601451
+13
+171.68680509406585
+23
+91.24000442487
+33
+217.9373521601451
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681074475182
+20
+-98.36023622047243
+30
+176.06259842519694
+11
+171.68681053254642
+21
+-91.24000442486981
+31
+170.55507227904
+12
+171.68680488186044
+22
+98.36023622047261
+32
+227.1425196850395
+13
+171.68680488186044
+23
+98.36023622047261
+33
+227.1425196850395
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681053254642
+20
+-91.24000442486981
+30
+170.55507227904
+11
+171.68681074475182
+21
+-98.36023622047243
+31
+176.06259842519694
+12
+171.68681053254642
+22
+-91.24000442486981
+32
+7.441872585372755
+13
+171.68681053254642
+23
+-91.24000442486981
+33
+7.441872585372755
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680488186044
+20
+98.36023622047261
+30
+227.1425196850395
+11
+171.68681053254642
+21
+-91.24000442486981
+31
+170.55507227904
+12
+171.68680509406585
+22
+91.24000442487
+32
+217.9373521601451
+13
+171.68680509406585
+23
+91.24000442487
+33
+217.9373521601451
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680488186044
+20
+98.36023622047261
+30
+227.1425196850395
+11
+171.68680509406585
+21
+91.24000442487
+31
+217.9373521601451
+12
+171.68680488186044
+22
+98.36023622047261
+32
+0.3216681102362128
+13
+171.68680488186044
+23
+98.36023622047261
+33
+0.3216681102362128
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680509406585
+20
+91.24000442487
+30
+217.9373521601451
+11
+166.68681053254642
+21
+-91.2400045738856
+31
+170.55507227904
+12
+166.68680509406585
+22
+91.2400042758542
+32
+217.9373521601451
+13
+166.68680509406585
+23
+91.2400042758542
+33
+217.9373521601451
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+166.68681053254642
+20
+-91.2400045738856
+30
+170.55507227904
+11
+171.68680509406585
+21
+91.24000442487
+31
+217.9373521601451
+12
+171.68681053254642
+22
+-91.24000442486981
+32
+170.55507227904
+13
+171.68681053254642
+23
+-91.24000442486981
+33
+170.55507227904
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+166.68681053254642
+20
+-91.2400045738856
+30
+170.55507227904
+11
+171.68681053254642
+21
+-91.24000442486981
+31
+7.441872585372755
+12
+166.68681053254642
+22
+-91.2400045738856
+32
+7.441872585372755
+13
+166.68681053254642
+23
+-91.2400045738856
+33
+7.441872585372755
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68681053254642
+20
+-91.24000442486981
+30
+7.441872585372755
+11
+166.68681053254642
+21
+-91.2400045738856
+31
+170.55507227904
+12
+171.68681053254642
+22
+-91.24000442486981
+32
+170.55507227904
+13
+171.68681053254642
+23
+-91.24000442486981
+33
+170.55507227904
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+166.68681053254642
+20
+-91.2400045738856
+30
+7.441872585372755
+11
+171.68680509406585
+21
+91.24000442487
+31
+7.441898879848383
+12
+166.68680509406585
+22
+91.2400042758542
+32
+7.441898879848383
+13
+166.68680509406585
+23
+91.2400042758542
+33
+7.441898879848383
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680509406585
+20
+91.24000442487
+30
+7.441898879848383
+11
+166.68681053254642
+21
+-91.2400045738856
+31
+7.441872585372755
+12
+171.68681053254642
+22
+-91.24000442486981
+32
+7.441872585372755
+13
+171.68681053254642
+23
+-91.24000442486981
+33
+7.441872585372755
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+171.68680509406585
+20
+91.24000442487
+30
+217.9373521601451
+11
+166.68680509406585
+21
+91.2400042758542
+31
+7.441898879848383
+12
+171.68680509406585
+22
+91.24000442487
+32
+7.441898879848383
+13
+171.68680509406585
+23
+91.24000442487
+33
+7.441898879848383
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+166.68680509406585
+20
+91.2400042758542
+30
+7.441898879848383
+11
+171.68680509406585
+21
+91.24000442487
+31
+217.9373521601451
+12
+166.68680509406585
+22
+91.2400042758542
+32
+217.9373521601451
+13
+166.68680509406585
+23
+91.2400042758542
+33
+217.9373521601451
+70
+1
+  0
+LINE
+ 8
+frame_half
+10
+192.11939488573717
+20
+-18.806592430868662
+30
+98.78595140714901
+11
+192.11958396451757
+21
+-18.812044202216626
+31
+98.7803176609457
+  0
+3DFACE
+ 8
+frame_half
+10
+169.1868200523188
+20
+35.43805451572439
+30
+98.35513302657242
+11
+195.93208234481858
+21
+35.43803453794092
+31
+155.2043307086615
+12
+195.93208234769213
+22
+35.438032218648914
+32
+98.35511811023626
+13
+195.93208234769213
+23
+35.438032218648914
+33
+98.35511811023626
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+195.93208234481858
+20
+35.43803453794092
+30
+155.2043307086615
+11
+169.1868200523188
+21
+35.43805451572439
+31
+98.35513302657242
+12
+169.1868200523188
+22
+35.43805451572439
+32
+155.20434292468212
+13
+169.1868200523188
+23
+35.43805451572439
+33
+155.20434292468212
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+195.93206584814033
+20
+-18.806575569150453
+30
+98.3551192167518
+11
+191.93206584793933
+21
+-18.806572071228686
+31
+102.3551214476279
+12
+169.18680355276706
+22
+-18.80655327207498
+32
+98.35513413308796
+13
+169.18680355276706
+23
+-18.80655327207498
+33
+98.35513413308796
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+191.93206584793933
+20
+-18.806572071228686
+30
+102.3551214476279
+11
+195.93206584814033
+21
+-18.806575569150453
+31
+98.3551192167518
+12
+195.93206584526678
+22
+-18.80657324985846
+32
+155.20433181517726
+13
+195.93206584526678
+23
+-18.80657324985846
+33
+155.20433181517726
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+191.93206584793933
+20
+-18.806572071228686
+30
+102.3551214476279
+11
+195.93206584526678
+21
+-18.80657324985846
+31
+155.20433181517726
+12
+191.93206584546897
+22
+-18.806572283087576
+32
+151.20433364219494
+13
+191.93206584546897
+23
+-18.806572283087576
+33
+151.20433364219494
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+191.93206584546897
+20
+-18.806572283087576
+30
+151.20433364219494
+11
+195.93206584526678
+21
+-18.80657324985846
+31
+155.20433181517726
+12
+173.18680355276365
+22
+-18.80656595048204
+32
+151.20434220417928
+13
+173.18680355276365
+23
+-18.80656595048204
+33
+151.20434220417928
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+169.18680355276706
+20
+-18.80655327207498
+30
+98.35513413308796
+11
+173.18680355276558
+21
+-18.806557480875455
+31
+102.35513190221339
+12
+169.18680355276337
+22
+-18.806565694589835
+32
+155.20434403119808
+13
+169.18680355276337
+23
+-18.806565694589835
+33
+155.20434403119808
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+173.18680355276558
+20
+-18.806557480875455
+30
+102.35513190221339
+11
+169.18680355276706
+21
+-18.80655327207498
+31
+98.35513413308796
+12
+191.93206584793933
+22
+-18.806572071228686
+32
+102.3551214476279
+13
+191.93206584793933
+23
+-18.806572071228686
+33
+102.3551214476279
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+169.18680355276337
+20
+-18.806565694589835
+30
+155.20434403119808
+11
+173.18680355276558
+21
+-18.806557480875455
+31
+102.35513190221339
+12
+173.18680355276365
+22
+-18.80656595048204
+32
+151.20434220417928
+13
+173.18680355276365
+23
+-18.80656595048204
+33
+151.20434220417928
+70
+13
+  0
+3DFACE
+ 8
+frame_half
+10
+169.18680355276337
+20
+-18.806565694589835
+30
+155.20434403119808
+11
+173.18680355276365
+21
+-18.80656595048204
+31
+151.20434220417928
+12
+195.93206584526678
+22
+-18.80657324985846
+32
+155.20433181517726
+13
+195.93206584526678
+23
+-18.80657324985846
+33
+155.20433181517726
+70
+3
+  0
+3DFACE
+ 8
+frame_half
+10
+195.93208234769213
+20
+35.438032218648914
+30
+98.35511811023626
+11
+195.93206584526678
+21
+-18.80657324985846
+31
+155.20433181517726
+12
+195.93206584814033
+22
+-18.806575569150453
+32
+98.3551192167518
+13
+195.93206584814033
+23
+-18.806575569150453
+33
+98.3551192167518
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+195.93206584526678
+20
+-18.80657324985846
+30
+155.20433181517726
+11
+195.93208234769213
+21
+35.438032218648914
+31
+98.35511811023626
+12
+195.93208234481858
+22
+35.43803453794092
+32
+155.2043307086615
+13
+195.93208234481858
+23
+35.43803453794092
+33
+155.2043307086615
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+169.1868200523188
+20
+35.43805451572439
+30
+98.35513302657242
+11
+195.93206584814033
+21
+-18.806575569150453
+31
+98.3551192167518
+12
+169.18680355276706
+22
+-18.80655327207498
+32
+98.35513413308796
+13
+169.18680355276706
+23
+-18.80655327207498
+33
+98.35513413308796
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+195.93206584814033
+20
+-18.806575569150453
+30
+98.3551192167518
+11
+169.1868200523188
+21
+35.43805451572439
+31
+98.35513302657242
+12
+195.93208234769213
+22
+35.438032218648914
+32
+98.35511811023626
+13
+195.93208234769213
+23
+35.438032218648914
+33
+98.35511811023626
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+169.1868200523188
+20
+35.43805451572439
+30
+155.20434292468212
+11
+169.18680355276706
+21
+-18.80655327207498
+31
+98.35513413308796
+12
+169.18680355276337
+22
+-18.806565694589835
+32
+155.20434403119808
+13
+169.18680355276337
+23
+-18.806565694589835
+33
+155.20434403119808
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+169.18680355276706
+20
+-18.80655327207498
+30
+98.35513413308796
+11
+169.1868200523188
+21
+35.43805451572439
+31
+155.20434292468212
+12
+169.1868200523188
+22
+35.43805451572439
+32
+98.35513302657242
+13
+169.1868200523188
+23
+35.43805451572439
+33
+98.35513302657242
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+195.93206584526678
+20
+-18.80657324985846
+30
+155.20433181517726
+11
+169.1868200523188
+21
+35.43805451572439
+31
+155.20434292468212
+12
+169.18680355276337
+22
+-18.806565694589835
+32
+155.20434403119808
+13
+169.18680355276337
+23
+-18.806565694589835
+33
+155.20434403119808
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+169.1868200523188
+20
+35.43805451572439
+30
+155.20434292468212
+11
+195.93206584526678
+21
+-18.80657324985846
+31
+155.20433181517726
+12
+195.93208234481858
+22
+35.43803453794092
+32
+155.2043307086615
+13
+195.93208234481858
+23
+35.43803453794092
+33
+155.2043307086615
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+149.6172528342156
+20
+-76.99953980767417
+30
+0.07164228285092733
+11
+164.3302911109016
+21
+-80.5596534414427
+31
+-9.928357717148824
+12
+156.97377191349227
+22
+-80.55965344144272
+32
+-9.928357717148824
+13
+156.97377191349227
+23
+-80.55965344144272
+33
+-9.928357717148824
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.3302911109016
+20
+-80.5596534414427
+30
+-9.928357717148824
+11
+149.6172528342156
+21
+-76.99953980767417
+31
+0.07164228285092733
+12
+164.33029122903434
+22
+-76.99953980767414
+32
+0.07164228285092733
+13
+164.33029122903434
+23
+-76.99953980767414
+33
+0.07164228285092733
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029144123975
+20
+-91.24000339887927
+30
+0.07164074271531895
+11
+156.97377191349227
+21
+-87.67988534666897
+31
+-9.928358597448263
+12
+164.33029121700434
+22
+-87.67988523704514
+32
+-9.92835925728443
+13
+164.33029121700434
+23
+-87.67988523704514
+33
+-9.92835925728443
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+156.97377191349227
+20
+-87.67988534666897
+30
+-9.928358597448263
+11
+164.33029144123975
+21
+-91.24000339887927
+31
+0.07164074271531895
+12
+149.6172528342156
+22
+-91.2400036181269
+32
+0.07164140255148688
+13
+149.6172528342156
+23
+-91.2400036181269
+33
+0.07164140255148688
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.3302858950658
+20
+87.67989181709197
+30
+-9.928332989879554
+11
+156.97376669765652
+21
+80.55965991186571
+31
+-9.928333870178992
+12
+156.97376669765652
+22
+87.67989181709194
+32
+-9.928332989879554
+13
+156.97376669765652
+23
+87.67989181709194
+33
+-9.928332989879554
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+156.97376669765652
+20
+80.55965991186571
+30
+-9.928333870178992
+11
+164.3302858950658
+21
+87.67989181709197
+31
+-9.928332989879554
+12
+164.3302860011686
+22
+80.55966002148952
+32
+-9.92833453001516
+13
+164.3302860011686
+23
+80.55966002148952
+33
+-9.92833453001516
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.3302860011686
+20
+80.55966002148952
+30
+-9.92833453001516
+11
+164.3302860131986
+21
+91.24000545086054
+31
+0.07166701012019949
+12
+164.330286225404
+22
+76.99954185965541
+32
+0.07166546998458934
+13
+164.330286225404
+23
+76.99954185965541
+33
+0.07166546998458934
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.3302860131986
+20
+91.24000545086054
+30
+0.07166701012019949
+11
+164.3302860011686
+21
+80.55966002148952
+31
+-9.92833453001516
+12
+164.3302858950658
+22
+87.67989181709197
+32
+-9.928332989879554
+13
+164.3302858950658
+23
+87.67989181709197
+33
+-9.928332989879554
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+149.61724761837985
+20
+91.24000545086051
+30
+0.07166701012019949
+11
+156.97376669765652
+21
+80.55965991186571
+31
+-9.928333870178992
+12
+149.6172476183798
+22
+76.99954164040778
+32
+0.07166612982075904
+13
+149.6172476183798
+23
+76.99954164040778
+33
+0.07166612982075904
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+156.97376669765652
+20
+80.55965991186571
+30
+-9.928333870178992
+11
+149.61724761837985
+21
+91.24000545086051
+31
+0.07166701012019949
+12
+156.97376669765652
+22
+87.67989181709194
+32
+-9.928332989879554
+13
+156.97376669765652
+23
+87.67989181709194
+33
+-9.928332989879554
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.330286225404
+20
+76.99954185965541
+30
+0.07166546998458934
+11
+156.97376669765652
+21
+80.55965991186571
+31
+-9.928333870178992
+12
+164.3302860011686
+22
+80.55966002148952
+32
+-9.92833453001516
+13
+164.3302860011686
+23
+80.55966002148952
+33
+-9.92833453001516
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+156.97376669765652
+20
+80.55965991186571
+30
+-9.928333870178992
+11
+164.330286225404
+21
+76.99954185965541
+31
+0.07166546998458934
+12
+149.6172476183798
+22
+76.99954164040778
+32
+0.07166612982075904
+13
+149.6172476183798
+23
+76.99954164040778
+33
+0.07166612982075904
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+149.61724761837985
+20
+91.24000545086051
+30
+0.07166701012019949
+11
+164.3302858950658
+21
+87.67989181709197
+31
+-9.928332989879554
+12
+156.97376669765652
+22
+87.67989181709194
+32
+-9.928332989879554
+13
+156.97376669765652
+23
+87.67989181709194
+33
+-9.928332989879554
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.3302858950658
+20
+87.67989181709197
+30
+-9.928332989879554
+11
+149.61724761837985
+21
+91.24000545086051
+31
+0.07166701012019949
+12
+164.3302860131986
+22
+91.24000545086054
+32
+0.07166701012019949
+13
+164.3302860131986
+23
+91.24000545086054
+33
+0.07166701012019949
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029121700434
+20
+-87.67988523704514
+30
+-9.92835925728443
+11
+164.33029122903434
+21
+-76.99953980767414
+31
+0.07164228285092733
+12
+164.33029144123975
+22
+-91.24000339887927
+32
+0.07164074271531895
+13
+164.33029144123975
+23
+-91.24000339887927
+33
+0.07164074271531895
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.33029122903434
+20
+-76.99953980767414
+30
+0.07164228285092733
+11
+164.33029121700434
+21
+-87.67988523704514
+31
+-9.92835925728443
+12
+164.3302911109016
+22
+-80.5596534414427
+32
+-9.928357717148824
+13
+164.3302911109016
+23
+-80.5596534414427
+33
+-9.928357717148824
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+149.6172528342156
+20
+-76.99953980767417
+30
+0.07164228285092733
+11
+156.97377191349227
+21
+-87.67988534666897
+31
+-9.928358597448263
+12
+149.6172528342156
+22
+-91.2400036181269
+32
+0.07164140255148688
+13
+149.6172528342156
+23
+-91.2400036181269
+33
+0.07164140255148688
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+156.97377191349227
+20
+-87.67988534666897
+30
+-9.928358597448263
+11
+149.6172528342156
+21
+-76.99953980767417
+31
+0.07164228285092733
+12
+156.97377191349227
+22
+-80.55965344144272
+32
+-9.928357717148824
+13
+156.97377191349227
+23
+-80.55965344144272
+33
+-9.928357717148824
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+164.3302911109016
+20
+-80.5596534414427
+30
+-9.928357717148824
+11
+156.97377191349227
+21
+-87.67988534666897
+31
+-9.928358597448263
+12
+156.97377191349227
+22
+-80.55965344144272
+32
+-9.928357717148824
+13
+156.97377191349227
+23
+-80.55965344144272
+33
+-9.928357717148824
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+156.97377191349227
+20
+-87.67988534666897
+30
+-9.928358597448263
+11
+164.3302911109016
+21
+-80.5596534414427
+31
+-9.928357717148824
+12
+164.33029121700434
+22
+-87.67988523704514
+32
+-9.92835925728443
+13
+164.33029121700434
+23
+-87.67988523704514
+33
+-9.92835925728443
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96358239829522
+20
+-98.36023594399529
+30
+176.06259842519694
+11
+180.96358267716596
+21
+-107.71732255816846
+31
+189.86102362204736
+12
+180.96358267716596
+22
+-107.71732255816846
+32
+176.06259842519694
+13
+180.96358267716596
+23
+-107.71732255816846
+33
+176.06259842519694
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96358267716596
+20
+-107.71732255816846
+30
+189.86102362204736
+11
+180.96358239829522
+21
+-98.36023594399529
+31
+176.06259842519694
+12
+180.96358198056998
+22
+-84.34409421171179
+32
+189.86102362204736
+13
+180.96358198056998
+23
+-84.34409421171179
+33
+189.86102362204736
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357653540383
+20
+98.36023649694977
+30
+227.1425196850395
+11
+180.96357619368825
+21
+109.82598452844579
+31
+236.60039370078752
+12
+180.96357683101826
+22
+88.44133885915446
+32
+236.60039370078752
+13
+180.96357683101826
+23
+88.44133885915446
+33
+236.60039370078752
+70
+1
+  0
+3DFACE
+ 8
+frame_half
+10
+180.96357619368825
+20
+109.82598452844579
+30
+236.60039370078752
+11
+180.96357653540383
+21
+98.36023649694977
+31
+227.1425196850395
+12
+180.96357619368825
+22
+109.82598452844579
+32
+227.1425196850395
+13
+180.96357619368825
+23
+109.82598452844579
+33
+227.1425196850395
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+164.33029129222393
+20
+-93.36023643971993
+30
+7.678158957060002
+11
+7.356519154496823
+21
+-93.36024111803414
+31
+168.7079137133973
+12
+7.356519154496823
+22
+-93.36024111803383
+32
+7.678156604932072
+13
+7.356519154496823
+23
+-93.36024111803383
+33
+7.678156604932072
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+7.356519154496823
+20
+-93.36024111803414
+30
+168.7079137133973
+11
+164.33029129222393
+21
+-93.36023643971993
+31
+7.678158957060002
+12
+164.33029129222393
+22
+-93.36023643971994
+32
+168.7061612498017
+13
+164.33029129222393
+23
+-93.36023643971994
+33
+168.7061612498017
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+7.356519452528403
+20
+93.36023132291092
+30
+219.78985302292708
+11
+164.33028572736413
+21
+93.3602360012251
+31
+7.678187350803336
+12
+7.356519452528403
+22
+93.36023132291126
+32
+7.67818600768036
+13
+7.356519452528403
+23
+93.36023132291126
+33
+7.67818600768036
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+164.33028572736413
+20
+93.3602360012251
+30
+7.678187350803336
+11
+7.356519452528403
+21
+93.36023132291092
+31
+219.78985302292708
+12
+164.33028572736413
+22
+93.3602360012251
+32
+219.78617284951383
+13
+164.33028572736413
+23
+93.3602360012251
+33
+219.78617284951383
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+166.68680509406585
+20
+91.2400042758542
+30
+217.9373521601451
+11
+166.68681053254642
+21
+-91.2400045738856
+31
+170.55507227904
+12
+166.68680509406585
+22
+91.2400042758542
+32
+7.441898879848383
+13
+166.68680509406585
+23
+91.2400042758542
+33
+7.441898879848383
+70
+0
+  0
+3DFACE
+ 8
+panels_half
+10
+166.68680509406585
+20
+91.2400042758542
+30
+7.441898879848383
+11
+166.68681053254642
+21
+-91.2400045738856
+31
+170.55507227904
+12
+166.68681053254642
+22
+-91.2400045738856
+32
+7.441872585372755
+13
+166.68681053254642
+23
+-91.2400045738856
+33
+7.441872585372755
+70
+0
+  0
+3DFACE
+ 8
+panels_half
+10
+191.93208098206628
+20
+8.315735296259732
+30
+151.2043360522785
+11
+173.1868186893629
+21
+8.315750098471852
+31
+102.35513431229701
+12
+191.93208098453664
+22
+8.315735508118621
+32
+102.35512385771152
+13
+191.93208098453664
+23
+8.315735508118621
+33
+102.35512385771152
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+173.1868186893629
+20
+8.315750098471852
+30
+102.35513431229701
+11
+191.93208098206628
+21
+8.315735296259732
+31
+151.2043360522785
+12
+173.18681868936096
+22
+8.315741628865268
+32
+151.20434461426285
+13
+173.18681868936096
+23
+8.315741628865268
+33
+151.20434461426285
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+173.18681868936096
+20
+8.315741628865268
+30
+151.20434461426285
+11
+191.93206584546897
+21
+-18.806572283087576
+31
+151.20433364219494
+12
+173.18680355276365
+22
+-18.80656595048204
+32
+151.20434220417928
+13
+173.18680355276365
+23
+-18.80656595048204
+33
+151.20434220417928
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+191.93206584546897
+20
+-18.806572283087576
+30
+151.20433364219494
+11
+173.18681868936096
+21
+8.315741628865268
+31
+151.20434461426285
+12
+191.93208098206628
+22
+8.315735296259732
+32
+151.2043360522785
+13
+191.93208098206628
+23
+8.315735296259732
+33
+151.2043360522785
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+173.18680355276558
+20
+-18.806557480875455
+30
+102.35513190221339
+11
+173.18681868936096
+21
+8.315741628865268
+31
+151.20434461426285
+12
+173.18680355276365
+22
+-18.80656595048204
+32
+151.20434220417928
+13
+173.18680355276365
+23
+-18.80656595048204
+33
+151.20434220417928
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+173.18681868936096
+20
+8.315741628865268
+30
+151.20434461426285
+11
+173.18680355276558
+21
+-18.806557480875455
+31
+102.35513190221339
+12
+173.1868186893629
+22
+8.315750098471852
+32
+102.35513431229701
+13
+173.1868186893629
+23
+8.315750098471852
+33
+102.35513431229701
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+191.93206584793933
+20
+-18.806572071228686
+30
+102.3551214476279
+11
+173.1868186893629
+21
+8.315750098471852
+31
+102.35513431229701
+12
+173.18680355276558
+22
+-18.806557480875455
+32
+102.35513190221339
+13
+173.18680355276558
+23
+-18.806557480875455
+33
+102.35513190221339
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+173.1868186893629
+20
+8.315750098471852
+30
+102.35513431229701
+11
+191.93206584793933
+21
+-18.806572071228686
+31
+102.3551214476279
+12
+191.93208098453664
+22
+8.315735508118621
+32
+102.35512385771152
+13
+191.93208098453664
+23
+8.315735508118621
+33
+102.35512385771152
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+191.93208098206628
+20
+8.315735296259732
+30
+151.2043360522785
+11
+191.93206584793933
+21
+-18.806572071228686
+31
+102.3551214476279
+12
+191.93206584546897
+22
+-18.806572283087576
+32
+151.20433364219494
+13
+191.93206584546897
+23
+-18.806572283087576
+33
+151.20433364219494
+70
+1
+  0
+3DFACE
+ 8
+panels_half
+10
+191.93206584793933
+20
+-18.806572071228686
+30
+102.3551214476279
+11
+191.93208098206628
+21
+8.315735296259732
+31
+151.2043360522785
+12
+191.93208098453664
+22
+8.315735508118621
+32
+102.35512385771152
+13
+191.93208098453664
+23
+8.315735508118621
+33
+102.35512385771152
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+108.5456607922482
+30
+249.43983074886108
+11
+54.42013089894506
+21
+111.14544989054545
+31
+246.84004165056382
+12
+14.604382809215494
+22
+111.14544989054545
+32
+246.84004165056382
+13
+14.604382809215494
+23
+111.14544989054545
+33
+246.84004165056382
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+111.14544989054545
+30
+246.84004165056382
+11
+14.604382809215494
+21
+108.5456607922482
+31
+249.43983074886108
+12
+54.42013089894506
+22
+108.5456607922482
+32
+249.43983074886108
+13
+54.42013089894506
+23
+108.5456607922482
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+97.89152693398054
+30
+243.28866369780795
+11
+14.604382809215494
+21
+98.84311578843918
+31
+239.73728574505208
+12
+54.42013089894506
+22
+98.84311578843918
+32
+239.73728574505208
+13
+54.42013089894506
+23
+98.84311578843918
+33
+239.73728574505208
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+98.84311578843918
+30
+239.73728574505208
+11
+54.42013089894506
+21
+97.89152693398054
+31
+243.28866369780795
+12
+14.604382809215494
+22
+97.89152693398054
+32
+243.28866369780795
+13
+14.604382809215494
+23
+97.89152693398054
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+236.18590779229618
+11
+14.604382809215494
+21
+101.44290488673643
+31
+237.1374966467548
+12
+14.604382809215494
+22
+104.99428283949231
+32
+236.18590779229618
+13
+14.604382809215494
+23
+104.99428283949231
+33
+236.18590779229618
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+101.44290488673643
+30
+237.1374966467548
+11
+54.42013089894506
+21
+104.99428283949231
+31
+236.18590779229618
+12
+54.42013089894506
+22
+101.44290488673643
+32
+237.1374966467548
+13
+54.42013089894506
+23
+101.44290488673643
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+111.14544989054545
+30
+239.73728574505205
+11
+14.604382809215494
+21
+108.5456607922482
+31
+237.1374966467548
+12
+14.604382809215494
+22
+111.14544989054545
+32
+239.73728574505205
+13
+14.604382809215494
+23
+111.14544989054545
+33
+239.73728574505205
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+108.5456607922482
+30
+237.1374966467548
+11
+54.42013089894506
+21
+111.14544989054545
+31
+239.73728574505205
+12
+54.42013089894506
+22
+108.5456607922482
+32
+237.1374966467548
+13
+54.42013089894506
+23
+108.5456607922482
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+98.84311578843918
+30
+246.84004165056385
+11
+14.604382809215494
+21
+97.89152693398054
+31
+243.28866369780795
+12
+54.42013089894506
+22
+97.89152693398054
+32
+243.28866369780795
+13
+54.42013089894506
+23
+97.89152693398054
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+97.89152693398054
+30
+243.28866369780795
+11
+54.42013089894506
+21
+98.84311578843918
+31
+246.84004165056385
+12
+14.604382809215494
+22
+98.84311578843918
+32
+246.84004165056385
+13
+14.604382809215494
+23
+98.84311578843918
+33
+246.84004165056385
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+111.14544989054545
+30
+246.84004165056382
+11
+54.42013089894506
+21
+112.09703874500408
+31
+243.28866369780795
+12
+14.604382809215494
+22
+112.09703874500408
+32
+243.28866369780795
+13
+14.604382809215494
+23
+112.09703874500408
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+112.09703874500408
+30
+243.28866369780795
+11
+14.604382809215494
+21
+111.14544989054545
+31
+246.84004165056382
+12
+54.42013089894506
+22
+111.14544989054545
+32
+246.84004165056382
+13
+54.42013089894506
+23
+111.14544989054545
+33
+246.84004165056382
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+98.84311578843918
+30
+239.73728574505208
+11
+14.604382809215494
+21
+101.44290488673643
+31
+237.1374966467548
+12
+54.42013089894506
+22
+101.44290488673643
+32
+237.1374966467548
+13
+54.42013089894506
+23
+101.44290488673643
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+101.44290488673643
+30
+237.1374966467548
+11
+54.42013089894506
+21
+98.84311578843918
+31
+239.73728574505208
+12
+14.604382809215494
+22
+98.84311578843918
+32
+239.73728574505208
+13
+14.604382809215494
+23
+98.84311578843918
+33
+239.73728574505208
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+112.09703874500408
+30
+243.28866369780795
+11
+54.42013089894506
+21
+111.14544989054545
+31
+239.73728574505205
+12
+14.604382809215494
+22
+111.14544989054545
+32
+239.73728574505205
+13
+14.604382809215494
+23
+111.14544989054545
+33
+239.73728574505205
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+111.14544989054545
+30
+239.73728574505205
+11
+14.604382809215494
+21
+112.09703874500408
+31
+243.28866369780795
+12
+54.42013089894506
+22
+112.09703874500408
+32
+243.28866369780795
+13
+54.42013089894506
+23
+112.09703874500408
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+98.84311578843918
+30
+246.84004165056385
+11
+14.604382809215494
+21
+101.44290488673643
+31
+249.43983074886108
+12
+14.604382809215494
+22
+98.84311578843918
+32
+246.84004165056385
+13
+14.604382809215494
+23
+98.84311578843918
+33
+246.84004165056385
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+101.44290488673643
+30
+249.43983074886108
+11
+54.42013089894506
+21
+98.84311578843918
+31
+246.84004165056385
+12
+54.42013089894506
+22
+101.44290488673643
+32
+249.43983074886108
+13
+54.42013089894506
+23
+101.44290488673643
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+101.44290488673643
+30
+249.43983074886108
+11
+14.604382809215494
+21
+104.99428283949231
+31
+250.39141960331972
+12
+14.604382809215494
+22
+101.44290488673643
+32
+249.43983074886108
+13
+14.604382809215494
+23
+101.44290488673643
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+250.39141960331972
+11
+54.42013089894506
+21
+101.44290488673643
+31
+249.43983074886108
+12
+54.42013089894506
+22
+104.99428283949231
+32
+250.39141960331972
+13
+54.42013089894506
+23
+104.99428283949231
+33
+250.39141960331972
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+250.39141960331972
+11
+14.604382809215494
+21
+108.5456607922482
+31
+249.43983074886108
+12
+14.604382809215494
+22
+104.99428283949231
+32
+250.39141960331972
+13
+14.604382809215494
+23
+104.99428283949231
+33
+250.39141960331972
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+108.5456607922482
+30
+249.43983074886108
+11
+54.42013089894506
+21
+104.99428283949231
+31
+250.39141960331972
+12
+54.42013089894506
+22
+108.5456607922482
+32
+249.43983074886108
+13
+54.42013089894506
+23
+108.5456607922482
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+108.5456607922482
+30
+237.1374966467548
+11
+14.604382809215494
+21
+104.99428283949231
+31
+236.18590779229618
+12
+14.604382809215494
+22
+108.5456607922482
+32
+237.1374966467548
+13
+14.604382809215494
+23
+108.5456607922482
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+236.18590779229618
+11
+54.42013089894506
+21
+108.5456607922482
+31
+237.1374966467548
+12
+54.42013089894506
+22
+104.99428283949231
+32
+236.18590779229618
+13
+54.42013089894506
+23
+104.99428283949231
+33
+236.18590779229618
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+98.84311578843918
+31
+246.84004165056385
+12
+14.604382809215494
+22
+101.44290488673643
+32
+249.43983074886108
+13
+14.604382809215494
+23
+101.44290488673643
+33
+249.43983074886108
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+250.39141960331972
+11
+118.74585968501914
+21
+108.5456607922482
+31
+249.43983074886108
+12
+118.74585968501914
+22
+104.99428283949231
+32
+250.39141960331972
+13
+118.74585968501914
+23
+104.99428283949231
+33
+250.39141960331972
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+108.5456607922482
+30
+249.43983074886108
+11
+158.56160777474872
+21
+104.99428283949231
+31
+250.39141960331972
+12
+158.56160777474872
+22
+108.5456607922482
+32
+249.43983074886108
+13
+158.56160777474872
+23
+108.5456607922482
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+98.84311578843918
+30
+246.84004165056385
+11
+118.74585968501914
+21
+97.89152693398054
+31
+243.28866369780795
+12
+158.56160777474872
+22
+97.89152693398054
+32
+243.28866369780795
+13
+158.56160777474872
+23
+97.89152693398054
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+97.89152693398054
+30
+243.28866369780795
+11
+158.56160777474872
+21
+98.84311578843918
+31
+246.84004165056385
+12
+118.74585968501914
+22
+98.84311578843918
+32
+246.84004165056385
+13
+118.74585968501914
+23
+98.84311578843918
+33
+246.84004165056385
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+236.18590779229618
+11
+118.74585968501914
+21
+101.44290488673643
+31
+237.1374966467548
+12
+118.74585968501914
+22
+104.99428283949231
+32
+236.18590779229618
+13
+118.74585968501914
+23
+104.99428283949231
+33
+236.18590779229618
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+101.44290488673643
+30
+237.1374966467548
+11
+158.56160777474872
+21
+104.99428283949231
+31
+236.18590779229618
+12
+158.56160777474872
+22
+101.44290488673643
+32
+237.1374966467548
+13
+158.56160777474872
+23
+101.44290488673643
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+98.84311578843918
+30
+246.84004165056385
+11
+118.74585968501914
+21
+101.44290488673643
+31
+249.43983074886108
+12
+118.74585968501914
+22
+98.84311578843918
+32
+246.84004165056385
+13
+118.74585968501914
+23
+98.84311578843918
+33
+246.84004165056385
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+101.44290488673643
+30
+249.43983074886108
+11
+158.56160777474872
+21
+98.84311578843918
+31
+246.84004165056385
+12
+158.56160777474872
+22
+101.44290488673643
+32
+249.43983074886108
+13
+158.56160777474872
+23
+101.44290488673643
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+108.5456607922482
+30
+249.43983074886108
+11
+158.56160777474872
+21
+111.14544989054545
+31
+246.84004165056382
+12
+118.74585968501914
+22
+111.14544989054545
+32
+246.84004165056382
+13
+118.74585968501914
+23
+111.14544989054545
+33
+246.84004165056382
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+111.14544989054545
+30
+246.84004165056382
+11
+118.74585968501914
+21
+108.5456607922482
+31
+249.43983074886108
+12
+158.56160777474872
+22
+108.5456607922482
+32
+249.43983074886108
+13
+158.56160777474872
+23
+108.5456607922482
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+112.09703874500408
+30
+243.28866369780795
+11
+54.42013089894506
+21
+104.99428283949231
+31
+243.28866369780795
+12
+54.42013089894506
+22
+111.14544989054545
+32
+239.73728574505205
+13
+54.42013089894506
+23
+111.14544989054545
+33
+239.73728574505205
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+101.44290488673643
+31
+237.1374966467548
+12
+118.74585968501914
+22
+98.84311578843918
+32
+239.73728574505208
+13
+118.74585968501914
+23
+98.84311578843918
+33
+239.73728574505208
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+112.09703874500408
+30
+243.28866369780795
+11
+158.56160777474872
+21
+111.14544989054545
+31
+239.73728574505205
+12
+118.74585968501914
+22
+111.14544989054545
+32
+239.73728574505205
+13
+118.74585968501914
+23
+111.14544989054545
+33
+239.73728574505205
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+111.14544989054545
+30
+239.73728574505205
+11
+118.74585968501914
+21
+112.09703874500408
+31
+243.28866369780795
+12
+158.56160777474872
+22
+112.09703874500408
+32
+243.28866369780795
+13
+158.56160777474872
+23
+112.09703874500408
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+111.14544989054545
+30
+239.73728574505205
+11
+118.74585968501914
+21
+108.5456607922482
+31
+237.1374966467548
+12
+118.74585968501914
+22
+111.14544989054545
+32
+239.73728574505205
+13
+118.74585968501914
+23
+111.14544989054545
+33
+239.73728574505205
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+108.5456607922482
+30
+237.1374966467548
+11
+158.56160777474872
+21
+111.14544989054545
+31
+239.73728574505205
+12
+158.56160777474872
+22
+108.5456607922482
+32
+237.1374966467548
+13
+158.56160777474872
+23
+108.5456607922482
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+108.5456607922482
+30
+237.1374966467548
+11
+118.74585968501914
+21
+104.99428283949231
+31
+236.18590779229618
+12
+118.74585968501914
+22
+108.5456607922482
+32
+237.1374966467548
+13
+118.74585968501914
+23
+108.5456607922482
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+236.18590779229618
+11
+158.56160777474872
+21
+108.5456607922482
+31
+237.1374966467548
+12
+158.56160777474872
+22
+104.99428283949231
+32
+236.18590779229618
+13
+158.56160777474872
+23
+104.99428283949231
+33
+236.18590779229618
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+111.14544989054545
+30
+246.84004165056382
+11
+158.56160777474872
+21
+112.09703874500408
+31
+243.28866369780795
+12
+118.74585968501914
+22
+112.09703874500408
+32
+243.28866369780795
+13
+118.74585968501914
+23
+112.09703874500408
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+112.09703874500408
+30
+243.28866369780795
+11
+118.74585968501914
+21
+111.14544989054545
+31
+246.84004165056382
+12
+158.56160777474872
+22
+111.14544989054545
+32
+246.84004165056382
+13
+158.56160777474872
+23
+111.14544989054545
+33
+246.84004165056382
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+98.84311578843918
+30
+239.73728574505208
+11
+118.74585968501914
+21
+101.44290488673643
+31
+237.1374966467548
+12
+158.56160777474872
+22
+101.44290488673643
+32
+237.1374966467548
+13
+158.56160777474872
+23
+101.44290488673643
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+101.44290488673643
+30
+237.1374966467548
+11
+158.56160777474872
+21
+98.84311578843918
+31
+239.73728574505208
+12
+118.74585968501914
+22
+98.84311578843918
+32
+239.73728574505208
+13
+118.74585968501914
+23
+98.84311578843918
+33
+239.73728574505208
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+101.44290488673643
+30
+249.43983074886108
+11
+118.74585968501914
+21
+104.99428283949231
+31
+250.39141960331972
+12
+118.74585968501914
+22
+101.44290488673643
+32
+249.43983074886108
+13
+118.74585968501914
+23
+101.44290488673643
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+250.39141960331972
+11
+158.56160777474872
+21
+101.44290488673643
+31
+249.43983074886108
+12
+158.56160777474872
+22
+104.99428283949231
+32
+250.39141960331972
+13
+158.56160777474872
+23
+104.99428283949231
+33
+250.39141960331972
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+97.89152693398054
+30
+243.28866369780795
+11
+118.74585968501914
+21
+98.84311578843918
+31
+239.73728574505208
+12
+158.56160777474872
+22
+98.84311578843918
+32
+239.73728574505208
+13
+158.56160777474872
+23
+98.84311578843918
+33
+239.73728574505208
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+98.84311578843918
+30
+239.73728574505208
+11
+158.56160777474872
+21
+97.89152693398054
+31
+243.28866369780795
+12
+118.74585968501914
+22
+97.89152693398054
+32
+243.28866369780795
+13
+118.74585968501914
+23
+97.89152693398054
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+111.14544989054545
+30
+239.73728574505205
+11
+158.56160777474872
+21
+104.99428283949231
+31
+243.28866369780795
+12
+158.56160777474872
+22
+108.5456607922482
+32
+237.1374966467548
+13
+158.56160777474872
+23
+108.5456607922482
+33
+237.1374966467548
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+108.5456607922482
+30
+237.1374966467548
+11
+14.604382809215494
+21
+104.99428283949231
+31
+236.18590779229618
+12
+14.604382809215494
+22
+104.99428283949231
+32
+243.28866369780795
+13
+14.604382809215494
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+101.44290488673643
+31
+249.43983074886108
+12
+14.604382809215494
+22
+104.99428283949231
+32
+250.39141960331972
+13
+14.604382809215494
+23
+104.99428283949231
+33
+250.39141960331972
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+108.5456607922482
+30
+249.43983074886108
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+104.99428283949231
+32
+250.39141960331972
+13
+14.604382809215494
+23
+104.99428283949231
+33
+250.39141960331972
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+98.84311578843918
+31
+239.73728574505208
+12
+14.604382809215494
+22
+97.89152693398054
+32
+243.28866369780795
+13
+14.604382809215494
+23
+97.89152693398054
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+111.14544989054545
+30
+239.73728574505205
+11
+14.604382809215494
+21
+108.5456607922482
+31
+237.1374966467548
+12
+14.604382809215494
+22
+104.99428283949231
+32
+243.28866369780795
+13
+14.604382809215494
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+111.14544989054545
+30
+246.84004165056382
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+108.5456607922482
+32
+249.43983074886108
+13
+14.604382809215494
+23
+108.5456607922482
+33
+249.43983074886108
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+236.18590779229618
+11
+14.604382809215494
+21
+101.44290488673643
+31
+237.1374966467548
+12
+14.604382809215494
+22
+104.99428283949231
+32
+243.28866369780795
+13
+14.604382809215494
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+101.44290488673643
+31
+237.1374966467548
+12
+14.604382809215494
+22
+98.84311578843918
+32
+239.73728574505208
+13
+14.604382809215494
+23
+98.84311578843918
+33
+239.73728574505208
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+112.09703874500408
+30
+243.28866369780795
+11
+14.604382809215494
+21
+111.14544989054545
+31
+239.73728574505205
+12
+14.604382809215494
+22
+104.99428283949231
+32
+243.28866369780795
+13
+14.604382809215494
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+97.89152693398054
+31
+243.28866369780795
+12
+14.604382809215494
+22
+98.84311578843918
+32
+246.84004165056385
+13
+14.604382809215494
+23
+98.84311578843918
+33
+246.84004165056385
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+112.09703874500408
+30
+243.28866369780795
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+111.14544989054545
+32
+246.84004165056382
+13
+14.604382809215494
+23
+111.14544989054545
+33
+246.84004165056382
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+112.09703874500408
+30
+243.28866369780795
+11
+54.42013089894506
+21
+111.14544989054545
+31
+246.84004165056382
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+112.09703874500408
+30
+243.28866369780795
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+112.09703874500408
+32
+243.28866369780795
+13
+14.604382809215494
+23
+112.09703874500408
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+112.09703874500408
+31
+243.28866369780795
+12
+14.604382809215494
+22
+97.89152693398054
+32
+243.28866369780795
+13
+14.604382809215494
+23
+97.89152693398054
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+97.89152693398054
+30
+243.28866369780795
+11
+54.42013089894506
+21
+112.09703874500408
+31
+243.28866369780795
+12
+54.42013089894506
+22
+97.89152693398054
+32
+243.28866369780795
+13
+54.42013089894506
+23
+97.89152693398054
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+97.89152693398054
+30
+243.28866369780795
+11
+54.42013089894506
+21
+112.09703874500408
+31
+243.28866369780795
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+250.39141960331972
+11
+54.42013089894506
+21
+101.44290488673643
+31
+249.43983074886108
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+101.44290488673643
+31
+237.1374966467548
+12
+54.42013089894506
+22
+104.99428283949231
+32
+236.18590779229618
+13
+54.42013089894506
+23
+104.99428283949231
+33
+236.18590779229618
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+104.99428283949231
+31
+236.18590779229618
+12
+14.604382809215494
+22
+104.99428283949231
+32
+236.18590779229618
+13
+14.604382809215494
+23
+104.99428283949231
+33
+236.18590779229618
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+236.18590779229618
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+104.99428283949231
+32
+250.39141960331972
+13
+14.604382809215494
+23
+104.99428283949231
+33
+250.39141960331972
+70
+13
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+104.99428283949231
+31
+250.39141960331972
+12
+54.42013089894506
+22
+104.99428283949231
+32
+250.39141960331972
+13
+54.42013089894506
+23
+104.99428283949231
+33
+250.39141960331972
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+101.44290488673643
+31
+249.43983074886108
+12
+54.42013089894506
+22
+98.84311578843918
+32
+246.84004165056385
+13
+54.42013089894506
+23
+98.84311578843918
+33
+246.84004165056385
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+108.5456607922482
+30
+237.1374966467548
+11
+54.42013089894506
+21
+104.99428283949231
+31
+243.28866369780795
+12
+54.42013089894506
+22
+104.99428283949231
+32
+236.18590779229618
+13
+54.42013089894506
+23
+104.99428283949231
+33
+236.18590779229618
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+108.5456607922482
+31
+237.1374966467548
+12
+54.42013089894506
+22
+108.5456607922482
+32
+237.1374966467548
+13
+54.42013089894506
+23
+108.5456607922482
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+108.5456607922482
+30
+237.1374966467548
+11
+54.42013089894506
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+104.99428283949231
+32
+243.28866369780795
+13
+14.604382809215494
+23
+104.99428283949231
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+104.99428283949231
+31
+243.28866369780795
+12
+54.42013089894506
+22
+101.44290488673643
+32
+249.43983074886108
+13
+54.42013089894506
+23
+101.44290488673643
+33
+249.43983074886108
+70
+13
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+101.44290488673643
+31
+249.43983074886108
+12
+14.604382809215494
+22
+101.44290488673643
+32
+249.43983074886108
+13
+14.604382809215494
+23
+101.44290488673643
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+111.14544989054545
+30
+239.73728574505205
+11
+54.42013089894506
+21
+104.99428283949231
+31
+243.28866369780795
+12
+54.42013089894506
+22
+108.5456607922482
+32
+237.1374966467548
+13
+54.42013089894506
+23
+108.5456607922482
+33
+237.1374966467548
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+98.84311578843918
+31
+246.84004165056385
+12
+54.42013089894506
+22
+97.89152693398054
+32
+243.28866369780795
+13
+54.42013089894506
+23
+97.89152693398054
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+111.14544989054545
+30
+239.73728574505205
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+111.14544989054545
+32
+239.73728574505205
+13
+14.604382809215494
+23
+111.14544989054545
+33
+239.73728574505205
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+111.14544989054545
+31
+239.73728574505205
+12
+14.604382809215494
+22
+98.84311578843918
+32
+246.84004165056385
+13
+14.604382809215494
+23
+98.84311578843918
+33
+246.84004165056385
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+98.84311578843918
+30
+246.84004165056385
+11
+54.42013089894506
+21
+111.14544989054545
+31
+239.73728574505205
+12
+54.42013089894506
+22
+98.84311578843918
+32
+246.84004165056385
+13
+54.42013089894506
+23
+98.84311578843918
+33
+246.84004165056385
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+98.84311578843918
+30
+246.84004165056385
+11
+54.42013089894506
+21
+111.14544989054545
+31
+239.73728574505205
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+97.89152693398054
+31
+243.28866369780795
+12
+54.42013089894506
+22
+98.84311578843918
+32
+239.73728574505208
+13
+54.42013089894506
+23
+98.84311578843918
+33
+239.73728574505208
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+111.14544989054545
+30
+246.84004165056382
+11
+54.42013089894506
+21
+108.5456607922482
+31
+249.43983074886108
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+111.14544989054545
+30
+246.84004165056382
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+111.14544989054545
+32
+246.84004165056382
+13
+14.604382809215494
+23
+111.14544989054545
+33
+246.84004165056382
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+111.14544989054545
+31
+246.84004165056382
+12
+14.604382809215494
+22
+98.84311578843918
+32
+239.73728574505208
+13
+14.604382809215494
+23
+98.84311578843918
+33
+239.73728574505208
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+98.84311578843918
+30
+239.73728574505208
+11
+54.42013089894506
+21
+111.14544989054545
+31
+246.84004165056382
+12
+54.42013089894506
+22
+98.84311578843918
+32
+239.73728574505208
+13
+54.42013089894506
+23
+98.84311578843918
+33
+239.73728574505208
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+98.84311578843918
+30
+239.73728574505208
+11
+54.42013089894506
+21
+111.14544989054545
+31
+246.84004165056382
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+98.84311578843918
+31
+239.73728574505208
+12
+54.42013089894506
+22
+101.44290488673643
+32
+237.1374966467548
+13
+54.42013089894506
+23
+101.44290488673643
+33
+237.1374966467548
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+108.5456607922482
+30
+249.43983074886108
+11
+54.42013089894506
+21
+104.99428283949231
+31
+250.39141960331972
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+14.604382809215494
+20
+104.99428283949231
+30
+243.28866369780795
+11
+54.42013089894506
+21
+101.44290488673643
+31
+237.1374966467548
+12
+14.604382809215494
+22
+101.44290488673643
+32
+237.1374966467548
+13
+14.604382809215494
+23
+101.44290488673643
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+101.44290488673643
+30
+237.1374966467548
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+54.42013089894506
+22
+104.99428283949231
+32
+243.28866369780795
+13
+54.42013089894506
+23
+104.99428283949231
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+104.99428283949231
+31
+243.28866369780795
+12
+14.604382809215494
+22
+108.5456607922482
+32
+249.43983074886108
+13
+14.604382809215494
+23
+108.5456607922482
+33
+249.43983074886108
+70
+13
+  0
+3DFACE
+ 8
+hinges_half
+10
+54.42013089894506
+20
+104.99428283949231
+30
+243.28866369780795
+11
+14.604382809215494
+21
+108.5456607922482
+31
+249.43983074886108
+12
+54.42013089894506
+22
+108.5456607922482
+32
+249.43983074886108
+13
+54.42013089894506
+23
+108.5456607922482
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+108.5456607922482
+30
+237.1374966467548
+11
+118.74585968501914
+21
+104.99428283949231
+31
+236.18590779229618
+12
+118.74585968501914
+22
+104.99428283949231
+32
+243.28866369780795
+13
+118.74585968501914
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+101.44290488673643
+31
+249.43983074886108
+12
+118.74585968501914
+22
+104.99428283949231
+32
+250.39141960331972
+13
+118.74585968501914
+23
+104.99428283949231
+33
+250.39141960331972
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+111.14544989054545
+30
+239.73728574505205
+11
+118.74585968501914
+21
+108.5456607922482
+31
+237.1374966467548
+12
+118.74585968501914
+22
+104.99428283949231
+32
+243.28866369780795
+13
+118.74585968501914
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+112.09703874500408
+30
+243.28866369780795
+11
+118.74585968501914
+21
+111.14544989054545
+31
+239.73728574505205
+12
+118.74585968501914
+22
+104.99428283949231
+32
+243.28866369780795
+13
+118.74585968501914
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+98.84311578843918
+31
+246.84004165056385
+12
+118.74585968501914
+22
+101.44290488673643
+32
+249.43983074886108
+13
+118.74585968501914
+23
+101.44290488673643
+33
+249.43983074886108
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+112.09703874500408
+30
+243.28866369780795
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+111.14544989054545
+32
+246.84004165056382
+13
+118.74585968501914
+23
+111.14544989054545
+33
+246.84004165056382
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+97.89152693398054
+31
+243.28866369780795
+12
+118.74585968501914
+22
+98.84311578843918
+32
+246.84004165056385
+13
+118.74585968501914
+23
+98.84311578843918
+33
+246.84004165056385
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+108.5456607922482
+30
+249.43983074886108
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+104.99428283949231
+32
+250.39141960331972
+13
+118.74585968501914
+23
+104.99428283949231
+33
+250.39141960331972
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+98.84311578843918
+31
+239.73728574505208
+12
+118.74585968501914
+22
+97.89152693398054
+32
+243.28866369780795
+13
+118.74585968501914
+23
+97.89152693398054
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+236.18590779229618
+11
+118.74585968501914
+21
+101.44290488673643
+31
+237.1374966467548
+12
+118.74585968501914
+22
+104.99428283949231
+32
+243.28866369780795
+13
+118.74585968501914
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+111.14544989054545
+30
+246.84004165056382
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+108.5456607922482
+32
+249.43983074886108
+13
+118.74585968501914
+23
+108.5456607922482
+33
+249.43983074886108
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+101.44290488673643
+31
+237.1374966467548
+12
+158.56160777474872
+22
+104.99428283949231
+32
+236.18590779229618
+13
+158.56160777474872
+23
+104.99428283949231
+33
+236.18590779229618
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+104.99428283949231
+31
+236.18590779229618
+12
+118.74585968501914
+22
+104.99428283949231
+32
+236.18590779229618
+13
+118.74585968501914
+23
+104.99428283949231
+33
+236.18590779229618
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+236.18590779229618
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+104.99428283949231
+32
+250.39141960331972
+13
+118.74585968501914
+23
+104.99428283949231
+33
+250.39141960331972
+70
+13
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+104.99428283949231
+31
+250.39141960331972
+12
+158.56160777474872
+22
+104.99428283949231
+32
+250.39141960331972
+13
+158.56160777474872
+23
+104.99428283949231
+33
+250.39141960331972
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+108.5456607922482
+30
+249.43983074886108
+11
+158.56160777474872
+21
+104.99428283949231
+31
+250.39141960331972
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+98.84311578843918
+31
+239.73728574505208
+12
+158.56160777474872
+22
+101.44290488673643
+32
+237.1374966467548
+13
+158.56160777474872
+23
+101.44290488673643
+33
+237.1374966467548
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+101.44290488673643
+31
+237.1374966467548
+12
+118.74585968501914
+22
+101.44290488673643
+32
+237.1374966467548
+13
+118.74585968501914
+23
+101.44290488673643
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+101.44290488673643
+30
+237.1374966467548
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+108.5456607922482
+32
+249.43983074886108
+13
+118.74585968501914
+23
+108.5456607922482
+33
+249.43983074886108
+70
+13
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+108.5456607922482
+31
+249.43983074886108
+12
+158.56160777474872
+22
+108.5456607922482
+32
+249.43983074886108
+13
+158.56160777474872
+23
+108.5456607922482
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+97.89152693398054
+31
+243.28866369780795
+12
+158.56160777474872
+22
+98.84311578843918
+32
+239.73728574505208
+13
+158.56160777474872
+23
+98.84311578843918
+33
+239.73728574505208
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+111.14544989054545
+30
+246.84004165056382
+11
+158.56160777474872
+21
+108.5456607922482
+31
+249.43983074886108
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+111.14544989054545
+30
+246.84004165056382
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+111.14544989054545
+32
+246.84004165056382
+13
+118.74585968501914
+23
+111.14544989054545
+33
+246.84004165056382
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+111.14544989054545
+31
+246.84004165056382
+12
+118.74585968501914
+22
+98.84311578843918
+32
+239.73728574505208
+13
+118.74585968501914
+23
+98.84311578843918
+33
+239.73728574505208
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+98.84311578843918
+30
+239.73728574505208
+11
+158.56160777474872
+21
+111.14544989054545
+31
+246.84004165056382
+12
+158.56160777474872
+22
+98.84311578843918
+32
+239.73728574505208
+13
+158.56160777474872
+23
+98.84311578843918
+33
+239.73728574505208
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+98.84311578843918
+30
+239.73728574505208
+11
+158.56160777474872
+21
+111.14544989054545
+31
+246.84004165056382
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+250.39141960331972
+11
+158.56160777474872
+21
+101.44290488673643
+31
+249.43983074886108
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+112.09703874500408
+30
+243.28866369780795
+11
+158.56160777474872
+21
+111.14544989054545
+31
+246.84004165056382
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+112.09703874500408
+30
+243.28866369780795
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+112.09703874500408
+32
+243.28866369780795
+13
+118.74585968501914
+23
+112.09703874500408
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+112.09703874500408
+31
+243.28866369780795
+12
+118.74585968501914
+22
+97.89152693398054
+32
+243.28866369780795
+13
+118.74585968501914
+23
+97.89152693398054
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+97.89152693398054
+30
+243.28866369780795
+11
+158.56160777474872
+21
+112.09703874500408
+31
+243.28866369780795
+12
+158.56160777474872
+22
+97.89152693398054
+32
+243.28866369780795
+13
+158.56160777474872
+23
+97.89152693398054
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+97.89152693398054
+30
+243.28866369780795
+11
+158.56160777474872
+21
+112.09703874500408
+31
+243.28866369780795
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+108.5456607922482
+30
+237.1374966467548
+11
+158.56160777474872
+21
+104.99428283949231
+31
+243.28866369780795
+12
+158.56160777474872
+22
+104.99428283949231
+32
+236.18590779229618
+13
+158.56160777474872
+23
+104.99428283949231
+33
+236.18590779229618
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+98.84311578843918
+31
+246.84004165056385
+12
+158.56160777474872
+22
+97.89152693398054
+32
+243.28866369780795
+13
+158.56160777474872
+23
+97.89152693398054
+33
+243.28866369780795
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+118.74585968501914
+21
+108.5456607922482
+31
+237.1374966467548
+12
+158.56160777474872
+22
+108.5456607922482
+32
+237.1374966467548
+13
+158.56160777474872
+23
+108.5456607922482
+33
+237.1374966467548
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+108.5456607922482
+30
+237.1374966467548
+11
+158.56160777474872
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+104.99428283949231
+32
+243.28866369780795
+13
+118.74585968501914
+23
+104.99428283949231
+33
+243.28866369780795
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+104.99428283949231
+31
+243.28866369780795
+12
+158.56160777474872
+22
+101.44290488673643
+32
+249.43983074886108
+13
+158.56160777474872
+23
+101.44290488673643
+33
+249.43983074886108
+70
+13
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+101.44290488673643
+31
+249.43983074886108
+12
+118.74585968501914
+22
+101.44290488673643
+32
+249.43983074886108
+13
+118.74585968501914
+23
+101.44290488673643
+33
+249.43983074886108
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+101.44290488673643
+31
+249.43983074886108
+12
+158.56160777474872
+22
+98.84311578843918
+32
+246.84004165056385
+13
+158.56160777474872
+23
+98.84311578843918
+33
+246.84004165056385
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+112.09703874500408
+30
+243.28866369780795
+11
+158.56160777474872
+21
+104.99428283949231
+31
+243.28866369780795
+12
+158.56160777474872
+22
+111.14544989054545
+32
+239.73728574505205
+13
+158.56160777474872
+23
+111.14544989054545
+33
+239.73728574505205
+70
+0
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+98.84311578843918
+30
+246.84004165056385
+11
+118.74585968501914
+21
+104.99428283949231
+31
+243.28866369780795
+12
+118.74585968501914
+22
+98.84311578843918
+32
+246.84004165056385
+13
+118.74585968501914
+23
+98.84311578843918
+33
+246.84004165056385
+70
+1
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+104.99428283949231
+30
+243.28866369780795
+11
+158.56160777474872
+21
+98.84311578843918
+31
+246.84004165056385
+12
+118.74585968501914
+22
+111.14544989054545
+32
+239.73728574505205
+13
+118.74585968501914
+23
+111.14544989054545
+33
+239.73728574505205
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+118.74585968501914
+20
+111.14544989054545
+30
+239.73728574505205
+11
+158.56160777474872
+21
+98.84311578843918
+31
+246.84004165056385
+12
+158.56160777474872
+22
+111.14544989054545
+32
+239.73728574505205
+13
+158.56160777474872
+23
+111.14544989054545
+33
+239.73728574505205
+70
+3
+  0
+3DFACE
+ 8
+hinges_half
+10
+158.56160777474872
+20
+111.14544989054545
+30
+239.73728574505205
+11
+158.56160777474872
+21
+98.84311578843918
+31
+246.84004165056385
+12
+158.56160777474872
+22
+104.99428283949231
+32
+243.28866369780795
+13
+158.56160777474872
+23
+104.99428283949231
+33
+243.28866369780795
+70
+1
+  0
+LINE
+ 8
+lid
+10
+173.57615198054154
+20
+89.62086612360419
+30
+247.01850393700798
+11
+173.57615198059867
+21
+89.61894967981907
+31
+247.0180258490584
+  0
+LINE
+ 8
+lid
+10
+173.5761520091809
+20
+101.88267226235872
+30
+231.96338551235425
+11
+173.62378941037633
+21
+101.88267714864598
+31
+231.96338582677174
+  0
+3DFACE
+ 8
+lid
+10
+155.25962204721512
+20
+-94.91475962800925
+30
+200.98174278680736
+11
+173.40095962196486
+21
+-61.09528078179717
+31
+209.41850264783722
+12
+155.25962103928768
+22
+-61.09527615457002
+32
+209.41889763779537
+13
+155.25962103928768
+23
+-61.09527615457002
+33
+209.41889763779537
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095962196486
+20
+-61.09528078179717
+30
+209.41850264783722
+11
+155.25962204721512
+21
+-94.91475962800925
+31
+200.98174278680736
+12
+155.2596220472446
+22
+-94.91574859551494
+32
+200.98149606299222
+13
+155.2596220472446
+23
+-94.91574859551494
+33
+200.98149606299222
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40095962196486
+20
+-61.09528078179717
+30
+209.41850264783722
+11
+155.2596220472446
+21
+-94.91574859551494
+31
+200.98149606299222
+12
+173.40096062992177
+22
+-94.91575322274208
+32
+200.98149477382145
+13
+173.40096062992177
+23
+-94.91575322274208
+33
+200.98149477382145
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758553285672875
+20
+98.66928670640114
+30
+244.8444881889765
+11
+18.758559055118518
+21
+-94.91575266367778
+31
+200.98149606299222
+12
+18.75855926275502
+22
+-101.88268179753607
+32
+194.81220472440953
+13
+18.75855926275502
+23
+-101.88268179753607
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758559055118518
+20
+-94.91575266367778
+30
+200.98149606299222
+11
+18.758553285672875
+21
+98.66928670640114
+31
+244.8444881889765
+12
+18.758558047161614
+22
+-61.09528022273286
+32
+209.41889763779537
+13
+18.758558047161614
+23
+-61.09528022273286
+33
+209.41889763779537
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+18.758558047161614
+20
+-61.09528022273286
+30
+209.41889763779537
+11
+18.758553285672875
+21
+98.66928670640114
+31
+244.8444881889765
+12
+18.758554555475172
+22
+56.06298749380262
+32
+238.6468503937009
+13
+18.758554555475172
+23
+56.06298749380262
+33
+238.6468503937009
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+18.758554555475172
+20
+56.06298749380262
+30
+238.6468503937009
+11
+18.758553285672875
+21
+98.66928670640114
+31
+244.8444881889765
+12
+18.758553555344527
+22
+89.62086150955072
+32
+247.01850393700798
+13
+18.758553555344527
+23
+89.62086150955072
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+54.371033870478705
+20
+89.61503930110042
+30
+247.01705118848423
+11
+68.94111261046264
+21
+89.62086300514947
+31
+247.01850393700798
+12
+54.371033870305155
+22
+89.62086257091512
+32
+247.01850393700798
+13
+54.371033870305155
+23
+89.62086257091512
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.94111261046264
+20
+89.62086300514947
+30
+247.01850393700798
+11
+54.371033870478705
+21
+89.61503930110042
+31
+247.01705118848423
+12
+54.371033870788715
+22
+89.6046374506915
+32
+247.01445620777636
+13
+54.371033870788715
+23
+89.6046374506915
+33
+247.01445620777636
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+68.94111261046264
+20
+89.62086300514947
+30
+247.01850393700798
+11
+54.371033870788715
+21
+89.6046374506915
+31
+247.01445620777636
+12
+54.371038362122235
+22
+-61.09527916136846
+32
+209.41889763779537
+13
+54.371038362122235
+23
+-61.09527916136846
+33
+209.41889763779537
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+68.94111261046264
+20
+89.62086300514947
+30
+247.01850393700798
+11
+54.371038362122235
+21
+-61.09527916136846
+31
+209.41889763779537
+12
+54.371039370079146
+22
+-94.91575160231338
+32
+200.98149606299222
+13
+54.371039370079146
+23
+-94.91575160231338
+33
+200.98149606299222
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+68.94111261046264
+20
+89.62086300514947
+30
+247.01850393700798
+11
+54.371039370079146
+21
+-94.91575160231338
+31
+200.98149606299222
+12
+68.94111261059379
+22
+89.61646217847384
+32
+247.01740604706768
+13
+68.94111261059379
+23
+89.61646217847384
+33
+247.01740604706768
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+68.94111261059379
+20
+89.61646217847384
+30
+247.01740604706768
+11
+54.371039370079146
+21
+-94.91575160231338
+31
+200.98149606299222
+12
+68.9411181101801
+22
+-94.91385437913766
+32
+200.98196926170036
+13
+68.9411181101801
+23
+-94.91385437913766
+33
+200.98196926170036
+70
+15
+  0
+3DFACE
+ 8
+lid
+10
+68.9411181101801
+20
+-94.91385437913766
+30
+200.98196926170036
+11
+54.371039370079146
+21
+-94.91575160231338
+31
+200.98149606299222
+12
+68.94111811023663
+22
+-94.91575116807901
+32
+200.98149606299222
+13
+68.94111811023663
+23
+-94.91575116807901
+33
+200.98149606299222
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.94111261059379
+20
+89.61646217847384
+30
+247.01740604706768
+11
+68.9411181101801
+21
+-94.91385437913766
+31
+200.98196926170036
+12
+68.94111261077951
+22
+89.61023080877725
+32
+247.01585148502548
+13
+68.94111261077951
+23
+89.61023080877725
+33
+247.01585148502548
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-5.499773591655099e-06
+20
+89.62086095048643
+30
+247.01850393700798
+11
+18.758553285672875
+21
+98.66928670640114
+31
+244.8444881889765
+12
+-5.769445243331006e-06
+22
+98.66928614733685
+32
+244.8444881889765
+13
+-5.769445243331006e-06
+23
+98.66928614733685
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758553285672875
+20
+98.66928670640114
+30
+244.8444881889765
+11
+-5.499773591655099e-06
+21
+89.62086095048643
+31
+247.01850393700798
+12
+18.758553555344527
+22
+89.62086150955072
+32
+247.01850393700798
+13
+18.758553555344527
+23
+89.62086150955072
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+2.0763689256853013e-07
+20
+-101.88268235660034
+30
+194.81220472440953
+11
+18.758559055118518
+21
+-94.91575266367778
+31
+200.98149606299222
+12
+3.9834802123550617e-13
+22
+-94.91575322274208
+32
+200.98149606299222
+13
+3.9834802123550617e-13
+23
+-94.91575322274208
+33
+200.98149606299222
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758559055118518
+20
+-94.91575266367778
+30
+200.98149606299222
+11
+2.0763689256853013e-07
+21
+-101.88268235660034
+31
+194.81220472440953
+12
+18.75855926275502
+22
+-101.88268179753607
+32
+194.81220472440953
+13
+18.75855926275502
+23
+-101.88268179753607
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.25962087534685
+20
+-55.5944887529952
+30
+216.04960629921268
+11
+155.25961754760118
+21
+56.06299156196549
+31
+238.6468503937009
+12
+155.25962103928768
+22
+-61.09527615457002
+32
+209.41889763779537
+13
+155.25962103928768
+23
+-61.09527615457002
+33
+209.41889763779537
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.25961754760118
+20
+56.06299156196549
+30
+238.6468503937009
+11
+155.25962087534685
+21
+-55.5944887529952
+31
+216.04960629921268
+12
+155.25961785266122
+22
+45.827164790311954
+32
+241.3515748031497
+13
+155.25961785266122
+23
+45.827164790311954
+33
+241.3515748031497
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.2596222548811
+20
+-101.8826777293732
+30
+194.81220472440953
+11
+173.40096062992177
+21
+-94.91575322274208
+31
+200.98149477382145
+12
+155.2596220472446
+22
+-94.91574859551494
+32
+200.98149606299222
+13
+155.2596220472446
+23
+-94.91574859551494
+33
+200.98149606299222
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40096062992177
+20
+-94.91575322274208
+30
+200.98149477382145
+11
+155.2596222548811
+21
+-101.8826777293732
+31
+194.81220472440953
+12
+173.40096083755827
+22
+-101.88268235660034
+32
+194.81220343523876
+13
+173.40096083755827
+23
+-101.88268235660034
+33
+194.81220343523876
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+126.31788399433434
+20
+98.66928991200895
+30
+244.8444881889765
+11
+126.31788976377997
+21
+-94.91574945806998
+31
+200.98149606299222
+12
+126.31788997141648
+22
+-101.88267859192827
+32
+194.81220472440953
+13
+126.31788997141648
+23
+-101.88267859192827
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+126.31788976377997
+20
+-94.91574945806998
+30
+200.98149606299222
+11
+126.31788399433434
+21
+98.66928991200895
+31
+244.8444881889765
+12
+126.31788426410866
+22
+89.61741980576197
+32
+247.0176445231636
+13
+126.31788426410866
+23
+89.61741980576197
+33
+247.0176445231636
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+126.31788426410866
+20
+89.61741980576197
+30
+247.0176445231636
+11
+126.31788399433434
+21
+98.66928991200895
+31
+244.8444881889765
+12
+126.31788426406312
+22
+89.61894827418696
+32
+247.01802583563773
+13
+126.31788426406312
+23
+89.61894827418696
+33
+247.01802583563773
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+126.31788426406312
+20
+89.61894827418696
+30
+247.01802583563773
+11
+126.31788399433434
+21
+98.66928991200895
+31
+244.8444881889765
+12
+126.317884264006
+22
+89.6208647151585
+32
+247.01850393700798
+13
+126.317884264006
+23
+89.6208647151585
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+111.7745771773918
+20
+89.62086428172206
+30
+247.01850393700798
+11
+126.31788399433434
+21
+98.66928991200895
+31
+244.8444881889765
+12
+111.77457690772016
+22
+98.66928947857245
+32
+244.8444881889765
+13
+111.77457690772016
+23
+98.66928947857245
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+126.31788399433434
+20
+98.66928991200895
+30
+244.8444881889765
+11
+111.7745771773918
+21
+89.62086428172206
+31
+247.01850393700798
+12
+126.317884264006
+22
+89.6208647151585
+32
+247.01850393700798
+13
+126.317884264006
+23
+89.6208647151585
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+111.7745826771658
+20
+-94.91574989150647
+30
+200.98149606299222
+11
+111.77457690772016
+21
+98.66928947857245
+31
+244.8444881889765
+12
+111.7745828848023
+22
+-101.88267902536472
+32
+194.81220472440953
+13
+111.7745828848023
+23
+-101.88267902536472
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+111.77457690772016
+20
+98.66928947857245
+30
+244.8444881889765
+11
+111.7745826771658
+21
+-94.91574989150647
+31
+200.98149606299222
+12
+111.7745771773918
+22
+89.62086428172206
+32
+247.01850393700798
+13
+111.7745771773918
+23
+89.62086428172206
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+111.7745828848023
+20
+-101.88267902536472
+30
+194.81220472440953
+11
+126.31788976377997
+21
+-94.91574945806998
+31
+200.98149606299222
+12
+111.7745826771658
+22
+-94.91574989150647
+32
+200.98149606299222
+13
+111.7745826771658
+23
+-94.91574989150647
+33
+200.98149606299222
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+126.31788976377997
+20
+-94.91574945806998
+30
+200.98149606299222
+11
+111.7745828848023
+21
+-101.88267902536472
+31
+194.81220472440953
+12
+126.31788997141648
+22
+-101.88267859192827
+32
+194.81220472440953
+13
+126.31788997141648
+23
+-101.88267859192827
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+54.371033870305155
+20
+89.62086257091512
+30
+247.01850393700798
+11
+68.941112340791
+21
+98.66928820199992
+31
+244.8444881889765
+12
+54.37103360063351
+22
+98.66928776776554
+32
+244.8444881889765
+13
+54.37103360063351
+23
+98.66928776776554
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.941112340791
+20
+98.66928820199992
+30
+244.8444881889765
+11
+54.371033870305155
+21
+89.62086257091512
+31
+247.01850393700798
+12
+68.94111261046264
+22
+89.62086300514947
+32
+247.01850393700798
+13
+68.94111261046264
+23
+89.62086300514947
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+54.371039577715635
+20
+-101.88268073617166
+30
+194.81220472440953
+11
+68.94111811023663
+21
+-94.91575116807901
+31
+200.98149606299222
+12
+54.371039370079146
+22
+-94.91575160231338
+32
+200.98149606299222
+13
+54.371039370079146
+23
+-94.91575160231338
+33
+200.98149606299222
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.94111811023663
+20
+-94.91575116807901
+30
+200.98149606299222
+11
+54.371039577715635
+21
+-101.88268073617166
+31
+194.81220472440953
+12
+68.94111831787316
+22
+-101.8826803019373
+32
+194.81220472440953
+13
+68.94111831787316
+23
+-101.8826803019373
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-4.499642948463389e-06
+20
+56.06298693473833
+30
+238.6468503937009
+11
+18.758553555344527
+21
+89.62086150955072
+31
+247.01850393700798
+12
+-5.499773591655099e-06
+22
+89.62086095048643
+32
+247.01850393700798
+13
+-5.499773591655099e-06
+23
+89.62086095048643
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758553555344527
+20
+89.62086150955072
+30
+247.01850393700798
+11
+-4.499642948463389e-06
+21
+56.06298693473833
+31
+238.6468503937009
+12
+18.758554555475172
+22
+56.06298749380262
+32
+238.6468503937009
+13
+18.758554555475172
+23
+56.06298749380262
+33
+238.6468503937009
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-1.1718973480512318e-06
+20
+-55.594493380222346
+30
+216.04960629921268
+11
+18.758554860535142
+21
+45.82716072214909
+31
+241.3515748031497
+12
+-4.194582979177852e-06
+22
+45.8271601630848
+32
+241.3515748031497
+13
+-4.194582979177852e-06
+23
+45.8271601630848
+33
+241.3515748031497
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758554860535142
+20
+45.82716072214909
+30
+241.3515748031497
+11
+-1.1718973480512318e-06
+21
+-55.594493380222346
+31
+216.04960629921268
+12
+18.758557883220774
+22
+-55.594492821158035
+32
+216.04960629921268
+13
+18.758557883220774
+23
+-55.594492821158035
+33
+216.04960629921268
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.25961654757327
+20
+89.61742066705047
+30
+247.01764453815136
+11
+173.40095513014776
+21
+89.62086095048643
+31
+247.0185026478372
+12
+155.2596165474706
+22
+89.62086557771359
+32
+247.01850393700798
+13
+155.2596165474706
+23
+89.62086557771359
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095513014776
+20
+89.62086095048643
+30
+247.0185026478372
+11
+155.25961654757327
+21
+89.61742066705047
+31
+247.01764453815136
+12
+155.25961654768776
+22
+89.61358009666061
+32
+247.01668643440806
+13
+155.25961654768776
+23
+89.61358009666061
+33
+247.01668643440806
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40095513014776
+20
+89.62086095048643
+30
+247.0185026478372
+11
+155.25961654768776
+21
+89.61358009666061
+31
+247.01668643440806
+12
+155.2596165477875
+22
+89.61023337167238
+32
+247.01585152984606
+13
+155.2596165477875
+23
+89.61023337167238
+33
+247.01585152984606
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40095513014776
+20
+89.62086095048643
+30
+247.0185026478372
+11
+155.2596165477875
+21
+89.61023337167238
+31
+247.01585152984606
+12
+155.25961654795418
+22
+89.60464044290792
+32
+247.0144562667884
+13
+155.25961654795418
+23
+89.60464044290792
+33
+247.0144562667884
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40095513014776
+20
+89.62086095048643
+30
+247.0185026478372
+11
+155.25961654795418
+21
+89.60464044290792
+31
+247.0144562667884
+12
+155.25961754760118
+22
+56.06299156196549
+32
+238.6468503937009
+13
+155.25961754760118
+23
+56.06299156196549
+33
+238.6468503937009
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40095513014776
+20
+89.62086095048643
+30
+247.0185026478372
+11
+155.25961754760118
+21
+56.06299156196549
+31
+238.6468503937009
+12
+173.40095613027842
+22
+56.06298693473833
+32
+238.64684910453013
+13
+173.40095613027842
+23
+56.06298693473833
+33
+238.64684910453013
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758558047161614
+20
+-61.09528022273286
+30
+209.41889763779537
+11
+-1.1718973480512318e-06
+21
+-55.594493380222346
+31
+216.04960629921268
+12
+-1.007956508480845e-06
+22
+-61.09528078179717
+32
+209.41850393700798
+13
+-1.007956508480845e-06
+23
+-61.09528078179717
+33
+209.41850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-1.1718973480512318e-06
+20
+-55.594493380222346
+30
+216.04960629921268
+11
+18.758558047161614
+21
+-61.09528022273286
+31
+209.41889763779537
+12
+18.758557883220774
+22
+-55.594492821158035
+32
+216.04960629921268
+13
+18.758557883220774
+23
+-55.594492821158035
+33
+216.04960629921268
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-4.194582979177852e-06
+20
+45.8271601630848
+30
+241.3515748031497
+11
+18.758554555475172
+21
+56.06298749380262
+31
+238.6468503937009
+12
+-4.499642948463389e-06
+22
+56.06298693473833
+32
+238.6468503937009
+13
+-4.499642948463389e-06
+23
+56.06298693473833
+33
+238.6468503937009
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758554555475172
+20
+56.06298749380262
+30
+238.6468503937009
+11
+-4.194582979177852e-06
+21
+45.8271601630848
+31
+241.3515748031497
+12
+18.758554860535142
+22
+45.82716072214909
+32
+241.3515748031497
+13
+18.758554860535142
+23
+45.82716072214909
+33
+241.3515748031497
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+3.9834802123550617e-13
+20
+-94.91575322274208
+30
+200.98149606299222
+11
+18.758558047161614
+21
+-61.09528022273286
+31
+209.41889763779537
+12
+-1.007956508480845e-06
+22
+-61.09528078179717
+32
+209.41850393700798
+13
+-1.007956508480845e-06
+23
+-61.09528078179717
+33
+209.41850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758558047161614
+20
+-61.09528022273286
+30
+209.41889763779537
+11
+3.9834802123550617e-13
+21
+-94.91575322274208
+31
+200.98149606299222
+12
+18.758559055118518
+22
+-94.91575266367778
+32
+200.98149606299222
+13
+18.758559055118518
+23
+-94.91575266367778
+33
+200.98149606299222
+70
+1
+  0
+LINE
+ 8
+lid
+10
+173.57615198059867
+20
+89.61894967981907
+30
+247.0180258490584
+11
+173.57615198063553
+21
+89.61771272440437
+31
+247.01771727049086
+  0
+LINE
+ 8
+lid
+10
+173.57615198063553
+20
+89.61771272440437
+30
+247.01771727049086
+11
+173.5761519806727
+21
+89.61646528989151
+31
+247.01740607774255
+  0
+LINE
+ 8
+lid
+10
+173.5761519806727
+20
+89.61646528989151
+30
+247.01740607774255
+11
+173.57615198071508
+21
+89.61504284452782
+31
+247.01705122570223
+  0
+3DFACE
+ 8
+lid
+10
+155.25961785266122
+20
+45.827164790311954
+30
+241.3515748031497
+11
+173.40095613027842
+21
+56.06298693473833
+31
+238.64684910453013
+12
+155.25961754760118
+22
+56.06299156196549
+32
+238.6468503937009
+13
+155.25961754760118
+23
+56.06299156196549
+33
+238.6468503937009
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095613027842
+20
+56.06298693473833
+30
+238.64684910453013
+11
+155.25961785266122
+21
+45.827164790311954
+31
+241.3515748031497
+12
+173.4009564353384
+22
+45.8271601630848
+32
+241.35157351397893
+13
+173.4009564353384
+23
+45.8271601630848
+33
+241.35157351397893
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.2596165474706
+20
+89.62086557771359
+30
+247.01850393700798
+11
+173.40095486047613
+21
+98.66928614733685
+31
+244.84448689980573
+12
+155.25961627779895
+22
+98.66929077456398
+32
+244.8444881889765
+13
+155.25961627779895
+23
+98.66929077456398
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095486047613
+20
+98.66928614733685
+30
+244.84448689980573
+11
+155.2596165474706
+21
+89.62086557771359
+31
+247.01850393700798
+12
+173.40095513014776
+22
+89.62086095048643
+32
+247.0185026478372
+13
+173.40095513014776
+23
+89.62086095048643
+33
+247.0185026478372
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.25962087534685
+20
+-55.5944887529952
+30
+216.04960629921268
+11
+173.4009564353384
+21
+45.8271601630848
+31
+241.35157351397893
+12
+155.25961785266122
+22
+45.827164790311954
+32
+241.3515748031497
+13
+155.25961785266122
+23
+45.827164790311954
+33
+241.3515748031497
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.4009564353384
+20
+45.8271601630848
+30
+241.35157351397893
+11
+155.25962087534685
+21
+-55.5944887529952
+31
+216.04960629921268
+12
+173.40095945802403
+22
+-55.594493380222346
+32
+216.04960501004192
+13
+173.40095945802403
+23
+-55.594493380222346
+33
+216.04960501004192
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095945802403
+20
+-55.594493380222346
+30
+216.04960501004192
+11
+155.25962103928768
+21
+-61.09527615457002
+31
+209.41889763779537
+12
+173.40095962196486
+22
+-61.09528078179717
+32
+209.41850264783722
+13
+173.40095962196486
+23
+-61.09528078179717
+33
+209.41850264783722
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.25962103928768
+20
+-61.09527615457002
+30
+209.41889763779537
+11
+173.40095945802403
+21
+-55.594493380222346
+31
+216.04960501004192
+12
+155.25962087534685
+22
+-55.5944887529952
+32
+216.04960629921268
+13
+155.25962087534685
+23
+-55.5944887529952
+33
+216.04960629921268
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.941112340791
+20
+98.66928820199992
+30
+244.8444881889765
+11
+68.94111811023663
+21
+-94.91575116807901
+31
+200.98149606299222
+12
+68.94111831787316
+22
+-101.8826803019373
+32
+194.81220472440953
+13
+68.94111831787316
+23
+-101.8826803019373
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.94111811023663
+20
+-94.91575116807901
+30
+200.98149606299222
+11
+68.941112340791
+21
+98.66928820199992
+31
+244.8444881889765
+12
+68.9411181101801
+22
+-94.91385437913766
+32
+200.98196926170036
+13
+68.9411181101801
+23
+-94.91385437913766
+33
+200.98196926170036
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+68.9411181101801
+20
+-94.91385437913766
+30
+200.98196926170036
+11
+68.941112340791
+21
+98.66928820199992
+31
+244.8444881889765
+12
+68.94111261077951
+22
+89.61023080877725
+32
+247.01585148502548
+13
+68.94111261077951
+23
+89.61023080877725
+33
+247.01585148502548
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+68.94111261077951
+20
+89.61023080877725
+30
+247.01585148502548
+11
+68.941112340791
+21
+98.66928820199992
+31
+244.8444881889765
+12
+68.94111261059379
+22
+89.61646217847384
+32
+247.01740604706768
+13
+68.94111261059379
+23
+89.61646217847384
+33
+247.01740604706768
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+68.94111261059379
+20
+89.61646217847384
+30
+247.01740604706768
+11
+68.941112340791
+21
+98.66928820199992
+31
+244.8444881889765
+12
+68.94111261046264
+22
+89.62086300514947
+32
+247.01850393700798
+13
+68.94111261046264
+23
+89.62086300514947
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+54.371039370079146
+20
+-94.91575160231338
+30
+200.98149606299222
+11
+54.37103360063351
+21
+98.66928776776554
+31
+244.8444881889765
+12
+54.371039577715635
+22
+-101.88268073617166
+32
+194.81220472440953
+13
+54.371039577715635
+23
+-101.88268073617166
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+54.37103360063351
+20
+98.66928776776554
+30
+244.8444881889765
+11
+54.371039370079146
+21
+-94.91575160231338
+31
+200.98149606299222
+12
+54.371038362122235
+22
+-61.09527916136846
+32
+209.41889763779537
+13
+54.371038362122235
+23
+-61.09527916136846
+33
+209.41889763779537
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+54.37103360063351
+20
+98.66928776776554
+30
+244.8444881889765
+11
+54.371038362122235
+21
+-61.09527916136846
+31
+209.41889763779537
+12
+54.371033870788715
+22
+89.6046374506915
+32
+247.01445620777636
+13
+54.371033870788715
+23
+89.6046374506915
+33
+247.01445620777636
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+54.37103360063351
+20
+98.66928776776554
+30
+244.8444881889765
+11
+54.371033870788715
+21
+89.6046374506915
+31
+247.01445620777636
+12
+54.371033870478705
+22
+89.61503930110042
+32
+247.01705118848423
+13
+54.371033870478705
+23
+89.61503930110042
+33
+247.01705118848423
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+54.37103360063351
+20
+98.66928776776554
+30
+244.8444881889765
+11
+54.371033870478705
+21
+89.61503930110042
+31
+247.01705118848423
+12
+54.371033870305155
+22
+89.62086257091512
+32
+247.01850393700798
+13
+54.371033870305155
+23
+89.62086257091512
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40096083755827
+20
+-101.88268235660034
+30
+194.81220343523876
+11
+1.7082673453572284e-07
+21
+-100.64757308805667
+31
+189.86102362204733
+12
+173.4009608007481
+22
+-100.64757308805667
+32
+189.86102233287656
+13
+173.4009608007481
+23
+-100.64757308805667
+33
+189.86102233287656
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+1.7082673453572284e-07
+20
+-100.64757308805667
+30
+189.86102362204733
+11
+173.40096083755827
+21
+-101.88268235660034
+31
+194.81220343523876
+12
+2.0763689256853013e-07
+22
+-101.88268235660034
+32
+194.81220472440953
+13
+2.0763689256853013e-07
+23
+-101.88268235660034
+33
+194.81220472440953
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+2.0763689256853013e-07
+20
+-101.88268235660034
+30
+194.81220472440953
+11
+173.40096083755827
+21
+-101.88268235660034
+31
+194.81220343523876
+12
+155.2596222548811
+22
+-101.8826777293732
+32
+194.81220472440953
+13
+155.2596222548811
+23
+-101.8826777293732
+33
+194.81220472440953
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+2.0763689256853013e-07
+20
+-101.88268235660034
+30
+194.81220472440953
+11
+155.2596222548811
+21
+-101.8826777293732
+31
+194.81220472440953
+12
+126.31788997141648
+22
+-101.88267859192827
+32
+194.81220472440953
+13
+126.31788997141648
+23
+-101.88267859192827
+33
+194.81220472440953
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+2.0763689256853013e-07
+20
+-101.88268235660034
+30
+194.81220472440953
+11
+126.31788997141648
+21
+-101.88267859192827
+31
+194.81220472440953
+12
+111.7745828848023
+22
+-101.88267902536472
+32
+194.81220472440953
+13
+111.7745828848023
+23
+-101.88267902536472
+33
+194.81220472440953
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+2.0763689256853013e-07
+20
+-101.88268235660034
+30
+194.81220472440953
+11
+111.7745828848023
+21
+-101.88267902536472
+31
+194.81220472440953
+12
+68.94111831787316
+22
+-101.8826803019373
+32
+194.81220472440953
+13
+68.94111831787316
+23
+-101.8826803019373
+33
+194.81220472440953
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+2.0763689256853013e-07
+20
+-101.88268235660034
+30
+194.81220472440953
+11
+68.94111831787316
+21
+-101.8826803019373
+31
+194.81220472440953
+12
+54.371039577715635
+22
+-101.88268073617166
+32
+194.81220472440953
+13
+54.371039577715635
+23
+-101.88268073617166
+33
+194.81220472440953
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+2.0763689256853013e-07
+20
+-101.88268235660034
+30
+194.81220472440953
+11
+54.371039577715635
+21
+-101.88268073617166
+31
+194.81220472440953
+12
+18.75855926275502
+22
+-101.88268179753607
+32
+194.81220472440953
+13
+18.75855926275502
+23
+-101.88268179753607
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-5.830738838863425e-06
+20
+100.72590021948669
+30
+236.6003937007875
+11
+173.40095486047613
+21
+98.66928614733685
+31
+244.84448689980573
+12
+173.40095479918253
+22
+100.72590021948669
+32
+236.60039241161672
+13
+173.40095479918253
+23
+100.72590021948669
+33
+236.60039241161672
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095486047613
+20
+98.66928614733685
+30
+244.84448689980573
+11
+-5.830738838863425e-06
+21
+100.72590021948669
+31
+236.6003937007875
+12
+-5.769445243331006e-06
+22
+98.66928614733685
+32
+244.8444881889765
+13
+-5.769445243331006e-06
+23
+98.66928614733685
+33
+244.8444881889765
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40095486047613
+20
+98.66928614733685
+30
+244.84448689980573
+11
+-5.769445243331006e-06
+21
+98.66928614733685
+31
+244.8444881889765
+12
+155.25961627779895
+22
+98.66929077456398
+32
+244.8444881889765
+13
+155.25961627779895
+23
+98.66929077456398
+33
+244.8444881889765
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+-5.769445243331006e-06
+21
+98.66928614733685
+31
+244.8444881889765
+12
+18.758553285672875
+22
+98.66928670640114
+32
+244.8444881889765
+13
+18.758553285672875
+23
+98.66928670640114
+33
+244.8444881889765
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+18.758553285672875
+21
+98.66928670640114
+31
+244.8444881889765
+12
+54.37103360063351
+22
+98.66928776776554
+32
+244.8444881889765
+13
+54.37103360063351
+23
+98.66928776776554
+33
+244.8444881889765
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+54.37103360063351
+21
+98.66928776776554
+31
+244.8444881889765
+12
+68.941112340791
+22
+98.66928820199992
+32
+244.8444881889765
+13
+68.941112340791
+23
+98.66928820199992
+33
+244.8444881889765
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+68.941112340791
+21
+98.66928820199992
+31
+244.8444881889765
+12
+111.77457690772016
+22
+98.66928947857245
+32
+244.8444881889765
+13
+111.77457690772016
+23
+98.66928947857245
+33
+244.8444881889765
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+111.77457690772016
+21
+98.66928947857245
+31
+244.8444881889765
+12
+126.31788399433434
+22
+98.66928991200895
+32
+244.8444881889765
+13
+126.31788399433434
+23
+98.66928991200895
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095479918253
+20
+100.72590021948669
+30
+236.60039241161672
+11
+-5.830738838863425e-06
+21
+88.4413334658682
+31
+236.6003937007875
+12
+-5.830738838863425e-06
+22
+100.72590021948669
+32
+236.6003937007875
+13
+-5.830738838863425e-06
+23
+100.72590021948669
+33
+236.6003937007875
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-5.830738838863425e-06
+20
+88.4413334658682
+30
+236.6003937007875
+11
+173.40095479918253
+21
+100.72590021948669
+31
+236.60039241161672
+12
+43.74033778568689
+22
+88.4413334658682
+32
+236.60039337559454
+13
+43.74033778568689
+23
+88.4413334658682
+33
+236.60039337559454
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+43.74033778568689
+20
+88.4413334658682
+30
+236.60039337559454
+11
+173.40095479918253
+21
+100.72590021948669
+31
+236.60039241161672
+12
+54.371033870305155
+22
+88.4413334658682
+32
+236.6003932965593
+13
+54.371033870305155
+23
+88.4413334658682
+33
+236.6003932965593
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+54.371033870305155
+20
+88.4413334658682
+30
+236.6003932965593
+11
+173.40095479918253
+21
+100.72590021948669
+31
+236.60039241161672
+12
+68.91434614140894
+22
+88.44133256583937
+32
+236.6003932965593
+13
+68.91434614140894
+23
+88.44133256583937
+33
+236.6003932965593
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+68.91434614140894
+20
+88.44133256583937
+30
+236.6003932965593
+11
+173.40095479918253
+21
+100.72590021948669
+31
+236.60039241161672
+12
+68.94111264561634
+22
+88.4413334350913
+32
+236.60039318823627
+13
+68.94111264561634
+23
+88.4413334350913
+33
+236.60039318823627
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+68.94111264561634
+20
+88.4413334350913
+30
+236.60039318823627
+11
+173.40095479918253
+21
+100.72590021948669
+31
+236.60039241161672
+12
+111.7745772127613
+22
+88.44133346590662
+32
+236.6003928697856
+13
+111.7745772127613
+23
+88.44133346590662
+33
+236.6003928697856
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+111.7745772127613
+20
+88.44133346590662
+30
+236.6003928697856
+11
+173.40095479918253
+21
+100.72590021948669
+31
+236.60039241161672
+12
+126.31788429915974
+22
+88.4413334658682
+32
+236.6003927616616
+13
+126.31788429915974
+23
+88.4413334658682
+33
+236.6003927616616
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+126.31788429915974
+20
+88.4413334658682
+30
+236.6003927616616
+11
+173.40095479918253
+21
+100.72590021948669
+31
+236.60039241161672
+12
+173.40095479918253
+22
+88.4413334658682
+32
+236.60039241161672
+13
+173.40095479918253
+23
+88.4413334658682
+33
+236.60039241161672
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+3.9834802123550617e-13
+20
+-94.91575322274208
+30
+200.98149606299222
+11
+1.7082673453572284e-07
+21
+-100.64757308805667
+31
+189.86102362204733
+12
+2.0763689256853013e-07
+22
+-101.88268235660034
+32
+194.81220472440953
+13
+2.0763689256853013e-07
+23
+-101.88268235660034
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+1.7082673453572284e-07
+20
+-100.64757308805667
+30
+189.86102362204733
+11
+3.9834802123550617e-13
+21
+-94.91575322274208
+31
+200.98149606299222
+12
+1.7082673453572284e-07
+22
+-84.34409960499812
+32
+189.86102362204733
+13
+1.7082673453572284e-07
+23
+-84.34409960499812
+33
+189.86102362204733
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+54.37103905501049
+20
+-84.34409829059867
+30
+189.86102321781914
+11
+1.7082673453572284e-07
+21
+-100.64757308805667
+31
+189.86102362204733
+12
+1.7082673453572284e-07
+22
+-84.34409960499812
+32
+189.86102362204733
+13
+1.7082673453572284e-07
+23
+-84.34409960499812
+33
+189.86102362204733
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+1.7082673453572284e-07
+20
+-100.64757308805667
+30
+189.86102362204733
+11
+54.37103905501049
+21
+-84.34409829059867
+31
+189.86102321781914
+12
+173.4009608007481
+22
+-100.64757308805667
+32
+189.86102233287656
+13
+173.4009608007481
+23
+-100.64757308805667
+33
+189.86102233287656
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+173.4009608007481
+20
+-100.64757308805667
+30
+189.86102233287656
+11
+54.37103905501049
+21
+-84.34409829059867
+31
+189.86102321781914
+12
+68.91435129096055
+22
+-84.34409829059867
+32
+189.86102321781914
+13
+68.91435129096055
+23
+-84.34409829059867
+33
+189.86102321781914
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.4009608007481
+20
+-100.64757308805667
+30
+189.86102233287656
+11
+68.91435129096055
+21
+-84.34409829059867
+31
+189.86102321781914
+12
+68.94111779516797
+22
+-84.34409811448558
+32
+189.8610231094961
+13
+68.94111779516797
+23
+-84.34409811448558
+33
+189.8610231094961
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.4009608007481
+20
+-100.64757308805667
+30
+189.86102233287656
+11
+68.94111779516797
+21
+-84.34409811448558
+31
+189.8610231094961
+12
+111.77458236206266
+22
+-84.34409690288955
+32
+189.86102279104543
+13
+111.77458236206266
+23
+-84.34409690288955
+33
+189.86102279104543
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.4009608007481
+20
+-100.64757308805667
+30
+189.86102233287656
+11
+111.77458236206266
+21
+-84.34409690288955
+31
+189.86102279104543
+12
+126.31788944871136
+22
+-84.34409739056986
+32
+189.86102268292143
+13
+126.31788944871136
+23
+-84.34409739056986
+33
+189.86102268292143
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.4009608007481
+20
+-100.64757308805667
+30
+189.86102233287656
+11
+126.31788944871136
+21
+-84.34409739056986
+31
+189.86102268292143
+12
+173.4009608007481
+22
+-84.34409541309498
+32
+189.86102233287656
+13
+173.4009608007481
+23
+-84.34409541309498
+33
+189.86102233287656
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+111.7745826771658
+20
+-94.91574989150647
+30
+200.98149606299222
+11
+126.317884264006
+21
+89.6208647151585
+31
+247.01850393700798
+12
+111.7745771773918
+22
+89.62086428172206
+32
+247.01850393700798
+13
+111.7745771773918
+23
+89.62086428172206
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+126.317884264006
+20
+89.6208647151585
+30
+247.01850393700798
+11
+111.7745826771658
+21
+-94.91574989150647
+31
+200.98149606299222
+12
+126.31788426410866
+22
+89.61741980576197
+32
+247.0176445231636
+13
+126.31788426410866
+23
+89.61741980576197
+33
+247.0176445231636
+70
+15
+  0
+3DFACE
+ 8
+lid
+10
+126.31788426410866
+20
+89.61741980576197
+30
+247.0176445231636
+11
+111.7745826771658
+21
+-94.91574989150647
+31
+200.98149606299222
+12
+126.31788976377997
+22
+-94.91574945806998
+32
+200.98149606299222
+13
+126.31788976377997
+23
+-94.91574945806998
+33
+200.98149606299222
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+126.317884264006
+20
+89.6208647151585
+30
+247.01850393700798
+11
+126.31788426410866
+21
+89.61741980576197
+31
+247.0176445231636
+12
+126.31788426406312
+22
+89.61894827418696
+32
+247.01802583563773
+13
+126.31788426406312
+23
+89.61894827418696
+33
+247.01802583563773
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.94111264561634
+20
+88.4413334350913
+30
+236.60039318823627
+11
+68.91435129096055
+21
+-84.34409829059867
+31
+189.86102321781914
+12
+68.91434614140894
+22
+88.44133256583937
+32
+236.6003932965593
+13
+68.91434614140894
+23
+88.44133256583937
+33
+236.6003932965593
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.91435129096055
+20
+-84.34409829059867
+30
+189.86102321781914
+11
+68.94111264561634
+21
+88.4413334350913
+31
+236.60039318823627
+12
+68.94111779516797
+22
+-84.34409811448558
+32
+189.8610231094961
+13
+68.94111779516797
+23
+-84.34409811448558
+33
+189.8610231094961
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+43.74033778568689
+20
+88.4413334658682
+30
+236.60039337559454
+11
+1.7082673453572284e-07
+21
+-84.34409960499812
+31
+189.86102362204733
+12
+-5.830738838863425e-06
+22
+88.4413334658682
+32
+236.6003937007875
+13
+-5.830738838863425e-06
+23
+88.4413334658682
+33
+236.6003937007875
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+1.7082673453572284e-07
+20
+-84.34409960499812
+30
+189.86102362204733
+11
+43.74033778568689
+21
+88.4413334658682
+31
+236.60039337559454
+12
+54.37103905501049
+22
+-84.34409829059867
+32
+189.86102321781914
+13
+54.37103905501049
+23
+-84.34409829059867
+33
+189.86102321781914
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+54.37103905501049
+20
+-84.34409829059867
+30
+189.86102321781914
+11
+43.74033778568689
+21
+88.4413334658682
+31
+236.60039337559454
+12
+54.371033870305155
+22
+88.4413334658682
+32
+236.6003932965593
+13
+54.371033870305155
+23
+88.4413334658682
+33
+236.6003932965593
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+111.7745772127613
+20
+88.44133346590662
+30
+236.6003928697856
+11
+68.94111779516797
+21
+-84.34409811448558
+31
+189.8610231094961
+12
+68.94111264561634
+22
+88.4413334350913
+32
+236.60039318823627
+13
+68.94111264561634
+23
+88.4413334350913
+33
+236.60039318823627
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+68.94111779516797
+20
+-84.34409811448558
+30
+189.8610231094961
+11
+111.7745772127613
+21
+88.44133346590662
+31
+236.6003928697856
+12
+111.77458236206266
+22
+-84.34409690288955
+32
+189.86102279104543
+13
+111.77458236206266
+23
+-84.34409690288955
+33
+189.86102279104543
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095479918253
+20
+88.4413334658682
+30
+236.60039241161672
+11
+126.31788944871136
+21
+-84.34409739056986
+31
+189.86102268292143
+12
+126.31788429915974
+22
+88.4413334658682
+32
+236.6003927616616
+13
+126.31788429915974
+23
+88.4413334658682
+33
+236.6003927616616
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+126.31788944871136
+20
+-84.34409739056986
+30
+189.86102268292143
+11
+173.40095479918253
+21
+88.4413334658682
+31
+236.60039241161672
+12
+173.4009608007481
+22
+-84.34409541309498
+32
+189.86102233287656
+13
+173.4009608007481
+23
+-84.34409541309498
+33
+189.86102233287656
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095613027842
+20
+56.06298693473833
+30
+238.64684910453013
+11
+173.40095945802403
+21
+-55.594493380222346
+31
+216.04960501004192
+12
+173.40095962196486
+22
+-61.09528078179717
+32
+209.41850264783722
+13
+173.40095962196486
+23
+-61.09528078179717
+33
+209.41850264783722
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095945802403
+20
+-55.594493380222346
+30
+216.04960501004192
+11
+173.40095613027842
+21
+56.06298693473833
+31
+238.64684910453013
+12
+173.4009564353384
+22
+45.8271601630848
+32
+241.35157351397893
+13
+173.4009564353384
+23
+45.8271601630848
+33
+241.35157351397893
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.4009608007481
+20
+-100.64757308805667
+30
+189.86102233287656
+11
+173.40096062992177
+21
+-94.91575322274208
+31
+200.98149477382145
+12
+173.40096083755827
+22
+-101.88268235660034
+32
+194.81220343523876
+13
+173.40096083755827
+23
+-101.88268235660034
+33
+194.81220343523876
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40096062992177
+20
+-94.91575322274208
+30
+200.98149477382145
+11
+173.4009608007481
+21
+-100.64757308805667
+31
+189.86102233287656
+12
+173.4009608007481
+22
+-84.34409541309498
+32
+189.86102233287656
+13
+173.4009608007481
+23
+-84.34409541309498
+33
+189.86102233287656
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40096062992177
+20
+-94.91575322274208
+30
+200.98149477382145
+11
+173.4009608007481
+21
+-84.34409541309498
+31
+189.86102233287656
+12
+173.40095962196486
+22
+-61.09528078179717
+32
+209.41850264783722
+13
+173.40095962196486
+23
+-61.09528078179717
+33
+209.41850264783722
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+173.40095962196486
+20
+-61.09528078179717
+30
+209.41850264783722
+11
+173.4009608007481
+21
+-84.34409541309498
+31
+189.86102233287656
+12
+173.40095479918253
+22
+88.4413334658682
+32
+236.60039241161672
+13
+173.40095479918253
+23
+88.4413334658682
+33
+236.60039241161672
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40095962196486
+20
+-61.09528078179717
+30
+209.41850264783722
+11
+173.40095479918253
+21
+88.4413334658682
+31
+236.60039241161672
+12
+173.40095613027842
+22
+56.06298693473833
+32
+238.64684910453013
+13
+173.40095613027842
+23
+56.06298693473833
+33
+238.64684910453013
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+173.40095613027842
+20
+56.06298693473833
+30
+238.64684910453013
+11
+173.40095479918253
+21
+88.4413334658682
+31
+236.60039241161672
+12
+173.40095513014776
+22
+89.62086095048643
+32
+247.0185026478372
+13
+173.40095513014776
+23
+89.62086095048643
+33
+247.0185026478372
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+173.40095513014776
+20
+89.62086095048643
+30
+247.0185026478372
+11
+173.40095479918253
+21
+88.4413334658682
+31
+236.60039241161672
+12
+173.40095479918253
+22
+100.72590021948669
+32
+236.60039241161672
+13
+173.40095479918253
+23
+100.72590021948669
+33
+236.60039241161672
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+173.40095513014776
+20
+89.62086095048643
+30
+247.0185026478372
+11
+173.40095479918253
+21
+100.72590021948669
+31
+236.60039241161672
+12
+173.40095486047613
+22
+98.66928614733685
+32
+244.84448689980573
+13
+173.40095486047613
+23
+98.66928614733685
+33
+244.84448689980573
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.4567417214692
+20
+-84.3440954164166
+30
+189.86102362204733
+11
+173.4009608007481
+21
+-84.34409541309498
+31
+189.86102233287656
+12
+173.40095479918253
+22
+88.4413334658682
+32
+236.60039241161672
+13
+173.40095479918253
+23
+88.4413334658682
+33
+236.60039241161672
+70
+0
+  0
+3DFACE
+ 8
+lid
+10
+-5.499773591655099e-06
+20
+89.62086095048643
+30
+247.01850393700798
+11
+-5.830738838863425e-06
+21
+100.72590021948669
+31
+236.6003937007875
+12
+-5.830738838863425e-06
+22
+88.4413334658682
+32
+236.6003937007875
+13
+-5.830738838863425e-06
+23
+88.4413334658682
+33
+236.6003937007875
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-5.830738838863425e-06
+20
+100.72590021948669
+30
+236.6003937007875
+11
+-5.499773591655099e-06
+21
+89.62086095048643
+31
+247.01850393700798
+12
+-5.769445243331006e-06
+22
+98.66928614733685
+32
+244.8444881889765
+13
+-5.769445243331006e-06
+23
+98.66928614733685
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-1.007956508480845e-06
+20
+-61.09528078179717
+30
+209.41850393700798
+11
+1.7082673453572284e-07
+21
+-84.34409960499812
+31
+189.86102362204733
+12
+3.9834802123550617e-13
+22
+-94.91575322274208
+32
+200.98149606299222
+13
+3.9834802123550617e-13
+23
+-94.91575322274208
+33
+200.98149606299222
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+1.7082673453572284e-07
+20
+-84.34409960499812
+30
+189.86102362204733
+11
+-1.007956508480845e-06
+21
+-61.09528078179717
+31
+209.41850393700798
+12
+-5.830738838863425e-06
+22
+88.4413334658682
+32
+236.6003937007875
+13
+-5.830738838863425e-06
+23
+88.4413334658682
+33
+236.6003937007875
+70
+3
+  0
+3DFACE
+ 8
+lid
+10
+-5.830738838863425e-06
+20
+88.4413334658682
+30
+236.6003937007875
+11
+-1.007956508480845e-06
+21
+-61.09528078179717
+31
+209.41850393700798
+12
+-4.499642948463389e-06
+22
+56.06298693473833
+32
+238.6468503937009
+13
+-4.499642948463389e-06
+23
+56.06298693473833
+33
+238.6468503937009
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+-5.830738838863425e-06
+20
+88.4413334658682
+30
+236.6003937007875
+11
+-4.499642948463389e-06
+21
+56.06298693473833
+31
+238.6468503937009
+12
+-5.499773591655099e-06
+22
+89.62086095048643
+32
+247.01850393700798
+13
+-5.499773591655099e-06
+23
+89.62086095048643
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-1.1718973480512318e-06
+20
+-55.594493380222346
+30
+216.04960629921268
+11
+-4.499642948463389e-06
+21
+56.06298693473833
+31
+238.6468503937009
+12
+-1.007956508480845e-06
+22
+-61.09528078179717
+32
+209.41850393700798
+13
+-1.007956508480845e-06
+23
+-61.09528078179717
+33
+209.41850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-4.499642948463389e-06
+20
+56.06298693473833
+30
+238.6468503937009
+11
+-1.1718973480512318e-06
+21
+-55.594493380222346
+31
+216.04960629921268
+12
+-4.194582979177852e-06
+22
+45.8271601630848
+32
+241.3515748031497
+13
+-4.194582979177852e-06
+23
+45.8271601630848
+33
+241.3515748031497
+70
+1
+  0
+LINE
+ 8
+lid
+10
+173.57615198071508
+20
+89.61504284452782
+30
+247.01705122570223
+11
+173.40095479918253
+21
+88.4413334658682
+31
+236.60039241161672
+  0
+LINE
+ 8
+lid
+10
+173.40096062992177
+20
+-94.91575322274208
+30
+200.98149477382145
+11
+173.4567417214692
+21
+-84.3440954164166
+31
+189.86102362204733
+  0
+3DFACE
+ 8
+lid
+10
+155.2596220472446
+20
+-94.91574859551494
+30
+200.98149606299222
+11
+155.25961627779895
+21
+98.66929077456398
+31
+244.8444881889765
+12
+155.2596222548811
+22
+-101.8826777293732
+32
+194.81220472440953
+13
+155.2596222548811
+23
+-101.8826777293732
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+155.2596220472446
+21
+-94.91574859551494
+31
+200.98149606299222
+12
+155.25962204721512
+22
+-94.91475962800925
+32
+200.98174278680736
+13
+155.25962204721512
+23
+-94.91475962800925
+33
+200.98174278680736
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+155.25962204721512
+21
+-94.91475962800925
+31
+200.98174278680736
+12
+155.25962103928768
+22
+-61.09527615457002
+32
+209.41889763779537
+13
+155.25962103928768
+23
+-61.09527615457002
+33
+209.41889763779537
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+155.25962103928768
+21
+-61.09527615457002
+31
+209.41889763779537
+12
+155.25961754760118
+22
+56.06299156196549
+32
+238.6468503937009
+13
+155.25961754760118
+23
+56.06299156196549
+33
+238.6468503937009
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+155.25961754760118
+21
+56.06299156196549
+31
+238.6468503937009
+12
+155.25961654795418
+22
+89.60464044290792
+32
+247.0144562667884
+13
+155.25961654795418
+23
+89.60464044290792
+33
+247.0144562667884
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+155.25961654795418
+21
+89.60464044290792
+31
+247.0144562667884
+12
+155.2596165477875
+22
+89.61023337167238
+32
+247.01585152984606
+13
+155.2596165477875
+23
+89.61023337167238
+33
+247.01585152984606
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+155.2596165477875
+21
+89.61023337167238
+31
+247.01585152984606
+12
+155.25961654768776
+22
+89.61358009666061
+32
+247.01668643440806
+13
+155.25961654768776
+23
+89.61358009666061
+33
+247.01668643440806
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+155.25961654768776
+21
+89.61358009666061
+31
+247.01668643440806
+12
+155.25961654757327
+22
+89.61742066705047
+32
+247.01764453815136
+13
+155.25961654757327
+23
+89.61742066705047
+33
+247.01764453815136
+70
+13
+  0
+3DFACE
+ 8
+lid
+10
+155.25961627779895
+20
+98.66929077456398
+30
+244.8444881889765
+11
+155.25961654757327
+21
+89.61742066705047
+31
+247.01764453815136
+12
+155.2596165474706
+22
+89.62086557771359
+32
+247.01850393700798
+13
+155.2596165474706
+23
+89.62086557771359
+33
+247.01850393700798
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+155.25962103928768
+20
+-61.09527615457002
+30
+209.41889763779537
+11
+173.40095613027842
+21
+56.06298693473833
+31
+238.64684910453013
+12
+155.25961754760118
+22
+56.06299156196549
+32
+238.6468503937009
+13
+155.25961754760118
+23
+56.06299156196549
+33
+238.6468503937009
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+173.40095613027842
+20
+56.06298693473833
+30
+238.64684910453013
+11
+155.25962103928768
+21
+-61.09527615457002
+31
+209.41889763779537
+12
+173.40095962196486
+22
+-61.09528078179717
+32
+209.41850264783722
+13
+173.40095962196486
+23
+-61.09528078179717
+33
+209.41850264783722
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758554555475172
+20
+56.06298749380262
+30
+238.6468503937009
+11
+18.758557883220774
+21
+-55.594492821158035
+31
+216.04960629921268
+12
+18.758558047161614
+22
+-61.09528022273286
+32
+209.41889763779537
+13
+18.758558047161614
+23
+-61.09528022273286
+33
+209.41889763779537
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758557883220774
+20
+-55.594492821158035
+30
+216.04960629921268
+11
+18.758554555475172
+21
+56.06298749380262
+31
+238.6468503937009
+12
+18.758554860535142
+22
+45.82716072214909
+32
+241.3515748031497
+13
+18.758554860535142
+23
+45.82716072214909
+33
+241.3515748031497
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+-1.007956508480845e-06
+20
+-61.09528078179717
+30
+209.41850393700798
+11
+18.758554555475172
+21
+56.06298749380262
+31
+238.6468503937009
+12
+-4.499642948463389e-06
+22
+56.06298693473833
+32
+238.6468503937009
+13
+-4.499642948463389e-06
+23
+56.06298693473833
+33
+238.6468503937009
+70
+1
+  0
+3DFACE
+ 8
+lid
+10
+18.758554555475172
+20
+56.06298749380262
+30
+238.6468503937009
+11
+-1.007956508480845e-06
+21
+-61.09528078179717
+31
+209.41850393700798
+12
+18.758558047161614
+22
+-61.09528022273286
+32
+209.41889763779537
+13
+18.758558047161614
+23
+-61.09528022273286
+33
+209.41889763779537
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+18.75855926275502
+20
+-101.88268179753607
+30
+194.81220472440953
+11
+54.37103360063351
+21
+98.66928776776552
+31
+244.8444881889765
+12
+18.758553285672875
+22
+98.66928670640112
+32
+244.8444881889765
+13
+18.758553285672875
+23
+98.66928670640112
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+54.37103360063351
+20
+98.66928776776552
+30
+244.8444881889765
+11
+18.75855926275502
+21
+-101.88268179753607
+31
+194.81220472440953
+12
+54.371039577715635
+22
+-101.88268073617166
+32
+194.81220472440953
+13
+54.371039577715635
+23
+-101.88268073617166
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+68.94111831787316
+20
+-101.8826803019373
+30
+194.81220472440953
+11
+111.77457690772016
+21
+98.66928947857247
+31
+244.8444881889765
+12
+68.941112340791
+22
+98.6692882019999
+32
+244.8444881889765
+13
+68.941112340791
+23
+98.6692882019999
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+111.77457690772016
+20
+98.66928947857247
+30
+244.8444881889765
+11
+68.94111831787316
+21
+-101.8826803019373
+31
+194.81220472440953
+12
+111.7745828848023
+22
+-101.88267902536472
+32
+194.81220472440953
+13
+111.7745828848023
+23
+-101.88267902536472
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+126.31788997141648
+20
+-101.88267859192827
+30
+194.81220472440953
+11
+155.25961627779895
+21
+98.66929077456399
+31
+244.8444881889765
+12
+126.31788399433434
+22
+98.66928991200894
+32
+244.8444881889765
+13
+126.31788399433434
+23
+98.66928991200894
+33
+244.8444881889765
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+155.25961627779895
+20
+98.66929077456399
+30
+244.8444881889765
+11
+126.31788997141648
+21
+-101.88267859192827
+31
+194.81220472440953
+12
+155.2596222548811
+22
+-101.8826777293732
+32
+194.81220472440953
+13
+155.2596222548811
+23
+-101.8826777293732
+33
+194.81220472440953
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+126.31788924104035
+20
+-77.37716733559479
+30
+196.03031412962812
+11
+111.77458236206266
+21
+-84.34409690288955
+31
+189.86102279104543
+12
+111.77458215442616
+22
+-77.37716776903129
+32
+196.03031412962812
+13
+111.77458215442616
+23
+-77.37716776903129
+33
+196.03031412962812
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+111.77458236206266
+20
+-84.34409690288955
+30
+189.86102279104543
+11
+126.31788924104035
+21
+-77.37716733559479
+31
+196.03031412962812
+12
+126.31788944871136
+22
+-84.34409739056986
+32
+189.86102268292143
+13
+126.31788944871136
+23
+-84.34409739056986
+33
+189.86102268292143
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+126.31788429915974
+20
+88.44133346586818
+30
+236.6003927616616
+11
+111.7745774822173
+21
+79.3929078355813
+31
+238.77440850969307
+12
+111.7745772127613
+22
+88.44133346590661
+32
+236.6003928697856
+13
+111.7745772127613
+23
+88.44133346590661
+33
+236.6003928697856
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+111.7745774822173
+20
+79.3929078355813
+30
+238.77440850969307
+11
+126.31788429915974
+21
+88.44133346586818
+31
+236.6003927616616
+12
+126.3178845688314
+22
+79.39290826901777
+32
+238.77440850969307
+13
+126.3178845688314
+23
+79.39290826901777
+33
+238.77440850969307
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+126.31788924104035
+20
+-77.37716733559479
+30
+196.03031412962812
+11
+126.31788429915974
+21
+88.44133346586818
+31
+236.6003927616616
+12
+126.31788944871136
+22
+-84.34409739056986
+32
+189.86102268292143
+13
+126.31788944871136
+23
+-84.34409739056986
+33
+189.86102268292143
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+126.31788429915974
+20
+88.44133346586818
+30
+236.6003927616616
+11
+126.31788924104035
+21
+-77.37716733559479
+31
+196.03031412962812
+12
+126.3178845688314
+22
+79.39290826901777
+32
+238.77440850969307
+13
+126.3178845688314
+23
+79.39290826901777
+33
+238.77440850969307
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+111.7745772127613
+20
+88.44133346590661
+30
+236.6003928697856
+11
+111.77458215442616
+21
+-77.37716776903129
+31
+196.03031412962812
+12
+111.77458236206266
+22
+-84.34409690288955
+32
+189.86102279104543
+13
+111.77458236206266
+23
+-84.34409690288955
+33
+189.86102279104543
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+111.77458215442616
+20
+-77.37716776903129
+30
+196.03031412962812
+11
+111.7745772127613
+21
+88.44133346590661
+31
+236.6003928697856
+12
+111.7745774822173
+22
+79.3929078355813
+32
+238.77440850969307
+13
+111.7745774822173
+23
+79.3929078355813
+33
+238.77440850969307
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+126.3178845688314
+20
+79.39290826901777
+30
+238.77440850969307
+11
+111.77458215442616
+21
+-77.37716776903129
+31
+196.03031412962812
+12
+111.7745774822173
+22
+79.3929078355813
+32
+238.77440850969307
+13
+111.7745774822173
+23
+79.3929078355813
+33
+238.77440850969307
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+111.77458215442616
+20
+-77.37716776903129
+30
+196.03031412962812
+11
+126.3178845688314
+21
+79.39290826901777
+31
+238.77440850969307
+12
+126.31788924104035
+22
+-77.37716733559479
+32
+196.03031412962812
+13
+126.31788924104035
+23
+-77.37716733559479
+33
+196.03031412962812
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+68.91434614140894
+20
+88.44133256583939
+30
+236.6003932965593
+11
+54.37103932446649
+21
+79.3929069355525
+31
+238.77440904459078
+12
+54.371033870305155
+22
+88.44133346586818
+32
+236.6003932965593
+13
+54.371033870305155
+23
+88.44133346586818
+33
+236.6003932965593
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+54.37103932446649
+20
+79.3929069355525
+30
+238.77440904459078
+11
+68.91434614140894
+21
+88.44133256583939
+31
+236.6003932965593
+12
+68.9143464110806
+22
+79.39290736898897
+32
+238.77440904459078
+13
+68.9143464110806
+23
+79.39290736898897
+33
+238.77440904459078
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+68.9143464110806
+20
+79.39290736898897
+30
+238.77440904459078
+11
+54.37104399667535
+21
+-77.3771686690601
+31
+196.03031466452583
+12
+54.37103932446649
+22
+79.3929069355525
+32
+238.77440904459078
+13
+54.37103932446649
+23
+79.3929069355525
+33
+238.77440904459078
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+54.37104399667535
+20
+-77.3771686690601
+30
+196.03031466452583
+11
+68.9143464110806
+21
+79.39290736898897
+31
+238.77440904459078
+12
+68.91435108328955
+22
+-77.3771682356236
+32
+196.03031466452583
+13
+68.91435108328955
+23
+-77.3771682356236
+33
+196.03031466452583
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+54.37104399667535
+20
+-77.3771686690601
+30
+196.03031466452583
+11
+68.91435129096055
+21
+-84.34409829059867
+31
+189.86102321781914
+12
+54.37103905501049
+22
+-84.34409829059867
+32
+189.86102321781914
+13
+54.37103905501049
+23
+-84.34409829059867
+33
+189.86102321781914
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+68.91435129096055
+20
+-84.34409829059867
+30
+189.86102321781914
+11
+54.37104399667535
+21
+-77.3771686690601
+31
+196.03031466452583
+12
+68.91435108328955
+22
+-77.3771682356236
+32
+196.03031466452583
+13
+68.91435108328955
+23
+-77.3771682356236
+33
+196.03031466452583
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+54.371033870305155
+20
+88.44133346586818
+30
+236.6003932965593
+11
+54.37104399667535
+21
+-77.3771686690601
+31
+196.03031466452583
+12
+54.37103905501049
+22
+-84.34409829059867
+32
+189.86102321781914
+13
+54.37103905501049
+23
+-84.34409829059867
+33
+189.86102321781914
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+54.37104399667535
+20
+-77.3771686690601
+30
+196.03031466452583
+11
+54.371033870305155
+21
+88.44133346586818
+31
+236.6003932965593
+12
+54.37103932446649
+22
+79.3929069355525
+32
+238.77440904459078
+13
+54.37103932446649
+23
+79.3929069355525
+33
+238.77440904459078
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+68.91435108328955
+20
+-77.3771682356236
+30
+196.03031466452583
+11
+68.91434614140894
+21
+88.44133256583939
+31
+236.6003932965593
+12
+68.91435129096055
+22
+-84.34409829059867
+32
+189.86102321781914
+13
+68.91435129096055
+23
+-84.34409829059867
+33
+189.86102321781914
+70
+1
+  0
+3DFACE
+ 8
+lid_panels
+10
+68.91434614140894
+20
+88.44133256583939
+30
+236.6003932965593
+11
+68.91435108328955
+21
+-77.3771682356236
+31
+196.03031466452583
+12
+68.9143464110806
+22
+79.39290736898897
+32
+238.77440904459078
+13
+68.9143464110806
+23
+79.39290736898897
+33
+238.77440904459078
+70
+1
+ 0
+ENDSEC
+ 0
+EOF
diff --git a/hacks/glx/dumpster_model.c b/hacks/glx/dumpster_model.c
new file mode 100644 (file)
index 0000000..c33d8cd
--- /dev/null
@@ -0,0 +1,1667 @@
+/* Generated from "dumpster.dxf" on 24-Apr-2025.
+   Smoothed vertex normals at 30°. Normalized to unit bounding box.
+   Components: axle, frame_half, hinges_half, inside_half, lid, lid_panels,
+     panels_half.
+ */
+
+#include "gllist.h"
+
+static const float dumpster_model_axle_data[] = {
+       1,0,0,0.480142,0.283784,0.65531,
+       1,0,0,0.480142,0.278577,0.656705,
+       1,0,0,0.480142,0.278577,0.646291,
+       0,-0.965926,0.258819,0.480142,0.269557,0.651498,
+       0,-0.965926,0.258819,-0.480142,0.268162,0.646291,
+       0,-0.965926,0.258819,0.480142,0.268162,0.646291,
+       0,-0.965926,0.258819,-0.480142,0.268162,0.646291,
+       0,-0.965926,0.258819,0.480142,0.269557,0.651498,
+       0,-0.965926,0.258819,-0.480142,0.269557,0.651498,
+       0,-0.707107,0.707107,0.480142,0.269557,0.651498,
+       0,-0.575061,0.81811,-0.480142,0.273369,0.65531,
+       0,-0.707107,0.707107,-0.480142,0.269557,0.651498,
+       0,-0.575061,0.81811,-0.480142,0.273369,0.65531,
+       0,-0.707107,0.707107,0.480142,0.269557,0.651498,
+       0,-0.420974,0.907073,0.480142,0.273369,0.65531,
+       0,-0.420974,0.907073,0.480142,0.273369,0.65531,
+       0,-0.258819,0.965926,-0.480142,0.278577,0.656705,
+       0,-0.575061,0.81811,-0.480142,0.273369,0.65531,
+       0,-0.258819,0.965926,-0.480142,0.278577,0.656705,
+       0,-0.420974,0.907073,0.480142,0.273369,0.65531,
+       0,-0.258819,0.965926,0.480142,0.278577,0.656705,
+       0,0.866025,-0.5,0.480142,0.287596,0.641084,
+       0,0.707107,-0.707107,-0.480142,0.283784,0.637272,
+       0,0.866025,-0.5,-0.480142,0.287596,0.641084,
+       0,0.707107,-0.707107,-0.480142,0.283784,0.637272,
+       0,0.866025,-0.5,0.480142,0.287596,0.641084,
+       0,0.707107,-0.707107,0.480142,0.283784,0.637272,
+       0,0.258819,0.965926,0.480142,0.278577,0.656705,
+       0,0.420974,0.907073,-0.480142,0.283784,0.65531,
+       0,0.258819,0.965926,-0.480142,0.278577,0.656705,
+       0,0.420974,0.907073,-0.480142,0.283784,0.65531,
+       0,0.258819,0.965926,0.480142,0.278577,0.656705,
+       0,0.575061,0.81811,0.480142,0.283784,0.65531,
+       0,0.965926,-0.258819,-0.480142,0.288991,0.646291,
+       0,0.866025,-0.5,0.480142,0.287596,0.641084,
+       0,0.866025,-0.5,-0.480142,0.287596,0.641084,
+       0,0.866025,-0.5,0.480142,0.287596,0.641084,
+       0,0.965926,-0.258819,-0.480142,0.288991,0.646291,
+       0,0.965926,-0.258819,0.480142,0.288991,0.646291,
+       0,0.575061,0.81811,0.480142,0.283784,0.65531,
+       0,0.707107,0.707107,-0.480142,0.287596,0.651498,
+       0,0.420974,0.907073,-0.480142,0.283784,0.65531,
+       0,0.707107,0.707107,-0.480142,0.287596,0.651498,
+       0,0.575061,0.81811,0.480142,0.283784,0.65531,
+       0,0.707107,0.707107,0.480142,0.287596,0.651498,
+       0,0.965926,0.258819,-0.480142,0.287596,0.651498,
+       0,0.965926,0.258819,0.480142,0.288991,0.646291,
+       0,0.965926,0.258819,-0.480142,0.288991,0.646291,
+       0,0.965926,0.258819,0.480142,0.288991,0.646291,
+       0,0.965926,0.258819,-0.480142,0.287596,0.651498,
+       0,0.965926,0.258819,0.480142,0.287596,0.651498,
+       0,0.258819,-0.965926,0.480142,0.283784,0.637272,
+       0,0.088962,-0.996035,-0.480142,0.278577,0.635876,
+       0,0.258819,-0.965926,-0.480142,0.283784,0.637272,
+       0,0.088962,-0.996035,-0.480142,0.278577,0.635876,
+       0,0.258819,-0.965926,0.480142,0.283784,0.637272,
+       0,-0.088962,-0.996035,0.480142,0.278577,0.635876,
+       0,-0.088962,-0.996035,0.480142,0.278577,0.635876,
+       0,-0.420974,-0.907073,-0.480142,0.273369,0.637272,
+       0,0.088962,-0.996035,-0.480142,0.278577,0.635876,
+       0,-0.420974,-0.907073,-0.480142,0.273369,0.637272,
+       0,-0.088962,-0.996035,0.480142,0.278577,0.635876,
+       0,-0.575061,-0.81811,0.480142,0.273369,0.637272,
+       0,-0.575061,-0.81811,0.480142,0.273369,0.637272,
+       0,-0.866025,-0.5,-0.480142,0.269557,0.641084,
+       0,-0.420974,-0.907073,-0.480142,0.273369,0.637272,
+       0,-0.866025,-0.5,-0.480142,0.269557,0.641084,
+       0,-0.575061,-0.81811,0.480142,0.273369,0.637272,
+       0,-0.866025,-0.5,0.480142,0.269557,0.641084,
+       0,-0.965926,-0.258819,0.480142,0.268162,0.646291,
+       0,-0.866025,-0.5,-0.480142,0.269557,0.641084,
+       0,-0.866025,-0.5,0.480142,0.269557,0.641084,
+       0,-0.866025,-0.5,-0.480142,0.269557,0.641084,
+       0,-0.965926,-0.258819,0.480142,0.268162,0.646291,
+       0,-0.965926,-0.258819,-0.480142,0.268162,0.646291,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.273369,0.65531,
+       -1,0,0,-0.480142,0.278577,0.656705,
+       1,0,0,0.480142,0.288991,0.646291,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.287596,0.641084,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.269557,0.641084,
+       1,0,0,0.480142,0.273369,0.637272,
+       1,0,0,0.480142,0.278577,0.656705,
+       1,0,0,0.480142,0.273369,0.65531,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.288991,0.646291,
+       1,0,0,0.480142,0.287596,0.651498,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.273369,0.637272,
+       1,0,0,0.480142,0.278577,0.635876,
+       1,0,0,0.480142,0.287596,0.651498,
+       1,0,0,0.480142,0.283784,0.65531,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.268162,0.646291,
+       1,0,0,0.480142,0.269557,0.641084,
+       1,0,0,0.480142,0.283784,0.637272,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.278577,0.635876,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.273369,0.65531,
+       1,0,0,0.480142,0.269557,0.651498,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.269557,0.651498,
+       1,0,0,0.480142,0.268162,0.646291,
+       1,0,0,0.480142,0.287596,0.641084,
+       1,0,0,0.480142,0.278577,0.646291,
+       1,0,0,0.480142,0.283784,0.637272,
+       -1,0,0,-0.480142,0.287596,0.641084,
+       -1,0,0,-0.480142,0.283784,0.637272,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       0,-1,0,0.480142,0.278577,0.646291,
+       0,-1,0,-0.480142,0.278577,0.635876,
+       0,-1,0,0.480142,0.278577,0.635876,
+       0,-1,0,-0.480142,0.278577,0.635876,
+       0,-1,0,0.480142,0.278577,0.646291,
+       0,-1,0,-0.480142,0.278577,0.646291,
+       0,-1,0,-0.480142,0.278577,0.646291,
+       0,-1,0,0.480142,0.278577,0.646291,
+       0,-1,0,0.480142,0.278577,0.656705,
+       0,-1,0,-0.480142,0.278577,0.646291,
+       0,-1,0,0.480142,0.278577,0.656705,
+       0,-1,0,-0.480142,0.278577,0.656705,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.268162,0.646291,
+       -1,0,0,-0.480142,0.269557,0.651498,
+       -1,0,0,-0.480142,0.283784,0.637272,
+       -1,0,0,-0.480142,0.278577,0.635876,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       0,0.866025,0.5,-0.480142,0.278577,0.646291,
+       0,0.866025,0.5,0.480142,0.283784,0.637272,
+       0,0.866025,0.5,-0.480142,0.283784,0.637272,
+       0,0.866025,0.5,0.480142,0.283784,0.637272,
+       0,0.866025,0.5,-0.480142,0.278577,0.646291,
+       0,0.866025,0.5,0.480142,0.278577,0.646291,
+       0,0.866025,0.5,0.480142,0.278577,0.646291,
+       0,0.866025,0.5,-0.480142,0.278577,0.646291,
+       0,0.866025,0.5,-0.480142,0.273369,0.65531,
+       0,0.866025,0.5,0.480142,0.278577,0.646291,
+       0,0.866025,0.5,-0.480142,0.273369,0.65531,
+       0,0.866025,0.5,0.480142,0.273369,0.65531,
+       -1,0,0,-0.480142,0.283784,0.65531,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.278577,0.656705,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.269557,0.651498,
+       -1,0,0,-0.480142,0.273369,0.65531,
+       0,-0.5,-0.866025,0.480142,0.287596,0.641084,
+       0,-0.5,-0.866025,-0.480142,0.278577,0.646291,
+       0,-0.5,-0.866025,-0.480142,0.287596,0.641084,
+       0,-0.5,-0.866025,-0.480142,0.278577,0.646291,
+       0,-0.5,-0.866025,0.480142,0.287596,0.641084,
+       0,-0.5,-0.866025,-0.480142,0.269557,0.651498,
+       0,-0.5,-0.866025,-0.480142,0.269557,0.651498,
+       0,-0.5,-0.866025,0.480142,0.287596,0.641084,
+       0,-0.5,-0.866025,0.480142,0.269557,0.651498,
+       -1,0,0,0.480142,0.269557,0.651498,
+       -1,0,0,0.480142,0.287596,0.641084,
+       -1,0,0,0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.269557,0.641084,
+       -1,0,0,-0.480142,0.268162,0.646291,
+       -1,0,0,-0.480142,0.288991,0.646291,
+       -1,0,0,-0.480142,0.287596,0.641084,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       0,0,1,0.480142,0.268162,0.646291,
+       0,0,1,-0.480142,0.278577,0.646291,
+       0,0,1,-0.480142,0.268162,0.646291,
+       0,0,1,-0.480142,0.278577,0.646291,
+       0,0,1,0.480142,0.268162,0.646291,
+       0,0,1,-0.480142,0.288991,0.646291,
+       0,0,1,-0.480142,0.288991,0.646291,
+       0,0,1,0.480142,0.268162,0.646291,
+       0,0,1,0.480142,0.288991,0.646291,
+       0.534871,0.774969,0.336653,0.480142,0.288991,0.646291,
+       0.44821,-0.649406,0.614312,0.480142,0.268162,0.646291,
+       0.920575,-0.276172,0.276172,0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.287596,0.651498,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.283784,0.65531,
+       -1,0,0,-0.480142,0.278577,0.635876,
+       -1,0,0,-0.480142,0.273369,0.637272,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       0,-0.866025,0.5,0.480142,0.278577,0.646291,
+       0,-0.866025,0.5,-0.480142,0.273369,0.637272,
+       0,-0.866025,0.5,0.480142,0.273369,0.637272,
+       0,-0.866025,0.5,-0.480142,0.273369,0.637272,
+       0,-0.866025,0.5,0.480142,0.278577,0.646291,
+       0,-0.866025,0.5,-0.480142,0.278577,0.646291,
+       0,-0.866025,0.5,-0.480142,0.278577,0.646291,
+       0,-0.866025,0.5,0.480142,0.278577,0.646291,
+       0,-0.866025,0.5,0.480142,0.283784,0.65531,
+       0,-0.866025,0.5,-0.480142,0.278577,0.646291,
+       0,-0.866025,0.5,0.480142,0.283784,0.65531,
+       0,-0.866025,0.5,-0.480142,0.283784,0.65531,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.273369,0.637272,
+       -1,0,0,-0.480142,0.269557,0.641084,
+       -1,0,0,-0.480142,0.288991,0.646291,
+       -1,0,0,-0.480142,0.278577,0.646291,
+       -1,0,0,-0.480142,0.287596,0.651498,
+       0,0.5,-0.866025,0.480142,0.287596,0.651498,
+       0,0.5,-0.866025,-0.480142,0.278577,0.646291,
+       0,0.5,-0.866025,-0.480142,0.287596,0.651498,
+       0,0.5,-0.866025,-0.480142,0.278577,0.646291,
+       0,0.5,-0.866025,0.480142,0.287596,0.651498,
+       0,0.5,-0.866025,-0.480142,0.269557,0.641084,
+       0,0.5,-0.866025,-0.480142,0.269557,0.641084,
+       0,0.5,-0.866025,0.480142,0.287596,0.651498,
+       0,0.5,-0.866025,0.480142,0.269557,0.641084,
+       -1,0,0,0.480142,0.269557,0.641084,
+       -1,0,0,0.480142,0.287596,0.651498,
+       -1,0,0,0.480142,0.278577,0.646291
+};
+static const struct gllist dumpster_model_axle_frame = {
+ GL_N3F_V3F, GL_TRIANGLES, 216, dumpster_model_axle_data, 0
+};
+const struct gllist *dumpster_model_axle = &dumpster_model_axle_frame;
+
+static const float dumpster_model_frame_half_data[] = {
+       0,0,-1,0.455529,0.260975,0.000853,
+       0,0,-1,0,-0.260975,0.000853,
+       0,0,-1,0,0.260975,0.000853,
+       0,0,-1,0,-0.260975,0.000853,
+       0,0,-1,0.455529,0.260975,0.000853,
+       0,0,-1,0.455529,-0.260975,0.000853,
+       0,1,0,0,0.291396,0.602678,
+       0,1,0,0.480142,0.291396,0.602667,
+       0,1,0,0.465575,0.291396,0.602667,
+       0,1,0,0.480142,0.291396,0.602667,
+       0,1,0,0,0.291396,0.602678,
+       0,1,0,0.480142,0.291396,0.627761,
+       0,1,0,0.480142,0.291396,0.627761,
+       0,1,0,0,0.291396,0.602678,
+       0,1,0,0,0.291396,0.627761,
+       0,0,1,0.480142,0.291396,0.627761,
+       0,0,1,0,0.291396,0.627761,
+       0,0,1,0.455529,0.291396,0.627761,
+       0,-1,0,0.455529,-0.260975,0.000853,
+       0,-1,0,0.43601,-0.260975,0.020372,
+       0,-1,0,0.019519,-0.260975,0.020372,
+       0,-1,0,0.43601,-0.260975,0.020372,
+       0,-1,0,0.455529,-0.260975,0.000853,
+       0,-1,0,0.455529,-0.260975,0.467139,
+       0,-1,0,0.43601,-0.260975,0.020372,
+       0,-1,0,0.455529,-0.260975,0.467139,
+       0,-1,0,0.43601,-0.260975,0.44762,
+       0,-1,0,0.43601,-0.260975,0.44762,
+       0,-1,0,0.455529,-0.260975,0.467139,
+       0,-1,0,0.019519,-0.260975,0.447625,
+       0,-1,0,0,-0.260975,0.000853,
+       0,-1,0,0.019519,-0.260975,0.020372,
+       0,-1,0,0,-0.260975,0.467144,
+       0,-1,0,0.019519,-0.260975,0.020372,
+       0,-1,0,0,-0.260975,0.000853,
+       0,-1,0,0.455529,-0.260975,0.000853,
+       0,-1,0,0,-0.260975,0.467144,
+       0,-1,0,0.019519,-0.260975,0.020372,
+       0,-1,0,0.019519,-0.260975,0.447625,
+       0,-1,0,0,-0.260975,0.467144,
+       0,-1,0,0.019519,-0.260975,0.447625,
+       0,-1,0,0.455529,-0.260975,0.467139,
+       0,1,0,0,0.260975,0.000853,
+       0,1,0,0.019519,0.260975,0.020372,
+       0,1,0,0.455529,0.260975,0.000853,
+       0,1,0,0.019519,0.260975,0.020372,
+       0,1,0,0,0.260975,0.000853,
+       0,1,0,0,0.260975,0.602678,
+       0,1,0,0.019519,0.260975,0.020372,
+       0,1,0,0,0.260975,0.602678,
+       0,1,0,0.019519,0.260975,0.583158,
+       0,1,0,0.019519,0.260975,0.583158,
+       0,1,0,0,0.260975,0.602678,
+       0,1,0,0.455529,0.260975,0.602667,
+       0,1,0,0.455529,0.260975,0.000853,
+       0,1,0,0.43601,0.260975,0.020372,
+       0,1,0,0.455529,0.260975,0.602667,
+       0,1,0,0.43601,0.260975,0.020372,
+       0,1,0,0.455529,0.260975,0.000853,
+       0,1,0,0.019519,0.260975,0.020372,
+       0,1,0,0.455529,0.260975,0.602667,
+       0,1,0,0.43601,0.260975,0.020372,
+       0,1,0,0.43601,0.260975,0.583149,
+       0,1,0,0.455529,0.260975,0.602667,
+       0,1,0,0.43601,0.260975,0.583149,
+       0,1,0,0.019519,0.260975,0.583158,
+       0,-1,0,0.480142,-0.285801,0.50375,
+       0,-1,0,0.465574,-0.285801,0.467139,
+       0,-1,0,0.480142,-0.285801,0.467139,
+       0,-1,0,0.465574,-0.285801,0.467139,
+       0,-1,0,0.480142,-0.285801,0.50375,
+       0,-1,0,0,-0.285801,0.467144,
+       0,-1,0,0,-0.285801,0.467144,
+       0,-1,0,0.480142,-0.285801,0.50375,
+       0,-1,0,0,-0.285801,0.50375,
+       0,0,1,0.480142,-0.285801,0.50375,
+       0,0,1,0,-0.223786,0.50375,
+       0,0,1,0,-0.285801,0.50375,
+       0,0,1,0,-0.223786,0.50375,
+       0,0,1,0.480142,-0.285801,0.50375,
+       0,-0.131708,0.991289,0.431095,-0.223786,0.50375,
+       0,-0.131708,0.991289,0.431095,-0.223786,0.50375,
+       0,0,1,0.480142,-0.285801,0.50375,
+       0,-0.131708,0.991289,0.480142,-0.223786,0.50375,
+       0,-0.065709,0.997839,0.431095,0.234657,0.627761,
+       0,0,1,0,0.291396,0.627761,
+       0,0,1,0,0.234657,0.627761,
+       0,0,1,0,0.291396,0.627761,
+       0,-0.065709,0.997839,0.431095,0.234657,0.627761,
+       0,0,1,0.455529,0.291396,0.627761,
+       0,0,1,0.455529,0.291396,0.627761,
+       0,-0.065709,0.997839,0.431095,0.234657,0.627761,
+       0,-0.131708,0.991289,0.480142,0.234657,0.627761,
+       0,0,1,0.455529,0.291396,0.627761,
+       0,-0.131708,0.991289,0.480142,0.234657,0.627761,
+       0,0,1,0.480142,0.291396,0.627761,
+       0,-0.131708,0.991289,0.431095,-0.223786,0.50375,
+       0,-0.131708,0.991289,0.480142,0.234657,0.627761,
+       0,-0.065709,0.997839,0.431095,0.234657,0.627761,
+       0,-0.131708,0.991289,0.480142,0.234657,0.627761,
+       0,-0.131708,0.991289,0.431095,-0.223786,0.50375,
+       0,-0.131708,0.991289,0.480142,-0.223786,0.50375,
+       -0.000004,0.101312,-0.994855,0.455529,-0.260975,0.467139,
+       -0.000011,-0.000002,-1,0,-0.285801,0.467144,
+       -0.000011,0,-1,0,-0.260975,0.467144,
+       -0.000011,-0.000002,-1,0,-0.285801,0.467144,
+       -0.000004,0.101312,-0.994855,0.455529,-0.260975,0.467139,
+       -0.000004,-0.000001,-1,0.465574,-0.285801,0.467139,
+       -0.000004,-0.000001,-1,0.465574,-0.285801,0.467139,
+       -0.000004,0.101312,-0.994855,0.455529,-0.260975,0.467139,
+       0,0.084378,-0.996434,0.480142,-0.260975,0.467139,
+       -0.000004,-0.000001,-1,0.465574,-0.285801,0.467139,
+       0,0.084378,-0.996434,0.480142,-0.260975,0.467139,
+       0,0,-1,0.480142,-0.285801,0.467139,
+       -0.000012,0.000002,-1,0.465575,0.291396,0.602667,
+       -0.000023,0.000004,-1,0,0.260975,0.602678,
+       -0.000023,0,-1,0,0.291396,0.602678,
+       -0.000023,0.000004,-1,0,0.260975,0.602678,
+       -0.000012,0.000002,-1,0.465575,0.291396,0.602667,
+       -0.000008,0.084381,-0.996434,0.455529,0.260975,0.602667,
+       -0.000008,0.084381,-0.996434,0.455529,0.260975,0.602667,
+       -0.000012,0.000002,-1,0.465575,0.291396,0.602667,
+       0,0.126682,-0.991943,0.480142,0.260975,0.602667,
+       0,0.126682,-0.991943,0.480142,0.260975,0.602667,
+       -0.000012,0.000002,-1,0.465575,0.291396,0.602667,
+       0,0,-1,0.480142,0.291396,0.602667,
+       0,0.126682,-0.991943,0.480142,0.260975,0.602667,
+       -0.000004,0.101312,-0.994855,0.455529,-0.260975,0.467139,
+       -0.000008,0.084381,-0.996434,0.455529,0.260975,0.602667,
+       -0.000004,0.101312,-0.994855,0.455529,-0.260975,0.467139,
+       0,0.126682,-0.991943,0.480142,0.260975,0.602667,
+       0,0.084378,-0.996434,0.480142,-0.260975,0.467139,
+       1,0,0,0.480142,0.260975,0.602667,
+       1,0,0,0.480142,-0.223786,0.50375,
+       1,0,0,0.480142,-0.260975,0.467139,
+       1,0,0,0.480142,-0.223786,0.50375,
+       1,0,0,0.480142,0.260975,0.602667,
+       1,0,0,0.480142,0.234657,0.627761,
+       1,0,0,0.019519,-0.260975,0.020372,
+       1,0,0,0.019519,-0.247708,0.447625,
+       1,0,0,0.019519,-0.260975,0.447625,
+       1,0,0,0.019519,-0.247708,0.447625,
+       1,0,0,0.019519,-0.260975,0.020372,
+       1,0,0,0.019519,-0.247708,0.020372,
+       0,0,1,0.019519,-0.260975,0.020372,
+       0,0,1,0.43601,-0.247708,0.020372,
+       0,0,1,0.019519,-0.247708,0.020372,
+       0,0,1,0.43601,-0.247708,0.020372,
+       0,0,1,0.019519,-0.260975,0.020372,
+       0,0,1,0.43601,-0.260975,0.020372,
+       -1,0,0,0.43601,-0.247708,0.44762,
+       -1,0,0,0.43601,-0.260975,0.020372,
+       -1,0,0,0.43601,-0.260975,0.44762,
+       -1,0,0,0.43601,-0.260975,0.020372,
+       -1,0,0,0.43601,-0.247708,0.44762,
+       -1,0,0,0.43601,-0.247708,0.020372,
+       -0.000011,0,-1,0.43601,-0.247708,0.44762,
+       -0.000011,0,-1,0.019519,-0.260975,0.447625,
+       -0.000011,0,-1,0.019519,-0.247708,0.447625,
+       -0.000011,0,-1,0.019519,-0.260975,0.447625,
+       -0.000011,0,-1,0.43601,-0.247708,0.44762,
+       -0.000011,0,-1,0.43601,-0.260975,0.44762,
+       -1,0,0,0.43601,0.260975,0.583149,
+       -1,0,0,0.43601,0.247708,0.020372,
+       -1,0,0,0.43601,0.247708,0.583149,
+       -1,0,0,0.43601,0.247708,0.020372,
+       -1,0,0,0.43601,0.260975,0.583149,
+       -1,0,0,0.43601,0.260975,0.020372,
+       0,0,1,0.019519,0.247708,0.020372,
+       0,0,1,0.43601,0.260975,0.020372,
+       0,0,1,0.019519,0.260975,0.020372,
+       0,0,1,0.43601,0.260975,0.020372,
+       0,0,1,0.019519,0.247708,0.020372,
+       0,0,1,0.43601,0.247708,0.020372,
+       1,0,0,0.019519,0.247708,0.020372,
+       1,0,0,0.019519,0.260975,0.583158,
+       1,0,0,0.019519,0.247708,0.583158,
+       1,0,0,0.019519,0.260975,0.583158,
+       1,0,0,0.019519,0.247708,0.020372,
+       1,0,0,0.019519,0.260975,0.020372,
+       -0.000023,0,-1,0.43601,0.260975,0.583149,
+       -0.000023,0,-1,0.019519,0.247708,0.583158,
+       -0.000023,0,-1,0.019519,0.260975,0.583158,
+       -0.000023,0,-1,0.019519,0.247708,0.583158,
+       -0.000023,0,-1,0.43601,0.260975,0.583149,
+       -0.000023,0,-1,0.43601,0.247708,0.583149,
+       1,0,0,0.455529,-0.260975,0.000853,
+       1,0,0,0.455529,-0.242083,0.019745,
+       1,0,0,0.455529,-0.260975,0.467139,
+       1,0,0,0.455529,-0.242083,0.019745,
+       1,0,0,0.455529,-0.260975,0.000853,
+       1,0,0,0.455529,0.260975,0.000853,
+       1,0,0,0.455529,-0.242083,0.019745,
+       1,0,0,0.455529,0.260975,0.000853,
+       1,0,0,0.455529,0.242083,0.019745,
+       1,0,0,0.455529,0.242083,0.019745,
+       1,0,0,0.455529,0.260975,0.000853,
+       1,0,0,0.455529,0.242083,0.578243,
+       1,0,0,0.455529,-0.260975,0.467139,
+       1,0,0,0.455529,-0.242083,0.452526,
+       1,0,0,0.455529,0.260975,0.602667,
+       1,0,0,0.455529,-0.242083,0.452526,
+       1,0,0,0.455529,-0.260975,0.467139,
+       1,0,0,0.455529,-0.242083,0.019745,
+       1,0,0,0.455529,0.260975,0.602667,
+       1,0,0,0.455529,-0.242083,0.452526,
+       1,0,0,0.455529,0.242083,0.578243,
+       1,0,0,0.455529,0.260975,0.602667,
+       1,0,0,0.455529,0.242083,0.578243,
+       1,0,0,0.455529,0.260975,0.000853,
+       0,0.251323,-0.967903,0.455529,0.242083,0.578243,
+       0,0.251323,-0.967903,0.442262,-0.242083,0.452526,
+       0,0.251323,-0.967903,0.442262,0.242083,0.578243,
+       0,0.251323,-0.967903,0.442262,-0.242083,0.452526,
+       0,0.251323,-0.967903,0.455529,0.242083,0.578243,
+       0,0.251323,-0.967903,0.455529,-0.242083,0.452526,
+       0,1,0,0.442262,-0.242083,0.452526,
+       0,1,0,0.455529,-0.242083,0.019745,
+       0,1,0,0.442262,-0.242083,0.019745,
+       0,1,0,0.455529,-0.242083,0.019745,
+       0,1,0,0.442262,-0.242083,0.452526,
+       0,1,0,0.455529,-0.242083,0.452526,
+       0,0,1,0.442262,-0.242083,0.019745,
+       0,0,1,0.455529,0.242083,0.019745,
+       0,0,1,0.442262,0.242083,0.019745,
+       0,0,1,0.455529,0.242083,0.019745,
+       0,0,1,0.442262,-0.242083,0.019745,
+       0,0,1,0.455529,-0.242083,0.019745,
+       0,-1,0,0.455529,0.242083,0.578243,
+       0,-1,0,0.442262,0.242083,0.019745,
+       0,-1,0,0.455529,0.242083,0.019745,
+       0,-1,0,0.442262,0.242083,0.019745,
+       0,-1,0,0.455529,0.242083,0.578243,
+       0,-1,0,0.442262,0.242083,0.578243,
+       0.000001,1,0,0.448896,0.094026,0.260961,
+       0.000001,1,0,0.519858,0.094026,0.411797,
+       0.000001,1,0,0.519858,0.094026,0.260961,
+       0.000001,1,0,0.519858,0.094026,0.411797,
+       0.000001,1,0,0.448896,0.094026,0.260961,
+       0.000001,1,0,0.448896,0.094026,0.411797,
+       -0.000001,-1,0,0.519858,-0.049899,0.260961,
+       -0.000001,-1,0,0.509245,-0.049899,0.271574,
+       -0.000001,-1,0,0.448896,-0.049899,0.260961,
+       -0.000001,-1,0,0.509245,-0.049899,0.271574,
+       -0.000001,-1,0,0.519858,-0.049899,0.260961,
+       0,-1,0,0.519858,-0.049899,0.411797,
+       -0.000001,-1,0,0.509245,-0.049899,0.271574,
+       0,-1,0,0.519858,-0.049899,0.411797,
+       0,-1,0,0.509245,-0.049899,0.401184,
+       0,-1,0,0.509245,-0.049899,0.401184,
+       0,-1,0,0.519858,-0.049899,0.411797,
+       0,-1,0,0.459509,-0.049899,0.401184,
+       -0.000001,-1,0,0.448896,-0.049899,0.260961,
+       -0.000001,-1,0,0.459509,-0.049899,0.271574,
+       0,-1,0,0.448896,-0.049899,0.411797,
+       -0.000001,-1,0,0.459509,-0.049899,0.271574,
+       -0.000001,-1,0,0.448896,-0.049899,0.260961,
+       -0.000001,-1,0,0.509245,-0.049899,0.271574,
+       0,-1,0,0.448896,-0.049899,0.411797,
+       -0.000001,-1,0,0.459509,-0.049899,0.271574,
+       0,-1,0,0.459509,-0.049899,0.401184,
+       0,-1,0,0.448896,-0.049899,0.411797,
+       0,-1,0,0.459509,-0.049899,0.401184,
+       0,-1,0,0.519858,-0.049899,0.411797,
+       1,0,0,0.519858,0.094026,0.260961,
+       1,0,0,0.519858,-0.049899,0.411797,
+       1,0,0,0.519858,-0.049899,0.260961,
+       1,0,0,0.519858,-0.049899,0.411797,
+       1,0,0,0.519858,0.094026,0.260961,
+       1,0,0,0.519858,0.094026,0.411797,
+       -0.000001,0,-1,0.448896,0.094026,0.260961,
+       -0.000001,0,-1,0.519858,-0.049899,0.260961,
+       -0.000001,0,-1,0.448896,-0.049899,0.260961,
+       -0.000001,0,-1,0.519858,-0.049899,0.260961,
+       -0.000001,0,-1,0.448896,0.094026,0.260961,
+       -0.000001,0,-1,0.519858,0.094026,0.260961,
+       -1,0,0,0.448896,0.094026,0.411797,
+       -1,0,0,0.448896,-0.049899,0.260961,
+       -1,0,0,0.448896,-0.049899,0.411797,
+       -1,0,0,0.448896,-0.049899,0.260961,
+       -1,0,0,0.448896,0.094026,0.411797,
+       -1,0,0,0.448896,0.094026,0.260961,
+       0,0,1,0.519858,-0.049899,0.411797,
+       0,0,1,0.448896,0.094026,0.411797,
+       0,0,1,0.448896,-0.049899,0.411797,
+       0,0,1,0.448896,0.094026,0.411797,
+       0,0,1,0.519858,-0.049899,0.411797,
+       0,0,1,0.519858,0.094026,0.411797,
+       0,0.942079,-0.335391,0.396973,-0.204299,0.00019,
+       0,0.942079,-0.335391,0.43601,-0.213745,-0.026342,
+       0,0.942079,-0.335391,0.416491,-0.213745,-0.026342,
+       0,0.942079,-0.335391,0.43601,-0.213745,-0.026342,
+       0,0.942079,-0.335391,0.396973,-0.204299,0.00019,
+       0,0.942079,-0.335391,0.43601,-0.204299,0.00019,
+       0,-0.942079,-0.335391,0.43601,-0.242083,0.00019,
+       0,-0.942079,-0.335391,0.416491,-0.232637,-0.026342,
+       0,-0.942079,-0.335391,0.43601,-0.232637,-0.026342,
+       0,-0.942079,-0.335391,0.416491,-0.232637,-0.026342,
+       0,-0.942079,-0.335391,0.43601,-0.242083,0.00019,
+       0,-0.942079,-0.335391,0.396973,-0.242083,0.00019,
+       0,0,-1,0.43601,0.232637,-0.026342,
+       0,0,-1,0.416491,0.213745,-0.026342,
+       0,0,-1,0.416491,0.232637,-0.026342,
+       0,0,-1,0.416491,0.213745,-0.026342,
+       0,0,-1,0.43601,0.232637,-0.026342,
+       0,0,-1,0.43601,0.213745,-0.026342,
+       1,0,0,0.43601,0.213745,-0.026342,
+       1,0,0,0.43601,0.242083,0.00019,
+       1,0,0,0.43601,0.204299,0.00019,
+       1,0,0,0.43601,0.242083,0.00019,
+       1,0,0,0.43601,0.213745,-0.026342,
+       1,0,0,0.43601,0.232637,-0.026342,
+       -0.805513,0,-0.592578,0.396973,0.242083,0.00019,
+       -0.805513,0,-0.592578,0.416491,0.213745,-0.026342,
+       -0.805513,0,-0.592577,0.396973,0.204299,0.00019,
+       -0.805513,0,-0.592578,0.416491,0.213745,-0.026342,
+       -0.805513,0,-0.592578,0.396973,0.242083,0.00019,
+       -0.805513,0,-0.592578,0.416491,0.232637,-0.026342,
+       0,-0.942079,-0.335391,0.43601,0.204299,0.00019,
+       0,-0.942079,-0.335391,0.416491,0.213745,-0.026342,
+       0,-0.942079,-0.335391,0.43601,0.213745,-0.026342,
+       0,-0.942079,-0.335391,0.416491,0.213745,-0.026342,
+       0,-0.942079,-0.335391,0.43601,0.204299,0.00019,
+       0,-0.942079,-0.335391,0.396973,0.204299,0.00019,
+       0,0.942079,-0.335391,0.396973,0.242083,0.00019,
+       0,0.942079,-0.335391,0.43601,0.232637,-0.026342,
+       0,0.942079,-0.335391,0.416491,0.232637,-0.026342,
+       0,0.942079,-0.335391,0.43601,0.232637,-0.026342,
+       0,0.942079,-0.335391,0.396973,0.242083,0.00019,
+       0,0.942079,-0.335391,0.43601,0.242083,0.00019,
+       1,0,0,0.43601,-0.232637,-0.026342,
+       1,0,0,0.43601,-0.204299,0.00019,
+       1,0,0,0.43601,-0.242083,0.00019,
+       1,0,0,0.43601,-0.204299,0.00019,
+       1,0,0,0.43601,-0.232637,-0.026342,
+       1,0,0,0.43601,-0.213745,-0.026342,
+       -0.805513,0,-0.592578,0.396973,-0.204299,0.00019,
+       -0.805513,0,-0.592578,0.416491,-0.232637,-0.026342,
+       -0.805513,0,-0.592577,0.396973,-0.242083,0.00019,
+       -0.805513,0,-0.592578,0.416491,-0.232637,-0.026342,
+       -0.805513,0,-0.592578,0.396973,-0.204299,0.00019,
+       -0.805513,0,-0.592578,0.416491,-0.213745,-0.026342,
+       0,0,-1,0.43601,-0.213745,-0.026342,
+       0,0,-1,0.416491,-0.232637,-0.026342,
+       0,0,-1,0.416491,-0.213745,-0.026342,
+       0,0,-1,0.416491,-0.232637,-0.026342,
+       0,0,-1,0.43601,-0.213745,-0.026342,
+       0,0,-1,0.43601,-0.232637,-0.026342,
+       1,0,0,0.480142,-0.260975,0.467139,
+       1,0,0,0.480142,-0.285801,0.50375,
+       1,0,0,0.480142,-0.285801,0.467139,
+       1,0,0,0.480142,-0.285801,0.50375,
+       1,0,0,0.480142,-0.260975,0.467139,
+       1,0,0,0.480142,-0.223786,0.50375,
+       1,0,0,0.480142,0.260975,0.602667,
+       1,0,0,0.480142,0.291396,0.627761,
+       1,0,0,0.480142,0.234657,0.627761,
+       1,0,0,0.480142,0.291396,0.627761,
+       1,0,0,0.480142,0.260975,0.602667,
+       1,0,0,0.480142,0.291396,0.602667
+};
+static const struct gllist dumpster_model_frame_half_frame = {
+ GL_N3F_V3F, GL_TRIANGLES, 360, dumpster_model_frame_half_data, 0
+};
+const struct gllist *dumpster_model_frame_half = &dumpster_model_frame_half_frame;
+
+static const float dumpster_model_hinges_half_data[] = {
+       0,0.707107,0.707107,0.038749,0.287999,0.661827,
+       0,0.81811,0.575061,0.14439,0.294897,0.654929,
+       0,0.907073,0.420974,0.038749,0.294897,0.654929,
+       0,0.81811,0.575061,0.14439,0.294897,0.654929,
+       0,0.707107,0.707107,0.038749,0.287999,0.661827,
+       0,0.707107,0.707107,0.14439,0.287999,0.661827,
+       0,-0.996035,-0.088962,0.14439,0.259731,0.645507,
+       0,-0.965926,-0.258819,0.038749,0.262256,0.636084,
+       0,-0.965926,-0.258819,0.14439,0.262256,0.636084,
+       0,-0.965926,-0.258819,0.038749,0.262256,0.636084,
+       0,-0.996035,-0.088962,0.14439,0.259731,0.645507,
+       0,-0.996035,0.088962,0.038749,0.259731,0.645507,
+       0,-0.258819,-0.965926,0.14439,0.278577,0.626661,
+       0,-0.5,-0.866025,0.038749,0.269154,0.629186,
+       0,-0.258819,-0.965926,0.038749,0.278577,0.626661,
+       0,-0.5,-0.866025,0.038749,0.269154,0.629186,
+       0,-0.258819,-0.965926,0.14439,0.278577,0.626661,
+       0,-0.5,-0.866025,0.14439,0.269154,0.629186,
+       0,0.707107,-0.707107,0.14439,0.294897,0.636084,
+       0,0.575061,-0.81811,0.038749,0.287999,0.629186,
+       0,0.707107,-0.707107,0.038749,0.294897,0.636084,
+       0,0.575061,-0.81811,0.038749,0.287999,0.629186,
+       0,0.707107,-0.707107,0.14439,0.294897,0.636084,
+       0,0.420974,-0.907073,0.14439,0.287999,0.629186,
+       0,-0.965926,0.258819,0.14439,0.262256,0.654929,
+       0,-0.996035,0.088962,0.038749,0.259731,0.645507,
+       0,-0.996035,-0.088962,0.14439,0.259731,0.645507,
+       0,-0.996035,0.088962,0.038749,0.259731,0.645507,
+       0,-0.965926,0.258819,0.14439,0.262256,0.654929,
+       0,-0.965926,0.258819,0.038749,0.262256,0.654929,
+       0,0.907073,0.420974,0.038749,0.294897,0.654929,
+       0,0.965926,0.258819,0.14439,0.297422,0.645507,
+       0,0.965926,0.258819,0.038749,0.297422,0.645507,
+       0,0.965926,0.258819,0.14439,0.297422,0.645507,
+       0,0.907073,0.420974,0.038749,0.294897,0.654929,
+       0,0.81811,0.575061,0.14439,0.294897,0.654929,
+       0,-0.707107,-0.707107,0.14439,0.262256,0.636084,
+       0,-0.5,-0.866025,0.038749,0.269154,0.629186,
+       0,-0.5,-0.866025,0.14439,0.269154,0.629186,
+       0,-0.5,-0.866025,0.038749,0.269154,0.629186,
+       0,-0.707107,-0.707107,0.14439,0.262256,0.636084,
+       0,-0.707107,-0.707107,0.038749,0.262256,0.636084,
+       0,0.965926,-0.258819,0.038749,0.297422,0.645507,
+       0,0.965926,-0.258819,0.14439,0.294897,0.636084,
+       0,0.965926,-0.258819,0.038749,0.294897,0.636084,
+       0,0.965926,-0.258819,0.14439,0.294897,0.636084,
+       0,0.965926,-0.258819,0.038749,0.297422,0.645507,
+       0,0.965926,-0.258819,0.14439,0.297422,0.645507,
+       0,-0.707107,0.707107,0.14439,0.262256,0.654929,
+       0,-0.575061,0.81811,0.038749,0.269154,0.661827,
+       0,-0.707107,0.707107,0.038749,0.262256,0.654929,
+       0,-0.575061,0.81811,0.038749,0.269154,0.661827,
+       0,-0.707107,0.707107,0.14439,0.262256,0.654929,
+       0,-0.420974,0.907073,0.14439,0.269154,0.661827,
+       0,-0.420974,0.907073,0.14439,0.269154,0.661827,
+       0,-0.258819,0.965926,0.038749,0.278577,0.664352,
+       0,-0.575061,0.81811,0.038749,0.269154,0.661827,
+       0,-0.258819,0.965926,0.038749,0.278577,0.664352,
+       0,-0.420974,0.907073,0.14439,0.269154,0.661827,
+       0,-0.258819,0.965926,0.14439,0.278577,0.664352,
+       0,0.258819,0.965926,0.14439,0.278577,0.664352,
+       0,0.258819,0.965926,0.038749,0.287999,0.661827,
+       0,0.258819,0.965926,0.038749,0.278577,0.664352,
+       0,0.258819,0.965926,0.038749,0.287999,0.661827,
+       0,0.258819,0.965926,0.14439,0.278577,0.664352,
+       0,0.258819,0.965926,0.14439,0.287999,0.661827,
+       0,0.420974,-0.907073,0.14439,0.287999,0.629186,
+       0,0.258819,-0.965926,0.038749,0.278577,0.626661,
+       0,0.575061,-0.81811,0.038749,0.287999,0.629186,
+       0,0.258819,-0.965926,0.038749,0.278577,0.626661,
+       0,0.420974,-0.907073,0.14439,0.287999,0.629186,
+       0,0.258819,-0.965926,0.14439,0.278577,0.626661,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.262256,0.654929,
+       -1,0,0,0.038749,0.269154,0.661827,
+       0,0.258819,0.965926,0.420704,0.278577,0.664352,
+       0,0.258819,0.965926,0.315063,0.287999,0.661827,
+       0,0.258819,0.965926,0.315063,0.278577,0.664352,
+       0,0.258819,0.965926,0.315063,0.287999,0.661827,
+       0,0.258819,0.965926,0.420704,0.278577,0.664352,
+       0,0.258819,0.965926,0.420704,0.287999,0.661827,
+       0,-0.965926,0.258819,0.420704,0.262256,0.654929,
+       0,-0.996035,0.088962,0.315063,0.259731,0.645507,
+       0,-0.996035,-0.088962,0.420704,0.259731,0.645507,
+       0,-0.996035,0.088962,0.315063,0.259731,0.645507,
+       0,-0.965926,0.258819,0.420704,0.262256,0.654929,
+       0,-0.965926,0.258819,0.315063,0.262256,0.654929,
+       0,-0.258819,-0.965926,0.420704,0.278577,0.626661,
+       0,-0.5,-0.866025,0.315063,0.269154,0.629186,
+       0,-0.258819,-0.965926,0.315063,0.278577,0.626661,
+       0,-0.5,-0.866025,0.315063,0.269154,0.629186,
+       0,-0.258819,-0.965926,0.420704,0.278577,0.626661,
+       0,-0.5,-0.866025,0.420704,0.269154,0.629186,
+       0,-0.707107,0.707107,0.420704,0.262256,0.654929,
+       0,-0.575061,0.81811,0.315063,0.269154,0.661827,
+       0,-0.707107,0.707107,0.315063,0.262256,0.654929,
+       0,-0.575061,0.81811,0.315063,0.269154,0.661827,
+       0,-0.707107,0.707107,0.420704,0.262256,0.654929,
+       0,-0.420974,0.907073,0.420704,0.269154,0.661827,
+       0,0.707107,0.707107,0.315063,0.287999,0.661827,
+       0,0.81811,0.575061,0.420704,0.294897,0.654929,
+       0,0.907073,0.420974,0.315063,0.294897,0.654929,
+       0,0.81811,0.575061,0.420704,0.294897,0.654929,
+       0,0.707107,0.707107,0.315063,0.287999,0.661827,
+       0,0.707107,0.707107,0.420704,0.287999,0.661827,
+       1,0,0,0.14439,0.297422,0.645507,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.294897,0.636084,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.269154,0.629186,
+       -1,0,0,0.315063,0.262256,0.636084,
+       0,0.965926,-0.258819,0.315063,0.297422,0.645507,
+       0,0.965926,-0.258819,0.420704,0.294897,0.636084,
+       0,0.965926,-0.258819,0.315063,0.294897,0.636084,
+       0,0.965926,-0.258819,0.420704,0.294897,0.636084,
+       0,0.965926,-0.258819,0.315063,0.297422,0.645507,
+       0,0.965926,-0.258819,0.420704,0.297422,0.645507,
+       0,0.707107,-0.707107,0.420704,0.294897,0.636084,
+       0,0.575061,-0.81811,0.315063,0.287999,0.629186,
+       0,0.707107,-0.707107,0.315063,0.294897,0.636084,
+       0,0.575061,-0.81811,0.315063,0.287999,0.629186,
+       0,0.707107,-0.707107,0.420704,0.294897,0.636084,
+       0,0.420974,-0.907073,0.420704,0.287999,0.629186,
+       0,0.420974,-0.907073,0.420704,0.287999,0.629186,
+       0,0.258819,-0.965926,0.315063,0.278577,0.626661,
+       0,0.575061,-0.81811,0.315063,0.287999,0.629186,
+       0,0.258819,-0.965926,0.315063,0.278577,0.626661,
+       0,0.420974,-0.907073,0.420704,0.287999,0.629186,
+       0,0.258819,-0.965926,0.420704,0.278577,0.626661,
+       0,0.907073,0.420974,0.315063,0.294897,0.654929,
+       0,0.965926,0.258819,0.420704,0.297422,0.645507,
+       0,0.965926,0.258819,0.315063,0.297422,0.645507,
+       0,0.965926,0.258819,0.420704,0.297422,0.645507,
+       0,0.907073,0.420974,0.315063,0.294897,0.654929,
+       0,0.81811,0.575061,0.420704,0.294897,0.654929,
+       0,-0.707107,-0.707107,0.420704,0.262256,0.636084,
+       0,-0.5,-0.866025,0.315063,0.269154,0.629186,
+       0,-0.5,-0.866025,0.420704,0.269154,0.629186,
+       0,-0.5,-0.866025,0.315063,0.269154,0.629186,
+       0,-0.707107,-0.707107,0.420704,0.262256,0.636084,
+       0,-0.707107,-0.707107,0.315063,0.262256,0.636084,
+       0,-0.420974,0.907073,0.420704,0.269154,0.661827,
+       0,-0.258819,0.965926,0.315063,0.278577,0.664352,
+       0,-0.575061,0.81811,0.315063,0.269154,0.661827,
+       0,-0.258819,0.965926,0.315063,0.278577,0.664352,
+       0,-0.420974,0.907073,0.420704,0.269154,0.661827,
+       0,-0.258819,0.965926,0.420704,0.278577,0.664352,
+       0,-0.996035,-0.088962,0.420704,0.259731,0.645507,
+       0,-0.965926,-0.258819,0.315063,0.262256,0.636084,
+       0,-0.965926,-0.258819,0.420704,0.262256,0.636084,
+       0,-0.965926,-0.258819,0.315063,0.262256,0.636084,
+       0,-0.996035,-0.088962,0.420704,0.259731,0.645507,
+       0,-0.996035,0.088962,0.315063,0.259731,0.645507,
+       1,0,0,0.420704,0.294897,0.636084,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.287999,0.629186,
+       -1,0,0,0.038749,0.287999,0.629186,
+       -1,0,0,0.038749,0.278577,0.626661,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.269154,0.661827,
+       -1,0,0,0.038749,0.278577,0.664352,
+       -1,0,0,0.038749,0.287999,0.661827,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.278577,0.664352,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.262256,0.636084,
+       -1,0,0,0.038749,0.259731,0.645507,
+       -1,0,0,0.038749,0.294897,0.636084,
+       -1,0,0,0.038749,0.287999,0.629186,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.294897,0.654929,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.287999,0.661827,
+       -1,0,0,0.038749,0.278577,0.626661,
+       -1,0,0,0.038749,0.269154,0.629186,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.269154,0.629186,
+       -1,0,0,0.038749,0.262256,0.636084,
+       -1,0,0,0.038749,0.297422,0.645507,
+       -1,0,0,0.038749,0.294897,0.636084,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.259731,0.645507,
+       -1,0,0,0.038749,0.262256,0.654929,
+       -1,0,0,0.038749,0.297422,0.645507,
+       -1,0,0,0.038749,0.278577,0.645507,
+       -1,0,0,0.038749,0.294897,0.654929,
+       1,0,0,0.14439,0.297422,0.645507,
+       1,0,0,0.14439,0.294897,0.654929,
+       1,0,0,0.14439,0.278577,0.645507,
+       0,0,-1,0.14439,0.297422,0.645507,
+       0,0.108104,-0.99414,0.038749,0.278577,0.645507,
+       0,0,-1,0.038749,0.297422,0.645507,
+       0,0.108104,-0.99414,0.038749,0.278577,0.645507,
+       0,0,-1,0.14439,0.297422,0.645507,
+       0,0,-1,0.038749,0.259731,0.645507,
+       0,0,-1,0.038749,0.259731,0.645507,
+       0,0,-1,0.14439,0.297422,0.645507,
+       0,0,-1,0.14439,0.259731,0.645507,
+       0.534871,-0.774969,-0.336653,0.14439,0.259731,0.645507,
+       0.44821,0.649406,-0.614312,0.14439,0.297422,0.645507,
+       0.920575,0.276172,-0.276172,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.278577,0.664352,
+       1,0,0,0.14439,0.269154,0.661827,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.269154,0.629186,
+       1,0,0,0.14439,0.278577,0.626661,
+       0,0.99174,-0.128264,0.038749,0.278577,0.645507,
+       0,1,0,0.14439,0.278577,0.626661,
+       0,1,0,0.038749,0.278577,0.626661,
+       0,1,0,0.14439,0.278577,0.626661,
+       0,0.99174,-0.128264,0.038749,0.278577,0.645507,
+       0,0.978392,-0.206759,0.14439,0.278577,0.645507,
+       0,0.978392,-0.206759,0.14439,0.278577,0.645507,
+       0,0.99174,-0.128264,0.038749,0.278577,0.645507,
+       0,1,0,0.038749,0.278577,0.664352,
+       0,0.978392,-0.206759,0.14439,0.278577,0.645507,
+       0,1,0,0.038749,0.278577,0.664352,
+       0,1,0,0.14439,0.278577,0.664352,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.269154,0.661827,
+       1,0,0,0.14439,0.262256,0.654929,
+       1,0,0,0.14439,0.287999,0.629186,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.278577,0.626661,
+       0,-0.866025,-0.5,0.14439,0.278577,0.645507,
+       0,-0.866025,-0.5,0.038749,0.287999,0.629186,
+       0,-0.866025,-0.5,0.14439,0.287999,0.629186,
+       0,-0.866025,-0.5,0.038749,0.287999,0.629186,
+       0,-0.866025,-0.5,0.14439,0.278577,0.645507,
+       0,-0.743933,-0.668254,0.038749,0.278577,0.645507,
+       0,-0.79474,-0.60695,0.038749,0.278577,0.645507,
+       0,-0.866025,-0.5,0.14439,0.278577,0.645507,
+       0,-0.866025,-0.5,0.14439,0.269154,0.661827,
+       0,-0.79474,-0.60695,0.038749,0.278577,0.645507,
+       0,-0.866025,-0.5,0.14439,0.269154,0.661827,
+       0,-0.866025,-0.5,0.038749,0.269154,0.661827,
+       1,0,0,0.14439,0.294897,0.636084,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.287999,0.629186,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.262256,0.654929,
+       1,0,0,0.14439,0.259731,0.645507,
+       0,-0.5,-0.866025,0.14439,0.294897,0.636084,
+       0,-0.403449,-0.915002,0.038749,0.278577,0.645507,
+       0,-0.5,-0.866025,0.038749,0.294897,0.636084,
+       0,-0.743933,-0.668254,0.038749,0.278577,0.645507,
+       0,-0.5,-0.866025,0.14439,0.294897,0.636084,
+       0,-0.5,-0.866025,0.038749,0.262256,0.654929,
+       0,-0.5,-0.866025,0.038749,0.262256,0.654929,
+       0,-0.5,-0.866025,0.14439,0.294897,0.636084,
+       0,-0.5,-0.866025,0.14439,0.262256,0.654929,
+       -1,0,0,0.14439,0.262256,0.654929,
+       -1,0,0,0.14439,0.294897,0.636084,
+       -1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.259731,0.645507,
+       1,0,0,0.14439,0.262256,0.636084,
+       1,0,0,0.14439,0.294897,0.654929,
+       1,0,0,0.14439,0.287999,0.661827,
+       1,0,0,0.14439,0.278577,0.645507,
+       0,0.5,-0.866025,0.14439,0.294897,0.654929,
+       0,0.5,-0.866025,0.038749,0.278577,0.645507,
+       0,0.5,-0.866025,0.038749,0.294897,0.654929,
+       0,0.5,-0.866025,0.038749,0.278577,0.645507,
+       0,0.5,-0.866025,0.14439,0.294897,0.654929,
+       0,0.5,-0.866025,0.038749,0.262256,0.636084,
+       0,0.5,-0.866025,0.038749,0.262256,0.636084,
+       0,0.5,-0.866025,0.14439,0.294897,0.654929,
+       0,0.5,-0.866025,0.14439,0.262256,0.636084,
+       -1,0,0,0.14439,0.262256,0.636084,
+       -1,0,0,0.14439,0.294897,0.654929,
+       -1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.278577,0.645507,
+       1,0,0,0.14439,0.262256,0.636084,
+       1,0,0,0.14439,0.269154,0.629186,
+       1,0,0,0.14439,0.287999,0.661827,
+       1,0,0,0.14439,0.278577,0.664352,
+       1,0,0,0.14439,0.278577,0.645507,
+       0,0.743933,-0.668254,0.038749,0.278577,0.645507,
+       0,0.866025,-0.5,0.14439,0.269154,0.629186,
+       0,0.866025,-0.5,0.038749,0.269154,0.629186,
+       0,0.866025,-0.5,0.14439,0.269154,0.629186,
+       0,0.743933,-0.668254,0.038749,0.278577,0.645507,
+       0,0.866025,-0.5,0.14439,0.278577,0.645507,
+       0,0.965926,-0.258819,0.14439,0.278577,0.645507,
+       0,0.965926,-0.258819,0.038749,0.278577,0.645507,
+       0,0.866025,-0.5,0.038749,0.287999,0.661827,
+       0,0.965926,-0.258819,0.14439,0.278577,0.645507,
+       0,0.866025,-0.5,0.038749,0.287999,0.661827,
+       0,0.866025,-0.5,0.14439,0.287999,0.661827,
+       -1,0,0,0.315063,0.287999,0.629186,
+       -1,0,0,0.315063,0.278577,0.626661,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.269154,0.661827,
+       -1,0,0,0.315063,0.278577,0.664352,
+       -1,0,0,0.315063,0.294897,0.636084,
+       -1,0,0,0.315063,0.287999,0.629186,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.297422,0.645507,
+       -1,0,0,0.315063,0.294897,0.636084,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.262256,0.654929,
+       -1,0,0,0.315063,0.269154,0.661827,
+       -1,0,0,0.315063,0.297422,0.645507,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.294897,0.654929,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.259731,0.645507,
+       -1,0,0,0.315063,0.262256,0.654929,
+       -1,0,0,0.315063,0.287999,0.661827,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.278577,0.664352,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.262256,0.636084,
+       -1,0,0,0.315063,0.259731,0.645507,
+       -1,0,0,0.315063,0.278577,0.626661,
+       -1,0,0,0.315063,0.269154,0.629186,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.294897,0.654929,
+       -1,0,0,0.315063,0.278577,0.645507,
+       -1,0,0,0.315063,0.287999,0.661827,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.269154,0.629186,
+       1,0,0,0.420704,0.278577,0.626661,
+       0,0.99174,-0.128264,0.315063,0.278577,0.645507,
+       0,1,0,0.420704,0.278577,0.626661,
+       0,1,0,0.315063,0.278577,0.626661,
+       0,1,0,0.420704,0.278577,0.626661,
+       0,0.99174,-0.128264,0.315063,0.278577,0.645507,
+       0,0.978392,-0.206759,0.420704,0.278577,0.645507,
+       0,0.978392,-0.206759,0.420704,0.278577,0.645507,
+       0,0.99174,-0.128264,0.315063,0.278577,0.645507,
+       0,1,0,0.315063,0.278577,0.664352,
+       0,0.978392,-0.206759,0.420704,0.278577,0.645507,
+       0,1,0,0.315063,0.278577,0.664352,
+       0,1,0,0.420704,0.278577,0.664352,
+       1,0,0,0.420704,0.287999,0.661827,
+       1,0,0,0.420704,0.278577,0.664352,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.262256,0.636084,
+       1,0,0,0.420704,0.269154,0.629186,
+       0,0.743933,-0.668254,0.315063,0.278577,0.645507,
+       0,0.866025,-0.5,0.420704,0.269154,0.629186,
+       0,0.866025,-0.5,0.315063,0.269154,0.629186,
+       0,0.866025,-0.5,0.420704,0.269154,0.629186,
+       0,0.743933,-0.668254,0.315063,0.278577,0.645507,
+       0,0.866025,-0.5,0.420704,0.278577,0.645507,
+       0,0.965926,-0.258819,0.420704,0.278577,0.645507,
+       0,0.965926,-0.258819,0.315063,0.278577,0.645507,
+       0,0.866025,-0.5,0.315063,0.287999,0.661827,
+       0,0.965926,-0.258819,0.420704,0.278577,0.645507,
+       0,0.866025,-0.5,0.315063,0.287999,0.661827,
+       0,0.866025,-0.5,0.420704,0.287999,0.661827,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.259731,0.645507,
+       1,0,0,0.420704,0.262256,0.636084,
+       1,0,0,0.420704,0.294897,0.654929,
+       1,0,0,0.420704,0.287999,0.661827,
+       1,0,0,0.420704,0.278577,0.645507,
+       0,0.5,-0.866025,0.420704,0.294897,0.654929,
+       0,0.5,-0.866025,0.315063,0.278577,0.645507,
+       0,0.5,-0.866025,0.315063,0.294897,0.654929,
+       0,0.5,-0.866025,0.315063,0.278577,0.645507,
+       0,0.5,-0.866025,0.420704,0.294897,0.654929,
+       0,0.5,-0.866025,0.315063,0.262256,0.636084,
+       0,0.5,-0.866025,0.315063,0.262256,0.636084,
+       0,0.5,-0.866025,0.420704,0.294897,0.654929,
+       0,0.5,-0.866025,0.420704,0.262256,0.636084,
+       -1,0,0,0.420704,0.262256,0.636084,
+       -1,0,0,0.420704,0.294897,0.654929,
+       -1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.278577,0.664352,
+       1,0,0,0.420704,0.269154,0.661827,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.297422,0.645507,
+       1,0,0,0.420704,0.294897,0.654929,
+       1,0,0,0.420704,0.278577,0.645507,
+       0,0,-1,0.420704,0.297422,0.645507,
+       0,0.258819,-0.965926,0.315063,0.278577,0.645507,
+       0,0,-1,0.315063,0.297422,0.645507,
+       0,0.258819,-0.965926,0.315063,0.278577,0.645507,
+       0,0,-1,0.420704,0.297422,0.645507,
+       0,0,-1,0.315063,0.259731,0.645507,
+       0,0,-1,0.315063,0.259731,0.645507,
+       0,0,-1,0.420704,0.297422,0.645507,
+       0,0,-1,0.420704,0.259731,0.645507,
+       0.534871,-0.774969,-0.336653,0.420704,0.259731,0.645507,
+       0.44821,0.649406,-0.614312,0.420704,0.297422,0.645507,
+       0.942809,0.235702,-0.235702,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.287999,0.629186,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.278577,0.626661,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.262256,0.654929,
+       1,0,0,0.420704,0.259731,0.645507,
+       0,-0.866025,-0.5,0.420704,0.278577,0.645507,
+       0,-0.866025,-0.5,0.315063,0.287999,0.629186,
+       0,-0.866025,-0.5,0.420704,0.287999,0.629186,
+       0,-0.866025,-0.5,0.315063,0.287999,0.629186,
+       0,-0.866025,-0.5,0.420704,0.278577,0.645507,
+       0,-0.866025,-0.5,0.315063,0.278577,0.645507,
+       0,-0.866025,-0.5,0.315063,0.278577,0.645507,
+       0,-0.866025,-0.5,0.420704,0.278577,0.645507,
+       0,-0.866025,-0.5,0.420704,0.269154,0.661827,
+       0,-0.866025,-0.5,0.315063,0.278577,0.645507,
+       0,-0.866025,-0.5,0.420704,0.269154,0.661827,
+       0,-0.866025,-0.5,0.315063,0.269154,0.661827,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.269154,0.661827,
+       1,0,0,0.420704,0.262256,0.654929,
+       1,0,0,0.420704,0.297422,0.645507,
+       1,0,0,0.420704,0.278577,0.645507,
+       1,0,0,0.420704,0.294897,0.636084,
+       0,0.5,0.866025,0.420704,0.262256,0.654929,
+       0,0.5,0.866025,0.315063,0.278577,0.645507,
+       0,0.5,0.866025,0.315063,0.262256,0.654929,
+       0,0.5,0.866025,0.315063,0.278577,0.645507,
+       0,0.5,0.866025,0.420704,0.262256,0.654929,
+       0,0.5,0.866025,0.315063,0.294897,0.636084,
+       0,0.5,0.866025,0.315063,0.294897,0.636084,
+       0,0.5,0.866025,0.420704,0.262256,0.654929,
+       0,0.5,0.866025,0.420704,0.294897,0.636084,
+       1,0,0,0.420704,0.294897,0.636084,
+       1,0,0,0.420704,0.262256,0.654929,
+       1,0,0,0.420704,0.278577,0.645507
+};
+static const struct gllist dumpster_model_hinges_half_frame = {
+ GL_N3F_V3F, GL_TRIANGLES, 432, dumpster_model_hinges_half_data, 0
+};
+const struct gllist *dumpster_model_hinges_half = &dumpster_model_hinges_half_frame;
+
+static const float dumpster_model_inside_half_data[] = {
+       0,-0.261118,0.965307,0.431095,-0.223786,0.088711,
+       0,-0.261118,0.965307,0,0.234657,0.212721,
+       0,-0.261118,0.965307,0,-0.223786,0.088711,
+       0,-0.261118,0.965307,0,0.234657,0.212721,
+       0,-0.261118,0.965307,0.431095,-0.223786,0.088711,
+       0,-0.261118,0.965307,0.431095,0.234657,0.212721,
+       0,-1,0,0.431095,0.234657,0.627761,
+       0,-1,0,0,0.234657,0.212721,
+       0,-1,0,0.431095,0.234657,0.212721,
+       0,-1,0,0,0.234657,0.212721,
+       0,-1,0,0.431095,0.234657,0.627761,
+       0,-1,0,0,0.234657,0.627761,
+       0,1,0,0,-0.223786,0.50375,
+       0,1,0,0.431095,-0.223786,0.088711,
+       0,1,0,0,-0.223786,0.088711,
+       0,1,0,0.431095,-0.223786,0.088711,
+       0,1,0,0,-0.223786,0.50375,
+       0,1,0,0.431095,-0.223786,0.50375,
+       -1,0,0,0.431095,0.234657,0.627761,
+       -1,0,0,0.431095,-0.223786,0.088711,
+       -1,0,0,0.431095,-0.223786,0.50375,
+       -1,0,0,0.431095,-0.223786,0.088711,
+       -1,0,0,0.431095,0.234657,0.627761,
+       -1,0,0,0.431095,0.234657,0.212721
+};
+static const struct gllist dumpster_model_inside_half_frame = {
+ GL_N3F_V3F, GL_TRIANGLES, 24, dumpster_model_inside_half_data, 0
+};
+const struct gllist *dumpster_model_inside_half = &dumpster_model_inside_half_frame;
+
+static const float dumpster_model_lid_data[] = {
+       0.000021,-0.242057,0.970262,0.411943,-0.251833,0.533256,
+       0.000016,-0.242055,0.970263,0.460077,-0.162101,0.555641,
+       0.000014,-0.242057,0.970262,0.411943,-0.162101,0.555642,
+       0.000016,-0.242055,0.970263,0.460077,-0.162101,0.555641,
+       0.000021,-0.242057,0.970262,0.411943,-0.251833,0.533256,
+       0.000007,-0.392342,0.919819,0.411943,-0.251836,0.533255,
+       0.000016,-0.242055,0.970263,0.460077,-0.162101,0.555641,
+       0.000007,-0.392342,0.919819,0.411943,-0.251836,0.533255,
+       0,-0.536306,0.844024,0.460077,-0.251836,0.533255,
+       1,0,0,0.049771,0.261795,0.649635,
+       1,0,0,0.049771,-0.251836,0.533255,
+       1,0,0,0.049771,-0.270321,0.516886,
+       1,0,0,0.049771,-0.251836,0.533255,
+       1,0,0,0.049771,0.261795,0.649635,
+       1,0,0,0.049771,-0.162101,0.555642,
+       1,0,0,0.049771,-0.162101,0.555642,
+       1,0,0,0.049771,0.261795,0.649635,
+       1,0,0,0.049771,0.148749,0.633191,
+       1,0,0,0.049771,0.148749,0.633191,
+       1,0,0,0.049771,0.261795,0.649635,
+       1,0,0,0.049771,0.237787,0.655403,
+       0,-0.242054,0.970263,0.14426,0.237771,0.655399,
+       0.000005,-0.165396,0.986227,0.182918,0.237787,0.655403,
+       0,0.077021,0.997029,0.14426,0.237787,0.655403,
+       0.000005,-0.165396,0.986227,0.182918,0.237787,0.655403,
+       0,-0.242054,0.970263,0.14426,0.237771,0.655399,
+       0,-0.242054,0.970263,0.14426,0.237744,0.655392,
+       0.000005,-0.165396,0.986227,0.182918,0.237787,0.655403,
+       0,-0.242054,0.970263,0.14426,0.237744,0.655392,
+       0.000016,-0.242056,0.970262,0.14426,-0.162101,0.555642,
+       0.000005,-0.165396,0.986227,0.182918,0.237787,0.655403,
+       0.000016,-0.242056,0.970262,0.14426,-0.162101,0.555642,
+       0.000006,-0.332305,0.943172,0.14426,-0.251836,0.533255,
+       0.000005,-0.165396,0.986227,0.182918,0.237787,0.655403,
+       0.000006,-0.332305,0.943172,0.14426,-0.251836,0.533255,
+       0,-0.242055,0.970263,0.182918,0.237775,0.6554,
+       0,-0.242055,0.970263,0.182918,0.237775,0.6554,
+       0.000006,-0.332305,0.943172,0.14426,-0.251836,0.533255,
+       0,-0.242055,0.970263,0.182918,-0.251831,0.533256,
+       0,-0.242055,0.970263,0.182918,-0.251831,0.533256,
+       0.000006,-0.332305,0.943172,0.14426,-0.251836,0.533255,
+       0,-0.536308,0.844022,0.182918,-0.251836,0.533255,
+       0.995705,-0.02241,0.089831,0.182918,0.237775,0.6554,
+       0.995705,-0.02241,0.089831,0.182918,-0.251831,0.533256,
+       0.995705,-0.02241,0.089831,0.182918,0.237759,0.655396,
+       0,0.077022,0.997029,0,0.237787,0.655403,
+       0,0.156711,0.987645,0.049771,0.261795,0.649635,
+       0,0.117625,0.993058,0,0.261795,0.649635,
+       0,0.156711,0.987645,0.049771,0.261795,0.649635,
+       0,0.077022,0.997029,0,0.237787,0.655403,
+       0,-0.085677,0.996323,0.049771,0.237787,0.655403,
+       0,-0.536304,0.844025,0,-0.270321,0.516886,
+       0,-0.536309,0.844022,0.049771,-0.251836,0.533255,
+       -0.000007,-0.392342,0.919819,0,-0.251836,0.533255,
+       0,-0.536309,0.844022,0.049771,-0.251836,0.533255,
+       0,-0.536304,0.844025,0,-0.270321,0.516886,
+       0,-0.66295,0.748664,0.049771,-0.270321,0.516886,
+       -1,0,0,0.411943,-0.147506,0.573235,
+       -1,0,0,0.411943,0.148749,0.633191,
+       -1,0,0,0.411943,-0.162101,0.555642,
+       -1,0,0,0.411943,0.148749,0.633191,
+       -1,0,0,0.411943,-0.147506,0.573235,
+       -1,0,0,0.411943,0.121591,0.640367,
+       0,-0.536304,0.844025,0.411943,-0.270321,0.516886,
+       0,-0.536306,0.844024,0.460077,-0.251836,0.533255,
+       0.000007,-0.392342,0.919819,0.411943,-0.251836,0.533255,
+       0,-0.536306,0.844024,0.460077,-0.251836,0.533255,
+       0,-0.536304,0.844025,0.411943,-0.270321,0.516886,
+       0,-0.465863,0.884857,0.460077,-0.270321,0.516886,
+       1,0,0,0.335153,0.261795,0.649635,
+       1,0,0,0.335153,-0.251836,0.533255,
+       1,0,0,0.335153,-0.270321,0.516886,
+       1,0,0,0.335153,-0.251836,0.533255,
+       1,0,0,0.335153,0.261795,0.649635,
+       0.992795,-0.029005,0.116265,0.335153,0.237778,0.655401,
+       0.992795,-0.029005,0.116265,0.335153,0.237778,0.655401,
+       1,0,0,0.335153,0.261795,0.649635,
+       0.992795,-0.029005,0.116265,0.335153,0.237782,0.655402,
+       0.992795,-0.029005,0.116265,0.335153,0.237782,0.655402,
+       1,0,0,0.335153,0.261795,0.649635,
+       0.983653,-0.043588,0.174722,0.335153,0.237787,0.655403,
+       0,0.07702,0.99703,0.296566,0.237787,0.655403,
+       0,0.233616,0.972329,0.335153,0.261795,0.649635,
+       0,0.233616,0.972329,0.296566,0.261795,0.649635,
+       0,0.233616,0.972329,0.335153,0.261795,0.649635,
+       0,0.07702,0.99703,0.296566,0.237787,0.655403,
+       0,-0.08568,0.996323,0.335153,0.237787,0.655403,
+       -1,0,0,0.296566,-0.251836,0.533255,
+       -1,0,0,0.296566,0.261795,0.649635,
+       -1,0,0,0.296566,-0.270321,0.516886,
+       -1,0,0,0.296566,0.261795,0.649635,
+       -1,0,0,0.296566,-0.251836,0.533255,
+       -1,0,0,0.296566,0.237787,0.655403,
+       0,-0.66295,0.748664,0.296566,-0.270321,0.516886,
+       0,-0.536308,0.844022,0.335153,-0.251836,0.533255,
+       0,-0.354888,0.934909,0.296566,-0.251836,0.533255,
+       0,-0.536308,0.844022,0.335153,-0.251836,0.533255,
+       0,-0.66295,0.748664,0.296566,-0.270321,0.516886,
+       0,-0.66295,0.748664,0.335153,-0.270321,0.516886,
+       0,0.077021,0.997029,0.14426,0.237787,0.655403,
+       0,0.233616,0.972329,0.182918,0.261795,0.649635,
+       0,0.233616,0.972329,0.14426,0.261795,0.649635,
+       0,0.233616,0.972329,0.182918,0.261795,0.649635,
+       0,0.077021,0.997029,0.14426,0.237787,0.655403,
+       0.000005,-0.165396,0.986227,0.182918,0.237787,0.655403,
+       0,-0.66295,0.748664,0.14426,-0.270321,0.516886,
+       0,-0.536308,0.844022,0.182918,-0.251836,0.533255,
+       0.000006,-0.332305,0.943172,0.14426,-0.251836,0.533255,
+       0,-0.536308,0.844022,0.182918,-0.251836,0.533255,
+       0,-0.66295,0.748664,0.14426,-0.270321,0.516886,
+       0,-0.66295,0.748664,0.182918,-0.270321,0.516886,
+       0,-0.120502,0.992713,0,0.148749,0.633191,
+       0,-0.085677,0.996323,0.049771,0.237787,0.655403,
+       0,0.077022,0.997029,0,0.237787,0.655403,
+       0,-0.085677,0.996323,0.049771,0.237787,0.655403,
+       0,-0.120502,0.992713,0,0.148749,0.633191,
+       -0.000004,-0.044383,0.999015,0.049771,0.148749,0.633191,
+       0,-0.242054,0.970263,0,-0.147506,0.573235,
+       0,-0.078399,0.996922,0.049771,0.121591,0.640367,
+       0,0.092202,0.99574,0,0.121591,0.640367,
+       0,-0.078399,0.996922,0.049771,0.121591,0.640367,
+       0,-0.242054,0.970263,0,-0.147506,0.573235,
+       0,-0.242054,0.970263,0.049771,-0.147506,0.573235,
+       0,-0.242051,0.970264,0.411943,0.237778,0.655401,
+       0,-0.176561,0.98429,0.460077,0.237787,0.655403,
+       0,0.077022,0.997029,0.411943,0.237787,0.655403,
+       0,-0.176561,0.98429,0.460077,0.237787,0.655403,
+       0,-0.242051,0.970264,0.411943,0.237778,0.655401,
+       0,-0.242051,0.970264,0.411943,0.237768,0.655398,
+       0,-0.176561,0.98429,0.460077,0.237787,0.655403,
+       0,-0.242051,0.970264,0.411943,0.237768,0.655398,
+       0,-0.242051,0.970264,0.411943,0.237759,0.655396,
+       0,-0.176561,0.98429,0.460077,0.237787,0.655403,
+       0,-0.242051,0.970264,0.411943,0.237759,0.655396,
+       0,-0.242051,0.970264,0.411943,0.237744,0.655392,
+       0,-0.176561,0.98429,0.460077,0.237787,0.655403,
+       0,-0.242051,0.970264,0.411943,0.237744,0.655392,
+       0,-0.120501,0.992713,0.411943,0.148749,0.633191,
+       0,-0.176561,0.98429,0.460077,0.237787,0.655403,
+       0,-0.120501,0.992713,0.411943,0.148749,0.633191,
+       0.000004,-0.044383,0.999015,0.460077,0.148749,0.633191,
+       -0.000007,-0.769645,0.638473,0.049771,-0.162101,0.555642,
+       -0.000007,-0.769645,0.638473,0,-0.147506,0.573235,
+       -0.000013,-0.769654,0.638461,0,-0.162101,0.555641,
+       -0.000007,-0.769645,0.638473,0,-0.147506,0.573235,
+       -0.000007,-0.769645,0.638473,0.049771,-0.162101,0.555642,
+       0,-0.769635,0.638484,0.049771,-0.147506,0.573235,
+       0,0.092202,0.99574,0,0.121591,0.640367,
+       -0.000004,-0.044383,0.999015,0.049771,0.148749,0.633191,
+       0,-0.120502,0.992713,0,0.148749,0.633191,
+       -0.000004,-0.044383,0.999015,0.049771,0.148749,0.633191,
+       0,0.092202,0.99574,0,0.121591,0.640367,
+       0,-0.078399,0.996922,0.049771,0.121591,0.640367,
+       -0.000007,-0.392342,0.919819,0,-0.251836,0.533255,
+       -0.000014,-0.242053,0.970263,0.049771,-0.162101,0.555642,
+       -0.000014,-0.242053,0.970263,0,-0.162101,0.555641,
+       -0.000014,-0.242053,0.970263,0.049771,-0.162101,0.555642,
+       -0.000007,-0.392342,0.919819,0,-0.251836,0.533255,
+       0,-0.536309,0.844022,0.049771,-0.251836,0.533255,
+       0,0.092202,0.99574,0.411943,0.121591,0.640367,
+       0.000004,-0.044383,0.999015,0.460077,0.148749,0.633191,
+       0,-0.120501,0.992713,0.411943,0.148749,0.633191,
+       0.000004,-0.044383,0.999015,0.460077,0.148749,0.633191,
+       0,0.092202,0.99574,0.411943,0.121591,0.640367,
+       0,-0.078399,0.996922,0.460077,0.121591,0.640367,
+       0,0.077022,0.997029,0.411943,0.237787,0.655403,
+       0,0.233616,0.972329,0.460077,0.261795,0.649635,
+       0,0.117625,0.993058,0.411943,0.261795,0.649635,
+       0,0.233616,0.972329,0.460077,0.261795,0.649635,
+       0,0.077022,0.997029,0.411943,0.237787,0.655403,
+       0,-0.176561,0.98429,0.460077,0.237787,0.655403,
+       0,-0.242054,0.970263,0.411943,-0.147506,0.573235,
+       0,-0.078399,0.996922,0.460077,0.121591,0.640367,
+       0,0.092202,0.99574,0.411943,0.121591,0.640367,
+       0,-0.078399,0.996922,0.460077,0.121591,0.640367,
+       0,-0.242054,0.970263,0.411943,-0.147506,0.573235,
+       0,-0.242054,0.970263,0.460077,-0.147506,0.573235,
+       0.000007,-0.769645,0.638473,0.460077,-0.147506,0.573235,
+       0.000007,-0.769645,0.638473,0.411943,-0.162101,0.555642,
+       0.000014,-0.769654,0.638461,0.460077,-0.162101,0.555641,
+       0.000007,-0.769645,0.638473,0.411943,-0.162101,0.555642,
+       0.000007,-0.769645,0.638473,0.460077,-0.147506,0.573235,
+       0,-0.769635,0.638484,0.411943,-0.147506,0.573235,
+       1,0,0,0.182918,0.261795,0.649635,
+       1,0,0,0.182918,-0.251836,0.533255,
+       1,0,0,0.182918,-0.270321,0.516886,
+       1,0,0,0.182918,-0.251836,0.533255,
+       1,0,0,0.182918,0.261795,0.649635,
+       0.995705,-0.02241,0.089831,0.182918,-0.251831,0.533256,
+       0.995705,-0.02241,0.089831,0.182918,-0.251831,0.533256,
+       1,0,0,0.182918,0.261795,0.649635,
+       0.995705,-0.02241,0.089831,0.182918,0.237759,0.655396,
+       0.995705,-0.02241,0.089831,0.182918,0.237759,0.655396,
+       1,0,0,0.182918,0.261795,0.649635,
+       0.995705,-0.02241,0.089831,0.182918,0.237775,0.6554,
+       0.995705,-0.02241,0.089831,0.182918,0.237775,0.6554,
+       1,0,0,0.182918,0.261795,0.649635,
+       1,0,0,0.182918,0.237787,0.655403,
+       -1,0,0,0.14426,-0.251836,0.533255,
+       -1,0,0,0.14426,0.261795,0.649635,
+       -1,0,0,0.14426,-0.270321,0.516886,
+       -1,0,0,0.14426,0.261795,0.649635,
+       -1,0,0,0.14426,-0.251836,0.533255,
+       -1,0,0,0.14426,-0.162101,0.555642,
+       -1,0,0,0.14426,0.261795,0.649635,
+       -1,0,0,0.14426,-0.162101,0.555642,
+       -1,0,0,0.14426,0.237744,0.655392,
+       -1,0,0,0.14426,0.261795,0.649635,
+       -1,0,0,0.14426,0.237744,0.655392,
+       -1,0,0,0.14426,0.237771,0.655399,
+       -1,0,0,0.14426,0.261795,0.649635,
+       -1,0,0,0.14426,0.237771,0.655399,
+       -1,0,0,0.14426,0.237787,0.655403,
+       0,-0.970266,-0.24204,0.460077,-0.270321,0.516886,
+       0,-0.970266,-0.24204,0,-0.267044,0.50375,
+       0,-0.970266,-0.24204,0.460077,-0.267044,0.50375,
+       0,-0.970266,-0.24204,0,-0.267044,0.50375,
+       0,-0.970266,-0.24204,0.460077,-0.270321,0.516886,
+       0,-0.970266,-0.24204,0,-0.270321,0.516886,
+       0,-0.412008,0.91118,0,-0.270321,0.516886,
+       0,-0.465863,0.884857,0.460077,-0.270321,0.516886,
+       0,-0.536304,0.844025,0.411943,-0.270321,0.516886,
+       0,0,-1,0,-0.270321,0.516886,
+       0,0,-1,0.411943,-0.270321,0.516886,
+       0,0,-1,0.335153,-0.270321,0.516886,
+       0,-0.12193,0.992539,0,-0.270321,0.516886,
+       0,0,1,0.335153,-0.270321,0.516886,
+       0,0,1,0.296566,-0.270321,0.516886,
+       0,0,-1,0,-0.270321,0.516886,
+       0,0,-1,0.296566,-0.270321,0.516886,
+       0,0,-1,0.182918,-0.270321,0.516886,
+       0,0,-1,0,-0.270321,0.516886,
+       0,0,-1,0.182918,-0.270321,0.516886,
+       0,0,-1,0.14426,-0.270321,0.516886,
+       0,0,-1,0,-0.270321,0.516886,
+       0,0,-1,0.14426,-0.270321,0.516886,
+       0,0,-1,0.049771,-0.270321,0.516886,
+       0,0.970264,0.242047,0,0.267251,0.627761,
+       0,0.970264,0.242047,0.460077,0.261795,0.649635,
+       0,0.970264,0.242047,0.460077,0.267251,0.627761,
+       0,0.970264,0.242047,0.460077,0.261795,0.649635,
+       0,0.970264,0.242047,0,0.267251,0.627761,
+       0,0.970264,0.242047,0,0.261795,0.649635,
+       0,0.24204,-0.970266,0.460077,0.261795,0.649635,
+       0,0.24204,-0.970266,0,0.261795,0.649635,
+       0,0.04864,-0.998816,0.411943,0.261795,0.649635,
+       0,0.117625,0.993058,0.411943,0.261795,0.649635,
+       0,0.117625,0.993058,0,0.261795,0.649635,
+       0,0.156711,0.987645,0.049771,0.261795,0.649635,
+       0,0.04864,-0.998816,0.411943,0.261795,0.649635,
+       0,0,-1,0.049771,0.261795,0.649635,
+       0,0,-1,0.14426,0.261795,0.649635,
+       0,0.04864,-0.998816,0.411943,0.261795,0.649635,
+       0,0,-1,0.14426,0.261795,0.649635,
+       0,0,-1,0.182918,0.261795,0.649635,
+       0,0.04864,-0.998816,0.411943,0.261795,0.649635,
+       0,0,-1,0.182918,0.261795,0.649635,
+       0,0,-1,0.296566,0.261795,0.649635,
+       0,0.04864,-0.998816,0.411943,0.261795,0.649635,
+       0,0,-1,0.296566,0.261795,0.649635,
+       0,0,-1,0.335153,0.261795,0.649635,
+       -0.000001,0.000004,-1,0.460077,0.267251,0.627761,
+       0,0.087719,-0.996145,0,0.234657,0.627761,
+       0,0,-1,0,0.267251,0.627761,
+       0,0.087719,-0.996145,0,0.234657,0.627761,
+       -0.000001,0.000004,-1,0.460077,0.267251,0.627761,
+       0,0.157993,-0.98744,0.116054,0.234657,0.627761,
+       0,0.157993,-0.98744,0.116054,0.234657,0.627761,
+       -0.000001,0.000004,-1,0.460077,0.267251,0.627761,
+       0,0.087719,-0.996145,0.14426,0.234657,0.627761,
+       0,0.087719,-0.996145,0.14426,0.234657,0.627761,
+       -0.000001,0.000004,-1,0.460077,0.267251,0.627761,
+       -0.000006,0.08773,-0.996144,0.182847,0.234657,0.627761,
+       -0.000006,0.08773,-0.996144,0.182847,0.234657,0.627761,
+       -0.000001,0.000004,-1,0.460077,0.267251,0.627761,
+       -0.000004,0.158,-0.987439,0.182918,0.234657,0.627761,
+       -0.000004,0.158,-0.987439,0.182918,0.234657,0.627761,
+       -0.000001,0.000004,-1,0.460077,0.267251,0.627761,
+       0,0.131708,-0.991289,0.296566,0.234657,0.627761,
+       0,0.131708,-0.991289,0.296566,0.234657,0.627761,
+       -0.000001,0.000004,-1,0.460077,0.267251,0.627761,
+       0,0.087719,-0.996145,0.335153,0.234657,0.627761,
+       0,0.087719,-0.996145,0.335153,0.234657,0.627761,
+       -0.000001,0.000004,-1,0.460077,0.267251,0.627761,
+       0.000006,0.197127,-0.980378,0.460077,0.234657,0.627761,
+       -1,0,0,0,-0.251836,0.533255,
+       -1,0,0,0,-0.267044,0.50375,
+       -1,0,0,0,-0.270321,0.516886,
+       -1,0,0,0,-0.267044,0.50375,
+       -1,0,0,0,-0.251836,0.533255,
+       -1,0,0,0,-0.223786,0.50375,
+       0,0.105329,-0.994437,0.14426,-0.223786,0.50375,
+       0,0,-1,0,-0.267044,0.50375,
+       0,0.175438,-0.98449,0,-0.223786,0.50375,
+       0,0,-1,0,-0.267044,0.50375,
+       0,0.105329,-0.994437,0.14426,-0.223786,0.50375,
+       -0.000001,-0.000004,-1,0.460077,-0.267044,0.50375,
+       -0.000001,-0.000004,-1,0.460077,-0.267044,0.50375,
+       0,0.105329,-0.994437,0.14426,-0.223786,0.50375,
+       -0.000006,0.131701,-0.991289,0.182847,-0.223786,0.50375,
+       -0.000001,-0.000004,-1,0.460077,-0.267044,0.50375,
+       -0.000006,0.131701,-0.991289,0.182847,-0.223786,0.50375,
+       -0.000002,0.157988,-0.987441,0.182918,-0.223786,0.50375,
+       -0.000001,-0.000004,-1,0.460077,-0.267044,0.50375,
+       -0.000002,0.157988,-0.987441,0.182918,-0.223786,0.50375,
+       0,0.087719,-0.996145,0.296566,-0.223786,0.50375,
+       -0.000001,-0.000004,-1,0.460077,-0.267044,0.50375,
+       0,0.087719,-0.996145,0.296566,-0.223786,0.50375,
+       0,0.131708,-0.991289,0.335153,-0.223786,0.50375,
+       -0.000001,-0.000004,-1,0.460077,-0.267044,0.50375,
+       0,0.131708,-0.991289,0.335153,-0.223786,0.50375,
+       0.000007,0.175438,-0.98449,0.460077,-0.223786,0.50375,
+       0,-0.354888,0.934909,0.296566,-0.251836,0.533255,
+       0,-0.08568,0.996323,0.335153,0.237787,0.655403,
+       0,0.07702,0.99703,0.296566,0.237787,0.655403,
+       0,-0.08568,0.996323,0.335153,0.237787,0.655403,
+       0,-0.354888,0.934909,0.296566,-0.251836,0.533255,
+       0,-0.242055,0.970263,0.335153,0.237778,0.655401,
+       0,-0.242055,0.970263,0.335153,0.237778,0.655401,
+       0,-0.354888,0.934909,0.296566,-0.251836,0.533255,
+       0,-0.536308,0.844022,0.335153,-0.251836,0.533255,
+       0.983653,-0.043588,0.174722,0.335153,0.237787,0.655403,
+       0.992795,-0.029005,0.116265,0.335153,0.237778,0.655401,
+       0.992795,-0.029005,0.116265,0.335153,0.237782,0.655402,
+       -0.000004,0.158,-0.987439,0.182918,0.234657,0.627761,
+       -0.000006,0.131701,-0.991289,0.182847,-0.223786,0.50375,
+       -0.000006,0.08773,-0.996144,0.182847,0.234657,0.627761,
+       -0.000006,0.131701,-0.991289,0.182847,-0.223786,0.50375,
+       -0.000004,0.158,-0.987439,0.182918,0.234657,0.627761,
+       -0.000002,0.157988,-0.987441,0.182918,-0.223786,0.50375,
+       0,0.157993,-0.98744,0.116054,0.234657,0.627761,
+       0,0.175438,-0.98449,0,-0.223786,0.50375,
+       0,0.087719,-0.996145,0,0.234657,0.627761,
+       0,0.175438,-0.98449,0,-0.223786,0.50375,
+       0,0.157993,-0.98744,0.116054,0.234657,0.627761,
+       0,0.105329,-0.994437,0.14426,-0.223786,0.50375,
+       0,0.105329,-0.994437,0.14426,-0.223786,0.50375,
+       0,0.157993,-0.98744,0.116054,0.234657,0.627761,
+       0,0.087719,-0.996145,0.14426,0.234657,0.627761,
+       0,0.131708,-0.991289,0.296566,0.234657,0.627761,
+       -0.000002,0.157988,-0.987441,0.182918,-0.223786,0.50375,
+       -0.000004,0.158,-0.987439,0.182918,0.234657,0.627761,
+       -0.000002,0.157988,-0.987441,0.182918,-0.223786,0.50375,
+       0,0.131708,-0.991289,0.296566,0.234657,0.627761,
+       0,0.087719,-0.996145,0.296566,-0.223786,0.50375,
+       0.000006,0.197127,-0.980378,0.460077,0.234657,0.627761,
+       0,0.131708,-0.991289,0.335153,-0.223786,0.50375,
+       0,0.087719,-0.996145,0.335153,0.234657,0.627761,
+       0,0.131708,-0.991289,0.335153,-0.223786,0.50375,
+       0.000006,0.197127,-0.980378,0.460077,0.234657,0.627761,
+       0.000007,0.175438,-0.98449,0.460077,-0.223786,0.50375,
+       1,0,0,0.460077,0.148749,0.633191,
+       1,0,0,0.460077,-0.147506,0.573235,
+       1,0,0,0.460077,-0.162101,0.555641,
+       1,0,0,0.460077,-0.147506,0.573235,
+       1,0,0,0.460077,0.148749,0.633191,
+       1,0,0,0.460077,0.121591,0.640367,
+       1,0,0,0.460077,-0.267044,0.50375,
+       1,0,0,0.460077,-0.251836,0.533255,
+       1,0,0,0.460077,-0.270321,0.516886,
+       1,0,0,0.460077,-0.251836,0.533255,
+       1,0,0,0.460077,-0.267044,0.50375,
+       1,0,0,0.460077,-0.223786,0.50375,
+       1,0,0,0.460077,-0.251836,0.533255,
+       1,0,0,0.460077,-0.223786,0.50375,
+       1,0,0,0.460077,-0.162101,0.555641,
+       1,0,0,0.460077,-0.162101,0.555641,
+       1,0,0,0.460077,-0.223786,0.50375,
+       1,0,0,0.460077,0.234657,0.627761,
+       1,0,0,0.460077,-0.162101,0.555641,
+       1,0,0,0.460077,0.234657,0.627761,
+       1,0,0,0.460077,0.148749,0.633191,
+       1,0,0,0.460077,0.148749,0.633191,
+       1,0,0,0.460077,0.234657,0.627761,
+       1,0,0,0.460077,0.237787,0.655403,
+       1,0,0,0.460077,0.237787,0.655403,
+       1,0,0,0.460077,0.234657,0.627761,
+       1,0,0,0.460077,0.267251,0.627761,
+       1,0,0,0.460077,0.237787,0.655403,
+       1,0,0,0.460077,0.267251,0.627761,
+       1,0,0,0.460077,0.261795,0.649635,
+       0.000022,0.26112,-0.965306,0.460225,-0.223786,0.50375,
+       0.000007,0.175438,-0.98449,0.460077,-0.223786,0.50375,
+       0.000006,0.197127,-0.980378,0.460077,0.234657,0.627761,
+       -1,0,0,0,0.237787,0.655403,
+       -1,0,0,0,0.267251,0.627761,
+       -1,0,0,0,0.234657,0.627761,
+       -1,0,0,0,0.267251,0.627761,
+       -1,0,0,0,0.237787,0.655403,
+       -1,0,0,0,0.261795,0.649635,
+       -1,0,0,0,-0.162101,0.555641,
+       -1,0,0,0,-0.223786,0.50375,
+       -1,0,0,0,-0.251836,0.533255,
+       -1,0,0,0,-0.223786,0.50375,
+       -1,0,0,0,-0.162101,0.555641,
+       -1,0,0,0,0.234657,0.627761,
+       -1,0,0,0,0.234657,0.627761,
+       -1,0,0,0,-0.162101,0.555641,
+       -1,0,0,0,0.148749,0.633191,
+       -1,0,0,0,0.234657,0.627761,
+       -1,0,0,0,0.148749,0.633191,
+       -1,0,0,0,0.237787,0.655403,
+       -1,0,0,0,-0.147506,0.573235,
+       -1,0,0,0,0.148749,0.633191,
+       -1,0,0,0,-0.162101,0.555641,
+       -1,0,0,0,0.148749,0.633191,
+       -1,0,0,0,-0.147506,0.573235,
+       -1,0,0,0,0.121591,0.640367,
+       -1,0,0,0.411943,-0.251836,0.533255,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,-0.270321,0.516886,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,-0.251836,0.533255,
+       -1,0,0,0.411943,-0.251833,0.533256,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,-0.251833,0.533256,
+       -1,0,0,0.411943,-0.162101,0.555642,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,-0.162101,0.555642,
+       -1,0,0,0.411943,0.148749,0.633191,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,0.148749,0.633191,
+       -1,0,0,0.411943,0.237744,0.655392,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,0.237744,0.655392,
+       -1,0,0,0.411943,0.237759,0.655396,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,0.237759,0.655396,
+       -1,0,0,0.411943,0.237768,0.655398,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,0.237768,0.655398,
+       -1,0,0,0.411943,0.237778,0.655401,
+       -1,0,0,0.411943,0.261795,0.649635,
+       -1,0,0,0.411943,0.237778,0.655401,
+       -1,0,0,0.411943,0.237787,0.655403,
+       0.000014,-0.242057,0.970262,0.411943,-0.162101,0.555642,
+       0.000004,-0.044383,0.999015,0.460077,0.148749,0.633191,
+       0,-0.120501,0.992713,0.411943,0.148749,0.633191,
+       0.000004,-0.044383,0.999015,0.460077,0.148749,0.633191,
+       0.000014,-0.242057,0.970262,0.411943,-0.162101,0.555642,
+       0.000016,-0.242055,0.970263,0.460077,-0.162101,0.555641,
+       1,0,0,0.049771,0.148749,0.633191,
+       1,0,0,0.049771,-0.147506,0.573235,
+       1,0,0,0.049771,-0.162101,0.555642,
+       1,0,0,0.049771,-0.147506,0.573235,
+       1,0,0,0.049771,0.148749,0.633191,
+       1,0,0,0.049771,0.121591,0.640367,
+       -0.000014,-0.242053,0.970263,0,-0.162101,0.555641,
+       -0.000004,-0.044383,0.999015,0.049771,0.148749,0.633191,
+       0,-0.120502,0.992713,0,0.148749,0.633191,
+       -0.000004,-0.044383,0.999015,0.049771,0.148749,0.633191,
+       -0.000014,-0.242053,0.970263,0,-0.162101,0.555641,
+       -0.000014,-0.242053,0.970263,0.049771,-0.162101,0.555642
+};
+static const struct gllist dumpster_model_lid_frame = {
+ GL_N3F_V3F, GL_TRIANGLES, 453, dumpster_model_lid_data, 0
+};
+const struct gllist *dumpster_model_lid = &dumpster_model_lid_frame;
+
+static const float dumpster_model_lid_panels_data[] = {
+       0,-0.242054,0.970263,0.049771,-0.270321,0.516886,
+       0,-0.242054,0.970263,0.14426,0.261795,0.649635,
+       0,-0.242054,0.970263,0.049771,0.261795,0.649635,
+       0,-0.242054,0.970263,0.14426,0.261795,0.649635,
+       0,-0.242054,0.970263,0.049771,-0.270321,0.516886,
+       0,-0.242054,0.970263,0.14426,-0.270321,0.516886,
+       0,-0.242054,0.970263,0.182918,-0.270321,0.516886,
+       0,-0.242054,0.970263,0.296566,0.261795,0.649635,
+       0,-0.242054,0.970263,0.182918,0.261795,0.649635,
+       0,-0.242054,0.970263,0.296566,0.261795,0.649635,
+       0,-0.242054,0.970263,0.182918,-0.270321,0.516886,
+       0,-0.242054,0.970263,0.296566,-0.270321,0.516886,
+       0,-0.242054,0.970263,0.335153,-0.270321,0.516886,
+       0,-0.242054,0.970263,0.411943,0.261795,0.649635,
+       0,-0.242054,0.970263,0.335153,0.261795,0.649635,
+       0,-0.242054,0.970263,0.411943,0.261795,0.649635,
+       0,-0.242054,0.970263,0.335153,-0.270321,0.516886,
+       0,-0.242054,0.970263,0.411943,-0.270321,0.516886,
+       0,0.542246,-0.84022,0.335153,-0.205301,0.520118,
+       0,0.66295,-0.748664,0.296566,-0.223786,0.50375,
+       0,0.405777,-0.913972,0.296566,-0.205301,0.520118,
+       0,0.66295,-0.748664,0.296566,-0.223786,0.50375,
+       0,0.542246,-0.84022,0.335153,-0.205301,0.520118,
+       0,0.66295,-0.748664,0.335153,-0.223786,0.50375,
+       0,-0.233616,-0.972329,0.335153,0.234657,0.627761,
+       0,-0.070006,-0.997547,0.296566,0.21065,0.633529,
+       0,-0.233616,-0.972329,0.296566,0.234657,0.627761,
+       0,-0.070006,-0.997547,0.296566,0.21065,0.633529,
+       0,-0.233616,-0.972329,0.335153,0.234657,0.627761,
+       0,0.100284,-0.994959,0.335153,0.21065,0.633529,
+       -1,0,0,0.335153,-0.205301,0.520118,
+       -1,0,0,0.335153,0.234657,0.627761,
+       -1,0,0,0.335153,-0.223786,0.50375,
+       -1,0,0,0.335153,0.234657,0.627761,
+       -1,0,0,0.335153,-0.205301,0.520118,
+       -1,0,0,0.335153,0.21065,0.633529,
+       1,0,0,0.296566,0.234657,0.627761,
+       1,0,0,0.296566,-0.205301,0.520118,
+       1,0,0,0.296566,-0.223786,0.50375,
+       1,0,0,0.296566,-0.205301,0.520118,
+       1,0,0,0.296566,0.234657,0.627761,
+       1,0,0,0.296566,0.21065,0.633529,
+       0,0.100284,-0.994959,0.335153,0.21065,0.633529,
+       0,0.405777,-0.913972,0.296566,-0.205301,0.520118,
+       0,-0.070006,-0.997547,0.296566,0.21065,0.633529,
+       0,0.405777,-0.913972,0.296566,-0.205301,0.520118,
+       0,0.100284,-0.994959,0.335153,0.21065,0.633529,
+       0,0.542246,-0.84022,0.335153,-0.205301,0.520118,
+       0,-0.233616,-0.972329,0.182847,0.234657,0.627761,
+       0,-0.070006,-0.997547,0.14426,0.21065,0.633529,
+       0,-0.233616,-0.972329,0.14426,0.234657,0.627761,
+       0,-0.070006,-0.997547,0.14426,0.21065,0.633529,
+       0,-0.233616,-0.972329,0.182847,0.234657,0.627761,
+       0,0.100284,-0.994959,0.182847,0.21065,0.633529,
+       0,0.100284,-0.994959,0.182847,0.21065,0.633529,
+       0,0.475444,-0.879746,0.14426,-0.205301,0.520118,
+       0,-0.070006,-0.997547,0.14426,0.21065,0.633529,
+       0,0.475444,-0.879746,0.14426,-0.205301,0.520118,
+       0,0.100284,-0.994959,0.182847,0.21065,0.633529,
+       0,0.475444,-0.879746,0.182847,-0.205301,0.520118,
+       0,0.475444,-0.879746,0.14426,-0.205301,0.520118,
+       0,0.66295,-0.748664,0.182847,-0.223786,0.50375,
+       0,0.66295,-0.748664,0.14426,-0.223786,0.50375,
+       0,0.66295,-0.748664,0.182847,-0.223786,0.50375,
+       0,0.475444,-0.879746,0.14426,-0.205301,0.520118,
+       0,0.475444,-0.879746,0.182847,-0.205301,0.520118,
+       1,0,-0.000001,0.14426,0.234657,0.627761,
+       1,0,-0.000001,0.14426,-0.205301,0.520118,
+       1,0,-0.000001,0.14426,-0.223786,0.50375,
+       1,0,-0.000001,0.14426,-0.205301,0.520118,
+       1,0,-0.000001,0.14426,0.234657,0.627761,
+       1,0,-0.000001,0.14426,0.21065,0.633529,
+       -1,0,0,0.182847,-0.205301,0.520118,
+       -1,0,0,0.182847,0.234657,0.627761,
+       -1,0,0,0.182847,-0.223786,0.50375,
+       -1,0,0,0.182847,0.234657,0.627761,
+       -1,0,0,0.182847,-0.205301,0.520118,
+       -1,0,0,0.182847,0.21065,0.633529
+};
+static const struct gllist dumpster_model_lid_panels_frame = {
+ GL_N3F_V3F, GL_TRIANGLES, 78, dumpster_model_lid_panels_data, 0
+};
+const struct gllist *dumpster_model_lid_panels = &dumpster_model_lid_panels_frame;
+
+static const float dumpster_model_panels_half_data[] = {
+       0,-1,0,0.43601,-0.247708,0.020372,
+       0,-1,0,0.019519,-0.247708,0.447625,
+       0,-1,0,0.019519,-0.247708,0.020372,
+       0,-1,0,0.019519,-0.247708,0.447625,
+       0,-1,0,0.43601,-0.247708,0.020372,
+       0,-1,0,0.43601,-0.247708,0.44762,
+       0,1,0,0.019519,0.247708,0.583158,
+       0,1,0,0.43601,0.247708,0.020372,
+       0,1,0,0.019519,0.247708,0.020372,
+       0,1,0,0.43601,0.247708,0.020372,
+       0,1,0,0.019519,0.247708,0.583158,
+       0,1,0,0.43601,0.247708,0.583149,
+       1,0,0,0.442262,0.242083,0.578243,
+       1,0,0,0.442262,-0.242083,0.452526,
+       1,0,0,0.442262,0.242083,0.019745,
+       1,0,0,0.442262,0.242083,0.019745,
+       1,0,0,0.442262,-0.242083,0.452526,
+       1,0,0,0.442262,-0.242083,0.019745,
+       -0.000001,-1,0,0.509245,0.022064,0.401184,
+       -0.000001,-1,0,0.459509,0.022064,0.271574,
+       -0.000001,-1,0,0.509245,0.022064,0.271574,
+       -0.000001,-1,0,0.459509,0.022064,0.271574,
+       -0.000001,-1,0,0.509245,0.022064,0.401184,
+       0,-1,0,0.459509,0.022064,0.401184,
+       0,0,-1,0.459509,0.022064,0.401184,
+       0,0,-1,0.509245,-0.049899,0.401184,
+       0,0,-1,0.459509,-0.049899,0.401184,
+       0,0,-1,0.509245,-0.049899,0.401184,
+       0,0,-1,0.459509,0.022064,0.401184,
+       0,0,-1,0.509245,0.022064,0.401184,
+       1,-0.000001,0,0.459509,-0.049899,0.271574,
+       1,-0.000001,0,0.459509,0.022064,0.401184,
+       1,-0.000001,0,0.459509,-0.049899,0.401184,
+       1,-0.000001,0,0.459509,0.022064,0.401184,
+       1,-0.000001,0,0.459509,-0.049899,0.271574,
+       1,-0.000001,0,0.459509,0.022064,0.271574,
+       0.000001,0,1,0.509245,-0.049899,0.271574,
+       0.000001,0,1,0.459509,0.022064,0.271574,
+       0.000001,0,1,0.459509,-0.049899,0.271574,
+       0.000001,0,1,0.459509,0.022064,0.271574,
+       0.000001,0,1,0.509245,-0.049899,0.271574,
+       0.000001,0,1,0.509245,0.022064,0.271574,
+       -1,0.000001,0,0.509245,0.022064,0.401184,
+       -1,0.000001,0,0.509245,-0.049899,0.271574,
+       -1,0.000001,0,0.509245,-0.049899,0.401184,
+       -1,0.000001,0,0.509245,-0.049899,0.271574,
+       -1,0.000001,0,0.509245,0.022064,0.401184,
+       -1,0.000001,0,0.509245,0.022064,0.271574
+};
+static const struct gllist dumpster_model_panels_half_frame = {
+ GL_N3F_V3F, GL_TRIANGLES, 48, dumpster_model_panels_half_data, 0
+};
+const struct gllist *dumpster_model_panels_half = &dumpster_model_panels_half_frame;
diff --git a/hacks/glx/dumpsterfire.c b/hacks/glx/dumpsterfire.c
new file mode 100644 (file)
index 0000000..c0392f4
--- /dev/null
@@ -0,0 +1,860 @@
+/* dumpsterfire, Copyright © 2025 Jamie Zawinski <jwz@jwz.org>
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or
+ * implied warranty.
+ *
+ * Created by jwz: 22-Apr-2025
+ *
+ * Dumpster model by: xx_n0va_x https://skfb.ly/psRAy
+ *   and slightly modified by jwz.
+ *   Licensed under Creative Commons Attribution
+ *   http://creativecommons.org/licenses/by/4.0/
+ */
+
+#define DEFAULTS       "*delay:              20000       \n" \
+                       "*showFPS:            False       \n" \
+                       "*wireframe:          False       \n" \
+                       "*dumpsterFrameColor: #777799"   "\n" \
+                       "*dumpsterPanelColor: #8888AA"   "\n" \
+                       "*insideColor:        #112211"   "\n" \
+                       "*hingesColor:        #666666"   "\n" \
+                       "*axleColor:          #444444"   "\n" \
+                       "*lidColor:           #8888FF"   "\n" \
+                       "*lidPanelColor:      #7777EE"   "\n" \
+
+# define release_dumpster 0
+
+#include "xlockmore.h"
+#include "colors.h"
+#include "rotator.h"
+#include "gltrackball.h"
+#include "gllist.h"
+#include <ctype.h>
+
+#define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3)
+
+extern const struct gllist
+  *dumpster_model_frame_half,
+  *dumpster_model_panels_half,
+  *dumpster_model_inside_half,
+  *dumpster_model_hinges_half,
+  *dumpster_model_axle,
+  *dumpster_model_lid,
+  *dumpster_model_lid_panels;
+
+static const struct gllist * const *all_objs[] = {
+  &dumpster_model_frame_half,
+  &dumpster_model_panels_half,
+  &dumpster_model_inside_half,
+  &dumpster_model_hinges_half,
+  &dumpster_model_axle,
+  &dumpster_model_lid,
+  &dumpster_model_lid_panels,
+};
+
+enum { FRAME_HALF, PANELS_HALF, INSIDE_HALF, HINGES_HALF, AXLE,
+       LID, LID_PANELS };
+
+typedef struct { GLfloat x, y, z; } XYZ;
+
+#ifdef USE_GL /* whole file */
+
+
+#define DEF_SPIN        "True"
+#define DEF_WANDER      "True"
+#define DEF_SPEED       "1.0"
+#define DEF_DENSITY     "1.0"
+
+typedef struct {
+  float        fade;
+  GLfloat color[4];
+  XYZ pos, speed, accel;
+  GLdouble sortkey;
+} particle;
+
+
+typedef struct {
+  GLXContext *glx_context;
+  rotator *rot;
+  trackball_state *trackball;
+  Bool button_down_p;
+  XYZ pos;
+  XYZ wind;
+  GLfloat tick;
+  enum { DROP, IGNITE, OPEN, BURN, QUENCH, CLOSE, ROLL } state;
+  GLfloat lid_angle[2];
+  GLuint *dlists;
+  GLfloat component_colors[countof(all_objs)][4];
+  GLuint texid;
+  GLuint sprite_dlist;
+  GLfloat density;
+  int nparticles;
+  particle *particles;
+
+} dumpster_configuration;
+
+static dumpster_configuration *bps = NULL;
+
+static Bool do_spin;
+static GLfloat speed_arg;
+static Bool do_wander;
+static GLfloat density_arg;
+
+static XrmOptionDescRec opts[] = {
+  { "-spin",    ".spin",    XrmoptionNoArg, "True" },
+  { "+spin",    ".spin",    XrmoptionNoArg, "False" },
+  { "-speed",   ".speed",   XrmoptionSepArg, 0 },
+  { "-wander",  ".wander",  XrmoptionNoArg, "True" },
+  { "+wander",  ".wander",  XrmoptionNoArg, "False" },
+  { "-density", ".density", XrmoptionSepArg, 0 },
+};
+
+static argtype vars[] = {
+  {&do_spin,     "spin",    "Spin",    DEF_SPIN,    t_Bool},
+  {&do_wander,   "wander",  "Wander",  DEF_WANDER,  t_Bool},
+  {&speed_arg,   "speed",   "Speed",   DEF_SPEED,   t_Float},
+  {&density_arg, "density", "Density", DEF_DENSITY, t_Float},
+};
+
+ENTRYPOINT ModeSpecOpt dumpster_opts = {
+  countof(opts), opts, countof(vars), vars, NULL};
+
+
+/* Color spectrum of temperature per Planck’s law.
+ */
+#undef RGB
+#define RGB(x) \
+  { (((x) >> 16) & 0xFF) / 255.0, \
+    (((x) >>  8) & 0xFF) / 255.0, \
+    (((x) >>  0) & 0xFF) / 255.0, \
+    1.0 }
+static const GLfloat fire_colors[][4] = {
+  RGB(0x352201),  /*   550°C  */
+  RGB(0x542803),  /*   630°C  */
+  RGB(0x681100),  /*   680°C  */
+  RGB(0x861600),  /*   740°C  */
+  RGB(0xA00000),  /*   780°C  */
+  RGB(0xC11B1B),  /*   810°C  */
+  RGB(0xD44115),  /*   850°C  */
+  RGB(0xE9582C),  /*   900°C  */
+  RGB(0xE97E1C),  /*   950°C  */
+  RGB(0xFFAA0F),  /*  1000°C  */
+  RGB(0xFBC034),  /*  1100°C  */
+  RGB(0xFFCF61),  /*  1200°C  */
+  RGB(0xFFE6AD),  /* >1300°C  */
+  RGB(0xFFE6AD),
+  RGB(0xFFE6AD),
+  RGB(0xFFE6AD),  /* Some padding so that the brightest flames aren't */
+  RGB(0xFFE6AD),  /* hidden inside the dumpster. */
+  RGB(0xFFE6AD),
+  RGB(0xFFE6AD),
+  RGB(0xFFE6AD),
+  RGB(0xFFE6AD),
+  RGB(0xFFE6AD),
+  RGB(0xFFE6AD),
+  RGB(0xFFE6AD),
+};
+
+
+static void
+build_texture (ModeInfo *mi)
+{
+  dumpster_configuration *bp = &bps[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  int x, y;
+  int size = 128;
+  int s2 = size / 2;
+  int bpl = size * 4;
+  unsigned char *data = malloc (bpl * size);
+
+  for (y = 0; y < size; y++)
+    {
+      for (x = 0; x < size; x++)
+        {
+          double dist = (sqrt (((s2 - x) * (s2 - x)) +
+                               ((s2 - y) * (s2 - y)))
+                         / s2);
+          unsigned char *c = &data [y * bpl + x * 4];
+          unsigned char v = 0xFF * sin (dist > 1 ? 0 : (1 - dist));
+          c[0] = 0xFF;
+          c[1] = 0xFF;
+          c[2] = 0xFF;
+          c[3] = v;
+        }
+    }
+
+  glGenTextures (1, &bp->texid);
+  glBindTexture (GL_TEXTURE_2D, bp->texid);
+
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+  glTexEnvi (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
+  glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
+  check_gl_error ("texture param");
+
+  glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0,
+                GL_RGBA, GL_UNSIGNED_BYTE, data);
+  check_gl_error ("texture");
+  free (data);
+
+  bp->sprite_dlist = glGenLists (1);
+  glNewList (bp->sprite_dlist, GL_COMPILE);
+  if (wire)
+    {
+      double th = 0;
+      glBegin(GL_LINE_LOOP);
+      for (th = 0; th < M_PI*2; th += M_PI / 6)
+        glVertex3f (0.5 * cos (th), 0.5 * sin (th), 0);
+      glEnd();
+    }
+  else
+    {
+      glBegin(GL_QUADS);
+      glTexCoord2d(1,1); glVertex3f( 0.5,  0.5, 0);
+      glTexCoord2d(0,1); glVertex3f(-0.5,  0.5, 0);
+      glTexCoord2d(0,0); glVertex3f(-0.5, -0.5, 0);
+      glTexCoord2d(1,0); glVertex3f( 0.5, -0.5, 0);
+      glEnd();
+    }
+  glEndList ();
+}
+
+
+static int
+particle_sort_cmp (const void *aa, const void *bb)
+{
+  const particle *a = (particle *) aa;
+  const particle *b = (particle *) bb;
+  return (a->sortkey == b->sortkey ? 0 : a->sortkey < b->sortkey ? -1 : 1);
+}
+
+
+static void
+draw_fire (ModeInfo *mi)
+{
+  dumpster_configuration *bp = &bps[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+
+  GLfloat size = 0.25 / bp->density;
+  GLfloat max_size = 1.20;  /* Don't peek through closed lid */
+  GLfloat min_size = 0.80;  /* Don't be too spotty */
+  int i;
+
+  switch (bp->state) {
+  case DROP: case CLOSE: case ROLL:
+    return;
+  default:
+    break;
+  }
+
+  /* I thought about making the sprites be tall ovals instead of circles,
+     for more of a flame shape, but with billboarding that only looks right
+     if "up" is the same for both camera and scene -- no tilting.
+   */
+
+  if (size > max_size) size = max_size;
+  if (size < min_size) size = min_size;
+  if (wire) size *= 0.6;
+
+  /* For transparency to work right, we have to draw the sprites from
+     back to front, from the perspective of the observer.  So project
+     the origin of each sprite to screen coordinates, and sort that by Z.
+   */
+  {
+    GLdouble mm[16], pm[16];
+    GLint vp[4];
+    glGetDoublev (GL_MODELVIEW_MATRIX, mm);
+    glGetDoublev (GL_PROJECTION_MATRIX, pm);
+    glGetIntegerv (GL_VIEWPORT, vp);
+
+    for (i = 0; i < bp->nparticles; i++)
+      {
+        GLdouble x, y, z;
+        particle *p = &bp->particles[i];
+        gluProject (p->pos.y, p->pos.z, p->pos.x, mm, pm, vp, &x, &y, &z);
+        p->sortkey = -z;
+      }
+    qsort (bp->particles, bp->nparticles, sizeof(*bp->particles),
+           particle_sort_cmp);
+  }
+
+  /* Render each particle.
+   */
+  glPushMatrix();
+
+  if (! wire)
+    {
+      glEnable (GL_DEPTH_TEST);
+      glDepthFunc (GL_LESS);
+      glDepthMask (GL_FALSE);
+
+      glEnable (GL_TEXTURE_2D);
+      glDisable (GL_LIGHTING);
+      glShadeModel (GL_FLAT);
+      glEnable (GL_BLEND);
+      glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+      glPolygonMode (GL_FRONT, GL_FILL);
+      glBindTexture (GL_TEXTURE_2D, bp->texid);
+    }
+
+  /* Position at the lid that is open. */
+  {
+    GLfloat s = 0.5;
+    glTranslatef ((bp->lid_angle[0] == 0 ? -1 : 1) * 2.3,
+                  4,
+                  0);
+    glRotatef (90, -1, 0, 0);  /* Z is up */
+    glScalef (s, s, s);
+  }
+
+  for (i = 0; i < bp->nparticles; i++)
+    {
+      GLfloat m[4][4];
+      particle *p = &bp->particles[i];
+
+      glColor4fv (p->color);
+      glMaterialfv (GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE,
+                    p->color);
+
+      /* Billboard the sprites to always face the camera.  Sadly, we have to
+         glTranslate and then de-billboard for each sprite, or the flame
+         always points toward the top of the screen instead of toward the top
+         of the dumpster.  I think this makes it impossible to draw all of
+         the polygons at once with glDrawArrays and a pair of vertex/color
+         arrays. */
+
+      glPushMatrix();
+
+      glTranslatef (p->pos.x, p->pos.y, p->pos.z);
+
+      glGetFloatv (GL_MODELVIEW_MATRIX, &m[0][0]);
+      m[0][0] = 1; m[1][0] = 0; m[2][0] = 0;
+      m[0][1] = 0; m[1][1] = 1; m[2][1] = 0;
+      m[0][2] = 0; m[1][2] = 0; m[2][2] = 1;
+      glLoadIdentity();
+      glMultMatrixf (&m[0][0]);
+
+      glScalef (size, size, size);
+      glCallList (bp->sprite_dlist);
+      mi->polygon_count++;
+
+      glPopMatrix();
+    }
+
+  glPopMatrix();
+
+  if (! wire)
+    glDepthMask (GL_TRUE);
+
+
+  /* Tick each particle for the next round.
+     #### TODO: Make the fire animate faster per speed_arg. It's hard.
+   */
+  for (i = 0; i < bp->nparticles; i++)
+    {
+      particle *p = &bp->particles[i];
+
+      p->pos.x += p->speed.x;
+      p->pos.y += p->speed.y;
+      p->pos.z += p->speed.z;
+
+      if (p->pos.z > 5.0)              /* Turn fire shape into a cone. */
+        {
+          p->accel.x = 0.0016 * (p->pos.x > 0 ? -1 : 1);
+          p->accel.y = 0.0016 * (p->pos.y > 0 ? -1 : 1);
+        }
+
+      p->speed.x += p->accel.x;
+      p->speed.y += p->accel.y;
+      p->speed.z += p->accel.z;
+
+      if (p->pos.z > 4.5)              /* Wind outside of the dumpster.     */
+        {
+          p->accel.x += bp->wind.x;    /* Technically this should increase  */
+          p->accel.y += bp->wind.y;    /* speed, but increasing accel looks */
+          p->accel.z += bp->wind.z;    /* better. */
+        }
+
+      /* Alpha is particle's remaining lifetime, and temperature. */
+      p->color[3] -= p->fade;
+
+      if (bp->state == QUENCH || bp->state == IGNITE)
+        p->color[3] -= p->fade * 3;
+
+      if (p->color[3] <= 0.0)  /* Dead: reset. */
+        {
+          if (bp->state < QUENCH)
+            {
+              /* First tune speed.z and accel.z to get the vertical seething
+                 to look ok.  Then tune fade to control flame height. */
+              p->pos.x   = 0;
+              p->pos.y   = 0;
+              p->pos.z   = 0;
+              p->speed.x = 0.12 * (frand(1) - 0.5);
+              p->speed.y = 0.12 * (frand(1) - 0.5);
+              p->speed.z = 0.06 * (frand(1) - 0.5);
+              p->accel.x = 0;
+              p->accel.y = 0;
+              p->accel.z = 0.0032;
+              p->fade = (frand (0.2) + 0.006);
+
+              memcpy (p->color, fire_colors[countof(fire_colors)-1],
+                      sizeof(p->color));
+            }
+        }
+      else
+        {
+          /* Colorize but preserve alpha. */
+          int i = p->color[3] * countof(fire_colors);
+          if (i >= countof(fire_colors)) i = countof(fire_colors)-1;
+          memcpy (p->color, fire_colors[i], sizeof(GLfloat) * 3);
+        }
+    }
+}
+
+
+ENTRYPOINT void
+reshape_dumpster (ModeInfo *mi, int width, int height)
+{
+  GLfloat h = (GLfloat) height / (GLfloat) width;
+  int y = 0;
+
+  if (width > height * 5) {   /* tiny window: show middle */
+    height = width * 9/16;
+    y = -height/2;
+    h = height / (GLfloat) width;
+  }
+
+  glViewport (0, y, (GLint) width, (GLint) height);
+
+  glMatrixMode(GL_PROJECTION);
+  glLoadIdentity();
+  gluPerspective (30.0, 1/h, 1.0, 100.0);
+
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+  gluLookAt( 0.0, 0.0, 30.0,
+             0.0, 0.0, 0.0,
+             0.0, 1.0, 0.0);
+
+  {
+    GLfloat s = (MI_WIDTH(mi) < MI_HEIGHT(mi)
+                 ? (MI_WIDTH(mi) / (GLfloat) MI_HEIGHT(mi))
+                 : 1);
+    glScalef (s, s, s);
+  }
+
+  glClear(GL_COLOR_BUFFER_BIT);
+}
+
+
+ENTRYPOINT Bool
+dumpster_handle_event (ModeInfo *mi, XEvent *event)
+{
+  dumpster_configuration *bp = &bps[MI_SCREEN(mi)];
+  if (gltrackball_event_handler (event, bp->trackball,
+                                 MI_WIDTH (mi), MI_HEIGHT (mi),
+                                 &bp->button_down_p))
+    return True;
+  else if (screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event))
+    {
+      switch (bp->state) {
+      case DROP: case IGNITE:
+        bp->state = ROLL-1;
+        bp->tick = 1.0;
+        return True;
+      case OPEN: case BURN:
+        bp->state = QUENCH-1;
+        bp->tick = 1.0;
+        if (bp->lid_angle[0] != 0)
+          bp->lid_angle[0] -= 0.6;
+        else
+          bp->lid_angle[1] -= 0.6;
+        return True;
+      default:
+        return False;
+      }
+    }
+
+  return False;
+}
+
+
+static void
+parse_color (ModeInfo *mi, char *key, GLfloat color[4])
+{
+  XColor xcolor;
+  char *string = get_string_resource (mi->dpy, key, "Color");
+  if (!XParseColor (mi->dpy, mi->xgwa.colormap, string, &xcolor))
+    {
+      fprintf (stderr, "%s: unparsable color in %s: %s\n", progname,
+               key, string);
+      exit (1);
+    }
+  free (string);
+
+  color[0] = xcolor.red   / 65536.0;
+  color[1] = xcolor.green / 65536.0;
+  color[2] = xcolor.blue  / 65536.0;
+  color[3] = 1;
+}
+
+
+ENTRYPOINT void
+init_dumpster (ModeInfo *mi)
+{
+  dumpster_configuration *bp;
+  int wire = MI_IS_WIREFRAME(mi);
+  int i;
+
+  MI_INIT (mi, bps);
+  bp = &bps[MI_SCREEN(mi)];
+
+  bp->glx_context = init_GL(mi);
+
+  reshape_dumpster (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
+
+  if (!wire)
+    {
+      GLfloat pos[4] = {0.4, 0.2, 0.4, 0.0};
+      GLfloat amb[4] = {0.2, 0.2, 0.2, 1.0};
+      GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0};
+      GLfloat spc[4] = {1.0, 1.0, 1.0, 1.0};
+
+      glEnable(GL_LIGHTING);
+      glEnable(GL_LIGHT0);
+      glEnable(GL_DEPTH_TEST);
+      glEnable(GL_CULL_FACE);
+
+      glLightfv(GL_LIGHT0, GL_POSITION, pos);
+      glLightfv(GL_LIGHT0, GL_AMBIENT,  amb);
+      glLightfv(GL_LIGHT0, GL_DIFFUSE,  dif);
+      glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
+    }
+
+  {
+    double spin_speed   = speed_arg * 0.04;
+    double wander_speed = speed_arg * 0.004;
+    double spin_accel   = 0.5;
+
+    bp->rot = make_rotator (do_spin ? spin_speed : 0,
+                            do_spin ? spin_speed : 0,
+                            do_spin ? spin_speed : 0,
+                            spin_accel,
+                            do_wander ? wander_speed : 0,
+                            False);
+    bp->trackball = gltrackball_init (True);
+  }
+
+  bp->density = density_arg;
+  bp->nparticles = 10000 * bp->density;
+  if (bp->nparticles < 10) bp->nparticles = 10;
+  bp->particles = (particle *)
+    calloc (bp->nparticles, sizeof(*bp->particles));
+
+  build_texture (mi);
+
+  bp->dlists = (GLuint *) calloc (countof(all_objs)+1, sizeof(GLuint));
+  for (i = 0; i < countof(all_objs); i++)
+    bp->dlists[i] = glGenLists (1);
+
+  bp->state = DROP;
+  bp->tick = 0;
+
+  for (i = 0; i < countof(all_objs); i++)
+    {
+      const struct gllist *gll = *all_objs[i];
+      char *key = 0;
+
+      glNewList (bp->dlists[i], GL_COMPILE);
+
+      glMatrixMode(GL_MODELVIEW);
+      glPushMatrix();
+      glMatrixMode(GL_TEXTURE);
+      glPushMatrix();
+      glMatrixMode(GL_MODELVIEW);
+
+      glRotatef (-90, 1, 0, 0);
+
+      glBindTexture (GL_TEXTURE_2D, 0);
+
+      switch (i) {
+      case FRAME_HALF:  key = "dumpsterFrameColor"; break;
+      case PANELS_HALF: key = "dumpsterPanelColor"; break;
+      case INSIDE_HALF: key = "insideColor";        break;
+      case HINGES_HALF: key = "hingesColor";        break;
+      case AXLE:        key = "axleColor";          break;
+      case LID:         key = "lidColor";           break;
+      case LID_PANELS:  key = "lidPanelColor";      break;
+      default:
+        abort();
+      }
+
+      parse_color (mi, key, bp->component_colors[i]);
+      renderList (gll, wire);
+
+      glMatrixMode(GL_TEXTURE);
+      glPopMatrix();
+      glMatrixMode(GL_MODELVIEW);
+      glPopMatrix();
+
+      glEndList ();
+    }
+}
+
+
+static int
+draw_component (ModeInfo *mi, int i, Bool half_p)
+{
+  dumpster_configuration *bp = &bps[MI_SCREEN(mi)];
+
+  static const GLfloat spec[4]  = {1.0, 1.0, 1.0, 1.0};
+  static const GLfloat shiny    = 128.0;
+  glMaterialfv (GL_FRONT_AND_BACK, GL_SPECULAR,  spec);
+  glMaterialf  (GL_FRONT_AND_BACK, GL_SHININESS, shiny);
+  glMaterialfv (GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE,
+                bp->component_colors[i]);
+
+  glFrontFace (GL_CCW);
+  glCallList (bp->dlists[i]);
+
+  if (half_p)
+    {
+      glPushMatrix();
+      glScalef (-1, 1, 1);
+      glFrontFace (GL_CW);
+      glCallList (bp->dlists[i]);
+      glPopMatrix();
+    }
+
+  return (half_p ? 2 : 1) * (*all_objs[i])->points / 3;
+}
+
+
+static double
+easeOutBounce (double i)
+{
+  double n1 = 7.5625;
+  double d1 = 2.75;
+  if (i < 1 / d1) {
+    return n1 * i * i;
+  } else if (i < 2 / d1) {
+    i -= (1.5 / d1);
+    return n1 * i * i + 0.75;
+  } else if (i < 2.5 / d1) {
+    i -= (2.25 / d1);
+    return n1 * i * i + 0.9375;
+  } else {
+    i -= (2.625 / d1);
+    return n1 * i * i + 0.984375;
+  }
+}
+
+
+static void
+tick_dumpster (ModeInfo *mi)
+{
+  dumpster_configuration *bp = &bps[MI_SCREEN(mi)];
+  GLfloat ts;
+  /* double fps = MI_DELAY(mi) ? 1000000.0 / MI_DELAY(mi) : 30; */
+  double fps = 27;
+
+  if (bp->button_down_p) return;
+
+  switch (bp->state) {
+  case DROP:   ts = 3;  break;
+  case IGNITE: ts = 1;  break;
+  case OPEN:   ts = 1;  break;
+  case BURN:   ts = 99; break;
+  case QUENCH: ts = 3;  break;
+  case CLOSE:  ts = 1;  break;
+  case ROLL:   ts = 3;  break;
+  default:    abort();  break;
+  }
+
+  bp->tick += speed_arg * (1 / (ts * fps));
+
+  if (bp->tick < 1) return;
+
+  bp->tick = 0;
+  bp->state = (bp->state + 1) % (ROLL + 1);
+
+  switch (bp->state) {
+  case IGNITE:                            /* Pick which lid we are opening */
+    bp->lid_angle[random() % 2] += 0.001;
+    bp->wind.x =  0.15 * (BELLRAND (1.0) - 0.5);
+    bp->wind.y = -0.15 * (BELLRAND (0.5));
+    bp->wind.z = 0;
+    break;
+  case ROLL:                              /* Close the rest of the way */
+    bp->lid_angle[0] = bp->lid_angle[1] = 0;
+    break;
+  case CLOSE:
+    memset (bp->particles, 0, bp->nparticles * sizeof (*bp->particles));
+    break;
+  case DROP:
+    gltrackball_reset (bp->trackball, 0, 0);
+  default:
+    break;
+  }
+}
+
+
+static void
+draw_box (ModeInfo *mi)
+{
+  dumpster_configuration *bp = &bps[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  GLfloat s;
+  int i;
+
+  glPushMatrix();
+
+  glDisable (GL_BLEND);
+  glEnable (GL_NORMALIZE);
+  glEnable (GL_DEPTH_TEST);
+  glDepthFunc (GL_LESS);
+  glDepthMask (GL_TRUE);
+
+  if (!wire)
+    {
+      glEnable (GL_LIGHTING);
+      glShadeModel (GL_SMOOTH);
+    }
+
+  s = 12;
+  glScalef (s, s, s);
+
+  switch (bp->state) {
+  case DROP:
+    bp->pos.x = bp->pos.y = 0;
+    bp->pos.z = (1 - easeOutBounce (bp->tick)) * 3;
+    break;
+  case OPEN:
+    bp->lid_angle[(bp->lid_angle[0] == 0.0 ? 1 : 0)] = bp->tick     + 0.0001;
+    break;
+  case CLOSE:
+    bp->lid_angle[(bp->lid_angle[0] == 0.0 ? 1 : 0)] = (1-bp->tick) + 0.0001;
+    break;
+  case ROLL:
+    bp->pos.x += bp->tick * 2;
+    break;
+  default:
+    bp->pos.z = 0;
+    break;
+  }
+
+  if (wire)
+    glColor3f (1, 1, 1);
+
+  glTranslatef (bp->pos.x, bp->pos.z, bp->pos.y);
+
+  mi->polygon_count += draw_component (mi, FRAME_HALF,  True);
+  mi->polygon_count += draw_component (mi, PANELS_HALF, True);
+  mi->polygon_count += draw_component (mi, INSIDE_HALF, True);
+  mi->polygon_count += draw_component (mi, AXLE,        False);
+  mi->polygon_count += draw_component (mi, HINGES_HALF, True);
+
+  for (i = 0; i < 2; i++)
+    {
+      const GLfloat deg = 115;
+      const XYZ off = { 0, 0.63, -0.25 };
+      double a2 = (bp->state == CLOSE
+                   ? 1 - easeOutBounce (1 - bp->lid_angle[i])
+                   :     easeOutBounce (    bp->lid_angle[i]));
+
+      glPushMatrix();
+      glTranslatef (off.x, off.y, off.z);
+      glRotatef (-deg * a2, 1, 0, 0);
+      glTranslatef (-off.x, -off.y, -off.z);
+
+      if (i == 1)
+        glTranslatef (-0.46, 0, 0);
+
+      mi->polygon_count += draw_component (mi, LID, False);
+      mi->polygon_count += draw_component (mi, LID_PANELS, False);
+      glPopMatrix();
+    }
+
+  glPopMatrix();
+}
+
+
+ENTRYPOINT void
+draw_dumpster (ModeInfo *mi)
+{
+  dumpster_configuration *bp = &bps[MI_SCREEN(mi)];
+  Display *dpy = MI_DISPLAY(mi);
+  Window window = MI_WINDOW(mi);
+  GLfloat s;
+
+  if (!bp->glx_context)
+    return;
+
+  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+  glPushMatrix ();
+
+  {
+    double x, y, z;
+    get_position (bp->rot, &x, &y, &z, !bp->button_down_p);
+    glTranslatef((x - 0.5) * 8,
+                 (y - 0.5) * 1,
+                 (z - 0.5) * 15);
+
+    gltrackball_rotate (bp->trackball);
+
+    get_rotation (bp->rot, &x, &y, &z, !bp->button_down_p);
+ /* glRotatef (x * 360, 1.0, 0.0, 0.0); */
+    glRotatef (y * 360, 0.0, 1.0, 0.0);
+ /* glRotatef (z * 360, 0.0, 0.0, 1.0); */
+  }
+
+  mi->polygon_count = 0;
+
+  glTranslatef (0, -6, 0);
+  s = 0.7;
+  glScalef (s, s, s);
+  glRotatef (5, 1, 0, 0);
+  glRotatef (-10, 0, 1, 0);
+
+  draw_box (mi);
+  draw_fire (mi);
+
+  glPopMatrix ();
+
+  if (mi->fps_p) do_fps (mi);
+  glFinish();
+
+  tick_dumpster (mi);
+
+  glXSwapBuffers(dpy, window);
+}
+
+
+ENTRYPOINT void
+free_dumpster (ModeInfo *mi)
+{
+  dumpster_configuration *bp = &bps[MI_SCREEN(mi)];
+  int i;
+  if (!bp->glx_context) return;
+  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
+  if (bp->trackball) gltrackball_free (bp->trackball);
+  free (bp->particles);
+  if (bp->texid) glDeleteTextures (1, &bp->texid);
+  for (i = 0; i < countof(all_objs); i++)
+    if (glIsList(bp->dlists[i])) glDeleteLists(bp->dlists[i], 1);
+  if (glIsList(bp->sprite_dlist)) glDeleteLists(bp->sprite_dlist, 1);
+}
+
+XSCREENSAVER_MODULE_2 ("DumpsterFire", dumpsterfire, dumpster)
+
+#endif /* USE_GL */
index 515239501c58646bbe185fe940991c89a9035768..1fa1e9ab1ee3c604efb80ffacf38ec0d65585c15 100644 (file)
@@ -201,7 +201,7 @@ reset_floater (ModeInfo *mi, floater *f)
 
       /* Only empty toasters barrel-roll, since we don't implement
         "toast falls out". */
-      if (f->loaded == 0 && !(random() % 10))
+      if (f->loaded == 0 && !(random() % 50))
         f->dwz = (BELLRAND(2.0) - 1.0) * (4 + BELLRAND(6));
     }
   else
index b76d63973c1a913d89cee167726d4d6e0250ad86..60c093f4c8aeb48d519b23e4179e7adfe6e2ff28 100644 (file)
@@ -1,4 +1,4 @@
-/* fps, Copyright (c) 2001-2018 Jamie Zawinski <jwz@jwz.org>
+/* fps, Copyright © 2001-2025 Jamie Zawinski <jwz@jwz.org>
  * Draw a frames-per-second display (Xlib and OpenGL).
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
@@ -66,7 +66,12 @@ xlockmore_gl_draw_fps (ModeInfo *mi)
       gl_fps_data *data = (gl_fps_data *) st->gl_fps_data;
       XWindowAttributes xgwa;
       XGetWindowAttributes (st->dpy, st->window, &xgwa);
+
+# ifndef HAVE_ANDROID
+      /* This crashes in the Android emulator, but not on real hardware. */
       glColor3f (1, 1, 1);
+# endif
+
       print_texture_label (st->dpy, data->texfont,
                            xgwa.width, xgwa.height,
                            (data->top_p ? 1 : 2),
index c5b851f21f13928123e62dd5b33689ec79e3c986..3cf31c8d2a0e1855a5190f7460d089c3b4d6f27b 100644 (file)
 #include <string.h>
 
 
+#define M_PI_F 3.1415926535898f
+
+
 #ifdef HAVE_GLSL
 
 extern const char *progname;
 
-/* Copy a 4x4 column-major matrix: c = m. */
+
+/* Normalize a vector. */
+static inline void glsl_Normalize(float v[3])
+{
+  float l;
+
+  l = sqrtf(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
+  if (l > 0.0f)
+    l = 1.0f/l;
+  v[0] *= l;
+  v[1] *= l;
+  v[2] *= l;
+}
+
+
+/* Compute the cross product v = v1 × v2. */
+static inline void glsl_Cross(float v[3], float v1[3], float v2[3])
+{
+  v[0] = v1[1]*v2[2]-v1[2]*v2[1];
+  v[1] = v1[2]*v2[0]-v1[0]*v2[2];
+  v[2] = v1[0]*v2[1]-v1[1]*v2[0];
+}
+
+
+/* Copy a 4×4 column-major matrix: c = m. */
 void glsl_CopyMatrix(GLfloat c[16], GLfloat m[16])
 {
   int i, j;
@@ -35,18 +62,18 @@ void glsl_CopyMatrix(GLfloat c[16], GLfloat m[16])
 }
 
 
-/* Create a 4x4 column-major identity matrix. */
+/* Create a 4×4 column-major identity matrix. */
 void glsl_Identity(GLfloat c[16])
 {
   int i, j;
 
   for (j=0; j<4; j++)
     for (i=0; i<4; i++)
-      c[GLSL__LINCOOR(i,j,4)] = (i==j);
+      c[GLSL__LINCOOR(i,j,4)] = (float)(i==j);
 }
 
 
-/* Multiply two 4x4 column-major matrices: c = c*m. */
+/* Multiply two 4×4 column-major matrices: c = c * m. */
 void glsl_MultMatrix(GLfloat c[16], GLfloat m[16])
 {
   int i, j;
@@ -65,8 +92,19 @@ void glsl_MultMatrix(GLfloat c[16], GLfloat m[16])
 }
 
 
-/* Multiply a 4x4 column-major matrix by a rotation matrix that rotates
-   around the axis (x,y,z) by the angle angle: c = c*r(angle,x,y,z). */
+/* Multiply a 4×4 column-major matrix with a vector: o = m * v. */
+void glsl_MultMatrixVector(GLfloat o[4], GLfloat m[16], GLfloat v[4])
+{
+  int i;
+
+  for (i=0; i<4; i++)
+    o[i] = (m[GLSL__LINCOOR(i,0,4)]*v[0]+m[GLSL__LINCOOR(i,1,4)]*v[1]+
+            m[GLSL__LINCOOR(i,2,4)]*v[2]+m[GLSL__LINCOOR(i,3,4)]*v[3]);
+}
+
+
+/* Multiply a 4×4 column-major matrix by a rotation matrix that rotates
+   around the axis (x,y,z) by the angle angle: c = c * r(angle,x,y,z). */
 void glsl_Rotate(GLfloat c[16], GLfloat angle, GLfloat x, GLfloat y, GLfloat z)
 {
   GLfloat l, t, ct, st, omct, n[3], r[16];
@@ -75,7 +113,7 @@ void glsl_Rotate(GLfloat c[16], GLfloat angle, GLfloat x, GLfloat y, GLfloat z)
   n[0] = x/l;
   n[1] = y/l;
   n[2] = z/l;
-  t = angle*M_PI/180.0f;
+  t = angle*M_PI_F/180.0f;
   ct = cosf(t);
   st = sinf(t);
   omct = 1.0f-ct;
@@ -104,7 +142,23 @@ void glsl_Rotate(GLfloat c[16], GLfloat angle, GLfloat x, GLfloat y, GLfloat z)
 }
 
 
-/* Multiply a 4x4 column-major matrix by a matrix that stretches, shrinks,
+/* Multiply a 4×4 column-major matrix by a matrix that translates an object
+   by a translation vector: c = c * t(tx,ty,tz). */
+void glsl_Translate(GLfloat c[16], GLfloat tx, GLfloat ty, GLfloat tz)
+{
+  int i;
+
+  for (i=0; i<4; i++)
+  {
+    c[GLSL__LINCOOR(i,3,4)] = (tx*c[GLSL__LINCOOR(i,0,4)]+
+                               ty*c[GLSL__LINCOOR(i,1,4)]+
+                               tz*c[GLSL__LINCOOR(i,2,4)]+
+                               c[GLSL__LINCOOR(i,3,4)]);
+  }
+}
+
+
+/* Multiply a 4×4 column-major matrix by a matrix that stretches, shrinks,
    or reflects an object along the axes: c = c*m(sx,sy,sz). */
 void glsl_Scale(GLfloat c[16], GLfloat sx, GLfloat sy, GLfloat sz)
 {
@@ -119,31 +173,55 @@ void glsl_Scale(GLfloat c[16], GLfloat sx, GLfloat sy, GLfloat sz)
 }
 
 
-/* Multiply a 4x4 column-major matrix by a matrix that translates an object
-   by a translation vector: c = c*t(tx,ty,tz). */
-void glsl_Translate(GLfloat c[16], GLfloat tx, GLfloat ty, GLfloat tz)
+/* Add a look-at viewing matrix to a 4×4 column-major matrix. */
+void glsl_LookAt(GLfloat c[16], GLfloat eyex, GLfloat eyey, GLfloat eyez,
+                 GLfloat centerx, GLfloat centery, GLfloat centerz,
+                 GLfloat upx, GLfloat upy, GLfloat upz)
 {
-  int i;
+  float forward[3], side[3], up[3];
+  GLfloat m[16];
 
-  for (i=0; i<4; i++)
-  {
-    c[GLSL__LINCOOR(i,3,4)] = (tx*c[GLSL__LINCOOR(i,0,4)]+
-                               ty*c[GLSL__LINCOOR(i,1,4)]+
-                               tz*c[GLSL__LINCOOR(i,2,4)]+
-                               c[GLSL__LINCOOR(i,3,4)]);
-  }
+  forward[0] = centerx-eyex;
+  forward[1] = centery-eyey;
+  forward[2] = centerz-eyez;
+  glsl_Normalize(forward);
+
+  up[0] = upx;
+  up[1] = upy;
+  up[2] = upz;
+
+  /* side = forward × up */
+  glsl_Cross(side,forward,up);
+  glsl_Normalize(side);
+
+  /* up = side × forward */
+  glsl_Cross(up,side,forward);
+
+  glsl_Identity(m);
+  m[GLSL__LINCOOR(0,0,4)] = side[0];
+  m[GLSL__LINCOOR(0,1,4)] = side[1];
+  m[GLSL__LINCOOR(0,2,4)] = side[2];
+  m[GLSL__LINCOOR(1,0,4)] = up[0];
+  m[GLSL__LINCOOR(1,1,4)] = up[1];
+  m[GLSL__LINCOOR(1,2,4)] = up[2];
+  m[GLSL__LINCOOR(2,0,4)] = -forward[0];
+  m[GLSL__LINCOOR(2,1,4)] = -forward[1];
+  m[GLSL__LINCOOR(2,2,4)] = -forward[2];
+
+  glsl_MultMatrix(c,m);
+  glsl_Translate(c,-eyex,-eyey,-eyez);
 }
 
 
-/* Add a perspective projection to a 4x4 column-major matrix. */
+/* Add a perspective projection to a 4×4 column-major matrix. */
 void glsl_Perspective(GLfloat c[16], GLfloat fovy, GLfloat aspect,
                       GLfloat z_near, GLfloat z_far)
 {
   GLfloat m[16];
-  double s, cot, dz;
-  double rad;
+  float s, cot, dz;
+  float rad;
 
-  rad = fovy*(0.5f*(float)M_PI/180.0f);
+  rad = fovy*(0.5f*(float)M_PI_F/180.0f);
   dz = z_far-z_near;
   s = sinf(rad);
   if (dz == 0.0f || s == 0.0f || aspect == 0.0f)
@@ -157,11 +235,12 @@ void glsl_Perspective(GLfloat c[16], GLfloat fovy, GLfloat aspect,
   m[GLSL__LINCOOR(3,2,4)] = -1.0f;
   m[GLSL__LINCOOR(2,3,4)] = -2.0f*z_near*z_far/dz;
   m[GLSL__LINCOOR(3,3,4)] = 0.0f;
+
   glsl_MultMatrix(c,m);
 }
 
 
-/* Add an orthographic projection to a 4x4 column-major matrix. */
+/* Add an orthographic projection to a 4×4 column-major matrix. */
 void glsl_Orthographic(GLfloat c[16], GLfloat left, GLfloat right,
                        GLfloat bottom, GLfloat top,
                        GLfloat nearval, GLfloat farval)
@@ -178,6 +257,7 @@ void glsl_Orthographic(GLfloat c[16], GLfloat left, GLfloat right,
   m[GLSL__LINCOOR(1,3,4)] = -(top+bottom)/(top-bottom);
   m[GLSL__LINCOOR(2,2,4)] = -2.0f/(farval-nearval);
   m[GLSL__LINCOOR(2,3,4)] = -(farval+nearval)/(farval-nearval);
+
   glsl_MultMatrix(c,m);
 }
 
@@ -189,48 +269,50 @@ GLboolean glsl_GetGlAndGlslVersions(GLint *gl_major, GLint *gl_minor,
 {
   const char *gl_version, *glsl_version;
   int n;
-  const char *err = 0;
+  const char *err = NULL;
 
   *gl_major = -1;
   *gl_minor = -1;
-  *glsl_major = 1;
+  *glsl_major = -1;
   *glsl_minor = -1;
   *gl_gles3 = GL_FALSE;
   gl_version = (const char *)glGetString(GL_VERSION);
   glsl_version = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);
   if (gl_version == NULL || glsl_version == NULL)
-    {
-      err = "GL version unknown";
-      goto DONE;
-    }
+  {
+    err = "GL version unknown";
+    goto DONE;
+  }
   if (!strncmp(gl_version,"OpenGL ES",9))
   {
     if (!strncmp(glsl_version,"OpenGL ES GLSL ES",17))
+    {
       *gl_gles3 = GL_TRUE;
+    }
     else
-      {
-        err = "GLSL not supported";
-        goto DONE;
-      }
+    {
+      err = "GLSL not supported";
+      goto DONE;
+    }
   }
   if (*gl_gles3)
     n = sscanf(&gl_version[9],"%d.%d",gl_major,gl_minor);
   else
     n = sscanf(gl_version,"%d.%d",gl_major,gl_minor);
   if (n != 2)
-    {
-      err = "GL version number unparsable";
-      goto DONE;
-    }
+  {
+    err = "GL version number unparsable";
+    goto DONE;
+  }
   if (*gl_gles3)
     n = sscanf(&glsl_version[17],"%d.%d",glsl_major,glsl_minor);
   else
     n = sscanf(glsl_version,"%d.%d",glsl_major,glsl_minor);
   if (n != 2)
-    {
-      err = "GLSL version number unparsable";
-      goto DONE;
-    }
+  {
+    err = "GLSL version number unparsable";
+    goto DONE;
+  }
 
  DONE:
 
index 20edc9dba7dd0980e55053e8b636bb48bc3fc4ba..9ed6f7983b2de98faafb69057679d431c69bdb27 100644 (file)
@@ -30,18 +30,27 @@ extern void glsl_Identity(GLfloat c[16]);
 /* Multiply two 4x4 column-major matrices: c = c*m. */
 extern void glsl_MultMatrix(GLfloat c[16], GLfloat m[16]);
 
+/* Multiply a 4×4 column-major matrix with a vector: o = m * v. */
+extern void glsl_MultMatrixVector(GLfloat o[4], GLfloat m[16], GLfloat v[4]);
+
 /* Multiply a 4x4 column-major matrix by a rotation matrix that rotates
    around the axis (x,y,z) by the angle angle: c = c*r(angle,x,y,z). */
 extern void glsl_Rotate(GLfloat c[16], GLfloat angle, GLfloat x, GLfloat y,
                         GLfloat z);
 
+/* Multiply a 4x4 column-major matrix by a matrix that translates an object
+   by a translation vector: c = c * t(tx,ty,tz). */
+extern void glsl_Translate(GLfloat c[16], GLfloat tx, GLfloat ty, GLfloat tz);
+
 /* Multiply a 4x4 column-major matrix by a matrix that stretches, shrinks,
    or reflects an object along the axes: c = c*s(sx,sy,sz). */
 extern void glsl_Scale(GLfloat c[16], GLfloat sx, GLfloat sy, GLfloat sz);
 
-/* Multiply a 4x4 column-major matrix by a matrix that translates an object
-   by a translation vector: c = c*t(tx,ty,tz). */
-extern void glsl_Translate(GLfloat c[16], GLfloat tx, GLfloat ty, GLfloat tz);
+/* Add a look-at viewing matrix to a 4×4 column-major matrix. */
+extern void glsl_LookAt(GLfloat c[16],
+                        GLfloat eyex, GLfloat eyey, GLfloat eyez,
+                        GLfloat centerx, GLfloat centery, GLfloat centerz,
+                        GLfloat upx, GLfloat upy, GLfloat upz);
 
 /* Add a perspective projection to a 4x4 column-major matrix. */
 extern void glsl_Perspective(GLfloat c[16], GLfloat fovy, GLfloat aspect,
index d7e133e7afce90f45681d1d91c7f2c3b2a76095e..fd8f5febdfc8cee38e2ab2f36f65d6da8dba05df 100644 (file)
@@ -625,6 +625,18 @@ static const struct model_s model[] = {
        RIGHT, ZERO, RIGHT, ZERO, ZERO }
     },
 
+    /* Models by stixpjr@gmail.com */
+    { "begging dog",
+      { ZERO, RIGHT, RIGHT, RIGHT, PIN, LEFT, RIGHT, ZERO, RIGHT,
+       LEFT, PIN, RIGHT, RIGHT, ZERO, LEFT, PIN, LEFT, RIGHT, PIN,
+       RIGHT, LEFT, PIN, LEFT }
+    },
+    { "swan",
+      { ZERO, PIN, ZERO, ZERO, ZERO, LEFT, ZERO, LEFT, ZERO, ZERO,
+       RIGHT, PIN, LEFT, ZERO, ZERO, LEFT, PIN, RIGHT, ZERO, ZERO,
+       LEFT, ZERO, LEFT }
+    },
+
     /* These models come from the website at 
      * http://www.geocities.com/stigeide/snake */
 #if 0
index 2002102ccff6d078ac4cadc4a7f7874d25817fec..47b4e710653a883aee9281c1b47aee466a93bdab 100644 (file)
@@ -1,4 +1,4 @@
-/* gltrackball, Copyright (c) 2002-2017 Jamie Zawinski <jwz@jwz.org>
+/* gltrackball, Copyright © 2002-2025 Jamie Zawinski <jwz@jwz.org>
  * GL-flavored wrapper for trackball.c
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
@@ -222,7 +222,14 @@ gltrackball_rotate (trackball_state *ts)
     }
 
   build_rotmatrix (m, ts->q);
+
+# ifndef HAVE_ANDROID
+  /* This crashes in the Android emulator, presumably because of the hellscape
+     that is OpenGLES 3.0. But since Android has no way to interact with hacks
+     as either screen savers or live wallpapers, the trackball code is all a
+     no-op anyway. */
   glMultMatrixf (&m[0][0]);
+# endif
 }
 
 
diff --git a/hacks/glx/hopfanimations.c b/hacks/glx/hopfanimations.c
new file mode 100644 (file)
index 0000000..0c5f0ea
--- /dev/null
@@ -0,0 +1,10224 @@
+/* hopfanimations.c --- Definition of the animations used in the Hopf
+   fibration. */
+/* Copyright (c) 2025 Carsten Steger <carsten@mirsanmir.org>.
+ *
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose and without fee is hereby granted,
+ * provided that the above copyright notice appear in all copies and that
+ * both that copyright notice and this permission notice appear in
+ * supporting documentation.
+ *
+ * This file is provided AS IS with no warranties of any kind.  The author
+ * shall have no liability with respect to the infringement of copyrights,
+ * trade secrets or any patents by this file or any part thereof.  In no
+ * event will the author be liable for any lost revenue or profits or
+ * other special, indirect and consequential damages.
+ *
+ * REVISION HISTORY:
+ * C. Steger - 26/02/06: Initial version
+ */
+
+#ifdef USE_GL
+
+#include "hopfanimations.h"
+
+
+#define ANIM_SO_NAME(f) anim_ ## f ## _s
+#define ANIM_SO_DEF(f) static animation_single_obj ANIM_SO_NAME(f)[]
+#define ANIM_SO_NUM(f) sizeof(ANIM_SO_NAME(f))/sizeof(*ANIM_SO_NAME(f))
+#define ANIM_MO_NAME(f) anim_ ## f ## _m
+#define ANIM_MO_REF(f) &ANIM_MO_NAME(f)
+#define ANIM_MO_DEF(f) static animation_multi_obj ANIM_MO_NAME(f)
+#define ANIM_PH_NAME(f) anim_ ## f ## _p
+#define ANIM_PH_DEF(f) static animation_multi_obj *ANIM_PH_NAME(f)[]
+#define ANIM_PH_NUM(f) sizeof(ANIM_PH_NAME(f))/sizeof(*ANIM_PH_NAME(f))
+#define ANIM_PS_NAME(f) anim_ ## f
+#define ANIM_PS_REF(f) &ANIM_PS_NAME(f)
+#define ANIM_PS_DEF(f) static animation_phases ANIM_PS_NAME(f)
+#define ANIMS_M_NAME(f) anims_ ## f ## _m
+#define ANIMS_M_DEF(f) static animation_phases *ANIMS_M_NAME(f)[]
+#define ANIMS_M_NUM(f) sizeof(ANIMS_M_NAME(f))/sizeof(*ANIMS_M_NAME(f))
+#define ANIMS_NAME(f) anims_ ## f
+#define ANIMS_REF(f) &ANIMS_NAME(f)
+#define ANIMS_DEF(f) static animations ANIMS_NAME(f)
+
+
+
+/*****************************************************************************
+ * The set of all transformations from a single point.
+ *****************************************************************************/
+
+/* The set of possible animations for a single point. */
+
+/* Rotate a point on the equator around the z axis. */
+ANIM_SO_DEF(single_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_rot_z) = {
+  ANIM_SO_NUM(single_point_rot_z),
+  ANIM_SO_NAME(single_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_rot_z) = {
+  ANIM_MO_REF(single_point_rot_z)
+};
+
+ANIM_PS_DEF(single_point_rot_z) = {
+  ANIM_PH_NUM(single_point_rot_z),
+  ANIM_PH_NAME(single_point_rot_z)
+};
+
+/* Move a point on the equator along a Hopf torus. */
+ANIM_SO_DEF(single_point_move_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    4,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_move_hopf_torus) = {
+  ANIM_SO_NUM(single_point_move_hopf_torus),
+  ANIM_SO_NAME(single_point_move_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_move_hopf_torus) = {
+  ANIM_MO_REF(single_point_move_hopf_torus)
+};
+
+ANIM_PS_DEF(single_point_move_hopf_torus) = {
+  ANIM_PH_NUM(single_point_move_hopf_torus),
+  ANIM_PH_NAME(single_point_move_hopf_torus)
+};
+
+/* Move a point on the equator along a Hopf spiral. */
+ANIM_SO_DEF(single_point_move_hopf_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -4.0f*M_PI_F,         EASING_CUBIC, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_move_hopf_spiral) = {
+  ANIM_SO_NUM(single_point_move_hopf_spiral),
+  ANIM_SO_NAME(single_point_move_hopf_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_move_hopf_spiral) = {
+  ANIM_MO_REF(single_point_move_hopf_spiral)
+};
+
+ANIM_PS_DEF(single_point_move_hopf_spiral) = {
+  ANIM_PH_NUM(single_point_move_hopf_spiral),
+  ANIM_PH_NAME(single_point_move_hopf_spiral)
+};
+
+/* Split a single point on the equator into two points, rotate the two
+   points around the z axis, and merge the two points again. */
+
+/* Phase 1: Split a single point on the equator into two points on the
+   equator by moving the second point on the equator. */
+ANIM_SO_DEF(single_point_to_double_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_ACCEL, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_double_point_rot_z) = {
+  ANIM_SO_NUM(single_point_to_double_point_rot_z),
+  ANIM_SO_NAME(single_point_to_double_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+/* Phase 2: Rotate the two points around the z axis. */
+ANIM_SO_DEF(double_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              M_PI_F,               EASING_LIN,   /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_point_rot_z) = {
+  ANIM_SO_NUM(double_point_rot_z),
+  ANIM_SO_NAME(double_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+/* Phase 3: Merge the two points on the equator into a single point on the
+   equator. */
+ANIM_SO_DEF(double_point_to_single_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               2.0f*M_PI_F,          EASING_DECEL, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_point_to_single_point_rot_z) = {
+  ANIM_SO_NUM(double_point_to_single_point_rot_z),
+  ANIM_SO_NAME(double_point_to_single_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(double_point_rot_z) = {
+  ANIM_MO_REF(single_point_to_double_point_rot_z),
+  ANIM_MO_REF(double_point_rot_z),
+  ANIM_MO_REF(double_point_to_single_point_rot_z),
+};
+
+ANIM_PS_DEF(double_point_rot_z) = {
+  ANIM_PH_NUM(double_point_rot_z),
+  ANIM_PH_NAME(double_point_rot_z)
+};
+
+/* Split a single point on the equator into two points, move the two
+   points along a Hopf torus, and merge the two points again. */
+
+/* Phase 1: Split a single point on the equator into two points on the
+   equator by moving the second point on a meridian. */
+ANIM_SO_DEF(single_point_to_double_point_move_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, -1.0f, 0.0f }, 0.0f, M_PI_F,        EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_double_point_move_hopf_torus) = {
+  ANIM_SO_NUM(single_point_to_double_point_move_hopf_torus),
+  ANIM_SO_NAME(single_point_to_double_point_move_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+/* Phase 2: Move the two points along the Hopf torus. */
+ANIM_SO_DEF(double_point_move_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    4,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              M_PI_F,               EASING_CUBIC, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    4,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_point_move_hopf_torus) = {
+  ANIM_SO_NUM(double_point_move_hopf_torus),
+  ANIM_SO_NAME(double_point_move_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 3: Merge the two points on the equator into a single point on the
+   equator by moving the second point along a meridian. */
+ANIM_SO_DEF(double_point_to_single_point_move_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, -1.0f, 0.0f }, M_PI_F, 2.0f*M_PI_F, EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_point_to_single_point_move_hopf_torus) = {
+  ANIM_SO_NUM(double_point_to_single_point_move_hopf_torus),
+  ANIM_SO_NAME(double_point_to_single_point_move_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(double_point_move_hopf_torus) = {
+  ANIM_MO_REF(single_point_to_double_point_move_hopf_torus),
+  ANIM_MO_REF(double_point_move_hopf_torus),
+  ANIM_MO_REF(double_point_to_single_point_move_hopf_torus),
+};
+
+ANIM_PS_DEF(double_point_move_hopf_torus) = {
+  ANIM_PH_NUM(double_point_move_hopf_torus),
+  ANIM_PH_NAME(double_point_move_hopf_torus)
+};
+
+/* Split a single point on the equator into two points, move the two
+   points along a Hopf spiral, and merge the two points again. */
+
+/* Phase 1: Split a single point on the equator into two points on the
+   equator by moving the second point along a Hopf spiral. */
+ANIM_SO_DEF(single_point_to_double_point_move_hopf_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -2.0f*M_PI_F,         EASING_ACCEL, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_double_point_move_hopf_spiral) = {
+  ANIM_SO_NUM(single_point_to_double_point_move_hopf_spiral),
+  ANIM_SO_NAME(single_point_to_double_point_move_hopf_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+/* Phase 2: Move the two points along the Hopf spiral. */
+ANIM_SO_DEF(double_point_move_hopf_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -4.0f*M_PI_F,         EASING_CUBIC, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    2.0f*M_PI_F,          -2.0f*M_PI_F,         EASING_LIN,   /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_point_move_hopf_spiral) = {
+  ANIM_SO_NUM(double_point_move_hopf_spiral),
+  ANIM_SO_NAME(double_point_move_hopf_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 3: Merge the two points on the equator into a single point on the
+   equator by moving the second point along a Hopf spiral. */
+ANIM_SO_DEF(double_point_to_single_point_move_hopf_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    2.0f*M_PI_F,          0.0f,                 EASING_DECEL, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_point_to_single_point_move_hopf_spiral) = {
+  ANIM_SO_NUM(double_point_to_single_point_move_hopf_spiral),
+  ANIM_SO_NAME(double_point_to_single_point_move_hopf_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_point_move_hopf_spiral) = {
+  ANIM_MO_REF(single_point_to_double_point_move_hopf_spiral),
+  ANIM_MO_REF(double_point_move_hopf_spiral),
+  ANIM_MO_REF(double_point_to_single_point_move_hopf_spiral),
+};
+
+ANIM_PS_DEF(double_point_move_hopf_spiral) = {
+  ANIM_PH_NUM(double_point_move_hopf_spiral),
+  ANIM_PH_NAME(double_point_move_hopf_spiral)
+};
+
+ANIMS_M_DEF(single_point) = {
+  ANIM_PS_REF(single_point_rot_z),
+  ANIM_PS_REF(single_point_move_hopf_torus),
+  ANIM_PS_REF(single_point_move_hopf_spiral),
+  ANIM_PS_REF(double_point_rot_z),
+  ANIM_PS_REF(double_point_move_hopf_torus),
+  ANIM_PS_REF(double_point_move_hopf_spiral)
+};
+
+ANIMS_DEF(single_point) = {
+  ANIMS_M_NUM(single_point),
+  ANIMS_M_NAME(single_point)
+};
+
+
+
+/* The set of possible animations for the transition from a single point
+   to a single torus. */
+
+/* Expand a point on the equator to a torus on the equator. */
+ANIM_SO_DEF(single_point_to_single_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_single_torus_rot_z) = {
+  ANIM_SO_NUM(single_point_to_single_torus_rot_z),
+  ANIM_SO_NAME(single_point_to_single_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_single_torus_rot_z) = {
+  ANIM_MO_REF(single_point_to_single_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_point_to_single_torus_rot_z) = {
+  ANIM_PH_NUM(single_point_to_single_torus_rot_z),
+  ANIM_PH_NAME(single_point_to_single_torus_rot_z)
+};
+
+/* Expand a point on the equator to a torus on the equator and rotate around
+   a random axis. */
+ANIM_SO_DEF(single_point_to_single_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_single_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_point_to_single_torus_rot_rnd),
+  ANIM_SO_NAME(single_point_to_single_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_single_torus_rot_rnd) = {
+  ANIM_MO_REF(single_point_to_single_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_point_to_single_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_point_to_single_torus_rot_rnd),
+  ANIM_PH_NAME(single_point_to_single_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(single_point_to_single_torus) = {
+  ANIM_PS_REF(single_point_to_single_torus_rot_z),
+  ANIM_PS_REF(single_point_to_single_torus_rot_rnd)
+};
+
+ANIMS_DEF(single_point_to_single_torus) = {
+  ANIMS_M_NUM(single_point_to_single_torus),
+  ANIMS_M_NAME(single_point_to_single_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single point
+   to a double torus. */
+
+/* Expand the point on the equator to two tori, rotate them around the
+   z axis, and move them to latitude ±45°. */
+ANIM_SO_DEF(single_point_to_double_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 -2.0f*M_PI_F,         EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_double_torus_rot_z) = {
+  ANIM_SO_NUM(single_point_to_double_torus_rot_z),
+  ANIM_SO_NAME(single_point_to_double_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_double_torus_rot_z) = {
+  ANIM_MO_REF(single_point_to_double_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_point_to_double_torus_rot_z) = {
+  ANIM_PH_NUM(single_point_to_double_torus_rot_z),
+  ANIM_PH_NAME(single_point_to_double_torus_rot_z)
+};
+
+/* Expand the point on the equator to two tori, rotate them around a random
+   axis, and move them to latitude ±45°. */
+ANIM_SO_DEF(single_point_to_double_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 -2.0f*M_PI_F,         EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_double_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_point_to_double_torus_rot_rnd),
+  ANIM_SO_NAME(single_point_to_double_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_double_torus_rot_rnd) = {
+  ANIM_MO_REF(single_point_to_double_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_point_to_double_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_point_to_double_torus_rot_rnd),
+  ANIM_PH_NAME(single_point_to_double_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(single_point_to_double_torus) = {
+  ANIM_PS_REF(single_point_to_double_torus_rot_z),
+  ANIM_PS_REF(single_point_to_double_torus_rot_rnd)
+};
+
+ANIMS_DEF(single_point_to_double_torus) = {
+  ANIMS_M_NUM(single_point_to_double_torus),
+  ANIMS_M_NAME(single_point_to_double_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single point
+   to a triple torus. */
+
+/* Expand the point on the equator to three tori, rotate two of them around
+   the z axis, and move them to latitude ±45°. */
+ANIM_SO_DEF(single_point_to_triple_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 -2.0f*M_PI_F,         EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_triple_torus_rot_z) = {
+  ANIM_SO_NUM(single_point_to_triple_torus_rot_z),
+  ANIM_SO_NAME(single_point_to_triple_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_triple_torus_rot_z) = {
+  ANIM_MO_REF(single_point_to_triple_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_point_to_triple_torus_rot_z) = {
+  ANIM_PH_NUM(single_point_to_triple_torus_rot_z),
+  ANIM_PH_NAME(single_point_to_triple_torus_rot_z)
+};
+
+/* Expand the point on the equator to three tori, rotate them around a
+   random axis, and move them to latitude ±45°. */
+ANIM_SO_DEF(single_point_to_triple_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 -2.0f*M_PI_F,         EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_triple_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_point_to_triple_torus_rot_rnd),
+  ANIM_SO_NAME(single_point_to_triple_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_triple_torus_rot_rnd) = {
+  ANIM_MO_REF(single_point_to_triple_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_point_to_triple_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_point_to_triple_torus_rot_rnd),
+  ANIM_PH_NAME(single_point_to_triple_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(single_point_to_triple_torus) = {
+  ANIM_PS_REF(single_point_to_triple_torus_rot_z),
+  ANIM_PS_REF(single_point_to_triple_torus_rot_rnd)
+};
+
+ANIMS_DEF(single_point_to_triple_torus) = {
+  ANIMS_M_NUM(single_point_to_triple_torus),
+  ANIMS_M_NAME(single_point_to_triple_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single point
+   to a single Seifert surface. */
+
+/* Expand a point on the equator to a Seifert surface on the equator. */
+ANIM_SO_DEF(single_point_to_single_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_single_seifert_rot_z) = {
+  ANIM_SO_NUM(single_point_to_single_seifert_rot_z),
+  ANIM_SO_NAME(single_point_to_single_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_single_seifert_rot_z) = {
+  ANIM_MO_REF(single_point_to_single_seifert_rot_z)
+};
+
+ANIM_PS_DEF(single_point_to_single_seifert_rot_z) = {
+  ANIM_PH_NUM(single_point_to_single_seifert_rot_z),
+  ANIM_PH_NAME(single_point_to_single_seifert_rot_z)
+};
+
+/* Expand a point on the equator to a Seifert surface on the equator and
+   rotate around a random axis. */
+ANIM_SO_DEF(single_point_to_single_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_single_seifert_rot_rnd) = {
+  ANIM_SO_NUM(single_point_to_single_seifert_rot_rnd),
+  ANIM_SO_NAME(single_point_to_single_seifert_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_single_seifert_rot_rnd) = {
+  ANIM_MO_REF(single_point_to_single_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(single_point_to_single_seifert_rot_rnd) = {
+  ANIM_PH_NUM(single_point_to_single_seifert_rot_rnd),
+  ANIM_PH_NAME(single_point_to_single_seifert_rot_rnd)
+};
+
+ANIMS_M_DEF(single_point_to_single_seifert) = {
+  ANIM_PS_REF(single_point_to_single_seifert_rot_z),
+  ANIM_PS_REF(single_point_to_single_seifert_rot_rnd)
+};
+
+ANIMS_DEF(single_point_to_single_seifert) = {
+  ANIMS_M_NUM(single_point_to_single_seifert),
+  ANIMS_M_NAME(single_point_to_single_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single point
+   to a triple Seifert surface. */
+
+/* Expand the point at the equator to a Seifert surface on the without
+   rotating it.  Expand the point at the equator to two Seifert surfaces at
+   latitude ±45° and rotate them around the z axis. */
+ANIM_SO_DEF(single_point_to_triple_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/24.0f,        EASING_CUBIC, /* offset */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/24.0f,        EASING_CUBIC, /* offset */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_triple_seifert_rot_z) = {
+  ANIM_SO_NUM(single_point_to_triple_seifert_rot_z),
+  ANIM_SO_NAME(single_point_to_triple_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_triple_seifert_rot_z) = {
+  ANIM_MO_REF(single_point_to_triple_seifert_rot_z)
+};
+
+ANIM_PS_DEF(single_point_to_triple_seifert_rot_z) = {
+  ANIM_PH_NUM(single_point_to_triple_seifert_rot_z),
+  ANIM_PH_NAME(single_point_to_triple_seifert_rot_z)
+};
+
+/* Move the single point at the equator to the south pole along a Hopf
+   spiral, then successively expand three Seifert surfaces from the single
+   point at the south pole and move them to latitudes 70°, 0°, and -70°,
+   respectively, then decrease the point density by a factor of two and move
+   the Seifert surfaces at latitude ±70° to latidude ±45°, all the while
+   rotating all Seifert surfaces around the z axis in opposite directions. */
+
+/* Phase 1: Move the single point at the equator to the south pole along a
+   Hopf Spiral. */
+ANIM_SO_DEF(single_point_to_triple_seifert_rot_z_move_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    -2.0f,                -2.0f,                EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+};
+
+ANIM_MO_DEF(single_point_to_triple_seifert_rot_z_move_spiral) = {
+  ANIM_SO_NUM(single_point_to_triple_seifert_rot_z_move_spiral),
+  ANIM_SO_NAME(single_point_to_triple_seifert_rot_z_move_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  120                                                         /* num_steps */
+};
+
+/* Phase 2: Expand a Seifert surface from the south pole and move it to
+   latitude 70°. */
+ANIM_SO_DEF(single_point_to_triple_seifert_rot_z_move_north_1) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F,               M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_ACCEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_triple_seifert_rot_z_move_north_1) = {
+  ANIM_SO_NUM(single_point_to_triple_seifert_rot_z_move_north_1),
+  ANIM_SO_NAME(single_point_to_triple_seifert_rot_z_move_north_1),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 3: Expand a Seifert surface from the south pole and move it to
+   latitude 0°. */
+ANIM_SO_DEF(single_point_to_triple_seifert_rot_z_move_north_2) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_ACCEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_triple_seifert_rot_z_move_north_2) = {
+  ANIM_SO_NUM(single_point_to_triple_seifert_rot_z_move_north_2),
+  ANIM_SO_NAME(single_point_to_triple_seifert_rot_z_move_north_2),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 4: Expand a Seifert surface from the south pole and move it to
+   latitude -70°. */
+ANIM_SO_DEF(single_point_to_triple_seifert_rot_z_move_north_3) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F,               8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_ACCEL  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_triple_seifert_rot_z_move_north_3) = {
+  ANIM_SO_NUM(single_point_to_triple_seifert_rot_z_move_north_3),
+  ANIM_SO_NAME(single_point_to_triple_seifert_rot_z_move_north_3),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 5: Rotate the three Seifert surfaces for one revolution. */
+ANIM_SO_DEF(single_point_to_triple_seifert_rot_z_linear) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     8.0f*M_PI_F/9.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_LIN    /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_triple_seifert_rot_z_linear) = {
+  ANIM_SO_NUM(single_point_to_triple_seifert_rot_z_linear),
+  ANIM_SO_NAME(single_point_to_triple_seifert_rot_z_linear),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 1: Rotate the three Seifert surfaces for one revolution, move the
+   Seifert surfaces at latitudes ±70° to latitudes ±45°, and decrease the
+   point density by a factor of two. */
+ANIM_SO_DEF(single_point_to_triple_seifert_rot_z_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_DECEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_DECEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_DECEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_DECEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_DECEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_DECEL  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_triple_seifert_rot_z_loosen) = {
+  ANIM_SO_NUM(single_point_to_triple_seifert_rot_z_loosen),
+  ANIM_SO_NAME(single_point_to_triple_seifert_rot_z_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_triple_seifert_move_north) = {
+  ANIM_MO_REF(single_point_to_triple_seifert_rot_z_move_spiral),
+  ANIM_MO_REF(single_point_to_triple_seifert_rot_z_move_north_1),
+  ANIM_MO_REF(single_point_to_triple_seifert_rot_z_move_north_2),
+  ANIM_MO_REF(single_point_to_triple_seifert_rot_z_move_north_3),
+  ANIM_MO_REF(single_point_to_triple_seifert_rot_z_linear),
+  ANIM_MO_REF(single_point_to_triple_seifert_rot_z_loosen)
+};
+
+ANIM_PS_DEF(single_point_to_triple_seifert_move_north) = {
+  ANIM_PH_NUM(single_point_to_triple_seifert_move_north),
+  ANIM_PH_NAME(single_point_to_triple_seifert_move_north)
+};
+
+ANIMS_M_DEF(single_point_to_triple_seifert) = {
+  ANIM_PS_REF(single_point_to_triple_seifert_rot_z),
+  ANIM_PS_REF(single_point_to_triple_seifert_move_north)
+};
+
+ANIMS_DEF(single_point_to_triple_seifert) = {
+  ANIMS_M_NUM(single_point_to_triple_seifert),
+  ANIMS_M_NAME(single_point_to_triple_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single point
+   to a single Hopf torus. */
+
+/* Expand a point on the equator to a Hopf torus on the equator and rotate
+   around a random axis. */
+ANIM_SO_DEF(single_point_to_single_hopf_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_single_hopf_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_point_to_single_hopf_torus_rot_rnd),
+  ANIM_SO_NAME(single_point_to_single_hopf_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_single_hopf_torus_rot_rnd) = {
+  ANIM_MO_REF(single_point_to_single_hopf_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_point_to_single_hopf_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_point_to_single_hopf_torus_rot_rnd),
+  ANIM_PH_NAME(single_point_to_single_hopf_torus_rot_rnd)
+};
+
+/* Expand the point to a loose Hopf torus and increase the point densify of
+   the Hopf torus by a factor of five. */
+
+/* Phase 1: Expand the point on the equator to a loose Hopf torus. */
+ANIM_SO_DEF(single_point_to_single_loose_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_point_to_single_loose_hopf_torus) = {
+  ANIM_SO_NUM(single_point_to_single_loose_hopf_torus),
+  ANIM_SO_NAME(single_point_to_single_loose_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+/* Phase 2: Increase the density of the Hopf torus. */
+ANIM_SO_DEF(single_hopf_torus_to_single_dense_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -2.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/60.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/60.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 2.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_to_single_dense_hopf_torus) = {
+  ANIM_SO_NUM(single_hopf_torus_to_single_dense_hopf_torus),
+  ANIM_SO_NAME(single_hopf_torus_to_single_dense_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_point_to_single_hopf_torus_densify) = {
+  ANIM_MO_REF(single_point_to_single_loose_hopf_torus),
+  ANIM_MO_REF(single_hopf_torus_to_single_dense_hopf_torus)
+};
+
+ANIM_PS_DEF(single_point_to_single_hopf_torus_densify) = {
+  ANIM_PH_NUM(single_point_to_single_hopf_torus_densify),
+  ANIM_PH_NAME(single_point_to_single_hopf_torus_densify)
+};
+
+ANIMS_M_DEF(single_point_to_single_hopf_torus) = {
+  ANIM_PS_REF(single_point_to_single_hopf_torus_rot_rnd),
+  ANIM_PS_REF(single_point_to_single_hopf_torus_densify)
+};
+
+ANIMS_DEF(single_point_to_single_hopf_torus) = {
+  ANIMS_M_NUM(single_point_to_single_hopf_torus),
+  ANIMS_M_NAME(single_point_to_single_hopf_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single point to
+   a single Hopf spiral. */
+
+/* Expand a point on the equator to a Hopf spiral and rotate around a random
+   axis with a certain probability. */
+ANIM_SO_DEF(single_hopf_spiral_to_single_point_expand_sector) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    0.0f,                 2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_single_point_expand_sector) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_single_point_expand_sector),
+  ANIM_SO_NAME(single_hopf_spiral_to_single_point_expand_sector),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_single_point_expand_sector) = {
+  ANIM_MO_REF(single_hopf_spiral_to_single_point_expand_sector)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_single_point_expand_sector) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_single_point_expand_sector),
+  ANIM_PH_NAME(single_hopf_spiral_to_single_point_expand_sector)
+};
+
+ANIMS_M_DEF(single_point_to_single_hopf_spiral) = {
+  ANIM_PS_REF(single_hopf_spiral_to_single_point_expand_sector)
+};
+
+ANIMS_DEF(single_point_to_single_hopf_spiral) = {
+  ANIMS_M_NUM(single_point_to_single_hopf_spiral),
+  ANIMS_M_NAME(single_point_to_single_hopf_spiral)
+};
+
+
+
+/*****************************************************************************
+ * The set of all transformations from a single torus.
+ *****************************************************************************/
+
+/* The set of possible animations for the transition from a single torus
+   to a single point. */
+
+/* Shrink a torus on the equator to a point on the equator. */
+ANIM_SO_DEF(single_torus_to_single_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_point_rot_z) = {
+  ANIM_SO_NUM(single_torus_to_single_point_rot_z),
+  ANIM_SO_NAME(single_torus_to_single_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_single_point_rot_z) = {
+  ANIM_MO_REF(single_torus_to_single_point_rot_z)
+};
+
+ANIM_PS_DEF(single_torus_to_single_point_rot_z) = {
+  ANIM_PH_NUM(single_torus_to_single_point_rot_z),
+  ANIM_PH_NAME(single_torus_to_single_point_rot_z)
+};
+
+/* Shrink a torus on the equator to a point on the equator and rotate around
+   a random axis. */
+ANIM_SO_DEF(single_torus_to_single_point_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_point_rot_rnd) = {
+  ANIM_SO_NUM(single_torus_to_single_point_rot_rnd),
+  ANIM_SO_NAME(single_torus_to_single_point_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_single_point_rot_rnd) = {
+  ANIM_MO_REF(single_torus_to_single_point_rot_rnd)
+};
+
+ANIM_PS_DEF(single_torus_to_single_point_rot_rnd) = {
+  ANIM_PH_NUM(single_torus_to_single_point_rot_rnd),
+  ANIM_PH_NAME(single_torus_to_single_point_rot_rnd)
+};
+
+ANIMS_M_DEF(single_torus_to_single_point) = {
+  ANIM_PS_REF(single_torus_to_single_point_rot_z),
+  ANIM_PS_REF(single_torus_to_single_point_rot_rnd)
+};
+
+ANIMS_DEF(single_torus_to_single_point) = {
+  ANIMS_M_NUM(single_torus_to_single_point),
+  ANIMS_M_NAME(single_torus_to_single_point)
+};
+
+
+
+/* The set of possible animations for a single torus. */
+
+/* Rotate a torus on the equator around the x axis of the total space. */
+ANIM_SO_DEF(single_torus_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_rot_x) = {
+  ANIM_SO_NUM(single_torus_rot_x),
+  ANIM_SO_NAME(single_torus_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_rot_x) = {
+  ANIM_MO_REF(single_torus_rot_x)
+};
+
+ANIM_PS_DEF(single_torus_rot_x) = {
+  ANIM_PH_NUM(single_torus_rot_x),
+  ANIM_PH_NAME(single_torus_rot_x)
+};
+
+/* Rotate a torus on the equator around the z axis. */
+ANIM_SO_DEF(single_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_rot_z) = {
+  ANIM_SO_NUM(single_torus_rot_z),
+  ANIM_SO_NAME(single_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_rot_z) = {
+  ANIM_MO_REF(single_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_torus_rot_z) = {
+  ANIM_PH_NUM(single_torus_rot_z),
+  ANIM_PH_NAME(single_torus_rot_z)
+};
+
+/* Rotate a torus on the equator around a random axis. */
+ANIM_SO_DEF(single_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_torus_rot_rnd),
+  ANIM_SO_NAME(single_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_rot_rnd) = {
+  ANIM_MO_REF(single_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_torus_rot_rnd),
+  ANIM_PH_NAME(single_torus_rot_rnd)
+};
+
+/* Move a torus on the equator up and down to latitude ±80° while rotating
+   it around the z axis and, with a certain probability, around a random
+   axis. */
+ANIM_SO_DEF(single_torus_move) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    1.0f*M_PI_F/18.0f,    17.0f*M_PI_F/18.0f,   EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_move) = {
+  ANIM_SO_NUM(single_torus_move),
+  ANIM_SO_NAME(single_torus_move),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_move) = {
+  ANIM_MO_REF(single_torus_move)
+};
+
+ANIM_PS_DEF(single_torus_move) = {
+  ANIM_PH_NUM(single_torus_move),
+  ANIM_PH_NAME(single_torus_move)
+};
+
+/* Rotate a torus on the equator to a meridian, yielding a parabolic ring
+   cyclide, and increase its density by a factor of three.  Then, rotate
+   it around the x axis of the total space.  Finally, rotate the torus back
+   to the equator and decrease its density by a factor of three. */
+
+/* Phase 1: Rotate the torus by 90° to a meridian and increase its point
+   density by a factor of three. */
+ANIM_SO_DEF(single_torus_to_meridian_torus_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/36.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/36.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_meridian_torus_densify) = {
+  ANIM_SO_NUM(single_torus_to_meridian_torus_densify),
+  ANIM_SO_NAME(single_torus_to_meridian_torus_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+/* Phase 2: Rotate the torus by 360° around the x axis of the total space. */
+ANIM_SO_DEF(single_meridian_torus_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_meridian_torus_rot_x) = {
+  ANIM_SO_NUM(single_meridian_torus_rot_x),
+  ANIM_SO_NAME(single_meridian_torus_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 3: Rotate the meridian torus by 90° to the equator and decrease its
+   point density by a factor of three. */
+ANIM_SO_DEF(single_meridian_torus_to_torus_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 2.0f*M_PI_F/2.0f,
+                                                EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/36.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 2.0f*M_PI_F/2.0f,
+                                                EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/36.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 2.0f*M_PI_F/2.0f,
+                                                EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_meridian_torus_to_torus_loosen) = {
+  ANIM_SO_NUM(single_meridian_torus_to_torus_loosen),
+  ANIM_SO_NAME(single_meridian_torus_to_torus_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_meridian) = {
+  ANIM_MO_REF(single_torus_to_meridian_torus_densify),
+  ANIM_MO_REF(single_meridian_torus_rot_x),
+  ANIM_MO_REF(single_meridian_torus_to_torus_loosen)
+};
+
+ANIM_PS_DEF(single_torus_meridian) = {
+  ANIM_PH_NUM(single_torus_meridian),
+  ANIM_PH_NAME(single_torus_meridian)
+};
+
+ANIMS_M_DEF(single_torus) = {
+  ANIM_PS_REF(single_torus_rot_x),
+  ANIM_PS_REF(single_torus_rot_z),
+  ANIM_PS_REF(single_torus_rot_rnd),
+  ANIM_PS_REF(single_torus_move),
+  ANIM_PS_REF(single_torus_meridian)
+};
+
+ANIMS_DEF(single_torus) = {
+  ANIMS_M_NUM(single_torus),
+  ANIMS_M_NAME(single_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single torus
+   to a double torus. */
+
+/* Split the torus at the equator into a dense torus, then split the torus
+   into two tori, rotate them around the z axis, and move them to latitude
+   ±45°. */
+
+/* Phase 1: Increase the point density by a factor of two. */
+ANIM_SO_DEF(single_torus_to_single_dense_torus_densify_two) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/48.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/48.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_dense_torus_densify_two) = {
+  ANIM_SO_NUM(single_torus_to_single_dense_torus_densify_two),
+  ANIM_SO_NAME(single_torus_to_single_dense_torus_densify_two),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  30                                                          /* num_steps */
+};
+
+/* Phase 2: Split the torus into two tori, rotate them around the z axis,
+   and move them to latitude ±45°. */
+ANIM_SO_DEF(single_dense_torus_to_double_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/48.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_torus_to_double_torus_rot_z) = {
+  ANIM_SO_NUM(single_dense_torus_to_double_torus_rot_z),
+  ANIM_SO_NAME(single_dense_torus_to_double_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_double_torus_rot_z) = {
+  ANIM_MO_REF(single_torus_to_single_dense_torus_densify_two),
+  ANIM_MO_REF(single_dense_torus_to_double_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_torus_to_double_torus_rot_z) = {
+  ANIM_PH_NUM(single_torus_to_double_torus_rot_z),
+  ANIM_PH_NAME(single_torus_to_double_torus_rot_z)
+};
+
+/* Split the torus at the equator into two tori, rotate them around a random
+   axis, and move them to latitude ±45°. */
+ANIM_SO_DEF(single_torus_to_double_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_double_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_torus_to_double_torus_rot_rnd),
+  ANIM_SO_NAME(single_torus_to_double_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_double_torus_rot_rnd) = {
+  ANIM_MO_REF(single_torus_to_double_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_torus_to_double_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_torus_to_double_torus_rot_rnd),
+  ANIM_PH_NAME(single_torus_to_double_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(single_torus_to_double_torus) = {
+  ANIM_PS_REF(single_torus_to_double_torus_rot_z),
+  ANIM_PS_REF(single_torus_to_double_torus_rot_rnd)
+};
+
+ANIMS_DEF(single_torus_to_double_torus) = {
+  ANIMS_M_NUM(single_torus_to_double_torus),
+  ANIMS_M_NAME(single_torus_to_double_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single torus
+   to a triple torus. */
+
+/* Increase the density of the torus at the equator by a factor of three,
+   split the dense torus into three tori, move two of the tori to latitude
+   ±45°, and rotate all three tori around the z axis. */
+
+/* Phase 1: Increase the point density by a factor of three. */
+ANIM_SO_DEF(single_torus_to_single_dense_torus_densify_three) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/36.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/36.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_dense_torus_densify_three) = {
+  ANIM_SO_NUM(single_torus_to_single_dense_torus_densify_three),
+  ANIM_SO_NAME(single_torus_to_single_dense_torus_densify_three),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  30                                                          /* num_steps */
+};
+
+/* Phase 2: Split the torus into three tori, rotate them around the z axis,
+   and move them to latitudes ±45° and 0°. */
+ANIM_SO_DEF(single_dense_torus_to_triple_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/36.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/36.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_torus_to_triple_torus_rot_z) = {
+  ANIM_SO_NUM(single_dense_torus_to_triple_torus_rot_z),
+  ANIM_SO_NAME(single_dense_torus_to_triple_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_triple_torus_rot_z) = {
+  ANIM_MO_REF(single_torus_to_single_dense_torus_densify_three),
+  ANIM_MO_REF(single_dense_torus_to_triple_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_torus_to_triple_torus_rot_z) = {
+  ANIM_PH_NUM(single_torus_to_triple_torus_rot_z),
+  ANIM_PH_NAME(single_torus_to_triple_torus_rot_z)
+};
+
+/* Split the torus at the equator into three tori, rotate them around a
+   random axis, and move them to latitudes ±45° and 0°. */
+ANIM_SO_DEF(single_torus_to_triple_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_triple_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_torus_to_triple_torus_rot_rnd),
+  ANIM_SO_NAME(single_torus_to_triple_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_triple_torus_rot_rnd) = {
+  ANIM_MO_REF(single_torus_to_triple_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_torus_to_triple_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_torus_to_triple_torus_rot_rnd),
+  ANIM_PH_NAME(single_torus_to_triple_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(single_torus_to_triple_torus) = {
+  ANIM_PS_REF(single_torus_to_triple_torus_rot_z),
+  ANIM_PS_REF(single_torus_to_triple_torus_rot_rnd)
+};
+
+ANIMS_DEF(single_torus_to_triple_torus) = {
+  ANIMS_M_NUM(single_torus_to_triple_torus),
+  ANIMS_M_NAME(single_torus_to_triple_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single torus
+   to a single Seifert surface. */
+
+/* Shrink the torus on the equator to a Seifert surface on the equator. */
+ANIM_SO_DEF(single_torus_to_single_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_seifert_rot_z) = {
+  ANIM_SO_NUM(single_torus_to_single_seifert_rot_z),
+  ANIM_SO_NAME(single_torus_to_single_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_single_seifert_rot_z) = {
+  ANIM_MO_REF(single_torus_to_single_seifert_rot_z)
+};
+
+ANIM_PS_DEF(single_torus_to_single_seifert_rot_z) = {
+  ANIM_PH_NUM(single_torus_to_single_seifert_rot_z),
+  ANIM_PH_NAME(single_torus_to_single_seifert_rot_z)
+};
+
+/* Shrink the torus on the equator to a Seifert surface on the equator and
+   rotate it around a random axis. */
+ANIM_SO_DEF(single_torus_to_single_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_seifert_rot_rnd) = {
+  ANIM_SO_NUM(single_torus_to_single_seifert_rot_rnd),
+  ANIM_SO_NAME(single_torus_to_single_seifert_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_single_seifert_rot_rnd) = {
+  ANIM_MO_REF(single_torus_to_single_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(single_torus_to_single_seifert_rot_rnd) = {
+  ANIM_PH_NUM(single_torus_to_single_seifert_rot_rnd),
+  ANIM_PH_NAME(single_torus_to_single_seifert_rot_rnd)
+};
+
+/* Flip half of the torus on the equator around a suitable axis on the
+   equator to form a Seifert surface on the equator. */
+ANIM_SO_DEF(single_torus_to_single_seifert_flip) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { -0.99785892f, 0.06540313f, 0.0f },
+                          0.0f, M_PI_F,         EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_seifert_flip) = {
+  ANIM_SO_NUM(single_torus_to_single_seifert_flip),
+  ANIM_SO_NAME(single_torus_to_single_seifert_flip),
+  0.25f,                                        EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_single_seifert_flip) = {
+  ANIM_MO_REF(single_torus_to_single_seifert_flip)
+};
+
+ANIM_PS_DEF(single_torus_to_single_seifert_flip) = {
+  ANIM_PH_NUM(single_torus_to_single_seifert_flip),
+  ANIM_PH_NAME(single_torus_to_single_seifert_flip)
+};
+
+ANIMS_M_DEF(single_torus_to_single_seifert) = {
+  ANIM_PS_REF(single_torus_to_single_seifert_rot_z),
+  ANIM_PS_REF(single_torus_to_single_seifert_rot_rnd),
+  ANIM_PS_REF(single_torus_to_single_seifert_flip)
+};
+
+ANIMS_DEF(single_torus_to_single_seifert) = {
+  ANIMS_M_NUM(single_torus_to_single_seifert),
+  ANIMS_M_NAME(single_torus_to_single_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single torus
+   to a triple Seifert surface. */
+
+/* Increase the density of the torus at the equator by a factor of three,
+   split the dense torus into three Seifert surfaces of sector 120°,
+   increase their sectors to 180°, and move two of the Seifert surfaces to
+   latitude ±45°, while rotating all three Seifert surfaces around the
+   z axis. */
+
+/* Phase 1: Increase the point density by a factor of three.  Already
+   defined above. */
+
+/* Phase 2: Split the dense torus into three Seifert surfaces of sector
+   120°, increase their sectors to 180°, and move two of the Seifert
+   surfaces to latitude ±45°, while rotating all three Seifert surfaces
+   around the z axis. */
+ANIM_SO_DEF(single_dense_torus_to_triple_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    4.0f*M_PI_F/3.0f,     -M_PI_F,              EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    2.0f*M_PI_F/3.0f,     -M_PI_F,              EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_torus_to_triple_seifert_rot_z) = {
+  ANIM_SO_NUM(single_dense_torus_to_triple_seifert_rot_z),
+  ANIM_SO_NAME(single_dense_torus_to_triple_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_triple_seifert_rot_z) = {
+  ANIM_MO_REF(single_torus_to_single_dense_torus_densify_three),
+  ANIM_MO_REF(single_dense_torus_to_triple_seifert_rot_z)
+};
+
+ANIM_PS_DEF(single_torus_to_triple_seifert_rot_z) = {
+  ANIM_PH_NUM(single_torus_to_triple_seifert_rot_z),
+  ANIM_PH_NAME(single_torus_to_triple_seifert_rot_z)
+};
+
+/* Split the torus at the equator into three Seifert surfaces, rotate them
+   around a random axis, shrink their sectors from 360° to 180°, and move
+   two of the Seifert surfaces from the equator to latitude ±45°. */
+ANIM_SO_DEF(single_torus_to_triple_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_SO_NUM(single_torus_to_triple_seifert_rot_rnd),
+  ANIM_SO_NAME(single_torus_to_triple_seifert_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_MO_REF(single_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(single_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_PH_NUM(single_torus_to_triple_seifert_rot_rnd),
+  ANIM_PH_NAME(single_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIMS_M_DEF(single_torus_to_triple_seifert) = {
+  ANIM_PS_REF(single_torus_to_triple_seifert_rot_z),
+  ANIM_PS_REF(single_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIMS_DEF(single_torus_to_triple_seifert) = {
+  ANIMS_M_NUM(single_torus_to_triple_seifert),
+  ANIMS_M_NAME(single_torus_to_triple_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single torus
+   to a single Hopf torus. */
+
+/* Fill the torus on the equator to a torus on the equator of five times
+   the point density and then deform it to the Hopf torus. */
+
+/* Phase 1: Increase the density of the torus on the equator. */
+ANIM_SO_DEF(single_torus_to_single_dense_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -2.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/60.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/60.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 2.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_dense_torus) = {
+  ANIM_SO_NUM(single_torus_to_single_dense_torus),
+  ANIM_SO_NAME(single_torus_to_single_dense_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Deform the torus on the equator to a Hopf torus. */
+ANIM_SO_DEF(single_dense_torus_to_single_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_torus_to_single_hopf_torus) = {
+  ANIM_SO_NUM(single_dense_torus_to_single_hopf_torus),
+  ANIM_SO_NAME(single_dense_torus_to_single_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_single_hopf_torus_densify) = {
+  ANIM_MO_REF(single_torus_to_single_dense_torus),
+  ANIM_MO_REF(single_dense_torus_to_single_hopf_torus)
+};
+
+ANIM_PS_DEF(single_torus_to_single_hopf_torus_densify) = {
+  ANIM_PH_NUM(single_torus_to_single_hopf_torus_densify),
+  ANIM_PH_NAME(single_torus_to_single_hopf_torus_densify)
+};
+
+ANIMS_M_DEF(single_torus_to_single_hopf_torus) = {
+  ANIM_PS_REF(single_torus_to_single_hopf_torus_densify)
+};
+
+ANIMS_DEF(single_torus_to_single_hopf_torus) = {
+  ANIMS_M_NUM(single_torus_to_single_hopf_torus),
+  ANIMS_M_NAME(single_torus_to_single_hopf_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single torus to
+   a single Hopf spiral. */
+
+/* Increase the point density of a torus on the equator (a collapsed Hopf
+   spiral) by a factor of three, increase its winding number to two and its
+   height to a Hopf spiral.. */
+
+/* Phase 1: Increase the density of the torus by a factor of three. */
+ANIM_SO_DEF(single_torus_to_single_dense_unwound_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/36.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/36.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_dense_unwound_torus) = {
+  ANIM_SO_NUM(single_torus_to_single_dense_unwound_torus),
+  ANIM_SO_NAME(single_torus_to_single_dense_unwound_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Wind the collapsed spiral and expand it. */
+ANIM_SO_DEF(single_dense_unwound_torus_to_single_hopf_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    1.0f,                 2.0f,                 EASING_CUBIC, /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_unwound_torus_to_single_hopf_spiral) = {
+  ANIM_SO_NUM(single_dense_unwound_torus_to_single_hopf_spiral),
+  ANIM_SO_NAME(single_dense_unwound_torus_to_single_hopf_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_single_hopf_spiral_wind) = {
+  ANIM_MO_REF(single_torus_to_single_dense_unwound_torus),
+  ANIM_MO_REF(single_dense_unwound_torus_to_single_hopf_spiral)
+};
+
+ANIM_PS_DEF(single_torus_to_single_hopf_spiral_wind) = {
+  ANIM_PH_NUM(single_torus_to_single_hopf_spiral_wind),
+  ANIM_PH_NAME(single_torus_to_single_hopf_spiral_wind)
+};
+
+/* Decrease the winding number of a Hopf spiral on the equator to three and
+   expand its height. */
+ANIM_SO_DEF(single_torus_to_single_hopf_spiral_unwind) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    3.0f,                 2.0f,                 EASING_CUBIC, /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_torus_to_single_hopf_spiral_unwind) = {
+  ANIM_SO_NUM(single_torus_to_single_hopf_spiral_unwind),
+  ANIM_SO_NAME(single_torus_to_single_hopf_spiral_unwind),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_torus_to_single_hopf_spiral_unwind) = {
+  ANIM_MO_REF(single_torus_to_single_hopf_spiral_unwind)
+};
+
+ANIM_PS_DEF(single_torus_to_single_hopf_spiral_unwind) = {
+  ANIM_PH_NUM(single_torus_to_single_hopf_spiral_unwind),
+  ANIM_PH_NAME(single_torus_to_single_hopf_spiral_unwind)
+};
+
+ANIMS_M_DEF(single_torus_to_single_hopf_spiral) = {
+  ANIM_PS_REF(single_torus_to_single_hopf_spiral_wind),
+  ANIM_PS_REF(single_torus_to_single_hopf_spiral_unwind)
+};
+
+ANIMS_DEF(single_torus_to_single_hopf_spiral) = {
+  ANIMS_M_NUM(single_torus_to_single_hopf_spiral),
+  ANIMS_M_NAME(single_torus_to_single_hopf_spiral)
+};
+
+
+
+/*****************************************************************************
+ * The set of all transformations from a double torus.
+ *****************************************************************************/
+
+/* The set of possible animations for the transition from a double torus
+   to a single point. */
+
+/* Rotate two tori at latitude ±45° around the z axis, shrink them to a
+   single point, and move them to the equator. */
+ANIM_SO_DEF(double_torus_to_single_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    -2.0f*M_PI_F,         0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_single_point_rot_z) = {
+  ANIM_SO_NUM(double_torus_to_single_point_rot_z),
+  ANIM_SO_NAME(double_torus_to_single_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_single_point_rot_z) = {
+  ANIM_MO_REF(double_torus_to_single_point_rot_z)
+};
+
+ANIM_PS_DEF(double_torus_to_single_point_rot_z) = {
+  ANIM_PH_NUM(double_torus_to_single_point_rot_z),
+  ANIM_PH_NAME(double_torus_to_single_point_rot_z)
+};
+
+/* Rotate two tori at latitude ±45° around a random axis, shrink them to a
+   single point, and move them to the equator. */
+ANIM_SO_DEF(double_torus_to_single_point_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    -2.0f*M_PI_F,         0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_single_point_rot_rnd) = {
+  ANIM_SO_NUM(double_torus_to_single_point_rot_rnd),
+  ANIM_SO_NAME(double_torus_to_single_point_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_single_point_rot_rnd) = {
+  ANIM_MO_REF(double_torus_to_single_point_rot_rnd)
+};
+
+ANIM_PS_DEF(double_torus_to_single_point_rot_rnd) = {
+  ANIM_PH_NUM(double_torus_to_single_point_rot_rnd),
+  ANIM_PH_NAME(double_torus_to_single_point_rot_rnd)
+};
+
+ANIMS_M_DEF(double_torus_to_single_point) = {
+  ANIM_PS_REF(double_torus_to_single_point_rot_z),
+  ANIM_PS_REF(double_torus_to_single_point_rot_rnd)
+};
+
+ANIMS_DEF(double_torus_to_single_point) = {
+  ANIMS_M_NUM(double_torus_to_single_point),
+  ANIMS_M_NAME(double_torus_to_single_point)
+};
+
+
+
+/* The set of possible animations for the transition from a double torus
+   to a single torus. */
+
+/* Rotate two tori at latitude ±45° around the z axis and merge them into
+   a dense torus at the equator, then reduce the point density by a factor
+   of two. */
+
+/* Phase 1: Rotate two tori at latitude ±45° around the z axis and merge
+   them into a dense torus at the equator. */
+ANIM_SO_DEF(double_torus_to_single_dense_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/48.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/48.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_single_dense_torus_rot_z) = {
+  ANIM_SO_NUM(double_torus_to_single_dense_torus_rot_z),
+  ANIM_SO_NAME(double_torus_to_single_dense_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 2: Reduce the point density by a factor of two. */
+ANIM_SO_DEF(single_dense_torus_to_single_torus_loosen_two) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/48.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_torus_to_single_torus_loosen_two) = {
+  ANIM_SO_NUM(single_dense_torus_to_single_torus_loosen_two),
+  ANIM_SO_NAME(single_dense_torus_to_single_torus_loosen_two),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  30                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_single_torus_rot_z) = {
+  ANIM_MO_REF(double_torus_to_single_dense_torus_rot_z),
+  ANIM_MO_REF(single_dense_torus_to_single_torus_loosen_two)
+};
+
+ANIM_PS_DEF(double_torus_to_single_torus_rot_z) = {
+  ANIM_PH_NUM(double_torus_to_single_torus_rot_z),
+  ANIM_PH_NAME(double_torus_to_single_torus_rot_z)
+};
+
+/* Rotate two tori at latitude ±45° around a random axis and move them to
+   the equator. */
+ANIM_SO_DEF(double_torus_to_single_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_single_torus_rot_rnd) = {
+  ANIM_SO_NUM(double_torus_to_single_torus_rot_rnd),
+  ANIM_SO_NAME(double_torus_to_single_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_single_torus_rot_rnd) = {
+  ANIM_MO_REF(double_torus_to_single_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(double_torus_to_single_torus_rot_rnd) = {
+  ANIM_PH_NUM(double_torus_to_single_torus_rot_rnd),
+  ANIM_PH_NAME(double_torus_to_single_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(double_torus_to_single_torus) = {
+  ANIM_PS_REF(double_torus_to_single_torus_rot_z),
+  ANIM_PS_REF(double_torus_to_single_torus_rot_rnd)
+};
+
+ANIMS_DEF(double_torus_to_single_torus) = {
+  ANIMS_M_NUM(double_torus_to_single_torus),
+  ANIMS_M_NAME(double_torus_to_single_torus)
+};
+
+
+
+/* The set of possible animations for a double torus. */
+
+/* Rotate two tori at latitude ±45° around the x axis of the total space
+   while rotating them around the z axis in opposite directions. */
+ANIM_SO_DEF(double_torus_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_rot_x) = {
+  ANIM_SO_NUM(double_torus_rot_x),
+  ANIM_SO_NAME(double_torus_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_rot_x) = {
+  ANIM_MO_REF(double_torus_rot_x)
+};
+
+ANIM_PS_DEF(double_torus_rot_x) = {
+  ANIM_PH_NUM(double_torus_rot_x),
+  ANIM_PH_NAME(double_torus_rot_x)
+};
+
+/* Rotate two tori at latitude ±45° by 90° around the y axis, then rotate
+   them by 360° around the x axis of the total space, and then once more by
+   90° around the y axis. */
+
+/* Phase 1: Rotate the tori around the y axis by 90°. */
+ANIM_SO_DEF(double_torus_rot_y_p1) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 1.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 1.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_rot_y_p1) = {
+  ANIM_SO_NUM(double_torus_rot_y_p1),
+  ANIM_SO_NAME(double_torus_rot_y_p1),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+/* Phase 2: Rotate the two rotated tori by 360° around the x axis of the
+   total space. */
+ANIM_SO_DEF(double_torus_rot_y_p2) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 1.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 1.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_rot_y_p2) = {
+  ANIM_SO_NUM(double_torus_rot_y_p2),
+  ANIM_SO_NAME(double_torus_rot_y_p2),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  180                                                         /* num_steps */
+};
+
+/* Phase 3: Rotate the tori around the y axis by an additional 90°. */
+ANIM_SO_DEF(double_torus_rot_y_p3) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 1.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 1.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_rot_y_p3) = {
+  ANIM_SO_NUM(double_torus_rot_y_p3),
+  ANIM_SO_NAME(double_torus_rot_y_p3),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_rot_y) = {
+  ANIM_MO_REF(double_torus_rot_y_p1),
+  ANIM_MO_REF(double_torus_rot_y_p2),
+  ANIM_MO_REF(double_torus_rot_y_p3)
+};
+
+ANIM_PS_DEF(double_torus_rot_y) = {
+  ANIM_PH_NUM(double_torus_rot_y),
+  ANIM_PH_NAME(double_torus_rot_y)
+};
+
+/* Move two tori at latitudes ±45° to latitude ±70° and increase their point
+   density by a factor of two, rotate the tori around a random axis, and
+   move them back to ±45° while decreasing their point density by a factor
+   of two. */
+
+/* Phase 1: Move two tori at latitude ±45° to latitude ±70° and increase
+   their point density by a factor of two. */
+ANIM_SO_DEF(double_torus_rot_rnd_move_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/48.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/48.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/48.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/48.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_rot_rnd_move_densify) = {
+  ANIM_SO_NUM(double_torus_rot_rnd_move_densify),
+  ANIM_SO_NAME(double_torus_rot_rnd_move_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Rotate the two tori by 360° around a random axis. */
+ANIM_SO_DEF(double_torus_rot_rnd_rot) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         M_PI_F/48.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     8.0f*M_PI_F/9.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         M_PI_F/48.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_rot_rnd_rot) = {
+  ANIM_SO_NUM(double_torus_rot_rnd_rot),
+  ANIM_SO_NAME(double_torus_rot_rnd_rot),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 3: Move two tori at latitude ±70° to latitude ±45° and decrease
+   their point density by a factor of two. */
+ANIM_SO_DEF(double_torus_rot_rnd_move_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/48.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/48.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_rot_rnd_move_loosen) = {
+  ANIM_SO_NUM(double_torus_rot_rnd_move_loosen),
+  ANIM_SO_NAME(double_torus_rot_rnd_move_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_rot_rnd) = {
+  ANIM_MO_REF(double_torus_rot_rnd_move_densify),
+  ANIM_MO_REF(double_torus_rot_rnd_rot),
+  ANIM_MO_REF(double_torus_rot_rnd_move_loosen)
+};
+
+ANIM_PS_DEF(double_torus_rot_rnd) = {
+  ANIM_PH_NUM(double_torus_rot_rnd),
+  ANIM_PH_NAME(double_torus_rot_rnd)
+};
+
+/* Move two tori at latitude ±45° to latitude ±5°, to latitude ±85° and
+   back to latitude ±45° while rotating them around the z axis and, with
+   a certain probability, around a random axis. */
+ANIM_SO_DEF(double_torus_move) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/18.0f,    M_PI_F/18.0f,         EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    10.0f*M_PI_F/18.0f,   17.0f*M_PI_F/18.0f,   EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_move) = {
+  ANIM_SO_NUM(double_torus_move),
+  ANIM_SO_NAME(double_torus_move),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_move) = {
+  ANIM_MO_REF(double_torus_move)
+};
+
+ANIM_PS_DEF(double_torus_move) = {
+  ANIM_PH_NUM(double_torus_move),
+  ANIM_PH_NAME(double_torus_move)
+};
+
+ANIMS_M_DEF(double_torus) = {
+  ANIM_PS_REF(double_torus_rot_x),
+  ANIM_PS_REF(double_torus_rot_y),
+  ANIM_PS_REF(double_torus_rot_rnd),
+  ANIM_PS_REF(double_torus_move)
+};
+
+ANIMS_DEF(double_torus) = {
+  ANIMS_M_NUM(double_torus),
+  ANIMS_M_NAME(double_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a double torus
+   to a triple torus. */
+
+/* Rotate the three tori around the z axis, split the torus at the equator
+   into two tori, and move them to latitude ±45°. */
+ANIM_SO_DEF(double_torus_to_triple_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/12.0f,         M_PI_F/12.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_triple_torus_rot_z) = {
+  ANIM_SO_NUM(double_torus_to_triple_torus_rot_z),
+  ANIM_SO_NAME(double_torus_to_triple_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_triple_torus_rot_z) = {
+  ANIM_MO_REF(double_torus_to_triple_torus_rot_z)
+};
+
+ANIM_PS_DEF(double_torus_to_triple_torus_rot_z) = {
+  ANIM_PH_NUM(double_torus_to_triple_torus_rot_z),
+  ANIM_PH_NAME(double_torus_to_triple_torus_rot_z)
+};
+
+/* Rotate two tori at around a random axis, split the tori at latitude ±45°
+   into two tori, and move them to the equator. */
+ANIM_SO_DEF(double_torus_to_triple_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_triple_torus_rot_rnd) = {
+  ANIM_SO_NUM(double_torus_to_triple_torus_rot_rnd),
+  ANIM_SO_NAME(double_torus_to_triple_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_triple_torus_rot_rnd) = {
+  ANIM_MO_REF(double_torus_to_triple_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(double_torus_to_triple_torus_rot_rnd) = {
+  ANIM_PH_NUM(double_torus_to_triple_torus_rot_rnd),
+  ANIM_PH_NAME(double_torus_to_triple_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(double_torus_to_triple_torus) = {
+  ANIM_PS_REF(double_torus_to_triple_torus_rot_z),
+  ANIM_PS_REF(double_torus_to_triple_torus_rot_rnd)
+};
+
+ANIMS_DEF(double_torus_to_triple_torus) = {
+  ANIMS_M_NUM(double_torus_to_triple_torus),
+  ANIMS_M_NAME(double_torus_to_triple_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a double torus
+   to a single Seifert surface. */
+
+/* Shrink the two tori from 360° sectors to 90° sectors, join them at the
+   equator, and reduce the point density by a factor of two. */
+
+/* Phase 1: Shrink the two tori from 360° sectors to 90° sectors and join
+   them at the equator. */
+ANIM_SO_DEF(double_torus_to_single_dense_seifert_two_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/2.0f,         -49.0f*M_PI_F/96.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F/2.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/2.0f,         -51.0f*M_PI_F/96.0f,  EASING_CUBIC, /* offset */
+    -2.0f*M_PI_F,         -M_PI_F/2.0f,         EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_single_dense_seifert_two_rot_z) = {
+  ANIM_SO_NUM(double_torus_to_single_dense_seifert_two_rot_z),
+  ANIM_SO_NAME(double_torus_to_single_dense_seifert_two_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 2: Reduce the point density by a factor of two. */
+ANIM_SO_DEF(single_dense_seifert_two_to_single_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    95.0f*M_PI_F/96.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    97.0f*M_PI_F/96.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_seifert_two_to_single_seifert_rot_z) = {
+  ANIM_SO_NUM(single_dense_seifert_two_to_single_seifert_rot_z),
+  ANIM_SO_NAME(single_dense_seifert_two_to_single_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  30                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_single_seifert_rot_z) = {
+  ANIM_MO_REF(double_torus_to_single_dense_seifert_two_rot_z),
+  ANIM_MO_REF(single_dense_seifert_two_to_single_seifert_rot_z)
+};
+
+ANIM_PS_DEF(double_torus_to_single_seifert_rot_z) = {
+  ANIM_PH_NUM(double_torus_to_single_seifert_rot_z),
+  ANIM_PH_NAME(double_torus_to_single_seifert_rot_z)
+};
+
+/* Shrink the two tori from 360° sectors to 180° sectors, reduce their
+   point point density by a factor of two, interleave them at the
+   equator, and rotate around a random axis. */
+ANIM_SO_DEF(double_torus_to_single_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    13.0f*M_PI_F/12.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    13.0f*M_PI_F/12.0f,   25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_single_seifert_rot_rnd) = {
+  ANIM_SO_NUM(double_torus_to_single_seifert_rot_rnd),
+  ANIM_SO_NAME(double_torus_to_single_seifert_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_single_seifert_rot_rnd) = {
+  ANIM_MO_REF(double_torus_to_single_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(double_torus_to_single_seifert_rot_rnd) = {
+  ANIM_PH_NUM(double_torus_to_single_seifert_rot_rnd),
+  ANIM_PH_NAME(double_torus_to_single_seifert_rot_rnd)
+};
+
+ANIMS_M_DEF(double_torus_to_single_seifert) = {
+  ANIM_PS_REF(double_torus_to_single_seifert_rot_z),
+  ANIM_PS_REF(double_torus_to_single_seifert_rot_rnd)
+};
+
+ANIMS_DEF(double_torus_to_single_seifert) = {
+  ANIMS_M_NUM(double_torus_to_single_seifert),
+  ANIMS_M_NAME(double_torus_to_single_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a double torus
+   to a triple Seifert surface. */
+
+/* Rotate the two tori around the z axis, split off two tori from the tori
+   at latitide ±45°, decrease their sector from 360° to 180°, and move them
+   to the equator. */
+ANIM_SO_DEF(double_torus_to_triple_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, M_PI_F, 0.0f,         EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, M_PI_F/2.0f, 0.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, M_PI_F/2.0f, 0.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, M_PI_F, 0.0f,        EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_triple_seifert_rot_z) = {
+  ANIM_SO_NUM(double_torus_to_triple_seifert_rot_z),
+  ANIM_SO_NAME(double_torus_to_triple_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_triple_seifert_rot_z) = {
+  ANIM_MO_REF(double_torus_to_triple_seifert_rot_z)
+};
+
+ANIM_PS_DEF(double_torus_to_triple_seifert_rot_z) = {
+  ANIM_PH_NUM(double_torus_to_triple_seifert_rot_z),
+  ANIM_PH_NAME(double_torus_to_triple_seifert_rot_z)
+};
+
+/* Split off two tori from the tori at latitude ±45°, decrease their sector
+   from 360° to 180°, move them to the equator, and rotate all Seifert
+   surfaces around a random axis with a certain probability. */
+ANIM_SO_DEF(double_torus_to_triple_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_SO_NUM(double_torus_to_triple_seifert_rot_rnd),
+  ANIM_SO_NAME(double_torus_to_triple_seifert_rot_rnd),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_MO_REF(double_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(double_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_PH_NUM(double_torus_to_triple_seifert_rot_rnd),
+  ANIM_PH_NAME(double_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIMS_M_DEF(double_torus_to_triple_seifert) = {
+  ANIM_PS_REF(double_torus_to_triple_seifert_rot_z),
+  ANIM_PS_REF(double_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIMS_DEF(double_torus_to_triple_seifert) = {
+  ANIMS_M_NUM(double_torus_to_triple_seifert),
+  ANIMS_M_NAME(double_torus_to_triple_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a double torus
+   to a Hopf torus. */
+
+/* Decrease the density of the two tori by a factor of two, deform them to
+   an interleaved Hopf torus, and increase the density of the Hopf torus by
+   a factor of five. */
+
+/* Phase 1: Decrease the density of the tori at ±45°. */
+ANIM_SO_DEF(double_torus_to_loose_double_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/12.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/12.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/12.0f,         M_PI_F/12.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_loose_double_torus) = {
+  ANIM_SO_NUM(double_torus_to_loose_double_torus),
+  ANIM_SO_NAME(double_torus_to_loose_double_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Deform the two to loose tori an interleaved Hopf torus. */
+ANIM_SO_DEF(loose_double_torus_to_loose_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/12.0f,         M_PI_F/12.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(loose_double_torus_to_loose_hopf_torus) = {
+  ANIM_SO_NUM(loose_double_torus_to_loose_hopf_torus),
+  ANIM_SO_NAME(loose_double_torus_to_loose_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+/* Phase 3: Increase the density of the Hopf torus by a factor of five. */
+ANIM_SO_DEF(loose_hopf_torus_to_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -2.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/60.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/60.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 2.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(loose_hopf_torus_to_hopf_torus) = {
+  ANIM_SO_NUM(loose_hopf_torus_to_hopf_torus),
+  ANIM_SO_NAME(loose_hopf_torus_to_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_single_hopf_torus_densify) = {
+  ANIM_MO_REF(double_torus_to_loose_double_torus),
+  ANIM_MO_REF(loose_double_torus_to_loose_hopf_torus),
+  ANIM_MO_REF(loose_hopf_torus_to_hopf_torus)
+};
+
+ANIM_PS_DEF(double_torus_to_single_hopf_torus_densify) = {
+  ANIM_PH_NUM(double_torus_to_single_hopf_torus_densify),
+  ANIM_PH_NAME(double_torus_to_single_hopf_torus_densify)
+};
+
+ANIMS_M_DEF(double_torus_to_single_hopf_torus) = {
+  ANIM_PS_REF(double_torus_to_single_hopf_torus_densify)
+};
+
+ANIMS_DEF(double_torus_to_single_hopf_torus) = {
+  ANIMS_M_NUM(double_torus_to_single_hopf_torus),
+  ANIMS_M_NAME(double_torus_to_single_hopf_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a double torus
+   to a single Hopf spiral. */
+
+/* Deform two tori at latitiude ±45° to two Hopf spirals, merge them, and
+   increase the point density by a factor of three halves. */
+
+/* Phase 1: Deform two tori at latitiude ±45° to two Hopf spirals and merge
+   them. */
+ANIM_SO_DEF(double_torus_to_single_loose_hopf_spiral_merge) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    -M_PI_F/4.0f,         0.0f,                 EASING_CUBIC, /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    M_PI_F/4.0f,          0.0f,                 EASING_CUBIC, /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(double_torus_to_single_loose_hopf_spiral_merge) = {
+  ANIM_SO_NUM(double_torus_to_single_loose_hopf_spiral_merge),
+  ANIM_SO_NAME(double_torus_to_single_loose_hopf_spiral_merge),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 2: Increase the density of the Hopf spiral by a factor of three
+   halves. */
+ANIM_SO_DEF(single_loose_hopf_spiral_to_single_hopf_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -23.0f*M_PI_F/24.0f,  -35.0f*M_PI_F/36.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -23.0f*M_PI_F/24.0f,  -34.0f*M_PI_F/36.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_loose_hopf_spiral_to_single_hopf_spiral) = {
+  ANIM_SO_NUM(single_loose_hopf_spiral_to_single_hopf_spiral),
+  ANIM_SO_NAME(single_loose_hopf_spiral_to_single_hopf_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(double_torus_to_single_hopf_spiral_merge) = {
+  ANIM_MO_REF(double_torus_to_single_loose_hopf_spiral_merge),
+  ANIM_MO_REF(single_loose_hopf_spiral_to_single_hopf_spiral)
+};
+
+ANIM_PS_DEF(double_torus_to_single_hopf_spiral_merge) = {
+  ANIM_PH_NUM(double_torus_to_single_hopf_spiral_merge),
+  ANIM_PH_NAME(double_torus_to_single_hopf_spiral_merge)
+};
+
+ANIMS_M_DEF(double_torus_to_single_hopf_spiral) = {
+  ANIM_PS_REF(double_torus_to_single_hopf_spiral_merge)
+};
+
+ANIMS_DEF(double_torus_to_single_hopf_spiral) = {
+  ANIMS_M_NUM(double_torus_to_single_hopf_spiral),
+  ANIMS_M_NAME(double_torus_to_single_hopf_spiral)
+};
+
+
+
+/*****************************************************************************
+ * The set of all transformations from a triple torus.
+ *****************************************************************************/
+
+/* The set of possible animations for the transition from a triple torus
+   to a single point. */
+
+/* Rotate two tori at latitude ±45° around the z axis, shrink them to a
+   single point, and move them to the equator.  Shrink the torus on the
+   equator to a single point without rotating it. */
+ANIM_SO_DEF(triple_torus_to_single_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    -2.0f*M_PI_F,         0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_single_point_rot_z) = {
+  ANIM_SO_NUM(triple_torus_to_single_point_rot_z),
+  ANIM_SO_NAME(triple_torus_to_single_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_single_point_rot_z) = {
+  ANIM_MO_REF(triple_torus_to_single_point_rot_z)
+};
+
+ANIM_PS_DEF(triple_torus_to_single_point_rot_z) = {
+  ANIM_PH_NUM(triple_torus_to_single_point_rot_z),
+  ANIM_PH_NAME(triple_torus_to_single_point_rot_z)
+};
+
+/* Rotate three tori around a random axis, move the two tori at latitude
+   ±45° to the equator, and shrink all three tori to a single point. */
+ANIM_SO_DEF(triple_torus_to_single_point_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    -2.0f*M_PI_F,         0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_single_point_rot_rnd) = {
+  ANIM_SO_NUM(triple_torus_to_single_point_rot_rnd),
+  ANIM_SO_NAME(triple_torus_to_single_point_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_single_point_rot_rnd) = {
+  ANIM_MO_REF(triple_torus_to_single_point_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_torus_to_single_point_rot_rnd) = {
+  ANIM_PH_NUM(triple_torus_to_single_point_rot_rnd),
+  ANIM_PH_NAME(triple_torus_to_single_point_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_torus_to_single_point) = {
+  ANIM_PS_REF(triple_torus_to_single_point_rot_z),
+  ANIM_PS_REF(triple_torus_to_single_point_rot_rnd)
+};
+
+ANIMS_DEF(triple_torus_to_single_point) = {
+  ANIMS_M_NUM(triple_torus_to_single_point),
+  ANIMS_M_NAME(triple_torus_to_single_point)
+};
+
+
+
+/* The set of possible animations for the transition from a triple torus
+   to a single torus. */
+
+/* Rotate two tori at latitude ±45° around the z axis and interleave them
+   into a dense torus at the equator, then reduce the point density by a
+   factor of three. */
+
+/* Phase 1: Rotate two tori at latitude ±45° around the z axis and merge
+   them into a dense torus at the equator. */
+ANIM_SO_DEF(triple_torus_to_single_dense_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/36.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/36.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_single_dense_torus_rot_z) = {
+  ANIM_SO_NUM(triple_torus_to_single_dense_torus_rot_z),
+  ANIM_SO_NAME(triple_torus_to_single_dense_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 2: Reduce the point density by a factor of three. */
+ANIM_SO_DEF(single_dense_torus_to_single_torus_loosen_three) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/36.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/36.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_torus_to_single_torus_loosen_three) = {
+  ANIM_SO_NUM(single_dense_torus_to_single_torus_loosen_three),
+  ANIM_SO_NAME(single_dense_torus_to_single_torus_loosen_three),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  30                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_single_torus_rot_z) = {
+  ANIM_MO_REF(triple_torus_to_single_dense_torus_rot_z),
+  ANIM_MO_REF(single_dense_torus_to_single_torus_loosen_three)
+};
+
+ANIM_PS_DEF(triple_torus_to_single_torus_rot_z) = {
+  ANIM_PH_NUM(triple_torus_to_single_torus_rot_z),
+  ANIM_PH_NAME(triple_torus_to_single_torus_rot_z)
+};
+
+/* Rotate three tori around a random axis and move the tori at latitudes
+   ±45° to the equator. */
+ANIM_SO_DEF(triple_torus_to_single_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_single_torus_rot_rnd) = {
+  ANIM_SO_NUM(triple_torus_to_single_torus_rot_rnd),
+  ANIM_SO_NAME(triple_torus_to_single_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_single_torus_rot_rnd) = {
+  ANIM_MO_REF(triple_torus_to_single_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_torus_to_single_torus_rot_rnd) = {
+  ANIM_PH_NUM(triple_torus_to_single_torus_rot_rnd),
+  ANIM_PH_NAME(triple_torus_to_single_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_torus_to_single_torus) = {
+  ANIM_PS_REF(triple_torus_to_single_torus_rot_z),
+  ANIM_PS_REF(triple_torus_to_single_torus_rot_rnd)
+};
+
+ANIMS_DEF(triple_torus_to_single_torus) = {
+  ANIMS_M_NUM(triple_torus_to_single_torus),
+  ANIMS_M_NAME(triple_torus_to_single_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a triple torus
+   to a double torus. */
+
+/* Rotate the three tori around the z axis, split the torus at the equator
+   into two tori, and move them to latitude ±45°. */
+ANIM_SO_DEF(triple_torus_to_double_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/12.0f,         M_PI_F/12.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_double_torus_rot_z) = {
+  ANIM_SO_NUM(triple_torus_to_double_torus_rot_z),
+  ANIM_SO_NAME(triple_torus_to_double_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_double_torus_rot_z) = {
+  ANIM_MO_REF(triple_torus_to_double_torus_rot_z)
+};
+
+ANIM_PS_DEF(triple_torus_to_double_torus_rot_z) = {
+  ANIM_PH_NUM(triple_torus_to_double_torus_rot_z),
+  ANIM_PH_NAME(triple_torus_to_double_torus_rot_z)
+};
+
+/* Rotate three tori at around a random axis, split the torus at the
+   equator into two tori, and move them to latitude ±45°. */
+ANIM_SO_DEF(triple_torus_to_double_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_double_torus_rot_rnd) = {
+  ANIM_SO_NUM(triple_torus_to_double_torus_rot_rnd),
+  ANIM_SO_NAME(triple_torus_to_double_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_double_torus_rot_rnd) = {
+  ANIM_MO_REF(triple_torus_to_double_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_torus_to_double_torus_rot_rnd) = {
+  ANIM_PH_NUM(triple_torus_to_double_torus_rot_rnd),
+  ANIM_PH_NAME(triple_torus_to_double_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_torus_to_double_torus) = {
+  ANIM_PS_REF(triple_torus_to_double_torus_rot_z),
+  ANIM_PS_REF(triple_torus_to_double_torus_rot_rnd)
+};
+
+ANIMS_DEF(triple_torus_to_double_torus) = {
+  ANIMS_M_NUM(triple_torus_to_double_torus),
+  ANIMS_M_NAME(triple_torus_to_double_torus)
+};
+
+
+
+/* The set of possible animations for a triple torus. */
+
+/* Rotate three tori at latitudes ±45° and 0° around the x axis of the
+   total space while rotating them around the z axis in different
+   directions. */
+ANIM_SO_DEF(triple_torus_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_rot_x) = {
+  ANIM_SO_NUM(triple_torus_rot_x),
+  ANIM_SO_NAME(triple_torus_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_rot_x) = {
+  ANIM_MO_REF(triple_torus_rot_x)
+};
+
+ANIM_PS_DEF(triple_torus_rot_x) = {
+  ANIM_PH_NUM(triple_torus_rot_x),
+  ANIM_PH_NAME(triple_torus_rot_x)
+};
+
+/* Rotate the three tori to a a vertical orientation, yielding a parabolic
+   ring cyclide and two interlocking tori, and increase their density by a
+   factor of three.  Then, rotate them around the x axis of the total space.
+   Next, move the two tori not on the equator between latitudes 1° and ±89°,
+   showing that two solid interlocking tori fill up the complete S³.
+   Finally, rotate the tori back to 0° and ±45° and decrease their density
+   by a factor of three. */
+
+/* Phase 1: Rotate the tori by 90° to a vertical position, increase their
+   point density by a factor of three, and rotate them around the x axis
+   of the total space by 90°. */
+ANIM_SO_DEF(triple_torus_to_vertical_triple_torus_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/36.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/36.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/36.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/36.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/36.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/36.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_vertical_triple_torus_densify) = {
+  ANIM_SO_NUM(triple_torus_to_vertical_triple_torus_densify),
+  ANIM_SO_NAME(triple_torus_to_vertical_triple_torus_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { -1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,     EASING_CUBIC, /* rot_space */
+  120                                                         /* num_steps */
+};
+
+/* Phase 2: Rotate the three tori by 360° around the x axis of the total
+   space. */
+ANIM_SO_DEF(vertical_triple_torus_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(vertical_triple_torus_rot_x) = {
+  ANIM_SO_NUM(vertical_triple_torus_rot_x),
+  ANIM_SO_NAME(vertical_triple_torus_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 5.0f*M_PI_F/2.0f,
+                                                EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 3: Move the two tori at latitides ±45° between latitudes 1° and
+   ±89°. */
+ANIM_SO_DEF(vertical_triple_torus_move_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    179.0f*M_PI_F/180.0f, 91.0f*M_PI_F/180.0f,  EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    1.0f*M_PI_F/180.0f,   89.0f*M_PI_F/180.0f,  EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(vertical_triple_torus_move_z) = {
+  ANIM_SO_NUM(vertical_triple_torus_move_z),
+  ANIM_SO_NAME(vertical_triple_torus_move_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 4: Rotate the tori back to 0° and ±45° and decrease their density
+   by a factor of three. */
+ANIM_SO_DEF(vertical_triple_torus_to_triple_torus_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/36.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/36.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/36.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/36.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/36.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/36.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F,  EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(vertical_triple_torus_to_triple_torus_loosen) = {
+  ANIM_SO_NUM(vertical_triple_torus_to_triple_torus_loosen),
+  ANIM_SO_NAME(vertical_triple_torus_to_triple_torus_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,     EASING_CUBIC, /* rot_space */
+  120                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_three_sphere) = {
+  ANIM_MO_REF(triple_torus_to_vertical_triple_torus_densify),
+  ANIM_MO_REF(vertical_triple_torus_rot_x),
+  ANIM_MO_REF(vertical_triple_torus_move_z),
+  ANIM_MO_REF(vertical_triple_torus_to_triple_torus_loosen)
+};
+
+ANIM_PS_DEF(triple_torus_three_sphere) = {
+  ANIM_PH_NUM(triple_torus_three_sphere),
+  ANIM_PH_NAME(triple_torus_three_sphere)
+};
+
+/* Move the two tori at latitudes ±45° to latitude ±70° and increase the
+   point density of all three tori by a factor of two, rotate the tori
+   around a random axis, move the two tori at latitude ±70° back to ±45°,
+   and decrease the point density of all three tori by a factor of two. */
+
+/* Phase 1: Move two tori at latitude ±45° to latitude ±70° and increase
+   the point density of all three tori by a factor of two. */
+ANIM_SO_DEF(triple_torus_rot_rnd_move_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/48.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/48.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/48.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/48.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F/48.0f,        EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/48.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_rot_rnd_move_densify) = {
+  ANIM_SO_NUM(triple_torus_rot_rnd_move_densify),
+  ANIM_SO_NAME(triple_torus_rot_rnd_move_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Rotate the two tori by 360° around a random axis. */
+ANIM_SO_DEF(triple_torus_rot_rnd_rot) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         M_PI_F/48.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         M_PI_F/48.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     8.0f*M_PI_F/9.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         M_PI_F/48.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_rot_rnd_rot) = {
+  ANIM_SO_NUM(triple_torus_rot_rnd_rot),
+  ANIM_SO_NAME(triple_torus_rot_rnd_rot),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 3: Move the two tori at latitude ±70° to latitude ±45° and
+   decrease the point density of all three tori by a factor of two. */
+ANIM_SO_DEF(triple_torus_rot_rnd_move_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/48.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/48.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/48.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_rot_rnd_move_loosen) = {
+  ANIM_SO_NUM(triple_torus_rot_rnd_move_loosen),
+  ANIM_SO_NAME(triple_torus_rot_rnd_move_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_rot_rnd) = {
+  ANIM_MO_REF(triple_torus_rot_rnd_move_densify),
+  ANIM_MO_REF(triple_torus_rot_rnd_rot),
+  ANIM_MO_REF(triple_torus_rot_rnd_move_loosen)
+};
+
+ANIM_PS_DEF(triple_torus_rot_rnd) = {
+  ANIM_PH_NUM(triple_torus_rot_rnd),
+  ANIM_PH_NAME(triple_torus_rot_rnd)
+};
+
+/* Move the two tori at latitude ±45° to latitude ±5°, to latitude ±85° and
+   back to latitude ±45° while rotating all three tori around the z axis
+   and, with a certain probability, around a random axis. */
+ANIM_SO_DEF(triple_torus_move) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/18.0f,         8.0f*M_PI_F/18.0f,    EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    17.0f*M_PI_F/18.0f,   10.0f*M_PI_F/18.0f,   EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_move) = {
+  ANIM_SO_NUM(triple_torus_move),
+  ANIM_SO_NAME(triple_torus_move),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_move) = {
+  ANIM_MO_REF(triple_torus_move)
+};
+
+ANIM_PS_DEF(triple_torus_move) = {
+  ANIM_PH_NUM(triple_torus_move),
+  ANIM_PH_NAME(triple_torus_move)
+};
+
+ANIMS_M_DEF(triple_torus) = {
+  ANIM_PS_REF(triple_torus_rot_x),
+  ANIM_PS_REF(triple_torus_three_sphere),
+  ANIM_PS_REF(triple_torus_rot_rnd),
+  ANIM_PS_REF(triple_torus_move)
+};
+
+ANIMS_DEF(triple_torus) = {
+  ANIMS_M_NUM(triple_torus),
+  ANIMS_M_NAME(triple_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a triple torus
+   to a single Seifert surface. */
+
+/* Shrink the three tori from 360° sectors to 60° sectors, join them at the
+   equator, and reduce the point density by a factor of three. */
+
+/* Phase 1: Shrink the three tori from 360° sectors to 60° sectors and join
+   them at the equator. */
+ANIM_SO_DEF(triple_torus_to_single_dense_seifert_three_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -3.0f*M_PI_F/2.0f,    -25.0f*M_PI_F/72.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F/3.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -3.0f*M_PI_F/2.0f,    -49.0f*M_PI_F/72.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F/3.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -3.0f*M_PI_F/2.0f,    -73.0f*M_PI_F/72.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F/3.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_single_dense_seifert_three_rot_z) = {
+  ANIM_SO_NUM(triple_torus_to_single_dense_seifert_three_rot_z),
+  ANIM_SO_NAME(triple_torus_to_single_dense_seifert_three_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 2: Reduce the point density by a factor of three. */
+ANIM_SO_DEF(single_dense_seifert_three_to_single_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    71.0f*M_PI_F/72.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    73.0f*M_PI_F/72.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_seifert_three_to_single_seifert_rot_z) = {
+  ANIM_SO_NUM(single_dense_seifert_three_to_single_seifert_rot_z),
+  ANIM_SO_NAME(single_dense_seifert_three_to_single_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  30                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_single_seifert_rot_z) = {
+  ANIM_MO_REF(triple_torus_to_single_dense_seifert_three_rot_z),
+  ANIM_MO_REF(single_dense_seifert_three_to_single_seifert_rot_z)
+};
+
+ANIM_PS_DEF(triple_torus_to_single_seifert_rot_z) = {
+  ANIM_PH_NUM(triple_torus_to_single_seifert_rot_z),
+  ANIM_PH_NAME(triple_torus_to_single_seifert_rot_z)
+};
+
+/* Shrink the three tori from 360° sectors to 180° sectors, reduce their
+   point point density by a factor of three, interleave them at the
+   equator, and rotate around a random axis. */
+ANIM_SO_DEF(triple_torus_to_single_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    13.0f*M_PI_F/12.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    14.0f*M_PI_F/12.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    13.0f*M_PI_F/12.0f,   25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    14.0f*M_PI_F/12.0f,   25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    13.0f*M_PI_F/12.0f,   26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    14.0f*M_PI_F/12.0f,   26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_single_seifert_rot_rnd) = {
+  ANIM_SO_NUM(triple_torus_to_single_seifert_rot_rnd),
+  ANIM_SO_NAME(triple_torus_to_single_seifert_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_single_seifert_rot_rnd) = {
+  ANIM_MO_REF(triple_torus_to_single_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_torus_to_single_seifert_rot_rnd) = {
+  ANIM_PH_NUM(triple_torus_to_single_seifert_rot_rnd),
+  ANIM_PH_NAME(triple_torus_to_single_seifert_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_torus_to_single_seifert) = {
+  ANIM_PS_REF(triple_torus_to_single_seifert_rot_z),
+  ANIM_PS_REF(triple_torus_to_single_seifert_rot_rnd)
+};
+
+ANIMS_DEF(triple_torus_to_single_seifert) = {
+  ANIMS_M_NUM(triple_torus_to_single_seifert),
+  ANIMS_M_NAME(triple_torus_to_single_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a triple torus
+   to a triple Seifert surface. */
+
+/* Rotate the three tori around the z axis and decrease their sector from
+   360° to 180°. */
+ANIM_SO_DEF(triple_torus_to_triple_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, -M_PI_F/2.0f, 2.0f*M_PI_F,
+                                                EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, -M_PI_F/2.0f, 2.0f*M_PI_F,
+                                                EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_triple_seifert_rot_z) = {
+  ANIM_SO_NUM(triple_torus_to_triple_seifert_rot_z),
+  ANIM_SO_NAME(triple_torus_to_triple_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_triple_seifert_rot_z) = {
+  ANIM_MO_REF(triple_torus_to_triple_seifert_rot_z)
+};
+
+ANIM_PS_DEF(triple_torus_to_triple_seifert_rot_z) = {
+  ANIM_PH_NUM(triple_torus_to_triple_seifert_rot_z),
+  ANIM_PH_NAME(triple_torus_to_triple_seifert_rot_z)
+};
+
+/* Decrease the sector of the tori from 360° to 180° and rotate them around
+   a random axis with a certain probability. */
+ANIM_SO_DEF(triple_torus_to_triple_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_SO_NUM(triple_torus_to_triple_seifert_rot_rnd),
+  ANIM_SO_NAME(triple_torus_to_triple_seifert_rot_rnd),
+  0.75f,                                        EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_MO_REF(triple_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_torus_to_triple_seifert_rot_rnd) = {
+  ANIM_PH_NUM(triple_torus_to_triple_seifert_rot_rnd),
+  ANIM_PH_NAME(triple_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_torus_to_triple_seifert) = {
+  ANIM_PS_REF(triple_torus_to_triple_seifert_rot_z),
+  ANIM_PS_REF(triple_torus_to_triple_seifert_rot_rnd)
+};
+
+ANIMS_DEF(triple_torus_to_triple_seifert) = {
+  ANIMS_M_NUM(triple_torus_to_triple_seifert),
+  ANIMS_M_NAME(triple_torus_to_triple_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a triple torus
+   to a Hopf torus. */
+
+/* Transform the three tori to a Hopf torus on the equator while increasing
+   the point density of the two tori at latitude ±45° by a factor of two. */
+ANIM_SO_DEF(triple_torus_to_single_hopf_torus_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -2.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -1.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 1.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 2.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_single_hopf_torus_densify) = {
+  ANIM_SO_NUM(triple_torus_to_single_hopf_torus_densify),
+  ANIM_SO_NAME(triple_torus_to_single_hopf_torus_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  120                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_single_hopf_torus_densify) = {
+  ANIM_MO_REF(triple_torus_to_single_hopf_torus_densify)
+};
+
+ANIM_PS_DEF(triple_torus_to_single_hopf_torus_densify) = {
+  ANIM_PH_NUM(triple_torus_to_single_hopf_torus_densify),
+  ANIM_PH_NAME(triple_torus_to_single_hopf_torus_densify)
+};
+
+ANIMS_M_DEF(triple_torus_to_single_hopf_torus) = {
+  ANIM_PS_REF(triple_torus_to_single_hopf_torus_densify)
+};
+
+ANIMS_DEF(triple_torus_to_single_hopf_torus) = {
+  ANIMS_M_NUM(triple_torus_to_single_hopf_torus),
+  ANIMS_M_NAME(triple_torus_to_single_hopf_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a triple torus
+   to a single Hopf spiral. */
+
+/* Deform the three tori to three Hopf spirals and merge them. */
+ANIM_SO_DEF(triple_torus_to_single_hopf_spiral_merge) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    -M_PI_F/4.0f,         0.0f,                 EASING_CUBIC, /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 -M_PI_F,              EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/2.0f,         -M_PI_F/3.0f,         EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    M_PI_F/4.0f,          0.0f,                 EASING_CUBIC, /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              M_PI_F/3.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_torus_to_single_hopf_spiral_merge) = {
+  ANIM_SO_NUM(triple_torus_to_single_hopf_spiral_merge),
+  ANIM_SO_NAME(triple_torus_to_single_hopf_spiral_merge),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_torus_to_single_hopf_spiral_merge) = {
+  ANIM_MO_REF(triple_torus_to_single_hopf_spiral_merge)
+};
+
+ANIM_PS_DEF(triple_torus_to_single_hopf_spiral_merge) = {
+  ANIM_PH_NUM(triple_torus_to_single_hopf_spiral_merge),
+  ANIM_PH_NAME(triple_torus_to_single_hopf_spiral_merge)
+};
+
+ANIMS_M_DEF(triple_torus_to_single_hopf_spiral) = {
+  ANIM_PS_REF(triple_torus_to_single_hopf_spiral_merge)
+};
+
+ANIMS_DEF(triple_torus_to_single_hopf_spiral) = {
+  ANIMS_M_NUM(triple_torus_to_single_hopf_spiral),
+  ANIMS_M_NAME(triple_torus_to_single_hopf_spiral)
+};
+
+
+
+/*****************************************************************************
+ * The set of all transformations from a single Seifert surface.
+ *****************************************************************************/
+
+/* The set of possible animations for the transition from a single Seifert
+   surface to a single point. */
+
+/* Shrink a Seifert surface on the equator to a point on the equator. */
+ANIM_SO_DEF(single_seifert_to_single_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_point_rot_z) = {
+  ANIM_SO_NUM(single_seifert_to_single_point_rot_z),
+  ANIM_SO_NAME(single_seifert_to_single_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_single_point_rot_z) = {
+  ANIM_MO_REF(single_seifert_to_single_point_rot_z)
+};
+
+ANIM_PS_DEF(single_seifert_to_single_point_rot_z) = {
+  ANIM_PH_NUM(single_seifert_to_single_point_rot_z),
+  ANIM_PH_NAME(single_seifert_to_single_point_rot_z)
+};
+
+/* Shrink a Seifert surface  on the equator to a point on the equator and
+   rotate around a random axis. */
+ANIM_SO_DEF(single_seifert_to_single_point_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_point_rot_rnd) = {
+  ANIM_SO_NUM(single_seifert_to_single_point_rot_rnd),
+  ANIM_SO_NAME(single_seifert_to_single_point_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_single_point_rot_rnd) = {
+  ANIM_MO_REF(single_seifert_to_single_point_rot_rnd)
+};
+
+ANIM_PS_DEF(single_seifert_to_single_point_rot_rnd) = {
+  ANIM_PH_NUM(single_seifert_to_single_point_rot_rnd),
+  ANIM_PH_NAME(single_seifert_to_single_point_rot_rnd)
+};
+
+ANIMS_M_DEF(single_seifert_to_single_point) = {
+  ANIM_PS_REF(single_seifert_to_single_point_rot_z),
+  ANIM_PS_REF(single_seifert_to_single_point_rot_rnd)
+};
+
+ANIMS_DEF(single_seifert_to_single_point) = {
+  ANIMS_M_NUM(single_seifert_to_single_point),
+  ANIMS_M_NAME(single_seifert_to_single_point)
+};
+
+
+
+/* The set of possible animations for the transition from a single Seifert
+   surface to a single torus. */
+
+/* Expand the Seifert surface on the equator to a torus on the equator. */
+ANIM_SO_DEF(single_seifert_to_single_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_torus_rot_z) = {
+  ANIM_SO_NUM(single_seifert_to_single_torus_rot_z),
+  ANIM_SO_NAME(single_seifert_to_single_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_single_torus_rot_z) = {
+  ANIM_MO_REF(single_seifert_to_single_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_seifert_to_single_torus_rot_z) = {
+  ANIM_PH_NUM(single_seifert_to_single_torus_rot_z),
+  ANIM_PH_NAME(single_seifert_to_single_torus_rot_z)
+};
+
+/* Expand the Seifert surface on the equator to a torus on the equator and
+   rotate it around a random axis. */
+ANIM_SO_DEF(single_seifert_to_single_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_seifert_to_single_torus_rot_rnd),
+  ANIM_SO_NAME(single_seifert_to_single_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_single_torus_rot_rnd) = {
+  ANIM_MO_REF(single_seifert_to_single_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_seifert_to_single_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_seifert_to_single_torus_rot_rnd),
+  ANIM_PH_NAME(single_seifert_to_single_torus_rot_rnd)
+};
+
+/* Flip half of a Seifert surface on the equator around a suitable axis on
+   the equator to form a torus on the equator. */
+ANIM_SO_DEF(single_seifert_to_single_torus_flip) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.99785892f, -0.06540313f, 0.0f },
+                          M_PI_F, 0.0f,         EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_torus_flip) = {
+  ANIM_SO_NUM(single_seifert_to_single_torus_flip),
+  ANIM_SO_NAME(single_seifert_to_single_torus_flip),
+  0.25f,                                        EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_single_torus_flip) = {
+  ANIM_MO_REF(single_seifert_to_single_torus_flip)
+};
+
+ANIM_PS_DEF(single_seifert_to_single_torus_flip) = {
+  ANIM_PH_NUM(single_seifert_to_single_torus_flip),
+  ANIM_PH_NAME(single_seifert_to_single_torus_flip)
+};
+
+ANIMS_M_DEF(single_seifert_to_single_torus) = {
+  ANIM_PS_REF(single_seifert_to_single_torus_rot_z),
+  ANIM_PS_REF(single_seifert_to_single_torus_rot_rnd),
+  ANIM_PS_REF(single_seifert_to_single_torus_flip)
+};
+
+ANIMS_DEF(single_seifert_to_single_torus) = {
+  ANIMS_M_NUM(single_seifert_to_single_torus),
+  ANIMS_M_NAME(single_seifert_to_single_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single Seifert
+   surface to a double torus. */
+
+/* Increase the point density of the two Seifert surfaces by a factor of
+   two, expand them from 90° sectors to 360° sectors, and split them at
+   the equator. */
+
+/* Phase 1: Increase the point density by a factor of two. */
+ANIM_SO_DEF(single_seifert_to_single_dense_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               95.0f*M_PI_F/96.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               97.0f*M_PI_F/96.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_dense_seifert_rot_z) = {
+  ANIM_SO_NUM(single_seifert_to_single_dense_seifert_rot_z),
+  ANIM_SO_NAME(single_seifert_to_single_dense_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  30                                                          /* num_steps */
+};
+
+/* Phase 2: Expand the two Seifert surfaces from 90° sectors to 360°
+   sectors and split them at the equator. */
+ANIM_SO_DEF(single_dense_seifert_to_double_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -49.0f*M_PI_F/96.0f,  -M_PI_F/2.0f,         EASING_CUBIC, /* offset */
+    M_PI_F/2.0f,          2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -51.0f*M_PI_F/96.0f,  -M_PI_F/2.0f,         EASING_CUBIC, /* offset */
+    -M_PI_F/2.0f,         -2.0f*M_PI_F,         EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_seifert_to_double_torus_rot_z) = {
+  ANIM_SO_NUM(single_dense_seifert_to_double_torus_rot_z),
+  ANIM_SO_NAME(single_dense_seifert_to_double_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_double_torus_rot_z) = {
+  ANIM_MO_REF(single_seifert_to_single_dense_seifert_rot_z),
+  ANIM_MO_REF(single_dense_seifert_to_double_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_seifert_to_double_torus_rot_z) = {
+  ANIM_PH_NUM(single_seifert_to_double_torus_rot_z),
+  ANIM_PH_NAME(single_seifert_to_double_torus_rot_z)
+};
+
+/* Split the two Seifert surfaces at the equator, expand them from 180°
+   sectors to 360° sectors, increase their point density by a factor of two,
+   and rotate around a random axis. */
+ANIM_SO_DEF(single_seifert_to_double_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               13.0f*M_PI_F/12.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   13.0f*M_PI_F/12.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_double_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_seifert_to_double_torus_rot_rnd),
+  ANIM_SO_NAME(single_seifert_to_double_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_double_torus_rot_rnd) = {
+  ANIM_MO_REF(single_seifert_to_double_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_seifert_to_double_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_seifert_to_double_torus_rot_rnd),
+  ANIM_PH_NAME(single_seifert_to_double_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(single_seifert_to_double_torus) = {
+  ANIM_PS_REF(single_seifert_to_double_torus_rot_z),
+  ANIM_PS_REF(single_seifert_to_double_torus_rot_rnd)
+};
+
+ANIMS_DEF(single_seifert_to_double_torus) = {
+  ANIMS_M_NUM(single_seifert_to_double_torus),
+  ANIMS_M_NAME(single_seifert_to_double_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single Seifert
+   surface to a triple torus. */
+
+/* Increase the point density of the three Seifert surfaces by a factor of
+   three, expand them from 60° sectors to 360° sectors, and split them at
+   the equator. */
+
+/* Phase 1: Increase the point density by a factor of three. */
+ANIM_SO_DEF(single_seifert_to_single_dense_seifert_three_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               71.0f*M_PI_F/72.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               73.0f*M_PI_F/72.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_dense_seifert_three_rot_z) = {
+  ANIM_SO_NUM(single_seifert_to_single_dense_seifert_three_rot_z),
+  ANIM_SO_NAME(single_seifert_to_single_dense_seifert_three_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  30                                                          /* num_steps */
+};
+
+/* Phase 2: Expand the three Seifert surfaces from 60° sectors to 360°
+   sectors and split them at the equator. */
+ANIM_SO_DEF(single_dense_seifert_three_to_triple_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -25.0f*M_PI_F/72.0f,  -3.0f*M_PI_F/2.0f,    EASING_CUBIC, /* offset */
+    M_PI_F/3.0f,          2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -49.0f*M_PI_F/72.0f,  -3.0f*M_PI_F/2.0f,    EASING_CUBIC, /* offset */
+    M_PI_F/3.0f,          2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -73.0f*M_PI_F/72.0f,  -3.0f*M_PI_F/2.0f,    EASING_CUBIC, /* offset */
+    M_PI_F/3.0f,          2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_seifert_three_to_triple_torus_rot_z) = {
+  ANIM_SO_NUM(single_dense_seifert_three_to_triple_torus_rot_z),
+  ANIM_SO_NAME(single_dense_seifert_three_to_triple_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_triple_torus_rot_z) = {
+  ANIM_MO_REF(single_seifert_to_single_dense_seifert_three_rot_z),
+  ANIM_MO_REF(single_dense_seifert_three_to_triple_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_seifert_to_triple_torus_rot_z) = {
+  ANIM_PH_NUM(single_seifert_to_triple_torus_rot_z),
+  ANIM_PH_NAME(single_seifert_to_triple_torus_rot_z)
+};
+
+/* Split the three Seifert surfaces at the equator, expand them from 180°
+   sectors to 360° sectors, increase their point density by a factor of
+   three, and rotate around a random axis. */
+ANIM_SO_DEF(single_seifert_to_triple_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               13.0f*M_PI_F/12.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               14.0f*M_PI_F/12.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   13.0f*M_PI_F/12.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   14.0f*M_PI_F/12.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   13.0f*M_PI_F/12.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   14.0f*M_PI_F/12.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_triple_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_seifert_to_triple_torus_rot_rnd),
+  ANIM_SO_NAME(single_seifert_to_triple_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_triple_torus_rot_rnd) = {
+  ANIM_MO_REF(single_seifert_to_triple_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_seifert_to_triple_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_seifert_to_triple_torus_rot_rnd),
+  ANIM_PH_NAME(single_seifert_to_triple_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(single_seifert_to_triple_torus) = {
+  ANIM_PS_REF(single_seifert_to_triple_torus_rot_z),
+  ANIM_PS_REF(single_seifert_to_triple_torus_rot_rnd)
+};
+
+ANIMS_DEF(single_seifert_to_triple_torus) = {
+  ANIMS_M_NUM(single_seifert_to_triple_torus),
+  ANIMS_M_NAME(single_seifert_to_triple_torus)
+};
+
+
+
+/* The set of possible animations for a single Seifert surface. */
+
+/* Rotate a Seifert surface on the x axis of the total space. */
+ANIM_SO_DEF(single_seifert_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_rot_x) = {
+  ANIM_SO_NUM(single_seifert_rot_x),
+  ANIM_SO_NAME(single_seifert_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_rot_x) = {
+  ANIM_MO_REF(single_seifert_rot_x)
+};
+
+ANIM_PS_DEF(single_seifert_rot_x) = {
+  ANIM_PH_NUM(single_seifert_rot_x),
+  ANIM_PH_NAME(single_seifert_rot_x)
+};
+
+/* Rotate a Seifert surface on the equator around the z axis. */
+ANIM_SO_DEF(single_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_rot_z) = {
+  ANIM_SO_NUM(single_seifert_rot_z),
+  ANIM_SO_NAME(single_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_rot_z) = {
+  ANIM_MO_REF(single_seifert_rot_z)
+};
+
+ANIM_PS_DEF(single_seifert_rot_z) = {
+  ANIM_PH_NUM(single_seifert_rot_z),
+  ANIM_PH_NAME(single_seifert_rot_z)
+};
+
+/* Rotate a Seifert surface on the equator around a random axis. */
+ANIM_SO_DEF(single_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_rot_rnd) = {
+  ANIM_SO_NUM(single_seifert_rot_rnd),
+  ANIM_SO_NAME(single_seifert_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_rot_rnd) = {
+  ANIM_MO_REF(single_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(single_seifert_rot_rnd) = {
+  ANIM_PH_NUM(single_seifert_rot_rnd),
+  ANIM_PH_NAME(single_seifert_rot_rnd)
+};
+
+/* Move a Seifert surface on the equator up and down to latitude ±80° while
+   rotating it around the z axis and, with a certain probability, around a
+   random axis. */
+ANIM_SO_DEF(single_seifert_move) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    1.0f*M_PI_F/18.0f,    17.0f*M_PI_F/18.0f,   EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_move) = {
+  ANIM_SO_NUM(single_seifert_move),
+  ANIM_SO_NAME(single_seifert_move),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_move) = {
+  ANIM_MO_REF(single_seifert_move)
+};
+
+ANIM_PS_DEF(single_seifert_move) = {
+  ANIM_PH_NUM(single_seifert_move),
+  ANIM_PH_NAME(single_seifert_move)
+};
+
+ANIMS_M_DEF(single_seifert) = {
+  ANIM_PS_REF(single_seifert_rot_x),
+  ANIM_PS_REF(single_seifert_rot_z),
+  ANIM_PS_REF(single_seifert_rot_rnd),
+  ANIM_PS_REF(single_seifert_move)
+};
+
+ANIMS_DEF(single_seifert) = {
+  ANIMS_M_NUM(single_seifert),
+  ANIMS_M_NAME(single_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single Seifert
+   surface to a triple Seifert surface. */
+
+/* Increase the point density of the three Seifert surfaces by a factor of
+   three, expand them from 60° sectors to 180° sectors, and split them at
+   the equator. */
+
+/* Phase 1: Increase the point density by a factor of three.  Already
+   defined above. */
+
+/* Phase 2: Expand the three Seifert surfaces from 60° sectors to 360°
+   sectors and split them at the equator. */
+ANIM_SO_DEF(single_dense_seifert_three_to_triple_seifert_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -25.0f*M_PI_F/72.0f,  -M_PI_F,              EASING_CUBIC, /* offset */
+    M_PI_F/3.0f,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -49.0f*M_PI_F/72.0f,  -M_PI_F,              EASING_CUBIC, /* offset */
+    M_PI_F/3.0f,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -73.0f*M_PI_F/72.0f,  -M_PI_F,              EASING_CUBIC, /* offset */
+    M_PI_F/3.0f,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_seifert_three_to_triple_seifert_rot_z) = {
+  ANIM_SO_NUM(single_dense_seifert_three_to_triple_seifert_rot_z),
+  ANIM_SO_NAME(single_dense_seifert_three_to_triple_seifert_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_triple_seifert_rot_z) = {
+  ANIM_MO_REF(single_seifert_to_single_dense_seifert_three_rot_z),
+  ANIM_MO_REF(single_dense_seifert_three_to_triple_seifert_rot_z)
+};
+
+ANIM_PS_DEF(single_seifert_to_triple_seifert_rot_z) = {
+  ANIM_PH_NUM(single_seifert_to_triple_seifert_rot_z),
+  ANIM_PH_NAME(single_seifert_to_triple_seifert_rot_z)
+};
+
+/* Split the three Seifert surfaces at the equator, increase their point
+   density by a factor of three, and rotate around a random axis. */
+ANIM_SO_DEF(single_seifert_to_triple_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_triple_seifert_rot_rnd) = {
+  ANIM_SO_NUM(single_seifert_to_triple_seifert_rot_rnd),
+  ANIM_SO_NAME(single_seifert_to_triple_seifert_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_triple_seifert_rot_rnd) = {
+  ANIM_MO_REF(single_seifert_to_triple_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(single_seifert_to_triple_seifert_rot_rnd) = {
+  ANIM_PH_NUM(single_seifert_to_triple_seifert_rot_rnd),
+  ANIM_PH_NAME(single_seifert_to_triple_seifert_rot_rnd)
+};
+
+ANIMS_M_DEF(single_seifert_to_triple_seifert) = {
+  ANIM_PS_REF(single_seifert_to_triple_seifert_rot_z),
+  ANIM_PS_REF(single_seifert_to_triple_seifert_rot_rnd)
+};
+
+ANIMS_DEF(single_seifert_to_triple_seifert) = {
+  ANIMS_M_NUM(single_seifert_to_triple_seifert),
+  ANIMS_M_NAME(single_seifert_to_triple_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single Seifert
+   surface to a single Hopf torus. */
+
+/* Expand the Seifert surface on the equator, fill it to five times the
+   density, and deform it to a Hopf torus. */
+ANIM_SO_DEF(single_seifert_to_single_hopf_torus_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               61.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               62.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               63.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               64.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_hopf_torus_densify) = {
+  ANIM_SO_NUM(single_seifert_to_single_hopf_torus_densify),
+  ANIM_SO_NAME(single_seifert_to_single_hopf_torus_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_single_hopf_torus_densify) = {
+  ANIM_MO_REF(single_seifert_to_single_hopf_torus_densify)
+};
+
+ANIM_PS_DEF(single_seifert_to_single_hopf_torus_densify) = {
+  ANIM_PH_NUM(single_seifert_to_single_hopf_torus_densify),
+  ANIM_PH_NAME(single_seifert_to_single_hopf_torus_densify)
+};
+
+ANIMS_M_DEF(single_seifert_to_single_hopf_torus) = {
+  ANIM_PS_REF(single_seifert_to_single_hopf_torus_densify)
+};
+
+ANIMS_DEF(single_seifert_to_single_hopf_torus) = {
+  ANIMS_M_NUM(single_seifert_to_single_hopf_torus),
+  ANIMS_M_NAME(single_seifert_to_single_hopf_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a Seifert surface
+   to a single single Hopf spiral. */
+
+/* Decrease the winding number of a Hopf spiral to one and collapse its
+   height to a Seifert surface on the equator, and decrease the point
+   density by a factor of three. */
+ANIM_SO_DEF(single_seifert_to_single_hopf_spiral_wind_hor) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    1.0f,                 2.0f,                 EASING_CUBIC, /* r */
+    M_PI_F/24.0f,         -M_PI_F,              EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    1.0f,                 2.0f,                 EASING_CUBIC, /* r */
+    M_PI_F/24.0f,         -35.0f*M_PI_F/36.0f,  EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    1.0f,                 2.0f,                 EASING_CUBIC, /* r */
+    M_PI_F/24.0f,         -34.0f*M_PI_F/36.0f , EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_hopf_spiral_wind_hor) = {
+  ANIM_SO_NUM(single_seifert_to_single_hopf_spiral_wind_hor),
+  ANIM_SO_NAME(single_seifert_to_single_hopf_spiral_wind_hor),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_single_hopf_spiral_wind_hor) = {
+  ANIM_MO_REF(single_seifert_to_single_hopf_spiral_wind_hor)
+};
+
+ANIM_PS_DEF(single_seifert_to_single_hopf_spiral_wind_hor) = {
+  ANIM_PH_NUM(single_seifert_to_single_hopf_spiral_wind_hor),
+  ANIM_PH_NAME(single_seifert_to_single_hopf_spiral_wind_hor)
+};
+
+/* Increase the point density of a Seifert surface on the equator by a
+   factor of three and rotate it to a vertical position, then wind the
+   Seifert surface to a Hopf spiral. */
+
+/* Phase 1: Increase the density of the Seifert surface and rotate it to a
+   vertical position. */
+ANIM_SO_DEF(single_seifert_to_single_dense_seifert_vert) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -2.0f*M_PI_F,         -M_PI_F,              EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -2.0f*M_PI_F,         -35.0f*M_PI_F/36.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -2.0f*M_PI_F,         -34.0f*M_PI_F/36.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_seifert_to_single_dense_seifert_vert) = {
+  ANIM_SO_NUM(single_seifert_to_single_dense_seifert_vert),
+  ANIM_SO_NAME(single_seifert_to_single_dense_seifert_vert),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  120                                                         /* num_steps */
+};
+
+/* Phase 2: Wind the Hopf spiral and expand it. */
+ANIM_SO_DEF(single_dense_seifert_to_single_hopf_spiral_wind) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    0.0f,                 2.0f,                 EASING_CUBIC, /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_seifert_to_single_hopf_spiral_wind) = {
+  ANIM_SO_NUM(single_dense_seifert_to_single_hopf_spiral_wind),
+  ANIM_SO_NAME(single_dense_seifert_to_single_hopf_spiral_wind),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_seifert_to_single_hopf_spiral_wind_vert) = {
+  ANIM_MO_REF(single_seifert_to_single_dense_seifert_vert),
+  ANIM_MO_REF(single_dense_seifert_to_single_hopf_spiral_wind)
+};
+
+ANIM_PS_DEF(single_seifert_to_single_hopf_spiral_wind_vert) = {
+  ANIM_PH_NUM(single_seifert_to_single_hopf_spiral_wind_vert),
+  ANIM_PH_NAME(single_seifert_to_single_hopf_spiral_wind_vert)
+};
+
+ANIMS_M_DEF(single_seifert_to_single_hopf_spiral) = {
+  ANIM_PS_REF(single_seifert_to_single_hopf_spiral_wind_hor),
+  ANIM_PS_REF(single_seifert_to_single_hopf_spiral_wind_vert)
+};
+
+ANIMS_DEF(single_seifert_to_single_hopf_spiral) = {
+  ANIMS_M_NUM(single_seifert_to_single_hopf_spiral),
+  ANIMS_M_NAME(single_seifert_to_single_hopf_spiral)
+};
+
+
+
+/*****************************************************************************
+ * The set of all transformations from a triple Seifert surface.
+ *****************************************************************************/
+
+/* The set of possible animations for the transition from a triple Seifert
+   surface to a single point. */
+
+/* Rotate two Seifert surfaces at latitude ±45° around the z axis, shrink
+   them to a single point, and move them to the equator.  Shrink the Seifert
+   surface on the equator to a single point without rotating it. */
+ANIM_SO_DEF(triple_seifert_to_single_point_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/24.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/24.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_point_rot_z) = {
+  ANIM_SO_NUM(triple_seifert_to_single_point_rot_z),
+  ANIM_SO_NAME(triple_seifert_to_single_point_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_single_point_rot_z) = {
+  ANIM_MO_REF(triple_seifert_to_single_point_rot_z)
+};
+
+ANIM_PS_DEF(triple_seifert_to_single_point_rot_z) = {
+  ANIM_PH_NUM(triple_seifert_to_single_point_rot_z),
+  ANIM_PH_NAME(triple_seifert_to_single_point_rot_z)
+};
+
+/* Rotate the three Seifert surfaces around the z axis in opposite
+   directions, move the Seifert surfaces at latitudes ±45° to latitudes
+   ±70°, and increase their point density by a factor of two, then move them
+   successively to the south pole while continuing to rotate, creating a
+   single point at the south pole, then move the point on the south pole to
+   its canonical position on the equator along a Hopf spiral. */
+
+/* Phase 1: Rotate the three Seifert surfaces for one revolution, move the
+   Seifert surfaces at latitudes ±45° to latitudes ±70°, and increase the
+   point density by a factor of two. */
+ANIM_SO_DEF(triple_seifert_to_single_point_rot_z_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_ACCEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_ACCEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_ACCEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_ACCEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_ACCEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_ACCEL  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_point_rot_z_densify) = {
+  ANIM_SO_NUM(triple_seifert_to_single_point_rot_z_densify),
+  ANIM_SO_NAME(triple_seifert_to_single_point_rot_z_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 2: Rotate the three Seifert surfaces for one revolution. */
+ANIM_SO_DEF(triple_seifert_to_single_point_rot_z_linear) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     8.0f*M_PI_F/9.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_LIN    /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_point_rot_z_linear) = {
+  ANIM_SO_NUM(triple_seifert_to_single_point_rot_z_linear),
+  ANIM_SO_NAME(triple_seifert_to_single_point_rot_z_linear),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 3: Move the Seifert surface at latitude -70° to the south pole. */
+ANIM_SO_DEF(triple_seifert_to_single_point_rot_z_move_south_1) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     M_PI_F,               EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_DECEL  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_point_rot_z_move_south_1) = {
+  ANIM_SO_NUM(triple_seifert_to_single_point_rot_z_move_south_1),
+  ANIM_SO_NAME(triple_seifert_to_single_point_rot_z_move_south_1),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 4: Move the Seifert surface at latitude 0° to the south pole. */
+ANIM_SO_DEF(triple_seifert_to_single_point_rot_z_move_south_2) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_LIN    /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_DECEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_point_rot_z_move_south_2) = {
+  ANIM_SO_NUM(triple_seifert_to_single_point_rot_z_move_south_2),
+  ANIM_SO_NAME(triple_seifert_to_single_point_rot_z_move_south_2),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 5: Move the Seifert surface at latitude 70° to the south pole. */
+ANIM_SO_DEF(triple_seifert_to_single_point_rot_z_move_south_3) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F,               EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_DECEL  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_point_rot_z_move_south_3) = {
+  ANIM_SO_NUM(triple_seifert_to_single_point_rot_z_move_south_3),
+  ANIM_SO_NAME(triple_seifert_to_single_point_rot_z_move_south_3),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 6: Move the single point at the south pole to the equator along a
+   Hopf Spiral. */
+ANIM_SO_DEF(triple_seifert_to_single_point_rot_z_move_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* offset */
+    0.0f,                 0.0f,                 EASING_NONE,  /* sector */
+    0,                    1,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_point_rot_z_move_spiral) = {
+  ANIM_SO_NUM(triple_seifert_to_single_point_rot_z_move_spiral),
+  ANIM_SO_NAME(triple_seifert_to_single_point_rot_z_move_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  120                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_single_point_move_south) = {
+  ANIM_MO_REF(triple_seifert_to_single_point_rot_z_densify),
+  ANIM_MO_REF(triple_seifert_to_single_point_rot_z_linear),
+  ANIM_MO_REF(triple_seifert_to_single_point_rot_z_move_south_1),
+  ANIM_MO_REF(triple_seifert_to_single_point_rot_z_move_south_2),
+  ANIM_MO_REF(triple_seifert_to_single_point_rot_z_move_south_3),
+  ANIM_MO_REF(triple_seifert_to_single_point_rot_z_move_spiral)
+};
+
+ANIM_PS_DEF(triple_seifert_to_single_point_move_south) = {
+  ANIM_PH_NUM(triple_seifert_to_single_point_move_south),
+  ANIM_PH_NAME(triple_seifert_to_single_point_move_south)
+};
+
+ANIMS_M_DEF(triple_seifert_to_single_point) = {
+  ANIM_PS_REF(triple_seifert_to_single_point_rot_z),
+  ANIM_PS_REF(triple_seifert_to_single_point_move_south)
+};
+
+ANIMS_DEF(triple_seifert_to_single_point) = {
+  ANIMS_M_NUM(triple_seifert_to_single_point),
+  ANIMS_M_NAME(triple_seifert_to_single_point)
+};
+
+
+
+/* The set of possible animations for the transition from a triple Seifert
+   surface to a single torus. */
+
+/* Rotate three Seifert surfaces around the z axis, reduce their sectors
+   from 180° to 120°, move the two Seifert surfaces at latitude ±45° to the
+   equator, and merge all three Seifert surfaces into a dense torus at the
+   equator, then reduce the point density by a factor of three. */
+
+/* Phase 1: Rotate three Seifert surfaces around the z axis, reduce their
+   sectors from 180° to 120°, move the two Seifert surfaces at latitude
+   ±45° to the equator, and merge all three Seifert surfaces into a dense
+   torus at the equator. */
+ANIM_SO_DEF(triple_seifert_to_single_dense_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              4.0f*M_PI_F/3.0f,     EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_dense_torus_rot_z) = {
+  ANIM_SO_NUM(triple_seifert_to_single_dense_torus_rot_z),
+  ANIM_SO_NAME(triple_seifert_to_single_dense_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 2: Reduce the point density by a factor of three.  Already defined
+   above. */
+
+ANIM_PH_DEF(triple_seifert_to_single_torus_rot_z) = {
+  ANIM_MO_REF(triple_seifert_to_single_dense_torus_rot_z),
+  ANIM_MO_REF(single_dense_torus_to_single_torus_loosen_three)
+};
+
+ANIM_PS_DEF(triple_seifert_to_single_torus_rot_z) = {
+  ANIM_PH_NUM(triple_seifert_to_single_torus_rot_z),
+  ANIM_PH_NAME(triple_seifert_to_single_torus_rot_z)
+};
+
+/* Rotate three Seifert surfaces around a random axis, expand their sectors
+   from 180° to 360°, and move the Seifert surfaces at latitude ±45° to the
+   equator. */
+ANIM_SO_DEF(triple_seifert_to_single_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_torus_rot_rnd) = {
+  ANIM_SO_NUM(triple_seifert_to_single_torus_rot_rnd),
+  ANIM_SO_NAME(triple_seifert_to_single_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_single_torus_rot_rnd) = {
+  ANIM_MO_REF(triple_seifert_to_single_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_seifert_to_single_torus_rot_rnd) = {
+  ANIM_PH_NUM(triple_seifert_to_single_torus_rot_rnd),
+  ANIM_PH_NAME(triple_seifert_to_single_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_seifert_to_single_torus) = {
+  ANIM_PS_REF(triple_seifert_to_single_torus_rot_z),
+  ANIM_PS_REF(triple_seifert_to_single_torus_rot_rnd)
+};
+
+ANIMS_DEF(triple_seifert_to_single_torus) = {
+  ANIMS_M_NUM(triple_seifert_to_single_torus),
+  ANIMS_M_NAME(triple_seifert_to_single_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a triple Seifert
+   surface to a double torus. */
+
+/* Rotate the three Seifert surfaces around the z axis, split the Seifert
+   surface at the equator into two Seifert surfaces, increase their sector
+   from 180° to 360°, and move them to latitude ±45°. */
+ANIM_SO_DEF(triple_seifert_to_double_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, M_PI_F,         EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, M_PI_F/2.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, M_PI_F,        EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_double_torus_rot_z) = {
+  ANIM_SO_NUM(triple_seifert_to_double_torus_rot_z),
+  ANIM_SO_NAME(triple_seifert_to_double_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_double_torus_rot_z) = {
+  ANIM_MO_REF(triple_seifert_to_double_torus_rot_z)
+};
+
+ANIM_PS_DEF(triple_seifert_to_double_torus_rot_z) = {
+  ANIM_PH_NUM(triple_seifert_to_double_torus_rot_z),
+  ANIM_PH_NAME(triple_seifert_to_double_torus_rot_z)
+};
+
+/* Split the Seifert surface at the equator into two Seifert surfaces,
+   increase their sector from 180° to 360°, move them to latitude ±45°, and
+   rotate all Seifert surfaces around a random axis with a certain
+   probability. */
+ANIM_SO_DEF(triple_seifert_to_double_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_double_torus_rot_rnd) = {
+  ANIM_SO_NUM(triple_seifert_to_double_torus_rot_rnd),
+  ANIM_SO_NAME(triple_seifert_to_double_torus_rot_rnd),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_double_torus_rot_rnd) = {
+  ANIM_MO_REF(triple_seifert_to_double_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_seifert_to_double_torus_rot_rnd) = {
+  ANIM_PH_NUM(triple_seifert_to_double_torus_rot_rnd),
+  ANIM_PH_NAME(triple_seifert_to_double_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_seifert_to_double_torus) = {
+  ANIM_PS_REF(triple_seifert_to_double_torus_rot_z),
+  ANIM_PS_REF(triple_seifert_to_double_torus_rot_rnd)
+};
+
+ANIMS_DEF(triple_seifert_to_double_torus) = {
+  ANIMS_M_NUM(triple_seifert_to_double_torus),
+  ANIMS_M_NAME(triple_seifert_to_double_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a triple Seifert
+   surface to a triple torus. */
+
+/* Rotate the three Seifert surfaces around the z axis and increase their
+   sector from 180° to 360°. */
+ANIM_SO_DEF(triple_seifert_to_triple_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 3.0f*M_PI_F/2.0f,
+                                                EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               0.0f,                 EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 3.0f*M_PI_F/2.0f,
+                                                EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_triple_torus_rot_z) = {
+  ANIM_SO_NUM(triple_seifert_to_triple_torus_rot_z),
+  ANIM_SO_NAME(triple_seifert_to_triple_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_triple_torus_rot_z) = {
+  ANIM_MO_REF(triple_seifert_to_triple_torus_rot_z)
+};
+
+ANIM_PS_DEF(triple_seifert_to_triple_torus_rot_z) = {
+  ANIM_PH_NUM(triple_seifert_to_triple_torus_rot_z),
+  ANIM_PH_NAME(triple_seifert_to_triple_torus_rot_z)
+};
+
+/* Increase the sector of the Seifert surfaces from 180° to 360° and rotate
+   them around a random axis with a certain probability. */
+ANIM_SO_DEF(triple_seifert_to_triple_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE , /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_triple_torus_rot_rnd) = {
+  ANIM_SO_NUM(triple_seifert_to_triple_torus_rot_rnd),
+  ANIM_SO_NAME(triple_seifert_to_triple_torus_rot_rnd),
+  0.75f,                                        EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_triple_torus_rot_rnd) = {
+  ANIM_MO_REF(triple_seifert_to_triple_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_seifert_to_triple_torus_rot_rnd) = {
+  ANIM_PH_NUM(triple_seifert_to_triple_torus_rot_rnd),
+  ANIM_PH_NAME(triple_seifert_to_triple_torus_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_seifert_to_triple_torus) = {
+  ANIM_PS_REF(triple_seifert_to_triple_torus_rot_z),
+  ANIM_PS_REF(triple_seifert_to_triple_torus_rot_rnd)
+};
+
+ANIMS_DEF(triple_seifert_to_triple_torus) = {
+  ANIMS_M_NUM(triple_seifert_to_triple_torus),
+  ANIMS_M_NAME(triple_seifert_to_triple_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a triple Seifert
+   surface to a single Seifert surface. */
+
+/* Shrink the three Seifert surfaces from 180° sectors to 60° sectors, join
+   them at the equator, and reduce the point density by a factor of three. */
+
+/* Phase 1: Shrink the three Seifert surfaces from 180° sectors to 60°
+   sectors and join them at the equator. */
+ANIM_SO_DEF(triple_seifert_to_single_dense_seifert_three_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -25.0f*M_PI_F/72.0f,  EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F/3.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -49.0f*M_PI_F/72.0f,  EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F/3.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -73.0f*M_PI_F/72.0f,  EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F/3.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_dense_seifert_three_rot_z) = {
+  ANIM_SO_NUM(triple_seifert_to_single_dense_seifert_three_rot_z),
+  ANIM_SO_NAME(triple_seifert_to_single_dense_seifert_three_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 2: Decrease the point density by a factor of three.  Already
+   defined above. */
+
+ANIM_PH_DEF(triple_seifert_to_single_seifert_rot_z) = {
+  ANIM_MO_REF(triple_seifert_to_single_dense_seifert_three_rot_z),
+  ANIM_MO_REF(single_dense_seifert_three_to_single_seifert_rot_z)
+};
+
+ANIM_PS_DEF(triple_seifert_to_single_seifert_rot_z) = {
+  ANIM_PH_NUM(triple_seifert_to_single_seifert_rot_z),
+  ANIM_PH_NAME(triple_seifert_to_single_seifert_rot_z)
+};
+
+/* Reduce the point density of the three Seifert surfaces by a factor of
+   three, interleave them at the equator, and rotate around a random axis. */
+ANIM_SO_DEF(triple_seifert_to_single_seifert_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   25.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    25.0f*M_PI_F/24.0f,   26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    26.0f*M_PI_F/24.0f,   26.0f*M_PI_F/24.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    8,                                  /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_seifert_rot_rnd) = {
+  ANIM_SO_NUM(triple_seifert_to_single_seifert_rot_rnd),
+  ANIM_SO_NAME(triple_seifert_to_single_seifert_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_single_seifert_rot_rnd) = {
+  ANIM_MO_REF(triple_seifert_to_single_seifert_rot_rnd)
+};
+
+ANIM_PS_DEF(triple_seifert_to_single_seifert_rot_rnd) = {
+  ANIM_PH_NUM(triple_seifert_to_single_seifert_rot_rnd),
+  ANIM_PH_NAME(triple_seifert_to_single_seifert_rot_rnd)
+};
+
+ANIMS_M_DEF(triple_seifert_to_single_seifert) = {
+  ANIM_PS_REF(triple_seifert_to_single_seifert_rot_z),
+  ANIM_PS_REF(triple_seifert_to_single_seifert_rot_rnd)
+};
+
+ANIMS_DEF(triple_seifert_to_single_seifert) = {
+  ANIMS_M_NUM(triple_seifert_to_single_seifert),
+  ANIMS_M_NAME(triple_seifert_to_single_seifert)
+};
+
+
+
+/* The set of possible animations for a triple Seifert surface. */
+
+/* Rotate three Seifert surfaces at latitudes ±45° and 0° around the x axis
+   of the total space while rotating them around the z axis in different
+   directions. */
+ANIM_SO_DEF(triple_seifert_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_rot_x) = {
+  ANIM_SO_NUM(triple_seifert_rot_x),
+  ANIM_SO_NAME(triple_seifert_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_rot_x) = {
+  ANIM_MO_REF(triple_seifert_rot_x)
+};
+
+ANIM_PS_DEF(triple_seifert_rot_x) = {
+  ANIM_PH_NUM(triple_seifert_rot_x),
+  ANIM_PH_NAME(triple_seifert_rot_x)
+};
+
+/* Rotate the three Seifert surfaces to a a vertical orientation, yielding
+   half of a parabolic ring cyclide and two interlocking Seifert surfaces,
+   and increase their density by a factor of two.  Then, rotate them around
+   the x axis of the total space.  Next, move the two Seifert surfaces not
+   on the equator between latitudes 1° and ±89°.  Finally, rotate the
+   Seifert surfaces back to 0° and ±45° and decrease their density by a
+   factor of two. */
+
+/* Phase 1: Rotate the Seifert surfaces by 90° to a vertical position,
+   increase their point density by a factor of two, and rotate them around
+   the x axis of the total space by 90°. */
+ANIM_SO_DEF(triple_seifert_to_vertical_triple_seifert_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_vertical_triple_seifert_densify) = {
+  ANIM_SO_NUM(triple_seifert_to_vertical_triple_seifert_densify),
+  ANIM_SO_NAME(triple_seifert_to_vertical_triple_seifert_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { -1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,     EASING_CUBIC, /* rot_space */
+  120                                                         /* num_steps */
+};
+
+/* Phase 2: Rotate the three Seifert surfaces by 360° around the x axis of
+   the total space. */
+ANIM_SO_DEF(vertical_triple_seifert_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(vertical_triple_seifert_rot_x) = {
+  ANIM_SO_NUM(vertical_triple_seifert_rot_x),
+  ANIM_SO_NAME(vertical_triple_seifert_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 5.0f*M_PI_F/2.0f,
+                                                EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 3: Move the two Seifert surfaces at latitides ±45° between
+   latitudes 1° and ±89°. */
+ANIM_SO_DEF(vertical_triple_seifert_move_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    179.0f*M_PI_F/180.0f, 91.0f*M_PI_F/180.0f,  EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    1.0f*M_PI_F/180.0f,   89.0f*M_PI_F/180.0f,  EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(vertical_triple_seifert_move_z) = {
+  ANIM_SO_NUM(vertical_triple_seifert_move_z),
+  ANIM_SO_NAME(vertical_triple_seifert_move_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, M_PI_F/2.0f,
+                                                EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 4: Rotate the Seifert surfaces back to 0° and ±45° and decrease
+   their density by a factor of two. */
+ANIM_SO_DEF(vertical_triple_seifert_to_triple_seifert_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,   EASING_CUBIC  /* rot_axis */
+  },
+};
+
+ANIM_MO_DEF(vertical_triple_seifert_to_triple_seifert_loosen) = {
+  ANIM_SO_NUM(vertical_triple_seifert_to_triple_seifert_loosen),
+  ANIM_SO_NAME(vertical_triple_seifert_to_triple_seifert_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { -1.0f, 0.0f, 0.0f }, M_PI_F/2.0f, 0.0f,     EASING_CUBIC, /* rot_space */
+  120                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_three_sphere) = {
+  ANIM_MO_REF(triple_seifert_to_vertical_triple_seifert_densify),
+  ANIM_MO_REF(vertical_triple_seifert_rot_x),
+  ANIM_MO_REF(vertical_triple_seifert_move_z),
+  ANIM_MO_REF(vertical_triple_seifert_to_triple_seifert_loosen)
+};
+
+ANIM_PS_DEF(triple_seifert_three_sphere) = {
+  ANIM_PH_NUM(triple_seifert_three_sphere),
+  ANIM_PH_NAME(triple_seifert_three_sphere)
+};
+
+/* Move the two Seifert surfaces at latitudes ±45° to latitude ±70° and
+   increase the point density of all three Seifert surfaces by a factor of
+   two, rotate the Seifert surfaces around the z axis and, with a certain
+   probability, around a random axis, move the two Seifert surfaces at
+   latitude ±70° back to ±45°, and decrease the point density of all three
+   Seifert surfaces by a factor of two. */
+
+/* Phase 1: Move two Seifert surfaces at latitude ±45° to latitude ±70° and
+   increase the point density of all three Seifert surfaces by a factor of
+   two. */
+ANIM_SO_DEF(triple_seifert_rot_rnd_move_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/9.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     8.0f*M_PI_F/9.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_rot_rnd_move_densify) = {
+  ANIM_SO_NUM(triple_seifert_rot_rnd_move_densify),
+  ANIM_SO_NAME(triple_seifert_rot_rnd_move_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Rotate the two tori by 360° around the z axis and, with a
+   certain probability, around a random axis. */
+ANIM_SO_DEF(triple_seifert_rot_rnd_rot) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/9.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     8.0f*M_PI_F/9.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    48,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_rot_rnd_rot) = {
+  ANIM_SO_NUM(triple_seifert_rot_rnd_rot),
+  ANIM_SO_NAME(triple_seifert_rot_rnd_rot),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 3: Move the two Seifert surfaces at latitude ±70° to latitude ±45°
+   and decrease the point density of all three Seifert surfaces by a factor
+   of two. */
+ANIM_SO_DEF(triple_seifert_rot_rnd_move_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/9.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    8.0f*M_PI_F/9.0f,     3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    49.0f*M_PI_F/48.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_rot_rnd_move_loosen) = {
+  ANIM_SO_NUM(triple_seifert_rot_rnd_move_loosen),
+  ANIM_SO_NAME(triple_seifert_rot_rnd_move_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_rot_rnd) = {
+  ANIM_MO_REF(triple_seifert_rot_rnd_move_densify),
+  ANIM_MO_REF(triple_seifert_rot_rnd_rot),
+  ANIM_MO_REF(triple_seifert_rot_rnd_move_loosen)
+};
+
+ANIM_PS_DEF(triple_seifert_rot_rnd) = {
+  ANIM_PH_NUM(triple_seifert_rot_rnd),
+  ANIM_PH_NAME(triple_seifert_rot_rnd)
+};
+
+/* Move the two Seifert surfaces at latitude ±45° to latitude ±5°, to
+   latitude ±85° and back to latitude ±45° while rotating all three Seifert
+   surfaces around the z axis and, with a certain probability, around a
+   random axis. */
+ANIM_SO_DEF(triple_seifert_move) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/18.0f,         8.0f*M_PI_F/18.0f,    EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    17.0f*M_PI_F/18.0f,   10.0f*M_PI_F/18.0f,   EASING_SIN,   /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, -1.0f }, 0.0f, 2.0f*M_PI_F,   EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_move) = {
+  ANIM_SO_NUM(triple_seifert_move),
+  ANIM_SO_NAME(triple_seifert_move),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_move) = {
+  ANIM_MO_REF(triple_seifert_move)
+};
+
+ANIM_PS_DEF(triple_seifert_move) = {
+  ANIM_PH_NUM(triple_seifert_move),
+  ANIM_PH_NAME(triple_seifert_move)
+};
+
+ANIMS_M_DEF(triple_seifert) = {
+  ANIM_PS_REF(triple_seifert_rot_x),
+  ANIM_PS_REF(triple_seifert_three_sphere),
+  ANIM_PS_REF(triple_seifert_rot_rnd),
+  ANIM_PS_REF(triple_seifert_move)
+};
+
+ANIMS_DEF(triple_seifert) = {
+  ANIMS_M_NUM(triple_seifert),
+  ANIMS_M_NAME(triple_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a triple Seifert
+   surface to a single Hopf torus. */
+
+/* Expand the sectors of the three Seifert surfaces from 180° to 360°,
+   transform them to a Hopf torus on the equator while increasing the point
+   density of the two Seifert surfaces at latitude ±45° by a factor of two. */
+ANIM_SO_DEF(triple_seifert_to_single_hopf_torus_densify) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               58.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               59.0f*M_PI_F/60.0f,   EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/2.0f,          EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F/60.0f,         EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* p */
+    0.0f,                 M_PI_F/8.0f,          EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               2.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    M_PI_F,               2.0f*M_PI_F,          EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_hopf_torus_densify) = {
+  ANIM_SO_NUM(triple_seifert_to_single_hopf_torus_densify),
+  ANIM_SO_NAME(triple_seifert_to_single_hopf_torus_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_single_hopf_torus_densify) = {
+  ANIM_MO_REF(triple_seifert_to_single_hopf_torus_densify)
+};
+
+ANIM_PS_DEF(triple_seifert_to_single_hopf_torus_densify) = {
+  ANIM_PH_NUM(triple_seifert_to_single_hopf_torus_densify),
+  ANIM_PH_NAME(triple_seifert_to_single_hopf_torus_densify)
+};
+
+ANIMS_M_DEF(triple_seifert_to_single_hopf_torus) = {
+  ANIM_PS_REF(triple_seifert_to_single_hopf_torus_densify)
+};
+
+ANIMS_DEF(triple_seifert_to_single_hopf_torus) = {
+  ANIMS_M_NUM(triple_seifert_to_single_hopf_torus),
+  ANIMS_M_NAME(triple_seifert_to_single_hopf_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a triple Seifert
+   surface to a single Hopf spiral. */
+
+/* Deform the three Seifert surfaces to three Hopf spirals and merge them. */
+ANIM_SO_DEF(triple_seifert_to_single_hopf_spiral_merge) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    -M_PI_F/4.0f,         0.0f,                 EASING_CUBIC, /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         -M_PI_F,              EASING_CUBIC, /* offset */
+    M_PI_F/2.0f,          2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    M_PI_F/48.0f,         -M_PI_F/3.0f,         EASING_CUBIC, /* offset */
+    M_PI_F/2.0f,          2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    M_PI_F/4.0f,          0.0f,                 EASING_CUBIC, /* p */
+    0.0f,                 1.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -47.0f*M_PI_F/48.0f,  M_PI_F/3.0f,          EASING_CUBIC, /* offset */
+    M_PI_F/2.0f,          2.0f*M_PI_F/3.0f,     EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(triple_seifert_to_single_hopf_spiral_merge) = {
+  ANIM_SO_NUM(triple_seifert_to_single_hopf_spiral_merge),
+  ANIM_SO_NAME(triple_seifert_to_single_hopf_spiral_merge),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(triple_seifert_to_single_hopf_spiral_merge) = {
+  ANIM_MO_REF(triple_seifert_to_single_hopf_spiral_merge)
+};
+
+ANIM_PS_DEF(triple_seifert_to_single_hopf_spiral_merge) = {
+  ANIM_PH_NUM(triple_seifert_to_single_hopf_spiral_merge),
+  ANIM_PH_NAME(triple_seifert_to_single_hopf_spiral_merge)
+};
+
+ANIMS_M_DEF(triple_seifert_to_single_hopf_spiral) = {
+  ANIM_PS_REF(triple_seifert_to_single_hopf_spiral_merge)
+};
+
+ANIMS_DEF(triple_seifert_to_single_hopf_spiral) = {
+  ANIMS_M_NUM(triple_seifert_to_single_hopf_spiral),
+  ANIMS_M_NAME(triple_seifert_to_single_hopf_spiral)
+};
+
+
+
+/*****************************************************************************
+ * The set of all transformations from a single Hopf torus.
+ *****************************************************************************/
+
+/* The set of possible animations for the transition from a single Hopf
+   torus to a single point. */
+
+/* Shrink a Hopf torus on the equator to a point on the equator and rotate
+   around a random axis. */
+ANIM_SO_DEF(single_hopf_torus_to_single_point_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_to_single_point_rot_rnd) = {
+  ANIM_SO_NUM(single_hopf_torus_to_single_point_rot_rnd),
+  ANIM_SO_NAME(single_hopf_torus_to_single_point_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_to_single_point_rot_rnd) = {
+  ANIM_MO_REF(single_hopf_torus_to_single_point_rot_rnd)
+};
+
+ANIM_PS_DEF(single_hopf_torus_to_single_point_rot_rnd) = {
+  ANIM_PH_NUM(single_hopf_torus_to_single_point_rot_rnd),
+  ANIM_PH_NAME(single_hopf_torus_to_single_point_rot_rnd)
+};
+
+/* Reduce the Hopf torus to a Hopf torus of one fifth the point density and
+   then deform it to a point on the equator. */
+
+/* Phase 1: Decrease the density of the Hopf torus. */
+ANIM_SO_DEF(single_dense_hopf_torus_to_single_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -2.0f*M_PI_F/60.0f,   0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/60.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/60.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    2.0f*M_PI_F/60.0f,    0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_hopf_torus_to_single_hopf_torus) = {
+  ANIM_SO_NUM(single_dense_hopf_torus_to_single_hopf_torus),
+  ANIM_SO_NAME(single_dense_hopf_torus_to_single_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Shrink the loose Hopf torus to a point on the equator. */
+ANIM_SO_DEF(single_loose_hopf_torus_to_single_point) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_loose_hopf_torus_to_single_point) = {
+  ANIM_SO_NUM(single_loose_hopf_torus_to_single_point),
+  ANIM_SO_NAME(single_loose_hopf_torus_to_single_point),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_to_single_point_loosen) = {
+  ANIM_MO_REF(single_dense_hopf_torus_to_single_hopf_torus),
+  ANIM_MO_REF(single_loose_hopf_torus_to_single_point)
+};
+
+ANIM_PS_DEF(single_hopf_torus_to_single_point_loosen) = {
+  ANIM_PH_NUM(single_hopf_torus_to_single_point_loosen),
+  ANIM_PH_NAME(single_hopf_torus_to_single_point_loosen)
+};
+
+ANIMS_M_DEF(single_hopf_torus_to_single_point) = {
+  ANIM_PS_REF(single_hopf_torus_to_single_point_rot_rnd),
+  ANIM_PS_REF(single_hopf_torus_to_single_point_loosen)
+};
+
+ANIMS_DEF(single_hopf_torus_to_single_point) = {
+  ANIMS_M_NUM(single_hopf_torus_to_single_point),
+  ANIMS_M_NAME(single_hopf_torus_to_single_point)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   torus to a single torus. */
+
+/* Reduce the Hopf torus to a Hopf torus of one fifth the point density and
+   then deform it to a torus on the equator. */
+
+/* Phase 1: Decrease the density of the Hopf torus.  Already defined above. */
+
+/* Phase 2: Deform the Hopf torus to a torus on the equator. */
+ANIM_SO_DEF(single_hopf_torus_to_single_loose_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_to_single_loose_torus) = {
+  ANIM_SO_NUM(single_hopf_torus_to_single_loose_torus),
+  ANIM_SO_NAME(single_hopf_torus_to_single_loose_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_to_single_torus_loosen) = {
+  ANIM_MO_REF(single_dense_hopf_torus_to_single_hopf_torus),
+  ANIM_MO_REF(single_hopf_torus_to_single_loose_torus)
+};
+
+ANIM_PS_DEF(single_hopf_torus_to_single_torus_loosen) = {
+  ANIM_PH_NUM(single_hopf_torus_to_single_torus_loosen),
+  ANIM_PH_NAME(single_hopf_torus_to_single_torus_loosen)
+};
+
+ANIMS_M_DEF(single_hopf_torus_to_single_torus) = {
+  ANIM_PS_REF(single_hopf_torus_to_single_torus_loosen)
+};
+
+ANIMS_DEF(single_hopf_torus_to_single_torus) = {
+  ANIMS_M_NUM(single_hopf_torus_to_single_torus),
+  ANIMS_M_NAME(single_hopf_torus_to_single_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a Hopf torus
+   to a double torus. */
+
+/* Decrease the density of the Hopf torus by a factor of five, disentangle
+   and deform the Hopf torus to two loose tori at latitude ±45°, and
+   increase the density of the two tori by a factor of two. */
+
+/* Phase 1: Decrease the density of the Hopf torus by a factor of five. */
+ANIM_SO_DEF(hopf_torus_to_loose_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -2.0f*M_PI_F/60.0f,   0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/60.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/60.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    2.0f*M_PI_F/60.0f,    0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(hopf_torus_to_loose_hopf_torus) = {
+  ANIM_SO_NUM(hopf_torus_to_loose_hopf_torus),
+  ANIM_SO_NAME(hopf_torus_to_loose_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Deform the interleaved Hopf torus to two loose tori. */
+ANIM_SO_DEF(loose_hopf_torus_to_loose_double_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/12.0f,         M_PI_F/12.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(loose_hopf_torus_to_loose_double_torus) = {
+  ANIM_SO_NUM(loose_hopf_torus_to_loose_double_torus),
+  ANIM_SO_NAME(loose_hopf_torus_to_loose_double_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+/* Phase 3: Increase the density of the tori at ±45°. */
+ANIM_SO_DEF(loose_double_torus_to_double_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/4.0f,          M_PI_F/4.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F/12.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/12.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    3.0f*M_PI_F/4.0f,     3.0f*M_PI_F/4.0f,     EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/12.0f,         M_PI_F/12.0f,         EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    12,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(loose_double_torus_to_double_torus) = {
+  ANIM_SO_NUM(loose_double_torus_to_double_torus),
+  ANIM_SO_NAME(loose_double_torus_to_double_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_to_double_torus_loosen) = {
+  ANIM_MO_REF(hopf_torus_to_loose_hopf_torus),
+  ANIM_MO_REF(loose_hopf_torus_to_loose_double_torus),
+  ANIM_MO_REF(loose_double_torus_to_double_torus)
+};
+
+ANIM_PS_DEF(single_hopf_torus_to_double_torus_loosen) = {
+  ANIM_PH_NUM(single_hopf_torus_to_double_torus_loosen),
+  ANIM_PH_NAME(single_hopf_torus_to_double_torus_loosen)
+};
+
+ANIMS_M_DEF(single_hopf_torus_to_double_torus) = {
+  ANIM_PS_REF(single_hopf_torus_to_double_torus_loosen)
+};
+
+ANIMS_DEF(single_hopf_torus_to_double_torus) = {
+  ANIMS_M_NUM(single_hopf_torus_to_double_torus),
+  ANIMS_M_NAME(single_hopf_torus_to_double_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   torus to a triple torus. */
+
+/* Transform Hopf torus on the equator into three tori while decreasing the
+   point density of the two tori at latitude ±45° by a factor of two. */
+ANIM_SO_DEF(single_hopf_torus_to_triple_torus_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -2.0f*M_PI_F/60.0f,   0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -1.0f*M_PI_F/60.0f,   0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    1.0f*M_PI_F/60.0f,    0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    2.0f*M_PI_F/60.0f,    0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_to_triple_torus_loosen) = {
+  ANIM_SO_NUM(single_hopf_torus_to_triple_torus_loosen),
+  ANIM_SO_NAME(single_hopf_torus_to_triple_torus_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  120                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_to_triple_torus_loosen) = {
+  ANIM_MO_REF(single_hopf_torus_to_triple_torus_loosen)
+};
+
+ANIM_PS_DEF(single_hopf_torus_to_triple_torus_loosen) = {
+  ANIM_PH_NUM(single_hopf_torus_to_triple_torus_loosen),
+  ANIM_PH_NAME(single_hopf_torus_to_triple_torus_loosen)
+};
+
+ANIMS_M_DEF(single_hopf_torus_to_triple_torus) = {
+  ANIM_PS_REF(single_hopf_torus_to_triple_torus_loosen)
+};
+
+ANIMS_DEF(single_hopf_torus_to_triple_torus) = {
+  ANIMS_M_NUM(single_hopf_torus_to_triple_torus),
+  ANIMS_M_NAME(single_hopf_torus_to_triple_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   torus to a single Seifert surface. */
+
+/* Shrink the Hopf torus, decrease its density to one fifth, and deform it
+   to a Seifert surface on the equator. */
+ANIM_SO_DEF(single_hopf_torus_to_single_seifert_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    61.0f*M_PI_F/60.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    62.0f*M_PI_F/60.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    63.0f*M_PI_F/60.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    64.0f*M_PI_F/60.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_to_single_seifert_loosen) = {
+  ANIM_SO_NUM(single_hopf_torus_to_single_seifert_loosen),
+  ANIM_SO_NAME(single_hopf_torus_to_single_seifert_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_to_single_seifert_loosen) = {
+  ANIM_MO_REF(single_hopf_torus_to_single_seifert_loosen)
+};
+
+ANIM_PS_DEF(single_hopf_torus_to_single_seifert_loosen) = {
+  ANIM_PH_NUM(single_hopf_torus_to_single_seifert_loosen),
+  ANIM_PH_NAME(single_hopf_torus_to_single_seifert_loosen)
+};
+
+ANIMS_M_DEF(single_hopf_torus_to_single_seifert) = {
+  ANIM_PS_REF(single_hopf_torus_to_single_seifert_loosen)
+};
+
+ANIMS_DEF(single_hopf_torus_to_single_seifert) = {
+  ANIMS_M_NUM(single_hopf_torus_to_single_seifert),
+  ANIMS_M_NAME(single_hopf_torus_to_single_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   torus to a triple Seifert surface. */
+
+/* Transform the Hopf torus on the equator into three Seifert surfaces while
+   decreasing the point density of the two Seifert surfaces at latitude ±45°
+   by a factor of two. */
+ANIM_SO_DEF(single_hopf_torus_to_triple_seifert_loosen) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    58.0f*M_PI_F/60.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    59.0f*M_PI_F/60.0f,   M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/2.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/60.0f,         M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          3.0f*M_PI_F/4.0f,     EASING_CUBIC, /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    2.0f*M_PI_F/60.0f,    M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_to_triple_seifert_loosen) = {
+  ANIM_SO_NUM(single_hopf_torus_to_triple_seifert_loosen),
+  ANIM_SO_NAME(single_hopf_torus_to_triple_seifert_loosen),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_to_triple_seifert_loosen) = {
+  ANIM_MO_REF(single_hopf_torus_to_triple_seifert_loosen)
+};
+
+ANIM_PS_DEF(single_hopf_torus_to_triple_seifert_loosen) = {
+  ANIM_PH_NUM(single_hopf_torus_to_triple_seifert_loosen),
+  ANIM_PH_NAME(single_hopf_torus_to_triple_seifert_loosen)
+};
+
+ANIMS_M_DEF(single_hopf_torus_to_triple_seifert) = {
+  ANIM_PS_REF(single_hopf_torus_to_triple_seifert_loosen)
+};
+
+ANIMS_DEF(single_hopf_torus_to_triple_seifert) = {
+  ANIMS_M_NUM(single_hopf_torus_to_triple_seifert),
+  ANIMS_M_NAME(single_hopf_torus_to_triple_seifert)
+};
+
+
+
+/* The set of possible animations for a single Hopf torus. */
+
+/* Rotate a Hopf torus around the x axis of the total space. */
+ANIM_SO_DEF(single_hopf_torus_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_rot_x) = {
+  ANIM_SO_NUM(single_hopf_torus_rot_x),
+  ANIM_SO_NAME(single_hopf_torus_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_rot_x) = {
+  ANIM_MO_REF(single_hopf_torus_rot_x)
+};
+
+ANIM_PS_DEF(single_hopf_torus_rot_x) = {
+  ANIM_PH_NUM(single_hopf_torus_rot_x),
+  ANIM_PH_NAME(single_hopf_torus_rot_x)
+};
+
+/* Rotate a Hopf torus around the z axis. */
+ANIM_SO_DEF(single_hopf_torus_rot_z) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_rot_z) = {
+  ANIM_SO_NUM(single_hopf_torus_rot_z),
+  ANIM_SO_NAME(single_hopf_torus_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_rot_z) = {
+  ANIM_MO_REF(single_hopf_torus_rot_z)
+};
+
+ANIM_PS_DEF(single_hopf_torus_rot_z) = {
+  ANIM_PH_NUM(single_hopf_torus_rot_z),
+  ANIM_PH_NAME(single_hopf_torus_rot_z)
+};
+
+/* Rotate a Hopf torus around a random axis. */
+ANIM_SO_DEF(single_hopf_torus_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_rot_rnd) = {
+  ANIM_SO_NUM(single_hopf_torus_rot_rnd),
+  ANIM_SO_NAME(single_hopf_torus_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_rot_rnd) = {
+  ANIM_MO_REF(single_hopf_torus_rot_rnd)
+};
+
+ANIM_PS_DEF(single_hopf_torus_rot_rnd) = {
+  ANIM_PH_NUM(single_hopf_torus_rot_rnd),
+  ANIM_PH_NAME(single_hopf_torus_rot_rnd)
+};
+
+/* Wave a Hopf torus around a random axis, i.e., animate the parameter q,
+   and rotate it with a certain probability around a random axis. */
+ANIM_SO_DEF(single_hopf_torus_wave) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    -M_PI_F/8.0f,         M_PI_F/8.0f,          EASING_COS,   /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_wave) = {
+  ANIM_SO_NUM(single_hopf_torus_wave),
+  ANIM_SO_NAME(single_hopf_torus_wave),
+  0.25f,                                        EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_wave) = {
+  ANIM_MO_REF(single_hopf_torus_wave)
+};
+
+ANIM_PS_DEF(single_hopf_torus_wave) = {
+  ANIM_PH_NUM(single_hopf_torus_wave),
+  ANIM_PH_NAME(single_hopf_torus_wave)
+};
+
+/* Move a Hopf torus on the equator up and down to latitude ±60° while
+   rotating it around the z axis and, with a certain probability, around
+   a random axis. */
+ANIM_SO_DEF(single_hopf_torus_move) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    1.0f*M_PI_F/6.0f,     5.0f*M_PI_F/6.0f,     EASING_SIN,   /* p */
+    M_PI_F/24.0f,         M_PI_F/8.0f,          EASING_COS,   /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_move) = {
+  ANIM_SO_NUM(single_hopf_torus_move),
+  ANIM_SO_NAME(single_hopf_torus_move),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_torus_move) = {
+  ANIM_MO_REF(single_hopf_torus_move)
+};
+
+ANIM_PS_DEF(single_hopf_torus_move) = {
+  ANIM_PH_NUM(single_hopf_torus_move),
+  ANIM_PH_NAME(single_hopf_torus_move)
+};
+
+/* Deform the Hopf torus into a Hopf flower, rotate the Hopf flower around
+   the x axis of the total space, and deform the Hopf flower back to the
+   Hopf torus. */
+
+/* Phase 1: Deform the Hopf torus into a Hopf flower. */
+ANIM_SO_DEF(single_hopf_torus_to_single_hopf_flower) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    0.0f,                 -0.5f,                EASING_CUBIC, /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_to_single_hopf_flower) = {
+  ANIM_SO_NUM(single_hopf_torus_to_single_hopf_flower),
+  ANIM_SO_NAME(single_hopf_torus_to_single_hopf_flower),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+/* Phase 2: Rotate the Hopf flower around the x axis of the total space. */
+ANIM_SO_DEF(single_hopf_flower_rot_x) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    -0.5f,                -0.5f,                EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_flower_rot_x) = {
+  ANIM_SO_NUM(single_hopf_flower_rot_x),
+  ANIM_SO_NAME(single_hopf_flower_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 3: Deform the Hopf flower into a Hopf torus. */
+ANIM_SO_DEF(single_hopf_flower_to_single_hopf_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    -0.5f,                0.0f,                 EASING_CUBIC, /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_flower_to_single_hopf_torus) = {
+  ANIM_SO_NUM(single_hopf_flower_to_single_hopf_torus),
+  ANIM_SO_NAME(single_hopf_flower_to_single_hopf_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_flower_rot_x) = {
+  ANIM_MO_REF(single_hopf_torus_to_single_hopf_flower),
+  ANIM_MO_REF(single_hopf_flower_rot_x),
+  ANIM_MO_REF(single_hopf_flower_to_single_hopf_torus)
+};
+
+ANIM_PS_DEF(single_hopf_flower_rot_x) = {
+  ANIM_PH_NUM(single_hopf_flower_rot_x),
+  ANIM_PH_NAME(single_hopf_flower_rot_x)
+};
+
+/* Deform the Hopf torus into a Hopf flower, rotate the Hopf flower around
+   the a random axis, and deform the Hopf flower back to the Hopf torus. */
+
+/* Phase 1: Deform the Hopf torus into a Hopf flower.  Already defined
+   above. */
+
+/* Phase 2: Rotate a Hopf flower around a random axis. */
+ANIM_SO_DEF(single_hopf_flower_rot_rnd) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          M_PI_F/8.0f,          EASING_NONE,  /* q */
+    -0.5f,                -0.5f,                EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_flower_rot_rnd) = {
+  ANIM_SO_NUM(single_hopf_flower_rot_rnd),
+  ANIM_SO_NAME(single_hopf_flower_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+/* Phase 3: Deform the Hopf flower into a Hopf torus.  Already defined
+   above. */
+
+ANIM_PH_DEF(single_hopf_flower_rot_rnd) = {
+  ANIM_MO_REF(single_hopf_torus_to_single_hopf_flower),
+  ANIM_MO_REF(single_hopf_flower_rot_rnd),
+  ANIM_MO_REF(single_hopf_flower_to_single_hopf_torus)
+};
+
+ANIM_PS_DEF(single_hopf_flower_rot_rnd) = {
+  ANIM_PH_NUM(single_hopf_flower_rot_rnd),
+  ANIM_PH_NAME(single_hopf_flower_rot_rnd)
+};
+
+/* Deform the Hopf torus into a Hopf flower, move the Hopf flower on the
+   equator up and down to latitude ±60° while rotating it around the z axis
+   and, with a certain probability, around a random axis, and deform the
+   Hopf flower back to the Hopf torus. */
+
+/* Phase 1: Deform the Hopf torus into a Hopf flower.  Already defined
+   above. */
+
+/* Phase 2: Move the Hopf flower on the equator up and down to latitude ±60°
+   while rotating it around the z axis and, with a certain probability,
+   around a random axis. */
+ANIM_SO_DEF(single_hopf_flower_move) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    1.0f*M_PI_F/6.0f,     5.0f*M_PI_F/6.0f,     EASING_SIN,   /* p */
+    M_PI_F/16.0f,         M_PI_F/8.0f,          EASING_COS,   /* q */
+    -0.5f,                -0.5f,                EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    120,                                /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_flower_move) = {
+  ANIM_SO_NUM(single_hopf_flower_move),
+  ANIM_SO_NAME(single_hopf_flower_move),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+/* Phase 3: Deform the Hopf flower into a Hopf torus.  Already defined
+   above. */
+
+ANIM_PH_DEF(single_hopf_flower_move) = {
+  ANIM_MO_REF(single_hopf_torus_to_single_hopf_flower),
+  ANIM_MO_REF(single_hopf_flower_move),
+  ANIM_MO_REF(single_hopf_flower_to_single_hopf_torus)
+};
+
+ANIM_PS_DEF(single_hopf_flower_move) = {
+  ANIM_PH_NUM(single_hopf_flower_move),
+  ANIM_PH_NAME(single_hopf_flower_move)
+};
+
+ANIMS_M_DEF(single_hopf_torus) = {
+  ANIM_PS_REF(single_hopf_torus_rot_x),
+  ANIM_PS_REF(single_hopf_torus_rot_z),
+  ANIM_PS_REF(single_hopf_torus_rot_rnd),
+  ANIM_PS_REF(single_hopf_torus_move),
+  ANIM_PS_REF(single_hopf_torus_wave),
+  ANIM_PS_REF(single_hopf_flower_rot_x),
+  ANIM_PS_REF(single_hopf_flower_rot_rnd),
+  ANIM_PS_REF(single_hopf_flower_move)
+};
+
+ANIMS_DEF(single_hopf_torus) = {
+  ANIMS_M_NUM(single_hopf_torus),
+  ANIMS_M_NAME(single_hopf_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   torus to a single Hopf spiral. */
+
+/* Reduce the Hopf torus to a Hopf torus of three fifths the point density
+   and deform it to a point on the equator, then deform the torus to a Hopf
+   spiral. */
+
+/* Phase 1: Decrease the density of the Hopf torus and deform it to a torus
+   on the equator. */
+ANIM_SO_DEF(single_hopf_torus_to_single_mid_density_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    1.0f*M_PI_F/60.0f,    1.0f*M_PI_F/36.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    2.0f*M_PI_F/60.0f,    1.0f*M_PI_F/36.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    3.0f*M_PI_F/60.0f,    2.0f*M_PI_F/36.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    M_PI_F/8.0f,          0.0f,                 EASING_CUBIC, /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    4.0f*M_PI_F/60.0f,    2.0f*M_PI_F/36.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    4,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_torus_to_single_mid_density_torus) = {
+  ANIM_SO_NUM(single_hopf_torus_to_single_mid_density_torus),
+  ANIM_SO_NAME(single_hopf_torus_to_single_mid_density_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  90                                                          /* num_steps */
+};
+
+/* Phase 2: Wind the collapsed spiral and expand it.  Already defined above. */
+
+ANIM_PH_DEF(single_hopf_torus_to_single_hopf_spiral_wind) = {
+  ANIM_MO_REF(single_hopf_torus_to_single_mid_density_torus),
+  ANIM_MO_REF(single_dense_unwound_torus_to_single_hopf_spiral)
+};
+
+ANIM_PS_DEF(single_hopf_torus_to_single_hopf_spiral_wind) = {
+  ANIM_PH_NUM(single_hopf_torus_to_single_hopf_spiral_wind),
+  ANIM_PH_NAME(single_hopf_torus_to_single_hopf_spiral_wind)
+};
+
+ANIMS_M_DEF(single_hopf_torus_to_single_hopf_spiral) = {
+  ANIM_PS_REF(single_hopf_torus_to_single_hopf_spiral_wind)
+};
+
+ANIMS_DEF(single_hopf_torus_to_single_hopf_spiral) = {
+  ANIMS_M_NUM(single_hopf_torus_to_single_hopf_spiral),
+  ANIMS_M_NAME(single_hopf_torus_to_single_hopf_spiral)
+};
+
+
+
+/*****************************************************************************
+ * The set of all transformations from a single Hopf spiral.
+ *****************************************************************************/
+
+/* The set of possible animations for the transition from a single Hopf
+   spiral to a single point. */
+
+/* Shrink a Hopf spiral to a point on the equator and rotate around a random
+   axis with a certain probability. */
+ANIM_SO_DEF(single_hopf_spiral_to_single_point_shrink_sector) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          0.0f,                 EASING_CUBIC, /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_single_point_shrink_sector) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_single_point_shrink_sector),
+  ANIM_SO_NAME(single_hopf_spiral_to_single_point_shrink_sector),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_single_point_shrink_sector) = {
+  ANIM_MO_REF(single_hopf_spiral_to_single_point_shrink_sector)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_single_point_shrink_sector) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_single_point_shrink_sector),
+  ANIM_PH_NAME(single_hopf_spiral_to_single_point_shrink_sector)
+};
+
+ANIMS_M_DEF(single_hopf_spiral_to_single_point) = {
+  ANIM_PS_REF(single_hopf_spiral_to_single_point_shrink_sector)
+};
+
+ANIMS_DEF(single_hopf_spiral_to_single_point) = {
+  ANIMS_M_NUM(single_hopf_spiral_to_single_point),
+  ANIMS_M_NAME(single_hopf_spiral_to_single_point)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   spiral to a single torus. */
+
+/* Decrease the winding number of a Hopf spiral to one and collapse its
+   height to a torus on the equator, and then decrease the point density
+   of the torus by a factor of three. */
+
+/* Phase 1: Unwind the spiral and collapse it to a dense torus. */
+ANIM_SO_DEF(single_hopf_spiral_to_single_dense_torus_unwind) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_single_dense_torus_unwind) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_single_dense_torus_unwind),
+  ANIM_SO_NAME(single_hopf_spiral_to_single_dense_torus_unwind),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+/* Phase 2: Decrease the density of the torus by a factor of three. */
+ANIM_SO_DEF(single_dense_unwound_torus_to_single_torus) = {
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    M_PI_F/36.0f,         0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    0.0f,                 0.0f,                 EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_TORUS,                                                /* generator */
+    M_PI_F/2.0f,          M_PI_F/2.0f,          EASING_NONE,  /* p */
+    0.0f,                 0.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/36.0f,        0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_unwound_torus_to_single_torus) = {
+  ANIM_SO_NUM(single_dense_unwound_torus_to_single_torus),
+  ANIM_SO_NAME(single_dense_unwound_torus_to_single_torus),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_single_torus_unwind) = {
+  ANIM_MO_REF(single_hopf_spiral_to_single_dense_torus_unwind),
+  ANIM_MO_REF(single_dense_unwound_torus_to_single_torus)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_single_torus_unwind) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_single_torus_unwind),
+  ANIM_PH_NAME(single_hopf_spiral_to_single_torus_unwind)
+};
+
+/* Increase the winding number of a Hopf spiral to three and collapse its
+   height to a torus on the equator. */
+ANIM_SO_DEF(single_hopf_spiral_to_single_torus_wind) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 3.0f,                 EASING_CUBIC, /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_single_torus_wind) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_single_torus_wind),
+  ANIM_SO_NAME(single_hopf_spiral_to_single_torus_wind),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_single_torus_wind) = {
+  ANIM_MO_REF(single_hopf_spiral_to_single_torus_wind)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_single_torus_wind) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_single_torus_wind),
+  ANIM_PH_NAME(single_hopf_spiral_to_single_torus_wind)
+};
+
+ANIMS_M_DEF(single_hopf_spiral_to_single_torus) = {
+  ANIM_PS_REF(single_hopf_spiral_to_single_torus_unwind),
+  ANIM_PS_REF(single_hopf_spiral_to_single_torus_wind)
+};
+
+ANIMS_DEF(single_hopf_spiral_to_single_torus) = {
+  ANIMS_M_NUM(single_hopf_spiral_to_single_torus),
+  ANIMS_M_NAME(single_hopf_spiral_to_single_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   spiral to a double torus. */
+
+/* Decrease the point density of a Hopf spiral by a factor of two thirds,
+   split the spiral into two parts, and deform the parts to two tori at
+   latitiude ±45°. */
+
+/* Phase 1: Decrease the density of the Hopf spiral by a factor of two
+   thirds. */
+ANIM_SO_DEF(single_hopf_spiral_to_single_loose_hopf_spiral) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -35.0f*M_PI_F/36.0f,  -23.0f*M_PI_F/24.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -34.0f*M_PI_F/36.0f,  -23.0f*M_PI_F/24.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_single_loose_hopf_spiral) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_single_loose_hopf_spiral),
+  ANIM_SO_NAME(single_hopf_spiral_to_single_loose_hopf_spiral),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  60                                                          /* num_steps */
+};
+
+/* Phase 2: Split the spiral into two parts and deform it to two tori at
+   latitiude ±45°. */
+ANIM_SO_DEF(single_loose_hopf_spiral_to_double_torus_split) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 -M_PI_F/4.0f,         EASING_CUBIC, /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -2.0f*M_PI_F,         EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    0.0f,                 M_PI_F,               EASING_CUBIC, /* offset */
+    M_PI_F,               M_PI_F,               EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_loose_hopf_spiral_to_double_torus_split) = {
+  ANIM_SO_NUM(single_loose_hopf_spiral_to_double_torus_split),
+  ANIM_SO_NAME(single_loose_hopf_spiral_to_double_torus_split),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_double_torus_split) = {
+  ANIM_MO_REF(single_hopf_spiral_to_single_loose_hopf_spiral),
+  ANIM_MO_REF(single_loose_hopf_spiral_to_double_torus_split)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_double_torus_split) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_double_torus_split),
+  ANIM_PH_NAME(single_hopf_spiral_to_double_torus_split)
+};
+
+ANIMS_M_DEF(single_hopf_spiral_to_double_torus) = {
+  ANIM_PS_REF(single_hopf_spiral_to_double_torus_split)
+};
+
+ANIMS_DEF(single_hopf_spiral_to_double_torus) = {
+  ANIMS_M_NUM(single_hopf_spiral_to_double_torus),
+  ANIMS_M_NAME(single_hopf_spiral_to_double_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   spiral to a triple torus. */
+
+/* Split the Hopf spiral into three parts and Deform them to three tori. */
+ANIM_SO_DEF(single_hopf_spiral_to_triple_torus_split) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 -M_PI_F/4.0f,         EASING_CUBIC, /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -2.0f*M_PI_F,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/3.0f,         -M_PI_F/2.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    M_PI_F/3.0f,          M_PI_F,               EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_triple_torus_split) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_triple_torus_split),
+  ANIM_SO_NAME(single_hopf_spiral_to_triple_torus_split),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_triple_torus_split) = {
+  ANIM_MO_REF(single_hopf_spiral_to_triple_torus_split)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_triple_torus_split) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_triple_torus_split),
+  ANIM_PH_NAME(single_hopf_spiral_to_triple_torus_split)
+};
+
+ANIMS_M_DEF(single_hopf_spiral_to_triple_torus) = {
+  ANIM_PS_REF(single_hopf_spiral_to_triple_torus_split)
+};
+
+ANIMS_DEF(single_hopf_spiral_to_triple_torus) = {
+  ANIMS_M_NUM(single_hopf_spiral_to_triple_torus),
+  ANIMS_M_NAME(single_hopf_spiral_to_triple_torus)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   spiral to a single Seifert surface. */
+
+/* Decrease the winding number of a Hopf spiral to one and collapse its
+   height to a Seifert surface on the equator, and decrease the point
+   density by a factor of three. */
+ANIM_SO_DEF(single_hopf_spiral_to_single_seifert_unwind_hor) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -M_PI_F,              M_PI_F/24.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -35.0f*M_PI_F/36.0f,  M_PI_F/24.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -34.0f*M_PI_F/36.0f,  M_PI_F/24.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          M_PI_F,               EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_single_seifert_unwind_hor) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_single_seifert_unwind_hor),
+  ANIM_SO_NAME(single_hopf_spiral_to_single_seifert_unwind_hor),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_single_seifert_unwind_hor) = {
+  ANIM_MO_REF(single_hopf_spiral_to_single_seifert_unwind_hor)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_single_seifert_unwind_hor) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_single_seifert_unwind_hor),
+  ANIM_PH_NAME(single_hopf_spiral_to_single_seifert_unwind_hor)
+};
+
+/* Decrease the winding number of a Hopf spiral to zero to create a vertical
+   Seifert surface, then decrease the point density by a factor of three and
+   rotate it to the equator. */
+
+/* Phase 1: Unwind the spiral and collapse it to a dense vertical Seifert
+   surface. */
+ANIM_SO_DEF(single_hopf_spiral_to_single_dense_seifert_unwind) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 0.0f,                 EASING_CUBIC, /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_single_dense_seifert_unwind) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_single_dense_seifert_unwind),
+  ANIM_SO_NAME(single_hopf_spiral_to_single_dense_seifert_unwind),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  180                                                         /* num_steps */
+};
+
+/* Phase 2: Decrease the density of the vertical Seifert surface and rotate
+   it to its canonical position. */
+ANIM_SO_DEF(single_dense_seifert_vert_to_single_seifert) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -2.0f*M_PI_F,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -35.0f*M_PI_F/36.0f,  -2.0f*M_PI_F,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    0.0f,                 0.0f,                 EASING_NONE,  /* r */
+    -34.0f*M_PI_F/36.0f,  -2.0f*M_PI_F,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 1.0f, 0.0f, 0.0f }, 0.0f, M_PI_F/2.0f,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_dense_seifert_vert_to_single_seifert) = {
+  ANIM_SO_NUM(single_dense_seifert_vert_to_single_seifert),
+  ANIM_SO_NAME(single_dense_seifert_vert_to_single_seifert),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  120                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_single_seifert_unwind_vert) = {
+  ANIM_MO_REF(single_hopf_spiral_to_single_dense_seifert_unwind),
+  ANIM_MO_REF(single_dense_seifert_vert_to_single_seifert)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_single_seifert_unwind_vert) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_single_seifert_unwind_vert),
+  ANIM_PH_NAME(single_hopf_spiral_to_single_seifert_unwind_vert)
+};
+
+ANIMS_M_DEF(single_hopf_spiral_to_single_seifert) = {
+  ANIM_PS_REF(single_hopf_spiral_to_single_seifert_unwind_hor),
+  ANIM_PS_REF(single_hopf_spiral_to_single_seifert_unwind_vert)
+};
+
+ANIMS_DEF(single_hopf_spiral_to_single_seifert) = {
+  ANIMS_M_NUM(single_hopf_spiral_to_single_seifert),
+  ANIMS_M_NAME(single_hopf_spiral_to_single_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   spiral to a triple Seifert surface. */
+
+/* Deform the three Seifert surfaces to three Hopf spirals and merge them. */
+ANIM_SO_DEF(single_hopf_spiral_to_triple_seifert_split) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 -M_PI_F/4.0f,         EASING_CUBIC, /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -95.0f*M_PI_F/48.0f,  EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F/3.0f,         M_PI_F/48.0f,         EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 M_PI_F/4.0f,          EASING_CUBIC, /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    M_PI_F/3.0f,          49.0f*M_PI_F/48.0f,   EASING_CUBIC, /* offset */
+    2.0f*M_PI_F/3.0f,     M_PI_F/2.0f,          EASING_CUBIC, /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_triple_seifert_split) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_triple_seifert_split),
+  ANIM_SO_NAME(single_hopf_spiral_to_triple_seifert_split),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_to_triple_seifert_split) = {
+  ANIM_MO_REF(single_hopf_spiral_to_triple_seifert_split)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_triple_seifert_split) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_triple_seifert_split),
+  ANIM_PH_NAME(single_hopf_spiral_to_triple_seifert_split)
+};
+
+ANIMS_M_DEF(single_hopf_spiral_to_triple_seifert) = {
+  ANIM_PS_REF(single_hopf_spiral_to_triple_seifert_split)
+};
+
+ANIMS_DEF(single_hopf_spiral_to_triple_seifert) = {
+  ANIMS_M_NUM(single_hopf_spiral_to_triple_seifert),
+  ANIMS_M_NAME(single_hopf_spiral_to_triple_seifert)
+};
+
+
+
+/* The set of possible animations for the transition from a single Hopf
+   spiral to a single Hopf torus. */
+
+/* Decrease the winding number of a Hopf spiral to one and collapse its
+   height to a torus on the equator, and increase the point density
+   of the torus by a factor of five thirds, then deform the torus to a
+   Hopf torus. */
+
+/* Phase 1: Unwind the spiral and collapse it to a dense torus. */
+ANIM_SO_DEF(single_hopf_spiral_to_single_torus_densify) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -M_PI_F,              0.0f,                 EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -35.0f*M_PI_F/36.0f,  1.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -35.0f*M_PI_F/36.0f,  2.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -34.0f*M_PI_F/36.0f,  3.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  },
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 0.0f,                 EASING_CUBIC, /* q */
+    2.0f,                 1.0f,                 EASING_CUBIC, /* r */
+    -34.0f*M_PI_F/36.0f,  4.0f*M_PI_F/60.0f,    EASING_CUBIC, /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    24,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_to_single_torus_densify) = {
+  ANIM_SO_NUM(single_hopf_spiral_to_single_torus_densify),
+  ANIM_SO_NAME(single_hopf_spiral_to_single_torus_densify),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  240                                                         /* num_steps */
+};
+
+/* Phase 2: Deform the torus on the equator to a Hopf torus.  Already
+   defined above. */
+
+ANIM_PH_DEF(single_hopf_spiral_to_single_hopf_torus_unwind) = {
+  ANIM_MO_REF(single_hopf_spiral_to_single_torus_densify),
+  ANIM_MO_REF(single_dense_torus_to_single_hopf_torus)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_to_single_hopf_torus_unwind) = {
+  ANIM_PH_NUM(single_hopf_spiral_to_single_hopf_torus_unwind),
+  ANIM_PH_NAME(single_hopf_spiral_to_single_hopf_torus_unwind)
+};
+
+ANIMS_M_DEF(single_hopf_spiral_to_single_hopf_torus) = {
+  ANIM_PS_REF(single_hopf_spiral_to_single_hopf_torus_unwind)
+};
+
+ANIMS_DEF(single_hopf_spiral_to_single_hopf_torus) = {
+  ANIMS_M_NUM(single_hopf_spiral_to_single_hopf_torus),
+  ANIMS_M_NAME(single_hopf_spiral_to_single_hopf_torus)
+};
+
+
+
+/* The set of possible animations for a single Hopf spiral. */
+
+/* Rotate a Hopf torus around the x axis of the total space. */
+ANIM_SO_DEF(single_hopf_spiral_rot_x) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_rot_x) = {
+  ANIM_SO_NUM(single_hopf_spiral_rot_x),
+  ANIM_SO_NAME(single_hopf_spiral_rot_x),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 1.0f, 0.0f, 0.0f }, 0.0f, 2.0f*M_PI_F,      EASING_CUBIC, /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_rot_x) = {
+  ANIM_MO_REF(single_hopf_spiral_rot_x)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_rot_x) = {
+  ANIM_PH_NUM(single_hopf_spiral_rot_x),
+  ANIM_PH_NAME(single_hopf_spiral_rot_x)
+};
+
+/* Rotate a Hopf torus around the z axis. */
+ANIM_SO_DEF(single_hopf_spiral_rot_z) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 1.0f }, 0.0f, 2.0f*M_PI_F,    EASING_CUBIC  /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_rot_z) = {
+  ANIM_SO_NUM(single_hopf_spiral_rot_z),
+  ANIM_SO_NAME(single_hopf_spiral_rot_z),
+  0.0f,                                         EASING_NONE,  /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_rot_z) = {
+  ANIM_MO_REF(single_hopf_spiral_rot_z)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_rot_z) = {
+  ANIM_PH_NUM(single_hopf_spiral_rot_z),
+  ANIM_PH_NAME(single_hopf_spiral_rot_z)
+};
+
+/* Rotate a Hopf spiral around a random axis. */
+ANIM_SO_DEF(single_hopf_spiral_rot_rnd) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    2.0f,                 2.0f,                 EASING_NONE,  /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_rot_rnd) = {
+  ANIM_SO_NUM(single_hopf_spiral_rot_rnd),
+  ANIM_SO_NAME(single_hopf_spiral_rot_rnd),
+  1.0f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_rot_rnd) = {
+  ANIM_MO_REF(single_hopf_spiral_rot_rnd)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_rot_rnd) = {
+  ANIM_PH_NUM(single_hopf_spiral_rot_rnd),
+  ANIM_PH_NAME(single_hopf_spiral_rot_rnd)
+};
+
+/* Animate the winding number of a Hopf spiral and rotate it with a certain
+   probability around a random axis. */
+ANIM_SO_DEF(single_hopf_spiral_winding) = {
+  {
+    GEN_SPIRAL,                                               /* generator */
+    0.0f,                 0.0f,                 EASING_NONE,  /* p */
+    1.0f,                 1.0f,                 EASING_NONE,  /* q */
+    1.0f,                 3.0f,                 EASING_SIN,   /* r */
+    -M_PI_F,              -M_PI_F,              EASING_NONE,  /* offset */
+    2.0f*M_PI_F,          2.0f*M_PI_F,          EASING_NONE,  /* sector */
+    0,                    72,                                 /* n, num */
+    { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,           EASING_NONE   /* rot_axis */
+  }
+};
+
+ANIM_MO_DEF(single_hopf_spiral_winding) = {
+  ANIM_SO_NUM(single_hopf_spiral_winding),
+  ANIM_SO_NAME(single_hopf_spiral_winding),
+  0.5f,                                         EASING_CUBIC, /* rotate_prob */
+  { 0.0f, 0.0f, 0.0f }, 0.0f, 0.0f,             EASING_NONE,  /* rot_space */
+  360                                                         /* num_steps */
+};
+
+ANIM_PH_DEF(single_hopf_spiral_winding) = {
+  ANIM_MO_REF(single_hopf_spiral_winding)
+};
+
+ANIM_PS_DEF(single_hopf_spiral_winding) = {
+  ANIM_PH_NUM(single_hopf_spiral_winding),
+  ANIM_PH_NAME(single_hopf_spiral_winding)
+};
+
+ANIMS_M_DEF(single_hopf_spiral) = {
+  ANIM_PS_REF(single_hopf_spiral_rot_x),
+  ANIM_PS_REF(single_hopf_spiral_rot_z),
+  ANIM_PS_REF(single_hopf_spiral_rot_rnd),
+  ANIM_PS_REF(single_hopf_spiral_winding)
+};
+
+ANIMS_DEF(single_hopf_spiral) = {
+  ANIMS_M_NUM(single_hopf_spiral),
+  ANIMS_M_NAME(single_hopf_spiral)
+};
+
+
+
+/*****************************************************************************
+ * The set of all possible animations.
+ *****************************************************************************/
+animations *hopf_animations[NUM_ANIM_STATES][NUM_ANIM_STATES] = {
+  {
+    ANIMS_REF(single_point),
+    ANIMS_REF(single_point_to_single_torus),
+    ANIMS_REF(single_point_to_double_torus),
+    ANIMS_REF(single_point_to_triple_torus),
+    ANIMS_REF(single_point_to_single_seifert),
+    ANIMS_REF(single_point_to_triple_seifert),
+    ANIMS_REF(single_point_to_single_hopf_torus),
+    ANIMS_REF(single_point_to_single_hopf_spiral)
+  },
+  {
+    ANIMS_REF(single_torus_to_single_point),
+    ANIMS_REF(single_torus),
+    ANIMS_REF(single_torus_to_double_torus),
+    ANIMS_REF(single_torus_to_triple_torus),
+    ANIMS_REF(single_torus_to_single_seifert),
+    ANIMS_REF(single_torus_to_triple_seifert),
+    ANIMS_REF(single_torus_to_single_hopf_torus),
+    ANIMS_REF(single_torus_to_single_hopf_spiral)
+  },
+  {
+    ANIMS_REF(double_torus_to_single_point),
+    ANIMS_REF(double_torus_to_single_torus),
+    ANIMS_REF(double_torus),
+    ANIMS_REF(double_torus_to_triple_torus),
+    ANIMS_REF(double_torus_to_single_seifert),
+    ANIMS_REF(double_torus_to_triple_seifert),
+    ANIMS_REF(double_torus_to_single_hopf_torus),
+    ANIMS_REF(double_torus_to_single_hopf_spiral)
+  },
+  {
+    ANIMS_REF(triple_torus_to_single_point),
+    ANIMS_REF(triple_torus_to_single_torus),
+    ANIMS_REF(triple_torus_to_double_torus),
+    ANIMS_REF(triple_torus),
+    ANIMS_REF(triple_torus_to_single_seifert),
+    ANIMS_REF(triple_torus_to_triple_seifert),
+    ANIMS_REF(triple_torus_to_single_hopf_torus),
+    ANIMS_REF(triple_torus_to_single_hopf_spiral)
+  },
+  {
+    ANIMS_REF(single_seifert_to_single_point),
+    ANIMS_REF(single_seifert_to_single_torus),
+    ANIMS_REF(single_seifert_to_double_torus),
+    ANIMS_REF(single_seifert_to_triple_torus),
+    ANIMS_REF(single_seifert),
+    ANIMS_REF(single_seifert_to_triple_seifert),
+    ANIMS_REF(single_seifert_to_single_hopf_torus),
+    ANIMS_REF(single_seifert_to_single_hopf_spiral)
+  },
+  {
+    ANIMS_REF(triple_seifert_to_single_point),
+    ANIMS_REF(triple_seifert_to_single_torus),
+    ANIMS_REF(triple_seifert_to_double_torus),
+    ANIMS_REF(triple_seifert_to_triple_torus),
+    ANIMS_REF(triple_seifert_to_single_seifert),
+    ANIMS_REF(triple_seifert),
+    ANIMS_REF(triple_seifert_to_single_hopf_torus),
+    ANIMS_REF(triple_seifert_to_single_hopf_spiral),
+  },
+  {
+    ANIMS_REF(single_hopf_torus_to_single_point),
+    ANIMS_REF(single_hopf_torus_to_single_torus),
+    ANIMS_REF(single_hopf_torus_to_double_torus),
+    ANIMS_REF(single_hopf_torus_to_triple_torus),
+    ANIMS_REF(single_hopf_torus_to_single_seifert),
+    ANIMS_REF(single_hopf_torus_to_triple_seifert),
+    ANIMS_REF(single_hopf_torus),
+    ANIMS_REF(single_hopf_torus_to_single_hopf_spiral)
+  },
+  {
+    ANIMS_REF(single_hopf_spiral_to_single_point),
+    ANIMS_REF(single_hopf_spiral_to_single_torus),
+    ANIMS_REF(single_hopf_spiral_to_double_torus),
+    ANIMS_REF(single_hopf_spiral_to_triple_torus),
+    ANIMS_REF(single_hopf_spiral_to_single_seifert),
+    ANIMS_REF(single_hopf_spiral_to_triple_seifert),
+    ANIMS_REF(single_hopf_spiral_to_single_hopf_torus),
+    ANIMS_REF(single_hopf_spiral)
+  }
+};
+
+#endif /* USE_GL */
diff --git a/hacks/glx/hopfanimations.h b/hacks/glx/hopfanimations.h
new file mode 100644 (file)
index 0000000..6e0b5a7
--- /dev/null
@@ -0,0 +1,123 @@
+/* hopfanimations.h --- Definition of the animations used in the Hopf
+   fibration. */
+/* Copyright (c) 2025 Carsten Steger <carsten@mirsanmir.org>.
+ *
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose and without fee is hereby granted,
+ * provided that the above copyright notice appear in all copies and that
+ * both that copyright notice and this permission notice appear in
+ * supporting documentation.
+ *
+ * This file is provided AS IS with no warranties of any kind.  The author
+ * shall have no liability with respect to the infringement of copyrights,
+ * trade secrets or any patents by this file or any part thereof.  In no
+ * event will the author be liable for any lost revenue or profits or
+ * other special, indirect and consequential damages.
+ *
+ * REVISION HISTORY:
+ * C. Steger - 26/02/06: Initial version
+ */
+
+#ifndef __HOPFANIMATIONS_H__
+#define __HOPFANIMATIONS_H__
+
+#ifdef USE_GL
+
+#include <stdbool.h>
+
+#define M_PI_F 3.1415926535898f
+
+#define ANIM_SINGLE_POINT       0
+#define ANIM_SINGLE_TORUS       1
+#define ANIM_DOUBLE_TORUS       2
+#define ANIM_TRIPLE_TORUS       3
+#define ANIM_SINGLE_SEIFERT     4
+#define ANIM_TRIPLE_SEIFERT     5
+#define ANIM_SINGLE_HOPF_TORUS  6
+#define ANIM_SINGLE_HOPF_SPIRAL 7
+#define NUM_ANIM_STATES         (ANIM_SINGLE_HOPF_SPIRAL+1)
+
+#define MAX_ANIM_GEOM 10
+
+#define GEN_TORUS     0
+#define GEN_SPIRAL    1
+
+#define EASING_NONE  0
+#define EASING_CUBIC 1
+#define EASING_SIN   2
+#define EASING_COS   3
+#define EASING_LIN   4
+#define EASING_ACCEL 5
+#define EASING_DECEL 6
+
+
+/* A data structure that holds the data for the animation state of a single
+   object. */
+typedef struct {
+  int generator;
+  float p;
+  float q;
+  float r;
+  float offset;
+  float sector;
+  int n;
+  int num;
+  bool rotate;
+  float quat_base[4];
+} animation_geometry;
+
+/* A data structure that holds the data for an animation using a single
+   object. */
+typedef struct {
+  int generator;
+  float p_start, p_end;
+  int easing_function_p;
+  float q_start, q_end;
+  int easing_function_q;
+  float r_start, r_end;
+  int easing_function_r;
+  float offset_start, offset_end;
+  int easing_function_offset;
+  float sector_start, sector_end;
+  int easing_function_sector;
+  int n;
+  int num;
+  float rot_axis_base[3];
+  float angle_start, angle_end;
+  int easing_function_rotate;
+} animation_single_obj;
+
+/* A data structure that holds the data for an animation using multiple
+   objects that are animated at the same time. */
+typedef struct {
+  int num;
+  animation_single_obj *anim_so;
+  float rotate_prob;
+  int easing_function_rot_rnd;
+  float rot_axis_space[3];
+  float angle_start, angle_end;
+  int easing_function_rot_space;
+  int num_steps;
+} animation_multi_obj;
+
+/* A data structure that holds the data for an animation that consists of
+   multiple phases that are animated consecutively. */
+typedef struct {
+  int num_phases;
+  animation_multi_obj **anim_mo;
+} animation_phases;
+
+/* A data structure that holds a set of related animations. */
+typedef struct {
+  int num_anim;
+  animation_phases **anim;
+} animations;
+
+
+/* The set of all possible animations. */
+extern animations *hopf_animations[NUM_ANIM_STATES][NUM_ANIM_STATES];
+
+
+#endif /* USE_GL */
+
+#endif /* __HOPFANIMATIONS_H__ */
diff --git a/hacks/glx/hopffibration.c b/hacks/glx/hopffibration.c
new file mode 100644 (file)
index 0000000..abc2097
--- /dev/null
@@ -0,0 +1,3647 @@
+/* hopffibration --- Displays the Hopf fibration of the 4D hypersphere S³. */
+
+#if 0
+static const char sccsid[] = "@(#)hopffibration.c  1.1 25/02/01 xlockmore";
+#endif
+
+/* Copyright (c) 2025 Carsten Steger <carsten@mirsanmir.org>. */
+
+/*
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose and without fee is hereby granted,
+ * provided that the above copyright notice appear in all copies and that
+ * both that copyright notice and this permission notice appear in
+ * supporting documentation.
+ *
+ * This file is provided AS IS with no warranties of any kind.  The author
+ * shall have no liability with respect to the infringement of copyrights,
+ * trade secrets or any patents by this file or any part thereof.  In no
+ * event will the author be liable for any lost revenue or profits or
+ * other special, indirect and consequential damages.
+ *
+ * REVISION HISTORY:
+ * C. Steger - 25/02/06: Initial version
+ */
+
+/*
+ * This program shows the Hopf fibration of the 4d hypersphere S³.
+ * The Hopf fibration is based on the Hopf map, a many-to-one
+ * continuous function from S³ onto the the ordinary 3d sphere S² such
+ * that each distinct point of S² is mapped from a distinct great
+ * circle S¹ of S³.  Hence, the inverse image of a point on S²
+ * corresponds to a great circle S¹ on S³. The sphere S² is called the
+ * base space, each S¹ corresponding to a point on S² is called a
+ * fiber, and S³ is called the total space.
+ *
+ * The program displays the base space S² as a semi-transparent gray
+ * sphere in the bottom right corner of the display.  The points on
+ * the base space are displayed as small colored spheres.  The fibers
+ * in the total space are displayed in the same color as the
+ * corresponding point on the base space.
+ *
+ * The fibers in the total space are projected from 4d to 3d using
+ * stereographic projection and then compressing the infinite 3d space
+ * to a finite 3d ball to display the fibers compactly.  All fibers
+ * except one fiber that passes through the north pole of S³ are thus
+ * projected to deformed circles in 3d.  The program displays these
+ * deformed circles as closed tubes (topological tori).  The single
+ * fiber that passes through the north pole of S³ is projected to an
+ * infinite line by the stereographic projection.  This line passes
+ * through infinity in 3d and therefore topologically is a circle.
+ * Compressing this infinite line to a finite ball maps it to a
+ * straight line segment.  The program displays this line segment as a
+ * cylinder.  However, it should be thought of as a circle through
+ * infinity.
+ *
+ * The fibers, base space, and base points are then projected to the
+ * screen either perspectively or orthographically.
+ *
+ * The program displays various interesting configurations of base
+ * points and fibers.  Look out for the following configurations:
+ *
+ *   • Any two fibers form a Hopf link.
+ *   • More generally, each fiber is linked with each other fiber
+ *     exactly once.
+ *   • Each circle on S² creates a set of fibers that forms a Clifford
+ *     torus on S³ (i.e., in 4d).  Clifford tori are flat (in the same
+ *     sense that the surface of a cylinder is flat).
+ *   • If a circle on S² is not a circle of latitude, the projection
+ *     of the Clifford torus to 3d results in a (compressed) Dupin
+ *     cyclide.
+ *   • More generally, any closed curve on S² creates a torus-like
+ *     surface on S³ that is flat.  These surfaces are called Hopf
+ *     tori or Bianchi-Pinkall flat tori.  Look for the wave-like
+ *     curve on S² to see a Hopf torus.
+ *   • A circular arc on S² creates a Hopf band on S³.  The Hopf
+ *     band is a Seifert surface of the Hopf link that forms the
+ *     boundaries of the Hopf band.
+ *   • Two or more circles of latitude on S² create two or more nested
+ *     Clifford tori on S³.
+ *   • More generally, two or more disjoint circles on S² create two
+ *     or more linked Clifford tori on S³.
+ *   • A great circle through the north pole of S² creates a parabolic
+ *     ring cyclide (which is compressed to lie within the ball in the
+ *     3d projection).  A parabolic ring cyclide divides the entire 3d
+ *     space into two congruent parts that are interlocked, i.e.,
+ *     linked.
+ *   • By turning a circle on S² so that it passes through the north
+ *     pole of S², the projection of the corresponding Clifford torus
+ *     reverses its inside and outside in 3d.
+ *   • The Clifford torus corresponding to a great circle on S²
+ *     divides S³ into two congruent solid tori that fill the entire
+ *     S³.  The two solid tori on S³ correspond to the two hemispheres
+ *     into which the great circle divides S².  The solid tori in S³
+ *     are attached to each other at the Clifford torus.  The
+ *     congruence of the solid tori is visible in a particularly
+ *     striking manner if the great circle that creates the Clifford
+ *     torus is rotated so that it passes through the north pole of
+ *     S², thereby creating a parabolic ring cyclide via the
+ *     projection of the Clifford torus to 3d (see above).
+ *
+ * During the animations, two kinds of motions are used.  Usually, the
+ * points on the base space are moved or rotated to particular
+ * configurations.  This is apparent by the small spheres that
+ * represent the base points changing their position on the base
+ * space, which leads to a corresponding change of the configuration
+ * of the fibers.  The base space itself, however, is not moved or
+ * rotated, i.e., its orientation remains fixed.  Sometimes, only the
+ * projection of the fibers is rotated in 3d to show some interesting
+ * configurations more clearly, e.g., that a Hopf torus has a hole
+ * like a regular torus.  In this case, the base space also maintains
+ * its orientation in space.  Since a rotation in 3d does not change
+ * the configuration of the fibers, in this kind of animation, the
+ * points on the base space also remain fixed.  Sometimes, both types
+ * of animations are combined, e.g., when the projection of one or
+ * more Clifford tori is rotated in 3d while the base points of the
+ * Clifford tori also rotate on the base space.  In this case, the
+ * base space will only show the movement of the base points on the
+ * base space and not the 3d rotation of the projection of the fibers.
+ *
+ * To enhance the 3d depth impression, the program displays the
+ * shadows of the fibers and base points by default.  This is done by
+ * way of a two-pass rendering algorithm in which the geometry is
+ * rendered twice.  Depending on the speed of the GPU, displaying
+ * shadows might slow down the rendering significantly.  If this is
+ * the case, the rendering of shadows can be switched off, saving one
+ * render pass and thus speeding up the rendering.
+ *
+ * Some of the animations render complex geometries with a very large
+ * number of polygons. This can cause the rendering to become slow on
+ * some types of GPU.  To speed up the rendering process, the amount
+ * of details that are rendered can be controlled in three
+ * granularities (coarse, medium, and fine).  Devices with relatively
+ * small screens and relatively low-powered GPUs, such as phones or
+ * tablets, should typically select coarse details.  Standard GPUs
+ * should select medium details (the default).  High-powered GPUs on
+ * large screens may benefit from fine details.
+ *
+ * By default, the base space and base points are displayed as
+ * described above.  If desired, the display of the base space and
+ * base points can be switched off so that only the fibers are
+ * displayed.
+ *
+ * During the animation of the Hopf fibration, sometimes multiple
+ * fibers that are very close to each other are displayed.  This can
+ * create disturbing aliasing artifacts that are especially noticeable
+ * when the fibers are moving or turning slowly.  Therefore, by
+ * default, the rendering is performed using anti-aliasing.  This
+ * typically has a negligible effect on the rendering speed.  However,
+ * if shadows have already been switched off, coarse details have been
+ * selected, and the rendering is still slow, anti-aliasing also can
+ * be switched off to check whether it has a noticeable effect on the
+ * rendering speed.
+ *
+ * This program was inspired by Niles Johnson's visualization of the
+ * Hopf fibration (https://nilesjohnson.net/hopf.html).
+ */
+
+
+#define DISP_PERSPECTIVE  0
+#define DISP_ORTHOGRAPHIC 1
+
+#define DISP_DETAILS_COARSE 0
+#define DISP_DETAILS_MEDIUM 1
+#define DISP_DETAILS_FINE   2
+
+#define DEF_SHADOWS       "True"
+#define DEF_BASE_SPACE    "True"
+#define DEF_ANTI_ALIASING "True"
+#define DEF_DETAILS       "medium"
+#define DEF_PROJECTION    "perspective"
+
+#ifdef STANDALONE
+# define DEFAULTS           "*delay:       20000 \n" \
+                            "*showFPS:     False \n" \
+                            "*prefersGLSL: True \n" \
+
+# define release_hopffibration 0
+# include "xlockmore.h"         /* from the xscreensaver distribution */
+#else  /* !STANDALONE */
+# include "xlock.h"             /* from the xlockmore distribution */
+#endif /* !STANDALONE */
+
+#ifdef USE_GL
+
+#include "glsl-utils.h"
+#include "gltrackball.h"
+#include "hopfanimations.h"
+
+
+#if (defined(HAVE_COCOA) || defined(__APPLE__)) && !defined(HAVE_IPHONE)
+#ifndef GL_COMPARE_REF_TO_TEXTURE
+#define GL_COMPARE_REF_TO_TEXTURE GL_COMPARE_R_TO_TEXTURE_ARB
+#endif
+#ifndef GL_TEXTURE_COMPARE_MODE
+#define GL_TEXTURE_COMPARE_MODE GL_TEXTURE_COMPARE_MODE_ARB
+#endif
+#ifndef GL_TEXTURE_COMPARE_FUNC
+#define GL_TEXTURE_COMPARE_FUNC GL_TEXTURE_COMPARE_FUNC_ARB
+#endif
+#endif
+
+
+/* Define ROT_BASE_SPACE if the base space and the base points should be
+   rotated in 3d in the same manner as the fibers in the total space.  This
+   is currently not defined since it makes the rotations of the base points
+   in 3d harder to distinguish from the animations that change the base
+   points on the base space and therefore change the fibers. */
+#undef ROT_BASE_SPACE
+
+#define LINCOOR(r,c,w) ((r)*(w)+(c))
+
+#define M_SQRT2_F 1.4142135623731f
+#define M_COS_1_F 0.9998476951564f
+
+#define MAX_CIRCLE_PNT     512
+#define MAX_CIRCLE_STACK   64
+#define TUBE_RADIUS        0.01f
+#define BASE_POINT_RADIUS  0.01f
+#define BASE_SPHERE_RADIUS 0.2f
+#define BASE_SPHERE_S      14
+
+#define NUM_TUBE_COARSE 8
+#define NUM_TUBE_MEDIUM 12
+#define NUM_TUBE_FINE   16
+
+#define BASE_SPHERE_S_COARSE 12
+#define BASE_SPHERE_S_MEDIUM 14
+#define BASE_SPHERE_S_FINE   20
+
+#define BASE_POINT_S_COARSE 2
+#define BASE_POINT_S_MEDIUM 2
+#define BASE_POINT_S_FINE   3
+
+#define MAX_CIRCLE_DIST_COARSE 0.0010f
+#define MAX_CIRCLE_DIST_MEDIUM 0.0005f
+#define MAX_CIRCLE_DIST_FINE   0.0003f
+
+#define MSAA_SAMPLES 4
+
+
+/* The number of vertices and triangles of an icosahedron. */
+#define NUM_ICOSA_VERT 12
+#define NUM_ICOSA_TRI  20
+
+/* The nontrivial coordinates of an icosahedron. */
+#define ICO_X1 0.8944271910f
+#define ICO_X2 0.2763932023f
+#define ICO_X3 0.7236067977f
+#define ICO_Y1 0.8506508084f
+#define ICO_Y2 0.5257311121f
+#define ICO_Z  0.4472135955f
+
+/* The vertices of an icosahedron. */
+static float icosa_vert[3*NUM_ICOSA_VERT] = {
+   0.0f,    0.0f,    1.0f,
+   ICO_X1,  0.0f,    ICO_Z,
+   ICO_X2,  ICO_Y1,  ICO_Z,
+  -ICO_X3,  ICO_Y2,  ICO_Z,
+  -ICO_X3, -ICO_Y2,  ICO_Z,
+   ICO_X2, -ICO_Y1,  ICO_Z,
+   ICO_X3,  ICO_Y2, -ICO_Z,
+  -ICO_X2,  ICO_Y1, -ICO_Z,
+  -ICO_X1,  0.0f,   -ICO_Z,
+  -ICO_X2, -ICO_Y1, -ICO_Z,
+   ICO_X3, -ICO_Y2, -ICO_Z,
+   0.0f,    0.0f,   -1.0f
+};
+
+/* The triangles of an icosahedron. */
+static int icosa_tri[3*NUM_ICOSA_TRI] = {
+   0,  1,  2,
+   0,  2,  3,
+   0,  3,  4,
+   0,  4,  5,
+   0,  5,  1,
+   1,  6,  2,
+   2,  7,  3,
+   3,  8,  4,
+   4,  9,  5,
+   5, 10,  1,
+   2,  6,  7,
+   3,  7,  8,
+   4,  8,  9,
+   5,  9, 10,
+   1, 10,  6,
+   6, 11,  7,
+   7, 11,  8,
+   8, 11,  9,
+   9, 11, 10,
+  10, 11,  6
+};
+
+/* A data structure that stores the data of an icosahedron. vert stores the
+   num_vert vertices, while tri stores the num_tri triangles of the
+   icosahedron. */
+typedef struct {
+  float *vert;
+  int num_vert;
+  int *tri;
+  int num_tri;
+} icosahedron_data;
+
+/* A data structure that stores the data of an icosphere.  vert stores the
+   num_vert vertices, while norm stores the corresponding normals at the
+   vertices.  tri stores the num_tri triangles.  stri stores the num_tri
+   triangles that have been sorted according to the depth (z value) of
+   their centroid after the vertices have been transformed by the rotation
+   matrix stored in smat. */
+typedef struct {
+  float *vert;
+  float *norm;
+  int num_vert;
+  int *tri;
+  int *stri;
+  int num_tri;
+  float smat[3][3];
+} icosphere_data;
+
+/* An instance of an icosahedron. */
+static icosahedron_data icosa = {
+  icosa_vert, NUM_ICOSA_VERT, icosa_tri, NUM_ICOSA_TRI
+};
+
+/* A data structure to sort triangles by the z value of their centroid. */
+typedef struct {
+  float z;
+  int idx;
+} trisort;
+
+
+/* A data structure that contains a base point of the Hopf fibration.  This
+   is a point on the unit sphere S² in 3D. Therefore, the Euclidean length
+   of the vector (a,b,c) is assumed to be 1. */
+typedef struct {
+  float a, b, c;
+} hopf_base_point;
+
+/* A data structure that holds the data that is necessary to construct a
+   Hopf circle fiber that has been projected from 4D to 3D.  See the
+   function init_hopf_circle. */
+typedef struct {
+  hopf_base_point base;
+  float al, be;
+  float atab;
+} hopf_circle_par;
+
+/* A data structure that holds the data for one Hopf circle fiber point in
+   3D.  p is the point, while t, n, and b define the Frenet-Serret frame of
+   the Hopf circle at p, i.e, t is the unit tangent vector, n the unit
+   normal vector, and b the unit binormal vector at p.  phi is the angle
+   parameter of p on the circle.  See the functions gen_hopf_circle_points,
+   hopf_circle_point, hopf_circle_tangent, and hopf_circle_binormal. */
+typedef struct {
+  float p[3];
+  float t[3];
+  float n[3];
+  float b[3];
+  float phi;
+} hopf_circle_pnt;
+
+/* A data structure that defines a segment from the starting point s to the
+   end point e on a Hopf circle.  This data structure is used to construct
+   the Hopf circle projected to 3D recursively in gen_hopf_circle_points. */
+typedef struct {
+  hopf_circle_pnt s, e;
+} hopf_circle_seg;
+
+
+#ifdef USE_MODULES
+ModStruct hopffibration_description =
+{"hopffibration", "init_hopffibration", "draw_hopffibration",
+ NULL, "draw_hopffibration", "change_hopffibration",
+ "free_hopffibration", hopffibration_opts, 20000, 1, 1, 1, 1.0, 4, "",
+ "Display a Hopf fibration",
+ 0, NULL};
+#endif
+
+
+static Bool shadows;
+static Bool base_space;
+static Bool anti_aliasing;
+static char *det;
+static char *proj;
+
+
+static XrmOptionDescRec opts[] =
+{
+  {"-shadows",       ".shadows",      XrmoptionNoArg,  "on"},
+  {"+shadows",       ".shadows",      XrmoptionNoArg,  "off"},
+  {"-base-space",    ".baseSpace",    XrmoptionNoArg,  "on"},
+  {"+base-space",    ".baseSpace",    XrmoptionNoArg,  "off"},
+  {"-anti-aliasing", ".antiAliasing", XrmoptionNoArg,  "on"},
+  {"+anti-aliasing", ".antiAliasing", XrmoptionNoArg,  "off"},
+  {"-details",       ".details",      XrmoptionSepArg, 0 },
+  {"-perspective",   ".projection",   XrmoptionNoArg,  "perspective" },
+  {"-orthographic",  ".projection",   XrmoptionNoArg,  "orthographic" },
+};
+
+static argtype vars[] =
+{
+  { &shadows,       "shadows",      "Shadows",      DEF_SHADOWS,       t_Bool },
+  { &base_space,    "baseSpace",    "BaseSpace",    DEF_BASE_SPACE,    t_Bool },
+  { &anti_aliasing, "antiAliasing", "AntiAliasing", DEF_ANTI_ALIASING, t_Bool },
+  { &det,           "details",      "Details",      DEF_DETAILS,       t_String },
+  { &proj,          "projection",   "Projection",   DEF_PROJECTION,    t_String }
+};
+
+ENTRYPOINT ModeSpecOpt hopffibration_opts =
+{sizeof opts / sizeof opts[0], opts, sizeof vars / sizeof vars[0], vars, NULL};
+
+
+typedef struct {
+  GLint WindH, WindW;
+  GLXContext *glx_context;
+  /* Options */
+  Bool shadows;
+  Bool base_space;
+  Bool anti_aliasing;
+  int projection;
+  int details;
+  int num_tube;
+  int base_sphere_s;
+  int base_point_s;
+  float max_circle_dist;
+  /* Aspect ratio of the current window */
+  float aspect;
+  /* Trackball states */
+  trackball_state *trackball;
+  Bool button_pressed;
+  /* 3D rotation angles */
+  float alpha, beta, delta;
+  /* Rotation angles for the base space. */
+  float zeta, eta, theta;
+  /* A rotation axis around which the base points are rotated. */
+  float rot_axis_base[3];
+  /* The quaternion corresponding to a rotation by the current angle around
+     rot_axis_base. */
+  float quat_base[4];
+  /* A rotation axis around which the Hopf fibers in the projection of the
+     total space are rotated. */
+  float rot_axis_space[3];
+  /* The quaternion corresponding to a rotation by the current angle around
+     rot_axis_space. */
+  float quat_space[4];
+  /* The angle range through which the Hopf fibers in the projection of the
+     total space are rotated. */
+  float angle_space_start, angle_space_end;
+  /* The current state of the animation. */
+  int anim_state;
+  /* A variable that counts how many cycles to remain in the current state. */
+  int anim_remain_in_state;
+  /* The phase of the current animation. */
+  int anim_phase;
+  /* The number of phases in the current animation. */
+  int anim_phase_num;
+  /* The number of steps in the current animation. */
+  int anim_step;
+  /* The easing function for a rotation around a rot_axis_base. */
+  int anim_easing_fct_rot_rnd;
+  /* The easing function for a rotation around rot_axis_space. */
+  int anim_easing_fct_rot_space;
+  /* If true, perform a rotation around rot_axis_base. */
+  Bool anim_rotate_rnd;
+  /* If true, perform a rotation around rot_axis_space. */
+  Bool anim_rotate_space;
+  /* The phases of the current animation. */
+  animation_phases *anim_phases;
+  /* The objects that are animated in the current phase of the current
+     animation. */
+  animation_multi_obj *anim;
+  /* The state variables current phase of the current animation. */
+  animation_geometry anim_geom[MAX_ANIM_GEOM];
+  /* The set of base points for which Hopf fibers should be computed and
+     displayed. */
+  hopf_base_point *base_points;
+  /* The maximum number of base points over all animations in
+     hopfanimations.c. */
+  int max_base_points;
+  /* The number of elements in base_points that have been set. */
+  int num_base_points;
+  /* An icosphere that is used to display the base sphere S². */
+  icosphere_data *sphere_base;
+  /* An icosphere that is used to display the base points in base_points on
+     the base sphere S². */
+  icosphere_data *sphere_base_point;
+  /* The number of polygons that were drawn. */
+  int num_poly;
+#ifdef HAVE_GLSL
+  float *vert;
+  float *norm;
+  int *tri;
+  Bool use_shaders, use_msaa_fbo, use_shadow_fbo;
+  Bool shadow_fbo_req_col_tex;
+  GLuint shader_program, shadow_program;
+  GLint pos_index, normal_index, color_index;
+  GLint mv_index, proj_index, lmvp_index;
+  GLint glbl_ambient_index, lt_ambient_index;
+  GLint lt_diffuse_index, lt_specular_index;
+  GLint lt_direction_index, lt_halfvect_index;
+  GLint ambient_index, diffuse_index;
+  GLint specular_index, shininess_index;
+  GLint use_shadows_index, shadow_smpl_index;
+  GLint shadow_pix_size_index;
+  GLint shadow_pos_index, shadow_lmvp_index;
+  GLuint *fiber_pos_buffer, *fiber_normal_buffer;
+  GLuint *fiber_indices_buffer;
+  GLuint *fiber_num_tri;
+  GLuint sphere_base_pos_buffer, sphere_base_normal_buffer;
+  GLuint sphere_base_indices_buffer;
+  GLuint sphere_base_num_tri;
+  GLuint base_point_pos_buffer, base_point_normal_buffer;
+  GLuint base_point_indices_buffer;
+  GLuint base_point_num_tri;
+  GLuint default_draw_fbo, default_read_fbo;
+  GLuint msaa_fbo, msaa_rb_color, msaa_rb_depth;
+  GLint msaa_samples;
+  GLuint shadow_fbo;
+  GLuint shadow_tex, shadow_col_tex, shadow_dummy_tex;
+  GLint shadow_tex_size, max_tex_size;
+#endif /* HAVE_GLSL */
+} hopffibrationstruct;
+
+static hopffibrationstruct *hopffibration = (hopffibrationstruct *) NULL;
+
+
+/* The camera position. */
+static float eye_pos[3] = {
+  0.0f, 0.0f, 3.0f
+};
+
+/* The position of the base sphere S² with respect to the projection of the
+   fibers in the total space S³, which all lie in a ball in 3-space
+   centrered at the origin. */
+static float base_offset[3] = {
+  0.9f, -0.9f, 0.25f
+};
+
+
+
+#ifdef HAVE_GLSL
+
+/* The GLSL versions that correspond to different versions of OpenGL. */
+static const GLchar *shader_version_2_1 =
+  "#version 120\n";
+static const GLchar *shader_version_3_0 =
+  "#version 130\n";
+static const GLchar *shader_version_3_0_es =
+  "#version 300 es\n"
+  "precision highp float;\n"
+  "precision highp int;\n"
+  "precision highp sampler2DShadow;\n";
+
+/* The vertex shader code is composed of code fragments that depend on
+   the OpenGL version and code fragments that are version-independent.
+   They are concatenated by glShaderSource in the function init_glsl(). */
+static const GLchar *vertex_shader_attribs_2_1 =
+  "attribute vec3 VertexPosition;\n"
+  "attribute vec3 VertexNormal;\n"
+  "attribute vec4 VertexColor;\n"
+  "\n"
+  "varying vec3 Normal;\n"
+  "varying vec4 Color;\n"
+  "varying vec4 ShadowCoord;\n"
+  "\n";
+static const GLchar *vertex_shader_attribs_3_0 =
+  "in vec3 VertexPosition;\n"
+  "in vec3 VertexNormal;\n"
+  "in vec4 VertexColor;\n"
+  "\n"
+  "out vec3 Normal;\n"
+  "out vec4 Color;\n"
+  "out vec4 ShadowCoord;\n"
+  "\n";
+static const GLchar *vertex_shader_main =
+  "uniform mat4 MatModelView;\n"
+  "uniform mat4 MatProj;\n"
+  "uniform mat4 MatLightMVP;\n"
+  "\n"
+  "void main (void)\n"
+  "{\n"
+  "  Color = VertexColor;\n"
+  "  Normal = normalize(MatModelView*vec4(VertexNormal,0.0f)).xyz;\n"
+  "  vec4 Position = MatModelView*vec4(VertexPosition,1.0f);\n"
+  "  gl_Position = MatProj*Position;\n"
+  "  ShadowCoord = 0.5f*MatLightMVP*vec4(VertexPosition,1.0f)+0.5f;\n"
+  "}\n";
+
+/* The fragment shader code is composed of code fragments that depend on
+   the OpenGL version and code fragments that are version-independent.
+   They are concatenated by glsl_CompileAndLinkShaders in the function
+   init_glsl(). */
+static const GLchar *fragment_shader_attribs_2_1 =
+  "varying vec3 Normal;\n"
+  "varying vec4 Color;\n"
+  "varying vec4 ShadowCoord;\n"
+  "\n"
+  "uniform vec4 LtGlblAmbient;\n"
+  "uniform vec4 LtAmbient, LtDiffuse, LtSpecular;\n"
+  "uniform vec3 LtDirection, LtHalfVector;\n"
+  "uniform vec4 MatAmbient;\n"
+  "uniform vec4 MatDiffuse;\n"
+  "uniform vec4 MatSpecular;\n"
+  "uniform float MatShininess;\n"
+  "uniform bool UseShadows;\n"
+  "uniform float ShadowPixSize;\n"
+  "uniform sampler2DShadow ShadowTex;\n"
+  "\n"
+  "float SampleShadowPCF(int x, int y)\n"
+  "{\n"
+  "  vec4 offset;\n"
+  "  \n"
+  "  offset = vec4(x*ShadowPixSize,y*ShadowPixSize,0.0f,0.0f);\n"
+  "  return shadow2DProj(ShadowTex,ShadowCoord+offset).x;\n"
+  "}\n"
+  "\n";
+static const GLchar *fragment_shader_attribs_3_0 =
+  "in vec3 Normal;\n"
+  "in vec4 Color;\n"
+  "in vec4 ShadowCoord;\n"
+  "\n"
+  "out vec4 FragColor;\n"
+  "\n"
+  "uniform vec4 LtGlblAmbient;\n"
+  "uniform vec4 LtAmbient, LtDiffuse, LtSpecular;\n"
+  "uniform vec3 LtDirection, LtHalfVector;\n"
+  "uniform vec4 MatAmbient;\n"
+  "uniform vec4 MatDiffuse;\n"
+  "uniform vec4 MatSpecular;\n"
+  "uniform float MatShininess;\n"
+  "uniform bool UseShadows;\n"
+  "uniform float ShadowPixSize;\n"
+  "uniform sampler2DShadow ShadowTex;\n"
+  "\n"
+  "float SampleShadowPCF(int x, int y)\n"
+  "{\n"
+  "  vec4 offset;\n"
+  "  \n"
+  "  offset = vec4(float(x)*ShadowPixSize,float(y)*ShadowPixSize,0.0f,0.0f);\n"
+  "  return textureProj(ShadowTex,ShadowCoord+offset);\n"
+  "}\n"
+  "\n";
+static const GLchar *fragment_shader_main =
+  "void main (void)\n"
+  "{\n"
+  "  vec3 normalDirection;\n"
+  "  vec4 ambientColor, diffuseColor, sceneColor;\n"
+  "  vec4 ambientLighting, diffuseReflection, specularReflection, color;\n"
+  "  float ndotl, ndoth, pf, shadowPCF;\n"
+  "  int i, j;\n"
+  "  \n"
+  "  if (gl_FrontFacing)\n"
+  "    normalDirection = normalize(Normal);\n"
+  "  else\n"
+  "    normalDirection = -normalize(Normal);\n"
+  "  sceneColor = Color*MatAmbient*LtGlblAmbient;\n"
+  "  ambientColor = Color*MatAmbient;\n"
+  "  diffuseColor = Color*MatDiffuse;\n"
+  "  \n"
+  "  if (UseShadows)\n"
+  "  {\n"
+  "    shadowPCF = 0.0f;\n"
+  "    for (i=-1; i<=1; i++)\n"
+  "      for (j=-1; j<=1; j++)\n"
+  "        shadowPCF += SampleShadowPCF(i,j);\n"
+  "    shadowPCF *= 1.0f/9.0f;\n"
+  "  }\n"
+  "  else\n"
+  "  {\n"
+  "    shadowPCF = 1.0f;\n"
+  "  }\n"
+  "  \n"
+  "  ndotl = max(0.0f,dot(normalDirection,LtDirection));\n"
+  "  ndoth = max(0.0f,dot(normalDirection,LtHalfVector));\n"
+  "  if (ndotl == 0.0f)\n"
+  "    pf = 0.0f;\n"
+  "  else\n"
+  "    pf = pow(ndoth,MatShininess);\n"
+  "  ambientLighting = ambientColor*LtAmbient;\n"
+  "  diffuseReflection = LtDiffuse*diffuseColor*ndotl;\n"
+  "  specularReflection = LtSpecular*MatSpecular*pf;\n"
+  "  color = sceneColor+ambientLighting;\n"
+  "  color += shadowPCF*(diffuseReflection+specularReflection);\n"
+  "  color.a = diffuseColor.a;\n";
+static const GLchar *fragment_shader_out_2_1 =
+  "  gl_FragColor = clamp(color,0.0f,1.0f);\n"
+  "}\n";
+static const GLchar *fragment_shader_out_3_0 =
+  "  FragColor = clamp(color,0.0f,1.0f);\n"
+  "}\n";
+
+
+/* The shadow vertex shader code is composed of code fragments that depend
+   on the OpenGL version and code fragments that are version-independent.
+   They are concatenated by glShaderSource in the function init_glsl(). */
+static const GLchar *shadow_vertex_shader_attribs_2_1 =
+  "attribute vec4 VertexPosition;\n"
+  "\n";
+static const GLchar *shadow_vertex_shader_attribs_3_0 =
+  "in vec4 VertexPosition;\n"
+  "\n";
+static const GLchar *shadow_vertex_shader_main =
+  "uniform mat4 MatLightMVP;\n"
+  "\n"
+  "void main (void)\n"
+  "{\n"
+  "  gl_Position = MatLightMVP*VertexPosition;\n"
+  "}\n";
+
+/* The shadow fragment shader code is composed of a single code fragment
+   that does not depend on the OpenGL version. */
+static const GLchar *shadow_fragment_shader_main =
+  "void main (void)\n"
+  "{\n"
+  "  // Nothing to do since depth is handled automatically by OpenGL.\n"
+  "}\n";
+
+#endif /* HAVE_GLSL */
+
+
+/* Compute the Euclidean norm of a vector. */
+static inline float norm(const float v[3])
+{
+  return sqrtf(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
+}
+
+
+/* Normalize a vector. */
+static inline void normalize(float v[3])
+{
+  float l;
+
+  l = norm(v);
+  if (l > 0.0f)
+    l = 1.0f/l;
+  v[0] *= l;
+  v[1] *= l;
+  v[2] *= l;
+}
+
+
+/* Normalize a vector to length r. */
+static inline void normalize_to_length(float v[3], float r)
+{
+  float l;
+
+  l = norm(v);
+  if (l > 0.0f)
+    l = r/l;
+  v[0] *= l;
+  v[1] *= l;
+  v[2] *= l;
+}
+
+
+/* Compute the vector difference s = a - b. */
+static inline void sub(float s[3], const float a[3], const float b[3])
+{
+  s[0] = a[0]-b[0];
+  s[1] = a[1]-b[1];
+  s[2] = a[2]-b[2];
+}
+
+
+/* Compute the cross product c = m × n. */
+static inline void cross(float c[3], const float m[3], const float n[3])
+{
+  c[0] = m[1]*n[2]-m[2]*n[1];
+  c[1] = m[2]*n[0]-m[0]*n[2];
+  c[2] = m[0]*n[1]-m[1]*n[0];
+}
+
+
+/* Compute the distance of the point p from the line given by the two points
+   q and r. It is assumed that q and r are distinct points. */
+static float distance_point_line(const float p[3], const float q[3],
+                                 const float r[3])
+{
+  float v[3], w[3], c[3], lv, lc;
+
+  sub(v,r,q);
+  lv = norm(v);
+  sub(w,p,q);
+  cross(c,w,v);
+  lc = norm(c);
+  if (lv > 0.0f)
+    return lc/lv;
+  else
+    return 0.0f;
+}
+
+
+/* Compute the identity matrix. */
+static void identity(float m[3][3])
+{
+  int i, j;
+
+  for (i=0; i<3; i++)
+    for (j=0; j<3; j++)
+      m[i][j] = (float)(i==j);
+}
+
+
+/* Add a rotation around the x axis to the matrix m. */
+static void rotatex(float m[3][3], float phi)
+{
+  float c, s, u, v;
+  int i;
+
+  phi *= M_PI_F/180.0f;
+  c = cosf(phi);
+  s = sinf(phi);
+  for (i=0; i<3; i++)
+  {
+    u = m[i][1];
+    v = m[i][2];
+    m[i][1] = c*u+s*v;
+    m[i][2] = -s*u+c*v;
+  }
+}
+
+
+/* Add a rotation around the y axis to the matrix m. */
+static void rotatey(float m[3][3], float phi)
+{
+  float c, s, u, v;
+  int i;
+
+  phi *= M_PI_F/180.0f;
+  c = cosf(phi);
+  s = sinf(phi);
+  for (i=0; i<3; i++)
+  {
+    u = m[i][0];
+    v = m[i][2];
+    m[i][0] = c*u-s*v;
+    m[i][2] = s*u+c*v;
+  }
+}
+
+
+/* Add a rotation around the z axis to the matrix m. */
+static void rotatez(float m[3][3], float phi)
+{
+  float c, s, u, v;
+  int i;
+
+  phi *= M_PI_F/180.0f;
+  c = cosf(phi);
+  s = sinf(phi);
+  for (i=0; i<3; i++)
+  {
+    u = m[i][0];
+    v = m[i][1];
+    m[i][0] = c*u+s*v;
+    m[i][1] = -s*u+c*v;
+  }
+}
+
+
+/* Compute the rotation matrix m from the rotation angles. */
+static void rotateall(float al, float be, float de, float m[3][3])
+{
+  int i, j;
+
+  for (i=0; i<3; i++)
+    for (j=0; j<3; j++)
+      m[i][j] = (i==j);
+  rotatex(m,al);
+  rotatey(m,be);
+  rotatez(m,de);
+}
+
+
+/* Multiply two rotation matrices: o = m * n. */
+static void mult_rotmat(float o[3][3], float m[3][3], float n[3][3])
+{
+  int i, j, k;
+
+  for (i=0; i<3; i++)
+  {
+    for (j=0; j<3; j++)
+    {
+      o[i][j] = 0.0f;
+      for (k=0; k<3; k++)
+        o[i][j] += m[i][k]*n[k][j];
+    }
+  }
+}
+
+
+/* Multiply a rotation matrix with a vector: o = m * v. */
+static void mult_rotmat_vec(float o[3], float m[3][3], float v[3])
+{
+  int i, j;
+
+  for (i=0; i<3; i++)
+  {
+    o[i] = 0.0f;
+    for (j=0; j<3; j++)
+      o[i] += m[i][j]*v[j];
+  }
+}
+
+
+/* Create a look-at rotation matrix. */
+static void look_at_rotmat(float c[3][3],
+                           GLfloat eyex, GLfloat eyey, GLfloat eyez,
+                           GLfloat centerx, GLfloat centery, GLfloat centerz,
+                           GLfloat upx, GLfloat upy, GLfloat upz)
+{
+  float forward[3], side[3], up[3];
+
+  forward[0] = centerx-eyex;
+  forward[1] = centery-eyey;
+  forward[2] = centerz-eyez;
+  normalize(forward);
+
+  up[0] = upx;
+  up[1] = upy;
+  up[2] = upz;
+
+  /* side = forward × up */
+  cross(side,forward,up);
+  normalize(side);
+
+  /* up = side × forward */
+  cross(up,side,forward);
+
+  c[0][0] = side[0];
+  c[0][1] = side[1];
+  c[0][2] = side[2];
+  c[1][0] = up[0];
+  c[1][1] = up[1];
+  c[1][2] = up[2];
+  c[2][0] = -forward[0];
+  c[2][1] = -forward[1];
+  c[2][2] = -forward[2];
+}
+
+
+/* Create a rotation matrix from a quaternion. */
+static void quat_to_rotmat(float q[4], float m[3][3])
+{
+  m[0][0] = q[0]*q[0]+q[1]*q[1]-q[2]*q[2]-q[3]*q[3];
+  m[0][1] = 2.0f*(q[1]*q[2]-q[0]*q[3]);
+  m[0][2] = 2.0f*(q[1]*q[3]+q[0]*q[2]);
+  m[1][0] = 2.0f*(q[1]*q[2]+q[0]*q[3]);
+  m[1][1] = q[0]*q[0]-q[1]*q[1]+q[2]*q[2]-q[3]*q[3];
+  m[1][2] = 2.0f*(q[2]*q[3]-q[0]*q[1]);
+  m[2][0] = 2.0f*(q[1]*q[3]-q[0]*q[2]);
+  m[2][1] = 2.0f*(q[2]*q[3]+q[0]*q[1]);
+  m[2][2] = q[0]*q[0]-q[1]*q[1]-q[2]*q[2]+q[3]*q[3];
+}
+
+
+/* Create a 4×4 column-major rotation matrix from a quaternion. */
+static void quat_to_rotmat_4x4(GLfloat q[4], GLfloat c[16])
+{
+  c[0] = q[0]*q[0]+q[1]*q[1]-q[2]*q[2]-q[3]*q[3];
+  c[1] = 2.0f*(q[1]*q[2]+q[0]*q[3]);
+  c[2] = 2.0f*(q[1]*q[3]-q[0]*q[2]);
+  c[3] = 0.0f;
+  c[4] = 2.0f*(q[1]*q[2]-q[0]*q[3]);
+  c[5] = q[0]*q[0]-q[1]*q[1]+q[2]*q[2]-q[3]*q[3];
+  c[6] = 2.0f*(q[2]*q[3]+q[0]*q[1]);
+  c[7] = 0.0f;
+  c[8] = 2.0f*(q[1]*q[3]+q[0]*q[2]);
+  c[9] = 2.0f*(q[2]*q[3]-q[0]*q[1]);
+  c[10] = q[0]*q[0]-q[1]*q[1]-q[2]*q[2]+q[3]*q[3];
+  c[11] = 0.0f;
+  c[12] = 0.0f;
+  c[13] = 0.0f;
+  c[14] = 0.0f;
+  c[15] = 1.0f;
+}
+
+
+/* Compute a 3×3 rotation matrix from an xscreensaver unit quaternion. Note
+   that xscreensaver has a different convention for unit quaternions than
+   the one that is used in this hack. */
+static void quat_to_rotmat_trackball(float p[4], float m[3][3])
+{
+  float al, be, de;
+  float r00, r01, r02, r12, r22;
+
+  r00 = 1.0f-2.0f*(p[1]*p[1]+p[2]*p[2]);
+  r01 = 2.0f*(p[0]*p[1]+p[2]*p[3]);
+  r02 = 2.0f*(p[2]*p[0]-p[1]*p[3]);
+  r12 = 2.0f*(p[1]*p[2]+p[0]*p[3]);
+  r22 = 1.0f-2.0f*(p[1]*p[1]+p[0]*p[0]);
+
+  al = atan2f(-r12,r22)*180.0f/M_PI_F;
+  be = atan2f(r02,sqrtf(r00*r00+r01*r01))*180.0f/M_PI_F;
+  de = atan2f(-r01,r00)*180.0f/M_PI_F;
+  rotateall(al,be,de,m);
+}
+
+
+/* Compute a 4×4 rotation matrix from an xscreensaver unit quaternion. Note
+   that xscreensaver has a different convention for unit quaternions than
+   the one that is used in this hack. */
+static void quat_to_rotmat_trackball_4x4(float p[4], float r[16])
+{
+  float m[3][3];
+  int   i, j;
+
+  quat_to_rotmat_trackball(p,m);
+  for (i=0; i<3; i++)
+  {
+    for (j=0; j<3; j++)
+      r[j*4+i] = m[i][j];
+    r[3*4+i] = 0.0f;
+    r[i*4+3] = 0.0f;
+  }
+  r[3*4+3] = 1.0f;
+}
+
+
+/* Generate the vertices and triangles of the icosphere icosp with radius r
+   by subdividing the triangles of the icosahedron icosa into s segments
+   along each edge, resulting in s*s triangles for each triangle of the
+   icosahedron. */
+static void gen_icosphere(const icosahedron_data *icosa, icosphere_data *icosp,
+                          int s, float r)
+{
+  int i, j, k, m, n, o, num_edge;
+  int i1, i2, i3, l1, l2, l3, k1, k2;
+  int vf, vt;
+  float t;
+  int num_vert, num_tri;
+  const float *v;
+  const int *tri;
+  unsigned char *adj;
+  int *edge_index_arr;
+  int **edge_indices;
+  float *vs;
+  int *vi, *ts;
+
+  /* Sanity check. */
+  if (s < 1)
+    s = 1;
+
+  v = icosa->vert;
+  num_vert = icosa->num_vert;
+  tri = icosa->tri;
+  num_tri = icosa->num_tri;
+  vs = icosp->vert;
+  ts = icosp->tri;
+
+  /* Allocate and clear the adjacency matrix. */
+  adj = calloc(num_vert*num_vert,sizeof(*adj));
+  /* Allocate the array that stores the vertex indices of a subdivided
+     triangle. */
+  vi = malloc((s+1)*(s+2)*sizeof(*vi));
+  if (adj == NULL || vi == NULL)
+    abort();
+
+  /* Initialize the adjacency matrix. */
+  for (i=0; i<num_tri; i++)
+  {
+    for (j=0; j<3; j++)
+    {
+      vf = tri[3*i+j];
+      vt = tri[3*i+(j+1)%3];
+      adj[LINCOOR(vf,vt,num_vert)] = 1;
+      adj[LINCOOR(vt,vf,num_vert)] = 1;
+    }
+  }
+
+  /* Count the number of edges of the polyhedron. */
+  num_edge = 0;
+  for (i=0; i<num_vert; i++)
+  {
+    for (j=i+1; j<num_vert; j++)
+      num_edge += adj[LINCOOR(i,j,num_vert)];
+  }
+
+  /* Allocate the array that holds all the vertex indices for the newly
+     created vertices on the edges. */
+  edge_index_arr = malloc(2*num_edge*(s-1)*sizeof(*edge_index_arr));
+  /* Allocate the matrix that holds the pointers to the indices for the
+     newly created vertices on the edges and populate the matrix with
+     pointers into edge_index_arr. */
+  edge_indices = malloc(num_vert*num_vert*sizeof(*edge_indices));
+  if (edge_index_arr == NULL || edge_indices == NULL)
+    abort();
+
+  n = 0;
+  for (i=0; i<num_vert; i++)
+  {
+    for (j=0; j<num_vert; j++)
+    {
+      k = LINCOOR(i,j,num_vert);
+      if (adj[k])
+      {
+        edge_indices[k] = &edge_index_arr[n*(s-1)];
+        n++;
+      }
+      else
+      {
+        edge_indices[k] = NULL;
+      }
+    }
+  }
+
+  /* Copy the input vertices to the output vertices. */
+  for (i=0; i<num_vert; i++)
+  {
+    k = 3*i;
+    vs[k+0] = v[k+0];
+    vs[k+1] = v[k+1];
+    vs[k+2] = v[k+2];
+  }
+  n = num_vert;
+
+  /* Compute the s-1 vertices that are added on each edge and set the edge
+     indices in the upper right half of the edge_indices array. */
+  for (i=0; i<num_vert; i++)
+  {
+    l1 = 3*i;
+    for (j=i+1; j<num_vert; j++)
+    {
+      l2 = 3*j;
+      k = LINCOOR(i,j,num_vert);
+      if (adj[k])
+      {
+        for (m=1; m<=s-1; m++)
+        {
+          l3 = 3*n;
+          t = (float)m/(float)s;
+          vs[l3+0] = (1.0f-t)*v[l1+0]+t*v[l2+0];
+          vs[l3+1] = (1.0f-t)*v[l1+1]+t*v[l2+1];
+          vs[l3+2] = (1.0f-t)*v[l1+2]+t*v[l2+2];
+          edge_indices[k][m-1] = n;
+          n++;
+        }
+      }
+    }
+  }
+
+  /* Set the lower left half of the edge_indices array to the reverse list
+     stored in the corresponding entry in the upper right half. */
+  for (i=0; i<num_vert; i++)
+  {
+    for (j=i+1; j<num_vert; j++)
+    {
+      k1 = LINCOOR(i,j,num_vert);
+      k2 = LINCOOR(j,i,num_vert);
+      if (adj[k1])
+      {
+        for (m=1; m<=s-1; m++)
+          edge_indices[k2][s-1-m] = edge_indices[k1][m-1];
+      }
+    }
+  }
+
+  m = 0;
+  for (i=0; i<num_tri; i++)
+  {
+    /* Compute the vertices that are added inside each triangle. */
+    i1 = tri[3*i+0];
+    i2 = tri[3*i+1];
+    i3 = tri[3*i+2];
+    o = 0;
+    vi[o++] = i2;
+    for (j=1; j<=s-1; j++)
+    {
+      k1 = edge_indices[LINCOOR(i2,i3,num_vert)][j-1];
+      k2 = edge_indices[LINCOOR(i2,i1,num_vert)][j-1];
+      vi[o++] = k1;
+      for (k=1; k<=j-1; k++)
+      {
+        l1 = 3*k1;
+        l2 = 3*k2;
+        l3 = 3*n;
+        t = (float)k/(float)j;
+        vs[l3+0] = (1.0f-t)*vs[l1+0]+t*vs[l2+0];
+        vs[l3+1] = (1.0f-t)*vs[l1+1]+t*vs[l2+1];
+        vs[l3+2] = (1.0f-t)*vs[l1+2]+t*vs[l2+2];
+        vi[o++] = n;
+        n++;
+      }
+      vi[o++] = k2;
+    }
+    vi[o++] = i3;
+    for (k=1; k<=j-1; k++)
+    {
+      k1 = edge_indices[LINCOOR(i3,i1,num_vert)][k-1];
+      vi[o++] = k1;
+    }
+    vi[o++] = i1;
+
+    /* Compute the triangles of the subdivided triangle. */
+    for (j=0; j<s; j++)
+    {
+      k1 = j*(j+1)/2;
+      k2 = k1+j+1;
+      for (k=0; k<j; k++)
+      {
+        ts[m+0] = vi[k1+k];
+        ts[m+1] = vi[k2+k];
+        ts[m+2] = vi[k2+k+1];
+        ts[m+3] = vi[k1+k];
+        ts[m+4] = vi[k2+k+1];
+        ts[m+5] = vi[k1+k+1];
+        m += 6;
+      }
+      ts[m+0] = vi[k1+k];
+      ts[m+1] = vi[k2+k];
+      ts[m+2] = vi[k2+k+1];
+      m += 3;
+    }
+  }
+
+  /* Normalize the length of the vertices so that they lie on the sphere of
+     radius r. */
+  for (i=0; i<n; i++)
+  {
+    k = 3*i;
+    normalize_to_length(&vs[k],r);
+  }
+
+  /* Free the temporary data structures. */
+  free(edge_indices);
+  free(edge_index_arr);
+  free(vi);
+  free(adj);
+}
+
+
+/* Generate the normals of the icosphere icosp. */
+static void gen_icosphere_normals(icosphere_data *icosp)
+{
+  int i, k, n;
+  const float *vert;
+  float *norm;
+
+  vert = icosp->vert;
+  norm = icosp->norm;
+  n = icosp->num_vert;
+
+  for (i=0; i<n; i++)
+  {
+    k = 3*i;
+    norm[k+0] = vert[k+0];
+    norm[k+1] = vert[k+1];
+    norm[k+2] = vert[k+2];
+    normalize(&norm[k]);
+  }
+}
+
+
+/* Generate the complete data necessary to render an icosphere with radius r
+   by subdividing the triangles of the icosahedron icosa into s segments
+   along each edge, resulting in s*s triangles for each triangle of the
+   icosahedron. */
+static icosphere_data *gen_icosphere_data(const icosahedron_data *icosa,
+                                          int s, float r)
+{
+  icosphere_data *icosp;
+
+  icosp = malloc(sizeof(*icosp));
+  if (icosp == NULL)
+    return NULL;
+
+  icosp->num_vert = s*s*(icosa->num_vert-2)+2;
+  icosp->num_tri = s*s*icosa->num_tri;
+  icosp->vert = malloc(3*(icosp->num_vert)*sizeof(*icosp->vert));
+  icosp->norm = malloc(3*icosp->num_vert*sizeof(*icosp->norm));
+  icosp->tri = malloc(3*(icosp->num_tri)*sizeof(*icosp->tri));
+  icosp->stri = malloc(3*(icosp->num_tri)*sizeof(*icosp->stri));
+  if (icosp->vert == NULL || icosp->norm == NULL ||
+      icosp->tri == NULL || icosp->stri == NULL)
+    return NULL;
+
+  gen_icosphere(icosa,icosp,s,r);
+  gen_icosphere_normals(icosp);
+
+  /* Cause the sorted triangles and sorted triangle normals to be computed
+     in sort_icosphere_triangles by setting the sort matrix to a null
+     matrix. */
+  memset(icosp->smat,0,sizeof(icosp->smat));
+
+  return icosp;
+}
+
+
+/* Compare the z values of two triangles; used by qsort. */
+static int compare_z(const void *p1, const void *p2)
+{
+  const trisort *t1, *t2;
+
+  t1 = (const trisort *)p1;
+  t2 = (const trisort *)p2;
+  if (t1->z > t2->z)
+    return 1;
+  else if (t1->z < t2->z)
+    return -1;
+  else
+    return 0;
+}
+
+
+/* Sort the triangles of the icosphere icosp based on the rotation given by
+   mat.  The sort is only performed if mat is different from the matrix smat
+   stored in icosp.  Return false if no sort was necessary and true if a
+   sort has been performed. */
+static Bool sort_icosphere_triangles(icosphere_data *icosp, float mat[3][3])
+{
+  int i, j, k, l;
+  float *vertz;
+  trisort *tris;
+  int num_vert, num_tri;
+  const float *vert;
+  const int *tri;
+  int *stri;
+
+  if (!memcmp(icosp->smat,mat,sizeof(icosp->smat)))
+    return False;
+
+  num_vert = icosp->num_vert;
+  num_tri = icosp->num_tri;
+  vert = icosp->vert;
+  tri = icosp->tri;
+  stri = icosp->stri;
+
+  vertz = malloc(num_vert*sizeof(*vertz));
+  tris = malloc(num_tri*sizeof(*tris));
+  if (vertz == NULL || tris == NULL)
+    abort();
+
+  /* Initialize the indices of the triangle sorting data structure. */
+  for (i=0; i<num_tri; i++)
+    tris[i].idx = i;
+
+  /* Compute the z coordinates of the vertices transformed with mat. */
+  for (i=0; i<num_vert; i++)
+  {
+    l = 3*i;
+    vertz[i] = mat[2][0]*vert[l+0]+mat[2][1]*vert[l+1]+mat[2][2]*vert[l+2];
+  }
+
+  /* Compute the z coordinate of the centroid of the transformed triangles. */
+  for (i=0; i<num_tri; i++)
+  {
+    l = 3*i;
+    tris[i].z = (1.0f/3.0f)*(vertz[tri[l+0]]+vertz[tri[l+1]]+vertz[tri[l+2]]);
+  }
+
+  /* Sort the z values of the centroids of the triangles. */
+  qsort(tris,num_tri,sizeof(*tris),compare_z);
+
+  /* Compute the sorted triangles according to the indices returned by
+     sorting the triangles. */
+  for (i=0; i<num_tri; i++)
+  {
+    k = 3*i;
+    l = 3*tris[i].idx;
+    for (j=0; j<3; j++)
+      stri[k+j] = tri[l+j];
+  }
+
+  free(tris);
+  free(vertz);
+
+  /* Store the current transformation matrix with the icosphere. */
+  memcpy(icosp->smat,mat,sizeof(icosp->smat));
+
+  return True;
+}
+
+
+/* Free the icosphere data stored in icosp. */
+static void free_icosphere_data(icosphere_data *icosp)
+{
+  free(icosp->stri);
+  free(icosp->tri);
+  free(icosp->norm);
+  free(icosp->vert);
+  free(icosp);
+}
+
+
+/* Initialize a Hopf circle fiber with base (a,b,c).  It is assumed that the
+   vector (a,b,c) has Euclidean length 1, i.e., that it lies on the unit
+   sphere S² in 3D. */
+static void init_hopf_circle(const hopf_base_point *hb, hopf_circle_par *hc)
+{
+  hc->base = *hb;
+  hc->al = sqrtf(0.5f*(1.0f+hb->c));
+  hc->be = sqrtf(0.5f*(1.0f-hb->c));
+  hc->atab = atan2f(-hb->a,hb->b);
+}
+
+
+/* Calculate a Hopf circle point with parameter phi. */
+static void hopf_circle_point(const hopf_circle_par *hc, float phi,
+                              hopf_circle_pnt *hcp)
+{
+  float pht, tht;
+  float w, x, y, z, r;
+
+  tht = phi;
+  pht = hc->atab-phi;
+
+  w = hc->al*cosf(tht);
+  x = -hc->be*cosf(pht);
+  y = -hc->be*sinf(pht);
+  z = hc->al*sinf(tht);
+  r = acosf(w)/(M_PI_F*sqrtf(1.0f-w*w));
+  hcp->p[0] = x*r;
+  hcp->p[1] = y*r;
+  hcp->p[2] = z*r;
+  hcp->phi = phi;
+}
+
+
+/* Calculate the unit tangent vector of a Hopf circle point with parameter
+   phi. */
+static void hopf_circle_tangent(const hopf_circle_par *hc, float phi,
+                                float t[3])
+{
+  float pht, tht;
+  float w, x, y, z, r, d, n, s;
+  float dwdp, dxdp, dydp, dzdp, drdp;
+  float dndp, dddp;
+
+  tht = phi;
+  pht = hc->atab-phi;
+
+  w = hc->al*cosf(tht);
+  x = -hc->be*cosf(pht);
+  y = -hc->be*sinf(pht);
+  z = hc->al*sinf(tht);
+
+  s = sqrtf(1.0f-w*w);
+  n = acosf(w);
+  d = M_PI_F*s;
+  r = n/d;
+
+  dwdp = -hc->al*sinf(tht);
+  dxdp = -hc->be*sinf(pht);
+  dydp = hc->be*cosf(pht);
+  dzdp = hc->al*cosf(tht);
+
+  dndp = -dwdp/s;
+  dddp = -M_PI_F*w*dwdp/s;
+  drdp = (dndp*d-n*dddp)/(d*d);
+
+  t[0] = dxdp*r+x*drdp;
+  t[1] = dydp*r+y*drdp;
+  t[2] = dzdp*r+z*drdp;
+  normalize(t);
+}
+
+
+/* Calculate the unit binormal vector for a Hopf circle. */
+static void hopf_circle_binormal(const hopf_circle_par *hc, float n[3])
+{
+  float a, b, c, sab, pisqr8, sq1pc, sq1mc, ac;
+
+  a = hc->base.a;
+  b = hc->base.b;
+  c = hc->base.c;
+  sab = sqrtf(a*a+b*b);
+  if (sab > 0.0f)
+  {
+    pisqr8 = 2.0f*M_SQRT2_F*M_PI_F;
+    sq1mc = sqrtf(1.0f-c);
+    sq1pc = sqrtf(1.0f+c);
+    ac = acosf(-sq1pc/M_SQRT2_F);
+    n[0] = -a*sq1pc*ac/(pisqr8*sab);
+    n[1] = -b*sq1pc*ac/(pisqr8*sab);
+    n[2] = sq1mc*ac/pisqr8;
+    normalize(n);
+  }
+  else if (c >= 0.0f) /* Actually, c = 1.0f in this case. */
+  {
+    n[0] = 1.0f;
+    n[1] = 0.0f;
+    n[2] = 0.0f;
+  }
+  else /* c < 0.0f, i.e., c = -1.0f in this case. */
+  {
+    n[0] = 0.0f;
+    n[1] = 0.0f;
+    n[2] = 1.0f;
+  }
+}
+
+
+/* Generate a Hopf circle as a polygon that approximates the true Hopf
+   circle with a maximum deviation <= MAX_CIRCLE_DIST. */
+static void gen_hopf_circle_points(ModeInfo *mi, const hopf_circle_par *hc,
+                                   hopf_circle_pnt *hcpa, int *num)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  int i, n, sp;
+  float phi, d, b[3];
+  hopf_circle_pnt hcp, hcps, hcpe;
+  hopf_circle_seg hcs[MAX_CIRCLE_STACK];
+
+  if (hc->base.c < M_COS_1_F)
+  {
+    /* Compute the initial four Hopf circle segments. */
+    sp = 0;
+    for (i=3; i>=0; i--)
+    {
+      phi = (float)i*M_PI_F/2.0f;
+      hopf_circle_point(hc,phi,&hcs[sp].s);
+      phi = (float)(i+1)*M_PI_F/2.0f;
+      hopf_circle_point(hc,phi,&hcs[sp].e);
+      sp++;
+    }
+
+    /* Recursively subdivide the Hopf circle segments until they approximate
+       the Hopf circle by a distance <= MAX_CIRCLE_DIST. */
+    n = 0;
+    sp--;
+    while (sp >= 0)
+    {
+      hcps = hcs[sp].s;
+      hcpe = hcs[sp].e;
+      phi = 0.5f*(hcps.phi+hcpe.phi);
+      hopf_circle_point(hc,phi,&hcp);
+      d = distance_point_line(hcp.p,hcps.p,hcpe.p);
+      if (d > hf->max_circle_dist)
+      {
+        if (sp >= MAX_CIRCLE_STACK-2)
+          abort();
+        hcs[sp].s.p[0] = hcp.p[0];
+        hcs[sp].s.p[1] = hcp.p[1];
+        hcs[sp].s.p[2] = hcp.p[2];
+        hcs[sp].s.phi = phi;
+        hcs[sp].e = hcpe;
+        sp++;
+        hcs[sp].s = hcps;
+        hcs[sp].e.p[0] = hcp.p[0];
+        hcs[sp].e.p[1] = hcp.p[1];
+        hcs[sp].e.p[2] = hcp.p[2];
+        hcs[sp].e.phi = phi;
+        sp++;
+      }
+      else
+      {
+        if (n >= MAX_CIRCLE_PNT-1)
+          abort();
+        hcpa[n] = hcps;
+        n++;
+      }
+      sp--;
+    }
+    hcpa[n] = hcpe;
+    n++;
+
+    /* Compute the unit tangent, unit normal, and unit binormal vectors of
+       each Hopf circle point. Note that the binormal vector is identical
+       for all Hopf circle points since the projected Hopf circle lies in a
+       plane in 3D. */
+    hopf_circle_binormal(hc,b);
+    for (i=0; i<n; i++)
+    {
+      hcpa[i].b[0] = b[0];
+      hcpa[i].b[1] = b[1];
+      hcpa[i].b[2] = b[2];
+      hopf_circle_tangent(hc,hcpa[i].phi,hcpa[i].t);
+      cross(hcpa[i].n,hcpa[i].b,hcpa[i].t);
+    }
+  }
+  else /* hc->base.c >= M_COS_1_F */
+  {
+    /* The circle for (a,b,c) = (0,0,1) projects to a vertical line segment. */
+    hcpa[0].p[0] = 0.0f;
+    hcpa[0].p[1] = 0.0f;
+    hcpa[0].p[2] = 1.0f;
+    hcpa[0].t[0] = 0.0f;
+    hcpa[0].t[1] = 0.0f;
+    hcpa[0].t[2] = 1.0f;
+    hcpa[0].n[0] = 0.0f;
+    hcpa[0].n[1] = 1.0f;
+    hcpa[0].n[2] = 0.0f;
+    hcpa[0].b[0] = 1.0f;
+    hcpa[0].b[1] = 0.0f;
+    hcpa[0].b[2] = 0.0f;
+    hcpa[0].phi = 0.0f;
+
+    hcpa[1].p[0] = 0.0f;
+    hcpa[1].p[1] = 0.0f;
+    hcpa[1].p[2] = -1.0f;
+    hcpa[1].t[0] = 0.0f;
+    hcpa[1].t[1] = 0.0f;
+    hcpa[1].t[2] = 1.0f;
+    hcpa[1].n[0] = 0.0f;
+    hcpa[1].n[1] = 1.0f;
+    hcpa[1].n[2] = 0.0f;
+    hcpa[1].b[0] = 1.0f;
+    hcpa[1].b[1] = 0.0f;
+    hcpa[1].b[2] = 0.0f;
+    hcpa[1].phi = 2.0f*M_PI_F;
+
+    n = 2;
+  }
+
+  *num = n;
+}
+
+
+/* Rotate the base point bp with the rotation matrix m. */
+static void rotate_base_point(float m[3][3], const hopf_base_point *bp,
+                              hopf_base_point *bpr)
+{
+  bpr->a = m[0][0]*bp->a+m[0][1]*bp->b+m[0][2]*bp->c;
+  bpr->b = m[1][0]*bp->a+m[1][1]*bp->b+m[1][2]*bp->c;
+  bpr->c = m[2][0]*bp->a+m[2][1]*bp->b+m[2][2]*bp->c;
+}
+
+
+/* Compute the color corresponding to the fiber (a,b,c). */
+static inline void color(const hopf_base_point *hb, float col[4])
+{
+  col[0] = 0.5f*(1.0f+hb->a);
+  col[1] = 0.5f*(1.0f+hb->b);
+  col[2] = 0.5f*(1.0f+hb->c);
+  col[3] = 1.0f;
+}
+
+
+/* Draw the icosphere icosp using the OpenGL fixed functionality. */
+static void draw_icosphere_ff(ModeInfo *mi, icosphere_data *icosp)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  int i, j, l;
+  float mat[3][3];
+  int num_tri;
+  const float *vert;
+  const int *tri;
+  const float *norm;
+#ifdef ROT_BASE_SPACE
+  float matl[3][3], mats[3][3], matq[3][3];
+  float qu[4], qr[3][3];
+
+  gltrackball_get_quaternion(hf->trackball,qu);
+  quat_to_rotmat_trackball(qu,qr);
+  quat_to_rotmat(hf->quat_space,mats);
+  look_at_rotmat(matl,eye_pos[0],eye_pos[1],eye_pos[2],
+                 0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+  mult_rotmat(matq,matl,qr);
+  mult_rotmat(mat,matq,mats);
+#else
+  look_at_rotmat(mat,eye_pos[0],eye_pos[1],eye_pos[2],
+                 0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+#endif
+  rotatex(mat,hf->alpha);
+  rotatey(mat,hf->beta);
+  rotatez(mat,hf->delta);
+  sort_icosphere_triangles(icosp,mat);
+
+  num_tri = icosp->num_tri;
+  vert = icosp->vert;
+  tri = icosp->stri;
+  norm = icosp->norm;
+
+  glBegin(GL_TRIANGLES);
+  for (i=0; i<num_tri; i++)
+  {
+    for (j=0; j<3; j++)
+    {
+      l = tri[3*i+j];
+      glNormal3fv(&norm[3*l]);
+      glVertex3fv(&vert[3*l]);
+    }
+  }
+  glEnd();
+
+  hf->num_poly += num_tri;
+}
+
+
+/* Draw a Hopf circle with base (a,b,c) using the OpenGL fixed
+   functionality. */
+static void draw_hopf_circle_ff(ModeInfo *mi, const hopf_base_point *hb)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  int i, j, k, l, m, num;
+  float p[3], n[3], phi, cp, sp, t, sgn;
+  hopf_circle_par hc;
+  hopf_circle_pnt hcpa[MAX_CIRCLE_PNT];
+
+  init_hopf_circle(hb,&hc);
+  gen_hopf_circle_points(mi,&hc,hcpa,&num);
+
+  for (i=0; i<num-1; i++)
+  {
+    glBegin(GL_TRIANGLE_STRIP);
+    for (j=hf->num_tube; j>=0; j--)
+    {
+      for (k=0; k<=1; k++)
+      {
+        l = i+k;
+        phi = j*(2.0f*M_PI_F/hf->num_tube);
+        cp = cosf(phi);
+        sp = sinf(phi);
+        for (m=0; m<3; m++)
+        {
+          t = cp*hcpa[l].n[m]+sp*hcpa[l].b[m];
+          p[m] = hcpa[l].p[m]+TUBE_RADIUS*t;
+          n[m] = t;
+        }
+        glNormal3fv(n);
+        glVertex3fv(p);
+      }
+    }
+    glEnd();
+  }
+
+  hf->num_poly += 2*(num-1)*hf->num_tube;
+
+  if (hc.base.c >= M_COS_1_F)
+  {
+    /* In this case, the Hopf circle projects to a vertical tube, which is
+       drawn by the above code. In addition, we must draw a circle at each
+       end of the tube. Note that we rely on the fact that num == 2 in this
+       case. */
+    for (i=0; i<num; i++)
+    {
+      if (i == 0)
+        sgn = 1.0f;
+      else
+        sgn = -1.0f;
+      n[0] = 0.0f;
+      n[1] = 0.0f;
+      n[2] = sgn;
+
+      glBegin(GL_TRIANGLE_FAN);
+      for (m=0; m<3; m++)
+        p[m] = hcpa[i].p[m];
+      glNormal3fv(n);
+      glVertex3fv(p);
+
+      for (j=hf->num_tube; j>=0; j--)
+      {
+        phi = sgn*j*(2.0f*M_PI_F/hf->num_tube);
+        cp = cosf(phi);
+        sp = sinf(phi);
+        for (m=0; m<3; m++)
+        {
+          t = cp*hcpa[i].n[m]+sp*hcpa[i].b[m];
+          p[m] = hcpa[i].p[m]+TUBE_RADIUS*t;
+        }
+        glNormal3fv(n);
+        glVertex3fv(p);
+      }
+      glEnd();
+    }
+
+    hf->num_poly += hf->num_tube;
+  }
+}
+
+
+/* Draw the num_bp Hopf circles given by hb using the OpenGL fixed
+   functionality. */
+static int draw_hopf_circles_ff(ModeInfo *mi)
+{
+  static const GLfloat light_ambient[] = { 0.3f, 0.3f, 0.3f, 1.0f };
+  static const GLfloat light_diffuse[] = { 0.7f, 0.7f, 0.7f, 1.0f };
+  static const GLfloat light_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+  static const GLfloat light_pos[] = { 0.7f, 0.7f, 1.0f, 0.0f };
+  static const GLfloat mat_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+  static const GLfloat mat_gray[] = { 0.7f, 0.7f, 0.7f, 0.6f };
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  hopf_base_point ht;
+  float col[4], mat[3][3], mat1[3][3], mat2[3][3], mats[16];
+  float qu[4], qr[16];
+  int i;
+
+  hf->num_poly = 0;
+
+  /* Compute the rotation matrix that rotates the base points. */
+  identity(mat1);
+  rotatex(mat1,hf->zeta);
+  rotatey(mat1,hf->eta);
+  rotatez(mat1,hf->theta);
+  quat_to_rotmat(hf->quat_base,mat2);
+  mult_rotmat(mat,mat2,mat1);
+
+  /* Compute the rotation matrix that rotates the space points. */
+  quat_to_rotmat_4x4(hf->quat_space,mats);
+
+  /* Compute the space rotation matrix from the trackball state. */
+  gltrackball_get_quaternion(hf->trackball,qu);
+  quat_to_rotmat_trackball_4x4(qu,qr);
+
+  glViewport(0,0,hf->WindW,hf->WindH);
+
+  glClearColor(0.0f,0.0f,0.0f,0.0f);
+  glClearDepth(1.0f);
+  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
+
+  glMatrixMode(GL_PROJECTION);
+  glLoadIdentity();
+  if (hf->projection == DISP_ORTHOGRAPHIC)
+  {
+    if (hf->aspect >= 1.0f)
+      glOrtho(-1.2*hf->aspect,1.2*hf->aspect,-1.2,1.2,0.1,10.0);
+    else
+      glOrtho(-1.2,1.2,-1.2/hf->aspect,1.2/hf->aspect,0.1,10.0);
+  }
+  else /* hf->projection == DISP_PERSPECTIVE */
+  {
+    if (hf->aspect >= 1.0f)
+      gluPerspective(45.0,hf->aspect,0.1,10.0);
+    else
+      gluPerspective(360.0/M_PI*atan(tan(45.0*M_PI/360.0)/hf->aspect),
+                     hf->aspect,0.1,10.0);
+  }
+
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+
+  glEnable(GL_DEPTH_TEST);
+  glDepthFunc(GL_LESS);
+  glShadeModel(GL_SMOOTH);
+  glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
+  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_TRUE);
+  glEnable(GL_LIGHTING);
+  glEnable(GL_LIGHT0);
+  glLightfv(GL_LIGHT0,GL_AMBIENT,light_ambient);
+  glLightfv(GL_LIGHT0,GL_DIFFUSE,light_diffuse);
+  glLightfv(GL_LIGHT0,GL_SPECULAR,light_specular);
+  glLightfv(GL_LIGHT0,GL_POSITION,light_pos);
+  glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,mat_specular);
+  glMaterialf(GL_FRONT_AND_BACK,GL_SHININESS,50.0f);
+  glDepthMask(GL_TRUE);
+  glDisable(GL_BLEND);
+  glEnable(GL_CULL_FACE);
+  glCullFace(GL_BACK);
+
+  gluLookAt(eye_pos[0],eye_pos[1],eye_pos[2],0.0,0.0,0.0,0.0,1.0,0.0);
+  glMultMatrixf(qr);
+  glMultMatrixf(mats);
+  glRotatef(hf->alpha,1.0f,0.0f,0.0f);
+  glRotatef(hf->beta,0.0f,1.0f,0.0f);
+  glRotatef(hf->delta,0.0f,0.0f,1.0f);
+
+  /* Draw the fibers (Hopf circles). */
+  for (i=0; i<hf->num_base_points; i++)
+  {
+    rotate_base_point(mat,&hf->base_points[i],&ht);
+
+    color(&ht,col);
+    glColor3fv(col);
+    glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,col);
+
+    draw_hopf_circle_ff(mi,&ht);
+  }
+
+  if (hf->base_space)
+  {
+    /* Draw the base points. */
+    for (i=0; i<hf->num_base_points; i++)
+    {
+      rotate_base_point(mat,&hf->base_points[i],&ht);
+
+      glMatrixMode(GL_MODELVIEW);
+      glLoadIdentity();
+
+      gluLookAt(eye_pos[0],eye_pos[1],eye_pos[2],0.0,0.0,0.0,0.0,1.0,0.0);
+      glTranslatef(base_offset[0],base_offset[1],base_offset[2]);
+#ifdef ROT_BASE_SPACE
+      glMultMatrixf(qr);
+      glMultMatrixf(mats);
+#endif
+      glRotatef(hf->alpha,1.0f,0.0f,0.0f);
+      glRotatef(hf->beta,0.0f,1.0f,0.0f);
+      glRotatef(hf->delta,0.0f,0.0f,1.0f);
+      glTranslatef(ht.a*BASE_SPHERE_RADIUS,ht.b*BASE_SPHERE_RADIUS,
+                   ht.c*BASE_SPHERE_RADIUS);
+
+      color(&ht,col);
+      glColor3fv(col);
+      glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,col);
+
+      draw_icosphere_ff(mi,hf->sphere_base_point);
+    }
+
+    /* Draw the base sphere. */
+    glDepthMask(GL_FALSE);
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
+    glDisable(GL_CULL_FACE);
+
+    glMatrixMode(GL_MODELVIEW);
+    glLoadIdentity();
+
+    gluLookAt(eye_pos[0],eye_pos[1],eye_pos[2],0.0,0.0,0.0,0.0,1.0,0.0);
+    glTranslatef(base_offset[0],base_offset[1],base_offset[2]);
+#ifdef ROT_BASE_SPACE
+    glMultMatrixf(qr);
+    glMultMatrixf(mats);
+#endif
+    glRotatef(hf->alpha,1.0f,0.0f,0.0f);
+    glRotatef(hf->beta,0.0f,1.0f,0.0f);
+    glRotatef(hf->delta,0.0f,0.0f,1.0f);
+
+    glColor3fv(mat_gray);
+    glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,mat_gray);
+    draw_icosphere_ff(mi,hf->sphere_base);
+
+    glDepthMask(GL_TRUE);
+    glDisable(GL_BLEND);
+  }
+
+  return hf->num_poly;
+}
+
+
+#ifdef HAVE_GLSL
+
+/* Generate vertex, normal, and index buffers for the icosphere icosp. */
+static GLuint gen_icosphere_buffers(ModeInfo *mi, icosphere_data *icosp,
+                                    GLint pos_buffer, GLuint normal_buffer,
+                                    GLuint indices_buffer)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  float mat[3][3];
+  int num_tri, num_vert;
+  const float *vert;
+  const int *tri;
+  const float *norm;
+#ifdef ROT_BASE_SPACE
+  float matl[3][3], mats[3][3], matq[3][3];
+  float qu[4], qr[3][3];
+
+  gltrackball_get_quaternion(hf->trackball,qu);
+  quat_to_rotmat_trackball(qu,qr);
+  quat_to_rotmat(hf->quat_space,mats);
+  look_at_rotmat(matl,eye_pos[0],eye_pos[1],eye_pos[2],
+                 0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+  mult_rotmat(matq,matl,qr);
+  mult_rotmat(mat,matq,mats);
+#else
+  look_at_rotmat(mat,eye_pos[0],eye_pos[1],eye_pos[2],
+                 0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+#endif
+  rotatex(mat,hf->alpha);
+  rotatey(mat,hf->beta);
+  rotatez(mat,hf->delta);
+  sort_icosphere_triangles(icosp,mat);
+
+  num_tri = icosp->num_tri;
+  num_vert = icosp->num_vert;
+  vert = icosp->vert;
+  tri = icosp->stri;
+  norm = icosp->norm;
+
+  glBindBuffer(GL_ARRAY_BUFFER,pos_buffer);
+  glBufferData(GL_ARRAY_BUFFER,3*num_vert*sizeof(GLfloat),vert,
+               GL_STREAM_DRAW);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
+
+  glBindBuffer(GL_ARRAY_BUFFER,normal_buffer);
+  glBufferData(GL_ARRAY_BUFFER,3*num_vert*sizeof(GLfloat),norm,
+               GL_STREAM_DRAW);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
+
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,indices_buffer);
+  glBufferData(GL_ELEMENT_ARRAY_BUFFER,3*num_tri*sizeof(GLuint),tri,
+               GL_STREAM_DRAW);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
+
+  return num_tri;
+}
+
+
+/* Generate vertex, normal, and index buffers for a Hopf circle with base
+   point (a,b,c). */
+static int gen_hopf_circle_buffers(ModeInfo *mi, const hopf_base_point *hb,
+                                   GLint pos_buffer, GLuint normal_buffer,
+                                   GLuint indices_buffer)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  int i, j, m, num;
+  int num_vert, num_tri;
+  float phi, cp, sp, t, sgn, n[3];
+  hopf_circle_par hc;
+  hopf_circle_pnt hcpa[MAX_CIRCLE_PNT];
+  float *vert, *norm;
+  int *tri;
+
+  vert = hf->vert;
+  norm = hf->norm;
+  tri = hf->tri;
+
+  init_hopf_circle(hb,&hc);
+  gen_hopf_circle_points(mi,&hc,hcpa,&num);
+
+  num_tri = 0;
+  for (i=0; i<num; i++)
+  {
+    for (j=hf->num_tube-1; j>=0; j--)
+    {
+      phi = j*(2.0f*M_PI_F/hf->num_tube);
+      cp = cosf(phi);
+      sp = sinf(phi);
+      for (m=0; m<3; m++)
+      {
+        t = cp*hcpa[i].n[m]+sp*hcpa[i].b[m];
+        vert[3*(i*hf->num_tube+j)+m] = hcpa[i].p[m]+TUBE_RADIUS*t;
+        norm[3*(i*hf->num_tube+j)+m] = t;
+      }
+      if (i < num-1)
+      {
+        tri[3*num_tri+0] = i*hf->num_tube+(j+1)%hf->num_tube;
+        tri[3*num_tri+1] = (i+1)*hf->num_tube+(j+1)%hf->num_tube;
+        tri[3*num_tri+2] = i*hf->num_tube+j;
+        tri[3*num_tri+3] = i*hf->num_tube+j;
+        tri[3*num_tri+4] = (i+1)*hf->num_tube+(j+1)%hf->num_tube;
+        tri[3*num_tri+5] = (i+1)*hf->num_tube+j;
+        num_tri += 2;
+      }
+    }
+  }
+  num_vert = num*hf->num_tube;
+
+  if (hc.base.c >= M_COS_1_F)
+  {
+    /* In this case, the Hopf circle projects to a vertical tube, which is
+       created by the above code. In addition, we must draw a disk at each
+       end of the tube.  Note that we rely on the fact that num == 2 in this
+       case.*/
+    for (i=0; i<num; i++)
+    {
+      if (i == 0)
+        sgn = 1.0f;
+      else
+        sgn = -1.0f;
+      n[0] = 0.0f;
+      n[1] = 0.0f;
+      n[2] = sgn;
+
+      /* Add the center of the circle to the vertex and normal arrays. */
+      for (m=0; m<3; m++)
+      {
+        vert[3*num_vert+m] = hcpa[i].p[m];
+        norm[3*num_vert+m] = n[m];
+      }
+
+      /* Add the vertices, normals, and triangles for the disk. */
+      for (j=hf->num_tube; j>=0; j--)
+      {
+        phi = sgn*j*(2.0f*M_PI_F/hf->num_tube);
+        cp = cosf(phi);
+        sp = sinf(phi);
+        for (m=0; m<3; m++)
+        {
+          t = cp*hcpa[i].n[m]+sp*hcpa[i].b[m];
+          vert[3*(num_vert+j+1)+m] = hcpa[i].p[m]+TUBE_RADIUS*t;
+          norm[3*(num_vert+j+1)+m] = n[m];
+        }
+        if (j > 0)
+        {
+          tri[3*num_tri+0] = num_vert;
+          tri[3*num_tri+1] = num_vert+j+1;
+          tri[3*num_tri+2] = num_vert+j;
+          num_tri++;
+        }
+      }
+      num_vert += hf->num_tube+2;
+    }
+  }
+
+  glBindBuffer(GL_ARRAY_BUFFER,pos_buffer);
+  glBufferData(GL_ARRAY_BUFFER,3*num_vert*sizeof(GLfloat),vert,
+               GL_STREAM_DRAW);
+
+  glBindBuffer(GL_ARRAY_BUFFER,normal_buffer);
+  glBufferData(GL_ARRAY_BUFFER,3*num_vert*sizeof(GLfloat),norm,
+               GL_STREAM_DRAW);
+
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,indices_buffer);
+  glBufferData(GL_ELEMENT_ARRAY_BUFFER,3*num_tri*sizeof(GLuint),tri,
+               GL_STREAM_DRAW);
+
+  return num_tri;
+}
+
+
+/* Draw a set of vertex, normal, and index buffers. */
+static void draw_buffers(ModeInfo *mi, int from_light, GLint pos_buffer,
+                         GLuint normal_buffer, GLuint indices_buffer,
+                         GLuint num_tri)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+
+  if (from_light)
+  {
+    glEnableVertexAttribArray(hf->shadow_pos_index);
+    glBindBuffer(GL_ARRAY_BUFFER,pos_buffer);
+    glVertexAttribPointer(hf->shadow_pos_index,3,GL_FLOAT,GL_FALSE,0,0);
+  }
+  else
+  {
+    glEnableVertexAttribArray(hf->pos_index);
+    glBindBuffer(GL_ARRAY_BUFFER,pos_buffer);
+    glVertexAttribPointer(hf->pos_index,3,GL_FLOAT,GL_FALSE,0,0);
+
+    glEnableVertexAttribArray(hf->normal_index);
+    glBindBuffer(GL_ARRAY_BUFFER,normal_buffer);
+    glVertexAttribPointer(hf->normal_index,3,GL_FLOAT,GL_FALSE,0,0);
+
+    glDisableVertexAttribArray(hf->color_index);
+  }
+
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,indices_buffer);
+
+  glDrawElements(GL_TRIANGLES,3*num_tri,GL_UNSIGNED_INT,(const GLvoid *)0);
+
+  if (from_light)
+  {
+    glDisableVertexAttribArray(hf->shadow_pos_index);
+  }
+  else
+  {
+    glDisableVertexAttribArray(hf->pos_index);
+    glDisableVertexAttribArray(hf->normal_index);
+  }
+  glBindBuffer(GL_ARRAY_BUFFER,0);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
+
+  hf->num_poly += num_tri;
+}
+
+
+/* Draw the num_bp Hopf circles given by hb using the OpenGL programmable
+   functionality. */
+static int draw_hopf_circles_pf(ModeInfo *mi)
+{
+  static const GLfloat light_model_ambient[] = { 0.2f, 0.2f, 0.2f, 1.0f };
+  static const GLfloat light_ambient[] = { 0.3f, 0.3f, 0.3f, 1.0f };
+  static const GLfloat light_diffuse[] = { 0.7f, 0.7f, 0.7f, 1.0f };
+  static const GLfloat light_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+  static const GLfloat light_pos[] = { 0.7f, 0.7f, 1.0f, 0.0f };
+  static const GLfloat mat_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+  static const GLfloat mat_diff_white[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+  static const GLfloat mat_gray[] = { 0.7f, 0.7f, 0.7f, 0.6f };
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  GLfloat p_mat[16], mv_mat[16];
+  GLfloat lp_mat[16], lmv_mat[16], lmvp_mat[16], lmvp_bp_mat[16];
+  GLfloat light_direction[3], half_vector[3], len;
+  hopf_base_point ht;
+  float col[4], mat[3][3], mat1[3][3], mat2[3][3], mats[16];
+  float qu[4], qr[16];
+  float mid_depth;
+  int i, from_light;
+
+  hf->num_poly = 0;
+
+  len = norm(light_pos);
+  light_direction[0] = light_pos[0]/len;
+  light_direction[1] = light_pos[1]/len;
+  light_direction[2] = light_pos[2]/len;
+  half_vector[0] = light_direction[0];
+  half_vector[1] = light_direction[1];
+  half_vector[2] = light_direction[2]+1.0f;
+  len = sqrtf(half_vector[0]*half_vector[0]+
+              half_vector[1]*half_vector[1]+
+              half_vector[2]*half_vector[2]);
+  half_vector[0] /= len;
+  half_vector[1] /= len;
+  half_vector[2] /= len;
+
+  /* Compute the rotation matrix that rotates the base points. */
+  identity(mat1);
+  rotatex(mat1,hf->zeta);
+  rotatey(mat1,hf->eta);
+  rotatez(mat1,hf->theta);
+  quat_to_rotmat(hf->quat_base,mat2);
+  mult_rotmat(mat,mat2,mat1);
+
+  /* Compute the rotation matrix that rotates the space points. */
+  quat_to_rotmat_4x4(hf->quat_space,mats);
+
+  /* Compute the space rotation matrix from the trackball state. */
+  gltrackball_get_quaternion(hf->trackball,qu);
+  quat_to_rotmat_trackball_4x4(qu,qr);
+
+  /* Generate the icosphere for the base. */
+  hf->sphere_base_num_tri =
+    gen_icosphere_buffers(mi,hf->sphere_base,hf->sphere_base_pos_buffer,
+                          hf->sphere_base_normal_buffer,
+                          hf->sphere_base_indices_buffer);
+
+  /* Generate the icosphere for a base point. */
+  hf->base_point_num_tri =
+    gen_icosphere_buffers(mi,hf->sphere_base_point,hf->base_point_pos_buffer,
+                          hf->base_point_normal_buffer,
+                          hf->base_point_indices_buffer);
+
+  /* Generate the Hopf circles. */
+  for (i=0; i<hf->num_base_points; i++)
+  {
+    rotate_base_point(mat,&hf->base_points[i],&ht);
+    hf->fiber_num_tri[i] =
+      gen_hopf_circle_buffers(mi,&ht,hf->fiber_pos_buffer[i],
+                              hf->fiber_normal_buffer[i],
+                              hf->fiber_indices_buffer[i]);
+  }
+
+  mid_depth = norm(eye_pos);
+  glsl_Identity(p_mat);
+  if (hf->projection == DISP_ORTHOGRAPHIC)
+  {
+    if (hf->aspect >= 1.0f)
+      glsl_Orthographic(p_mat,-1.2f*hf->aspect,1.2f*hf->aspect,-1.2f,1.2f,
+                        mid_depth-1.2f,mid_depth+1.2f);
+    else
+      glsl_Orthographic(p_mat,-1.2f,1.2f,-1.2f/hf->aspect,1.2f/hf->aspect,
+                        mid_depth-1.2f,mid_depth+1.2f);
+  }
+  else /* hf->projection == DISP_PERSPECTIVE */
+  {
+    if (hf->aspect >= 1.0f)
+      glsl_Perspective(p_mat,45.0f,hf->aspect,mid_depth-1.2f,mid_depth+1.2f);
+    else
+      glsl_Perspective(p_mat,
+                       360.0f/M_PI_F*atanf(tanf(45.0f*M_PI_F/360.0f)/
+                                           hf->aspect),
+                       hf->aspect,mid_depth-1.2f,mid_depth+1.2f);
+  }
+
+  mid_depth = norm(light_pos);
+  glsl_Identity(lp_mat);
+  glsl_Orthographic(lp_mat,-1.4f,1.4f,-1.4f,1.4f,
+                    mid_depth-1.4f,mid_depth+1.4f);
+
+  for (from_light = (hf->shadows && hf->use_shadow_fbo); from_light >= 0;
+       from_light--)
+  {
+    glsl_Identity(mv_mat);
+    glsl_LookAt(mv_mat,eye_pos[0],eye_pos[1],eye_pos[2],
+                0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+    glsl_MultMatrix(mv_mat,qr);
+    glsl_MultMatrix(mv_mat,mats);
+    glsl_Rotate(mv_mat,hf->alpha,1.0f,0.0f,0.0f);
+    glsl_Rotate(mv_mat,hf->beta,0.0f,1.0f,0.0f);
+    glsl_Rotate(mv_mat,hf->delta,0.0f,0.0f,1.0f);
+
+    glsl_Identity(lmv_mat);
+    glsl_LookAt(lmv_mat,light_pos[0],light_pos[1],light_pos[2],
+                0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+    glsl_MultMatrix(lmv_mat,qr);
+    glsl_MultMatrix(lmv_mat,mats);
+    glsl_Rotate(lmv_mat,hf->alpha,1.0f,0.0f,0.0f);
+    glsl_Rotate(lmv_mat,hf->beta,0.0f,1.0f,0.0f);
+    glsl_Rotate(lmv_mat,hf->delta,0.0f,0.0f,1.0f);
+
+    glsl_CopyMatrix(lmvp_mat,lp_mat);
+    glsl_MultMatrix(lmvp_mat,lmv_mat);
+
+    if (from_light)
+    {
+      glBindFramebuffer(GL_FRAMEBUFFER,hf->shadow_fbo);
+
+      glUseProgram(hf->shadow_program);
+
+      glViewport(0,0,hf->shadow_tex_size,hf->shadow_tex_size);
+
+      glClearDepth(1.0f);
+      glClear(GL_DEPTH_BUFFER_BIT);
+
+      glColorMask(GL_FALSE,GL_FALSE,GL_FALSE,GL_FALSE);
+
+      glEnable(GL_POLYGON_OFFSET_FILL);
+      glPolygonOffset(4.0f,4.0f);
+
+      glEnable(GL_DEPTH_TEST);
+      glDepthFunc(GL_LESS);
+      glDepthMask(GL_TRUE);
+      glDisable(GL_BLEND);
+      glEnable(GL_CULL_FACE);
+      glCullFace(GL_BACK);
+
+      glUniformMatrix4fv(hf->shadow_lmvp_index,1,GL_FALSE,lmvp_mat);
+    }
+    else
+    {
+      if (hf->use_msaa_fbo)
+      {
+        glBindFramebuffer(GL_FRAMEBUFFER,hf->msaa_fbo);
+      }
+      else
+      {
+        glBindFramebuffer(GL_DRAW_FRAMEBUFFER,hf->default_draw_fbo);
+        glBindFramebuffer(GL_READ_FRAMEBUFFER,hf->default_read_fbo);
+      }
+
+      glUseProgram(hf->shader_program);
+
+      glViewport(0,0,hf->WindW,hf->WindH);
+
+      glClearColor(0.0f,0.0f,0.0f,0.0f);
+      glClearDepth(1.0f);
+      glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
+
+      glEnable(GL_DEPTH_TEST);
+      glDepthFunc(GL_LESS);
+      glDepthMask(GL_TRUE);
+      glDisable(GL_BLEND);
+      glEnable(GL_CULL_FACE);
+      glCullFace(GL_BACK);
+
+      glUniform4fv(hf->glbl_ambient_index,1,light_model_ambient);
+      glUniform4fv(hf->lt_ambient_index,1,light_ambient);
+      glUniform4fv(hf->lt_diffuse_index,1,light_diffuse);
+      glUniform4fv(hf->lt_specular_index,1,light_specular);
+      glUniform3fv(hf->lt_direction_index,1,light_direction);
+      glUniform3fv(hf->lt_halfvect_index,1,half_vector);
+      glUniform4fv(hf->ambient_index,1,mat_diff_white);
+      glUniform4fv(hf->diffuse_index,1,mat_diff_white);
+      glUniform4fv(hf->specular_index,1,mat_specular);
+      glUniform1f(hf->shininess_index,50.0f);
+
+      glUniformMatrix4fv(hf->proj_index,1,GL_FALSE,p_mat);
+      glUniformMatrix4fv(hf->mv_index,1,GL_FALSE,mv_mat);
+      glUniformMatrix4fv(hf->lmvp_index,1,GL_FALSE,lmvp_mat);
+
+      glVertexAttrib4f(hf->color_index,1.0f,1.0f,1.0f,1.0f);
+
+      glUniform1i(hf->use_shadows_index,(hf->shadows && hf->use_shadow_fbo));
+      glUniform1f(hf->shadow_pix_size_index,1.0f/hf->shadow_tex_size);
+      glActiveTexture(GL_TEXTURE0);
+      if (hf->shadows && hf->use_shadow_fbo)
+        glBindTexture(GL_TEXTURE_2D,hf->shadow_tex);
+      else
+        glBindTexture(GL_TEXTURE_2D,hf->shadow_dummy_tex);
+      glUniform1i(hf->shadow_smpl_index,0);
+    }
+
+    /* Draw the fibers (Hopf circles). */
+    for (i=0; i<hf->num_base_points; i++)
+    {
+      rotate_base_point(mat,&hf->base_points[i],&ht);
+
+      if (!from_light)
+      {
+        color(&ht,col);
+        glVertexAttrib4fv(hf->color_index,col);
+      }
+
+      draw_buffers(mi,from_light,hf->fiber_pos_buffer[i],
+                   hf->fiber_normal_buffer[i],hf->fiber_indices_buffer[i],
+                   hf->fiber_num_tri[i]);
+    }
+
+    if (hf->base_space)
+    {
+      /* Draw the base points. */
+      for (i=0; i<hf->num_base_points; i++)
+      {
+        rotate_base_point(mat,&hf->base_points[i],&ht);
+
+        glsl_Identity(mv_mat);
+        glsl_LookAt(mv_mat,eye_pos[0],eye_pos[1],eye_pos[2],
+                    0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+        glsl_Translate(mv_mat,base_offset[0],base_offset[1],base_offset[2]);
+#ifdef ROT_BASE_SPACE
+        glsl_MultMatrix(mv_mat,qr);
+        glsl_MultMatrix(mv_mat,mats);
+#endif
+        glsl_Rotate(mv_mat,hf->alpha,1.0f,0.0f,0.0f);
+        glsl_Rotate(mv_mat,hf->beta,0.0f,1.0f,0.0f);
+        glsl_Rotate(mv_mat,hf->delta,0.0f,0.0f,1.0f);
+        glsl_Translate(mv_mat,
+                       ht.a*BASE_SPHERE_RADIUS,
+                       ht.b*BASE_SPHERE_RADIUS,
+                       ht.c*BASE_SPHERE_RADIUS);
+
+        glsl_Identity(lmv_mat);
+        glsl_LookAt(lmv_mat,light_pos[0],light_pos[1],light_pos[2],
+                    0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+        glsl_Translate(lmv_mat,base_offset[0],base_offset[1],base_offset[2]);
+#ifdef ROT_BASE_SPACE
+        glsl_MultMatrix(lmv_mat,qr);
+        glsl_MultMatrix(lmv_mat,mats);
+#endif
+        glsl_Rotate(lmv_mat,hf->alpha,1.0f,0.0f,0.0f);
+        glsl_Rotate(lmv_mat,hf->beta,0.0f,1.0f,0.0f);
+        glsl_Rotate(lmv_mat,hf->delta,0.0f,0.0f,1.0f);
+        glsl_Translate(lmv_mat,
+                       ht.a*BASE_SPHERE_RADIUS,
+                       ht.b*BASE_SPHERE_RADIUS,
+                       ht.c*BASE_SPHERE_RADIUS);
+
+        glsl_CopyMatrix(lmvp_bp_mat,lp_mat);
+        glsl_MultMatrix(lmvp_bp_mat,lmv_mat);
+
+        if (from_light)
+        {
+          glUniformMatrix4fv(hf->shadow_lmvp_index,1,GL_FALSE,lmvp_bp_mat);
+        }
+        else
+        {
+          glUniformMatrix4fv(hf->proj_index,1,GL_FALSE,p_mat);
+          glUniformMatrix4fv(hf->mv_index,1,GL_FALSE,mv_mat);
+          glUniformMatrix4fv(hf->lmvp_index,1,GL_FALSE,lmvp_bp_mat);
+
+          color(&ht,col);
+          glVertexAttrib4fv(hf->color_index,col);
+        }
+
+        draw_buffers(mi,from_light,hf->base_point_pos_buffer,
+                     hf->base_point_normal_buffer,
+                     hf->base_point_indices_buffer,hf->base_point_num_tri);
+      }
+
+      if (!from_light)
+      {
+        /* Draw the base sphere, but not for the shadow buffer pass since
+           it is transparent. */
+        glDepthMask(GL_FALSE);
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
+        glDisable(GL_CULL_FACE);
+
+        glsl_Identity(mv_mat);
+        glsl_LookAt(mv_mat,eye_pos[0],eye_pos[1],eye_pos[2],
+                    0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+        glsl_Translate(mv_mat,base_offset[0],base_offset[1],base_offset[2]);
+#ifdef ROT_BASE_SPACE
+        glsl_MultMatrix(mv_mat,qr);
+        glsl_MultMatrix(mv_mat,mats);
+#endif
+        glsl_Rotate(mv_mat,hf->alpha,1.0f,0.0f,0.0f);
+        glsl_Rotate(mv_mat,hf->beta,0.0f,1.0f,0.0f);
+        glsl_Rotate(mv_mat,hf->delta,0.0f,0.0f,1.0f);
+
+        glsl_Identity(lmv_mat);
+        glsl_LookAt(lmv_mat,light_pos[0],light_pos[1],light_pos[2],
+                    0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+        glsl_Translate(lmv_mat,base_offset[0],base_offset[1],base_offset[2]);
+#ifdef ROT_BASE_SPACE
+        glsl_MultMatrix(lmv_mat,qr);
+        glsl_MultMatrix(lmv_mat,mats);
+#endif
+        glsl_Rotate(lmv_mat,hf->alpha,1.0f,0.0f,0.0f);
+        glsl_Rotate(lmv_mat,hf->beta,0.0f,1.0f,0.0f);
+        glsl_Rotate(lmv_mat,hf->delta,0.0f,0.0f,1.0f);
+
+        glsl_CopyMatrix(lmvp_bp_mat,lp_mat);
+        glsl_MultMatrix(lmvp_bp_mat,lmv_mat);
+
+        glUniformMatrix4fv(hf->proj_index,1,GL_FALSE,p_mat);
+        glUniformMatrix4fv(hf->mv_index,1,GL_FALSE,mv_mat);
+        glUniformMatrix4fv(hf->lmvp_index,1,GL_FALSE,lmvp_bp_mat);
+
+        glVertexAttrib4fv(hf->color_index,mat_gray);
+
+        draw_buffers(mi,from_light,hf->sphere_base_pos_buffer,
+                     hf->sphere_base_normal_buffer,
+                     hf->sphere_base_indices_buffer,hf->sphere_base_num_tri);
+
+        glDepthMask(GL_TRUE);
+        glDisable(GL_BLEND);
+      }
+    }
+
+    glUseProgram(0);
+
+    if (from_light)
+    {
+      glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE);
+      glDisable(GL_POLYGON_OFFSET_FILL);
+    }
+    else
+    {
+      if (hf->use_msaa_fbo)
+      {
+        glBindFramebuffer(GL_DRAW_FRAMEBUFFER,hf->default_draw_fbo);
+        glBindFramebuffer(GL_READ_FRAMEBUFFER,hf->msaa_fbo);
+
+        glBlitFramebuffer(0,0,hf->WindW,hf->WindH,0,0,hf->WindW,hf->WindH,
+                          GL_COLOR_BUFFER_BIT,GL_NEAREST);
+      }
+    }
+
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,hf->default_draw_fbo);
+    glBindFramebuffer(GL_READ_FRAMEBUFFER,hf->default_read_fbo);
+  }
+
+  glBindTexture(GL_TEXTURE_2D,0);
+
+  return hf->num_poly;
+}
+
+#endif /* HAVE_GLSL */
+
+
+/* Generate a torus, a Hopf torus, or a Hopf flower on the base.  A torus is
+   generated if q = 0.0 and r = 0.0.  A Hopf torus is generated if r = 0.0.
+   Otherwise, a Hopf flower is generated. */
+static void gen_hopf_torus_base(ModeInfo *mi, float p, float q, float r,
+                                int n, float offset, float sector, int num,
+                                Bool rotate, float quat[4])
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  float t, g, h;
+  float bp[3], bpr[3];
+  float m[3][3];
+  int i;
+
+  if (num < 1)
+    num = 1;
+  if ((p == 0.0f || p == M_PI_F) && q == 0.0f)
+    num = 1;
+  if (sector == 0.0f)
+    num = 1;
+
+  if (rotate)
+    quat_to_rotmat(quat,m);
+
+  for (i=0; i<num; i++)
+  {
+    t = offset+i*sector/num;
+    if (q == 0.0f)
+      g = p;
+    else
+      g = p+q*sinf(n*t);
+    if (r == 0.0f)
+      h = t;
+    else
+      h = t+r*cosf(n*t);
+    bp[0] = cosf(h)*sinf(g);
+    bp[1] = sinf(h)*sinf(g);
+    bp[2] = cosf(g);
+    if (!rotate)
+    {
+      hf->base_points[hf->num_base_points].a = bp[0];
+      hf->base_points[hf->num_base_points].b = bp[1];
+      hf->base_points[hf->num_base_points].c = bp[2];
+    }
+    else
+    {
+      mult_rotmat_vec(bpr,m,bp);
+      hf->base_points[hf->num_base_points].a = bpr[0];
+      hf->base_points[hf->num_base_points].b = bpr[1];
+      hf->base_points[hf->num_base_points].c = bpr[2];
+    }
+    hf->num_base_points++;
+  }
+}
+
+
+/* Generate a Hopf spiral on the base. */
+static void gen_hopf_spiral_base(ModeInfo *mi, float p, float q, float r,
+                                 float offset, float sector, int num,
+                                 Bool rotate, float quat[4])
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  float t;
+  float bp[3], bpr[3];
+  float m[3][3];
+  int i;
+
+  if (num < 1)
+    num = 1;
+  if (sector == 0.0f)
+    num = 1;
+
+  if (rotate)
+    quat_to_rotmat(quat,m);
+
+  for (i=0; i<num; i++)
+  {
+    t = offset+i*sector/num;
+    bp[0] = cosf(-r*t)*cosf(p+0.5f*q*t);
+    bp[1] = sinf(-r*t)*cosf(p+0.5f*q*t);
+    bp[2] = -sinf(p+0.5f*q*t);
+    if (!rotate)
+    {
+      hf->base_points[hf->num_base_points].a = bp[0];
+      hf->base_points[hf->num_base_points].b = bp[1];
+      hf->base_points[hf->num_base_points].c = bp[2];
+    }
+    else
+    {
+      mult_rotmat_vec(bpr,m,bp);
+      hf->base_points[hf->num_base_points].a = bpr[0];
+      hf->base_points[hf->num_base_points].b = bpr[1];
+      hf->base_points[hf->num_base_points].c = bpr[2];
+    }
+    hf->num_base_points++;
+  }
+}
+
+
+/* An easing function for values of t between 0 and 1. */
+static float ease(float t, int easing)
+{
+  if (easing == EASING_NONE)
+    return 0.0f;
+  else if (easing == EASING_CUBIC)
+    return t*t*(3.0f-2.0f*t);
+  else if (easing == EASING_SIN)
+    return (t < 0.25f ? 0.5f*ease(4.0f*t,EASING_CUBIC)+0.5f :
+            (t > 0.75f ? 0.5f*ease(4.0f*t-3.0f,EASING_CUBIC) :
+             1.0f-ease(2.0f*t-0.5f,EASING_CUBIC)));
+  else if (easing == EASING_COS)
+    return (t < 0.5f ? 1.0f-ease(2.0f*t,EASING_CUBIC) :
+            ease(2.0f*t-1.0f,EASING_CUBIC));
+  else if (easing == EASING_LIN)
+    return t;
+  else if (easing == EASING_ACCEL)
+    return t*t*(2.0f-t);
+  else if (easing == EASING_DECEL)
+    return t*(1.0f+t*(1.0f-t));
+  else
+    return 0.0f;
+}
+
+
+/* Compute a quaternion from a rotation axis and an angle in radians. */
+static void axis_angle_to_quat(float rot_axis[3], float angle, float q[4])
+{
+  q[0] = cosf(0.5f*angle);
+  q[1] = sinf(0.5f*angle)*rot_axis[0];
+  q[2] = sinf(0.5f*angle)*rot_axis[1];
+  q[3] = sinf(0.5f*angle)*rot_axis[2];
+}
+
+
+/* Compute a uniformly distributed random rotation axis, i.e., a vector of
+   length 1 that is uniformly distributed on the unit sphere S². */
+static void gen_random_rot_axis(float rot_axis[3])
+{
+  float p, t;
+
+  t = (float)frand(2.0f*M_PI_F);
+  p = acosf((float)frand(2.0f)-1.0f);
+  rot_axis[0] = sinf(p)*cosf(t);
+  rot_axis[1] = sinf(p)*sinf(t);
+  rot_axis[2] = cosf(p);
+}
+
+
+/* Set the animation quaternions based on the animation time t. */
+static void set_animation_quats(ModeInfo *mi, float t)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  float te;
+
+  if (hf->anim_rotate_rnd)
+  {
+    te = 2.0f*M_PI_F*ease(t,hf->anim_easing_fct_rot_rnd);
+    axis_angle_to_quat(hf->rot_axis_base,te,hf->quat_base);
+  }
+  if (hf->anim_rotate_space)
+  {
+    te = ease(t,hf->anim_easing_fct_rot_space);
+    te = (1.0f-te)*hf->angle_space_start+te*hf->angle_space_end;
+    axis_angle_to_quat(hf->rot_axis_space,te,hf->quat_space);
+  }
+}
+
+
+/* Set the geometry parameters for the animation object i based on the
+   animation time t. */
+static void set_animation_geometry(ModeInfo *mi, float t, int i)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  float te, angle;
+
+  /* Sanity check. */
+  if (i >= MAX_ANIM_GEOM)
+    return;
+
+  hf->anim_geom[i].num = hf->anim->anim_so[i].num;
+  te = ease(t,hf->anim->anim_so[i].easing_function_p);
+  hf->anim_geom[i].p = ((1.0f-te)*hf->anim->anim_so[i].p_start+
+                        te*hf->anim->anim_so[i].p_end);
+  te = ease(t,hf->anim->anim_so[i].easing_function_q);
+  hf->anim_geom[i].q = ((1.0f-te)*hf->anim->anim_so[i].q_start+
+                        te*hf->anim->anim_so[i].q_end);
+  te = ease(t,hf->anim->anim_so[i].easing_function_r);
+  hf->anim_geom[i].r = ((1.0f-te)*hf->anim->anim_so[i].r_start+
+                        te*hf->anim->anim_so[i].r_end);
+  te = ease(t,hf->anim->anim_so[i].easing_function_offset);
+  hf->anim_geom[i].offset = ((1.0f-te)*hf->anim->anim_so[i].offset_start+
+                             te*hf->anim->anim_so[i].offset_end);
+  te = ease(t,hf->anim->anim_so[i].easing_function_sector);
+  hf->anim_geom[i].sector = ((1.0f-te)*hf->anim->anim_so[i].sector_start+
+                             te*hf->anim->anim_so[i].sector_end);
+  if (hf->anim_geom[i].rotate)
+  {
+    te = ease(t,hf->anim->anim_so[i].easing_function_rotate);
+    angle = ((1.0f-te)*hf->anim->anim_so[i].angle_start+
+             te*hf->anim->anim_so[i].angle_end);
+    axis_angle_to_quat(hf->anim->anim_so[i].rot_axis_base,angle,
+                       hf->anim_geom[i].quat_base);
+  }
+}
+
+
+/* Select the next animation phase. */
+static void init_next_anim_phase(ModeInfo *mi, int phase)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  int i;
+  float nrm;
+
+  hf->anim = hf->anim_phases->anim_mo[phase];
+  hf->anim_step = 0;
+
+  for (i=0; i<hf->anim->num; i++)
+  {
+    /* Sanity check. */
+    if (i >= MAX_ANIM_GEOM)
+      break;
+
+    hf->anim_geom[i].generator = hf->anim->anim_so[i].generator;
+
+    hf->anim_geom[i].n = hf->anim->anim_so[i].n;
+
+    nrm = norm(hf->anim->anim_so[i].rot_axis_base);
+    hf->anim_geom[i].rotate = (nrm > 0.0f);
+    hf->anim_geom[i].quat_base[0] = 1.0f;
+    hf->anim_geom[i].quat_base[1] = 0.0f;
+    hf->anim_geom[i].quat_base[2] = 0.0f;
+    hf->anim_geom[i].quat_base[3] = 0.0f;
+  }
+
+  if (hf->anim->rotate_prob == 0.0f)
+    hf->anim_rotate_rnd = False;
+  else if (hf->anim->rotate_prob == 1.0f)
+    hf->anim_rotate_rnd = True;
+  else
+    hf->anim_rotate_rnd = (frand(1.0f) < hf->anim->rotate_prob);
+  hf->anim_easing_fct_rot_rnd = hf->anim->easing_function_rot_rnd;
+
+  gen_random_rot_axis(hf->rot_axis_base);
+
+  hf->anim_easing_fct_rot_space = hf->anim->easing_function_rot_space;
+  hf->rot_axis_space[0] = hf->anim->rot_axis_space[0];
+  hf->rot_axis_space[1] = hf->anim->rot_axis_space[1];
+  hf->rot_axis_space[2] = hf->anim->rot_axis_space[2];
+  nrm = norm(hf->rot_axis_space);
+  hf->anim_rotate_space = (nrm > 0.0f);
+  hf->angle_space_start = hf->anim->angle_start;
+  hf->angle_space_end = hf->anim->angle_end;
+
+  set_animation_quats(mi,0.0f);
+  for (i=0; i<hf->anim->num; i++)
+    set_animation_geometry(mi,0.0f,i);
+}
+
+
+/* Select the next animation. */
+static void set_next_anim(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  int anim_state_next, idx_anim, num_anim;
+  animations *anims_next;
+
+  if (hf->anim_remain_in_state <= 0)
+    anim_state_next = (int)floor(frand(NUM_ANIM_STATES));
+  else
+    anim_state_next = hf->anim_state;
+
+  if (anim_state_next == hf->anim_state)
+  {
+    hf->anim_remain_in_state--;
+  }
+  else
+  {
+    /* Set the number of animations to execute in the next animation state
+       based on the number of available animations in that state. */
+    hf->anim_remain_in_state =
+      (hopf_animations[anim_state_next][anim_state_next]->num_anim+2)/3;
+  }
+
+  anims_next = hopf_animations[hf->anim_state][anim_state_next];
+  hf->anim_state = anim_state_next;
+
+  num_anim = anims_next->num_anim;
+
+  if (num_anim > 1)
+  {
+    /* If there are animations to choose from, avoid using the same
+       animation twice in succession. */
+    do
+      idx_anim = (int)floor(frand(num_anim));
+    while (hf->anim_phases == anims_next->anim[idx_anim]);
+  }
+  else
+  {
+    /* Select the only available choice. */
+    idx_anim = 0;
+  }
+
+  hf->anim_phases = anims_next->anim[idx_anim];
+  hf->anim_phase_num = hf->anim_phases->num_phases;
+  hf->anim_phase = 0;
+
+  init_next_anim_phase(mi,hf->anim_phase);
+}
+
+
+/* Display the Hopf fibration. */
+static void display_hopffibration(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  float t;
+  int i;
+
+  if (!hf->button_pressed)
+  {
+    hf->anim_step++;
+    t = (float)hf->anim_step/(float)hf->anim->num_steps;
+
+    set_animation_quats(mi,t);
+    for (i=0; i<hf->anim->num; i++)
+      set_animation_geometry(mi,t,i);
+  
+    if (hf->anim_step >= hf->anim->num_steps)
+    {
+      hf->anim_step = 0;
+      if (hf->anim_phase < hf->anim_phase_num-1)
+      {
+        hf->anim_phase++;
+        init_next_anim_phase(mi,hf->anim_phase);
+      }
+      else
+      {
+        set_next_anim(mi);
+      }
+    }
+  }
+
+  gltrackball_rotate(hf->trackball);
+
+  hf->num_base_points = 0;
+
+  for (i=0; i<hf->anim->num; i++)
+  {
+    if (hf->anim_geom[i].generator == GEN_TORUS)
+      gen_hopf_torus_base(mi,hf->anim_geom[i].p,hf->anim_geom[i].q,
+                          hf->anim_geom[i].r,hf->anim_geom[i].n,
+                          hf->anim_geom[i].offset,hf->anim_geom[i].sector,
+                          hf->anim_geom[i].num,hf->anim_geom[i].rotate,
+                          hf->anim_geom[i].quat_base);
+    else /* hf->anim_geom[i].generator == GEN_SPIRAL */
+      gen_hopf_spiral_base(mi,hf->anim_geom[i].p,hf->anim_geom[i].q,
+                           hf->anim_geom[i].r,hf->anim_geom[i].offset,
+                           hf->anim_geom[i].sector,hf->anim_geom[i].num,
+                           hf->anim_geom[i].rotate,hf->anim_geom[i].quat_base);
+  }
+
+#ifdef HAVE_GLSL
+  if (hf->use_shaders)
+    mi->polygon_count = draw_hopf_circles_pf(mi);
+  else
+#endif /* HAVE_GLSL */
+    mi->polygon_count = draw_hopf_circles_ff(mi);
+}
+
+
+#ifdef HAVE_GLSL
+
+/* Compute the next highest power of 2 of v. */
+static unsigned int next_pow2(unsigned int v)
+{
+  v--;
+  v |= v >> 1;
+  v |= v >> 2;
+  v |= v >> 4;
+  v |= v >> 8;
+  v |= v >> 16;
+  v++;
+  v += (v == 0);
+  return v;
+}
+
+
+/* Delete the storage for the MSAA framebuffer object. */
+static void delete_msaa_fbo(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+
+  if (hf->use_msaa_fbo)
+  {
+    glBindFramebuffer(GL_FRAMEBUFFER,hf->msaa_fbo);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,
+                              GL_RENDERBUFFER,0);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,
+                              GL_RENDERBUFFER,0);
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,hf->default_draw_fbo);
+    glBindFramebuffer(GL_READ_FRAMEBUFFER,hf->default_read_fbo);
+    glDeleteRenderbuffers(1,&hf->msaa_rb_color);
+    glDeleteRenderbuffers(1,&hf->msaa_rb_depth);
+    glDeleteFramebuffers(1,&hf->msaa_fbo);
+  }
+}
+
+
+/* Allocate the storage for the MSAA framebuffer object. */
+static void init_msaa_fbo(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  GLenum status;
+
+  if (hf->use_msaa_fbo)
+  {
+    glGenFramebuffers(1,&hf->msaa_fbo);
+
+    glGenRenderbuffers(1,&hf->msaa_rb_color);
+    glGenRenderbuffers(1,&hf->msaa_rb_depth);
+
+    glBindRenderbuffer(GL_RENDERBUFFER,hf->msaa_rb_color);
+    glRenderbufferStorageMultisample(GL_RENDERBUFFER,hf->msaa_samples,GL_RGBA8,
+                                     hf->WindW,hf->WindH);
+
+    glBindRenderbuffer(GL_RENDERBUFFER,hf->msaa_rb_depth);
+    glRenderbufferStorageMultisample(GL_RENDERBUFFER,hf->msaa_samples,
+                                     GL_DEPTH_COMPONENT24,
+                                     hf->WindW,hf->WindH);
+    glBindRenderbuffer(GL_RENDERBUFFER,0);
+
+    glBindFramebuffer(GL_FRAMEBUFFER,hf->msaa_fbo);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,
+                              GL_RENDERBUFFER,hf->msaa_rb_color);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,
+                              GL_RENDERBUFFER,hf->msaa_rb_depth);
+
+    status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+    if (status != GL_FRAMEBUFFER_COMPLETE)
+    {
+      delete_msaa_fbo(mi);
+      hf->use_msaa_fbo = False;
+    }
+
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,hf->default_draw_fbo);
+    glBindFramebuffer(GL_READ_FRAMEBUFFER,hf->default_read_fbo);
+  }
+}
+
+
+/* Delete the storage for the shadow map framebuffer object. */
+static void delete_shadow_fbo(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+
+  if (hf->use_shadow_fbo)
+  {
+    glBindFramebuffer(GL_FRAMEBUFFER,hf->shadow_fbo);
+    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_TEXTURE_2D,
+                           0,0);
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,hf->default_draw_fbo);
+    glBindFramebuffer(GL_READ_FRAMEBUFFER,hf->default_read_fbo);
+    glDeleteTextures(1,&hf->shadow_tex);
+    if (hf->shadow_fbo_req_col_tex)
+      glDeleteTextures(1,&hf->shadow_col_tex);
+    glDeleteFramebuffers(1,&hf->shadow_fbo);
+  }
+}
+
+
+/* Allocate the storage for the shadow map framebuffer object. */
+static void init_shadow_fbo(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  GLenum status;
+  GLenum draw_buffer = GL_NONE;
+
+  if (hf->use_shadow_fbo)
+  {
+    glGenFramebuffers(1,&hf->shadow_fbo);
+
+    glGenTextures(1,&hf->shadow_tex);
+
+    hf->shadow_tex_size = MIN(next_pow2(hf->WindW),next_pow2(hf->WindH));
+    hf->shadow_tex_size = MIN(hf->shadow_tex_size,hf->max_tex_size);
+    glBindTexture(GL_TEXTURE_2D,hf->shadow_tex);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_MODE,
+                    GL_COMPARE_REF_TO_TEXTURE);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC,GL_LEQUAL);
+    glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT32F,hf->shadow_tex_size,
+                 hf->shadow_tex_size,0,GL_DEPTH_COMPONENT,GL_FLOAT,NULL);
+    glBindTexture(GL_TEXTURE_2D,0);
+
+    glBindFramebuffer(GL_FRAMEBUFFER,hf->shadow_fbo);
+    glDrawBuffers(1,&draw_buffer);
+    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_TEXTURE_2D,
+                           hf->shadow_tex,0);
+
+    if (hf->shadow_fbo_req_col_tex)
+    {
+      draw_buffer = GL_COLOR_ATTACHMENT0;
+
+      glGenTextures(1,&hf->shadow_col_tex);
+
+      glBindTexture(GL_TEXTURE_2D,hf->shadow_col_tex);
+      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
+      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
+      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
+
+      glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,hf->shadow_tex_size,
+                   hf->shadow_tex_size,0,GL_RGBA,GL_UNSIGNED_BYTE,NULL);
+      glBindTexture(GL_TEXTURE_2D,0);
+
+      glDrawBuffers(1,&draw_buffer);
+      glBindFramebuffer(GL_FRAMEBUFFER,hf->shadow_fbo);
+      glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,
+                             hf->shadow_col_tex,0);
+    }
+    else
+    {
+      hf->shadow_col_tex = 0;
+    }
+
+    status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+    if (status != GL_FRAMEBUFFER_COMPLETE)
+    {
+      delete_shadow_fbo(mi);
+      hf->use_shadow_fbo = False;
+    }
+
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,hf->default_draw_fbo);
+    glBindFramebuffer(GL_READ_FRAMEBUFFER,hf->default_read_fbo);
+  }
+}
+
+
+/* Initialize the programmable OpenGL functionality. */
+static void init_glsl(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+  GLint gl_major, gl_minor, glsl_major, glsl_minor;
+  GLboolean gl_gles3;
+  const GLchar *vertex_shader_source[3];
+  const GLchar *fragment_shader_source[4];
+  GLint samples, sample_buffers;
+  const char *gl_ext;
+  static float one = 1.0f;
+
+  hf->use_shaders = False;
+  hf->use_msaa_fbo = False;
+  hf->use_shadow_fbo = False;
+  hf->shader_program = 0;
+  hf->shadow_program = 0;
+
+  if (!glsl_GetGlAndGlslVersions(&gl_major,&gl_minor,&glsl_major,&glsl_minor,
+                                 &gl_gles3))
+    return;
+
+  if (!gl_gles3)
+  {
+    if (gl_major < 3 ||
+        (glsl_major < 1 || (glsl_major == 1 && glsl_minor < 30)))
+    {
+      if ((gl_major < 2 || (gl_major == 2 && gl_minor < 1)) ||
+          (glsl_major < 1 || (glsl_major == 1 && glsl_minor < 20)))
+        return;
+      /* We have at least OpenGL 2.1 and at least GLSL 1.20. */
+      vertex_shader_source[0] = shader_version_2_1;
+      vertex_shader_source[1] = vertex_shader_attribs_2_1;
+      vertex_shader_source[2] = vertex_shader_main;
+      fragment_shader_source[0] = shader_version_2_1;
+      fragment_shader_source[1] = fragment_shader_attribs_2_1;
+      fragment_shader_source[2] = fragment_shader_main;
+      fragment_shader_source[3] = fragment_shader_out_2_1;
+    }
+    else
+    {
+      /* We have at least OpenGL 3.0 and at least GLSL 1.30. */
+      vertex_shader_source[0] = shader_version_3_0;
+      vertex_shader_source[1] = vertex_shader_attribs_3_0;
+      vertex_shader_source[2] = vertex_shader_main;
+      fragment_shader_source[0] = shader_version_3_0;
+      fragment_shader_source[1] = fragment_shader_attribs_3_0;
+      fragment_shader_source[2] = fragment_shader_main;
+      fragment_shader_source[3] = fragment_shader_out_3_0;
+    }
+  }
+  else /* gl_gles3 */
+  {
+    if (gl_major < 3 || glsl_major < 3)
+      return;
+    /* We have at least OpenGL ES 3.0 and at least GLSL ES 3.0. */
+    vertex_shader_source[0] = shader_version_3_0_es;
+    vertex_shader_source[1] = vertex_shader_attribs_3_0;
+    vertex_shader_source[2] = vertex_shader_main;
+    fragment_shader_source[0] = shader_version_3_0_es;
+    fragment_shader_source[1] = fragment_shader_attribs_3_0;
+    fragment_shader_source[2] = fragment_shader_main;
+    fragment_shader_source[3] = fragment_shader_out_3_0;
+  }
+
+  if (!glsl_CompileAndLinkShaders(3,vertex_shader_source,
+                                  4,fragment_shader_source,
+                                  &hf->shader_program))
+    return;
+
+  hf->pos_index = glGetAttribLocation(hf->shader_program,
+                                      "VertexPosition");
+  hf->normal_index = glGetAttribLocation(hf->shader_program,
+                                         "VertexNormal");
+  hf->color_index = glGetAttribLocation(hf->shader_program,
+                                        "VertexColor");
+  hf->mv_index = glGetUniformLocation(hf->shader_program,
+                                      "MatModelView");
+  hf->proj_index = glGetUniformLocation(hf->shader_program,
+                                        "MatProj");
+  hf->lmvp_index = glGetUniformLocation(hf->shader_program,
+                                        "MatLightMVP");
+  hf->glbl_ambient_index = glGetUniformLocation(hf->shader_program,
+                                                "LtGlblAmbient");
+  hf->lt_ambient_index = glGetUniformLocation(hf->shader_program,
+                                              "LtAmbient");
+  hf->lt_diffuse_index = glGetUniformLocation(hf->shader_program,
+                                              "LtDiffuse");
+  hf->lt_specular_index = glGetUniformLocation(hf->shader_program,
+                                               "LtSpecular");
+  hf->lt_direction_index = glGetUniformLocation(hf->shader_program,
+                                                "LtDirection");
+  hf->lt_halfvect_index = glGetUniformLocation(hf->shader_program,
+                                               "LtHalfVector");
+  hf->ambient_index = glGetUniformLocation(hf->shader_program,
+                                           "MatAmbient");
+  hf->diffuse_index = glGetUniformLocation(hf->shader_program,
+                                           "MatDiffuse");
+  hf->specular_index = glGetUniformLocation(hf->shader_program,
+                                            "MatSpecular");
+  hf->shininess_index = glGetUniformLocation(hf->shader_program,
+                                             "MatShininess");
+  hf->use_shadows_index = glGetUniformLocation(hf->shader_program,
+                                               "UseShadows");
+  hf->shadow_smpl_index = glGetUniformLocation(hf->shader_program,
+                                               "ShadowTex");
+  hf->shadow_pix_size_index = glGetUniformLocation(hf->shader_program,
+                                                   "ShadowPixSize");
+  if (hf->pos_index == -1 ||
+      hf->normal_index == -1 ||
+      hf->color_index == -1 ||
+      hf->mv_index == -1 ||
+      hf->proj_index == -1 ||
+      hf->lmvp_index == -1 ||
+      hf->glbl_ambient_index == -1 ||
+      hf->lt_ambient_index == -1 ||
+      hf->lt_diffuse_index == -1 ||
+      hf->lt_specular_index == -1 ||
+      hf->lt_direction_index == -1 ||
+      hf->lt_halfvect_index == -1 ||
+      hf->ambient_index == -1 ||
+      hf->diffuse_index == -1 ||
+      hf->specular_index == -1 ||
+      hf->shininess_index == -1 ||
+      hf->use_shadows_index == -1 ||
+      hf->shadow_smpl_index == -1 ||
+      hf->shadow_pix_size_index == -1)
+  {
+    glDeleteProgram(hf->shader_program);
+    hf->shader_program = 0;
+    return;
+  }
+
+  if (!gl_gles3)
+  {
+    if (gl_major < 3 ||
+        (glsl_major < 1 || (glsl_major == 1 && glsl_minor < 30)))
+    {
+      if ((gl_major < 2 || (gl_major == 2 && gl_minor < 1)) ||
+          (glsl_major < 1 || (glsl_major == 1 && glsl_minor < 20)))
+        return;
+      /* We have at least OpenGL 2.1 and at least GLSL 1.20. */
+      vertex_shader_source[0] = shader_version_2_1;
+      vertex_shader_source[1] = shadow_vertex_shader_attribs_2_1;
+      vertex_shader_source[2] = shadow_vertex_shader_main;
+      fragment_shader_source[0] = shader_version_2_1;
+      fragment_shader_source[1] = shadow_fragment_shader_main;
+    }
+    else
+    {
+      /* We have at least OpenGL 3.0 and at least GLSL 1.30. */
+      vertex_shader_source[0] = shader_version_3_0;
+      vertex_shader_source[1] = shadow_vertex_shader_attribs_3_0;
+      vertex_shader_source[2] = shadow_vertex_shader_main;
+      fragment_shader_source[0] = shader_version_3_0;
+      fragment_shader_source[1] = shadow_fragment_shader_main;
+    }
+  }
+  else /* gl_gles3 */
+  {
+    if (gl_major < 3 || glsl_major < 3)
+      return;
+    /* We have at least OpenGL ES 3.0 and at least GLSL ES 3.0. */
+    vertex_shader_source[0] = shader_version_3_0_es;
+    vertex_shader_source[1] = shadow_vertex_shader_attribs_3_0;
+    vertex_shader_source[2] = shadow_vertex_shader_main;
+    fragment_shader_source[0] = shader_version_3_0_es;
+    fragment_shader_source[1] = shadow_fragment_shader_main;
+  }
+
+  if (!glsl_CompileAndLinkShaders(3,vertex_shader_source,
+                                  2,fragment_shader_source,
+                                  &hf->shadow_program))
+  {
+    glDeleteProgram(hf->shader_program);
+    hf->shader_program = 0;
+    return;
+  }
+
+  hf->use_shadow_fbo = True;
+  hf->shadow_pos_index = glGetAttribLocation(hf->shadow_program,
+                                             "VertexPosition");
+  hf->shadow_lmvp_index = glGetUniformLocation(hf->shadow_program,
+                                               "MatLightMVP");
+  if (hf->shadow_pos_index == -1 ||
+      hf->shadow_lmvp_index == -1)
+  {
+    glDeleteProgram(hf->shader_program);
+    glDeleteProgram(hf->shadow_program);
+    hf->shader_program = 0;
+    hf->shadow_program = 0;
+    hf->use_shadow_fbo = False;
+  }
+
+  hf->fiber_pos_buffer = calloc(hf->max_base_points,
+                                sizeof(*hf->fiber_pos_buffer));
+  hf->fiber_normal_buffer = calloc(hf->max_base_points,
+                                   sizeof(*hf->fiber_normal_buffer));
+  hf->fiber_indices_buffer = calloc(hf->max_base_points,
+                                    sizeof(*hf->fiber_indices_buffer));
+  hf->fiber_num_tri = calloc(hf->max_base_points,sizeof(*hf->fiber_num_tri));
+  if (hf->fiber_pos_buffer == NULL || hf->fiber_normal_buffer == NULL ||
+      hf->fiber_indices_buffer == NULL || hf->fiber_num_tri == NULL)
+    abort();
+  glGenBuffers(hf->max_base_points,hf->fiber_pos_buffer);
+  glGenBuffers(hf->max_base_points,hf->fiber_normal_buffer);
+  glGenBuffers(hf->max_base_points,hf->fiber_indices_buffer);
+  glGenBuffers(1,&hf->sphere_base_pos_buffer);
+  glGenBuffers(1,&hf->sphere_base_normal_buffer);
+  glGenBuffers(1,&hf->sphere_base_indices_buffer);
+  glGenBuffers(1,&hf->base_point_pos_buffer);
+  glGenBuffers(1,&hf->base_point_normal_buffer);
+  glGenBuffers(1,&hf->base_point_indices_buffer);
+
+  glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING,(GLint *)&hf->default_draw_fbo);
+  glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING,(GLint *)&hf->default_read_fbo);
+
+  glGetIntegerv(GL_SAMPLES,&samples);
+  glGetIntegerv(GL_SAMPLE_BUFFERS,&sample_buffers);
+  hf->use_msaa_fbo = (hf->anti_aliasing &&
+                      (sample_buffers == 0 || samples == 0));
+  if (!gl_gles3 && gl_major < 3 && hf->use_msaa_fbo)
+  {
+    gl_ext = (const char *)glGetString(GL_EXTENSIONS);
+    if (gl_ext == NULL)
+      hf->use_msaa_fbo = False;
+    else
+      hf->use_msaa_fbo =
+        (strstr(gl_ext,"GL_EXT_framebuffer_object") != NULL &&
+         strstr(gl_ext,"GL_EXT_framebuffer_blit") != NULL &&
+         strstr(gl_ext,"GL_EXT_framebuffer_multisample") != NULL);
+  }
+
+  if (hf->use_msaa_fbo)
+  {
+    glEnable(GL_MULTISAMPLE);
+    glGetIntegerv(GL_MAX_SAMPLES,&hf->msaa_samples);
+    hf->msaa_samples = MIN(hf->msaa_samples,MSAA_SAMPLES);
+    init_msaa_fbo(mi);
+  }
+
+  hf->shadow_fbo_req_col_tex = (!gl_gles3 && gl_major < 3);
+  if (!gl_gles3 && gl_major < 3 && hf->use_shadow_fbo)
+  {
+    gl_ext = (const char *)glGetString(GL_EXTENSIONS);
+    if (gl_ext == NULL)
+      hf->use_shadow_fbo = GL_FALSE;
+    else
+      hf->use_shadow_fbo =
+        (strstr(gl_ext,"GL_ARB_shadow") != NULL &&
+         strstr(gl_ext,"GL_ARB_depth_buffer_float") != NULL);
+  }
+
+  if (hf->use_shadow_fbo)
+  {
+    glGetIntegerv(GL_MAX_TEXTURE_SIZE,&hf->max_tex_size);
+    init_shadow_fbo(mi);
+  }
+
+  glGenTextures(1,&hf->shadow_dummy_tex);
+  glBindTexture(GL_TEXTURE_2D,hf->shadow_dummy_tex);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_MODE,
+                  GL_COMPARE_REF_TO_TEXTURE);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC,GL_LEQUAL);
+  glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT32F,1,1,0,
+               GL_DEPTH_COMPONENT,GL_FLOAT,&one);
+  glBindTexture(GL_TEXTURE_2D,0);
+
+  hf->vert = calloc(3*MAX_CIRCLE_PNT*hf->num_tube,sizeof(*hf->vert));
+  hf->norm = calloc(3*MAX_CIRCLE_PNT*hf->num_tube,sizeof(*hf->norm));
+  hf->tri = calloc(2*3*MAX_CIRCLE_PNT*hf->num_tube,sizeof(*hf->tri));
+
+  hf->use_shaders = True;
+}
+
+#endif /* HAVE_GLSL */
+
+
+static int get_max_base_points(void)
+{
+  int i, j, k, l, m, n, max_base_points;
+  animations *anims;
+  animation_phases *anim_ps;
+  animation_multi_obj *anim_mo;
+
+  max_base_points = 0;
+  for (i=0; i<NUM_ANIM_STATES; i++)
+  {
+    for (j=0; j<NUM_ANIM_STATES; j++)
+    {
+      anims = hopf_animations[i][j];
+      for (k=0; k<anims->num_anim; k++)
+      {
+        anim_ps = anims->anim[k];
+        for (l=0; l<anim_ps->num_phases; l++)
+        {
+          anim_mo = anim_ps->anim_mo[l];
+          n = 0;
+          for (m=0; m<anim_mo->num; m++)
+            n += anim_mo->anim_so[m].num;
+          if (n > max_base_points)
+            max_base_points = n;
+        }
+      }
+    }
+  }
+  return max_base_points;
+}
+
+
+static void init(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+
+  if (hf->details == DISP_DETAILS_COARSE)
+  {
+    hf->num_tube = NUM_TUBE_COARSE;
+    hf->base_sphere_s = BASE_SPHERE_S_COARSE;
+    hf->base_point_s = BASE_POINT_S_COARSE;
+    hf->max_circle_dist = MAX_CIRCLE_DIST_COARSE;
+  }
+  else if (hf->details == DISP_DETAILS_FINE)
+  {
+    hf->num_tube = NUM_TUBE_FINE;
+    hf->base_sphere_s = BASE_SPHERE_S_FINE;
+    hf->base_point_s = BASE_POINT_S_FINE;
+    hf->max_circle_dist = MAX_CIRCLE_DIST_FINE;
+  }
+  else /* hf->details == DISP_DETAILS_MEDIUM) */
+  {
+    hf->num_tube = NUM_TUBE_MEDIUM;
+    hf->base_sphere_s = BASE_SPHERE_S_MEDIUM;
+    hf->base_point_s = BASE_POINT_S_MEDIUM;
+    hf->max_circle_dist = MAX_CIRCLE_DIST_MEDIUM;
+  }
+
+  hf->alpha = 290.0f;
+  hf->beta = 0.0f;
+  hf->delta = 270.0f;
+
+  hf->zeta = 0.0f;
+  hf->eta = 0.0f;
+  hf->theta = 0.0f;
+
+  hf->rot_axis_base[0] = 1.0f;
+  hf->rot_axis_base[1] = 0.0f;
+  hf->rot_axis_base[2] = 0.0f;
+  hf->quat_base[0] = 1.0f;
+  hf->quat_base[1] = 0.0f;
+  hf->quat_base[2] = 0.0f;
+  hf->quat_base[3] = 0.0f;
+
+
+  hf->rot_axis_space[0] = 1.0f;
+  hf->rot_axis_space[1] = 0.0f;
+  hf->rot_axis_space[2] = 0.0f;
+  hf->quat_space[0] = 1.0f;
+  hf->quat_space[1] = 0.0f;
+  hf->quat_space[2] = 0.0f;
+  hf->quat_space[3] = 0.0f;
+
+  hf->angle_space_start = 0.0f;
+  hf->angle_space_end = 2.0f*M_PI_F;
+
+  hf->anim = NULL;
+  hf->anim_state = (int)floor(frand(NUM_ANIM_STATES));
+  hf->anim_remain_in_state = 1;
+  hf->anim_step = 0;
+  hf->anim_phases = NULL;
+  hf->anim_phase_num = 0;
+  hf->anim_phase = 0;
+  hf->anim_rotate_rnd = False;
+  hf->anim_rotate_space = False;
+  hf->anim_easing_fct_rot_rnd = EASING_NONE;
+  hf->anim_easing_fct_rot_space = EASING_NONE;
+  set_next_anim(mi);
+
+  hf->max_base_points = get_max_base_points();
+  hf->base_points = calloc(hf->max_base_points,sizeof(*hf->base_points));
+  if (hf->base_points == NULL)
+    abort();
+  hf->num_base_points = 0;
+
+  hf->sphere_base = gen_icosphere_data(&icosa,hf->base_sphere_s,
+                                       BASE_SPHERE_RADIUS);
+  hf->sphere_base_point = gen_icosphere_data(&icosa,hf->base_point_s,
+                                             BASE_POINT_RADIUS);
+  if (hf->sphere_base == NULL || hf->sphere_base_point == NULL)
+    abort();
+
+#ifdef HAVE_GLSL
+  init_glsl(mi);
+#endif /* HAVE_GLSL */
+}
+
+
+ENTRYPOINT void reshape_hopffibration(ModeInfo *mi, int width, int height)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+
+  hf->WindW = (GLint)width;
+  hf->WindH = (GLint)height;
+  hf->aspect = (GLfloat)width/(GLfloat)height;
+#ifdef HAVE_GLSL
+  delete_msaa_fbo(mi);
+  init_msaa_fbo(mi);
+  delete_shadow_fbo(mi);
+  init_shadow_fbo(mi);
+#endif /* HAVE_GLSL */
+}
+
+
+ENTRYPOINT Bool hopffibration_handle_event(ModeInfo *mi, XEvent *event)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+
+  if (event->xany.type == ButtonPress &&
+      event->xbutton.button == Button1)
+  {
+    hf->button_pressed = True;
+    gltrackball_start(hf->trackball,event->xbutton.x,event->xbutton.y,
+                      MI_WIDTH(mi),MI_HEIGHT(mi));
+    return True;
+  }
+  else if (event->xany.type == ButtonRelease &&
+           event->xbutton.button == Button1)
+  {
+    hf->button_pressed = False;
+    gltrackball_stop(hf->trackball);
+    return True;
+  }
+  else if (event->xany.type == MotionNotify && hf->button_pressed)
+  {
+    gltrackball_track(hf->trackball,event->xmotion.x,event->xmotion.y,
+                      MI_WIDTH(mi),MI_HEIGHT(mi));
+    return True;
+  }
+
+  return False;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *-----------------------------------------------------------------------------
+ *    Xlock hooks.
+ *-----------------------------------------------------------------------------
+ *-----------------------------------------------------------------------------
+ */
+
+/*
+ *-----------------------------------------------------------------------------
+ *    Initialize hopffibration.  Called each time the window changes.
+ *-----------------------------------------------------------------------------
+ */
+
+ENTRYPOINT void init_hopffibration(ModeInfo *mi)
+{
+  hopffibrationstruct *hf;
+
+  MI_INIT(mi,hopffibration);
+  hf = &hopffibration[MI_SCREEN(mi)];
+
+  hf->shadows = shadows;
+
+  hf->base_space = base_space;
+
+  hf->anti_aliasing = anti_aliasing;
+
+  /* Set the projection mode. */
+  if (!strcasecmp(proj,"perspective"))
+  {
+    hf->projection = DISP_PERSPECTIVE;
+  }
+  else if (!strcasecmp(proj,"orthographic"))
+  {
+    hf->projection = DISP_ORTHOGRAPHIC;
+  }
+  else
+  {
+    hf->projection = DISP_PERSPECTIVE;
+  }
+
+  /* Set the detail level. */
+  if (!strcasecmp(det,"coarse"))
+  {
+    hf->details = DISP_DETAILS_COARSE;
+  }
+  else if (!strcasecmp(det,"fine"))
+  {
+    hf->details = DISP_DETAILS_FINE;
+  }
+  else
+  {
+    hf->details = DISP_DETAILS_MEDIUM;
+  }
+
+  hf->trackball = gltrackball_init(False);
+  hf->button_pressed = False;
+
+  if ((hf->glx_context = init_GL(mi)) != NULL)
+  {
+    reshape_hopffibration(mi,MI_WIDTH(mi),MI_HEIGHT(mi));
+    init(mi);
+  }
+  else
+  {
+    MI_CLEARWINDOW(mi);
+  }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *    Called by the mainline code periodically to update the display.
+ *-----------------------------------------------------------------------------
+ */
+ENTRYPOINT void draw_hopffibration(ModeInfo *mi)
+{
+  Display *display = MI_DISPLAY(mi);
+  Window window = MI_WINDOW(mi);
+  hopffibrationstruct *hf;
+
+  if (hopffibration == NULL)
+    return;
+  hf = &hopffibration[MI_SCREEN(mi)];
+
+  MI_IS_DRAWN(mi) = True;
+  if (!hf->glx_context)
+    return;
+
+  glXMakeCurrent(display,window,*hf->glx_context);
+
+  display_hopffibration(mi);
+
+  if (MI_IS_FPS(mi))
+    do_fps (mi);
+
+  glFlush();
+
+  glXSwapBuffers(display,window);
+}
+
+
+#ifndef STANDALONE
+ENTRYPOINT void change_hopffibration(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+
+  if (!hf->glx_context)
+    return;
+
+  glXMakeCurrent(MI_DISPLAY(mi),MI_WINDOW(mi),*hf->glx_context);
+  init(mi);
+}
+#endif /* !STANDALONE */
+
+
+ENTRYPOINT void free_hopffibration(ModeInfo *mi)
+{
+  hopffibrationstruct *hf = &hopffibration[MI_SCREEN(mi)];
+
+  if (!hf->glx_context) return;
+  glXMakeCurrent(MI_DISPLAY(mi),MI_WINDOW(mi),*hf->glx_context);
+
+  gltrackball_free(hf->trackball);
+
+  free(hf->base_points);
+  free_icosphere_data(hf->sphere_base_point);
+  free_icosphere_data(hf->sphere_base);
+
+#ifdef HAVE_GLSL
+  if (hf->use_shaders)
+  {
+    glUseProgram(0);
+    if (hf->shader_program != 0)
+      glDeleteProgram(hf->shader_program);
+    if (hf->shadow_program != 0)
+      glDeleteProgram(hf->shadow_program);
+    glDeleteBuffers(1,&hf->base_point_pos_buffer);
+    glDeleteBuffers(1,&hf->base_point_normal_buffer);
+    glDeleteBuffers(1,&hf->base_point_indices_buffer);
+    glDeleteBuffers(1,&hf->sphere_base_pos_buffer);
+    glDeleteBuffers(1,&hf->sphere_base_normal_buffer);
+    glDeleteBuffers(1,&hf->sphere_base_indices_buffer);
+    glDeleteBuffers(hf->max_base_points,hf->fiber_pos_buffer);
+    glDeleteBuffers(hf->max_base_points,hf->fiber_normal_buffer);
+    glDeleteBuffers(hf->max_base_points,hf->fiber_indices_buffer);
+    glDeleteTextures(1,&hf->shadow_dummy_tex);
+    delete_msaa_fbo(mi);
+    delete_shadow_fbo(mi);
+    free(hf->fiber_pos_buffer);
+    free(hf->fiber_normal_buffer);
+    free(hf->fiber_indices_buffer);
+    free(hf->fiber_num_tri);
+    free(hf->vert);
+    free(hf->norm);
+    free(hf->tri);
+  }
+#endif /* HAVE_GLSL */
+}
+
+
+XSCREENSAVER_MODULE ("HopfFibration", hopffibration)
+
+#endif /* USE_GL */
diff --git a/hacks/glx/hopffibration.man b/hacks/glx/hopffibration.man
new file mode 100644 (file)
index 0000000..46d8d4c
--- /dev/null
@@ -0,0 +1,285 @@
+.TH XScreenSaver 1 "" "X Version 11"
+.SH NAME
+hopffibration \- Draws the Hopf fibration of the 4d hypersphere
+.SH SYNOPSIS
+.B hopffibration
+[\-\-display \fIhost:display.screen\fP]
+[\-\-install]
+[\-\-visual \fIvisual\fP]
+[\-\-window]
+[\-\-root]
+[\-\-window\-id \fInumber\fP]
+[\-\-delay \fIusecs\fP]
+[\-\-fps]
+[\-\-shadows]
+[\-\-details coarse | medium | fine]
+[\-\-base-space]
+[\-\-anti-aliasing]
+[\-\-perspective]
+[\-\-orthographic]
+.SH DESCRIPTION
+The \fIhopffibration\fP program shows the Hopf fibration of the 4d
+hypersphere.  The Hopf fibration is based on the Hopf map, a
+many-to-one continuous function from the 4d hypersphere (the 3-sphere)
+onto the the ordinary 3d sphere (the 2-sphere) such that each distinct
+point of the 2-sphere is mapped from a distinct great circle (a
+1-sphere) of the 3-sphere.  Hence, the inverse image of a point on the
+2-sphere corresponds to a great circle on the 3-sphere.  The 2-sphere
+is called the base space, each circle corresponding to a point on the
+2-sphere is called a fiber, and the 3-sphere is called the total
+space.
+.PP
+The program displays the base space (the 2-sphere) as a
+semi-transparent gray sphere in the bottom right corner of the
+display.  The points on the base space are displayed as small colored
+spheres.  The fibers in the total space are displayed in the same
+color as the corresponding points on the base space.
+.PP
+The fibers in the total space are projected from 4d to 3d using
+stereographic projection and then compressing the infinite 3d space to
+a finite 3d ball to display the fibers compactly.  All fibers except
+one fiber that passes through the north pole of the 3-sphere are thus
+projected to deformed circles in 3d.  The program displays these
+deformed circles as closed tubes (topological tori).  The single fiber
+that passes through the north pole of the 3-sphere is projected to an
+infinite line by the stereographic projection.  This line passes
+through infinity in 3d and therefore topologically is a circle.
+Compressing this infinite line to a finite ball maps it to a straight
+line segment.  The program displays this line segment as a cylinder.
+However, it should be thought of as a circle through infinity.
+.PP
+The fibers, base space, and base points are then projected to the
+screen either perspectively or orthographically.
+.PP
+The program displays various interesting configurations of base points
+and fibers.  Look out for the following configurations:
+.nr PI 2n
+.IP \[bu]
+Any two fibers form a Hopf link.
+.IP \[bu]
+More generally, each fiber is linked with each other fiber exactly
+once.
+.IP \[bu]
+Each circle on the 2-sphere creates a set of fibers that forms a
+Clifford torus on the 3-sphere (i.e., in 4d).  Clifford tori are flat
+(in the same sense that the surface of a cylinder is flat).
+.IP \[bu]
+If a circle on the 2-sphere is not a circle of latitude, the
+projection of the Clifford torus to 3d results in a (compressed) Dupin
+cyclide.
+.IP \[bu]
+More generally, any closed curve on the 2-sphere creates a torus-like
+surface on the 3-sphere that is flat.  These surfaces are called Hopf
+tori or Bianchi-Pinkall flat tori.  Look for the wave-like curve on
+the 2-sphere to see a Hopf torus.
+.IP \[bu]
+A circular arc on the 2-sphere creates a Hopf band on the 3-sphere.
+The Hopf band is a Seifert surface of the Hopf link that forms the
+boundaries of the Hopf band.
+.IP \[bu]
+Two or more circles of latitude on the 2-sphere create two or more
+nested Clifford tori on the 3-sphere.
+.IP \[bu]
+More generally, two or more disjoint circles on the 2-sphere create
+two or more linked Clifford tori on the 3-sphere.
+.IP \[bu]
+A great circle through the north pole of the 2-sphere creates a
+parabolic ring cyclide (which is compressed to lie within the ball in
+the 3d projection).  A parabolic ring cyclide divides the entire 3d
+space into two congruent parts that are interlocked, i.e., linked.
+.IP \[bu]
+By turning a circle on the 2-sphere so that it passes through the
+north pole of the 2-sphere, the projection of the corresponding
+Clifford torus reverses its inside and outside in 3d.
+.IP \[bu]
+The Clifford torus corresponding to a great circle on the 2-sphere
+divides the 3-sphere into two congruent solid tori that fill the
+entire 3-sphere.  The two solid tori on the 3-sphere correspond to the
+two hemispheres into which the great circle divides the 2-sphere.  The
+solid tori in the 3-sphere are attached to each other at the Clifford
+torus.  The congruence of the solid tori is visible in a particularly
+striking manner if the great circle that creates the Clifford torus is
+rotated so that it passes through the north pole of the 2-sphere,
+thereby creating a parabolic ring cyclide via the projection of the
+Clifford torus to 3d (see above).
+.PP
+During the animations, two kinds of motions are used.  Usually, the
+points on the base space are moved or rotated to particular
+configurations.  This is apparent by the small spheres that represent
+the base points changing their position on the base space, which leads
+to a corresponding change of the configuration of the fibers.  The
+base space itself, however, is not moved or rotated, i.e., its
+orientation remains fixed.  Sometimes, only the projection of the
+fibers is rotated in 3d to show some interesting configurations more
+clearly, e.g., that a Hopf torus has a hole like a regular torus.  In
+this case, the base space also maintains its orientation in space.
+Since a rotation in 3d does not change the configuration of the
+fibers, in this kind of animation, the points on the base space also
+remain fixed.  Sometimes, both types of animations are combined, e.g.,
+when the projection of one or more Clifford tori is rotated in 3d
+while the base points of the Clifford tori also rotate on the base
+space.  In this case, the base space will only show the movement of
+the base points on the base space and not the 3d rotation of the
+projection of the fibers.
+.PP
+To enhance the 3d depth impression, the program displays the shadows
+of the fibers and base points by default.  This is done by way of a
+two-pass rendering algorithm in which the geometry is rendered twice.
+Depending on the speed of the GPU, displaying shadows might slow down
+the rendering significantly.  If this is the case, the rendering of
+shadows can be switched off, saving one render pass and thus speeding
+up the rendering.
+.PP
+Some of the animations render complex geometries with a very large
+number of polygons. This can cause the rendering to become slow on
+some types of GPU.  To speed up the rendering process, the amount of
+details that are rendered can be controlled in three granularities
+(coarse, medium, and fine).  Devices with relatively small screens and
+relatively low-powered GPUs, such as phones or tablets, should
+typically select coarse details.  Standard GPUs should select medium
+details (the default).  High-powered GPUs on large screens may benefit
+from fine details.
+.PP
+By default, the base space and base points are displayed as described
+above.  If desired, the display of the base space and base points can
+be switched off so that only the fibers are displayed.
+.PP
+During the animation of the Hopf fibration, sometimes multiple fibers
+that are very close to each other are displayed.  This can create
+disturbing aliasing artifacts that are especially noticeable when the
+fibers are moving or turning slowly.  Therefore, by default, the
+rendering is performed using anti-aliasing.  This typically has a
+negligible effect on the rendering speed.  However, if shadows have
+already been switched off, coarse details have been selected, and the
+rendering is still slow, anti-aliasing also can be switched off to
+check whether it has a noticeable effect on the rendering speed.
+.PP
+This program was inspired by Niles Johnson's visualization of the Hopf
+fibration (https://nilesjohnson.net/hopf.html).
+.SH OPTIONS
+.I hopffibration
+accepts the following options:
+.TP 8
+.B \-\-window
+Draw on a newly-created window.  This is the default.
+.TP 8
+.B \-\-root
+Draw on the root window.
+.TP 8
+.B \-\-window\-id \fInumber\fP
+Draw on the specified window.
+.TP 8
+.B \-\-install
+Install a private colormap for the window.
+.TP 8
+.B \-\-visual \fIvisual\fP
+Specify which visual to use.  Legal values are the name of a visual
+class, or the id number (decimal or hex) of a specific visual.
+.TP 8
+.B \-\-delay \fImicroseconds\fP
+How much of a delay should be introduced between steps of the
+animation.  Default 20000, or 1/50th second.
+.PP
+The following options determine whether shadows are displayed.
+.TP 8
+.B \-\-shadows
+Display the fibers, base space, and base points with shadows
+(default).
+.TP 8
+.B \-\-no-shadows
+Display the fibers, base space, and base points without shadows.
+.PP
+The following three options are mutually exclusive.  They determine
+with what level of detail the fibers, base space, and base points are
+rendered.
+.TP 8
+.B \-\-details coarse
+Render the fibers, base space, and base points with a level of detail
+that is suitable for low-powered GPUs and small screens, e.g., phones
+or tablets.
+.TP 8
+.B \-\-details medium
+Render the fibers, base space, and base points with a level of detail
+that is suitable for regular GPUs (default).
+.TP 8
+.B \-\-details fine
+Render the fibers, base space, and base points with a level of detail
+that is suitable for high-powered GPUs and large screens.
+.PP
+The following options determine whether the base space and base points
+are displayed.
+.TP 8
+.B \-\-base-space
+Display the base space and base points (default).
+.TP 8
+.B \-\-no-base-space
+Do not display the base space and base points.
+.PP
+The following options determine whether anti-aliasing is used to
+display the fibers, base space, and base points.
+.TP 8
+.B \-\-anti-aliasing
+Display the fibers, base space, and base points with anti-aliasing
+(default).
+.TP 8
+.B \-\-no-anti-aliasing
+Display the fibers, base space, and base points without anti-aliasing.
+.PP
+The following two options are mutually exclusive.  They determine how
+the fibers, base space, and base points are projected from 3d to 2d
+(i.e., to the screen).
+.TP 8
+.B \-\-perspective
+Project the fibers, base space, and base points from 3d to 2d using a
+perspective projection (default).
+.TP 8
+.B \-\-orthographic
+Project the fibers, base space, and base points from 3d to 2d using a
+orthographic projection.
+.TP 8
+.B \-\-fps
+Display the current frame rate, CPU load, and polygon count.
+.SH INTERACTION
+If you run this program in standalone mode, you can rotate the fibers
+by dragging the mouse while pressing the left mouse button.
+.SH ENVIRONMENT
+.PP
+.TP 8
+.B DISPLAY
+to get the default host and display number.
+.TP 8
+.B XENVIRONMENT
+to get the name of a resource file that overrides the global resources
+stored in the RESOURCE_MANAGER property.
+.TP 8
+.B XSCREENSAVER_WINDOW
+The window ID to use with \fI\-\-root\fP.
+.SH SEE ALSO
+.BR X (1),
+.BR xscreensaver (1),
+.BR hypertorus (1)
+.SH FURTHER INFORMATION
+.nr PI 2n
+.IP \[bu]
+https://en.wikipedia.org/wiki/Hopf_fibration
+.IP \[bu]
+https://en.wikipedia.org/wiki/Hopf_link
+.IP \[bu]
+https://en.wikipedia.org/wiki/Clifford_torus
+.IP \[bu]
+https://en.wikipedia.org/wiki/Seifert_surface
+.IP \[bu]
+https://en.wikipedia.org/wiki/Dupin_cyclide
+.IP \[bu]
+https://en.wikipedia.org/wiki/3-sphere
+.SH COPYRIGHT
+Copyright \(co 2025 by Carsten Steger.  Permission to use, copy,
+modify, distribute, and sell this software and its documentation for
+any purpose is hereby granted without fee, provided that the above
+copyright notice appear in all copies and that both that copyright
+notice and this permission notice appear in supporting documentation.
+No representations are made about the suitability of this software for
+any purpose.  It is provided "as is" without express or implied
+warranty.
+.SH AUTHOR
+Carsten Steger <carsten@mirsanmir.org>, 06-feb-2025.
diff --git a/hacks/glx/klondike-game.c b/hacks/glx/klondike-game.c
new file mode 100644 (file)
index 0000000..9f968d5
--- /dev/null
@@ -0,0 +1,814 @@
+/* klondike, Copyright (c) 2024  Joshua Timmons <josh@developerx.com>
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or
+ * implied warranty.
+ */
+
+#include "xlockmore.h"
+#include <ctype.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include "gltrackball.h"
+#include "klondike-game.h"
+
+// random position offset for sloppy mode
+#define RANDOM_POSITION_OFFSET (bp->sloppy ? (((float)random()) / ((float)RAND_MAX) - 0.5) * 0.0125 : 0)
+
+// static const char *suits[] = {"Diamonds", "Clubs", "Hearts", "Spades"};
+// static const char *short_suits[] = {"D", "C", "H", "S"};
+// static const char *ranks[] = {"", "Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King"};
+// static const char *short_ranks[] = {"", "A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"};
+
+static void remove_card_from_deck(game_state_struct *, card_struct *);
+
+// initialize the deck
+void klondike_initialize_deck(klondike_configuration *bp)
+{
+    card_struct *deck = bp->game_state->deck;
+    int index = 0;
+
+    for (int suit = 0; suit < NUM_SUITS; suit++)
+    {
+        for (int rank = ACE; rank <= KING; rank++)
+        {
+
+            deck[index].suit = (Suit)suit;
+            deck[index].rank = (Rank)rank;
+            deck[index].is_face_up = 0;
+            deck[index].x = bp->deck_x;
+            deck[index].y = bp->deck_y;
+            deck[index].start_x = bp->deck_x;
+            deck[index].start_y = bp->deck_y;
+            deck[index].dest_x = bp->deck_x;
+            deck[index].dest_y = bp->deck_y;
+            deck[index].start_frame = 0;
+            deck[index].end_frame = 0;
+            deck[index].start_angle = 0.0f;
+            deck[index].end_angle = 0.0f;
+            deck[index].angle = 0.0f;
+            deck[index].z = 0.0f;
+            deck[index].start_z = 0.0f;
+            index++;
+        }
+    }
+}
+
+// shuffle the deck
+void klondike_shuffle_deck(card_struct deck[])
+{
+    for (int i = NUM_CARDS - 1; i > 0; i--)
+    {
+        int j = random() % (i + 1);
+        card_struct temp = deck[i];
+        deck[i] = deck[j];
+        deck[j] = temp;
+    }
+}
+
+// deal the cards to the tableaus
+void klondike_deal_cards(klondike_configuration *bp)
+{
+    game_state_struct *game_state = bp->game_state;
+
+    for (int i = 0; i < 7; i++)
+    {
+        game_state->tableau_size[i] = 0;
+
+        for (int j = 0; j <= i; j++)
+        {
+            game_state->tableau[i][j] = game_state->deck[0];
+            remove_card_from_deck(game_state, &game_state->tableau[i][j]);
+            if (j == i)
+            {
+                game_state->tableau[i][j].is_face_up = 1; // The top card of each pile is face up
+            }
+            game_state->tableau_size[i]++;
+        }
+    }
+
+    game_state->waste_size = 0;
+    game_state->foundation_size[CLUBS] = 0;
+    game_state->foundation_size[DIAMONDS] = 0;
+    game_state->foundation_size[HEARTS] = 0;
+    game_state->foundation_size[SPADES] = 0;
+    game_state->moves = 0;
+    game_state->moves_since_waste_flip = 0;
+}
+
+// clone the game state
+static game_state_struct *clone_game_state(game_state_struct *game_state)
+{
+    game_state_struct *newgame_state = (game_state_struct *)malloc(sizeof(game_state_struct));
+    for (int i = 0; i < NUM_CARDS; i++)
+    {
+        newgame_state->deck[i] = game_state->deck[i];
+    }
+    for (int i = 0; i < 7; i++)
+    {
+        for (int j = 0; j < 20; j++)
+        {
+            newgame_state->tableau[i][j] = game_state->tableau[i][j];
+        }
+        newgame_state->tableau_size[i] = game_state->tableau_size[i];
+    }
+    for (int i = 0; i < MAX_WASTE; i++)
+    {
+        newgame_state->waste[i] = game_state->waste[i];
+    }
+    newgame_state->waste_size = game_state->waste_size;
+    for (int i = 0; i < 4; i++)
+    {
+        for (int j = 0; j < MAX_FOUNDATION; j++)
+        {
+            newgame_state->foundation[i][j] = game_state->foundation[i][j];
+        }
+        newgame_state->foundation_size[i] = game_state->foundation_size[i];
+    }
+
+    newgame_state->moves = game_state->moves;
+    newgame_state->moves_since_waste_flip = game_state->moves_since_waste_flip;
+
+    return newgame_state;
+}
+
+// free the game state
+void klondike_free_game_state(game_state_struct *game_state)
+{
+    free(game_state);
+}
+
+// get the number of cards remaining in the deck by subtracting the sum of the tableau, foundations, and waste size from 52
+int klondike_deck_size(game_state_struct *game_state)
+{
+    // count the cards in the tableau
+    int tableauSize = 0;
+    for (int i = 0; i < 7; i++)
+    {
+        tableauSize += game_state->tableau_size[i];
+    }
+
+    // count the cards in the foundation
+    int foundation_size = 0;
+    for (int i = 0; i < 4; i++)
+    {
+        foundation_size += game_state->foundation_size[i];
+    }
+
+    // count the cards in the waste
+    int waste_size = game_state->waste_size;
+    int ret = 52 - tableauSize - foundation_size - waste_size;
+
+    return ret;
+}
+
+// reset the waste pile
+static void reset_waste(klondike_configuration *bp, game_state_struct *game_state)
+{
+    for (int i = 0; i < game_state->waste_size; i++)
+    {
+        game_state->deck[i] = game_state->waste[i];
+
+        card_struct *animated_card = &game_state->deck[i];
+        animated_card->start_frame = bp->tick + (i+5) * bp->animation_ticks / 10;
+        animated_card->end_frame = bp->tick + (i+5) * bp->animation_ticks / 10 + bp->animation_ticks;
+        animated_card->start_x = animated_card->x;
+        animated_card->start_y = animated_card->y;
+        animated_card->dest_x = bp->deck_x + RANDOM_POSITION_OFFSET;
+        animated_card->dest_y = bp->deck_y + RANDOM_POSITION_OFFSET;
+        animated_card->start_angle = 180.0f;
+        animated_card->end_angle = 360.0f;
+        animated_card->start_z = animated_card->z;
+        
+        game_state->waste[i].rank = NONE;
+        game_state->waste[i].suit = 0;
+        game_state->waste[i].is_face_up = 0;
+    }
+
+    game_state->waste_size = 0;
+    game_state->moves_since_waste_flip = 0;
+}
+
+// remove a card from the deck and move the subsequent cards up
+static void remove_card_from_deck(game_state_struct *state, card_struct *card)
+{
+    for (int i = 0; i < NUM_CARDS; i++)
+    {
+        if (state->deck[i].suit == card->suit && state->deck[i].rank == card->rank)
+        {
+            for (int j = i; j < NUM_CARDS - 1; j++)
+            {
+                state->deck[j] = state->deck[j + 1];
+            }
+
+            state->deck[NUM_CARDS - 1].rank = 0;
+            state->deck[NUM_CARDS - 1].suit = 0;
+            state->deck[NUM_CARDS - 1].is_face_up = 0;
+            break;
+        }
+    }
+}
+
+// find the card in either the tableau or the waste pile
+// and move it to the foundation
+static game_state_struct *move_card_to_foundation(klondike_configuration *bp, game_state_struct *game_state, card_struct *card)
+{
+    //  create a new game_state clone
+    game_state_struct *ret = clone_game_state(game_state);
+
+    for (int i = 0; i < 7; i++)
+    {
+        int j = ret->tableau_size[i] - 1;
+        if (ret->tableau[i][j].rank == (card->rank) && ret->tableau[i][j].suit == card->suit && card->is_face_up)
+        {
+            if (card->rank == ACE || ret->foundation[card->suit][ret->foundation_size[card->suit] - 1].rank == card->rank - 1)
+            {
+                ret->foundation[card->suit][ret->foundation_size[card->suit]] = game_state->tableau[i][j];
+                ret->tableau_size[i]--;
+                for (int k = j; k < ret->tableau_size[i]; k++)
+                {
+                    ret->tableau[i][k] = ret->tableau[i][k + 1];
+                }
+
+                ret->foundation[card->suit][ret->foundation_size[card->suit]].start_frame = bp->tick;
+                ret->foundation[card->suit][ret->foundation_size[card->suit]].end_frame = bp->tick + bp->animation_ticks;
+                ret->foundation[card->suit][ret->foundation_size[card->suit]].start_x = ret->foundation[card->suit][ret->foundation_size[card->suit]].x;
+                ret->foundation[card->suit][ret->foundation_size[card->suit]].start_y = ret->foundation[card->suit][ret->foundation_size[card->suit]].y;
+                ret->foundation[card->suit][ret->foundation_size[card->suit]].dest_x = bp->foundation_placeholders[card->suit].x;
+                ret->foundation[card->suit][ret->foundation_size[card->suit]].dest_y = bp->foundation_placeholders[card->suit].y;
+                ret->foundation[card->suit][ret->foundation_size[card->suit]].start_angle = ret->foundation[card->suit][ret->foundation_size[card->suit]].end_angle;
+                ret->foundation[card->suit][ret->foundation_size[card->suit]].start_z = ret->foundation[card->suit][ret->foundation_size[card->suit]].z + 3;
+
+                ret->foundation_size[card->suit]++;
+
+                //("Moved %s of %s from tableau to foundation\n", ranks[card->rank], suits[card->suit]);
+                return ret;
+            }
+        }
+    }
+
+    if (ret->waste[ret->waste_size - 1].rank == card->rank && ret->waste[ret->waste_size - 1].suit == card->suit && card->is_face_up)
+    {
+        if (card->rank == ACE || ret->foundation[card->suit][ret->foundation_size[card->suit] - 1].rank == card->rank - 1)
+        {
+            ret->foundation[card->suit][ret->foundation_size[card->suit]] = game_state->waste[ret->waste_size - 1];
+
+            ret->foundation[card->suit][ret->foundation_size[card->suit]].start_frame = bp->tick;
+            ret->foundation[card->suit][ret->foundation_size[card->suit]].end_frame = bp->tick + bp->animation_ticks;
+            ret->foundation[card->suit][ret->foundation_size[card->suit]].start_x = ret->foundation[card->suit][ret->foundation_size[card->suit]].x;
+            ret->foundation[card->suit][ret->foundation_size[card->suit]].start_y = ret->foundation[card->suit][ret->foundation_size[card->suit]].y;
+            ret->foundation[card->suit][ret->foundation_size[card->suit]].dest_x = bp->foundation_placeholders[card->suit].x;
+            ret->foundation[card->suit][ret->foundation_size[card->suit]].dest_y = bp->foundation_placeholders[card->suit].y;
+            ret->foundation[card->suit][ret->foundation_size[card->suit]].start_angle = ret->foundation[card->suit][ret->foundation_size[card->suit]].end_angle;
+            ret->foundation[card->suit][ret->foundation_size[card->suit]].start_z = ret->foundation[card->suit][ret->foundation_size[card->suit]].z + 3;
+
+            ret->foundation_size[card->suit]++;
+            ret->waste_size--;
+
+            return ret;
+        }
+    }
+
+    klondike_free_game_state(ret);
+    return NULL;
+}
+
+// Move the king with the fewest hidden cards on tableau pile to empty tableau
+static game_state_struct *move_king_to_empty_tableau(klondike_configuration *bp)
+{
+    game_state_struct *ret = clone_game_state(bp->game_state);
+    int minHidden = 20;
+    int minPile = -1;
+    for (int i = 0; i < 7; i++)
+    {
+        if (ret->tableau_size[i] > 0 && ret->tableau[i][ret->tableau_size[i] - 1].rank == KING)
+        {
+            int hidden = 0;
+            for (int j = 0; j < ret->tableau_size[i] - 1; j++)
+            {
+                if (!ret->tableau[i][j].is_face_up)
+                {
+                    hidden++;
+                }
+            }
+            if (hidden < minHidden && hidden > 0)
+            {
+                minHidden = hidden;
+                minPile = i;
+            }
+        }
+    }
+    if (minPile != -1)
+    {
+        for (int i = 0; i < 7; i++)
+        {
+            if (ret->tableau_size[i] == 0)
+            {
+                ret->tableau[i][0] = ret->tableau[minPile][ret->tableau_size[minPile] - 1];
+                ret->tableau_size[i]++;
+                ret->tableau_size[minPile]--;
+
+                card_struct *animated_card = &ret->tableau[i][0];
+                animated_card->start_frame = bp->tick;
+                animated_card->end_frame = bp->tick + bp->animation_ticks;
+                animated_card->start_x = animated_card->x;
+                animated_card->start_y = animated_card->y;
+                animated_card->dest_x = bp->tableau_placeholders[i].x + RANDOM_POSITION_OFFSET;
+                animated_card->dest_y = bp->tableau_placeholders[i].y + RANDOM_POSITION_OFFSET;
+                animated_card->start_angle = 180.0f;
+                animated_card->end_angle = 180.0f;
+                animated_card->start_z = animated_card->z;
+
+                return ret;
+            }
+        }
+    }
+
+    klondike_free_game_state(ret);
+    return NULL;
+}
+
+// Allow moves to a tableau if the top card on the destination tableau is the opposite color and one rank higher
+static int can_move_to_tableau(game_state_struct *game_state, card_struct *card, int toPile)
+{
+    // which pile is the card in?
+    int pile = -1;
+    for (int i = 0; i < 7 && pile == -1; i++)
+    {
+        for (int j = 0; j < game_state->tableau_size[i]; j++)
+        {
+            if (game_state->tableau[i][j].rank == card->rank && game_state->tableau[i][j].suit == card->suit)
+            {
+                pile = i;
+                break;
+            }
+        }
+    }
+
+    // number of face down cards in that pile
+    int hidden = 0;
+    if (pile != -1)
+    {
+        for (int i = 0; i < game_state->tableau_size[pile] - 1; i++)
+        {
+            if (!game_state->tableau[pile][i].is_face_up)
+            {
+                hidden++;
+            }
+        }
+    }
+
+    if (hidden > 0 && game_state->tableau_size[toPile] == 0 && card->rank == KING && card->is_face_up)
+    {
+        return 1;
+    }
+    card_struct *topCard = &game_state->tableau[toPile][game_state->tableau_size[toPile] - 1];
+    if (card->is_face_up && topCard->is_face_up && card->rank == topCard->rank - 1 && card->suit % 2 != topCard->suit % 2)
+    {
+        return 1;
+    }
+
+    return 0;
+}
+
+// TODO: Prefer moving tableaus with more hidden cards
+// Move the visible cards from one table tableau onto another tableau if the top card of the destination tableau is the 
+//opposite color and one rank higher than the bottom card of the source tableau
+static game_state_struct *move_tableau_base_card_to_tableau(klondike_configuration *bp)
+{
+    game_state_struct *game_state = bp->game_state;
+    game_state_struct *ret = clone_game_state(game_state);
+
+    for (unsigned int preferredRank = 13; preferredRank > 0; preferredRank--)
+    {
+        for (int preferredHidden = 6; preferredHidden >= 0; preferredHidden--)
+        {
+            for (int i = 0; i < 7; i++)
+            {
+                // i is the index of the source tableau
+                // proceed if the number of hidden cards in tableau[i] is equal to preferredHidden
+                if (ret->tableau_size[i] >= 0)
+                {
+                    int hidden = 0;
+                    for (int j = 0; j < ret->tableau_size[i] - 1; j++)
+                    {
+                        if (!ret->tableau[i][j].is_face_up)
+                        {
+                            hidden++;
+                        }
+                    }
+
+                    if (hidden != preferredHidden)
+                    {
+                        continue;
+                    }
+
+                    for (int j = 0; j < 7; j++)
+                    {
+                        if (i != j && ret->tableau_size[j] >= 0)
+                        {
+                            // base case is the first face up card in the tableau
+                            int baseCardIndex = -1;
+                            for (int k = 0; k < ret->tableau_size[i]; k++)
+                            {
+                                if (ret->tableau[i][k].is_face_up)
+                                {
+                                    baseCardIndex = k;
+                                    break;
+                                }
+                            }
+
+                            if (baseCardIndex > -1 && ret->tableau[i][baseCardIndex].rank == preferredRank && can_move_to_tableau(ret, &ret->tableau[i][baseCardIndex], j))
+                            {
+                                int is_face_up = 0;
+                                for (int l = 0; l < ret->tableau_size[j]; l++)
+                                {
+                                    if (ret->tableau[j][l].is_face_up)
+                                    {
+                                        is_face_up++;
+                                    }
+                                }
+
+                                for (int k = baseCardIndex; k < ret->tableau_size[i]; k++)
+                                {
+                                    ret->tableau[j][ret->tableau_size[j]] = ret->tableau[i][k];
+
+                                    card_struct *animated_card = &ret->tableau[j][ret->tableau_size[j]];
+                                    animated_card->start_frame = bp->tick;
+                                    animated_card->end_frame = bp->tick + bp->animation_ticks;
+                                    animated_card->start_x = animated_card->x;
+                                    animated_card->start_y = animated_card->y;
+                                    animated_card->dest_x = bp->tableau_placeholders[j].x + RANDOM_POSITION_OFFSET;
+                                    animated_card->dest_y = bp->tableau_placeholders[j].y - (is_face_up + k - baseCardIndex) * 0.05 + RANDOM_POSITION_OFFSET;
+                                    animated_card->start_angle = animated_card->end_angle;
+                                    animated_card->start_z = animated_card->z;
+
+                                    ret->tableau_size[j]++;
+                                }
+                                ret->tableau_size[i] = baseCardIndex;
+
+                                return ret;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    klondike_free_game_state(ret);
+    return NULL;
+}
+
+// find the next card for each foundation and see if it is one of the visible cards on one of the tableaus
+static game_state_struct *reveal_foundation_move(klondike_configuration *bp)
+{
+    game_state_struct *ret = clone_game_state(bp->game_state);
+    for (int i = 0; i < 4; i++)
+    {
+        if (ret->foundation_size[i] > 0)
+        {
+            for (int j = 0; j < 7; j++)
+            {
+                for (int k = 0; k < ret->tableau_size[j]; k++)
+                {
+                    if (ret->tableau[j][k].rank == ret->foundation[i][ret->foundation_size[i] - 1].rank + 1 && ret->tableau[j][k].is_face_up == 1 && ret->foundation[i][ret->foundation_size[i] - 1].suit == ret->tableau[j][k].suit)
+                    {
+                        // see if the the card at ret->tableau[j][k+1] can be moved to another foundation
+                        if (ret->tableau_size[j] > k + 1)
+                        {
+                            game_state_struct *ret2 = NULL;
+                            if ((ret2 = move_card_to_foundation(bp, ret, &ret->tableau[j][k + 1])))
+                            {
+                                klondike_free_game_state(ret);
+                                return ret2;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    klondike_free_game_state(ret);
+    return NULL;
+}
+
+// Move the base card of a tableau to the foundation if possible
+static game_state_struct *move_tableau_base_card_to_foundation(klondike_configuration *bp)
+{
+    game_state_struct *ret = clone_game_state(bp->game_state);
+    for (int i = 0; i < 7; i++)
+    {
+        if (ret->tableau_size[i] > 0)
+        {
+            for (int j = 0; j < 4; j++)
+            {
+                if (ret->foundation_size[j] == 0 && ret->tableau[i][ret->tableau_size[i] - 1].rank == ACE)
+                {
+                    game_state_struct *ret2;
+
+                    if ((ret2 = move_card_to_foundation(bp, ret, &ret->tableau[i][ret->tableau_size[i] - 1])))
+                    {
+                        klondike_free_game_state(ret);
+                        return ret2;
+                    }
+                }
+                else if (ret->foundation_size[j] > 0 && ret->foundation[j][ret->foundation_size[j] - 1].rank == ret->tableau[i][ret->tableau_size[i] - 1].rank - 1 && ret->foundation[j][ret->foundation_size[j] - 1].suit == ret->tableau[i][ret->tableau_size[i] - 1].suit && ret->tableau[i][ret->tableau_size[i] - 1].is_face_up)
+                {
+                    game_state_struct *ret2;
+                    if ((ret2 = move_card_to_foundation(bp, ret, &ret->tableau[i][ret->tableau_size[i] - 1])))
+                    {
+                        klondike_free_game_state(ret);
+                        return ret2;
+                    }
+                }
+            }
+        }
+    }
+
+    klondike_free_game_state(ret);
+    return NULL;
+}
+
+// Move the top card on the waste pile to one of the foundations if possible
+static game_state_struct *move_waste_to_foundation(klondike_configuration *bp)
+{
+    game_state_struct *ret = clone_game_state(bp->game_state);
+    if (ret->waste_size > 0)
+    {
+        for (unsigned int i = 0; i < 4; i++)
+        {
+            if ((ret->foundation_size[i] > 0 && ret->foundation[i][ret->foundation_size[i] - 1].rank == ret->waste[ret->waste_size - 1].rank - 1) || (ret->foundation_size[i] == 0 && ret->waste[ret->waste_size - 1].rank == ACE && ret->waste[ret->waste_size - 1].suit == i))
+            {
+                game_state_struct *ret2;
+                if ((ret2 = move_card_to_foundation(bp, ret, &ret->waste[ret->waste_size - 1])))
+                {
+                    klondike_free_game_state(ret);
+                    ret = NULL;
+                    return ret2;
+                }
+            }
+        }
+    }
+
+    klondike_free_game_state(ret);
+    return NULL;
+}
+
+// move the top card on the waste pile to one of the tableaus if possible
+static game_state_struct *move_waste_to_tableau(klondike_configuration *bp)
+{
+    game_state_struct *ret = clone_game_state(bp->game_state);
+    if (ret->waste_size > 0)
+    {
+        for (int i = 0; i < 7; i++)
+        {
+            if (ret->tableau_size[i] > 0 && ret->tableau[i][ret->tableau_size[i] - 1].rank == ret->waste[ret->waste_size - 1].rank + 1 && ret->tableau[i][ret->tableau_size[i] - 1].suit % 2 != ret->waste[ret->waste_size - 1].suit % 2)
+            {
+                ret->tableau[i][ret->tableau_size[i]] = ret->waste[ret->waste_size - 1];
+
+                int is_face_up = 0;
+                for (int k = 0; k < ret->tableau_size[i]; k++)
+                {
+                    if (ret->tableau[i][k].is_face_up)
+                    {
+                        is_face_up++;
+                    }
+                }
+
+                card_struct *animated_card = &ret->tableau[i][ret->tableau_size[i]];
+                animated_card->start_frame = bp->tick;
+                animated_card->end_frame = bp->tick + bp->animation_ticks;
+                animated_card->start_x = animated_card->x;
+                animated_card->start_y = animated_card->y;
+                animated_card->dest_x = bp->tableau_placeholders[i].x + RANDOM_POSITION_OFFSET;
+                animated_card->dest_y = bp->tableau_placeholders[i].y - (is_face_up) * 0.05 + RANDOM_POSITION_OFFSET;
+                animated_card->start_angle = 180.0f;
+                animated_card->end_angle = 180.0f;
+                animated_card->start_z = animated_card->z;
+
+                ret->tableau_size[i]++;
+                ret->waste_size--;
+                return ret;
+            }
+
+            if (ret->tableau_size[i] == 0 && ret->waste[ret->waste_size - 1].rank == KING)
+            {
+                ret->tableau[i][0] = ret->waste[ret->waste_size - 1];
+
+                int is_face_up = 0;
+
+                card_struct *animated_card = &ret->tableau[i][0];
+                animated_card->start_frame = bp->tick;
+                animated_card->end_frame = bp->tick + bp->animation_ticks;
+                animated_card->start_x = animated_card->x;
+                animated_card->start_y = animated_card->y;
+                animated_card->dest_x = bp->tableau_placeholders[i].x + RANDOM_POSITION_OFFSET;
+                animated_card->dest_y = bp->tableau_placeholders[i].y - (is_face_up) * 0.05 + RANDOM_POSITION_OFFSET;
+                animated_card->start_angle = 180.0f;
+                animated_card->end_angle = 180.0f;
+                animated_card->start_z = animated_card->z;
+
+                ret->tableau_size[i]++;
+                ret->waste_size--;
+                return ret;
+            }
+        }
+    }
+
+    klondike_free_game_state(ret);
+    return NULL;
+}
+
+// move a card from the deck to the waste pile if there is at least one card remaining in the deck
+static game_state_struct *move_deck_to_waste(klondike_configuration *bp)
+{
+    game_state_struct *ret = clone_game_state(bp->game_state);
+
+    // get the number of cards already on the board
+    int boardSize = 0;
+    for (int i = 0; i < 7; i++)
+    {
+        boardSize += ret->tableau_size[i];
+    }
+    for (int i = 0; i < 4; i++)
+    {
+        boardSize += ret->foundation_size[i];
+    }
+    boardSize += ret->waste_size;
+
+    if (boardSize == 52)
+    {
+        klondike_free_game_state(ret);
+        return NULL;
+    }
+
+    for (int i = 0; i < bp->draw_count; i++)
+    {
+        if (boardSize < 52)
+        {
+            ret->waste[ret->waste_size] = ret->deck[0];
+            ret->waste[ret->waste_size].is_face_up = 1;
+
+            card_struct *animated_card = &ret->waste[ret->waste_size];
+            animated_card->start_frame = bp->tick + bp->animation_ticks / 4 * i;
+            animated_card->end_frame = animated_card->start_frame + bp->animation_ticks;
+            animated_card->start_x = bp->deck_x;
+            animated_card->start_y = bp->deck_y;
+            animated_card->dest_x = bp->waste_x + 0.025 * ret->waste_size + RANDOM_POSITION_OFFSET;
+            animated_card->dest_y = bp->waste_y + RANDOM_POSITION_OFFSET;
+            animated_card->start_angle = 0.0f;
+            animated_card->end_angle = 180.0f;
+            animated_card->start_z = animated_card->z;
+
+            ret->waste_size++;
+
+            remove_card_from_deck(ret, &ret->deck[0]);
+        }
+
+        boardSize++;
+    }
+
+    return ret;
+}
+
+// Turn any over last tableau card that is face down
+static game_state_struct *turn_over_last_tableau_card(klondike_configuration *bp)
+{
+    game_state_struct *ret = clone_game_state(bp->game_state);
+    for (int i = 0; i < 7; i++)
+    {
+        if (ret->tableau_size[i] > 0 && ret->tableau[i][ret->tableau_size[i] - 1].is_face_up == 0)
+        {
+            ret->tableau[i][ret->tableau_size[i] - 1].is_face_up = 1;
+
+            ret->tableau[i][ret->tableau_size[i] - 1].start_frame = bp->tick;
+            ret->tableau[i][ret->tableau_size[i] - 1].end_frame = bp->tick + bp->animation_ticks;
+            ret->tableau[i][ret->tableau_size[i] - 1].start_x = ret->tableau[i][ret->tableau_size[i] - 1].x;
+            ret->tableau[i][ret->tableau_size[i] - 1].start_y = ret->tableau[i][ret->tableau_size[i] - 1].y;
+            ret->tableau[i][ret->tableau_size[i] - 1].dest_x = ret->tableau[i][ret->tableau_size[i] - 1].x;
+            ret->tableau[i][ret->tableau_size[i] - 1].dest_y = ret->tableau[i][ret->tableau_size[i] - 1].y;
+            ret->tableau[i][ret->tableau_size[i] - 1].start_angle = 0.0f;
+            ret->tableau[i][ret->tableau_size[i] - 1].end_angle = 180.0f;
+
+            return ret;
+        }
+    }
+
+    klondike_free_game_state(ret);
+    return NULL;
+}
+
+// This function should return a new game state that is the result of making the next move in the game
+// The function should not modify the original game state
+// The function should return NULL if there are no possible moves
+static game_state_struct *next_move_inner(klondike_configuration *bp)
+{
+    game_state_struct *ret = NULL;
+
+    if ((ret = turn_over_last_tableau_card(bp)))
+    {
+        ret->moves_since_waste_flip++;
+        return ret;
+    }
+
+    if ((ret = move_tableau_base_card_to_foundation(bp)))
+    {
+        ret->moves_since_waste_flip++;
+        return ret;
+    }
+
+    if ((ret = move_king_to_empty_tableau(bp)))
+    {
+        ret->moves_since_waste_flip++;
+        return ret;
+    }
+
+    if ((ret = move_tableau_base_card_to_tableau(bp)))
+    {
+        ret->moves_since_waste_flip++;
+        return ret;
+    }
+
+    if ((ret = reveal_foundation_move(bp)))
+    {
+        ret->moves_since_waste_flip++;
+        return ret;
+    }
+
+    if ((ret = move_waste_to_foundation(bp)))
+    {
+        ret->moves_since_waste_flip++;
+        return ret;
+    }
+
+    if ((ret = move_waste_to_tableau(bp)))
+    {
+        ret->moves_since_waste_flip++;
+        return ret;
+    }
+
+    if ((ret = move_deck_to_waste(bp)))
+    {
+        return ret;
+    }
+
+    if (bp->game_state->moves_since_waste_flip > 0)
+    {
+        ret = clone_game_state(bp->game_state);
+        reset_waste(bp, ret);
+        return ret;
+    }
+
+    return NULL;
+}
+
+game_state_struct *klondike_next_move(klondike_configuration *bp)
+{
+    game_state_struct *ret = NULL;
+    if ((ret = next_move_inner(bp)))
+    {
+        ret->moves++;
+
+        // zero the cards in the foundations arrays past the foundation length
+        for (int i = 0; i < 4; i++)
+        {
+            for (int j = ret->foundation_size[i]; j < 13; j++)
+            {
+                ret->foundation[i][j].rank = NONE;
+                ret->foundation[i][j].suit = 0;
+                ret->foundation[i][j].is_face_up = 0;
+            }
+        }
+
+        // zero the cards in the tableau arrays past the tableau length
+        for (int i = 0; i < 7; i++)
+        {
+            for (int j = ret->tableau_size[i]; j < 13; j++)
+            {
+                ret->tableau[i][j].rank = 0;
+                ret->tableau[i][j].suit = 0;
+                ret->tableau[i][j].is_face_up = 0;
+            }
+        }
+
+        // zero the cards in the waste array past the waste length
+        for (int i = ret->waste_size; i < 24; i++)
+        {
+            ret->waste[i].rank = 0;
+            ret->waste[i].suit = 0;
+            ret->waste[i].is_face_up = 0;
+        }
+
+        return ret;
+    }
+
+    return NULL;
+}
diff --git a/hacks/glx/klondike-game.h b/hacks/glx/klondike-game.h
new file mode 100644 (file)
index 0000000..f31dce5
--- /dev/null
@@ -0,0 +1,107 @@
+/* klondike, Copyright (c) 2024  Joshua Timmons <josh@developerx.com>
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or
+ * implied warranty.
+ */
+
+#ifndef __KLONDIKE_GAME_H__
+#define __KLONDIKE_GAME_H__
+
+#include "gltrackball.h"
+
+#define NUM_SUITS 4
+#define NUM_RANKS 13
+#define NUM_CARDS 52
+#define MAX_WASTE 24
+#define MAX_FOUNDATION 13
+#define MAX_TABLEAU 20
+#define MAX_TEXTURE 53
+#define BACK_TEXTURE 52
+
+typedef enum { DIAMONDS, CLUBS, HEARTS, SPADES } Suit;
+typedef enum { NONE=0, ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING } Rank;
+
+typedef struct {
+    Suit suit;
+    Rank rank;
+    int is_face_up; // 0 for face down, 1 for face up
+
+    float x;
+    float y;
+    float z;
+    float start_x;
+    float start_y;
+    float dest_x;
+    float dest_y;
+    float start_frame;
+    float end_frame;
+    float angle;
+    float start_angle;
+    float end_angle;
+    float start_z;    
+} card_struct;
+
+typedef struct {
+    card_struct deck[NUM_CARDS];
+    card_struct tableau[7][MAX_TABLEAU];
+    int tableau_size[7];
+    card_struct waste[MAX_WASTE];
+    int waste_size;
+    card_struct foundation[4][MAX_FOUNDATION];
+    int foundation_size[4];
+
+    int moves;
+    int moves_since_waste_flip;
+
+    // todo: the tableaus do not track the number of face up cards. This is a bug.
+} game_state_struct;
+
+typedef struct {
+  GLXContext *glx_context;
+  card_struct foundation_placeholders[4];
+  card_struct tableau_placeholders[7];
+  float waste_x;
+  float waste_y;
+  float deck_x;
+  float deck_y;
+
+  float scale;
+
+  int tick;
+  int universe_tick;
+  float camera_phase;
+
+  int final_animation;
+  int redeal;
+    
+  game_state_struct *game_state;
+
+  GLuint fronts[52];
+  GLuint back;
+
+  trackball_state *trackball;
+  Bool button_down_p;
+
+  // Preferences
+  GLuint animation_ticks;
+  int draw_count;
+  int camera_speed;
+  Bool sloppy;
+
+} klondike_configuration;
+
+
+void klondike_initialize_deck(klondike_configuration *bp);
+void klondike_shuffle_deck(card_struct deck[]);
+void klondike_deal_cards(klondike_configuration *bp);
+void klondike_free_game_state(game_state_struct *game_state);
+int klondike_deck_size(game_state_struct *game_state);
+void klondike_reset_board(klondike_configuration *bp);
+game_state_struct *klondike_next_move(klondike_configuration *bp);
+
+#endif /* __KLONDIKE_GAME_H__ */
diff --git a/hacks/glx/klondike.c b/hacks/glx/klondike.c
new file mode 100644 (file)
index 0000000..15677d3
--- /dev/null
@@ -0,0 +1,809 @@
+/* klondike, Copyright (c) 2024 Joshua Timmons <josh@developerx.com>\r
+ *\r
+ * Permission to use, copy, modify, distribute, and sell this software and its\r
+ * documentation for any purpose is hereby granted without fee, provided that\r
+ * the above copyright notice appear in all copies and that both that\r
+ * copyright notice and this permission notice appear in supporting\r
+ * documentation.  No representations are made about the suitability of this\r
+ * software for any purpose.  It is provided "as is" without express or\r
+ * implied warranty.\r
+ */\r
+\r
+#define DEFAULTS       "*delay:        30000       \n" \\r
+                       \r
+#define release_klondike 0\r
+\r
+#include "xlockmore.h"\r
+#include <ctype.h>\r
+#include "gltrackball.h"\r
+#include "klondike-game.h"\r
+#include "ximage-loader.h"\r
+#define I_HAVE_XPM\r
+#include "../images/gen/back_png.h"\r
+#include "../images/gen/CA_png.h"\r
+#include "../images/gen/C2_png.h"\r
+#include "../images/gen/C3_png.h"\r
+#include "../images/gen/C4_png.h"\r
+#include "../images/gen/C5_png.h"\r
+#include "../images/gen/C6_png.h"\r
+#include "../images/gen/C7_png.h"\r
+#include "../images/gen/C8_png.h"\r
+#include "../images/gen/C9_png.h"\r
+#include "../images/gen/CT_png.h"\r
+#include "../images/gen/CJ_png.h"\r
+#include "../images/gen/CQ_png.h"\r
+#include "../images/gen/CK_png.h"\r
+\r
+#include "../images/gen/DA_png.h"\r
+#include "../images/gen/D2_png.h"\r
+#include "../images/gen/D3_png.h"\r
+#include "../images/gen/D4_png.h"\r
+#include "../images/gen/D5_png.h"\r
+#include "../images/gen/D6_png.h"\r
+#include "../images/gen/D7_png.h"\r
+#include "../images/gen/D8_png.h"\r
+#include "../images/gen/D9_png.h"\r
+#include "../images/gen/DT_png.h"\r
+#include "../images/gen/DJ_png.h"\r
+#include "../images/gen/DQ_png.h"\r
+#include "../images/gen/DK_png.h"\r
+\r
+#include "../images/gen/SA_png.h"\r
+#include "../images/gen/S2_png.h"\r
+#include "../images/gen/S3_png.h"\r
+#include "../images/gen/S4_png.h"\r
+#include "../images/gen/S5_png.h"\r
+#include "../images/gen/S6_png.h"\r
+#include "../images/gen/S7_png.h"\r
+#include "../images/gen/S8_png.h"\r
+#include "../images/gen/S9_png.h"\r
+#include "../images/gen/ST_png.h"\r
+#include "../images/gen/SJ_png.h"\r
+#include "../images/gen/SQ_png.h"\r
+#include "../images/gen/SK_png.h"\r
+\r
+#include "../images/gen/HA_png.h"\r
+#include "../images/gen/H2_png.h"\r
+#include "../images/gen/H3_png.h"\r
+#include "../images/gen/H4_png.h"\r
+#include "../images/gen/H5_png.h"\r
+#include "../images/gen/H6_png.h"\r
+#include "../images/gen/H7_png.h"\r
+#include "../images/gen/H8_png.h"\r
+#include "../images/gen/H9_png.h"\r
+#include "../images/gen/HT_png.h"\r
+#include "../images/gen/HJ_png.h"\r
+#include "../images/gen/HQ_png.h"\r
+#include "../images/gen/HK_png.h"\r
+\r
+#ifdef USE_GL /* whole file */\r
+\r
+#define DEF_CAMERA_SPEED "50"\r
+#define DEF_SPEED "60"\r
+#define DEF_DRAW_COUNT "3"\r
+#define DEF_SLOPPY "True"\r
+\r
+// global variables\r
+static klondike_configuration *bps = NULL;\r
+\r
+static GLuint animation_ticks;\r
+static int draw_count;\r
+static int camera_speed;\r
+static Bool sloppy;\r
+\r
+// options\r
+static XrmOptionDescRec opts[] = {\r
+ {"-speed", ".speed", XrmoptionSepArg, 0},\r
+ {"-camera_speed", ".cameraSpeed", XrmoptionSepArg, 0},\r
+ {"-sloppy", ".sloppy", XrmoptionNoArg, "True"},\r
+ {"+sloppy", ".sloppy", XrmoptionNoArg, "False"},\r
+ {"-draw", ".drawCount", XrmoptionSepArg, 0}};\r
+\r
+// variables for the options\r
+static argtype vars[] = {\r
+ {&sloppy, "sloppy", "Sloppy", DEF_SLOPPY, t_Bool},\r
+ {&animation_ticks, "speed", "Speed", DEF_SPEED, t_Int},\r
+ {&draw_count, "drawCount", "DrawCount", DEF_DRAW_COUNT, t_Int},\r
+ {&camera_speed, "cameraSpeed", "CameraSpeed", DEF_CAMERA_SPEED, t_Int}};\r
+\r
+ENTRYPOINT ModeSpecOpt klondike_opts = {countof(opts), opts, countof(vars), vars, NULL};\r
+\r
+// Local function prototypes\r
+static void initialize_placeholders(klondike_configuration *bp, int width, int height);\r
+static double ease_in_out_quart(double x);\r
+static double ease_out_quart(double x);\r
+static int compare_cards(const void *a, const void *b);\r
+static void animate_board_to_deck(klondike_configuration *bp);\r
+static void animate_initial_board(klondike_configuration *bp);\r
+\r
+// Function to load a texture\r
+// Function to load a texture\r
+static GLuint load_texture(ModeInfo *mi, const char *name,\r
+                        const unsigned char *buffer, unsigned length)\r
+{\r
+ GLuint texture_id;\r
+ XImage *image = image_data_to_ximage(MI_DISPLAY(mi), MI_VISUAL(mi),\r
+                                      buffer, length);\r
+\r
+ glGenTextures(1, &texture_id);\r
+ glBindTexture(GL_TEXTURE_2D, texture_id);\r
+\r
+ // Set texture parameters\r
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\r
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\r
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\r
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\r
+\r
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\r
+ /* glPixelStorei(GL_UNPACK_ROW_LENGTH, image->width); */\r
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,\r
+              image->width, image->height, 0,\r
+              GL_RGBA, GL_UNSIGNED_BYTE, image->data);\r
+ check_gl_error("load texture");\r
+ check_gl_error("mipmap");\r
+ XDestroyImage(image);\r
+ return texture_id;\r
+}\r
+\r
+/* Window management, etc\r
+*/\r
+ENTRYPOINT void\r
+reshape_klondike(ModeInfo *mi, int width, int height)\r
+{\r
+ int y = 0;\r
+\r
+ if (width > height * 5)\r
+ { /* tiny window: show middle */\r
+     height = width * 9 / 16;\r
+     y = -height / 2;\r
+ }\r
+\r
+ glViewport(0, y, (GLint)width, (GLint)height);\r
+\r
+ // Enable blending\r
+ glEnable(GL_BLEND);\r
+\r
+ // Set the blending function\r
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\r
+\r
+ // Enable multisampling\r
+ glEnable(GL_MULTISAMPLE);\r
+\r
+ glEnable(GL_DEPTH_TEST);\r
+ glDepthFunc(GL_LESS);\r
+\r
+ {\r
+     GLfloat s = (MI_WIDTH(mi) < MI_HEIGHT(mi)\r
+                      ? (MI_WIDTH(mi) / (GLfloat)MI_HEIGHT(mi))\r
+                      : 1);\r
+     glScalef(s, s, s);\r
+ }\r
+\r
+ initialize_placeholders(&bps[MI_SCREEN(mi)], width, height);\r
+\r
+ glClear(GL_COLOR_BUFFER_BIT);\r
+}\r
+\r
+// initialize the placeholders for the foundation and tableau\r
+static void initialize_placeholders(klondike_configuration *bp, int width, int height)\r
+{\r
+ float xscale = width > height ? 0.53 * height / width : 0.53;\r
+\r
+ if (width < height)\r
+ {\r
+     xscale *= width / 1280.0f;\r
+ }\r
+\r
+ for (int i = 0; i < 4; i++)\r
+ {\r
+     bp->foundation_placeholders[i].x = 0.15 + -0.4f + (0.075 + 0.15 * i * xscale / 0.3f) * bp->scale;\r
+     bp->foundation_placeholders[i].y = 0.7f * bp->scale;\r
+ }\r
+\r
+ for (int i = 0; i < 7; i++)\r
+ {\r
+     bp->tableau_placeholders[i].x = 0.15 + -0.55f + 0.15 * i * xscale / 0.3f * bp->scale;\r
+     bp->tableau_placeholders[i].y = 0.3f * bp->scale;\r
+ }\r
+\r
+ bp->waste_x = bp->tableau_placeholders[0].x;\r
+ bp->waste_y = -0.65f * bp->scale;\r
+ bp->deck_x = bp->tableau_placeholders[6].x;\r
+ bp->deck_y = -0.65f * bp->scale;\r
+}\r
+\r
+// animate the initial board\r
+static void animate_initial_board(klondike_configuration *bp)\r
+{\r
+ int n = 0;\r
+ for (int i = 0; i < 7; i++)\r
+ {\r
+     for (int j = 0; j < 7; j++)\r
+     {\r
+         if (i < bp->game_state->tableau_size[j])\r
+         {\r
+             card_struct *card = &bp->game_state->tableau[j][i];\r
+             card->start_frame = 10 + n * animation_ticks / 4;\r
+             card->end_frame = card->start_frame + animation_ticks;\r
+             card->start_x = bp->deck_x;\r
+             card->start_y = bp->deck_y;\r
+             card->dest_x = bp->tableau_placeholders[j].x;\r
+             card->dest_y = bp->tableau_placeholders[j].y;\r
+             card->angle = 0.0f;\r
+             card->start_angle = 0.0f;\r
+             card->is_face_up = i == bp->game_state->tableau_size[j] - 1;\r
+             card->end_angle = card->is_face_up ? 180.0f : 0.0f;\r
+\r
+             n++;\r
+         }\r
+     }\r
+ }\r
+}\r
+\r
+ENTRYPOINT Bool\r
+klondike_handle_event(ModeInfo *mi, XEvent *event)\r
+{\r
+  klondike_configuration *bp = &bps[MI_SCREEN(mi)];\r
+  if (gltrackball_event_handler (event, bp->trackball,\r
+                                MI_WIDTH (mi), MI_HEIGHT (mi),\r
+                                &bp->button_down_p))\r
+    return True;\r
\r
+  return False;\r
+}\r
+\r
+ENTRYPOINT void\r
+init_klondike(ModeInfo *mi)\r
+{\r
+ klondike_configuration *bp;\r
+ int wire = MI_IS_WIREFRAME(mi);\r
+\r
+ MI_INIT(mi, bps);\r
+ bp = &bps[MI_SCREEN(mi)];\r
+\r
+ bp->glx_context = init_GL(mi);\r
+\r
+ reshape_klondike(mi, MI_WIDTH(mi), MI_HEIGHT(mi));\r
+\r
+ if (!wire)\r
+ {\r
+     GLfloat pos[4] = {0.0, 0.0, 1.0, 0.0};  // Changed light position to be in front\r
+     GLfloat amb[4] = {0.8, 0.8, 0.8, 1.0};  // Increased ambient light\r
+     GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0};\r
+     GLfloat spc[4] = {0.0, 0.0, 0.0, 1.0};  // Removed specular highlight\r
+\r
+     // enable lighting, depth testing, normaliztion, culling, texture mapping, blending\r
+     glEnable(GL_LIGHTING);\r
+     glEnable(GL_LIGHT0);\r
+     glEnable(GL_DEPTH_TEST);\r
+     glEnable(GL_NORMALIZE);\r
+     glEnable(GL_CULL_FACE);\r
+     glEnable(GL_TEXTURE_2D);\r
+     glEnable(GL_BLEND);\r
+     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\r
+\r
+     // Set up material properties\r
+     GLfloat mat_ambient[] = {1.0, 1.0, 1.0, 1.0};\r
+     GLfloat mat_diffuse[] = {1.0, 1.0, 1.0, 1.0};\r
+     GLfloat mat_specular[] = {0.0, 0.0, 0.0, 1.0};\r
+     GLfloat mat_shininess[] = {0.0};\r
+     \r
+     // set the material properties\r
+     glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, mat_ambient);\r
+     glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, mat_diffuse);\r
+     glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, mat_specular);\r
+     glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, mat_shininess);\r
+\r
+     // set the light position\r
+     glLightfv(GL_LIGHT0, GL_POSITION, pos);\r
+     glLightfv(GL_LIGHT0, GL_AMBIENT, amb);\r
+     glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);\r
+     glLightfv(GL_LIGHT0, GL_SPECULAR, spc);\r
+\r
+     // Load textures\r
+     unsigned int n = 0;      \r
+     \r
+     bp->fronts[n++] = load_texture(mi, "DA", DA_png, sizeof(DA_png));\r
+     bp->fronts[n++] = load_texture(mi, "D2", D2_png, sizeof(D2_png));\r
+     bp->fronts[n++] = load_texture(mi, "D3", D3_png, sizeof(D3_png));\r
+     bp->fronts[n++] = load_texture(mi, "D4", D4_png, sizeof(D4_png));\r
+     bp->fronts[n++] = load_texture(mi, "D5", D5_png, sizeof(D5_png));\r
+     bp->fronts[n++] = load_texture(mi, "D6", D6_png, sizeof(D6_png));\r
+     bp->fronts[n++] = load_texture(mi, "D7", D7_png, sizeof(D7_png));\r
+     bp->fronts[n++] = load_texture(mi, "D8", D8_png, sizeof(D8_png));\r
+     bp->fronts[n++] = load_texture(mi, "D9", D9_png, sizeof(D9_png));\r
+     bp->fronts[n++] = load_texture(mi, "DT", DT_png, sizeof(DT_png));\r
+     bp->fronts[n++] = load_texture(mi, "DJ", DJ_png, sizeof(DJ_png));\r
+     bp->fronts[n++] = load_texture(mi, "DQ", DQ_png, sizeof(DQ_png));\r
+     bp->fronts[n++] = load_texture(mi, "DK", DK_png, sizeof(DK_png));\r
+     \r
+     bp->fronts[n++] = load_texture(mi, "CA", CA_png, sizeof(CA_png));\r
+     bp->fronts[n++] = load_texture(mi, "C2", C2_png, sizeof(C2_png));\r
+     bp->fronts[n++] = load_texture(mi, "C3", C3_png, sizeof(C3_png));\r
+     bp->fronts[n++] = load_texture(mi, "C4", C4_png, sizeof(C4_png));\r
+     bp->fronts[n++] = load_texture(mi, "C5", C5_png, sizeof(C5_png));\r
+     bp->fronts[n++] = load_texture(mi, "C6", C6_png, sizeof(C6_png));\r
+     bp->fronts[n++] = load_texture(mi, "C7", C7_png, sizeof(C7_png));\r
+     bp->fronts[n++] = load_texture(mi, "C8", C8_png, sizeof(C8_png));\r
+     bp->fronts[n++] = load_texture(mi, "C9", C9_png, sizeof(C9_png));\r
+     bp->fronts[n++] = load_texture(mi, "CT", CT_png, sizeof(CT_png));\r
+     bp->fronts[n++] = load_texture(mi, "CJ", CJ_png, sizeof(CJ_png));\r
+     bp->fronts[n++] = load_texture(mi, "CQ", CQ_png, sizeof(CQ_png));\r
+     bp->fronts[n++] = load_texture(mi, "CK", CK_png, sizeof(CK_png));\r
+         \r
+     bp->fronts[n++] = load_texture(mi, "HA", HA_png, sizeof(HA_png));\r
+     bp->fronts[n++] = load_texture(mi, "H2", H2_png, sizeof(H2_png));\r
+     bp->fronts[n++] = load_texture(mi, "H3", H3_png, sizeof(H3_png));\r
+     bp->fronts[n++] = load_texture(mi, "H4", H4_png, sizeof(H4_png));\r
+     bp->fronts[n++] = load_texture(mi, "H5", H5_png, sizeof(H5_png));\r
+     bp->fronts[n++] = load_texture(mi, "H6", H6_png, sizeof(H6_png));\r
+     bp->fronts[n++] = load_texture(mi, "H7", H7_png, sizeof(H7_png));\r
+     bp->fronts[n++] = load_texture(mi, "H8", H8_png, sizeof(H8_png));\r
+     bp->fronts[n++] = load_texture(mi, "H9", H9_png, sizeof(H9_png));\r
+     bp->fronts[n++] = load_texture(mi, "HT", HT_png, sizeof(HT_png));\r
+     bp->fronts[n++] = load_texture(mi, "HJ", HJ_png, sizeof(HJ_png));\r
+     bp->fronts[n++] = load_texture(mi, "HQ", HQ_png, sizeof(HQ_png));\r
+     bp->fronts[n++] = load_texture(mi, "HK", HK_png, sizeof(HK_png));\r
+\r
+     bp->fronts[n++] = load_texture(mi, "SA", SA_png, sizeof(SA_png));\r
+     bp->fronts[n++] = load_texture(mi, "S2", S2_png, sizeof(S2_png));\r
+     bp->fronts[n++] = load_texture(mi, "S3", S3_png, sizeof(S3_png));\r
+     bp->fronts[n++] = load_texture(mi, "S4", S4_png, sizeof(S4_png));\r
+     bp->fronts[n++] = load_texture(mi, "S5", S5_png, sizeof(S5_png));\r
+     bp->fronts[n++] = load_texture(mi, "S6", S6_png, sizeof(S6_png));\r
+     bp->fronts[n++] = load_texture(mi, "S7", S7_png, sizeof(S7_png));\r
+     bp->fronts[n++] = load_texture(mi, "S8", S8_png, sizeof(S8_png));\r
+     bp->fronts[n++] = load_texture(mi, "S9", S9_png, sizeof(S9_png));\r
+     bp->fronts[n++] = load_texture(mi, "ST", ST_png, sizeof(ST_png));\r
+     bp->fronts[n++] = load_texture(mi, "SJ", SJ_png, sizeof(SJ_png));\r
+     bp->fronts[n++] = load_texture(mi, "SQ", SQ_png, sizeof(SQ_png));\r
+     bp->fronts[n++] = load_texture(mi, "SK", SK_png, sizeof(SK_png));\r
+\r
+     bp->back = load_texture(mi, "back", back_png, sizeof(back_png));                \r
+ }\r
+\r
+ bp->scale = 1.1f;\r
+\r
+ initialize_placeholders(bp, MI_WIDTH(mi), MI_HEIGHT(mi));\r
+\r
+ bp->trackball = gltrackball_init (True);\r
+\r
+ // initialize the game state\r
+ bp->game_state = (game_state_struct *)malloc(sizeof(game_state_struct));\r
+ bp->tick = 0;\r
+ bp->universe_tick = 0;\r
+\r
+ bp->camera_phase = (random() / (float)RAND_MAX) * 0.2 * M_PI;\r
+\r
+ bp->redeal = 0;\r
+ bp->final_animation = 0;\r
+\r
+ // set the draw count to 3 if it is not 1 or 3\r
+ if (draw_count != 1 && draw_count != 3)\r
+ {\r
+     draw_count = 3;\r
+ }\r
+\r
+ bp->animation_ticks = animation_ticks;\r
+ bp->draw_count = draw_count;\r
+ bp->camera_speed = camera_speed;\r
+ bp->sloppy = sloppy;\r
+}\r
+\r
+// ease in out quartic\r
+static double ease_in_out_quart(double x)\r
+{\r
+ if (x < 0.5)\r
+ {\r
+     return 8 * x * x * x * x;\r
+ }\r
+ else\r
+ {\r
+     return 1 - pow(-2 * x + 2, 4) / 2;\r
+ }\r
+}\r
+\r
+// ease out quartic\r
+static double ease_out_quart(double x)\r
+{\r
+ return 1 - pow(1 - x, 4);\r
+}\r
+\r
+// sort cards by z position then by end_frame\r
+static int compare_cards(const void *a, const void *b)\r
+{\r
+ card_struct **ca = (card_struct **)a;\r
+ card_struct **cb = (card_struct **)b;\r
+\r
+ // sort by z position then by end_frame\r
+ if ((*ca)->z != (*cb)->z)\r
+ {\r
+     return ((*ca)->z > (*cb)->z) ? 1 : -1;\r
+ }\r
+\r
+ // sort by end_frame\r
+ return (*ca)->end_frame - (*cb)->end_frame;\r
+}\r
+\r
+// collect the cards from the board back to the deck to be redealt\r
+static void animate_board_to_deck(klondike_configuration *bp)\r
+{\r
+ int n = 0;\r
+ for (int i = 0; i < 7; i++)\r
+ {\r
+     for (int j = 0; j < bp->game_state->tableau_size[i]; j++)\r
+     {\r
+         card_struct *card = &bp->game_state->tableau[i][j];\r
+         card->start_frame = bp->tick + n * animation_ticks / 12;\r
+         card->end_frame = card->start_frame + animation_ticks;\r
+         card->start_x = card->x;\r
+         card->start_y = card->y;\r
+         card->dest_x = bp->deck_x;\r
+         card->dest_y = bp->deck_y;\r
+         card->start_angle = card->angle;\r
+         card->end_angle = 360.0f;\r
+         card->is_face_up = 0;\r
+         n++;\r
+     }\r
+ }\r
+\r
+ for (int i = 0; i < 4; i++)\r
+ {\r
+     for (int j = 0; j < bp->game_state->foundation_size[i]; j++)\r
+     {\r
+         card_struct *card = &bp->game_state->foundation[i][j];\r
+         card->start_frame = bp->tick + n * animation_ticks / 12;\r
+         card->end_frame = card->start_frame + animation_ticks;\r
+         card->start_x = card->x;\r
+         card->start_y = card->y;\r
+         card->dest_x = bp->deck_x;\r
+         card->dest_y = bp->deck_y;\r
+         card->start_angle = card->angle;\r
+         card->end_angle = 360.0f;\r
+         card->is_face_up = 0;\r
+         n++;\r
+     }\r
+ }\r
+\r
+ for (int i = 0; i < bp->game_state->waste_size; i++)\r
+ {\r
+     card_struct *card = &bp->game_state->waste[i];\r
+     card->start_frame = bp->tick + n * animation_ticks / 12;\r
+     card->end_frame = card->start_frame + animation_ticks;\r
+     card->start_x = card->x;\r
+     card->start_y = card->y;\r
+     card->dest_x = bp->deck_x;\r
+     card->dest_y = bp->deck_y;\r
+     card->start_angle = card->angle;\r
+     card->end_angle = 360.0f;\r
+     card->is_face_up = 0;\r
+     n++;\r
+ }\r
+\r
+ bp->final_animation = bp->tick + n * animation_ticks / 12 + animation_ticks;\r
+}\r
+\r
+ENTRYPOINT void\r
+draw_klondike(ModeInfo *mi)\r
+{\r
+ klondike_configuration *bp = &bps[MI_SCREEN(mi)];\r
+ Display *dpy = MI_DISPLAY(mi);\r
+ Window window = MI_WINDOW(mi);\r
+ card_struct *renderCards[52];\r
+\r
+ if (!bp->glx_context)\r
+ {\r
+     fprintf(stderr, "%s: no graphics context\n", progname);\r
+     return;\r
+ }\r
+\r
+ glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);\r
+\r
+ glShadeModel(GL_SMOOTH);\r
+ glEnable(GL_DEPTH_TEST);\r
+ glEnable(GL_NORMALIZE);\r
+ glEnable(GL_CULL_FACE);\r
+ glEnable(GL_TEXTURE_2D);\r
+ glEnable(GL_BLEND);\r
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\r
+ glEnable(GL_LIGHTING);\r
+ glEnable(GL_LIGHT0);\r
+\r
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\r
+\r
+ int width = MI_WIDTH(mi);\r
+ int height = MI_HEIGHT(mi);\r
+\r
+ glScalef(1.1, 1.1, 1.1);\r
+\r
+ float offset_x = 0.0f;\r
+ float offset_y = 0.0f;\r
+ float scale = 1.0f;\r
+\r
+ if (bp->tick == 0)\r
+ {\r
+     initialize_placeholders(bp, MI_WIDTH(mi), MI_HEIGHT(mi));\r
+\r
+     klondike_initialize_deck(bp);\r
+     klondike_shuffle_deck(bp->game_state->deck);\r
+\r
+     klondike_deal_cards(bp);\r
+     animate_initial_board(bp);\r
+ }\r
+\r
+ bp->tick++;\r
+ bp->universe_tick++;\r
+ int animatedCardCount = 0;\r
+\r
+ int last_animation = 0;\r
+\r
+ // render the tableaus\r
+ for (int i = 0; i < 7; i++)\r
+ {\r
+     for (int j = 0; j < bp->game_state->tableau_size[i]; j++)\r
+     {\r
+         card_struct *card = &(bp->game_state->tableau[i][j]);\r
+         renderCards[animatedCardCount++] = card;\r
+     }\r
+ }\r
+\r
+ // render the foundations\r
+ for (int i = 0; i < 4; i++)\r
+ {\r
+     for (int j = 0; j < bp->game_state->foundation_size[i]; j++)\r
+     {\r
+         card_struct *card = &(bp->game_state->foundation[i][j]);\r
+         renderCards[animatedCardCount++] = card;\r
+\r
+         card->z = j;\r
+     }\r
+ }\r
+\r
+ // render the waste pile\r
+ for (int j = 0; j < bp->game_state->waste_size; j++)\r
+ {\r
+     card_struct *card = &(bp->game_state->waste[j]);\r
+     card->z = j;\r
+     renderCards[animatedCardCount++] = card;\r
+ }\r
+\r
+ // render the deck\r
+ int ds = klondike_deck_size(bp->game_state);\r
+ for (int j = ds - 1; j >= 0; j--)\r
+ {\r
+     card_struct *card = &(bp->game_state->deck[j]);\r
+     // Only set z position, don't modify x,y here\r
+     card->z = ds - 1 - j;\r
+     renderCards[animatedCardCount + j] = card;\r
+ }\r
+\r
+ animatedCardCount += ds;\r
+\r
+ for (int i = 0; i < animatedCardCount; i++)\r
+ {\r
+     card_struct *card = renderCards[i];\r
+\r
+     card->z = i / 10.0f;\r
+\r
+     if (bp->tick >= card->start_frame && bp->tick < card->end_frame)\r
+     {\r
+         float n = ((float)bp->tick - (float)card->start_frame) / (card->end_frame - card->start_frame);\r
+         float eased2 = ease_in_out_quart(n);\r
+         float eased_card_z = card->start_z * (1.0f - eased2);\r
+         card->z += eased_card_z;\r
+         card->z += 8 * sin(n * M_PI);\r
+     }\r
+     else {\r
+         card->start_z = 0;\r
+     }\r
\r
+ }\r
+\r
+\r
+\r
+ // qsort the rendercards by animation order\r
+ qsort(renderCards, animatedCardCount, sizeof(card_struct *), compare_cards);\r
+\r
+ // camera\r
\r
+ float speed_factor = camera_speed / 100.0f;\r
+ float camera_theta = M_PI / 2 + (sin((bp->universe_tick + bp->camera_phase) * 0.0065 * speed_factor) * 0.225);\r
+ float camera_phi =   -0.55 + (sin((bp->universe_tick + bp->camera_phase) * 0.008   * speed_factor) * 0.25);\r
+ float camera_d = 3.5 + (sin((bp->universe_tick + bp->camera_phase) * 0.013 * speed_factor));\r
+ float camera_x = camera_d * cos(camera_theta) * sin(camera_phi);\r
+ float camera_y = camera_d * sin(camera_theta) * sin(camera_phi);\r
+ float camera_z = camera_d * cos(camera_phi);\r
\r
+ // Use immediate mode rendering instead of shaders\r
+ glMatrixMode(GL_MODELVIEW);\r
+ glLoadIdentity();\r
+ gluPerspective (30.0, 1.0, 1.0, 200);\r
+ gluLookAt( camera_x, camera_y, camera_z,   // Camera position (eye point)\r
+     0.1, -0.0, 0,     // Look-at point (center)\r
+     0, 0, 1);    // Up vector\r
+\r
+ glRotatef (current_device_rotation(), 0, 0, 1);\r
+ gltrackball_rotate (bp->trackball);\r
+\r
+ for (int i = 0; i < animatedCardCount; i++)\r
+ {\r
+     glPushMatrix();\r
+\r
+     card_struct *card = renderCards[i];\r
+\r
+     float tx, ty, tz;\r
+\r
+     if (card->end_frame > last_animation)\r
+     {\r
+         last_animation = card->end_frame;\r
+     }\r
+\r
+     // card->z = i / 10.0f;\r
+\r
+     if (bp->tick >= card->start_frame && bp->tick < card->end_frame)\r
+     {\r
+         float n = ((float)bp->tick - (float)card->start_frame) / (card->end_frame - card->start_frame);\r
+         float eased = ease_out_quart(n);\r
+         float eased2 = ease_in_out_quart(n);\r
+\r
+         if (card->dest_x != card->start_x)\r
+         {\r
+             card->x = card->start_x + eased * (card->dest_x - card->start_x);\r
+         }\r
+         else\r
+         {\r
+             card->x = card->start_x;\r
+         }\r
+\r
+         if (card->dest_y != card->start_y)\r
+         {\r
+             card->y = card->start_y + eased * (card->dest_y - card->start_y);\r
+         }\r
+         else\r
+         {\r
+             card->y = card->start_y;\r
+         }\r
+\r
+         // card->z += 25 * sin(n * M_PI);\r
+\r
+         if (card->end_angle != card->start_angle)\r
+         {\r
+             card->angle = card->start_angle + eased2 * (card->end_angle - card->start_angle);\r
+         }\r
+         else\r
+         {\r
+             card->angle = card->start_angle;\r
+         }\r
+     }\r
+     // Make sure card positions are finalized after animation completes\r
+     else if (bp->tick >= card->end_frame)\r
+     {\r
+         card->x = card->dest_x;\r
+         card->y = card->dest_y;\r
+         card->angle = card->end_angle;\r
+     }\r
+     \r
+     float s = 1.0f;\r
+     GLuint current_texture = -1;\r
+     \r
+     if (card->angle > 90.0f && card->angle < 270.0f && (bp->tick > card->start_frame || bp->tick < card->end_frame))\r
+     {\r
+         int n = card->suit * 13 + card->rank - 1;\r
+         current_texture = bp->fronts[n];\r
+         s = -1.0f;\r
+     }\r
+     else\r
+     {\r
+         current_texture = bp->back;\r
+     }\r
+\r
+     tx = card->x;\r
+     ty = card->y;\r
+     tz = card->z * 0.025f;\r
+\r
+     float translateX = tx + offset_x;\r
+     float translateY = ty + offset_y;\r
+     float translateZ = tz;\r
+\r
+     float horiz_card_aspect_scale = 0.53 * height / width;\r
+     float vert_card_aspect_scale = 0.53;\r
+\r
+     if (width < height)\r
+     {\r
+         horiz_card_aspect_scale *= width / 1280.0f;\r
+         vert_card_aspect_scale *= width / 1280.0f;\r
+     }\r
+\r
+     float scaleX = s * horiz_card_aspect_scale * 0.45f * scale;\r
+     float scaleY = vert_card_aspect_scale * 0.45f * scale;\r
+     float scaleZ = horiz_card_aspect_scale * 0.45f * scale;\r
+\r
+     glTranslatef(translateX, translateY, translateZ);\r
+     glRotatef(card->angle, 0.0f, 1.0f, 0.0f);\r
+     glScalef(scaleX, scaleY, scaleZ);\r
+\r
+     // Bind the appropriate texture\r
+     glBindTexture(GL_TEXTURE_2D, current_texture);\r
+\r
+     // Draw the card quad with proper texture coordinates\r
+     glBegin(GL_QUADS);\r
+     // Front face\r
+     glNormal3f(0.0f, 0.0f, 1.0f);\r
+     glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.5f, -0.75f, 0.0f);\r
+     glTexCoord2f(1.0f, 0.0f); glVertex3f(0.5f, -0.75f, 0.0f);\r
+     glTexCoord2f(1.0f, 1.0f); glVertex3f(0.5f, 0.75f, 0.0f);\r
+     glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.5f, 0.75f, 0.0f);\r
+     glEnd();\r
+\r
+     glPopMatrix();\r
+ }\r
+\r
+ if (mi->fps_p) do_fps (mi);\r
\r
+ glFinish();\r
+ glXSwapBuffers(dpy, window);\r
+\r
+ if (bp->tick >= last_animation)\r
+ {\r
+     game_state_struct *n = NULL;\r
+     if (bp->redeal)\r
+     {\r
+         n = (game_state_struct *)malloc(sizeof(game_state_struct));\r
+         bp->tick = 0;\r
+         bp->final_animation = 0;\r
+         bp->redeal = 0;\r
+     }\r
+     else if (bp->final_animation == 0)\r
+     {\r
+         n = klondike_next_move(bp);\r
+     }\r
+\r
+     if (bp->final_animation != 0 && bp->tick >= bp->final_animation)\r
+     {\r
+         // resetBoard(game_state);\r
+         bp->redeal = 1;\r
+     }\r
+     else if (bp->final_animation == 0 && n == NULL)\r
+     {\r
+         animate_board_to_deck(bp);\r
+\r
+         // Check for win\r
+# if 0 // unused            \r
+         int won_game = 0;\r
+         for (int i = 0; i < 4; i++)\r
+         {\r
+             won_game &= (bp->game_state->foundation_size[i] == 13);\r
+         }\r
+# endif                \r
+     }\r
+\r
+     if (n != NULL)\r
+     {\r
+         klondike_free_game_state(bp->game_state);\r
+         bp->game_state = n;\r
+     }\r
+ }\r
+}\r
+\r
+ENTRYPOINT void\r
+free_klondike(ModeInfo *mi)\r
+{\r
+ klondike_configuration *bp = &bps[MI_SCREEN(mi)];\r
+ if (!bp->glx_context)\r
+     return;\r
+ glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);\r
+\r
+ // Free the textures\r
+ for (int i = 0; i < 52; i++)\r
+ {\r
+     glDeleteTextures(1, &bp->fronts[i]);\r
+ }\r
+\r
+ glDeleteTextures(1, &bp->back);\r
+\r
+ klondike_free_game_state(bp->game_state);\r
+}\r
+\r
+XSCREENSAVER_MODULE_2("Klondike", klondike, klondike)\r
+\r
+#endif /* USE_GL */\r
diff --git a/hacks/glx/platonicfolding.c b/hacks/glx/platonicfolding.c
new file mode 100644 (file)
index 0000000..2b19cc9
--- /dev/null
@@ -0,0 +1,3522 @@
+/* platonicfolding --- Displays the unfolding and folding of the Platonic
+   solids. */
+
+#if 0
+static const char sccsid[] = "@(#)platonicfolding.c  1.1 25/03/18 xlockmore";
+#endif
+
+/* Copyright (c) 2025 Carsten Steger <carsten@mirsanmir.org>. */
+
+/*
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose and without fee is hereby granted,
+ * provided that the above copyright notice appear in all copies and that
+ * both that copyright notice and this permission notice appear in
+ * supporting documentation.
+ *
+ * This file is provided AS IS with no warranties of any kind.  The author
+ * shall have no liability with respect to the infringement of copyrights,
+ * trade secrets or any patents by this file or any part thereof.  In no
+ * event will the author be liable for any lost revenue or profits or
+ * other special, indirect and consequential damages.
+ *
+ * REVISION HISTORY:
+ * C. Steger - 25/03/18: Initial version
+ */
+
+/*
+ * This program shows the unfolding and folding of the Platonic
+ * solids.  For the five Platonic solids (the tetrahedron, cube,
+ * octahedron, dodecahedron, and icosahedron), all unfoldings of its
+ * faces are non-overlapping: they form a net. The tetrahedron has 16
+ * unfoldings, of which two are essentially different
+ * (non-isomorphic), the cube and octahedron each have 384 unfoldings,
+ * of which eleven are non-isomorphic, and the dodecahedron and
+ * icosahedron each have 5,184,000 unfoldings, of which 43,380 are
+ * non-isomorphic. This program displays randomly selected unfoldings
+ * for the five Platonic solids.  Note that while it is guaranteed
+ * that the nets of the Platonic solids are non-overlapping, their
+ * faces occasionally intersect during the unfolding and folding.
+ *
+ * The program displays the Platonic solids either using different
+ * colors for each face (face colors) or with a illuminated view of
+ * the earth (earth colors).  When using face colors, the colors of
+ * the faces are randomly chosen each time a new Platonic solid is
+ * selected.  When using earth colors, the Platonic solid is displayed
+ * as if the sphere of the earth were illuminated with the current
+ * position of the sun at the time the program is run.  The hemisphere
+ * the sun is currently illuminating is displayed with a satellite
+ * image of the earth by day and the other hemisphere is displayed
+ * with a satellite image of the earth by night.  The specular
+ * highlight on the illuminated hemisphere (which is only shown over
+ * bodies of water) is the subsolar point (the point on earth above
+ * which the sun is perpendicular).  The earth's sphere is then
+ * projected onto the Platonic solid via a gnomonic projection.  The
+ * program randomly selects whether the north pole or the south pole
+ * is facing upwards.  The inside of the earth is displayed with a
+ * magma-like texture.
+ *
+ * At the beginning of each cycle, the program selects one of the five
+ * Platonic solids randomly and moves it to the center of the screen.
+ * It then repeatedly selects a random net of the polyhedron and
+ * unfolds and folds the polyhedron.  The unfolding and folding can
+ * occur around each edge of the net successively or around all edges
+ * simultaneously.  At the end of each cycle, the Platonic solid is
+ * moved offscreen and the next cycle begins.
+ *
+ * While the Platonic solid is moved on the screen or is unfolded or
+ * folded, it is rotated by default.  If earth colors are used, the
+ * rotation is always performed in the direction the earth is rotating
+ * (counterclockwise as viewed from the north pole towards the center
+ * of the earth).  This rotation optionally can be switched off.
+ */
+
+
+#define DEF_ROTATE   "True"
+#define DEF_COLORS   "random"
+#define DEF_FOLDINGS "random"
+
+#define COLORS_FACE  0
+#define COLORS_EARTH 1
+#define NUM_COLORS   2
+
+#define RANDOM_NUM_FOLDINGS -1
+
+#ifdef STANDALONE
+# define DEFAULTS           "*delay:       25000 \n" \
+                            "*showFPS:     False \n" \
+                            "*prefersGLSL: True \n" \
+
+# define release_platonicfolding 0
+# include "xlockmore.h"         /* from the xscreensaver distribution */
+#else  /* !STANDALONE */
+# include "xlock.h"             /* from the xlockmore distribution */
+#endif /* !STANDALONE */
+
+#ifdef USE_GL
+
+/* The possible animation states. */
+#define ANIM_INIT       0
+#define ANIM_APPEAR     1
+#define ANIM_DISAPPEAR  2
+#define ANIM_UNFOLD_JNT 3
+#define ANIM_FOLD_JNT   4
+#define ANIM_UNFOLD_SEP 5
+#define ANIM_FOLD_SEP   6
+
+/* The possible easing functions. */
+#define EASING_QUINTIC 0
+#define EASING_ACCEL   1
+#define EASING_DECEL   2
+
+/* Hash size for the Perlin noise generator. */
+#define HASH_SIZE 256
+
+/* Number of octaves of the Perlin noise generator to use for the magma
+   noise texture. */
+#define NUM_OCTAVES 4
+
+/* Frequency of the first octave of the Perlin noise generator to use for
+   the magma noise texture. */
+#define START_FREQUENCY 4
+
+/* Size of one dimension of the 3D magma noise texture. */
+#define MAGMA_TEX_SIZE  128
+
+/* Gamma value to use for the magma look-up table. */
+#define MAGMA_LUT_GAMMA (1.0f/1.5f)
+
+#ifndef M_PI
+#define M_PI            3.14159265358979323846
+#endif
+#define M_PI_F          3.14159265359f
+#define SQRT_2_2_F      0.70710678119f
+#define SQRT_2_4_F      0.35355339059f
+#define SQRT_3_2_F      0.86602540379f
+#define COS_36_F        0.80901699438f
+#define SIN_36_F        0.58778525229f
+#define COS_72_F        0.30901699438f
+#define SIN_72_F        0.95105651630f
+#define DODECA_IN_RAD_F 1.30901699437f
+#define ICOSA_IN_RAD_F  1.30901699437f
+
+/* The maximum stack size for the traversal of the polygon tree that
+   represents the folding and unfolding of a Platonic solid. */
+#define MAX_STACK 20
+
+/* The maximum number of folding angles. */
+#define NUM_ANGLES 19
+
+/* The maximum number of vertices over all polygons of any unfolding.  This
+   is achieved for the dodecahedron (12*5 vertices) and the icosahedron
+   (20*3 vertices). */
+#define NUM_VERTEX 60
+
+/* Constants to label vertices during the computation of the minimum
+   spanning tree of an unfolding of a Platonic solid. */
+#define UNSEEN (-1)
+#define SEEN   (-2)
+
+/* The different Platonic solid types. */
+#define TETRAHEDRON  0
+#define HEXAHEDRON   1
+#define OCTAHEDRON   2
+#define DODECAHEDRON 3
+#define ICOSAHEDRON  4
+
+/* The number of faces and edges for each Platonic solid. */
+#define TETRAHEDRON_NUM_FACES  4
+#define TETRAHEDRON_NUM_EDGES  6
+#define HEXAHEDRON_NUM_FACES   6
+#define HEXAHEDRON_NUM_EDGES   12
+#define OCTAHEDRON_NUM_FACES   8
+#define OCTAHEDRON_NUM_EDGES   12
+#define DODECAHEDRON_NUM_FACES 12
+#define DODECAHEDRON_NUM_EDGES 30
+#define ICOSAHEDRON_NUM_FACES  20
+#define ICOSAHEDRON_NUM_EDGES  30
+
+/* The maximum rotation angle for a triangle in the folding of a
+   tetrahedron.  This is 180° minus the dihedral angle of the faces, which
+   is acos(1/3) = atan(2*sqrt(2)).  Hence, this angle is acos(-1/3) =
+   2*atan(sqrt(2)). */
+#define TETRAHEDRON_MAX_ANGLE   109.4712206344907f
+#define TETRAHEDRON_DELTA_ANGLE (TETRAHEDRON_MAX_ANGLE/30.0f)
+
+/* The maximum rotation angle for a square in the folding of a hexahedron
+   (cube).  This is 180° minus the dihedral angle of the faces, which
+   obviously is 90°. */
+#define HEXAHEDRON_MAX_ANGLE   90.0f
+#define HEXAHEDRON_DELTA_ANGLE (HEXAHEDRON_MAX_ANGLE/30.0f)
+
+/* The maximum rotation angle for a triangle in the folding of an
+   octahedron.  This is 180° minus the dihedral angle of the faces, which
+   is acos(-1/3) = 2*atan(sqrt(2)).  Hence, this angle is acos(1/3) =
+   atan(2*sqrt(2)). */
+#define OCTAHEDRON_MAX_ANGLE   70.5287793655093f
+#define OCTAHEDRON_DELTA_ANGLE (OCTAHEDRON_MAX_ANGLE/30.0f)
+
+/* The maximum rotation angle for a pentagon in the folding of a
+   dodecahedron.  This is 180° minus the dihedral angle of the faces, which
+   is 180°-2*atan((sqrt(5)-1)/2).  Hence, this angle is
+   2*atan((sqrt(5)-1)/2). */
+#define DODECAHEDRON_MAX_ANGLE   63.4349488229220f
+#define DODECAHEDRON_DELTA_ANGLE (DODECAHEDRON_MAX_ANGLE/30.0f)
+
+/* The maximum rotation angle for a triangle in the folding of an
+   icosahedron.  This is 180° minus the dihedral angle of the faces, which
+   is 180°-2*atan((3-sqrt(5))/2).  Hence, this angle is
+   2*atan((3-sqrt(5))/2). */
+#define ICOSAHEDRON_MAX_ANGLE   41.8103148957786f
+#define ICOSAHEDRON_DELTA_ANGLE (ICOSAHEDRON_MAX_ANGLE/30.0f)
+
+
+#include <stdbool.h>
+#include "glsl-utils.h"
+#include "gltrackball.h"
+
+#include "images/gen/earth_png.h"
+#include "images/gen/earth_night_png.h"
+#include "images/gen/earth_water_png.h"
+#include "ximage-loader.h"
+
+
+#ifdef USE_MODULES
+ModStruct platonicfolding_description =
+{"platonicfolding", "init_platonicfolding", "draw_platonicfolding",
+ NULL, "draw_platonicfolding", "change_platonicfolding",
+ "free_platonicfolding", &platonicfolding_opts, 20000, 1, 1, 1, 1.0, 4, "",
+ "Display the unfolding and folding of Platonic solids",
+ 0, NULL};
+#endif
+
+
+static Bool rotate;
+static char *color_mode;
+static char *foldings;
+
+
+static XrmOptionDescRec opts[] =
+{
+  {"-rotate",       ".rotate",   XrmoptionNoArg,  "on"},
+  {"+rotate",       ".rotate",   XrmoptionNoArg,  "off"},
+  {"-colors",       ".colors",   XrmoptionSepArg, 0 },
+  {"-face-colors",  ".colors",   XrmoptionNoArg,  "face" },
+  {"-earth-colors", ".colors",   XrmoptionNoArg,  "earth" },
+  {"-foldings",     ".foldings", XrmoptionSepArg, 0 },
+};
+
+static argtype vars[] =
+{
+  { &rotate,     "rotate",   "Rotate",   DEF_ROTATE,   t_Bool },
+  { &color_mode, "colors",   "Colors",   DEF_COLORS,   t_String },
+  { &foldings,   "foldings", "Foldings", DEF_FOLDINGS, t_String }
+};
+
+ENTRYPOINT ModeSpecOpt platonicfolding_opts =
+{sizeof opts / sizeof opts[0], opts, sizeof vars / sizeof vars[0], vars, NULL};
+
+
+/* ------------------------------------------------------------------------- */
+
+
+typedef float vertex[4];
+typedef float normal[4];
+typedef float color[4];
+typedef float texcoord[3];
+typedef float matrix[4][4];
+
+/* Data structure to represent an edge of a graph that represents the
+   adjacency of polygons in a polyhedron. The variables src and dst
+   describe which polygon faces are adjacent.  The variables src_edge and
+   dst_edge describe which edges of the respective faces are touching.
+   The variable weight is set to a random value to create the minimum
+   spanning tree. */
+typedef struct {
+  int src, dst;
+  int src_edge, dst_edge;
+  float weight;
+} edge;
+
+/* Data structure to represent an edge-based graph with num_v vertices,
+   num_e edges, and an array of edges.  The graph represents the adjacency
+   of faces in a polyhedron via the array edges. */
+typedef struct {
+  int num_v, num_e;
+  edge *edges;
+} edge_graph;
+
+/* Disjoint set union (DSU) data structure with parent and rank.  This is
+   used in the algorithm that determines the edges of a minimum spanning
+   tree of a polyhedron. */
+typedef struct {
+  int *parent;
+  int *rank;
+} dsu;
+
+/* Data structure for an adjacency-list-based graph.  This is used to
+   convert the edges of the minimum spanning tree into a graph that is then
+   traversed to create the actual minimum spanning tree.  The variable v
+   stores the face number of the polygon in the polyhedron.  The variables
+   edge_parent and edge_self store the edges by which a polygon that is
+   adjacent to v is connected to the polygon v.  The value of edge_parent
+   refers to the edge in the parent polygon of v in the graph, while
+   edge_self refers to the edge in the polygon v. */
+typedef struct graph_node {
+  struct graph_node *next;
+  int v;
+  int edge_parent, edge_self;
+} graph_node;
+
+/* The data structure for a tree that describes an unfolded and folded
+   polyhedron.  The pointer next points to the next base polygon on the
+   same tree level.  The pointer child points to the first polygon on the
+   next lower tree level.  Hence, the unfolding is described by a general
+   tree.  The index of the polygon in the polyhedron is given by
+   polygon_index.  This is used to color the polygons in different foldings
+   consistently.  The indices edge_parent and edge_self contain the indices
+   of the edge in the parent polygon and the edge in the polygon described
+   by the current node that are attached to each other.  Since it is assumed
+   that the base polygon is oriented counterclockwise, the edges point in
+   opposite directions.  The root of the tree has no parent.  Therefore,
+   both indices must be set to -1 for the root node.  The matrix
+   unfold_pose describes the pose of the polygon described by the node with
+   respect to its parent polygon in the unfolded state.  Hence, it is a
+   relative pose and not an absolute pose.  The variable fold_angle_index
+   is used to determine which angle to use for the rotation of the polygon
+   described by a node with respect to its parent polygon.  The values of
+   unfold_pose and fold_angle_index are determined dynamically by the order
+   of the tree traversal in the initialization function
+   determine_unfolding_poses.  The variable fold_angle stores the current
+   fold angle of the polygon described by the node with respect to its
+   parent.  The matrix fold_pose stores the relative pose obtained by
+   rotating the polygon described by a node with respect to its parent
+   polygon around the edge described by edge_parent and edge_self by the
+   angle fold_angle.  The values of fold_angle and fold_pose are typically
+   determined when the folded polyhedron is drawn. */
+typedef struct tree_node {
+  struct tree_node *next;
+  struct tree_node *child;
+  int polygon_index;
+  int edge_parent, edge_self;
+  matrix unfold_pose;
+  int fold_angle_index;
+  float fold_angle;
+  matrix fold_pose;
+} tree_node;
+
+/* The data structure for a polygon consisting of num vertices v, a single
+   normal vector n, num texture coordinates t (one per vertex), and a
+   color c. */
+typedef struct {
+  int num;
+  vertex *v;
+  normal n;
+  texcoord *t;
+  color c;
+} polygon;
+
+/* The data structure for a set of polygons with polygon-specific data such
+   as texture coordinates for each vertex. */
+typedef struct {
+  int num;
+  polygon *poly;
+} polygons;
+
+
+/* ------------------------------------------------------------------------- */
+
+
+/* The edge-based adjacency graph of a tetrahedron. */
+static edge tetrahedron_edges[TETRAHEDRON_NUM_EDGES] = {
+  {  0,  1, 0, 0, 0.0f },
+  {  0,  2, 2, 0, 0.0f },
+  {  0,  3, 1, 0, 0.0f },
+  {  1,  2, 1, 2, 0.0f },
+  {  1,  3, 2, 1, 0.0f },
+  {  2,  3, 1, 2, 0.0f }
+};
+
+/* The edge-based adjacency graph of a hexahedron. */
+static edge hexahedron_edges[HEXAHEDRON_NUM_EDGES] = {
+  {  0,  1, 0, 0, 0.0f },
+  {  0,  2, 1, 0, 0.0f },
+  {  0,  3, 2, 0, 0.0f },
+  {  0,  4, 3, 0, 0.0f },
+  {  1,  2, 3, 1, 0.0f },
+  {  1,  4, 1, 3, 0.0f },
+  {  1,  5, 2, 0, 0.0f },
+  {  2,  3, 3, 1, 0.0f },
+  {  2,  5, 2, 3, 0.0f },
+  {  3,  4, 3, 1, 0.0f },
+  {  3,  5, 2, 2, 0.0f },
+  {  4,  5, 2, 1, 0.0f }
+};
+
+/* The edge-based adjacency graph of an octahedron. */
+static edge octahedron_edges[OCTAHEDRON_NUM_EDGES] = {
+  {  0,  1, 0, 0, 0.0f },
+  {  0,  2, 1, 0, 0.0f },
+  {  0,  3, 2, 0, 0.0f },
+  {  1,  4, 1, 2, 0.0f },
+  {  1,  5, 2, 1, 0.0f },
+  {  2,  5, 1, 0, 0.0f },
+  {  2,  6, 2, 0, 0.0f },
+  {  3,  4, 2, 0, 0.0f },
+  {  3,  6, 1, 2, 0.0f },
+  {  4,  7, 1, 1, 0.0f },
+  {  5,  7, 2, 0, 0.0f },
+  {  6,  7, 1, 2, 0.0f }
+};
+
+/* The edge-based adjacency graph of a dodecahedron. */
+static edge dodecahedron_edges[DODECAHEDRON_NUM_EDGES] = {
+  {  0,  1, 0, 0, 0.0f },
+  {  0,  2, 1, 4, 0.0f },
+  {  0,  3, 2, 0, 0.0f },
+  {  0,  4, 3, 3, 0.0f },
+  {  0,  5, 4, 1, 0.0f },
+  {  1,  2, 4, 0, 0.0f },
+  {  1,  5, 1, 0, 0.0f },
+  {  1,  6, 2, 0, 0.0f },
+  {  1,  7, 3, 0, 0.0f },
+  {  2,  3, 3, 1, 0.0f },
+  {  2,  7, 1, 4, 0.0f },
+  {  2,  8, 2, 2, 0.0f },
+  {  3,  4, 4, 4, 0.0f },
+  {  3,  8, 2, 1, 0.0f },
+  {  3,  9, 3, 0, 0.0f },
+  {  4,  5, 2, 2, 0.0f },
+  {  4,  9, 0, 4, 0.0f },
+  {  4, 10, 1, 4, 0.0f },
+  {  5,  6, 4, 1, 0.0f },
+  {  5, 10, 3, 3, 0.0f },
+  {  6,  7, 4, 1, 0.0f },
+  {  6, 10, 2, 2, 0.0f },
+  {  6, 11, 3, 3, 0.0f },
+  {  7,  8, 3, 3, 0.0f },
+  {  7, 11, 2, 2, 0.0f },
+  {  8,  9, 0, 1, 0.0f },
+  {  8, 11, 4, 1, 0.0f },
+  {  9, 10, 3, 0, 0.0f },
+  {  9, 11, 2, 0, 0.0f },
+  { 10, 11, 1, 4, 0.0f }
+};
+
+/* The edge-based adjacency graph of an icosahedron. */
+static edge icosahedron_edges[ICOSAHEDRON_NUM_EDGES] = {
+  {  0,  1, 2, 0, 0.0f },
+  {  0,  2, 0, 0, 0.0f },
+  {  0,  3, 1, 0, 0.0f },
+  {  1,  4, 2, 0, 0.0f },
+  {  1,  9, 1, 0, 0.0f },
+  {  2,  5, 1, 2, 0.0f },
+  {  2,  6, 2, 1, 0.0f },
+  {  3,  7, 1, 0, 0.0f },
+  {  3,  8, 2, 0, 0.0f },
+  {  4,  5, 2, 0, 0.0f },
+  {  4, 10, 1, 0, 0.0f },
+  {  5, 15, 1, 2, 0.0f },
+  {  6,  7, 0, 1, 0.0f },
+  {  6, 14, 2, 1, 0.0f },
+  {  7, 13, 2, 0, 0.0f },
+  {  8,  9, 2, 1, 0.0f },
+  {  8, 12, 1, 2, 0.0f },
+  {  9, 11, 2, 1, 0.0f },
+  { 10, 11, 1, 0, 0.0f },
+  { 10, 18, 2, 0, 0.0f },
+  { 11, 17, 2, 1, 0.0f },
+  { 12, 13, 0, 2, 0.0f },
+  { 12, 17, 1, 2, 0.0f },
+  { 13, 16, 1, 0, 0.0f },
+  { 14, 15, 2, 1, 0.0f },
+  { 14, 16, 0, 1, 0.0f },
+  { 15, 18, 0, 2, 0.0f },
+  { 16, 19, 2, 0, 0.0f },
+  { 17, 19, 0, 2, 0.0f },
+  { 18, 19, 1, 1, 0.0f }
+};
+
+/* The vertices of a tetrahedron triangle.  All vertices lie on the unit
+   circle. */
+static vertex tetrahedron_triangle_vert[3] = {
+  {  1.0f,        0.0f, -SQRT_2_4_F, 1.0f },
+  { -0.5f,  SQRT_3_2_F, -SQRT_2_4_F, 1.0f },
+  { -0.5f, -SQRT_3_2_F, -SQRT_2_4_F, 1.0f }
+};
+
+/* The triangle that is used for the tetrahedron. */
+static polygon tetrahedron_triangle = {
+  3,
+  tetrahedron_triangle_vert,
+  { 0.0f, 0.0f, 1.0f, 0.0f },
+  NULL,
+  { 0.0f, 0.0f, 0.0f, 1.0f }
+};
+
+/* The eye position for a terahedron. */
+static const GLfloat tetrahedron_eye_pos[3] = { 0.0f, 0.0f, 6.0f };
+
+
+/* The vertices of a hexahedron square.  All vertices lie on the unit
+   circle. */
+static vertex hexahedron_square_vert[4] = {
+  {  SQRT_2_2_F, -SQRT_2_2_F, -SQRT_2_2_F, 1.0f },
+  {  SQRT_2_2_F,  SQRT_2_2_F, -SQRT_2_2_F, 1.0f },
+  { -SQRT_2_2_F,  SQRT_2_2_F, -SQRT_2_2_F, 1.0f },
+  { -SQRT_2_2_F, -SQRT_2_2_F, -SQRT_2_2_F, 1.0f }
+};
+
+/* The square that is used for the hexahedron (cube). */
+static polygon hexahedron_square = {
+  4,
+  hexahedron_square_vert,
+  { 0.0f, 0.0f, 1.0f, 0.0f },
+  NULL,
+  { 0.0f, 0.0f, 0.0f, 1.0f }
+};
+
+/* The eye position for a hexahedron. */
+static const GLfloat hexahedron_eye_pos[3] = { 0.0f, 0.0f, 6.0f };
+
+
+/* The vertices of an octahedron triangle.  All vertices lie on the unit
+   circle. */
+static vertex octahedron_triangle_vert[3] = {
+  {  1.0f,        0.0f, -SQRT_2_2_F, 1.0f },
+  { -0.5f,  SQRT_3_2_F, -SQRT_2_2_F, 1.0f },
+  { -0.5f, -SQRT_3_2_F, -SQRT_2_2_F, 1.0f }
+};
+
+/* The triangle that is used for the octahedron. */
+static polygon octahedron_triangle = {
+  3,
+  octahedron_triangle_vert,
+  { 0.0f, 0.0f, 1.0f, 0.0f },
+  NULL,
+  { 0.0f, 0.0f, 0.0f, 1.0f }
+};
+
+/* The eye position for an octahedron. */
+static const GLfloat octahedron_eye_pos[3] = { 0.0f, 0.0f, 6.0f };
+
+
+/* The vertices of a dodecahedron pentagon.  All vertices lie on the unit
+   circle. */
+static vertex dodecahedron_pentagon_vert[5] = {
+  {      1.0f,      0.0f, -DODECA_IN_RAD_F, 1.0f },
+  {  COS_72_F,  SIN_72_F, -DODECA_IN_RAD_F, 1.0f },
+  { -COS_36_F,  SIN_36_F, -DODECA_IN_RAD_F, 1.0f },
+  { -COS_36_F, -SIN_36_F, -DODECA_IN_RAD_F, 1.0f },
+  {  COS_72_F, -SIN_72_F, -DODECA_IN_RAD_F, 1.0f }
+};
+
+/* The pentagon that is used for the dodecahedron. */
+static polygon dodecahedron_pentagon = {
+  5,
+  dodecahedron_pentagon_vert,
+  { 0.0f, 0.0f, 1.0f, 0.0f },
+  NULL,
+  { 0.0f, 0.0f, 0.0f, 1.0f }
+};
+
+/* The eye position for a dodecahedron. */
+static const GLfloat dodecahedron_eye_pos[3] = { 0.0f, 0.0f, 12.0f };
+
+
+/* The vertices of a icosahedron triangle.  All vertices lie on the unit
+   circle. */
+static vertex icosahedron_triangle_vert[3] = {
+  {  1.0f,        0.0f, -ICOSA_IN_RAD_F, 1.0f },
+  { -0.5f,  SQRT_3_2_F, -ICOSA_IN_RAD_F, 1.0f },
+  { -0.5f, -SQRT_3_2_F, -ICOSA_IN_RAD_F, 1.0f }
+};
+
+/* The triangle that is used for the icosahedron. */
+static polygon icosahedron_triangle = {
+  3,
+  icosahedron_triangle_vert,
+  { 0.0f, 0.0f, 1.0f, 0.0f },
+  NULL,
+  { 0.0f, 0.0f, 0.0f, 1.0f }
+};
+
+/* The eye position for an icosahedron. */
+static const GLfloat icosahedron_eye_pos[3] = { 0.0f, 0.0f, 12.0f };
+
+
+/* ------------------------------------------------------------------------- */
+
+
+typedef struct {
+  GLint WindH, WindW;
+  GLXContext *glx_context;
+  /* Options */
+  bool rotate;
+  int colors;
+  bool random_colors;
+  int num_foldings;
+  /* Aspect ratio of the current window */
+  float aspect;
+  /* Trackball states */
+  trackball_state *trackball;
+  Bool button_pressed;
+  /* The folding angle data. */
+  float angle[NUM_ANGLES];
+  float max_angle, delta_angle, angle_dir;
+  /* The index of the current angle that is folded or unfolded. */
+  int fold_angle;
+  /* The number of angles that can be folded for the current polyhedron. */
+  int num_fold_angles;
+  /* The 3D rotation angles. */
+  float alpha, beta, delta, delta_delta;
+  /* The eype position of the current polyhedron. */
+  const GLfloat *eye_pos;
+  /* The unfolding tree of the current polyhedron. */
+  tree_node *polygon_unfolding;
+  /* The base polygons of the current polyhedron. */
+  polygons *base_polygons;
+  /* Should the north pole or the south pole face upwards? */
+  bool north_up;
+  /* Animation state variables. */
+  int anim_state, anim_poly, anim_step, anim_num_steps, anim_remaining;
+  float poly_pos[3], spoly_pos[3], dpoly_pos[3];
+  /* The current color rotation matrix. */
+  matrix color_matrix;
+  /* Buffers for drawing a polyhedron. */
+  vertex vtx[NUM_VERTEX];
+  normal nrm[NUM_VERTEX];
+  color col[NUM_VERTEX];
+  texcoord tex[NUM_VERTEX];
+  int num[NUM_VERTEX];
+#ifdef HAVE_GLSL
+  bool use_shaders, use_mipmaps;
+  GLuint shader_program;
+  GLint pos_index, normal_index, color_index, tex_index;
+  GLint mv_index, proj_index;
+  GLint glbl_ambient_index, lt_ambient_index;
+  GLint lt_diffuse_index, lt_specular_index;
+  GLint lt_direction_index, lt_halfvect_index;
+  GLint ambient_index, diffuse_index;
+  GLint specular_index, shininess_index;
+  GLint bool_textures_index, north_up_index, magma_offs_index;
+  GLint samp_mgm_index, samp_day_index, samp_ngt_index, samp_wtr_index;
+  GLuint vertex_buffer, normal_buffer, color_buffer, tex_buffer;
+  GLuint magma_tex, earth_tex[3];
+  GLubyte *magma_tex_rg;
+  GLfloat magma_tex_offset[3];
+  bool textures_supported, aniso_textures, use_textures;
+#endif /* HAVE_GLSL */
+} platonicfoldingstruct;
+
+static platonicfoldingstruct *platonicfolding = (platonicfoldingstruct *) NULL;
+
+
+/* ------------------------------------------------------------------------- */
+
+
+#ifdef HAVE_GLSL
+
+/* The hash table for the Perlin noise generator.  In Perlin's original
+   code, only half of this table is stored and then copied to a hash table
+   twice the size.  We directly store the table of twice the size. */
+static GLushort permutation[2*HASH_SIZE] = {
+  151, 160, 137,  91,  90,  15, 131,  13,
+  201,  95,  96,  53, 194, 233,   7, 225,
+  140,  36, 103,  30,  69, 142,   8,  99,
+   37, 240,  21,  10,  23, 190,   6, 148,
+  247, 120, 234,  75,   0,  26, 197,  62,
+   94, 252, 219, 203, 117,  35,  11,  32,
+   57, 177,  33,  88, 237, 149,  56,  87,
+  174,  20, 125, 136, 171, 168,  68, 175,
+   74, 165,  71, 134, 139,  48,  27, 166,
+   77, 146, 158, 231,  83, 111, 229, 122,
+   60, 211, 133, 230, 220, 105,  92,  41,
+   55,  46, 245,  40, 244, 102, 143,  54,
+   65,  25,  63, 161,   1, 216,  80,  73,
+  209,  76, 132, 187, 208,  89,  18, 169,
+  200, 196, 135, 130, 116, 188, 159,  86,
+  164, 100, 109, 198, 173, 186,   3,  64,
+   52, 217, 226, 250, 124, 123,   5, 202,
+   38, 147, 118, 126, 255,  82,  85, 212,
+  207, 206,  59, 227,  47,  16,  58,  17,
+  182, 189,  28,  42, 223, 183, 170, 213,
+  119, 248, 152,   2,  44, 154, 163,  70,
+  221, 153, 101, 155, 167,  43, 172,   9,
+  129,  22,  39, 253,  19,  98, 108, 110,
+   79, 113, 224, 232, 178, 185, 112, 104,
+  218, 246,  97, 228, 251,  34, 242, 193,
+  238, 210, 144,  12, 191, 179, 162, 241,
+   81,  51, 145, 235, 249,  14, 239, 107,
+   49, 192, 214,  31, 181, 199, 106, 157,
+  184,  84, 204, 176, 115, 121,  50,  45,
+  127,   4, 150, 254, 138, 236, 205,  93,
+  222, 114,  67,  29,  24,  72, 243, 141,
+  128, 195,  78,  66, 215,  61, 156, 180,
+  151, 160, 137,  91,  90,  15, 131,  13,
+  201,  95,  96,  53, 194, 233,   7, 225,
+  140,  36, 103,  30,  69, 142,   8,  99,
+   37, 240,  21,  10,  23, 190,   6, 148,
+  247, 120, 234,  75,   0,  26, 197,  62,
+   94, 252, 219, 203, 117,  35,  11,  32,
+   57, 177,  33,  88, 237, 149,  56,  87,
+  174,  20, 125, 136, 171, 168,  68, 175,
+   74, 165,  71, 134, 139,  48,  27, 166,
+   77, 146, 158, 231,  83, 111, 229, 122,
+   60, 211, 133, 230, 220, 105,  92,  41,
+   55,  46, 245,  40, 244, 102, 143,  54,
+   65,  25,  63, 161,   1, 216,  80,  73,
+  209,  76, 132, 187, 208,  89,  18, 169,
+  200, 196, 135, 130, 116, 188, 159,  86,
+  164, 100, 109, 198, 173, 186,   3,  64,
+   52, 217, 226, 250, 124, 123,   5, 202,
+   38, 147, 118, 126, 255,  82,  85, 212,
+  207, 206,  59, 227,  47,  16,  58,  17,
+  182, 189,  28,  42, 223, 183, 170, 213,
+  119, 248, 152,   2,  44, 154, 163,  70,
+  221, 153, 101, 155, 167,  43, 172,   9,
+  129,  22,  39, 253,  19,  98, 108, 110,
+   79, 113, 224, 232, 178, 185, 112, 104,
+  218, 246,  97, 228, 251,  34, 242, 193,
+  238, 210, 144,  12, 191, 179, 162, 241,
+   81,  51, 145, 235, 249,  14, 239, 107,
+   49, 192, 214,  31, 181, 199, 106, 157,
+  184,  84, 204, 176, 115, 121,  50,  45,
+  127,   4, 150, 254, 138, 236, 205,  93,
+  222, 114,  67,  29,  24,  72, 243, 141,
+  128, 195,  78,  66, 215,  61, 156, 180
+};
+
+
+/* The fade function of Perlin's improved noise generator. */
+static inline float fade(float t)
+{
+  return ((6.0f*t-15.0f)*t+10.0f)*t*t*t;
+}
+
+
+/* The linear interpolation function of Perlin's improved noise generator. */
+static inline float lerp(float t, float a, float b)
+{
+  return a+t*(b-a);
+}
+
+
+/* The gradient noise generator of Perlin's improved noise generator. */
+static float grad(GLushort hash, float x, float y, float z)
+{
+  GLushort h;
+  float u, v;
+
+  /* Convert the low four bits of the hash code into twelve gradient
+     directions. */
+  h = hash&15;
+  u = h<8 ? x : y;
+  v = h<4 ? y : h==12 || h==14 ? x : z;
+  return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
+}
+
+
+/* Perlin's improved noise generator. */
+static float noise3(float x, float y, float z)
+{
+  static const GLushort *p = permutation;
+  float u, v, w;
+  GLushort X, Y, Z, A, B, AA, AB, BA, BB;
+
+  /* Find unit cube that contains the point (x,y,z). */
+  X = (int)floorf(x) & (HASH_SIZE-1);
+  Y = (int)floorf(y) & (HASH_SIZE-1);
+  Z = (int)floorf(z) & (HASH_SIZE-1);
+  /* Find the relative x, y, and z coordinates of the point in the cube. */
+  x -= floorf(x);
+  y -= floorf(y);
+  z -= floorf(z);
+  /* Compute fade curves for each of x, y, and z. */
+  u = fade(x);
+  v = fade(y);
+  w = fade(z);
+  /* Compute the hash coordinates of the eight cube corners. */
+  A = p[X]+Y;
+  AA = p[A]+Z;
+  AB = p[A+1]+Z;
+  B = p[X+1]+Y;
+  BA = p[B]+Z;
+  BB = p[B+1]+Z;
+  /* Add the blended results from the eight corners of the cube. */
+  return lerp(w,lerp(v,lerp(u,grad(p[AA],x,y,z),
+                              grad(p[BA],x-1.0f,y,z)),
+                       lerp(u,grad(p[AB],x,y-1.0f,z),
+                              grad(p[BB],x-1.0f,y-1.0f,z))),
+                lerp(v,lerp(u,grad(p[AA+1],x,y,z-1.0f),
+                              grad(p[BA+1],x-1.0f,y,z-1.0f)),
+                       lerp(u,grad(p[AB+1],x,y-1.0f,z-1.0f),
+                              grad(p[BB+1],x-1.0f,y-1.0f,z-1.0f))));
+}
+
+
+/* The smoothstep function of the OpenGL shading language. */
+static inline float smoothstep(float min, float max, float x)
+{
+  float t;
+
+  if (x < min)
+  {
+    return 0.0f;
+  }
+  else if (x > max)
+  {
+    return 1.0f;
+  }
+  else
+  {
+    t = (x-min)/(max-min);
+    return t*t*(3.0f-2.0f*t);
+  }
+}
+
+
+/* Create a red and green look-up table that simulates the look of a heated
+   material.  The blue look-up table would entirely consist of zeros, so we
+   don't create it. */
+static void create_magma_lut_rg(GLubyte lutr[256], GLubyte lutg[256])
+{
+  int i;
+
+  for (i=0; i<256; i++)
+  {
+    lutr[i] = (GLubyte)(255.0f*powf(smoothstep(0.0f,159.0f,(float)i),
+                                    MAGMA_LUT_GAMMA));
+    lutg[i] = (GLubyte)(255.0f*powf(smoothstep(96.0f,255.0f,(float)i),
+                                    MAGMA_LUT_GAMMA));
+  }
+}
+
+
+/* Create a 3D magma texture of dimension size×size×size.  Since the blue
+   channel would be entirely filled with zeros, we omit this channel and
+   create a texture consisting only of the red and green channels. */
+static GLubyte *gen_3d_magma_texture_rg(int size)
+{
+  const float startx = 0.0f, starty = 0.0f, startz = 0.0f;
+  int f, i, j, k, frequency;
+  float x, y, z, inc[NUM_OCTAVES];
+  float a, amp[NUM_OCTAVES], n;
+  GLubyte *ptr, *tex_ptr, g, lutr[256], lutg[256];
+
+  create_magma_lut_rg(lutr,lutg);
+
+  tex_ptr = malloc(2*size*size*size*sizeof(*tex_ptr));
+  if (tex_ptr == NULL)
+    abort();
+
+  frequency = START_FREQUENCY;
+  a = 196.0f;
+  for (f=0; f<NUM_OCTAVES; f++)
+  {
+    inc[f] = (float)frequency/(float)size;
+    amp[f] = a;
+    frequency *= 2;
+    a *= 0.5f;
+  }
+
+  ptr = tex_ptr;
+  for (i=0; i<size; i++)
+  {
+    for (j=0; j<size; j++)
+    {
+      for (k=0; k<size; k++, ptr+=2)
+      {
+        n = 0.0f;
+        for (f=0; f<NUM_OCTAVES; f++)
+        {
+          x = startx+i*inc[f];
+          y = starty+j*inc[f];
+          z = startz+k*inc[f];
+          n += fabsf(noise3(x,y,z))*amp[f];
+        }
+        if (n > 255.0f)
+          n = 255.0f;
+        g = (unsigned char)floorf(n+0.5f);
+        ptr[0] = lutr[g];
+        ptr[1] = lutg[g];
+      }
+    }
+  }
+
+  return tex_ptr;
+}
+
+#endif /* HAVE_GLSL */
+
+
+/* ------------------------------------------------------------------------- */
+
+
+#ifdef HAVE_GLSL
+
+/* Convert degrees to radians, */
+static inline double rad(double deg)
+{
+  return (M_PI/180.0)*deg;
+}
+
+
+/* Convert radians to degrees, */
+static inline double deg(double rad)
+{
+  return (180.0/M_PI)*rad;
+}
+
+
+/* Compute the Fortran modulo function. */
+static inline double modulo(double a, double p)
+{
+  return a-floor(a/p)*p;
+}
+
+
+/* Get the Julian date based on the current time. */
+static inline double get_current_julian_date(void)
+{
+  return (double)time(NULL)/86400.0+2440587.5;
+}
+
+
+/* Get the direction of the sun on the Julian date given by jd.  The
+   function calculates the latitude and longitude of the subsolar point,
+   i.e., the point on earth for which the sun is perpendicular.  The
+   longitude and latitude are then converted to a direction vector that
+   points to the sun.  See: Taiping Zhang, Paul W. Stackhouse Jr., Bradley
+   Macpherson, and J. Colleen Mikovitz: A solar azimuth formula that
+   renders circumstantial treatment unnecessary without compromising
+   mathematical rigor: Mathematical setup, application and extension of a
+   formula based on the subsolar point and atan2 function. Renewable Energy
+   172:1333-1340, 2021. */
+static inline void get_sun_direction(double jd, float sun[4])
+{
+  double n, utc;
+  double l, g, gr, lambda, lambdar, epsilon, epsilonr;
+  double alpha, delta, eot, lat, lon;
+  float latr, lonr;
+
+  n = jd-2451545.0;
+  utc = 24.0*modulo(jd-0.5,1.0);
+  l = modulo(280.460+0.9856474*n,360.0);
+  g = modulo(357.528+0.9856003*n,360.0);
+  gr = rad(g);
+  lambda = modulo(l+1.915*sin(gr)+0.020*sin(2.0*gr),360.0);
+  epsilon = 23.439-0.0000004*n;
+  lambdar = rad(lambda);
+  epsilonr = rad(epsilon);
+  alpha = modulo(deg(atan2(cos(epsilonr)*sin(lambdar),cos(lambdar))),360.0);
+  delta = deg(asin(sin(epsilonr)*sin(lambdar)));
+  eot = modulo(l-alpha+180.0,360.0)-180.0;
+  lat = delta;
+  lon = -15.0*(utc-12.0+eot*(4.0/60.0));
+  latr = (float)rad(lat);
+  lonr = (float)rad(lon);
+  sun[0] = cosf(latr)*cosf(lonr);
+  sun[1] = cosf(latr)*sinf(lonr);
+  sun[2] = sinf(latr);
+  sun[3] = 0.0f;
+}
+
+#endif /* HAVE_GLSL */
+
+
+/* ------------------------------------------------------------------------- */
+
+
+#ifdef HAVE_GLSL
+
+/* The GLSL versions that correspond to different versions of OpenGL. */
+static const GLchar *shader_version_2_1 =
+  "#version 120\n";
+static const GLchar *shader_version_3_0 =
+  "#version 130\n";
+static const GLchar *shader_version_3_0_es =
+  "#version 300 es\n"
+  "precision highp float;\n"
+  "precision highp int;\n"
+  "precision highp sampler2D;\n"
+  "precision highp sampler3D;\n";
+
+/* The vertex shader code is composed of code fragments that depend on
+   the OpenGL version and code fragments that are version-independent.
+   They are concatenated by glShaderSource in the function init_glsl(). */
+static const GLchar *vertex_shader_attribs_2_1 =
+  "attribute vec4 VertexPosition;\n"
+  "attribute vec4 VertexNormal;\n"
+  "attribute vec4 VertexColor;\n"
+  "attribute vec3 VertexTexCoord;\n"
+  "\n"
+  "varying vec3 Normal;\n"
+  "varying vec4 Color;\n"
+  "varying vec3 EarthTexCoord;\n"
+  "varying vec3 LightTexCoord;\n"
+  "varying vec3 MagmaTexCoord;\n"
+  "\n";
+static const GLchar *vertex_shader_attribs_3_0 =
+  "in vec4 VertexPosition;\n"
+  "in vec4 VertexNormal;\n"
+  "in vec4 VertexColor;\n"
+  "in vec3 VertexTexCoord;\n"
+  "\n"
+  "out vec3 Normal;\n"
+  "out vec4 Color;\n"
+  "out vec3 EarthTexCoord;\n"
+  "out vec3 LightTexCoord;\n"
+  "out vec3 MagmaTexCoord;\n"
+  "\n";
+static const GLchar *vertex_shader_main =
+  "uniform mat4 MatModelView;\n"
+  "uniform mat4 MatProj;\n"
+  "uniform vec3 TexOffsetMagma;\n"
+  "uniform bool NorthUp;\n"
+  "\n"
+  "void main (void)\n"
+  "{\n"
+  "  Color = VertexColor;\n"
+  "  Normal = normalize(MatModelView*VertexNormal).xyz;\n"
+  "  vec4 Position = MatModelView*VertexPosition;\n"
+  "  gl_Position = MatProj*Position;\n"
+  "  if (NorthUp)\n"
+  "    EarthTexCoord = VertexTexCoord;\n"
+  "  else\n"
+  "    EarthTexCoord = vec3(-1.0f,1.0f,-1.0f)*VertexTexCoord;\n"
+  "  LightTexCoord = (MatModelView*vec4(VertexTexCoord,0.0f)).xyz;\n"
+  "  MagmaTexCoord = 0.4f*(VertexTexCoord+1.0f)+TexOffsetMagma;\n"
+  "}\n";
+
+/* The fragment shader code is composed of code fragments that depend on
+   the OpenGL version and code fragments that are version-independent.
+   They are concatenated by glsl_CompileAndLinkShaders in the function
+   init_glsl(). */
+static const GLchar *fragment_shader_attribs_2_1 =
+  "varying vec3 Normal;\n"
+  "varying vec4 Color;\n"
+  "varying vec3 EarthTexCoord;\n"
+  "varying vec3 LightTexCoord;\n"
+  "varying vec3 MagmaTexCoord;\n"
+  "\n";
+static const GLchar *fragment_shader_attribs_3_0 =
+  "in vec3 Normal;\n"
+  "in vec4 Color;\n"
+  "in vec3 EarthTexCoord;\n"
+  "in vec3 LightTexCoord;\n"
+  "in vec3 MagmaTexCoord;\n"
+  "\n"
+  "out vec4 FragColor;\n"
+  "\n";
+static const GLchar *fragment_shader_main =
+  "const float I_M_PI_F = 0.318309886184f;\n"
+  "const float I_M_2PI_F = 0.159154943092f;\n"
+  "uniform vec4 LtGlblAmbient;\n"
+  "uniform vec4 LtAmbient, LtDiffuse, LtSpecular;\n"
+  "uniform vec3 LtDirection, LtHalfVector;\n"
+  "uniform vec4 MatAmbient;\n"
+  "uniform vec4 MatDiffuse;\n"
+  "uniform vec4 MatSpecular;\n"
+  "uniform float MatShininess;\n"
+  "uniform bool BoolTextures;\n"
+  "uniform sampler3D TextureSamplerMagma;\n"
+  "uniform sampler2D TextureSamplerDay;\n"
+  "uniform sampler2D TextureSamplerNight;\n"
+  "uniform sampler2D TextureSamplerWater;\n"
+  "\n"
+  "void main (void)\n"
+  "{\n"
+  "  vec3 normalDirection;\n"
+  "  vec4 ambientColor, diffuseColor, sceneColor;\n"
+  "  vec4 ambientLighting, diffuseReflection, specularReflection;\n"
+  "  vec4 color, colord, colorn, specd, specn;\n"
+  "  vec3 normTexCoord, normLightCoord\n;"
+  "  vec2 texCoor;\n"
+  "  float ndotl, ldotls, ndoth, attd, attn, pf, lat, lon;\n"
+  "  \n"
+  "  if (!BoolTextures)\n"
+  "  {\n"
+  "    if (gl_FrontFacing)\n"
+  "    {\n"
+  "      normalDirection = normalize(Normal);\n"
+  "      sceneColor = Color*MatAmbient*LtGlblAmbient;\n"
+  "      ambientColor = Color*MatAmbient;\n"
+  "      diffuseColor = Color*MatDiffuse;\n"
+  "    }\n"
+  "    else\n"
+  "    {\n"
+  "      normalDirection = -normalize(Normal);\n"
+  "      sceneColor = Color*MatAmbient*LtGlblAmbient;\n"
+  "      ambientColor = Color*MatAmbient;\n"
+  "      diffuseColor = Color*MatDiffuse;\n"
+  "    }\n"
+  "    \n"
+  "    ndotl = max(0.0f,dot(normalDirection,LtDirection));\n"
+  "    ndoth = max(0.0f,dot(normalDirection,LtHalfVector));\n"
+  "    if (ndotl == 0.0f)\n"
+  "      pf = 0.0f;\n"
+  "    else\n"
+  "      pf = pow(ndoth,MatShininess);\n"
+  "    ambientLighting = ambientColor*LtAmbient;\n"
+  "    diffuseReflection = LtDiffuse*diffuseColor*ndotl;\n"
+  "    specularReflection = LtSpecular*MatSpecular*pf;\n"
+  "    color = sceneColor+ambientLighting+diffuseReflection+\n"
+  "            specularReflection;\n"
+  "    color.a = diffuseColor.a;\n"
+  "  }\n";
+static const GLchar *fragment_shader_out_2_1 =
+  "  else\n"
+  "  {\n"
+  "    if (gl_FrontFacing)\n"
+  "    {\n"
+  "      color = texture3D(TextureSamplerMagma,MagmaTexCoord);\n"
+  "      sceneColor = color*MatAmbient*LtGlblAmbient;\n"
+  "      ambientColor = color*MatAmbient;\n"
+  "      diffuseColor = color*MatDiffuse;\n"
+  "      specularReflection = vec4(0.0f,0.0f,0.0f,0.0f);\n"
+  "      diffuseReflection = LtDiffuse*diffuseColor;\n"
+  "    }\n"
+  "    else\n"
+  "    {\n"
+  "      normTexCoord = normalize(EarthTexCoord);\n"
+  "      normLightCoord = normalize(LightTexCoord);\n"
+  "      lat = asin(normTexCoord.z);\n"
+  "      lon = atan(normTexCoord.y,normTexCoord.x);\n"
+  "      texCoor = vec2(0.5f+lon*I_M_2PI_F,0.5f+lat*I_M_PI_F);\n"
+  "      ndotl = max(0.0f,dot(normLightCoord,LtDirection));\n"
+  "      ndoth = max(0.0f,dot(normLightCoord,LtHalfVector));\n"
+  "      if (ndotl == 0.0f)\n"
+  "        pf = 0.0f;\n"
+  "      else\n"
+  "        pf = pow(ndoth,MatShininess);\n"
+  "      specularReflection = LtSpecular*MatSpecular*pf;\n"
+  "      ldotls = dot(normLightCoord,LtDirection);\n"
+  "      colord = texture2D(TextureSamplerDay,texCoor);\n"
+  "      attd = smoothstep(-0.2f,0.2f,ldotls);\n"
+  "      colorn = texture2D(TextureSamplerNight,texCoor);\n"
+  "      attn = smoothstep(-0.2f,0.2f,-ldotls);\n"
+  "      color = attd*colord+attn*colorn;\n"
+  "      specd = texture2D(TextureSamplerWater,texCoor);\n"
+  "      specn = vec4(0.0f,0.0f,0.0f,1.0f);\n"
+  "      specularReflection *= attd*specd+attn*specn;\n"
+  "      sceneColor = color*MatAmbient*LtGlblAmbient;\n"
+  "      ambientColor = color*MatAmbient;\n"
+  "      diffuseColor = color*MatDiffuse;\n"
+  "      diffuseReflection = LtDiffuse*diffuseColor*ndotl;\n"
+  "    }\n"
+  "    \n"
+  "    ambientLighting = ambientColor*LtAmbient;\n"
+  "    color = sceneColor+ambientLighting+diffuseReflection+\n"
+  "            specularReflection;\n"
+  "    color.a = diffuseColor.a;\n"
+  "  }\n"
+  "  gl_FragColor = clamp(color,0.0f,1.0f);\n"
+  "}\n";
+static const GLchar *fragment_shader_out_3_0 =
+  "  else\n"
+  "  {\n"
+  "    if (gl_FrontFacing)\n"
+  "    {\n"
+  "      color = texture(TextureSamplerMagma,MagmaTexCoord);\n"
+  "      sceneColor = color*MatAmbient*LtGlblAmbient;\n"
+  "      ambientColor = color*MatAmbient;\n"
+  "      diffuseColor = color*MatDiffuse;\n"
+  "      specularReflection = vec4(0.0f,0.0f,0.0f,0.0f);\n"
+  "      diffuseReflection = LtDiffuse*diffuseColor;\n"
+  "    }\n"
+  "    else\n"
+  "    {\n"
+  "      normTexCoord = normalize(EarthTexCoord);\n"
+  "      normLightCoord = normalize(LightTexCoord);\n"
+  "      lat = asin(normTexCoord.z);\n"
+  "      lon = atan(normTexCoord.y,normTexCoord.x);\n"
+  "      texCoor = vec2(0.5f+lon*I_M_2PI_F,0.5f+lat*I_M_PI_F);\n"
+  "      ndotl = max(0.0f,dot(normLightCoord,LtDirection));\n"
+  "      ndoth = max(0.0f,dot(normLightCoord,LtHalfVector));\n"
+  "      if (ndotl == 0.0f)\n"
+  "        pf = 0.0f;\n"
+  "      else\n"
+  "        pf = pow(ndoth,MatShininess);\n"
+  "      specularReflection = LtSpecular*MatSpecular*pf;\n"
+  "      ldotls = dot(normLightCoord,LtDirection);\n"
+  "      colord = texture(TextureSamplerDay,texCoor);\n"
+  "      attd = smoothstep(-0.2f,0.2f,ldotls);\n"
+  "      colorn = texture(TextureSamplerNight,texCoor);\n"
+  "      attn = smoothstep(-0.2f,0.2f,-ldotls);\n"
+  "      color = attd*colord+attn*colorn;\n"
+  "      specd = texture(TextureSamplerWater,texCoor);\n"
+  "      specn = vec4(0.0f,0.0f,0.0f,1.0f);\n"
+  "      specularReflection *= attd*specd+attn*specn;\n"
+  "      sceneColor = color*MatAmbient*LtGlblAmbient;\n"
+  "      ambientColor = color*MatAmbient;\n"
+  "      diffuseColor = color*MatDiffuse;\n"
+  "      diffuseReflection = LtDiffuse*diffuseColor*ndotl;\n"
+  "    }\n"
+  "    \n"
+  "    ambientLighting = ambientColor*LtAmbient;\n"
+  "    color = sceneColor+ambientLighting+diffuseReflection+\n"
+  "            specularReflection;\n"
+  "    color.a = diffuseColor.a;\n"
+  "  }\n"
+  "  FragColor = clamp(color,0.0f,1.0f);\n"
+  "}\n";
+
+#endif /* HAVE_GLSL */
+
+
+/* ------------------------------------------------------------------------- */
+
+
+/* Copy a vector c = v. */
+static inline void copy_vector(float c[3], const float v[3])
+{
+  c[0] = v[0];
+  c[1] = v[1];
+  c[2] = v[2];
+}
+
+
+/* Compute the negative of a vector n = -a. */
+static inline void neg(float n[3], const float a[3])
+{
+  n[0] = -a[0];
+  n[1] = -a[1];
+  n[2] = -a[2];
+}
+
+
+/* Scale a vector by a factor of s: w = v * s. */
+static inline void scale(float w[3], const float v[3], float s)
+{
+  w[0] = v[0]*s;
+  w[1] = v[1]*s;
+  w[2] = v[2]*s;
+}
+
+
+/* Compute the vector sum s = a + b. */
+static inline void add(float s[3], const float a[3], const float b[3])
+{
+  s[0] = a[0]+b[0];
+  s[1] = a[1]+b[1];
+  s[2] = a[2]+b[2];
+}
+
+
+/* Compute the vector difference s = a - b. */
+static inline void sub(float s[3], const float a[3], const float b[3])
+{
+  s[0] = a[0]-b[0];
+  s[1] = a[1]-b[1];
+  s[2] = a[2]-b[2];
+}
+
+
+/* Compute the midpoint of two vectors m = (a + b) / 2. */
+static inline void mid(float m[3], const float a[3], const float b[3])
+{
+  m[0] = 0.5f*(a[0]+b[0]);
+  m[1] = 0.5f*(a[1]+b[1]);
+  m[2] = 0.5f*(a[2]+b[2]);
+}
+
+
+/* Compute the dot product of two vectors. */
+static inline float dot(const float v[3], const float w[3])
+{
+  return v[0]*w[0]+v[1]*w[1]+v[2]*w[2];
+}
+
+
+/* Compute the Euclidean norm of a vector. */
+static inline float norm(const float v[3])
+{
+  return sqrtf(dot(v,v));
+}
+
+
+/* Normalize a vector. */
+static inline void normalize(float v[3])
+{
+  float l;
+
+  l = norm(v);
+  if (l > 0.0f)
+    l = 1.0f/l;
+  v[0] *= l;
+  v[1] *= l;
+  v[2] *= l;
+}
+
+
+/* Compute the cross product c = m × n. */
+static inline void cross(float c[3], const float m[3], const float n[3])
+{
+  c[0] = m[1]*n[2]-m[2]*n[1];
+  c[1] = m[2]*n[0]-m[0]*n[2];
+  c[2] = m[0]*n[1]-m[1]*n[0];
+}
+
+
+/* Copy a matrix: c = m. */
+static void copy_matrix(matrix c, matrix m)
+{
+  int i, j;
+
+  for (j=0; j<4; j++)
+    for (i=0; i<4; i++)
+      c[i][j] = m[i][j];
+}
+
+
+/* Compute the identity matrix. */
+static void identity_matrix(matrix m)
+{
+  int i, j;
+
+  for (i=0; i<4; i++)
+    for (j=0; j<4; j++)
+      m[i][j] = (float)(i==j);
+}
+
+
+/* Multiply two matrices: c = c * m. */
+static void mult_matrix(matrix c, matrix m)
+{
+  int i, j;
+  matrix t;
+
+  /* Copy c to t. */
+  copy_matrix(t,c);
+  /* Compute c = t*m. */
+  for (j=0; j<4; j++)
+    for (i=0; i<4; i++)
+      c[i][j] = (t[i][0]*m[0][j]+t[i][1]*m[1][j]+
+                 t[i][2]*m[2][j]+t[i][3]*m[3][j]);
+}
+
+
+/* Add a translation matrix from the right to a matrix. */
+static void translate_matrix(matrix m, const float t[3])
+{
+  int i;
+
+  for (i=0; i<4; i++)
+    m[i][3] = (t[0]*m[i][0]+t[1]*m[i][1]+t[2]*m[i][2]+m[i][3]);
+}
+
+
+/* Add a rotation in the xy plane to the matrix m. */
+static void rotate_xy_matrix(matrix m, float phi)
+{
+  float c, s, u, v;
+  int i;
+
+  phi *= M_PI_F/180.0f;
+  c = cosf(phi);
+  s = sinf(phi);
+  for (i=0; i<4; i++)
+  {
+    u = m[i][0];
+    v = m[i][1];
+    m[i][0] = c*u+s*v;
+    m[i][1] = -s*u+c*v;
+  }
+}
+
+
+/* Multiply a matrix with a vector: o = m * v. */
+static void mult_matrix_vector(float o[4], matrix m, float v[4])
+{
+  int i, j;
+
+  for (i=0; i<4; i++)
+  {
+    o[i] = 0.0f;
+    for (j=0; j<4; j++)
+      o[i] += m[i][j]*v[j];
+  }
+}
+
+
+/* Generate a uniformly distributed random rotation matrix. */
+static void rnd_rot_matrix(matrix m)
+{
+  float theta, phi, z, r, vx, vy, vz, st, ct, sx, sy;
+
+  theta = (float)frand(2.0*M_PI);
+  phi = (float)frand(2.0*M_PI);
+  z = (float)frand(2.0);
+  r  = sqrtf(z);
+  vx = r*sinf(phi);
+  vy = r*cosf(phi);
+  vz = sqrtf(2.0f-z);
+  st = sinf(theta);
+  ct = cosf(theta);
+  sx = vx*ct-vy*st;
+  sy = vx*st+vy*ct;
+
+  identity_matrix(m);
+  m[0][0] = vx*sx-ct;
+  m[0][1] = vx*sy-st;
+  m[0][2] = vx*vz;
+  m[1][0] = vy*sx+st;
+  m[1][1] = vy*sy-ct;
+  m[1][2] = vy*vz;
+  m[2][0] = vz*sx;
+  m[2][1] = vz*sy;
+  m[2][2] = 1.0f-z;
+}
+
+
+/* An easing function for values of t between 0 and max. */
+static inline float ease(float t, float max, int easing)
+{
+  float s, e;
+
+  s = t/max;
+  if (easing == EASING_QUINTIC)
+    e = ((6.0f*s-15.0f)*s+10.0f)*s*s*s;
+  else if (easing == EASING_ACCEL)
+    e = s*s*(2.0f-s);
+  else if (easing == EASING_DECEL)
+    e = s*(1.0f+s*(1.0f-s));
+  else
+    e = 0.0f;
+  return max*e;
+}
+
+
+/* ------------------------------------------------------------------------- */
+
+
+/* Add a rotation around the x axis to the matrix m. */
+static void rotatex(float m[3][3], float phi)
+{
+  float c, s, u, v;
+  int i;
+
+  phi *= M_PI_F/180.0f;
+  c = cosf(phi);
+  s = sinf(phi);
+  for (i=0; i<3; i++)
+  {
+    u = m[i][1];
+    v = m[i][2];
+    m[i][1] = c*u+s*v;
+    m[i][2] = -s*u+c*v;
+  }
+}
+
+
+/* Add a rotation around the y axis to the matrix m. */
+static void rotatey(float m[3][3], float phi)
+{
+  float c, s, u, v;
+  int i;
+
+  phi *= M_PI_F/180.0f;
+  c = cosf(phi);
+  s = sinf(phi);
+  for (i=0; i<3; i++)
+  {
+    u = m[i][0];
+    v = m[i][2];
+    m[i][0] = c*u-s*v;
+    m[i][2] = s*u+c*v;
+  }
+}
+
+
+/* Add a rotation around the z axis to the matrix m. */
+static void rotatez(float m[3][3], float phi)
+{
+  float c, s, u, v;
+  int i;
+
+  phi *= M_PI_F/180.0f;
+  c = cosf(phi);
+  s = sinf(phi);
+  for (i=0; i<3; i++)
+  {
+    u = m[i][0];
+    v = m[i][1];
+    m[i][0] = c*u+s*v;
+    m[i][1] = -s*u+c*v;
+  }
+}
+
+
+/* Compute the rotation matrix m from the rotation angles. */
+static void rotateall(float al, float be, float de, float m[3][3])
+{
+  int i, j;
+
+  for (i=0; i<3; i++)
+    for (j=0; j<3; j++)
+      m[i][j] = (i==j);
+  rotatex(m,al);
+  rotatey(m,be);
+  rotatez(m,de);
+}
+
+
+/* Compute a 4x4 rotation matrix from an xscreensaver unit quaternion. Note
+   that xscreensaver has a different convention for unit quaternions than
+   the one that is used in this hack. */
+static void quat_to_rotmat_trackball(float p[4], float r[16])
+{
+  float al, be, de;
+  float r00, r01, r02, r12, r22;
+  float m[3][3];
+  int   i, j;
+
+  r00 = 1.0f-2.0f*(p[1]*p[1]+p[2]*p[2]);
+  r01 = 2.0f*(p[0]*p[1]+p[2]*p[3]);
+  r02 = 2.0f*(p[2]*p[0]-p[1]*p[3]);
+  r12 = 2.0f*(p[1]*p[2]+p[0]*p[3]);
+  r22 = 1.0f-2.0f*(p[1]*p[1]+p[0]*p[0]);
+
+  al = atan2f(-r12,r22)*180.0f/M_PI_F;
+  be = atan2f(r02,sqrtf(r00*r00+r01*r01))*180.0f/M_PI_F;
+  de = atan2f(-r01,r00)*180.0f/M_PI_F;
+  rotateall(al,be,de,m);
+  for (i=0; i<3; i++)
+  {
+    for (j=0; j<3; j++)
+      r[j*4+i] = m[i][j];
+    r[3*4+i] = 0.0f;
+    r[i*4+3] = 0.0f;
+  }
+  r[3*4+3] = 1.0f;
+}
+
+
+/* ------------------------------------------------------------------------- */
+
+
+/* Create an edge-based graph eg with num_v vertices, num_e edges, and the
+   edges passed in edges. */
+static edge_graph *create_edge_graph(int num_v, int num_e, edge *edges)
+{
+  edge_graph *eg;
+
+  eg = malloc(sizeof(*eg));
+  if (eg == NULL)
+    abort();
+  eg->edges = malloc(num_e*sizeof(*eg->edges));
+  if (eg->edges == NULL)
+    abort();
+  eg->num_v = num_v;
+  eg->num_e = num_e;
+  memcpy(eg->edges,edges,num_e*sizeof(*eg->edges));
+
+  return eg;
+}
+
+
+/* Free the edge-based graph eg. */
+static void free_edge_graph(edge_graph *eg)
+{
+  free(eg->edges);
+  free(eg);
+}
+
+
+/* Create a DSU with num_v vertices. */
+static dsu *create_dsu(int num_v)
+{
+  dsu *d;
+  int i;
+
+  d = malloc(sizeof(*d));
+  if (d == NULL)
+    abort();
+  d->parent = malloc(num_v*sizeof(*d->parent));
+  d->rank = malloc(num_v*sizeof(*d->rank));
+  if (d->parent == NULL || d->rank == NULL)
+    abort();
+  for (i=0; i<num_v; i++)
+  {
+    d->parent[i] = i;
+    d->rank[i] = 0;
+  }
+  return d;
+}
+
+
+/* Free a DSU. */
+static void free_dsu(dsu *d)
+{
+  free(d->parent);
+  free(d->rank);
+  free(d);
+}
+
+
+/* Find the representative (root) of a set with path compression. */
+static int find_root(dsu *d, int x)
+{
+  if (d->parent[x] != x)
+    d->parent[x] = find_root(d,d->parent[x]);
+  return d->parent[x];
+}
+
+
+/* Compute the union of two sets by rank. */
+static void union_sets(dsu *d, int x, int y)
+{
+  int root_x, root_y;
+
+  root_x = find_root(d,x);
+  root_y = find_root(d,y);
+  if (root_x != root_y)
+  {
+    if (d->rank[root_x] < d->rank[root_y])
+    {
+      d->parent[root_x] = root_y;
+    }
+    else if (d->rank[root_x] > d->rank[root_y])
+    {
+      d->parent[root_y] = root_x;
+    }
+    else
+    {
+      d->parent[root_y] = root_x;
+      d->rank[root_x]++;
+    }
+  }
+}
+
+
+/* Compare function for sorting edges by weight. */
+static int compare_edges(const void *a, const void *b)
+{
+  const edge *edge_a, *edge_b;
+
+  edge_a = (edge *)a;
+  edge_b = (edge *)b;
+  if (edge_a->weight > edge_b->weight)
+    return 1;
+  else if (edge_a->weight < edge_b->weight)
+    return -1;
+  else
+    return 0;
+}
+
+
+/* Kruskal's algorithm to find the g->num_v-1 edges of the minimum spanning
+   tree of the edge-based graph g. */
+static edge *minimum_spanning_tree_edges(edge_graph *g)
+{
+  edge *mst_edges, next_edge;
+  dsu *d;
+  int edge_count, i;
+  int src_root, dst_root;
+
+  mst_edges = malloc((g->num_v-1)*sizeof(*mst_edges));
+  if (mst_edges == NULL)
+    abort();
+
+  qsort(g->edges,g->num_e,sizeof(*g->edges),compare_edges);
+
+  d = create_dsu(g->num_v);
+
+  edge_count = 0;
+  i = 0;
+  while (edge_count<g->num_v-1 && i<g->num_e)
+  {
+    next_edge = g->edges[i++];
+    src_root = find_root(d,next_edge.src);
+    dst_root = find_root(d,next_edge.dst);
+
+    if (src_root != dst_root)
+    {
+      mst_edges[edge_count++] = next_edge;
+      union_sets(d,src_root,dst_root);
+    }
+  }
+
+  free_dsu(d);
+
+  return mst_edges;
+}
+
+
+/* Create an adjacenct-list-based graph from a set of edges. */
+static graph_node **create_adj_list_graph_from_edges(const edge *e, int num_v,
+                                                     int num_e)
+{
+  graph_node **ag, *n;
+  int i, src, dst, src_edge, dst_edge;
+
+  ag = malloc(num_v*sizeof(*ag));
+  if (ag == NULL)
+    abort();
+  for (i=0; i<num_v; i++)
+    ag[i] = NULL;
+  for (i=0; i<num_e; i++)
+  {
+    src = e[i].src;
+    dst = e[i].dst;
+    src_edge = e[i].src_edge;
+    dst_edge = e[i].dst_edge;
+    n = malloc(sizeof(*n));
+    if (n == NULL)
+      abort();
+    n->v = src;
+    n->edge_parent = dst_edge;
+    n->edge_self = src_edge;
+    n->next = ag[dst];
+    ag[dst] = n;
+    n = malloc(sizeof(*n));
+    if (n == NULL)
+      abort();
+    n->v = dst;
+    n->edge_parent = src_edge;
+    n->edge_self = dst_edge;
+    n->next = ag[src];
+    ag[src] = n;
+  }
+  return ag;
+}
+
+
+/* Free an adjacenct-list-based graph. */
+static void free_adj_list_graph(graph_node **ag, int num_v)
+{
+  graph_node *a, *an;
+  int i;
+
+  for (i=0; i<num_v; i++)
+  {
+    a = ag[i];
+    while (a != NULL)
+    {
+      an = a->next;
+      free(a);
+      a = an;
+    }
+  }
+  free(ag);
+}
+
+
+/* Create a minimum spanning tree from a adjacency-list-based graph that
+   represents the minimum spanning tree. */
+static tree_node *create_minimum_spanning_tree(graph_node **ag, int num_v)
+{
+  int i, k, id, sp;
+  int *val, *vstack;
+  tree_node **tstack, *t, *tp, *tn, *tree;
+  graph_node *n;
+
+  val = malloc(num_v*sizeof(*val));
+  vstack = malloc(num_v*sizeof(*vstack));
+  tstack = malloc(num_v*sizeof(*tstack));
+  if (val == NULL || vstack == NULL || tstack == NULL)
+    abort();
+
+  for (i=0; i<num_v; i++)
+    val[i] = UNSEEN;
+
+  tree = NULL;
+  id = 0;
+  sp = 0;
+  for (i=0; i<num_v; i++)
+  {
+    if (val[i] == UNSEEN)
+    {
+      t = malloc(sizeof(*t));
+      if (t == NULL)
+        abort();
+      t->next = NULL;
+      t->child = NULL;
+      t->polygon_index = i;
+      t->edge_parent = -1;
+      t->edge_self = -1;
+      tree = t;
+      vstack[sp] = i;
+      tstack[sp] = t;
+      while (sp >= 0)
+      {
+        k = vstack[sp];
+        tp = tstack[sp];
+        sp--;
+        val[k] = id++;
+        tn = NULL;
+        for (n=ag[k]; n!=NULL; n=n->next)
+        {
+          if (val[n->v] == UNSEEN)
+          {
+            t = malloc(sizeof(*t));
+            if (t == NULL)
+              abort();
+            t->next = NULL;
+            t->child = NULL;
+            t->polygon_index = n->v;
+            t->edge_parent = n->edge_parent;
+            t->edge_self = n->edge_self;
+            if (tp->child == NULL)
+            {
+              tp->child = t;
+              tn = t;
+            }
+            else
+            {
+              tn->next = t;
+              tn = t;
+            }
+            sp++;
+            vstack[sp] = n->v;
+            tstack[sp] = t;
+            val[n->v] = SEEN;
+          }
+        }
+      }
+    }
+  }
+
+  free(tstack);
+  free(vstack);
+  free(val);
+
+  return tree;
+}
+
+
+/* Free a tree. */
+static void free_tree(tree_node *t)
+{
+  if (t->child != NULL)
+    free_tree(t->child);
+  if (t->next != NULL)
+    free_tree(t->next);
+  free(t);
+}
+
+
+/* Generate a random polyhedron unfolding of the polyhedron with num_faces
+   faces and num_edges edges represented by the adjacency information of the
+   faces given by adj. */
+static tree_node *create_random_polyhedron_unfolding(int num_faces,
+                                                     int num_edges,
+                                                     edge *adj)
+{
+  edge_graph *eg;
+  edge *mst_edges;
+  graph_node **ag;
+  tree_node *mst;
+  int i;
+
+  /* Create an edge-based adjacency graph from adj. */
+  eg = create_edge_graph(num_faces,num_edges,adj);
+
+  /* Set random weights for the edges in eg. */
+  for (i=0; i<num_edges; i++)
+    eg->edges[i].weight = (float)frand(1.0);
+
+  /* Compute the edges of the minimum spanning tree. */
+  mst_edges = minimum_spanning_tree_edges(eg);
+
+  /* Create an adjacency-list-based graph from the edges of the minimum
+     spanning tree. */
+  ag = create_adj_list_graph_from_edges(mst_edges,num_faces,num_faces-1);
+
+  /* Create an actual minimum spanning tree from the adjacency-list-based
+     graph. */
+  mst = create_minimum_spanning_tree(ag,num_faces);
+
+  free_adj_list_graph(ag,num_faces);
+  free(mst_edges);
+  free_edge_graph(eg);
+
+  return mst;
+}
+
+
+/* ------------------------------------------------------------------------- */
+
+
+/* Compute the fold pose of the base polygon of the node given by node and
+   the angle given by angles at the index node->fold_angle_index.  The
+   function assumes that node->unfold_pose is relative to its parent node
+   has been set by the caller before the call.  Furthermore, the function
+   assumes that node->fold_pose has been initialized with its parent's fold
+   pose.  On output, the function modifies node->fold_pose to store the
+   fold pose of the polygon described by the node. */
+static void compute_fold_pose(tree_node *node, const polygon *base_polygon,
+                              const float *angles, float max_angle)
+{
+  int i, i1, i2;
+  float t[3], mt[3], a[3], b[3], c[3], phi;
+  vertex t1, t2;
+  matrix matr, mats, matst;
+
+  if (node->fold_angle_index >= 0)
+  {
+    phi = angles[node->fold_angle_index];
+    node->fold_angle = ease(phi,max_angle,EASING_QUINTIC);
+
+    i1 = node->edge_self;
+    i2 = (i1+1)%base_polygon->num;
+
+    /* Compute the translation vector.  This is the midpoint of the edge of
+       the polygon that is adjacent to the parent polygon. */
+    mult_matrix_vector(t1,node->unfold_pose,base_polygon->v[i1]);
+    mult_matrix_vector(t2,node->unfold_pose,base_polygon->v[i2]);
+    mid(t,t1,t2);
+    neg(mt,t);
+
+    /* Compute the direction that remains fixed during the rotation.  This
+       is the normalized vector that points from the first to the second
+       vertex of the polygon that is adjacent to the parent polygon. */
+    sub(c,t2,t1);
+    normalize(c);
+
+    /* Compute the first basis vector of the plane in which the rotation is
+       performed.  This is the normalized version of the translation vector
+       that was computed above, except its z coordinate is 0. */
+    a[0] = t[0];
+    a[1] = t[1];
+    a[2] = 0.0f;
+    normalize(a);
+
+    /* Compute the second basis vector of the plane in which the rotation
+       is performed.  This is simply the z direction. */
+    b[0] = 0.0f;
+    b[1] = 0.0f;
+    b[2] = 1.0f;
+
+    /* Insert the three vectors into the matrix mats and its transpose
+       matst. */
+    identity_matrix(mats);
+    identity_matrix(matst);
+    for (i=0; i<3; i++)
+    {
+      mats[i][0] = a[i];
+      mats[i][1] = b[i];
+      mats[i][2] = c[i];
+      matst[0][i] = a[i];
+      matst[1][i] = b[i];
+      matst[2][i] = c[i];
+    }
+
+    /* Compute the rotation matrix that rotates the polygon. */
+    identity_matrix(matr);
+    translate_matrix(matr,t);
+    mult_matrix(matr,mats);
+    rotate_xy_matrix(matr,node->fold_angle);
+    mult_matrix(matr,matst);
+    translate_matrix(matr,mt);
+
+    /* Add the fold angle transformation. */
+    mult_matrix(node->fold_pose,matr);
+  }
+
+  /* Add the unfolding pose to the current polygon. */
+  mult_matrix(node->fold_pose,node->unfold_pose);
+}
+
+
+/* Determine the color data of the polygons, i.e., the colors of the faces
+   and the texture coordinates of its vertices, based on the folded
+   polyhedron. */
+static void determine_polygon_color_data(tree_node *unfolding,
+                                         const polygons *base_polygons,
+                                         float max_angle, matrix color_matrix)
+{
+  float angles[NUM_ANGLES];
+  int i, sp, pindex;
+  vertex tv, c, cr;
+  matrix matp;
+  tree_node *node_stack[MAX_STACK], *node;
+  polygon *poly;
+
+  for (i=0; i<NUM_ANGLES; i++)
+    angles[i] = max_angle;
+
+  sp = 0;
+  node_stack[sp] = unfolding;
+  while (sp >= 0)
+  {
+    node = node_stack[sp--];
+    pindex = node->polygon_index;
+    poly = &base_polygons->poly[pindex];
+
+    /* Initialize the fold pose when processing the root of the tree. */
+    if (node->edge_parent < 0 || node->edge_self < 0)
+      identity_matrix(node->fold_pose);
+
+    /* The parent fold pose is initially stored in the current node.  Save
+       it for later. */
+    copy_matrix(matp,node->fold_pose);
+
+    /* Compute the folding transformation to the current polygon. */
+    compute_fold_pose(node,poly,angles,max_angle);
+
+    /* Transform the polygon, compute the center of the transformed polygon,
+       and normalize it to length 1. */
+    for (i=0; i<3; i++)
+      c[i] = 0.0f;
+    c[3] = 1.0f;
+    for (i=0; i<poly->num; i++)
+    {
+      /* Transform the vertices of the base polygon with the fold
+         transformation. */
+      mult_matrix_vector(tv,node->fold_pose,poly->v[i]);
+      add(c,c,tv);
+      /* Set the 3D texture coordinate of the current vertex to the position
+         of the vertex on a unit sphere. */
+      normalize(tv);
+      copy_vector(poly->t[i],tv);
+    }
+    mult_matrix_vector(cr,color_matrix,c);
+    normalize(cr);
+
+    /* Set the color of the polygon. */
+    for (i=0; i<3; i++)
+      poly->c[i] = 0.5f*(cr[i]+1.0f);
+    poly->c[3] = 1.0f;
+
+    if (node->child != NULL)
+    {
+      copy_matrix(node->child->fold_pose,node->fold_pose);
+      node_stack[++sp] = node->child;
+    }
+    if (node->next != NULL)
+    {
+      copy_matrix(node->next->fold_pose,matp);
+      node_stack[++sp] = node->next;
+    }
+  }
+}
+
+
+/* Determine the poses of the polygons in the unfolding. */
+static void determine_unfolding_poses(tree_node *unfolding,
+                                      const polygons *base_polygons)
+{
+  tree_node *node_stack[MAX_STACK], *node;
+  int sp, p1, p2, s1, s2, ai, pindex;
+  float vp[3], vs[3], c[3], m[3], t[3], dp, phi;
+  polygon *poly;
+
+  ai = 0;
+  sp = 0;
+  node_stack[sp] = unfolding;
+  while (sp >= 0)
+  {
+    node = node_stack[sp--];
+    pindex = node->polygon_index;
+    poly = &base_polygons->poly[pindex];
+
+    if (node->edge_parent >= 0 && node->edge_self >= 0)
+    {
+      node->fold_angle_index = ai++;
+
+      /* The unfold poses are all relative to its parent node.  Therefore,
+         they must be initialized with the identity matrix. */
+      identity_matrix(node->unfold_pose);
+
+      /* The rotation angle phi is determined from the vectors corresponding
+         to the edges that are adjacent to each other in the polyhedron,
+         i.e., the edges that should align in the unfolded state after the
+         transformation of the base polygon. */
+      p1 = node->edge_parent;
+      p2 = (p1+1)%poly->num;
+      s1 = node->edge_self;
+      s2 = (s1+1)%poly->num;
+
+      /* We have to take into account that, since both polygons must be
+         oriented counterclockwise, the edge in the transformed polygon
+         must be taken in the opposite direction to compute the correct
+         rotation angle. */
+      sub(vp,poly->v[p2],poly->v[p1]);
+      sub(vs,poly->v[s1],poly->v[s2]);
+      normalize(vp);
+      normalize(vs);
+      cross(c,vs,vp);
+      dp = dot(vs,vp);
+      phi = (180.0f/M_PI_F)*atan2f(c[2],dp);
+
+      /* Compute the translation vector.  This is the midpoint of the edge
+         in the parent polygon that is adjacent to the transformed polygon,
+         scaled by a factor of 2.  However, since all polygons must lie in
+         one plane in the unfolded state, we must set the z coordinate of
+         the translation to 0. */
+      mid(m,poly->v[p1],poly->v[p2]);
+      m[2] = 0.0f;
+      scale(t,m,2.0f);
+
+      /* Compute the transformation matrix of the current polygon with
+         respect to its parent. */
+      translate_matrix(node->unfold_pose,t);
+      rotate_xy_matrix(node->unfold_pose,phi);
+    }
+    else
+    {
+      node->fold_angle_index = -1;
+
+      /* The unfold pose for the root of the tree is the identity matrix by
+         definition. */
+      identity_matrix(node->unfold_pose);
+    }
+
+    if (node->child != NULL)
+      node_stack[++sp] = node->child;
+    if (node->next != NULL)
+      node_stack[++sp] = node->next;
+  }
+}
+
+
+/* Initialize the base polygons. */
+static polygons *init_base_polygons(tree_node *unfolding,
+                                    const polygon *base_polygon)
+{
+  tree_node *node_stack[MAX_STACK], *node;
+  int i, num_poly, sp;
+  polygon *poly;
+  polygons *polys;
+
+  /* Count the number of polygons in the unfolding. */
+  num_poly = 0;
+  sp = 0;
+  node_stack[sp] = unfolding;
+  while (sp >= 0)
+  {
+    node = node_stack[sp--];
+    num_poly++;
+    if (node->child != NULL)
+      node_stack[++sp] = node->child;
+    if (node->next != NULL)
+      node_stack[++sp] = node->next;
+  }
+
+  polys = malloc(sizeof(*polys));
+  poly = malloc(num_poly*sizeof(*poly));
+  if (polys == NULL || poly == NULL)
+    abort();
+
+  polys->poly = poly;
+  polys->num = num_poly;
+
+  /* Copy the base polygon to all base polygons and set the texture
+     coordinates and color to 0. */
+  for (i=0; i<num_poly; i++)
+  {
+    poly[i].v = malloc(base_polygon->num*sizeof(*poly[i].v));
+    poly[i].t = malloc(base_polygon->num*sizeof(*poly[i].t));
+    if (poly[i].v == NULL || poly[i].t == NULL)
+      abort();
+    poly[i].num = base_polygon->num;
+    memcpy(poly[i].v,base_polygon->v,base_polygon->num*sizeof(*poly[i].v));
+    memcpy(poly[i].n,base_polygon->n,sizeof(poly[i].n));
+    memset(poly[i].t,0,base_polygon->num*sizeof(*poly[i].t));
+    memset(poly[i].c,0,sizeof(poly[i].c));
+  }
+
+  return polys;
+}
+
+
+/* Free the base polygons. */
+static void free_base_polygons(polygons *base_polygons)
+{
+  int i;
+
+  for (i=0; i<base_polygons->num; i++)
+  {
+    free(base_polygons->poly[i].v);
+    free(base_polygons->poly[i].t);
+  }
+  free(base_polygons->poly);
+  free(base_polygons);
+}
+
+
+/* Initialize the color matrix that determines the random face colors. */
+static void init_color_matrix(matrix color_matrix)
+{
+  rnd_rot_matrix(color_matrix);
+}
+
+
+/* Initialize the polygon unfolding for a polyhedron of type poly. */
+static void init_polygon_unfolding(ModeInfo *mi, int poly)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+
+  switch (poly)
+  {
+    case TETRAHEDRON:
+      free_base_polygons(pf->base_polygons);
+      free_tree(pf->polygon_unfolding);
+      pf->polygon_unfolding =
+        create_random_polyhedron_unfolding(TETRAHEDRON_NUM_FACES,
+                                           TETRAHEDRON_NUM_EDGES,
+                                           tetrahedron_edges);
+      pf->max_angle = TETRAHEDRON_MAX_ANGLE;
+      pf->delta_angle = TETRAHEDRON_DELTA_ANGLE;
+      pf->num_fold_angles = TETRAHEDRON_NUM_FACES-1;
+      pf->eye_pos = tetrahedron_eye_pos;
+      pf->base_polygons = init_base_polygons(pf->polygon_unfolding,
+                                             &tetrahedron_triangle);
+      determine_unfolding_poses(pf->polygon_unfolding,pf->base_polygons);
+      determine_polygon_color_data(pf->polygon_unfolding,pf->base_polygons,
+                                   pf->max_angle,pf->color_matrix);
+      break;
+    case HEXAHEDRON:
+      free_base_polygons(pf->base_polygons);
+      free_tree(pf->polygon_unfolding);
+      pf->polygon_unfolding =
+        create_random_polyhedron_unfolding(HEXAHEDRON_NUM_FACES,
+                                           HEXAHEDRON_NUM_EDGES,
+                                           hexahedron_edges);
+      pf->max_angle = HEXAHEDRON_MAX_ANGLE;
+      pf->delta_angle = HEXAHEDRON_DELTA_ANGLE;
+      pf->num_fold_angles = HEXAHEDRON_NUM_FACES-1;
+      pf->eye_pos = hexahedron_eye_pos;
+      pf->base_polygons = init_base_polygons(pf->polygon_unfolding,
+                                             &hexahedron_square);
+      determine_unfolding_poses(pf->polygon_unfolding,pf->base_polygons);
+      determine_polygon_color_data(pf->polygon_unfolding,pf->base_polygons,
+                                   pf->max_angle,pf->color_matrix);
+      break;
+    case OCTAHEDRON:
+      free_base_polygons(pf->base_polygons);
+      free_tree(pf->polygon_unfolding);
+      pf->polygon_unfolding =
+        create_random_polyhedron_unfolding(OCTAHEDRON_NUM_FACES,
+                                           OCTAHEDRON_NUM_EDGES,
+                                           octahedron_edges);
+      pf->max_angle = OCTAHEDRON_MAX_ANGLE;
+      pf->delta_angle = OCTAHEDRON_DELTA_ANGLE;
+      pf->num_fold_angles = OCTAHEDRON_NUM_FACES-1;
+      pf->eye_pos = octahedron_eye_pos;
+      pf->base_polygons = init_base_polygons(pf->polygon_unfolding,
+                                             &octahedron_triangle);
+      determine_unfolding_poses(pf->polygon_unfolding,pf->base_polygons);
+      determine_polygon_color_data(pf->polygon_unfolding,pf->base_polygons,
+                                   pf->max_angle,pf->color_matrix);
+      break;
+    case DODECAHEDRON:
+      free_base_polygons(pf->base_polygons);
+      free_tree(pf->polygon_unfolding);
+      pf->polygon_unfolding =
+        create_random_polyhedron_unfolding(DODECAHEDRON_NUM_FACES,
+                                           DODECAHEDRON_NUM_EDGES,
+                                           dodecahedron_edges);
+      pf->max_angle = DODECAHEDRON_MAX_ANGLE;
+      pf->delta_angle = DODECAHEDRON_DELTA_ANGLE;
+      pf->num_fold_angles = DODECAHEDRON_NUM_FACES-1;
+      pf->eye_pos = dodecahedron_eye_pos;
+      pf->base_polygons = init_base_polygons(pf->polygon_unfolding,
+                                             &dodecahedron_pentagon);
+      determine_unfolding_poses(pf->polygon_unfolding,pf->base_polygons);
+      determine_polygon_color_data(pf->polygon_unfolding,pf->base_polygons,
+                                   pf->max_angle,pf->color_matrix);
+      break;
+    case ICOSAHEDRON:
+      free_base_polygons(pf->base_polygons);
+      free_tree(pf->polygon_unfolding);
+      pf->polygon_unfolding =
+        create_random_polyhedron_unfolding(ICOSAHEDRON_NUM_FACES,
+                                           ICOSAHEDRON_NUM_EDGES,
+                                           icosahedron_edges);
+      pf->max_angle = ICOSAHEDRON_MAX_ANGLE;
+      pf->delta_angle = ICOSAHEDRON_DELTA_ANGLE;
+      pf->num_fold_angles = ICOSAHEDRON_NUM_FACES-1;
+      pf->eye_pos = icosahedron_eye_pos;
+      pf->base_polygons = init_base_polygons(pf->polygon_unfolding,
+                                             &icosahedron_triangle);
+      determine_unfolding_poses(pf->polygon_unfolding,pf->base_polygons);
+      determine_polygon_color_data(pf->polygon_unfolding,pf->base_polygons,
+                                   pf->max_angle,pf->color_matrix);
+      break;
+  }
+}
+
+
+/* ------------------------------------------------------------------------- */
+
+
+/* Generate the vertex, normal, color, and texture buffers for a polygon
+   folding. */
+static int gen_polygon_folding_data(tree_node *unfolding,
+                                    const polygons *base_polygons,
+                                    const float *angles, float max_angle,
+                                    vertex *vtx, normal *nrm, color *col,
+                                    texcoord *tex, int *num)
+{
+  int i, j, n, num_poly, sp, pindex;
+  vertex tv, tn;
+  matrix matp;
+  tree_node *node_stack[MAX_STACK], *node;
+  polygon *poly;
+
+  num_poly = 0;
+  n = 0;
+  sp = 0;
+  node_stack[sp] = unfolding;
+  while (sp >= 0)
+  {
+    node = node_stack[sp--];
+    pindex = node->polygon_index;
+    poly = &base_polygons->poly[pindex];
+
+    /* Initialize the fold pose when processing the root of the tree. */
+    if (node->edge_parent < 0 || node->edge_self < 0)
+      identity_matrix(node->fold_pose);
+
+    /* The parent's fold pose is initially stored in the current node.  Save
+       it for later. */
+    copy_matrix(matp,node->fold_pose);
+
+    /* Compute the folding transformation to the current polygon. */
+    compute_fold_pose(node,poly,angles,max_angle);
+
+    /* Transform the normal vector of the polygon.  Note that we assume
+       that node->fold_pose is a rigid transformation.  Otherwise, we would
+       have to compute and use its inverse transpose. */
+    mult_matrix_vector(tn,node->fold_pose,poly->n);
+
+    for (i=0; i<poly->num; i++)
+    {
+      /* Transform the vertices of the base polygon with the fold
+         transformation. */
+      mult_matrix_vector(tv,node->fold_pose,poly->v[i]);
+
+      for (j=0; j<4; j++)
+        vtx[n][j] = tv[j];
+      for (j=0; j<4; j++)
+        nrm[n][j] = tn[j];
+      for (j=0; j<4; j++)
+        col[n][j] = poly->c[j];
+      for (j=0; j<3; j++)
+        tex[n][j] = poly->t[i][j];
+      n++;
+    }
+
+    num[num_poly] = poly->num;
+    num_poly++;
+
+    if (node->child != NULL)
+    {
+      copy_matrix(node->child->fold_pose,node->fold_pose);
+      node_stack[++sp] = node->child;
+    }
+    if (node->next != NULL)
+    {
+      copy_matrix(node->next->fold_pose,matp);
+      node_stack[++sp] = node->next;
+    }
+  }
+
+  return num_poly;
+}
+
+
+/* Draw the polygon folding using OpenGL's fixed functionality. */
+static int polygon_folding_ff(ModeInfo *mi)
+{
+  static const GLfloat light_ambient[] = { 0.3f, 0.3f, 0.3f, 1.0f };
+  static const GLfloat light_diffuse[] = { 0.7f, 0.7f, 0.7f, 1.0f };
+  static const GLfloat light_specular[] = { 0.75f, 0.75f, 0.75f, 1.0f };
+  static const GLfloat light_pos[] = { 1.0f, 1.0f, 1.0f, 0.0f };
+  static const GLfloat mat_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+  float qu[4], qr[16];
+  int i, j, n, num_poly;
+
+  /* Collect the polygon data to draw. */
+  num_poly = gen_polygon_folding_data(pf->polygon_unfolding,pf->base_polygons,
+                                      pf->angle,pf->max_angle,pf->vtx,
+                                      pf->nrm,pf->col,pf->tex,pf->num);
+
+  glViewport(0,0,pf->WindW,pf->WindH);
+
+  glClearColor(0.0f,0.0f,0.0f,0.0f);
+  glClearDepth(1.0f);
+  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
+
+  glMatrixMode(GL_PROJECTION);
+  glLoadIdentity();
+  if (pf->aspect >= 1.0f)
+    gluPerspective(45.0,pf->aspect,0.1,30.0);
+  else
+    gluPerspective(360.0/M_PI*atan(tan(45.0*M_PI/360.0)/pf->aspect),
+                   pf->aspect,0.1,30.0);
+
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+
+  /* Note: in the unfolded state, all visible, i.e, front-facing, triangles
+     are oriented counterclockwise.  This means that in the folded state,
+     all visible triangles, i.e., the triangles that describe the surface of
+     the polyhedron, are back-facing.  Therefore, we must disable face
+     culling. */
+  glFrontFace(GL_CCW);
+  glDisable(GL_CULL_FACE);
+  glEnable(GL_DEPTH_TEST);
+  glDepthFunc(GL_LESS);
+  glDepthMask(GL_TRUE);
+  glDisable(GL_BLEND);
+  glShadeModel(GL_SMOOTH);
+  glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
+  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_TRUE);
+  glEnable(GL_LIGHTING);
+  glEnable(GL_LIGHT0);
+  glLightfv(GL_LIGHT0,GL_AMBIENT,light_ambient);
+  glLightfv(GL_LIGHT0,GL_DIFFUSE,light_diffuse);
+  glLightfv(GL_LIGHT0,GL_SPECULAR,light_specular);
+  glLightfv(GL_LIGHT0,GL_POSITION,light_pos);
+  glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,mat_specular);
+  glMaterialf(GL_FRONT_AND_BACK,GL_SHININESS,30.0f);
+
+  gltrackball_get_quaternion(pf->trackball,qu);
+  quat_to_rotmat_trackball(qu,qr);
+
+  gluLookAt(pf->eye_pos[0],pf->eye_pos[1],pf->eye_pos[2],0.0,0.0,0.0,
+            0.0,1.0,0.0);
+  glTranslatef(pf->poly_pos[0],pf->poly_pos[1],pf->poly_pos[2]);
+  glMultMatrixf(qr);
+  glRotatef(pf->alpha,1.0f,0.0f,0.0f);
+  glRotatef(pf->beta,0.0f,1.0f,0.0f);
+  glRotatef(pf->delta,0.0f,0.0f,1.0f);
+
+  /* Draw the collected polygons. */
+  n = 0;
+  for (i=0; i<num_poly; i++)
+  {
+    glBegin(GL_TRIANGLE_FAN);
+    for (j=0; j<pf->num[i]; j++)
+    {
+      glColor3fv(pf->col[n]);
+      glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE,pf->col[n]);
+      glNormal3fv(pf->nrm[n]);
+      glVertex4fv(pf->vtx[n]);
+      n++;
+    }
+    glEnd();
+  }
+
+  return num_poly;
+}
+
+
+#ifdef HAVE_GLSL
+
+/* Draw the polygon folding using OpenGL's programmable functionality. */
+static int polygon_folding_pf(ModeInfo *mi)
+{
+  static const GLfloat light_model_amb[] = { 0.2f, 0.2f, 0.2f, 1.0f };
+  static const GLfloat light_ambient[] = { 0.3f, 0.3f, 0.3f, 1.0f };
+  static const GLfloat light_diffuse[] = { 0.7f, 0.7f, 0.7f, 1.0f };
+  static const GLfloat light_model_amb_earth[] = { 0.3f, 0.3f, 0.3f, 1.0f };
+  static const GLfloat light_ambient_earth[] = { 0.5f, 0.5f, 0.5f, 1.0f };
+  static const GLfloat light_diffuse_earth[] = { 0.5f, 0.5f, 0.5f, 1.0f };
+  static const GLfloat light_specular[] = { 0.75f, 0.75f, 0.75f, 1.0f };
+  static const GLfloat light_pos[] = { 1.0f, 1.0f, 1.0f, 0.0f };
+  static const GLfloat mat_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+  static const GLfloat mat_diff_white[] = { 1.0f, 1.0f, 1.0f, 1.0f };
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+  float p_mat[16], mv_mat[16], sun_pos[4];
+  float qu[4], qr[16];
+  float light_direction[4], half_vector[3];
+  int i, n, num_poly;
+
+  /* Collect the polygon data to draw. */
+  num_poly = gen_polygon_folding_data(pf->polygon_unfolding,pf->base_polygons,
+                                      pf->angle,pf->max_angle,pf->vtx,
+                                      pf->nrm,pf->col,pf->tex,pf->num);
+
+  /* Count the number of elements in the polygon data. */
+  n = 0;
+  for (i=0; i<num_poly; i++)
+    n += pf->num[i];
+
+  /* Copy the collected polygon data to the respective buffer. */
+  glBindBuffer(GL_ARRAY_BUFFER,pf->vertex_buffer);
+  glBufferData(GL_ARRAY_BUFFER,4*n*sizeof(GLfloat),pf->vtx,GL_STREAM_DRAW);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
+
+  glBindBuffer(GL_ARRAY_BUFFER,pf->normal_buffer);
+  glBufferData(GL_ARRAY_BUFFER,4*n*sizeof(GLfloat),pf->nrm,GL_STREAM_DRAW);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
+
+  glBindBuffer(GL_ARRAY_BUFFER,pf->color_buffer);
+  glBufferData(GL_ARRAY_BUFFER,4*n*sizeof(GLfloat),pf->col,GL_STREAM_DRAW);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
+
+  glBindBuffer(GL_ARRAY_BUFFER,pf->tex_buffer);
+  glBufferData(GL_ARRAY_BUFFER,3*n*sizeof(GLfloat),pf->tex,GL_STREAM_DRAW);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
+
+  glsl_Identity(p_mat);
+  if (pf->aspect >= 1.0f)
+    glsl_Perspective(p_mat,45.0f,pf->aspect,0.1f,30.0f);
+  else
+    glsl_Perspective(p_mat,
+                     360.0f/M_PI_F*atanf(tanf(45.0f*M_PI_F/360.0f)/
+                                         pf->aspect),
+                     pf->aspect,0.1f,30.0f);
+
+  gltrackball_get_quaternion(pf->trackball,qu);
+  quat_to_rotmat_trackball(qu,qr);
+
+  glsl_Identity(mv_mat);
+  glsl_LookAt(mv_mat,pf->eye_pos[0],pf->eye_pos[1],pf->eye_pos[2],
+              0.0f,0.0f,0.0f,0.0f,1.0f,0.0f);
+  glsl_Translate(mv_mat,pf->poly_pos[0],pf->poly_pos[1],pf->poly_pos[2]);
+  glsl_MultMatrix(mv_mat,qr);
+  glsl_Rotate(mv_mat,pf->alpha,1.0f,0.0f,0.0f);
+  glsl_Rotate(mv_mat,pf->beta,0.0f,1.0f,0.0f);
+  glsl_Rotate(mv_mat,pf->delta,0.0f,0.0f,1.0f);
+
+  if (!pf->use_textures)
+  {
+    light_direction[0] = light_pos[0];
+    light_direction[1] = light_pos[1];
+    light_direction[2] = light_pos[2];
+    light_direction[3] = 0.0f;
+    normalize(light_direction);
+    half_vector[0] = light_direction[0];
+    half_vector[1] = light_direction[1];
+    half_vector[2] = light_direction[2]+1.0f;
+    normalize(half_vector);
+  }
+  else
+  {
+    get_sun_direction(get_current_julian_date(),sun_pos);
+    if (!pf->north_up)
+    {
+      sun_pos[0] = -sun_pos[0];
+      sun_pos[2] = -sun_pos[2];
+    }
+    glsl_MultMatrixVector(light_direction,mv_mat,sun_pos);
+    normalize(light_direction);
+    half_vector[0] = light_direction[0];
+    half_vector[1] = light_direction[1];
+    half_vector[2] = light_direction[2];
+    normalize(half_vector);
+  }
+
+  glUseProgram(pf->shader_program);
+
+  glViewport(0,0,pf->WindW,pf->WindH);
+
+  glClearColor(0.0f,0.0f,0.0f,0.0f);
+  glClearDepth(1.0f);
+  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
+
+  glFrontFace(GL_CCW);
+  glDisable(GL_CULL_FACE);
+  glEnable(GL_DEPTH_TEST);
+  glDepthFunc(GL_LESS);
+  glDepthMask(GL_TRUE);
+  glDisable(GL_BLEND);
+
+  if (!pf->use_textures)
+  {
+    glUniform4fv(pf->glbl_ambient_index,1,light_model_amb);
+    glUniform4fv(pf->lt_ambient_index,1,light_ambient);
+    glUniform4fv(pf->lt_diffuse_index,1,light_diffuse);
+  }
+  else
+  {
+    glUniform4fv(pf->glbl_ambient_index,1,light_model_amb_earth);
+    glUniform4fv(pf->lt_ambient_index,1,light_ambient_earth);
+    glUniform4fv(pf->lt_diffuse_index,1,light_diffuse_earth);
+  }
+  glUniform4fv(pf->lt_specular_index,1,light_specular);
+  glUniform3fv(pf->lt_direction_index,1,light_direction);
+  glUniform3fv(pf->lt_halfvect_index,1,half_vector);
+  glUniform4fv(pf->ambient_index,1,mat_diff_white);
+  glUniform4fv(pf->diffuse_index,1,mat_diff_white);
+  glUniform4fv(pf->specular_index,1,mat_specular);
+  glUniform1f(pf->shininess_index,30.0f);
+  glUniform1i(pf->north_up_index,pf->north_up);
+  glUniform1i(pf->bool_textures_index,pf->use_textures);
+  glUniform3fv(pf->magma_offs_index,1,pf->magma_tex_offset);
+
+  glUniformMatrix4fv(pf->proj_index,1,GL_FALSE,p_mat);
+  glUniformMatrix4fv(pf->mv_index,1,GL_FALSE,mv_mat);
+
+  if (pf->textures_supported)
+  {
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_3D,pf->magma_tex);
+    glUniform1i(pf->samp_mgm_index,0);
+
+    glActiveTexture(GL_TEXTURE1);
+    glBindTexture(GL_TEXTURE_2D,pf->earth_tex[0]);
+    glUniform1i(pf->samp_day_index,1);
+
+    glActiveTexture(GL_TEXTURE2);
+    glBindTexture(GL_TEXTURE_2D,pf->earth_tex[1]);
+    glUniform1i(pf->samp_ngt_index,2);
+
+    glActiveTexture(GL_TEXTURE3);
+    glBindTexture(GL_TEXTURE_2D,pf->earth_tex[2]);
+    glUniform1i(pf->samp_wtr_index,3);
+  }
+
+  /* Draw the collected polygons. */
+  glEnableVertexAttribArray(pf->pos_index);
+  glBindBuffer(GL_ARRAY_BUFFER,pf->vertex_buffer);
+  glVertexAttribPointer(pf->pos_index,4,GL_FLOAT,GL_FALSE,0,0);
+
+  glEnableVertexAttribArray(pf->normal_index);
+  glBindBuffer(GL_ARRAY_BUFFER,pf->normal_buffer);
+  glVertexAttribPointer(pf->normal_index,4,GL_FLOAT,GL_FALSE,0,0);
+
+  glEnableVertexAttribArray(pf->color_index);
+  glBindBuffer(GL_ARRAY_BUFFER,pf->color_buffer);
+  glVertexAttribPointer(pf->color_index,4,GL_FLOAT,GL_FALSE,0,0);
+
+  glEnableVertexAttribArray(pf->tex_index);
+  glBindBuffer(GL_ARRAY_BUFFER,pf->tex_buffer);
+  glVertexAttribPointer(pf->tex_index,3,GL_FLOAT,GL_FALSE,0,0);
+
+  n = 0;
+  for (i=0; i<num_poly; i++)
+  {
+    glDrawArrays(GL_TRIANGLE_FAN,n,pf->num[i]);
+    n += pf->num[i];
+  }
+
+  glDisableVertexAttribArray(pf->pos_index);
+  glDisableVertexAttribArray(pf->normal_index);
+  glDisableVertexAttribArray(pf->color_index);
+  glDisableVertexAttribArray(pf->tex_index);
+  glBindBuffer(GL_ARRAY_BUFFER,0);
+
+  glUseProgram(0);
+
+  return num_poly;
+}
+
+#endif /* HAVE_GLSL */
+
+
+/* Display the Platonic solid unfolding and folding. */
+static void display_platonicfolding(ModeInfo *mi)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+  int i, poly;
+  bool change_dir;
+  float t;
+
+  if (!pf->button_pressed)
+  {
+    if (pf->anim_state == ANIM_INIT)
+    {
+#ifdef HAVE_GLSL
+      /* Select whether to use textures.  Textures are used in one third of
+         the animations if the user has selected random colors. */
+      if (pf->random_colors)
+        pf->use_textures = pf->textures_supported && frand(1.0) < 1.0/3.0;
+      else
+        pf->use_textures = (pf->colors == COLORS_EARTH);
+      
+      /* Select a random offset for the magma texture. */
+      pf->magma_tex_offset[0] = 0.2f*((float)frand(1.0)-0.5f);
+      pf->magma_tex_offset[1] = 0.2f*((float)frand(1.0)-0.5f);
+      pf->magma_tex_offset[2] = 0.2f*((float)frand(1.0)-0.5f);
+#endif /* HAVE_GLSL */
+
+      /* Select whether the north or south pole are facing upwards in the
+         texture.  Both cases occur with equal probability. */
+      pf->north_up = frand(1.0) < 0.5;
+
+      /* Select whether to fold upwards or downwards.  Both cases occur with
+         equal probability. */
+      pf->alpha = frand(1.0) < 0.5 ? 300.0f : 120.0f;
+
+      /* Set the rotation around the y axis to 0. */
+      pf->beta = 0.0f;
+
+      /* Initialize the rotation around the z axis to a random angle. */
+      pf->delta = (float)frand(360.0);
+
+      /* Set the angle increment for the rotation around the z axis. */
+      if (pf->north_up)
+        pf->delta_delta = 0.5f;
+      else
+        pf->delta_delta = -0.5f;
+
+      /* Select a random polyhedron that is different from the last
+         polyhedron. */
+      do
+        poly = (int)floor(frand(5.0));
+      while (poly == pf->anim_poly);
+      pf->anim_poly = poly;
+      init_color_matrix(pf->color_matrix);
+      init_polygon_unfolding(mi,pf->anim_poly);
+      if (pf->num_foldings == RANDOM_NUM_FOLDINGS)
+      {
+        if (pf->anim_poly == TETRAHEDRON)
+          pf->anim_remaining = 3+(int)floor(frand(3.0));
+        else if (pf->anim_poly == HEXAHEDRON || pf->anim_poly == OCTAHEDRON)
+          pf->anim_remaining = 4+(int)floor(frand(5.0));
+        else /* pf->anim_poly==DODECAHEDRON || pf->anim_poly==ICOSAHEDRON */
+          pf->anim_remaining = 6+(int)floor(frand(7.0));
+      }
+      else
+      {
+        pf->anim_remaining = pf->num_foldings;
+      }
+
+      /* Set all angles to the folded state. */
+      for (i=0; i<pf->num_fold_angles; i++)
+        pf->angle[i] = pf->max_angle;
+      for (; i<NUM_ANGLES; i++)
+        pf->angle[i] = 0.0f;
+
+      /* Initialize the fold angle index. */
+      pf->fold_angle = 0;
+
+      /* Set the appear and disappear parameters of the polyhedron. */
+      if (pf->aspect >= 1.0f)
+      {
+        pf->spoly_pos[0] = 0.0f;
+        pf->spoly_pos[1] = -pf->eye_pos[2]*(2.0f/3.0f);
+        pf->spoly_pos[2] = 0.0f;
+        pf->dpoly_pos[0] = 0.0f;
+        pf->dpoly_pos[1] = pf->eye_pos[2]*(2.0f/3.0f);
+        pf->dpoly_pos[2] = 0.0f;
+      }
+      else
+      {
+        pf->spoly_pos[0] = -pf->eye_pos[2]*(2.0f/3.0f);
+        pf->spoly_pos[1] = 0.0f;
+        pf->spoly_pos[2] = 0.0f;
+        pf->dpoly_pos[0] = pf->eye_pos[2]*(2.0f/3.0f);
+        pf->dpoly_pos[1] = 0.0f;
+        pf->dpoly_pos[2] = 0.0f;
+      }
+      pf->poly_pos[0] = pf->spoly_pos[0];
+      pf->poly_pos[1] = pf->spoly_pos[1];
+      pf->poly_pos[2] = pf->spoly_pos[2];
+
+      pf->anim_num_steps = 180;
+      pf->anim_step = 0;
+      pf->anim_state = ANIM_APPEAR;
+    }
+
+    if (pf->anim_state == ANIM_APPEAR)
+    {
+      t = (float)pf->anim_step/(float)pf->anim_num_steps;
+      t = ease(t,1.0f,EASING_DECEL);
+      pf->poly_pos[0] = pf->spoly_pos[0]+t*pf->dpoly_pos[0];
+      pf->poly_pos[1] = pf->spoly_pos[1]+t*pf->dpoly_pos[1];
+      pf->poly_pos[2] = pf->spoly_pos[2]+t*pf->dpoly_pos[2];
+
+      if (pf->rotate)
+        pf->delta += pf->delta_delta;
+
+      pf->anim_step++;
+      if (pf->anim_step > pf->anim_num_steps)
+      {
+        /* Select the unfolding mode.  Separate unfolding is used in one
+           fourth of the cases. */
+        pf->anim_state =
+          frand(1.0) < 0.25 ? ANIM_UNFOLD_SEP : ANIM_UNFOLD_JNT;
+      }
+    }
+    else if (pf->anim_state == ANIM_DISAPPEAR)
+    {
+      t = (float)pf->anim_step/(float)pf->anim_num_steps;
+      t = 1.0f+ease(t,1.0f,EASING_ACCEL);
+      pf->poly_pos[0] = pf->spoly_pos[0]+t*pf->dpoly_pos[0];
+      pf->poly_pos[1] = pf->spoly_pos[1]+t*pf->dpoly_pos[1];
+      pf->poly_pos[2] = pf->spoly_pos[2]+t*pf->dpoly_pos[2];
+
+      if (pf->rotate)
+        pf->delta += pf->delta_delta;
+
+      pf->anim_step++;
+      if (pf->anim_step > pf->anim_num_steps)
+        pf->anim_state = ANIM_INIT;
+    }
+    else if (pf->anim_state == ANIM_UNFOLD_JNT)
+    {
+      change_dir = false;
+      for (i=0; i<pf->num_fold_angles; i++)
+      {
+        pf->angle[i] -= pf->delta_angle/3.0f;
+        if (pf->angle[i] < 0.0f)
+        {
+          pf->angle[i] = 0.0f;
+          change_dir = true;
+        }
+      }
+
+      if (pf->rotate)
+        pf->delta += pf->delta_delta;
+
+      if (change_dir)
+        pf->anim_state = ANIM_FOLD_JNT;
+    }
+    else if (pf->anim_state == ANIM_FOLD_JNT)
+    {
+      change_dir = false;
+      for (i=0; i<pf->num_fold_angles; i++)
+      {
+        pf->angle[i] += pf->delta_angle/3.0f;
+        if (pf->angle[i] > pf->max_angle)
+        {
+          pf->angle[i] = pf->max_angle;
+          change_dir = true;
+        }
+      }
+
+      if (pf->rotate)
+        pf->delta += pf->delta_delta;
+
+      if (change_dir)
+      {
+        pf->anim_remaining--;
+        if (pf->anim_remaining > 0)
+        {
+          /* Create a new unfolding of the current polhedron type. */
+          init_polygon_unfolding(mi,pf->anim_poly);
+          /* Select the unfolding mode.  Separate unfolding is used in one
+             fourth of the cases. */
+          pf->anim_state =
+            frand(1.0) < 0.25 ? ANIM_UNFOLD_SEP : ANIM_UNFOLD_JNT;
+        }
+        else
+        {
+          pf->anim_num_steps = 180;
+          pf->anim_step = 0;
+          pf->anim_state = ANIM_DISAPPEAR;
+        }
+      }
+    }
+    else if (pf->anim_state == ANIM_UNFOLD_SEP)
+    {
+      change_dir = false;
+      pf->angle[pf->fold_angle] -= pf->delta_angle;
+      if (pf->angle[pf->fold_angle] < 0.0f)
+      {
+        pf->angle[pf->fold_angle] = 0.0f;
+        pf->fold_angle++;
+        if (pf->fold_angle >= pf->num_fold_angles)
+        {
+          pf->fold_angle = pf->num_fold_angles-1;
+          change_dir = true;
+        }
+      }
+
+      if (pf->rotate)
+        pf->delta += pf->delta_delta;
+
+      if (change_dir)
+        pf->anim_state = ANIM_FOLD_SEP;
+    }
+    else if (pf->anim_state == ANIM_FOLD_SEP)
+    {
+      change_dir = false;
+      pf->angle[pf->fold_angle] += pf->delta_angle;
+      if (pf->angle[pf->fold_angle] > pf->max_angle)
+      {
+        pf->angle[pf->fold_angle] = pf->max_angle;
+        pf->fold_angle--;
+        if (pf->fold_angle < 0)
+        {
+          pf->fold_angle = 0;
+          change_dir = true;
+        }
+      }
+
+      if (pf->rotate)
+        pf->delta += pf->delta_delta;
+
+      if (change_dir)
+      {
+        pf->anim_remaining--;
+        if (pf->anim_remaining > 0)
+        {
+          /* Create a new unfolding of the current polhedron type. */
+          init_polygon_unfolding(mi,pf->anim_poly);
+          /* Select the unfolding mode.  Separate unfolding is used in one
+             fourth of the cases. */
+          pf->anim_state =
+            frand(1.0) < 0.25 ? ANIM_UNFOLD_SEP : ANIM_UNFOLD_JNT;
+        }
+        else
+        {
+          pf->anim_num_steps = 180;
+          pf->anim_step = 0;
+          pf->anim_state = ANIM_DISAPPEAR;
+        }
+      }
+    }
+  }
+
+  gltrackball_rotate(pf->trackball);
+
+#ifdef HAVE_GLSL
+  if (pf->use_shaders)
+    mi->polygon_count = polygon_folding_pf(mi);
+  else
+#endif /* HAVE_GLSL */
+    mi->polygon_count = polygon_folding_ff(mi);
+}
+
+
+
+/* ------------------------------------------------------------------------- */
+
+
+#ifdef HAVE_GLSL
+
+
+/* Set up and enable texturing on our object */
+static void setup_xpm_texture(ModeInfo *mi, const unsigned char *data,
+                              unsigned long size)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+  XImage *image;
+  GLint max_aniso, aniso;
+
+  image = image_data_to_ximage(MI_DISPLAY(mi),MI_VISUAL(mi),data,size);
+#ifndef HAVE_JWZGLES
+  glEnable(GL_TEXTURE_2D);
+#endif
+  glPixelStorei(GL_UNPACK_ALIGNMENT,1);
+  glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,image->width,image->height,0,GL_RGBA,
+               GL_UNSIGNED_BYTE,image->data);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
+  if (pf->use_mipmaps)
+  {
+    glGenerateMipmap(GL_TEXTURE_2D);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL,4);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,
+                    GL_LINEAR_MIPMAP_LINEAR);
+  }
+  else
+  {
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
+  }
+  if (pf->aniso_textures)
+  {
+    glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT,&max_aniso);
+    aniso = MIN(max_aniso,10);
+    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_ANISOTROPY_EXT,aniso);
+  }
+  XDestroyImage(image);
+}
+
+
+/* Generate the textures that show the the earth by day and night. */
+static void gen_earth_textures(ModeInfo *mi)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+
+  glGenTextures(3,pf->earth_tex);
+
+  /* Set up the earth by day texture. */
+  glBindTexture(GL_TEXTURE_2D,pf->earth_tex[0]);
+  setup_xpm_texture(mi,earth_png,sizeof(earth_png));
+
+  /* Set up the earth by night texture. */
+  glBindTexture(GL_TEXTURE_2D,pf->earth_tex[1]);
+  setup_xpm_texture(mi,earth_night_png,sizeof(earth_night_png));
+
+  /* Set up the earth water texture. */
+  glBindTexture(GL_TEXTURE_2D,pf->earth_tex[2]);
+  setup_xpm_texture(mi,earth_water_png,sizeof(earth_water_png));
+
+  glBindTexture(GL_TEXTURE_2D,0);
+}
+
+
+/* Generate the magma texture. */
+static void gen_magma_texture(ModeInfo *mi)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+  GLint max_aniso, aniso;
+
+  pf->magma_tex_rg = gen_3d_magma_texture_rg(MAGMA_TEX_SIZE);
+
+#ifndef HAVE_JWZGLES
+  glEnable(GL_TEXTURE_3D);
+#endif
+  glGenTextures(1,&pf->magma_tex);
+  glBindTexture(GL_TEXTURE_3D,pf->magma_tex);
+  glPixelStorei(GL_UNPACK_ALIGNMENT,1);
+#ifdef HAVE_JWZGLES
+  glTexImage3D(GL_TEXTURE_3D,0,GL_RG8,MAGMA_TEX_SIZE,MAGMA_TEX_SIZE,
+               MAGMA_TEX_SIZE,0,GL_RG,GL_UNSIGNED_BYTE,pf->magma_tex_rg);
+#else
+  glTexImage3D(GL_TEXTURE_3D,0,GL_RG,MAGMA_TEX_SIZE,MAGMA_TEX_SIZE,
+               MAGMA_TEX_SIZE,0,GL_RG,GL_UNSIGNED_BYTE,pf->magma_tex_rg);
+#endif
+  glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_WRAP_R,GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
+  if (pf->use_mipmaps)
+  {
+    glGenerateMipmap(GL_TEXTURE_3D);
+    glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_MIN_FILTER,
+                    GL_LINEAR_MIPMAP_LINEAR);
+    glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_MAX_LEVEL,4);
+  }
+  else
+  {
+    glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
+  }
+  if (pf->aniso_textures)
+  {
+    glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT,&max_aniso);
+    aniso = MIN(max_aniso,10);
+    glTexParameteri(GL_TEXTURE_3D,GL_TEXTURE_MAX_ANISOTROPY_EXT,aniso);
+  }
+  glBindTexture(GL_TEXTURE_2D,0);
+}
+
+
+/* Initialize the programmable OpenGL functionality. */
+static void init_glsl(ModeInfo *mi)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+  GLint gl_major, gl_minor, glsl_major, glsl_minor;
+  GLboolean gl_gles3;
+  const char *gl_ext;
+  const GLchar *vertex_shader_source[3];
+  const GLchar *fragment_shader_source[4];
+
+  pf->use_textures = false;
+  pf->use_mipmaps = false;
+
+  pf->magma_tex_offset[0] = 0.0f;
+  pf->magma_tex_offset[1] = 0.0f;
+  pf->magma_tex_offset[2] = 0.0f;
+
+  pf->use_shaders = false;
+  pf->textures_supported = false;
+  pf->aniso_textures = false;
+  pf->shader_program = 0;
+
+  if (!glsl_GetGlAndGlslVersions(&gl_major,&gl_minor,&glsl_major,&glsl_minor,
+                                 &gl_gles3))
+    return;
+
+  if (!gl_gles3)
+  {
+    if (gl_major < 3 ||
+        (glsl_major < 1 || (glsl_major == 1 && glsl_minor < 30)))
+    {
+      if ((gl_major < 2 || (gl_major == 2 && gl_minor < 1)) ||
+          (glsl_major < 1 || (glsl_major == 1 && glsl_minor < 20)))
+        return;
+      /* We have at least OpenGL 2.1 and at least GLSL 1.20. */
+      vertex_shader_source[0] = shader_version_2_1;
+      vertex_shader_source[1] = vertex_shader_attribs_2_1;
+      vertex_shader_source[2] = vertex_shader_main;
+      fragment_shader_source[0] = shader_version_2_1;
+      fragment_shader_source[1] = fragment_shader_attribs_2_1;
+      fragment_shader_source[2] = fragment_shader_main;
+      fragment_shader_source[3] = fragment_shader_out_2_1;
+    }
+    else
+    {
+      /* We have at least OpenGL 3.0 and at least GLSL 1.30. */
+      vertex_shader_source[0] = shader_version_3_0;
+      vertex_shader_source[1] = vertex_shader_attribs_3_0;
+      vertex_shader_source[2] = vertex_shader_main;
+      fragment_shader_source[0] = shader_version_3_0;
+      fragment_shader_source[1] = fragment_shader_attribs_3_0;
+      fragment_shader_source[2] = fragment_shader_main;
+      fragment_shader_source[3] = fragment_shader_out_3_0;
+    }
+  }
+  else /* gl_gles3 */
+  {
+    if (gl_major < 3 || glsl_major < 3)
+      return;
+    /* We have at least OpenGL ES 3.0 and at least GLSL ES 3.0. */
+    vertex_shader_source[0] = shader_version_3_0_es;
+    vertex_shader_source[1] = vertex_shader_attribs_3_0;
+    vertex_shader_source[2] = vertex_shader_main;
+    fragment_shader_source[0] = shader_version_3_0_es;
+    fragment_shader_source[1] = fragment_shader_attribs_3_0;
+    fragment_shader_source[2] = fragment_shader_main;
+    fragment_shader_source[3] = fragment_shader_out_3_0;
+  }
+
+  if (!glsl_CompileAndLinkShaders(3,vertex_shader_source,
+                                  4,fragment_shader_source,
+                                  &pf->shader_program))
+    return;
+
+  pf->pos_index =
+    glGetAttribLocation(pf->shader_program,"VertexPosition");
+  pf->normal_index =
+    glGetAttribLocation(pf->shader_program,"VertexNormal");
+  pf->color_index =
+    glGetAttribLocation(pf->shader_program,"VertexColor");
+  pf->tex_index =
+    glGetAttribLocation(pf->shader_program,"VertexTexCoord");
+  pf->mv_index =
+    glGetUniformLocation(pf->shader_program,"MatModelView");
+  pf->proj_index =
+    glGetUniformLocation(pf->shader_program,"MatProj");
+  pf->magma_offs_index =
+    glGetUniformLocation(pf->shader_program,"TexOffsetMagma");
+  pf->north_up_index =
+    glGetUniformLocation(pf->shader_program,"NorthUp");
+  pf->glbl_ambient_index =
+    glGetUniformLocation(pf->shader_program,"LtGlblAmbient");
+  pf->lt_ambient_index =
+    glGetUniformLocation(pf->shader_program,"LtAmbient");
+  pf->lt_diffuse_index =
+    glGetUniformLocation(pf->shader_program,"LtDiffuse");
+  pf->lt_specular_index =
+    glGetUniformLocation(pf->shader_program,"LtSpecular");
+  pf->lt_direction_index =
+    glGetUniformLocation(pf->shader_program,"LtDirection");
+  pf->lt_halfvect_index =
+    glGetUniformLocation(pf->shader_program,"LtHalfVector");
+  pf->ambient_index =
+    glGetUniformLocation(pf->shader_program,"MatAmbient");
+  pf->diffuse_index =
+    glGetUniformLocation(pf->shader_program,"MatDiffuse");
+  pf->specular_index =
+    glGetUniformLocation(pf->shader_program,"MatSpecular");
+  pf->shininess_index =
+    glGetUniformLocation(pf->shader_program,"MatShininess");
+  pf->bool_textures_index =
+    glGetUniformLocation(pf->shader_program,"BoolTextures");
+  pf->samp_mgm_index =
+    glGetUniformLocation(pf->shader_program,"TextureSamplerMagma");
+  pf->samp_day_index =
+    glGetUniformLocation(pf->shader_program,"TextureSamplerDay");
+  pf->samp_ngt_index =
+    glGetUniformLocation(pf->shader_program,"TextureSamplerNight");
+  pf->samp_wtr_index =
+    glGetUniformLocation(pf->shader_program,"TextureSamplerWater");
+  if (pf->pos_index == -1 ||
+      pf->normal_index == -1 ||
+      pf->color_index == -1 ||
+      pf->tex_index == -1 ||
+      pf->mv_index == -1 ||
+      pf->proj_index == -1 ||
+      pf->magma_offs_index == -1 ||
+      pf->north_up_index == -1 ||
+      pf->glbl_ambient_index == -1 ||
+      pf->lt_ambient_index == -1 ||
+      pf->lt_diffuse_index == -1 ||
+      pf->lt_specular_index == -1 ||
+      pf->lt_direction_index == -1 ||
+      pf->lt_halfvect_index == -1 ||
+      pf->ambient_index == -1 ||
+      pf->diffuse_index == -1 ||
+      pf->specular_index == -1 ||
+      pf->shininess_index == -1 ||
+      pf->bool_textures_index == -1 ||
+      pf->samp_mgm_index == -1 ||
+      pf->samp_day_index == -1 ||
+      pf->samp_ngt_index == -1 ||
+      pf->samp_wtr_index == -1)
+  {
+    glDeleteProgram(pf->shader_program);
+    return;
+  }
+
+  glGenBuffers(1,&pf->vertex_buffer);
+  glGenBuffers(1,&pf->normal_buffer);
+  glGenBuffers(1,&pf->color_buffer);
+  glGenBuffers(1,&pf->tex_buffer);
+
+  pf->textures_supported = true;
+  if (!gl_gles3 && gl_major < 3)
+  {
+    gl_ext = (const char *)glGetString(GL_EXTENSIONS);
+    if (gl_ext == NULL)
+      pf->textures_supported = false;
+    else
+      pf->textures_supported = (strstr(gl_ext,"GL_ARB_texture_rg") != NULL);
+  }
+
+  pf->magma_tex_rg = NULL;
+  if (pf->textures_supported)
+  {
+    gl_ext = (char *)glGetString(GL_EXTENSIONS);
+    if (gl_ext == NULL)
+      gl_ext = "";
+#if defined(HAVE_IPHONE)
+    /* Don't use anisotropic texture filtering on iOS since it leads to
+       artifacts at the ±180° meridian. */
+    pf->aniso_textures = false;
+#else
+    pf->aniso_textures =
+      ((strstr(gl_ext,"GL_EXT_texture_filter_anisotropic") != NULL) ||
+       (strstr(gl_ext,"GL_ARB_texture_filter_anisotropic") != NULL));
+#endif
+
+    if (!gl_gles3 && gl_major < 3)
+    {
+      /* On OpenGL 2.1 systems, such as the native macOS OpenGL version used
+         by XScreenSaver, we need the GL_ARB_framebuffer_object extension
+         to be able to use the glGenerateMipmap function. */
+      if (strstr(gl_ext,"GL_ARB_framebuffer_object") != NULL)
+        pf->use_mipmaps = true;
+    }
+    else if ((!gl_gles3 && gl_major >= 3) || gl_gles3)
+    {
+      pf->use_mipmaps = true;
+    }
+
+    gen_magma_texture(mi);
+    gen_earth_textures(mi);
+  }
+
+  pf->use_shaders = true;
+#if 0
+  fprintf(stderr,"Use shaders = %d\n",pf->use_shaders);
+  fprintf(stderr,"Textures supported = %d\n",pf->textures_supported);
+  fprintf(stderr,"Error = %d\n",glGetError());
+#endif
+}
+
+#endif /* HAVE_GLSL */
+
+
+/* Initialize the data structures. */
+static void init(ModeInfo *mi)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+  int i;
+
+#ifdef HAVE_GLSL
+  init_glsl(mi);
+#endif /* HAVE_GLSL */
+
+  pf->anim_state = ANIM_INIT;
+  pf->anim_poly = -1;
+  pf->fold_angle = 0;
+  pf->north_up = true;
+
+  pf->alpha = 300.0f;
+  pf->beta = 0.0f;
+  pf->delta = 270.0f;
+  pf->delta_delta = 0.0f;
+
+  pf->poly_pos[0] = 0.0f;
+  pf->poly_pos[1] = 0.0f;
+  pf->poly_pos[2] = 0.0f;
+
+  for (i=0; i<NUM_ANGLES; i++)
+    pf->angle[i] = 0.0f;
+  pf->angle_dir = 1.0f;
+
+  init_color_matrix(pf->color_matrix);
+
+  pf->polygon_unfolding =
+    create_random_polyhedron_unfolding(TETRAHEDRON_NUM_FACES,
+                                       TETRAHEDRON_NUM_EDGES,
+                                       tetrahedron_edges);
+  pf->max_angle = TETRAHEDRON_MAX_ANGLE;
+  pf->delta_angle = TETRAHEDRON_DELTA_ANGLE;
+  pf->num_fold_angles = TETRAHEDRON_NUM_FACES-1;
+  pf->eye_pos = tetrahedron_eye_pos;
+  pf->base_polygons = init_base_polygons(pf->polygon_unfolding,
+                                         &tetrahedron_triangle);
+  determine_unfolding_poses(pf->polygon_unfolding,pf->base_polygons);
+  determine_polygon_color_data(pf->polygon_unfolding,pf->base_polygons,
+                               pf->max_angle,pf->color_matrix);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *-----------------------------------------------------------------------------
+ *    Xlock hooks.
+ *-----------------------------------------------------------------------------
+ *-----------------------------------------------------------------------------
+ */
+
+
+ENTRYPOINT void reshape_platonicfolding(ModeInfo *mi, int width, int height)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+
+  pf->WindW = (GLint)width;
+  pf->WindH = (GLint)height;
+  pf->aspect = (GLfloat)width/(GLfloat)height;
+
+  /* Set the appear and disappear parameters of the polyhedron. */
+  if (pf->eye_pos != NULL)
+  {
+    if (pf->aspect >= 1.0f)
+    {
+      pf->spoly_pos[0] = 0.0f;
+      pf->spoly_pos[1] = -pf->eye_pos[2]*(2.0f/3.0f);
+      pf->spoly_pos[2] = 0.0f;
+      pf->dpoly_pos[0] = 0.0f;
+      pf->dpoly_pos[1] = pf->eye_pos[2]*(2.0f/3.0f);
+      pf->dpoly_pos[2] = 0.0f;
+    }
+    else
+    {
+      pf->spoly_pos[0] = -pf->eye_pos[2]*(2.0f/3.0f);
+      pf->spoly_pos[1] = 0.0f;
+      pf->spoly_pos[2] = 0.0f;
+      pf->dpoly_pos[0] = pf->eye_pos[2]*(2.0f/3.0f);
+      pf->dpoly_pos[1] = 0.0f;
+      pf->dpoly_pos[2] = 0.0f;
+    }
+  }
+}
+
+
+ENTRYPOINT Bool platonicfolding_handle_event(ModeInfo *mi, XEvent *event)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+
+  if (event->xany.type == ButtonPress &&
+      event->xbutton.button == Button1)
+  {
+    pf->button_pressed = True;
+    gltrackball_start(pf->trackball,event->xbutton.x,event->xbutton.y,
+                      MI_WIDTH(mi),MI_HEIGHT(mi));
+    return True;
+  }
+  else if (event->xany.type == ButtonRelease &&
+           event->xbutton.button == Button1)
+  {
+    pf->button_pressed = False;
+    gltrackball_stop(pf->trackball);
+    return True;
+  }
+  else if (event->xany.type == MotionNotify && pf->button_pressed)
+  {
+    gltrackball_track(pf->trackball,event->xmotion.x,event->xmotion.y,
+                      MI_WIDTH(mi),MI_HEIGHT(mi));
+    return True;
+  }
+  else if (screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event))
+  {
+    pf->anim_state = ANIM_INIT;
+    return True;
+  }
+
+  return False;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *-----------------------------------------------------------------------------
+ *    Xlock hooks.
+ *-----------------------------------------------------------------------------
+ *-----------------------------------------------------------------------------
+ */
+
+/*
+ *-----------------------------------------------------------------------------
+ *    Initialize platonicfolding.  Called each time the window changes.
+ *-----------------------------------------------------------------------------
+ */
+
+ENTRYPOINT void init_platonicfolding(ModeInfo *mi)
+{
+  platonicfoldingstruct *pf;
+  int n;
+
+  MI_INIT(mi,platonicfolding);
+  pf = &platonicfolding[MI_SCREEN(mi)];
+
+  pf->rotate = rotate;
+
+  /* Set the color mode. */
+  if (!strcasecmp(color_mode,"face"))
+  {
+    pf->colors = COLORS_FACE;
+    pf->random_colors = false;
+  }
+  else if (!strcasecmp(color_mode,"earth"))
+  {
+    pf->colors = COLORS_EARTH;
+    pf->random_colors = false;
+  }
+  else
+  {
+    pf->colors = random() % NUM_COLORS;
+    pf->random_colors = true;
+  }
+#ifndef HAVE_GLSL
+  if (pf->colors == COLORS_EARTH)
+    pf->colors = COLORS_FACE;
+#endif
+
+  /* Set the number of foldings. */
+  pf->num_foldings = RANDOM_NUM_FOLDINGS;
+  if (!strcasecmp(foldings,"random"))
+  {
+    pf->num_foldings = RANDOM_NUM_FOLDINGS;
+  }
+  else
+  {
+    n = sscanf(foldings,"%d",&pf->num_foldings);
+    if (n != 1)
+      pf->num_foldings = RANDOM_NUM_FOLDINGS;
+    else if (pf->num_foldings < 1)
+      pf->num_foldings = 1;
+    else if (pf->num_foldings > 20)
+      pf->num_foldings = 20;
+  }
+
+  pf->trackball = gltrackball_init(False);
+  pf->button_pressed = False;
+
+  if ((pf->glx_context = init_GL(mi)) != NULL)
+  {
+    pf->eye_pos = NULL;
+    reshape_platonicfolding(mi,MI_WIDTH(mi),MI_HEIGHT(mi));
+    init(mi);
+  }
+  else
+  {
+    MI_CLEARWINDOW(mi);
+  }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *    Called by the mainline code periodically to update the display.
+ *-----------------------------------------------------------------------------
+ */
+ENTRYPOINT void draw_platonicfolding(ModeInfo *mi)
+{
+  Display *display = MI_DISPLAY(mi);
+  Window window = MI_WINDOW(mi);
+  platonicfoldingstruct *pf;
+
+  if (platonicfolding == NULL)
+    return;
+  pf = &platonicfolding[MI_SCREEN(mi)];
+
+  MI_IS_DRAWN(mi) = True;
+  if (!pf->glx_context)
+    return;
+
+  glXMakeCurrent(display,window,*pf->glx_context);
+
+  display_platonicfolding(mi);
+
+  if (MI_IS_FPS(mi))
+    do_fps (mi);
+
+  glFlush();
+
+  glXSwapBuffers(display,window);
+}
+
+
+#ifndef STANDALONE
+ENTRYPOINT void change_platonicfolding(ModeInfo *mi)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+
+  if (!pf->glx_context)
+    return;
+
+  glXMakeCurrent(MI_DISPLAY(mi),MI_WINDOW(mi),*pf->glx_context);
+  init(mi);
+}
+#endif /* !STANDALONE */
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *    The display is being taken away from us.  Free up malloc'ed
+ *      memory and X resources that we've alloc'ed.
+ *-----------------------------------------------------------------------------
+ */
+
+ENTRYPOINT void free_platonicfolding(ModeInfo *mi)
+{
+  platonicfoldingstruct *pf = &platonicfolding[MI_SCREEN(mi)];
+
+  if (!pf->glx_context) return;
+  glXMakeCurrent(MI_DISPLAY(mi),MI_WINDOW(mi),*pf->glx_context);
+
+  gltrackball_free(pf->trackball);
+
+  free_base_polygons(pf->base_polygons);
+
+#ifdef HAVE_GLSL
+  if (pf->use_shaders)
+  {
+    glUseProgram(0);
+    if (pf->shader_program != 0)
+      glDeleteProgram(pf->shader_program);
+    glDeleteBuffers(1,&pf->vertex_buffer);
+    glDeleteBuffers(1,&pf->normal_buffer);
+    glDeleteBuffers(1,&pf->color_buffer);
+    glDeleteBuffers(1,&pf->tex_buffer);
+    if (pf->textures_supported)
+    {
+      glDeleteTextures(1,&pf->magma_tex);
+      free(pf->magma_tex_rg);
+      glDeleteTextures(3,pf->earth_tex);
+    }
+  }
+#endif /* HAVE_GLSL */
+}
+
+
+XSCREENSAVER_MODULE ("PlatonicFolding", platonicfolding)
+
+#endif /* USE_GL */
diff --git a/hacks/glx/platonicfolding.man b/hacks/glx/platonicfolding.man
new file mode 100644 (file)
index 0000000..5c1ebf3
--- /dev/null
@@ -0,0 +1,165 @@
+.TH XScreenSaver 1 "" "X Version 11"
+.SH NAME
+platonicfolding \- Draws the unfolding and folding of the Platonic solids
+.SH SYNOPSIS
+.B platonicfolding
+[\-\-display \fIhost:display.screen\fP]
+[\-\-install]
+[\-\-visual \fIvisual\fP]
+[\-\-window]
+[\-\-root]
+[\-\-window\-id \fInumber\fP]
+[\-\-delay \fIusecs\fP]
+[\-\-fps]
+[\-\-rotate]
+[\-\-colors \fIcolor-scheme\fP]
+[\-\-face-colors]
+[\-\-earth-colors]
+[\-\-foldings \fInum-foldings\fP]
+.SH DESCRIPTION
+The \fIplatonicfolding\fP program shows the unfolding and folding of
+the Platonic solids.  For the five Platonic solids (the tetrahedron,
+cube, octahedron, dodecahedron, and icosahedron), all unfoldings of
+its faces are non-overlapping: they form a net. The tetrahedron has 16
+unfoldings, of which two are essentially different (non-isomorphic),
+the cube and octahedron each have 384 unfoldings, of which eleven are
+non-isomorphic, and the dodecahedron and icosahedron each have
+5,184,000 unfoldings, of which 43,380 are non-isomorphic. This program
+displays randomly selected unfoldings for the five Platonic solids.
+Note that while it is guaranteed that the nets of the Platonic solids
+are non-overlapping, their faces occasionally intersect during the
+unfolding and folding.
+.PP
+The program displays the Platonic solids either using different colors
+for each face (face colors) or with a illuminated view of the earth
+(earth colors).  When using face colors, the colors of the faces are
+randomly chosen each time a new Platonic solid is selected.  When
+using earth colors, the Platonic solid is displayed as if the sphere
+of the earth were illuminated with the current position of the sun at
+the time the program is run.  The hemisphere the sun is currently
+illuminating is displayed with a satellite image of the earth by day
+and the other hemisphere is displayed with a satellite image of the
+earth by night.  The specular highlight on the illuminated hemisphere
+(which is only shown over bodies of water) is the subsolar point (the
+point on earth above which the sun is perpendicular).  The earth's
+sphere is then projected onto the Platonic solid via a gnomonic
+projection.  The program randomly selects whether the north pole or
+the south pole is facing upwards.  The inside of the earth is
+displayed with a magma-like texture.
+.PP
+At the beginning of each cycle, the program selects one of the five
+Platonic solids randomly and moves it to the center of the screen.  It
+then repeatedly selects a random net of the polyhedron and unfolds and
+folds the polyhedron.  The unfolding and folding can occur around each
+edge of the net successively or around all edges simultaneously.  At
+the end of each cycle, the Platonic solid is moved offscreen and the
+next cycle begins.
+.PP
+While the Platonic solid is moved on the screen or is unfolded or
+folded, it is rotated by default.  If earth colors are used, the
+rotation is always performed in the direction the earth is rotating
+(counterclockwise as viewed from the north pole towards the center of
+the earth).  This rotation optionally can be switched off.
+.SH OPTIONS
+.I platonicfolding
+accepts the following options:
+.TP 8
+.B \-\-window
+Draw on a newly-created window.  This is the default.
+.TP 8
+.B \-\-root
+Draw on the root window.
+.TP 8
+.B \-\-window\-id \fInumber\fP
+Draw on the specified window.
+.TP 8
+.B \-\-install
+Install a private colormap for the window.
+.TP 8
+.B \-\-visual \fIvisual\fP
+Specify which visual to use.  Legal values are the name of a visual
+class, or the id number (decimal or hex) of a specific visual.
+.TP 8
+.B \-\-delay \fImicroseconds\fP
+How much of a delay should be introduced between steps of the
+animation.  Default 25000, or 1/40th second.
+.PP
+The following options determine whether the Platonic solid is being
+rotated.
+.TP 8
+.B \-\-rotate
+Rotate the Platonic solid (default).
+.TP 8
+.B \-\-no-rotate
+Do not rotate the Platonic solid.
+.PP
+The following three options are mutually exclusive.  They determine
+how to color the Platonic solid.
+.TP 8
+.B \-\-colors random
+Display the Platonic solid with a random color scheme (default).
+.TP 8
+.B \-\-colors face \fP(Shortcut: \fB\-\-face-colors\fP)
+Display the Platonic solid with different colors for each face.  The
+colors of the faces are identical on the inside and outside of the
+Platonic solid.
+.TP 8
+.B \-\-colors earth \fP(Shortcut: \fB\-\-earth-colors\fP)
+Display the Platonic solid with a texture of earth as illuminated by
+the sun at the time the program is run.
+.PP
+The following option determines how many unfoldings and foldings to
+perform per cycle.
+.TP 8
+.B \-\-foldings random
+Use a random number of unfoldings and foldings per cycle (default).
+.TP 8
+.B \-\-foldings \fIint\fP
+If an integer number is specified, it is clipped to the range 1...20
+and the clipped number is used as the number of unfoldings and
+foldings per cycle.
+.SH INTERACTION
+If you run this program in standalone mode, you can rotate the
+Platonic solid by dragging the mouse while pressing the left mouse
+button.
+.SH ENVIRONMENT
+.PP
+.TP 8
+.B DISPLAY
+to get the default host and display number.
+.TP 8
+.B XENVIRONMENT
+to get the name of a resource file that overrides the global resources
+stored in the RESOURCE_MANAGER property.
+.TP 8
+.B XSCREENSAVER_WINDOW
+The window ID to use with \fI\-\-root\fP.
+.SH SEE ALSO
+.BR X (1),
+.BR xscreensaver (1),
+.SH FURTHER INFORMATION
+Takashi Horiyama, Wataru Shoji: Edge Unfoldings of Platonic Solids
+Never Overlap.  In: 23rd Canadian Conference on Computational
+Geometry, 2011.
+.PP
+Takashi Horiyama, Wataru Shoji: The Number of Different Unfoldings of
+Polyhedra.  In: 24th International Symposium on Algorithms and
+Computation, pp. 623-633, 2013.
+.PP
+Taiping Zhang, Paul W. Stackhouse Jr., Bradley Macpherson, J. Colleen
+Mikovitz: A solar azimuth formula that renders circumstantial
+treatment unnecessary without compromising mathematical rigor:
+Mathematical setup, application and extension of a formula based on
+the subsolar point and atan2 function.  Renewable Energy
+172:1333-1340, 2021.
+.SH COPYRIGHT
+Copyright \(co 2025 by Carsten Steger.  Permission to use, copy,
+modify, distribute, and sell this software and its documentation for
+any purpose is hereby granted without fee, provided that the above
+copyright notice appear in all copies and that both that copyright
+notice and this permission notice appear in supporting documentation.
+No representations are made about the suitability of this software for
+any purpose.  It is provided "as is" without express or implied
+warranty.
+.SH AUTHOR
+Carsten Steger <carsten@mirsanmir.org>, 18-mar-2025.
index 3f87a9cd508da741e0dc11f1717de508d794a056..27085ce77b5ed7ef53ba249980869d0d76ef9f91 100644 (file)
@@ -2431,7 +2431,7 @@ construct_polyhedra (polyhedron ***polyhedra_ret)
   Malloc (result, last_uniform * 2 + 3, polyhedron*);
 
   while (index < last_uniform) {
-    char sym[4];
+    char sym[20];
     Polyhedron *P;
 
     sprintf(sym, "#%d", index + 1);
index 5dc366b965435d0929cd1a169421cccb2d5cc7f7..2328472801b4c59aa30fb082961b16ae86f4d74e 100644 (file)
@@ -1,4 +1,4 @@
-/* sonar, Copyright © 1998-2021 Jamie Zawinski and Stephen Martin
+/* sonar, Copyright © 1998-2025 Jamie Zawinski and Stephen Martin
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -461,7 +461,7 @@ read_hosts_file (sonar_sensor_data *ssd, const char *filename)
 #endif
         {
           char *str_error = strerror(errno);
-          fprintf(stderr, "%s:  %s: %s", progname, filename, str_error);
+          fprintf(stderr, "%s:  %s: %s\n", progname, filename, str_error);
         }
       return 0;
     }
@@ -1454,6 +1454,43 @@ ping_scan (sonar_sensor_data *ssd)
 }
 
 
+/* Reorder the host list randomly.
+ */
+static 
+sonar_bogie *
+shuffle_list (sonar_bogie *list)
+{
+  int count, i;
+  sonar_bogie **a, *n;
+  if (!list) return list;
+
+  /* Convert the linked list to an array. */
+  for (n = list, count = 0; n; n = n->next)
+    count++;
+  a = (void *) malloc (count * sizeof(*a));
+  for (n = list, i = 0; n; n = n->next)
+    a[i++] = n;
+  
+  /* Fisher–Yates shuffle. */
+  for (i = count-1; i > 0; i--)
+    {
+      int j = random() % i;
+      void *s = a[i];
+      a[i] = a[j];
+      a[j] = s;
+    }
+
+  /* Back to a list. */
+  a[0]->next = NULL;
+  for (i = 1; i < count; i++)
+    a[i]->next = a[i-1];
+  list = a[count-1];
+  free (a);
+
+  return list;
+}
+
+
 /* Returns a list of hosts to ping based on the "-ping" argument.
  */
 static sonar_bogie *
@@ -1464,6 +1501,7 @@ parse_mode (sonar_sensor_data *ssd, char **error_ret, char **desc_ret,
   char *source, *token, *end, dummy;
   sonar_bogie *hostlist = 0;
   const char *fallback = "subnet";
+  Bool shuffle_p = False;
 
  AGAIN:
 
@@ -1537,6 +1575,7 @@ parse_mode (sonar_sensor_data *ssd, char **error_ret, char **desc_ret,
         {
 # ifdef READ_FILES
           new = read_hosts_file (ssd, token);
+          shuffle_p = True;
 # else
           if (pd->debug_p) fprintf (stderr, "%s:  skipping file\n", progname);
 # endif
@@ -1545,6 +1584,7 @@ parse_mode (sonar_sensor_data *ssd, char **error_ret, char **desc_ret,
       else if (!stat (token, &st))
         {
           new = read_hosts_file (ssd, token);
+          shuffle_p = True;
         }
 # endif /* READ_FILES */
       else
@@ -1552,6 +1592,7 @@ parse_mode (sonar_sensor_data *ssd, char **error_ret, char **desc_ret,
           /* not an existant file - must be a host name
            */
           new = bogie_for_host (ssd, token, NULL);
+          shuffle_p = True;
         }
 
       if (new)
@@ -1583,9 +1624,13 @@ parse_mode (sonar_sensor_data *ssd, char **error_ret, char **desc_ret,
                  progname, fallback);
       ping_arg = fallback;
       fallback = 0;
+      shuffle_p = False;
       goto AGAIN;
     }
 
+  if (shuffle_p)
+    hostlist = shuffle_list (hostlist);
+
   return hostlist;
 }
 
@@ -1734,6 +1779,28 @@ sonar_init_ping (Display *dpy, char **error_ret, char **desc_ret,
   /* Disavow privs */
   if (setuid(getuid()) == -1) abort();
 
+
+  /* Feb 2025: On macOS 14.7, sometimes sendto() gives us a free SIGPIPE,
+     as a treat.
+   */
+  {
+# ifdef HAVE_SIGACTION
+    struct sigaction a;
+    a.sa_handler = SIG_IGN;
+    sigemptyset (&a.sa_mask);
+    a.sa_flags = 0;
+    if (sigaction (SIGPIPE, &a, 0) < 0)
+# else  /* !HAVE_SIGACTION */
+    if (((long) signal (SIGPIPE, SIG_IGN)) == -1L)
+# endif /* !HAVE_SIGACTION */
+      {
+        char buf [255];
+        sprintf (buf, "%s: couldn't block SIGPIPE", progname);
+        perror (buf);
+        /* abort(); */
+      }
+  }
+
   pd->pid = getpid() & 0xFFFF;
   pd->seq = 0;
   pd->timeout = timeout;
index 509e97c6e7dd3151fcc44e7d45c43608dc2493a8..bd9c990e85e44356cf4be4210088e14ad4a8eb27 100644 (file)
@@ -1,5 +1,5 @@
 /* xlock-gl.c --- xscreensaver compatibility layer for xlockmore GL modules.
- * xscreensaver, Copyright © 1997-2024 Jamie Zawinski <jwz@jwz.org>
+ * xscreensaver, Copyright © 1997-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -49,18 +49,20 @@ BadValue_ehandler (Display *dpy, XErrorEvent *error)
 #endif /* !HAVE_EGL */
 
 
-#undef glEnable
+#ifndef HAVE_JWZGLES
+# undef glEnable
 void (* glEnable_fn) (GLuint) = glEnable;
 
-#if defined(__linux__) && (defined(__arm__) || defined(__ARM_ARCH))
-# define PI_LIKE  /* Raspberry Pi-adjacent */
+# if defined(__linux__) && (defined(__arm__) || defined(__ARM_ARCH))
+#  define PI_LIKE  /* Raspberry Pi-adjacent */
 static void
 glEnable_bad_line_smooth (GLuint cap)
 {
   if (cap != GL_LINE_SMOOTH)
     glEnable (cap);
 }
-#endif
+# endif /* PI_LIKE */
+#endif /* HAVE_JWZGLES */
 
 
 GLXContext *
@@ -129,7 +131,7 @@ init_GL(ModeInfo * mi)
 
     /* This is re-used, no need to close it. */
     d->egl_display = eglGetPlatformDisplay (EGL_PLATFORM_X11_KHR,
-                                            (EGLNativeDisplayType) dpy, NULL);
+                                            (void *) dpy, NULL);
     if (!d->egl_display)
       {
         fprintf (stderr, "%s: eglGetPlatformDisplay failed\n", progname);
diff --git a/hacks/images/klondike/C2.png b/hacks/images/klondike/C2.png
new file mode 100644 (file)
index 0000000..6f36f98
Binary files /dev/null and b/hacks/images/klondike/C2.png differ
diff --git a/hacks/images/klondike/C3.png b/hacks/images/klondike/C3.png
new file mode 100644 (file)
index 0000000..47a3560
Binary files /dev/null and b/hacks/images/klondike/C3.png differ
diff --git a/hacks/images/klondike/C4.png b/hacks/images/klondike/C4.png
new file mode 100644 (file)
index 0000000..8f13bf8
Binary files /dev/null and b/hacks/images/klondike/C4.png differ
diff --git a/hacks/images/klondike/C5.png b/hacks/images/klondike/C5.png
new file mode 100644 (file)
index 0000000..a71dac2
Binary files /dev/null and b/hacks/images/klondike/C5.png differ
diff --git a/hacks/images/klondike/C6.png b/hacks/images/klondike/C6.png
new file mode 100644 (file)
index 0000000..8adc3d9
Binary files /dev/null and b/hacks/images/klondike/C6.png differ
diff --git a/hacks/images/klondike/C7.png b/hacks/images/klondike/C7.png
new file mode 100644 (file)
index 0000000..2e9dde3
Binary files /dev/null and b/hacks/images/klondike/C7.png differ
diff --git a/hacks/images/klondike/C8.png b/hacks/images/klondike/C8.png
new file mode 100644 (file)
index 0000000..b026a41
Binary files /dev/null and b/hacks/images/klondike/C8.png differ
diff --git a/hacks/images/klondike/C9.png b/hacks/images/klondike/C9.png
new file mode 100644 (file)
index 0000000..71ad18d
Binary files /dev/null and b/hacks/images/klondike/C9.png differ
diff --git a/hacks/images/klondike/CA.png b/hacks/images/klondike/CA.png
new file mode 100644 (file)
index 0000000..b08ccb4
Binary files /dev/null and b/hacks/images/klondike/CA.png differ
diff --git a/hacks/images/klondike/CJ.png b/hacks/images/klondike/CJ.png
new file mode 100644 (file)
index 0000000..71e6ef1
Binary files /dev/null and b/hacks/images/klondike/CJ.png differ
diff --git a/hacks/images/klondike/CK.png b/hacks/images/klondike/CK.png
new file mode 100644 (file)
index 0000000..06df256
Binary files /dev/null and b/hacks/images/klondike/CK.png differ
diff --git a/hacks/images/klondike/CQ.png b/hacks/images/klondike/CQ.png
new file mode 100644 (file)
index 0000000..4b62428
Binary files /dev/null and b/hacks/images/klondike/CQ.png differ
diff --git a/hacks/images/klondike/CT.png b/hacks/images/klondike/CT.png
new file mode 100644 (file)
index 0000000..b4af197
Binary files /dev/null and b/hacks/images/klondike/CT.png differ
diff --git a/hacks/images/klondike/D2.png b/hacks/images/klondike/D2.png
new file mode 100644 (file)
index 0000000..cf8a9d8
Binary files /dev/null and b/hacks/images/klondike/D2.png differ
diff --git a/hacks/images/klondike/D3.png b/hacks/images/klondike/D3.png
new file mode 100644 (file)
index 0000000..d2ae224
Binary files /dev/null and b/hacks/images/klondike/D3.png differ
diff --git a/hacks/images/klondike/D4.png b/hacks/images/klondike/D4.png
new file mode 100644 (file)
index 0000000..9d11be7
Binary files /dev/null and b/hacks/images/klondike/D4.png differ
diff --git a/hacks/images/klondike/D5.png b/hacks/images/klondike/D5.png
new file mode 100644 (file)
index 0000000..ba6b31a
Binary files /dev/null and b/hacks/images/klondike/D5.png differ
diff --git a/hacks/images/klondike/D6.png b/hacks/images/klondike/D6.png
new file mode 100644 (file)
index 0000000..bca8989
Binary files /dev/null and b/hacks/images/klondike/D6.png differ
diff --git a/hacks/images/klondike/D7.png b/hacks/images/klondike/D7.png
new file mode 100644 (file)
index 0000000..70f58be
Binary files /dev/null and b/hacks/images/klondike/D7.png differ
diff --git a/hacks/images/klondike/D8.png b/hacks/images/klondike/D8.png
new file mode 100644 (file)
index 0000000..cf55abf
Binary files /dev/null and b/hacks/images/klondike/D8.png differ
diff --git a/hacks/images/klondike/D9.png b/hacks/images/klondike/D9.png
new file mode 100644 (file)
index 0000000..a41632c
Binary files /dev/null and b/hacks/images/klondike/D9.png differ
diff --git a/hacks/images/klondike/DA.png b/hacks/images/klondike/DA.png
new file mode 100644 (file)
index 0000000..02a52c1
Binary files /dev/null and b/hacks/images/klondike/DA.png differ
diff --git a/hacks/images/klondike/DJ.png b/hacks/images/klondike/DJ.png
new file mode 100644 (file)
index 0000000..5db00c6
Binary files /dev/null and b/hacks/images/klondike/DJ.png differ
diff --git a/hacks/images/klondike/DK.png b/hacks/images/klondike/DK.png
new file mode 100644 (file)
index 0000000..7b6cbed
Binary files /dev/null and b/hacks/images/klondike/DK.png differ
diff --git a/hacks/images/klondike/DQ.png b/hacks/images/klondike/DQ.png
new file mode 100644 (file)
index 0000000..fcb247d
Binary files /dev/null and b/hacks/images/klondike/DQ.png differ
diff --git a/hacks/images/klondike/DT.png b/hacks/images/klondike/DT.png
new file mode 100644 (file)
index 0000000..f346222
Binary files /dev/null and b/hacks/images/klondike/DT.png differ
diff --git a/hacks/images/klondike/H2.png b/hacks/images/klondike/H2.png
new file mode 100644 (file)
index 0000000..91eddc0
Binary files /dev/null and b/hacks/images/klondike/H2.png differ
diff --git a/hacks/images/klondike/H3.png b/hacks/images/klondike/H3.png
new file mode 100644 (file)
index 0000000..e21150b
Binary files /dev/null and b/hacks/images/klondike/H3.png differ
diff --git a/hacks/images/klondike/H4.png b/hacks/images/klondike/H4.png
new file mode 100644 (file)
index 0000000..68d7ff7
Binary files /dev/null and b/hacks/images/klondike/H4.png differ
diff --git a/hacks/images/klondike/H5.png b/hacks/images/klondike/H5.png
new file mode 100644 (file)
index 0000000..b24f90c
Binary files /dev/null and b/hacks/images/klondike/H5.png differ
diff --git a/hacks/images/klondike/H6.png b/hacks/images/klondike/H6.png
new file mode 100644 (file)
index 0000000..7cabe9f
Binary files /dev/null and b/hacks/images/klondike/H6.png differ
diff --git a/hacks/images/klondike/H7.png b/hacks/images/klondike/H7.png
new file mode 100644 (file)
index 0000000..c3fbf57
Binary files /dev/null and b/hacks/images/klondike/H7.png differ
diff --git a/hacks/images/klondike/H8.png b/hacks/images/klondike/H8.png
new file mode 100644 (file)
index 0000000..d18c213
Binary files /dev/null and b/hacks/images/klondike/H8.png differ
diff --git a/hacks/images/klondike/H9.png b/hacks/images/klondike/H9.png
new file mode 100644 (file)
index 0000000..85b86a0
Binary files /dev/null and b/hacks/images/klondike/H9.png differ
diff --git a/hacks/images/klondike/HA.png b/hacks/images/klondike/HA.png
new file mode 100644 (file)
index 0000000..fcfd3fb
Binary files /dev/null and b/hacks/images/klondike/HA.png differ
diff --git a/hacks/images/klondike/HJ.png b/hacks/images/klondike/HJ.png
new file mode 100644 (file)
index 0000000..84824cb
Binary files /dev/null and b/hacks/images/klondike/HJ.png differ
diff --git a/hacks/images/klondike/HK.png b/hacks/images/klondike/HK.png
new file mode 100644 (file)
index 0000000..05602bf
Binary files /dev/null and b/hacks/images/klondike/HK.png differ
diff --git a/hacks/images/klondike/HQ.png b/hacks/images/klondike/HQ.png
new file mode 100644 (file)
index 0000000..cce52fc
Binary files /dev/null and b/hacks/images/klondike/HQ.png differ
diff --git a/hacks/images/klondike/HT.png b/hacks/images/klondike/HT.png
new file mode 100644 (file)
index 0000000..3d7d056
Binary files /dev/null and b/hacks/images/klondike/HT.png differ
diff --git a/hacks/images/klondike/S2.png b/hacks/images/klondike/S2.png
new file mode 100644 (file)
index 0000000..2b708c6
Binary files /dev/null and b/hacks/images/klondike/S2.png differ
diff --git a/hacks/images/klondike/S3.png b/hacks/images/klondike/S3.png
new file mode 100644 (file)
index 0000000..0658f23
Binary files /dev/null and b/hacks/images/klondike/S3.png differ
diff --git a/hacks/images/klondike/S4.png b/hacks/images/klondike/S4.png
new file mode 100644 (file)
index 0000000..fbd09b3
Binary files /dev/null and b/hacks/images/klondike/S4.png differ
diff --git a/hacks/images/klondike/S5.png b/hacks/images/klondike/S5.png
new file mode 100644 (file)
index 0000000..483a068
Binary files /dev/null and b/hacks/images/klondike/S5.png differ
diff --git a/hacks/images/klondike/S6.png b/hacks/images/klondike/S6.png
new file mode 100644 (file)
index 0000000..26ced1d
Binary files /dev/null and b/hacks/images/klondike/S6.png differ
diff --git a/hacks/images/klondike/S7.png b/hacks/images/klondike/S7.png
new file mode 100644 (file)
index 0000000..72e61eb
Binary files /dev/null and b/hacks/images/klondike/S7.png differ
diff --git a/hacks/images/klondike/S8.png b/hacks/images/klondike/S8.png
new file mode 100644 (file)
index 0000000..84d0684
Binary files /dev/null and b/hacks/images/klondike/S8.png differ
diff --git a/hacks/images/klondike/S9.png b/hacks/images/klondike/S9.png
new file mode 100644 (file)
index 0000000..8e25664
Binary files /dev/null and b/hacks/images/klondike/S9.png differ
diff --git a/hacks/images/klondike/SA.png b/hacks/images/klondike/SA.png
new file mode 100644 (file)
index 0000000..b9aec47
Binary files /dev/null and b/hacks/images/klondike/SA.png differ
diff --git a/hacks/images/klondike/SJ.png b/hacks/images/klondike/SJ.png
new file mode 100644 (file)
index 0000000..3e55fe1
Binary files /dev/null and b/hacks/images/klondike/SJ.png differ
diff --git a/hacks/images/klondike/SK.png b/hacks/images/klondike/SK.png
new file mode 100644 (file)
index 0000000..c5dce74
Binary files /dev/null and b/hacks/images/klondike/SK.png differ
diff --git a/hacks/images/klondike/SQ.png b/hacks/images/klondike/SQ.png
new file mode 100644 (file)
index 0000000..dd83cdc
Binary files /dev/null and b/hacks/images/klondike/SQ.png differ
diff --git a/hacks/images/klondike/ST.png b/hacks/images/klondike/ST.png
new file mode 100644 (file)
index 0000000..c6a9bb3
Binary files /dev/null and b/hacks/images/klondike/ST.png differ
diff --git a/hacks/images/klondike/attribution.txt b/hacks/images/klondike/attribution.txt
new file mode 100644 (file)
index 0000000..fa52785
--- /dev/null
@@ -0,0 +1,18 @@
+# Attributions of images for the klondike screensaver
+
+The card images in this file are derived from the following sources:
+
+## Card Fronts
+https://commons.wikimedia.org/wiki/Category:SVG_English_pattern_playing_cards
+Author: Дмитрий Фомин (Dmitry Fomin) 
+License: CC0 (public domain)
+
+## Card Back
+https://commons.wikimedia.org/wiki/File:Reverso_baraja_española_rojo.svg
+Author: Germarquezm
+License: Creative Commons Attribution-Share Alike 3.0 Unported 
+
+## Modifications
+The png files in this folder were made from the original SVG files by rendering the SVG to PNG with Inkscape, then padded with a drop shadow using ImageMagick.
+
+
diff --git a/hacks/images/klondike/back.png b/hacks/images/klondike/back.png
new file mode 100644 (file)
index 0000000..59d36b3
Binary files /dev/null and b/hacks/images/klondike/back.png differ
diff --git a/hacks/images/klondike/back0.png b/hacks/images/klondike/back0.png
new file mode 100644 (file)
index 0000000..1dc1e6f
Binary files /dev/null and b/hacks/images/klondike/back0.png differ
index 67a1fe6be5806332670507a1e029a46d1a6319da..c96715fefa7d83b3a401931e753548c4f205ac6c 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright © 1999-2022 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 1999-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
  *
  * Phosphor -- simulate a glass tty with long-sustain phosphor.
  * Written by Jamie Zawinski <jwz@jwz.org>
- * Pty and vt100 emulation by Fredrik Tolf <fredrik@dolda2000.com>
  */
 
 #include "screenhack.h"
 #include "textclient.h"
+#include "ansi-tty.h"
 #include "ximage-loader.h"
 #include "utf8wc.h"
 
 #define FADE   3
 #define STATE_MAX FADE
 
-#define CURSOR_INDEX 128
-
 #define NPAR 16
 
+#undef USE_XFT_BITMAP  /* Not working reliably */
 #define BUILTIN_FONT
 
 #ifdef BUILTIN_FONT
 # include "images/gen/6x10font_png.h"
 #endif /* BUILTIN_FONT */
 
+
 typedef struct {
-  unsigned char name;
+  unsigned long name;  /* Unicode character */
   int width, height;
   Pixmap pixmap;
 #ifdef FUZZY_BORDER
   Pixmap pixmap2;
+  Pixmap cache;
 #endif /* FUZZY_BORDER */
   Bool blank_p;
 } p_char;
 
 typedef struct {
-  p_char *p_char;
+  unsigned char c;
   int state;
   Bool changed;
+  Bool invert_p;
+  Bool symbol_p;
 } p_cell;
 
 typedef struct {
@@ -75,12 +78,11 @@ typedef struct {
   int ticks;
   int mode;
 
-  int escstate;
-  int csiparam[NPAR];
-  int curparam;
-  int unicruds; unsigned char unicrud[7];
+  p_char   *chars[256];
+  p_char  *ichars[256];
+  p_char  *schars[256];
+  p_char *sichars[256];
 
-  p_char **chars;
   p_cell *cells;
   XGCValues gcv;
   GC gc0;
@@ -89,25 +91,23 @@ typedef struct {
   GC gc2;
 #endif /* FUZZY_BORDER */
   GC *gcs;
-  XImage *font_bits;
+  XImage *font_bits, *sym_font_bits;
 
   int cursor_x, cursor_y;
+  Bool cursor_on;
   XtIntervalId cursor_timer;
   Time cursor_blink;
   int delay;
-  Bool pty_p;
 
   text_data *tc;
-
-  char last_c;
-  int bk;
+  ansi_tty *tty;
 
 } p_state;
 
 
-static void capture_font_bits (p_state *state);
-static p_char *make_character (p_state *state, int c);
-static void char_to_pixmap (p_state *state, p_char *pc, int c);
+static void capture_font_bits (p_state *state, Bool symbol_p);
+static void char_to_pixmap (p_state *state, p_char *pc, unsigned long c,
+                            Bool invert_p, Bool symbol_p);
 
 
 /* About font metrics:
@@ -140,7 +140,6 @@ static void char_to_pixmap (p_state *state, p_char *pc, int c);
  */
 
 
-static void clear (p_state *);
 static void set_cursor (p_state *, Bool on);
 
 static unsigned short scale_color_channel (unsigned short ch1, unsigned short ch2)
@@ -151,6 +150,8 @@ static unsigned short scale_color_channel (unsigned short ch1, unsigned short ch
 #define FONT6x10_WIDTH (256*7)
 #define FONT6x10_HEIGHT 10
 
+static void phosphor_tty_send (void *, const char *);
+
 static void *
 phosphor_init (Display *dpy, Window window)
 {
@@ -167,17 +168,24 @@ phosphor_init (Display *dpy, Window window)
 /*  XSelectInput (dpy, window, state->xgwa.your_event_mask | ExposureMask);*/
 
   state->delay = get_integer_resource (dpy, "delay", "Integer");
-  state->pty_p = get_boolean_resource (dpy, "usePty", "UsePty");
+
+# ifndef HAVE_XFT              /* Custom fonts only work under real Xft. */
+  free (fontname);
+  fontname = strdup ("builtin");
+#  ifndef BUILTIN_FONT
+#   error BUILTIN_FONT is required unless HAVE_XFT.
+#  endif
+# endif /* HAVE_XFT */
 
   if (!strcasecmp (fontname, "builtin") ||
       !strcasecmp (fontname, "(builtin)"))
     {
-#ifndef BUILTIN_FONT
+# ifndef BUILTIN_FONT
       fprintf (stderr, "%s: no builtin font\n", progname);
       state->font = load_xft_font_retry (dpy,
                                          screen_number (state->xgwa.screen),
                                          "fixed");
-#endif /* !BUILTIN_FONT */
+# endif /* !BUILTIN_FONT */
     }
   else
     {
@@ -196,7 +204,6 @@ phosphor_init (Display *dpy, Window window)
   /* Xft uses 'scale' */
   state->scale = get_integer_resource (dpy, "phosphorScale", "Integer");
   state->ticks = STATE_MAX + get_integer_resource (dpy, "ticks", "Integer");
-  state->escstate = 0;
 
   if (state->scale <= 0) state->scale = 1;
   if (state->ticks <= 0) state->ticks = 1;
@@ -229,7 +236,7 @@ phosphor_init (Display *dpy, Window window)
     state->xmargin = 96;
     state->ymargin = state->xmargin;
   }
-# endif
+# endif /* HAVE_IPHONE */
 
   state->grid_width = ((state->xgwa.width - state->xmargin * 2) /
                        (state->char_width * state->scale));
@@ -237,7 +244,6 @@ phosphor_init (Display *dpy, Window window)
                         (state->char_height * state->scale));
   state->cells = (p_cell *) calloc (sizeof(p_cell),
                                     state->grid_width * state->grid_height);
-  state->chars = (p_char **) calloc (sizeof(p_char *), 256);
 
   state->gcs = (GC *) calloc (sizeof(GC), state->ticks + 1);
 
@@ -302,17 +308,17 @@ phosphor_init (Display *dpy, Window window)
     /* Now, GCs all around.
      */
     state->gcv.cap_style = CapRound;
-#ifdef FUZZY_BORDER
+# ifdef FUZZY_BORDER
     state->gcv.line_width = (int) (((long) state->scale) * 1.3);
     if (state->gcv.line_width == state->scale)
       state->gcv.line_width++;
-#else /* !FUZZY_BORDER */
+# else /* !FUZZY_BORDER */
     state->gcv.line_width = (int) (((long) state->scale) * 0.9);
     if (state->gcv.line_width >= state->scale)
       state->gcv.line_width = state->scale - 1;
     if (state->gcv.line_width < 1)
       state->gcv.line_width = 1;
-#endif /* !FUZZY_BORDER */
+# endif /* !FUZZY_BORDER */
 
     flags = (GCForeground | GCBackground | GCCapStyle | GCLineWidth);
 
@@ -339,18 +345,21 @@ phosphor_init (Display *dpy, Window window)
     free (colors);
   }
 
-  capture_font_bits (state);
+  capture_font_bits (state, False);
+  capture_font_bits (state, True);
 
-  set_cursor (state, True);
+  state->tty = ansi_tty_init (state->grid_width, state->grid_height);
+  state->tty->closure  = state;
+  state->tty->tty_send = phosphor_tty_send;
 
-/*  clear (state);*/
+  set_cursor (state, True);
 
   state->tc = textclient_open (dpy);
   textclient_reshape (state->tc,
                       state->xgwa.width  - state->xmargin * 2,
                       state->xgwa.height - state->ymargin * 2,
-                      state->grid_width  - 1,
-                      state->grid_height - 1,
+                      state->grid_width,
+                      state->grid_height,
                       0);
 
   return state;
@@ -409,8 +418,20 @@ resize_grid (p_state *state)
 }
 
 
+static p_char *
+make_character (p_state *state, unsigned char c, Bool invert_p, Bool symbol_p)
+{
+  p_char *pc = (p_char *) malloc (sizeof (*pc));
+  pc->name = (c + (symbol_p ? 256 : 0)) * (invert_p ? -1 : 1);
+  pc->width =  state->scale * state->char_width;
+  pc->height = state->scale * state->char_height;
+  char_to_pixmap (state, pc, c, invert_p, symbol_p);
+  return pc;
+}
+
+
 static void
-capture_font_bits (p_state *state)
+capture_font_bits (p_state *state, Bool symbol_p)
 {
   XftFont *font = state->font;
   int safe_width, height;
@@ -448,6 +469,19 @@ capture_font_bits (p_state *state)
       if (pix_w != FONT6x10_WIDTH) abort();
       if (pix_h != FONT6x10_HEIGHT) abort();
 
+      if (symbol_p)
+        {
+          /* "6x10font.png" has the DEC Special Graphics characters down in
+             the control area, so we just need to shift them into place. */
+          int cw    = FONT6x10_WIDTH / 256;
+          int from  = cw * 0x01;
+          int to    = cw * 0x60;
+          int len   = cw * 31;
+          for (y = 0; y < pix_h; y++)
+            for (x = to; x < to + len; x++)
+              XPutPixel (mm, x, y, XGetPixel (mm, x - (to - from), y));
+        }
+
       XGetWindowAttributes (state->dpy, state->window, &xgwa);
       im2 = XCreateImage (state->dpy, xgwa.visual, 1, XYBitmap, 0, 0,
                           pix_w, pix_h, 8, 0);
@@ -481,7 +515,7 @@ capture_font_bits (p_state *state)
       XftTextExtentsUtf8 (state->dpy, state->font,
                           (FcChar8 *) "N", 1, &overall);
       /* #### maybe safe_width should take lbearing into account */
-      safe_width  = overall.xOff;
+      safe_width  = overall.xOff + 1;
       state->char_height = state->font->ascent + state->font->descent;
       height = state->char_height;
     }
@@ -505,12 +539,12 @@ capture_font_bits (p_state *state)
                            GCCapStyle | GCLineWidth),
                           &state->gcv);
 
-#ifdef HAVE_JWXYZ
+# ifdef HAVE_JWXYZ
   jwxyz_XSetAntiAliasing (state->dpy, state->gc0, False);
   jwxyz_XSetAntiAliasing (state->dpy, state->gc1, False);
-#endif
+# endif
 
-#ifdef FUZZY_BORDER
+# ifdef FUZZY_BORDER
   {
     state->gcv.line_width = (int) (((long) state->scale) * 0.8);
     if (state->gcv.line_width >= state->scale)
@@ -522,7 +556,7 @@ capture_font_bits (p_state *state)
                              GCCapStyle | GCLineWidth),
                             &state->gcv);
   }
-#endif /* FUZZY_BORDER */
+# endif /* FUZZY_BORDER */
 
   XFillRectangle (state->dpy, p, state->gc0, 0, 0, (safe_width * 256), height);
 
@@ -537,21 +571,49 @@ capture_font_bits (p_state *state)
   else
 # endif /* BUILTIN_FONT */
     {
+# ifdef USE_XFT_BITMAP
+      Pixmap pm_mono = XCreatePixmap (state->dpy, state->window,
+                                      (safe_width * 256), height,
+                                      1);
+# else  /* !USE_XFT_BITMAP */
       Pixmap pm_color = XCreatePixmap (state->dpy, state->window,
                                        (safe_width * 256), height,
                                        state->xgwa.depth);
-      XImage *xim_color, *xim_mono;
+      XImage *xim_color;
+      int x, y;
+# endif /* !USE_XFT_BITMAP */
+      XImage *xim_mono;
       GC text_gc;
       XGCValues gcv;
       XftDraw *xftdraw;
       XftColor xft_fg;
-      int x, y;
 
-      /* Maybe this should be using XftDrawCreateBitmap instead of 
-         XftDrawCreate to render the font with 1-bit hinting? */
+# ifdef USE_XFT_BITMAP
+      /* Create a 1-bit pixmap and draw the text into it, fg/bg 1/0. */
+
+      /* When using XftDrawCreateBitmap, XftDrawStringUtf8 always draws
+         fg = 0 regardless of what is in the XftColor, so the background
+         must be 1. */
+
+      /* This is working (and looks better) on macOS X11, but no characters
+         are showing up at all on "real" X11 on a Raspberry Pi.  Sigh.
+         It also doesn't work under Cocoa with fake-Xft. */
+
+      gcv.foreground = 1;
+      xft_fg.pixel   = 0;
+      xft_fg.color.red = xft_fg.color.green = xft_fg.color.blue = ~0L;
+
+      text_gc = XCreateGC (state->dpy, pm_mono, GCForeground, &gcv);
+      XFillRectangle (state->dpy, pm_mono, text_gc, 0, 0,
+                      (safe_width * 256), height);
+      xftdraw = XftDrawCreateBitmap (state->dpy, pm_mono);
+
+# else  /* !USE_XFT_BITMAP */
 
       /* Create a full-depth pixmap and draw the text into it, fg/bg ~0/0. */
-      gcv.foreground = 0;
+
+      gcv.foreground =
+        BlackPixelOfScreen (DefaultScreenOfDisplay (state->dpy));
       text_gc = XCreateGC (state->dpy, pm_color, GCForeground, &gcv);
       XFillRectangle (state->dpy, pm_color, text_gc, 0, 0,
                       (safe_width * 256), height);
@@ -559,15 +621,42 @@ capture_font_bits (p_state *state)
                                state->xgwa.visual, state->xgwa.colormap);
       xft_fg.pixel = ~0L;
       xft_fg.color.red = xft_fg.color.green = xft_fg.color.blue = ~0L;
+
+# endif /* !USE_XFT_BITMAP */
+
       for (i = 0; i < 256; i++)
-        XftDrawStringUtf8 (xftdraw, &xft_fg, state->font,
-                           i * safe_width, font->ascent,
-                           (FcChar8 *) (string + i), 1);
+        if (!symbol_p)
+          XftDrawStringUtf8 (xftdraw, &xft_fg, state->font,
+                             i * safe_width, font->ascent,
+                             (FcChar8 *) (string + i), 1);
+        else
+          {
+            unsigned long uc = ansi_graphics_unicode[i];
+            char ss[10];
+            int L = utf8_encode (uc, ss, sizeof(ss)-1);
+            XftDrawStringUtf8 (xftdraw, &xft_fg, state->font,
+                               i * safe_width, font->ascent,
+                               (FcChar8 *) ss, L);
+          }
+
+# ifdef USE_XFT_BITMAP
+      /* Retrieve a 1-bit XImage. */
+      xim_mono = XGetImage (state->dpy, pm_mono, 0, 0, 
+                            i * safe_width, font->ascent,
+                            1, XYPixmap);
+      if (! xim_mono) abort();
 
+      {
+        int i; /* Invert */
+        for (i = 0; i < xim_mono->width * xim_mono->height / 8; i++)
+          xim_mono->data[i] = ~xim_mono->data[i];
+      }
+
+# else  /* !USE_XFT_BITMAP */
       /* Retrieve a full-depth XImage. */
       xim_color = XGetImage (state->dpy, pm_color, 0, 0, 
                              i * safe_width, font->ascent,
-                             state->xgwa.depth, ZPixmap);
+                             ~0L, ZPixmap);
 
       /* Convert it to a mono XImage. */
       xim_mono = XCreateImage (state->dpy, state->xgwa.visual,
@@ -579,60 +668,71 @@ capture_font_bits (p_state *state)
       for (y = 0; y < xim_color->height; y++)
         for (x = 0; x < xim_color->width; x++)
           XPutPixel (xim_mono, x, y, XGetPixel (xim_color, x, y) ? 1 : 0);
+      XDestroyImage (xim_color);
+      xim_color = 0;
+# endif /* !USE_XFT_BITMAP */
 
       /* Copy mono ximage to mono pixmap */
       XFreeGC (state->dpy, text_gc);
-      text_gc = XCreateGC (state->dpy, p, GCForeground, &gcv);
+      gcv.foreground = 1;
+      gcv.background = 0;
+      text_gc = XCreateGC (state->dpy, p, GCForeground|GCBackground, &gcv);
       XPutImage (state->dpy, p, text_gc, xim_mono, 0, 0, 0, 0,
                  (safe_width * 256), height);
       XFreeGC (state->dpy, text_gc);
-      XDestroyImage (xim_color);
       XDestroyImage (xim_mono);
+    }
+
 # if 0
-      XWriteBitmapFile(state->dpy, "/tmp/tvfont.xbm", p, 
-                       (safe_width * 256), height,
-                       -1, -1);
+  XWriteBitmapFile(state->dpy,
+                   (symbol_p ? "/tmp/tvfont2.xbm" : "/tmp/tvfont.xbm"),
+                   p, 
+                   (safe_width * 256), height,
+                   -1, -1);
 # endif
-    }
 
-  /* Draw the cursor. */
-  XFillRectangle (state->dpy, p, state->gc1,
-                  (CURSOR_INDEX * safe_width), 1,
-                  state->char_width,
-                  state->char_height);
+  {
+    XImage *im = XGetImage (state->dpy, p, 0, 0,
+                            (safe_width * 256), height, ~0L, XYPixmap);
+    if (symbol_p)
+      state->sym_font_bits = im;
+    else
+      state->font_bits = im;
+  }
 
-  state->font_bits = XGetImage (state->dpy, p, 0, 0,
-                                (safe_width * 256), height, ~0L, XYPixmap);
   XFreePixmap (state->dpy, p);
 
-  for (i = 0; i < 256; i++)
-    state->chars[i] = make_character (state, i);
-}
-
-
-static p_char *
-make_character (p_state *state, int c)
-{
-  p_char *pc = (p_char *) malloc (sizeof (*pc));
-  pc->name = (unsigned char) c;
-  pc->width =  state->scale * state->char_width;
-  pc->height = state->scale * state->char_height;
-  char_to_pixmap (state, pc, c);
-  return pc;
+  for (i = 0; i < countof(state->chars); i++)
+    {
+      p_char *c1 = make_character (state, i, False, symbol_p);
+      p_char *c2 = make_character (state, i, True,  symbol_p);
+      if (symbol_p)
+        {
+          state->schars [i] = c1;
+          state->sichars[i] = c2;
+        }
+      else
+        {
+          state->chars [i] = c1;
+          state->ichars[i] = c2;
+        }
+    }
 }
 
 
 static void
-char_to_pixmap (p_state *state, p_char *pc, int c)
+char_to_pixmap (p_state *state, p_char *pc, unsigned long c,
+                Bool invert_p, Bool symbol_p)
 {
   Pixmap p = 0;
   GC gc;
-#ifdef FUZZY_BORDER
+# ifdef FUZZY_BORDER
   Pixmap p2 = 0;
   GC gc2;
-#endif /* FUZZY_BORDER */
+# endif /* FUZZY_BORDER */
   int from, to;
   int x1, y;
+  XImage *font_bits = (symbol_p ? state->sym_font_bits : state->font_bits);
 
   int safe_width = state->char_width + 1;
 
@@ -642,61 +742,102 @@ char_to_pixmap (p_state *state, p_char *pc, int c)
   gc = state->gc1;
   p = XCreatePixmap (state->dpy, state->window, width, height, 1);
   XFillRectangle (state->dpy, p, state->gc0, 0, 0, width, height);
-#ifdef FUZZY_BORDER
+# ifdef FUZZY_BORDER
   gc2 = state->gc2;
   p2 = XCreatePixmap (state->dpy, state->window, width, height, 1);
   XFillRectangle (state->dpy, p2, state->gc0, 0, 0, width, height);
-#endif /* FUZZY_BORDER */
+# endif /* FUZZY_BORDER */
 
   from = safe_width * c;
   to =   safe_width * (c + 1);
 
-#if 0
+# if 0
   if (c > 75 && c < 150)
     {
-      printf ("\n=========== %d (%c)\n", c, c);
+      printf ("\n=========== %lu (%c)\n", c, (char) c);
       for (y = 0; y < state->char_height; y++)
         {
           for (x1 = from; x1 < to; x1++)
-            printf (XGetPixel (state->font_bits, x1, y) ? "* " : ". ");
+            printf (XGetPixel (font_bits, x1, y) ? "* " : ". ");
           printf ("\n");
         }
     }
-#endif
+# endif
 
-  pc->blank_p = True;
+  pc->blank_p = !invert_p;
   for (y = 0; y < state->char_height; y++)
     for (x1 = from; x1 < to; x1++)
-      if (XGetPixel (state->font_bits, x1, y))
-        {
-          int xoff = state->scale / 2;
-          int x2;
-          for (x2 = x1; x2 < to; x2++)
-            if (!XGetPixel (state->font_bits, x2, y))
-              break;
-          x2--;
-          XDrawLine (state->dpy, p, gc,
-                     (x1 - from) * state->scale + xoff, y * state->scale,
-                     (x2 - from) * state->scale + xoff, y * state->scale);
-#ifdef FUZZY_BORDER
-          XDrawLine (state->dpy, p2, gc2,
-                     (x1 - from) * state->scale + xoff, y * state->scale,
-                     (x2 - from) * state->scale + xoff, y * state->scale);
-#endif /* FUZZY_BORDER */
-          x1 = x2;
-          pc->blank_p = False;
-        }
-
-  /*  if (pc->blank_p && c == CURSOR_INDEX)
-    abort();*/
+      {
+        unsigned long pix = XGetPixel (font_bits, x1, y);
+        if (invert_p) pix = !pix;
+        if (pix)
+          {
+            int xoff = state->scale / 2;
+            int x2;
+            for (x2 = x1; x2 < to; x2++)
+              {
+                pix = XGetPixel (font_bits, x2, y);
+                if (invert_p) pix = !pix;
+                if (!pix)
+                  break;
+              }
+            x2--;
+            XDrawLine (state->dpy, p, gc,
+                       (x1 - from) * state->scale + xoff, y * state->scale,
+                       (x2 - from) * state->scale + xoff, y * state->scale);
+# ifdef FUZZY_BORDER
+            XDrawLine (state->dpy, p2, gc2,
+                       (x1 - from) * state->scale + xoff, y * state->scale,
+                       (x2 - from) * state->scale + xoff, y * state->scale);
+# endif /* FUZZY_BORDER */
+            x1 = x2;
+            pc->blank_p = False;
+          }
+      }
 
   pc->pixmap = p;
-#ifdef FUZZY_BORDER
+# ifdef FUZZY_BORDER
   pc->pixmap2 = p2;
-#endif /* FUZZY_BORDER */
+# endif /* FUZZY_BORDER */
+
+# ifdef FUZZY_BORDER
+  pc->cache = 0;
+  if (get_boolean_resource (state->dpy, "cache", "Cache"))
+    {
+      /* Cache every possible frame of this character's fade in a pixmap,
+         because XSetClipMask has become *incredibly* fucking slow on
+         "modern" X11 systems (e.g. Raspbian 11.)  I blame compositors. */
+
+      int st;
+      pc->cache = XCreatePixmap (state->dpy, state->window,
+                                 width * state->ticks, height,
+                                 state->xgwa.depth);
+      for (st = 0; st < state->ticks; st++)
+        {
+          int tx = st * width;
+          int ty = 0;
+          GC gc1 = state->gcs[st];
+          GC gc2 = ((st + 2) < state->ticks
+                    ? state->gcs[st + 2]
+                    : 0);
+          GC gc3 = (gc2 ? gc2 : gc1);
+          if (gc3)
+            XCopyPlane (state->dpy, pc->pixmap, pc->cache, gc3,
+                        0, 0, width, height, tx, ty, 1L);
+          if (gc2)
+            {
+              XSetClipMask (state->dpy, gc1, pc->pixmap2);
+              XSetClipOrigin (state->dpy, gc1, tx, ty);
+              XFillRectangle (state->dpy, pc->cache, gc1,
+                              tx, ty, width, height);
+              XSetClipMask (state->dpy, gc1, None);
+            }
+        }
+    }
+# endif /* FUZZY_BORDER */
 }
 
-\f
+
 /* Managing the display. 
  */
 
@@ -706,20 +847,13 @@ static void cursor_off_timer (XtPointer closure, XtIntervalId *id);
 static Bool
 set_cursor_1 (p_state *state, Bool on)
 {
-  p_cell *cell = &state->cells[state->grid_width * state->cursor_y
-                              + state->cursor_x];
-  p_char *cursor = state->chars[CURSOR_INDEX];
-  int new_state = (on ? NORMAL : FADE);
-
-  if (cell->p_char != cursor)
-    cell->changed = True;
-
-  if (cell->state != new_state)
-    cell->changed = True;
-
-  cell->p_char = cursor;
-  cell->state = new_state;
-  return cell->changed;
+  p_cell *cell = &state->cells[state->grid_width * state->cursor_y +
+                               state->cursor_x];
+  if (state->cursor_on == on)
+    return False;
+  state->cursor_on = on;
+  cell->changed = True;
+  return True;
 }
 
 static void
@@ -756,26 +890,6 @@ cursor_on_timer (XtPointer closure, XtIntervalId *id)
 }
 
 
-static void
-clear (p_state *state)
-{
-  int x, y;
-  state->cursor_x = 0;
-  state->cursor_y = 0;
-  for (y = 0; y < state->grid_height; y++)
-    for (x = 0; x < state->grid_width; x++)
-      {
-        p_cell *cell = &state->cells[state->grid_width * y + x];
-        if (cell->state == FLARE || cell->state == NORMAL)
-          {
-            cell->state = FADE;
-            cell->changed = True;
-          }
-      }
-  set_cursor (state, True);
-}
-
-
 static void
 decay (p_state *state)
 {
@@ -793,7 +907,10 @@ decay (p_state *state)
           {
             cell->state++;
             if (cell->state >= state->ticks)
-              cell->state = BLANK;
+              {
+                cell->state = BLANK;
+                cell->c = ' ';
+              }
             cell->changed = True;
           }
       }
@@ -801,536 +918,95 @@ decay (p_state *state)
 
 
 static void
-scroll (p_state *state)
+print_char (p_state *state, int c)
 {
   int x, y;
+  ansi_tty *tty = state->tty;
+  ansi_tty_print (tty, c);
 
-  for (x = 0; x < state->grid_width; x++)
-    {
-      p_cell *from = 0, *to = 0;
-      for (y = 1; y < state->grid_height; y++)
-        {
-          from = &state->cells[state->grid_width * y     + x];
-          to   = &state->cells[state->grid_width * (y-1) + x];
+  for (y = 0; y < tty->height; y++)
+    for (x = 0; x < tty->width; x++)
+      {
+        tty_char *tcell = &tty->grid [tty->width * y + x];
+        p_cell   *cell  = &state->cells [state->grid_width * y + x];
+        Bool inv_p = tcell->flags & (TTY_BOLD | TTY_ITALIC | TTY_INVERSE);
+        Bool sym_p = tcell->flags & (TTY_SYMBOLS);
+        unsigned char latin1 = 0;
+        p_char *pc;
 
-          if ((from->state == FLARE || from->state == NORMAL) &&
-              !from->p_char->blank_p)
-            {
-              *to = *from;
-              to->state = NORMAL;  /* should be FLARE?  Looks bad... */
-            }
-          else
-            {
-              if (to->state == FLARE || to->state == NORMAL)
-                to->state = FADE;
-            }
+        if (tty->inverse_p) inv_p = !inv_p;
 
-          to->changed = True;
-        }
+        if ( cell->c == 0)  cell->c = ' ';
+        if (tcell->c == 0) tcell->c = ' ';
 
-      to = from;
-      if (to && (to->state == FLARE || to->state == NORMAL))
-        {
-          to->state = FADE;
-          to->changed = True;
-        }
-    }
-  set_cursor (state, True);
-}
+        if (tcell->c <= 128)
+          latin1 = tcell->c;
+        else
+          {
+            /* Convert non-ASCII Unicode to closest Latin1. */
+            char utf8[10];
+            if (utf8_encode (tcell->c, utf8, sizeof(utf8)-1))
+              {
+                char *s = utf8_to_latin1 (utf8, FALSE);
+                if (s)
+                  {
+                    latin1 = s[0];
+                    free (s);
+                  }
+              }
+          }
 
+        if (!latin1) latin1 = ' ';
 
-static int
-process_unicrud (p_state *state, int c)
-{
-  if ((c & 0xE0) == 0xC0) {        /* 110xxxxx: 11 bits, 2 bytes */
-    state->unicruds = 1;
-    state->unicrud[0] = c;
-    state->escstate = 102;
-  } else if ((c & 0xF0) == 0xE0) { /* 1110xxxx: 16 bits, 3 bytes */
-    state->unicruds = 1;
-    state->unicrud[0] = c;
-    state->escstate = 103;
-  } else if ((c & 0xF8) == 0xF0) { /* 11110xxx: 21 bits, 4 bytes */
-    state->unicruds = 1;
-    state->unicrud[0] = c;
-    state->escstate = 104;
-  } else if ((c & 0xFC) == 0xF8) { /* 111110xx: 26 bits, 5 bytes */
-    state->unicruds = 1;
-    state->unicrud[0] = c;
-    state->escstate = 105;
-  } else if ((c & 0xFE) == 0xFC) { /* 1111110x: 31 bits, 6 bytes */
-    state->unicruds = 1;
-    state->unicrud[0] = c;
-    state->escstate = 106;
-  } else if (state->unicruds == 0) {
-    return c;
-  } else {
-    int total = state->escstate - 100;  /* see what I did there */
-    if (state->unicruds < total) {
-      /* Buffer more bytes of the UTF-8 sequence */
-      state->unicrud[state->unicruds++] = c;
-    }
+        pc = (cell->symbol_p
+              ? (inv_p ? state->sichars[cell->c] : state->schars[cell->c])
+              : (inv_p ?  state->ichars[cell->c] :  state->chars[cell->c]));
 
-    if (state->unicruds >= total) {
-      /* Done! Convert it to Latin1 and print that. */
-      char *s;
-      state->unicrud[state->unicruds] = 0;
-      s = utf8_to_latin1 ((const char *) state->unicrud, False);
-      state->unicruds = 0;
-      state->escstate = 0;
-      if (s) {
-        c = (unsigned char) s[0];
-        free (s);
-        return c;
+        if (!pc->blank_p &&
+            latin1 == ' ' &&
+            !inv_p)
+          {
+            /* Replacing existing character with a blank:
+               fade out the previous character. */
+            if (cell->state == FLARE || cell->state == NORMAL)
+              cell->state = FADE;
+            cell->changed = True;
+          }
+        else if (cell->c != tcell->c ||
+                 cell->state >= FADE ||
+                 cell->invert_p != inv_p ||
+                 cell->symbol_p != sym_p)
+          {
+            /* Adding a new character: flare it in. */
+            cell->invert_p = inv_p;
+            cell->symbol_p = sym_p;
+            /* cell->state = FLARE;  -- Looks bad when scrolling */
+            cell->state = NORMAL;
+            cell->changed = True;
+            cell->c = latin1;
+          }
       }
-    }
-  }
-  return 0;
-}
-
 
-static void
-print_char (p_state *state, int c)
-{
-  int cols = state->grid_width;
-  int rows = state->grid_height;
-  p_cell *cell = &state->cells[state->grid_width * state->cursor_y
-                              + state->cursor_x];
-
-  /* Start the cursor fading (in case we don't end up overwriting it.) */
-  if (cell->state == FLARE || cell->state == NORMAL)
-    {
-      cell->state = FADE;
-      cell->changed = True;
-    }
-  
-#ifdef HAVE_FORKPTY
-  if (state->pty_p) /* Only interpret VT100 sequences if running in pty-mode.
-                       It would be nice if we could just interpret them all
-                       the time, but that would require subprocesses to send
-                       CRLF line endings instead of bare LF, so that's no good.
-                     */
-    {
-      int i;
-      int start, end;
-
-      /* Mostly duplicated in apple2-main.c */
-
-      switch (state->escstate)
-       {
-       case 0:
-         switch (c)
-           {
-           case 7: /* BEL */
-             /* Dummy case - we don't want the screensaver to beep */
-              /* #### But maybe this should flash the screen? */
-             break;
-           case 8: /* BS */
-             if (state->cursor_x > 0)
-               state->cursor_x--;
-             break;
-           case 9: /* HT */
-             if (state->cursor_x < cols - 8)
-               {
-                 state->cursor_x = (state->cursor_x & ~7) + 8;
-               }
-             else
-               {
-                 state->cursor_x = 0;
-                 if (state->cursor_y < rows - 1)
-                   state->cursor_y++;
-                 else
-                   scroll (state);
-               }
-             break;
-           case 10: /* LF */
-# ifndef HAVE_FORKPTY
-              state->cursor_x = 0;     /* No ptys on iPhone; assume CRLF. */
-# endif
-           case 11: /* VT */
-           case 12: /* FF */
-             if(state->last_c == 13)
-               {
-                 cell->state = NORMAL;
-                 cell->p_char = state->chars[state->bk];
-                 cell->changed = True;
-               }
-             if (state->cursor_y < rows - 1)
-               state->cursor_y++;
-             else
-               scroll (state);
-             break;
-           case 13: /* CR */
-             state->cursor_x = 0;
-             cell = &state->cells[cols * state->cursor_y];
-             if((cell->p_char == NULL) || (cell->p_char->name == CURSOR_INDEX))
-               state->bk = ' ';
-             else
-               state->bk = cell->p_char->name;
-             break;
-           case 14: /* SO */
-           case 15: /* SI */
-              /* Dummy case - there is one and only one font. */
-             break;
-           case 24: /* CAN */
-           case 26: /* SUB */
-             /* Dummy case - these interrupt escape sequences, so
-                they don't do anything in this state */
-             break;
-           case 27: /* ESC */
-             state->escstate = 1;
-             break;
-           case 127: /* DEL */
-             /* Dummy case - this is supposed to be ignored */
-             break;
-           case 155: /* CSI */
-             state->escstate = 2;
-             for(i = 0; i < NPAR; i++)
-               state->csiparam[i] = 0;
-             state->curparam = 0;
-             break;
-           default:
-
-            PRINT: /* Come from states 102-106 */
-              c = process_unicrud (state, c);
-              if (! c)
-                break;
-
-              /* If the cursor is in column 39 and we print a character, then
-                 that character shows up in column 39, and the cursor is no
-                 longer visible on the screen (it's in "column 40".)  If
-                 another character is printed, then that character shows up in
-                 column 0, and the cursor moves to column 1.
-
-                 This is empirically what xterm and gnome-terminal do, so that
-                 must be the right thing.  (In xterm, the cursor vanishes,
-                 whereas; in gnome-terminal, the cursor overprints the
-                 character in col 39.)
-               */
-             cell->state = FLARE;
-             cell->p_char = state->chars[c];
-             cell->changed = True;
-             state->cursor_x++;
-
-             if (c != ' ' && cell->p_char->blank_p)
-               cell->p_char = state->chars[CURSOR_INDEX];
-
-             if (state->cursor_x >= cols - 1 /*####*/)
-               {
-                 state->cursor_x = 0;
-                 if (state->cursor_y >= rows - 1)
-                   scroll (state);
-                 else
-                   state->cursor_y++;
-               }
-             break;
-           }
-         break;
-       case 1:
-         switch (c)
-           {
-           case 24: /* CAN */
-           case 26: /* SUB */
-             state->escstate = 0;
-             break;
-           case 'c': /* Reset */
-             clear (state);
-             state->escstate = 0;
-             break;
-           case 'D': /* Linefeed */
-             if (state->cursor_y < rows - 1)
-               state->cursor_y++;
-             else
-               scroll (state);
-             state->escstate = 0;
-             break;
-           case 'E': /* Newline */
-             state->cursor_x = 0;
-             state->escstate = 0;
-             break;
-           case 'M': /* Reverse newline */
-             if (state->cursor_y > 0)
-               state->cursor_y--;
-             state->escstate = 0;
-             break;
-           case '7': /* Save state */
-             state->saved_x = state->cursor_x;
-             state->saved_y = state->cursor_y;
-             state->escstate = 0;
-             break;
-           case '8': /* Restore state */
-             state->cursor_x = state->saved_x;
-             state->cursor_y = state->saved_y;
-             state->escstate = 0;
-             break;
-           case '[': /* CSI */
-             state->escstate = 2;
-             for(i = 0; i < NPAR; i++)
-               state->csiparam[i] = 0;
-             state->curparam = 0;
-             break;
-            case '%': /* Select charset */
-              /* @: Select default (ISO 646 / ISO 8859-1)
-                 G: Select UTF-8
-                 8: Select UTF-8 (obsolete)
-
-                 We can just ignore this and always process UTF-8, I think?
-                 We must still catch the last byte, though.
-               */
-           case '(':
-           case ')':
-             /* I don't support different fonts either - see above
-                for SO and SI */
-             state->escstate = 3;
-             break;
-           default:
-             /* Escape sequences not supported:
-              * 
-              * H - Set tab stop
-              * Z - Terminal identification
-              * > - Keypad change
-              * = - Other keypad change
-              * ] - OS command
-              */
-             state->escstate = 0;
-             break;
-           }
-         break;
-       case 2:
-         switch (c)
-           {
-           case 24: /* CAN */
-           case 26: /* SUB */
-             state->escstate = 0;
-             break;
-            case '0': case '1': case '2': case '3': case '4':
-            case '5': case '6': case '7': case '8': case '9':
-             if (state->curparam < NPAR)
-               state->csiparam[state->curparam] = (state->csiparam[state->curparam] * 10) + (c - '0');
-             break;
-           case ';':
-             state->csiparam[++state->curparam] = 0;
-             break;
-           case '[':
-             state->escstate = 3;
-             break;
-           case '@':
-             for (i = 0; i < state->csiparam[0]; i++)
-               {
-                 if(++state->cursor_x > cols)
-                   {
-                     state->cursor_x = 0;
-                     if (state->cursor_y < rows - 1)
-                       state->cursor_y++;
-                     else
-                       scroll (state);
-                   }
-                 cell = &state->cells[cols * state->cursor_y + state->cursor_x];
-                 if (cell->state == FLARE || cell->state == NORMAL)
-                   {
-                     cell->state = FADE;
-                     cell->changed = True;
-                   }
-               }
-             state->escstate = 0;
-             break;
-           case 'F':
-             state->cursor_x = 0;
-           case 'A':
-             if (state->csiparam[0] == 0)
-               state->csiparam[0] = 1;
-             if ((state->cursor_y -= state->csiparam[0]) < 0)
-               state->cursor_y = 0;
-             state->escstate = 0;
-             break;
-           case 'E':
-             state->cursor_x = 0;
-           case 'e':
-           case 'B':
-             if (state->csiparam[0] == 0)
-               state->csiparam[0] = 1;
-             if ((state->cursor_y += state->csiparam[0]) >= rows - 1 /*####*/)
-               state->cursor_y = rows - 1;
-             state->escstate = 0;
-             break;
-           case 'a':
-           case 'C':
-             if (state->csiparam[0] == 0)
-               state->csiparam[0] = 1;
-             if ((state->cursor_x += state->csiparam[0]) >= cols - 1 /*####*/)
-               state->cursor_x = cols - 1;
-             state->escstate = 0;
-             break;
-           case 'D':
-             if (state->csiparam[0] == 0)
-               state->csiparam[0] = 1;
-             if ((state->cursor_x -= state->csiparam[0]) < 0)
-               state->cursor_x = 0;
-             state->escstate = 0;
-             break;
-           case 'd':
-             if ((state->cursor_y = (state->csiparam[0] - 1)) >= rows - 1 /*####*/)
-               state->cursor_y = rows - 1;
-             state->escstate = 0;
-             break;
-           case '`':
-           case 'G':
-             if ((state->cursor_x = (state->csiparam[0] - 1)) >= cols - 1 /*####*/)
-               state->cursor_x = cols - 1;
-             state->escstate = 0;
-             break;
-           case 'f':
-           case 'H':
-             if ((state->cursor_y = (state->csiparam[0] - 1)) >= rows - 1 /*####*/)
-               state->cursor_y = rows - 1;
-             if ((state->cursor_x = (state->csiparam[1] - 1)) >= cols - 1 /*####*/)
-               state->cursor_x = cols - 1;
-             if(state->cursor_y < 0)
-               state->cursor_y = 0;
-             if(state->cursor_x < 0)
-               state->cursor_x = 0;
-             state->escstate = 0;
-             break;
-           case 'J':
-             start = 0;
-             end = rows * cols;
-             if (state->csiparam[0] == 0)
-               start = cols * state->cursor_y + state->cursor_x;
-             if (state->csiparam[0] == 1)
-               end = cols * state->cursor_y + state->cursor_x;
-             for (i = start; i < end; i++)
-               {
-                 cell = &state->cells[i];
-                 if (cell->state == FLARE || cell->state == NORMAL)
-                   {
-                     cell->state = FADE;
-                     cell->changed = True;
-                   }
-               }
-             set_cursor (state, True);
-             state->escstate = 0;
-             break;
-           case 'K':
-             start = 0;
-             end = cols;
-             if (state->csiparam[0] == 0)
-               start = state->cursor_x;
-             if (state->csiparam[1] == 1)
-               end = state->cursor_x;
-             for (i = start; i < end; i++)
-               {
-                 if (cell->state == FLARE || cell->state == NORMAL)
-                   {
-                     cell->state = FADE;
-                     cell->changed = True;
-                   }
-                 cell++;
-               }
-             state->escstate = 0;
-             break;
-            case 'm': /* Set attributes unimplemented (bold, blink, rev) */
-              state->escstate = 0;
-              break;
-           case 's': /* Save position */
-             state->saved_x = state->cursor_x;
-             state->saved_y = state->cursor_y;
-             state->escstate = 0;
-             break;
-           case 'u': /* Restore position */
-             state->cursor_x = state->saved_x;
-             state->cursor_y = state->saved_y;
-             state->escstate = 0;
-             break;
-           case '?': /* DEC Private modes */
-             if ((state->curparam != 0) || (state->csiparam[0] != 0))
-               state->escstate = 0;
-             break;
-           default:
-             /* Known unsupported CSIs:
-              *
-              * L - Insert blank lines
-              * M - Delete lines (I don't know what this means...)
-              * P - Delete characters
-              * X - Erase characters (difference with P being...?)
-              * c - Terminal identification
-              * g - Clear tab stop(s)
-              * h - Set mode (Mainly due to its complexity and lack of good
-                     docs)
-              * l - Clear mode
-              * m - Set mode (Phosphor is, per defenition, green on black)
-              * n - Status report
-              * q - Set keyboard LEDs
-              * r - Set scrolling region (too exhausting - noone uses this,
-                     right?)
-              */
-             state->escstate = 0;
-             break;
-           }
-         break;
-       case 3:
-         state->escstate = 0;
-         break;
-
-        case 102:      /* states 102-106 are for UTF-8 decoding */
-        case 103:
-        case 104:
-        case 105:
-        case 106:
-          goto PRINT;
-
-        default:
-          abort();
-       }
-      set_cursor (state, True);
-    }
-  else
-#endif /* HAVE_FORKPTY */
+  /* If the cursor has moved, turn it on and flare it. */
+  if (state->cursor_x != tty->x ||
+      state->cursor_y != tty->y)
     {
-      if (c == '\t') c = ' ';   /* blah. */
-
-      if (c == '\r' || c == '\n')  /* handle CR, LF, or CRLF as "new line". */
-       {
-         if (c == '\n' && state->last_c == '\r')
-           ;   /* CRLF -- do nothing */
-         else
-           {
-             state->cursor_x = 0;
-             if (state->cursor_y == rows - 1)
-               scroll (state);
-             else
-               state->cursor_y++;
-           }
-       }
-      else if (c == '\014')
-       {
-         clear (state);
-       }
-      else
-       {
-          c = process_unicrud (state, c);
-          if (!c) return;
-
-         cell->state = FLARE;
-         cell->p_char = state->chars[c];
-         cell->changed = True;
-         state->cursor_x++;
-
-         if (c != ' ' && cell->p_char->blank_p)
-           cell->p_char = state->chars[CURSOR_INDEX];
-
-         if (state->cursor_x >= cols - 1)
-           {
-             state->cursor_x = 0;
-             if (state->cursor_y >= rows - 1)
-               scroll (state);
-             else
-               state->cursor_y++;
-           }
-       }
+      p_cell *oc = &state->cells [state->grid_width * state->cursor_y +
+                                  state->cursor_x];
+      p_cell *nc = &state->cells [state->grid_width * tty->y +
+                                  tty->x];
+      oc->changed = True;
+      nc->changed = True;
+
+      /* If the new cell is already fading out, do not bring its old
+         character back underneath this new cursor. */
+      if (nc->state >= FADE)
+        nc->c = ' ';
+      nc->state = FLARE;
+      state->cursor_x = tty->x;
+      state->cursor_y = tty->y;
       set_cursor (state, True);
     }
-
-  state->last_c = c;
 }
 
 
@@ -1343,48 +1019,76 @@ update_display (p_state *state, Bool changed_only)
     for (x = 0; x < state->grid_width; x++)
       {
         p_cell *cell = &state->cells[state->grid_width * y + x];
+        int st = cell->state;
+        Bool inv_p = cell->invert_p;
+        Bool sym_p = cell->symbol_p;
+        Bool cursor_p = (x == state->cursor_x && y == state->cursor_y);
+        unsigned char c = cell->c;
+        p_char *pc;
         int width, height, tx, ty;
 
         if (changed_only && !cell->changed)
           continue;
 
+        if (cursor_p)
+          {
+            if (state->cursor_on)
+              inv_p = !inv_p;
+            if (st >= FADE)
+              c = ' ';
+            if (st == BLANK || st >= FADE)
+              st = NORMAL;
+          }
+
+        if (!c) c = ' ';
+        pc = (sym_p
+              ? (inv_p ? state->sichars[c] : state->schars[c])
+              : (inv_p ?  state->ichars[c] :  state->chars[c]));
+
         width  = state->char_width  * state->scale;
         height = state->char_height * state->scale;
         tx = x * width  + state->xmargin;
         ty = y * height + state->ymargin;
 
-        if (cell->state == BLANK || cell->p_char->blank_p)
+        if (pc->blank_p || (cell->state == BLANK && !cursor_p))
           {
             XFillRectangle (state->dpy, state->window, state->gcs[BLANK],
                             tx, ty, width, height);
           }
+# ifdef FUZZY_BORDER
+        else if (pc->cache)
+          {
+            int src_x = st * width;
+            int src_y = 0;
+            XCopyArea (state->dpy, pc->cache, state->window, state->gcs[BLANK],
+                       src_x, src_y, width, height, tx, ty);
+          }
+# endif /* FUZZY_BORDER */
         else
           {
-#ifdef FUZZY_BORDER
-            GC gc1 = state->gcs[cell->state];
-            GC gc2 = ((cell->state + 2) < state->ticks
-                      ? state->gcs[cell->state + 2]
+# ifdef FUZZY_BORDER
+            GC gc1 = state->gcs[st];
+            GC gc2 = ((st + 2) < state->ticks
+                      ? state->gcs[st + 2]
                       : 0);
             GC gc3 = (gc2 ? gc2 : gc1);
             if (gc3)
-              XCopyPlane (state->dpy, cell->p_char->pixmap, state->window, gc3,
+              XCopyPlane (state->dpy, pc->pixmap, state->window, gc3,
                           0, 0, width, height, tx, ty, 1L);
             if (gc2)
               {
-                XSetClipMask (state->dpy, gc1, cell->p_char->pixmap2);
+                XSetClipMask (state->dpy, gc1, pc->pixmap2);
                 XSetClipOrigin (state->dpy, gc1, tx, ty);
                 XFillRectangle (state->dpy, state->window, gc1,
                                 tx, ty, width, height);
                 XSetClipMask (state->dpy, gc1, None);
               }
-#else /* !FUZZY_BORDER */
-
+# else /* !FUZZY_BORDER */
             XCopyPlane (state->dpy,
-                        cell->p_char->pixmap, state->window,
-                        state->gcs[cell->state],
+                        pc->pixmap, state->window,
+                        state->gcs[st],
                         0, 0, width, height, tx, ty, 1L);
-
-#endif /* !FUZZY_BORDER */
+# endif /* !FUZZY_BORDER */
           }
 
         cell->changed = False;
@@ -1417,15 +1121,25 @@ phosphor_reshape (Display *dpy, Window window, void *closure,
 
   if (! changed_p) return;
 
+  ansi_tty_resize (state->tty, state->grid_width, state->grid_height);
+
   textclient_reshape (state->tc,
                       w - state->xmargin * 2,
                       h - state->ymargin * 2,
-                      state->grid_width  - 1,
-                      state->grid_height - 1,
+                      state->grid_width,
+                      state->grid_height,
                       0);
 }
 
 
+static void
+phosphor_tty_send (void *closure, const char *text)
+{
+  p_state *state = (p_state *) closure;
+  textclient_puts (state->tc, text);
+}
+
+
 static Bool
 phosphor_event (Display *dpy, Window window, void *closure, XEvent *event)
 {
@@ -1434,7 +1148,7 @@ phosphor_event (Display *dpy, Window window, void *closure, XEvent *event)
   if (event->xany.type == Expose)
     update_display (state, False);
   else if (event->xany.type == KeyPress)
-    return textclient_putc (state->tc, &event->xkey);
+    return textclient_putc_event (state->tc, &event->xkey);
   return False;
 }
 
@@ -1448,23 +1162,31 @@ phosphor_free (Display *dpy, Window window, void *closure)
   if (state->cursor_timer)
     XtRemoveTimeOut (state->cursor_timer);
 
+  ansi_tty_free (state->tty);
+
   if (state->gc0) XFreeGC (dpy, state->gc0);
   if (state->gc1) XFreeGC (dpy, state->gc1);
-#ifdef FUZZY_BORDER
+# ifdef FUZZY_BORDER
   if (state->gc2) XFreeGC (dpy, state->gc2);
-#endif /* FUZZY_BORDER */
+# endif /* FUZZY_BORDER */
   for (i = 0; i < state->ticks; i++)
     if (state->gcs[i]) XFreeGC (dpy, state->gcs[i]);
   free (state->gcs);
-  for (i = 0; i < 256; i++) {
+  for (i = 0; i < countof(state->chars); i++) {
     XFreePixmap (dpy, state->chars[i]->pixmap);
-#ifdef FUZZY_BORDER
+    XFreePixmap (dpy, state->ichars[i]->pixmap);
+# ifdef FUZZY_BORDER
     XFreePixmap (dpy, state->chars[i]->pixmap2);
-#endif /* FUZZY_BORDER */
+    XFreePixmap (dpy, state->ichars[i]->pixmap2);
+    if (state->chars[i]->cache)
+      XFreePixmap (dpy, state->chars[i]->cache);
+    if (state->ichars[i]->cache)
+      XFreePixmap (dpy, state->ichars[i]->cache);
+# endif /* FUZZY_BORDER */
     free (state->chars[i]);
   }
   XDestroyImage (state->font_bits);
-  free (state->chars);
+  XDestroyImage (state->sym_font_bits);
   free (state->cells);
   free (state);
 }
@@ -1472,17 +1194,10 @@ phosphor_free (Display *dpy, Window window, void *closure)
 
 
 static const char *phosphor_defaults [] = {
-/*  ".lowrez:                true", */
+/*".lowrez:                true", */
   ".background:                   Black",
   ".foreground:                   #00FF00",
   "*fpsSolid:             true",
-#if defined(BUILTIN_FONT)
-  "*font:                 (builtin)",
-#elif defined(HAVE_COCOA)
-  "*font:                 Monaco 15",
-#else
-  "*font:                 fixed",
-#endif
   "*phosphorScale:        6",
   "*ticks:                20",
   "*delay:                50000",
@@ -1491,11 +1206,35 @@ static const char *phosphor_defaults [] = {
   "*relaunch:             5",
   "*metaSendsESC:         True",
   "*swapBSDEL:            True",
-#ifdef HAVE_FORKPTY
+
+# if defined(BUILTIN_FONT)
+  "*font:                 (builtin)",
+# elif defined(HAVE_COCOA)
+  "*font:                 Monaco 15",
+# else
+  "*font:                 fixed",
+# endif
+
+# ifdef HAVE_JWXYZ
+  "*cache:                False",
+# else
+  "*cache:                True",
+# endif
+
+# ifdef HAVE_FORKPTY
   "*usePty:                True",
-#else  /* !HAVE_FORKPTY */
+# else
   "*usePty:                False",
-#endif /* !HAVE_FORKPTY */
+# endif
+
+  /* For debugging vt100 at 80x24: */
+  /*
+  "*delay: 0",
+  "*phosphorScale: 3",
+  ".geometry: =1470x760",
+  "*program: echo $COLUMNS x $ROWS ; /usr/local/bin/vttest",
+  */
+
   0
 };
 
@@ -1511,6 +1250,8 @@ static XrmOptionDescRec phosphor_options [] = {
   { "-esc",            ".metaSendsESC",        XrmoptionNoArg, "True"  },
   { "-bs",             ".swapBSDEL",           XrmoptionNoArg, "False" },
   { "-del",            ".swapBSDEL",           XrmoptionNoArg, "True"  },
+  { "-cache",          ".cache",               XrmoptionNoArg, "True"  },
+  { "-no-cache",       ".cache",               XrmoptionNoArg, "False" },
   { 0, 0, 0, 0 }
 };
 
index 4f8dc157b499b806464b9663422917d45b435302..af4d7e12273b55afad7de5e71099df701d9db929 100644 (file)
@@ -6,7 +6,8 @@ phosphor \- simulates an old terminal with long-sustain phosphor
 [\-\-display \fIhost:display.screen\fP] [\-\-window] [\-\-root]
 [\-\-window\-id \fInumber\fP][\-\-install]
 [\-\-visual \fIvisual\fP] [\-\-font \fIfont\fP] [\-\-scale \fIint\fP]
-[\-\-ticks \fIint\fP] [\-\-delay \fIusecs\fP] [\-\-program \fIcommand\fP]
+[\-\-ticks \fIint\fP] [\-\-delay \fIusecs\fP] [\-\-no\-cache]
+[\-\-program \fIcommand\fP]
 [\-\-meta] [\-\-esc] [\-\-bs] [\-\-del]
 [\-\-fps]
 .SH DESCRIPTION
@@ -49,6 +50,9 @@ The number of colors to use when fading to black.  Default 20.
 The speed of the terminal: how long to wait between drawing each character.
 Default 50000, or about 1/20th second.
 .TP 8
+.B \-\-cache | \-\-no-cache
+Speed up rendering at the expense of memory and startup time. Default yes.
+.TP 8
 .B \-\-pty
 Launch the sub-program under a PTY, so that it can address the screen
 directly.  This is the default.
index c80199bd09ff6b728320b1a53f171048fb2624f6..6a582e07536b6cd8aad6cbf826e0dbea1046d636 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright © 1992-2022 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 1992-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -1021,7 +1021,50 @@ main (int argc, char **argv)
 
 #ifdef HAVE_RECORD_ANIM
   {
-    int frames = get_integer_resource (dpy, "recordAnim", "Integer");
+    char *str = get_string_resource (dpy, "recordAnim", "Time");
+    int fps = 30;
+    int h = 0, m = 0, s = 0;
+    int frames = 0;
+    char c, suf[20];
+    *suf = 0;
+
+    if (!str || !*str)
+      ;
+    else if (3 == sscanf (str, " %d:%d:%d %c", &h, &m, &s, &c))  /* H:MM:SS */
+      frames = fps * (h*60*60 + m*60 + s);
+    else if (2 == sscanf (str,    " %d:%d %c",     &m, &s, &c))  /*    M:SS */
+      frames = fps * (m*60 + s);
+    else if (1 == sscanf (str,       " %d %c",         &s, &c))  /* frames  */
+      frames = s;
+    else if (2 == sscanf (str, "%d %10s %c", &h, suf, &c))
+      {
+        if (!strcasecmp (suf, "h") ||                           /* 1 H     */
+            !strcasecmp (suf, "hour") ||
+            !strcasecmp (suf, "hours"))
+          frames = fps * h*60*60;
+        else if (!strcasecmp (suf, "m") ||                      /* 2 min   */
+                 !strcasecmp (suf, "min") ||
+                 !strcasecmp (suf, "mins") ||
+                 !strcasecmp (suf, "minute") ||
+                 !strcasecmp (suf, "minutes"))
+          frames = fps * h*60;
+        else if (!strcasecmp (suf, "s") ||                      /* 30 sec  */
+                 !strcasecmp (suf, "sec") ||
+                 !strcasecmp (suf, "secs") ||
+                 !strcasecmp (suf, "second") ||
+                 !strcasecmp (suf, "seconds"))
+          frames = fps * h;
+        else
+          goto FAIL;
+      }
+    else
+      {
+      FAIL:
+        fprintf (stderr, "%s: unparsable duration: %s\n", progname, str);
+        exit (1);
+      }
+
+    if (str) free (str);
     if (frames > 0)
       anim_state = screenhack_record_anim_init (xgwa.screen, window, frames);
   }
index 11a9d7d40519dab0e8f82b8f11aabaa8ca39d97d..dab3e878ab99937efabcf7bed245dafe3835ae48 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright © 1992-2023 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 1992-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
 #   include <GL/glx.h>
 #  endif
 
+# ifndef HAVE_JWZGLES
    /* Sep 2022, Sep 2023: The Raspberry Pi 4b Broadcom driver doesn't do
       GL_LINE_SMOOTH properly, so we must disable it. See init_GL(). */
    extern void (* glEnable_fn) (GLuint);
 #  define glEnable (* glEnable_fn)
+# endif /* !HAVE_JWZGLES */
 
 # endif /* real X11 */
 
index 75e7106e515c18fa3aebdabd7e02063d7840efb0..0bfe756e541aa12a11853333d2808424d8203ca7 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -w
 #
-# webcollage, Copyright © 1999-2024 Jamie Zawinski <jwz@jwz.org>
+# webcollage, Copyright © 1999-2025 Jamie Zawinski <jwz@jwz.org>
 # This program decorates the screen with random images from the web.
 # One satisfied customer described it as "a nonstop pop culture brainbath."
 #
@@ -47,7 +47,7 @@ use LWP::UserAgent;
 
 
 my $progname = $0; $progname =~ s@.*/@@g;
-my ($version) = ('$Revision: 1.193 $' =~ m/\s(\d[.\d]+)\s/s);
+my ($version) = ('$Revision: 1.195 $' =~ m/\s(\d[.\d]+)\s/s);
 my $copyright = "WebCollage $version, Copyright © 1999-2024" .
     " Jamie Zawinski <jwz\@jwz.org>\n" .
     "                  https://www.jwz.org/webcollage/\n";
@@ -99,24 +99,25 @@ my @search_methods = (
                       # a short-running screen saver, but not as a batch job.
                       # I haven't found a workaround.
                       #
-                        5, "googlephotos",  \&pick_from_google_image_photos,
-                        3, "googleimgs",    \&pick_from_google_images,
-                        3, "googlenums",    \&pick_from_google_image_numbers,
+                        4, "googlephotos",  \&pick_from_google_image_photos,
+                        2, "googleimgs",    \&pick_from_google_images,
+                        2, "googlenums",    \&pick_from_google_image_numbers,
 
                       # So let's try Bing instead. No rate limiting yet!
                       #
                        13, "bingphotos",    \&pick_from_bing_image_photos,
-                       10, "bingimgs",      \&pick_from_bing_images,
-                        9, "bingnums",      \&pick_from_bing_image_numbers,
+                        9, "bingimgs",      \&pick_from_bing_images,
+                        8, "bingnums",      \&pick_from_bing_image_numbers,
 
                        20, "flickr_recent", \&pick_from_flickr_recent,
                        15, "flickr_random", \&pick_from_flickr_random,
-                        6, "livejournal",   \&pick_from_livejournal_images,
+                       10, "flickr_commons",\&pick_from_flickr_commons,
 
                        11, "imgur",         \&pick_from_imgur,
+                        2, "livejournal",   \&pick_from_livejournal_images,
 
                      # Tumblr doesn't have an "or" search, so this isn't great.
-                        3, "tumblr",        \&pick_from_tumblr,
+                        2, "tumblr",        \&pick_from_tumblr,
 
                      # I ran out of usable access tokens, May 2017
                      #  0, "instagram",     \&pick_from_instagram,
@@ -401,7 +402,7 @@ sub get_document_1($$$) {
       $url =~ m@^https?://random\.yahoo\.com/@s  ||
       $url =~ m@^https?://[^./]+\.google\.com/@s ||
       $url =~ m@^https?://www\.livejournal\.com/@s ||
-      $url =~ m@^https?://imgur\.com/@s) {
+      $url =~ m@^https?://([^./]+\.)?imgur\.com/@s) {
     # block this, you turkeys.
     $user_agent = 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.7)' .
                   ' Gecko/20070914 Firefox/2.0.0.7';
@@ -435,6 +436,12 @@ sub get_document_1($$$) {
                         print "\n<<[[\n"; $req->dump; print "\n]]\n";
                         return;
                       });
+    $ua->add_handler ("response_done",
+                      sub($$$) {
+                        my ($req, $ua, $h) = @_;
+                        print "\n<<[[\n"; $req->dump; print "\n]]\n";
+                        return;
+                      });
   }
 
   if ($verbose_http) {
@@ -1117,41 +1124,41 @@ sub depoison(@) {
 # random image from it.
 # returns the url of the page loaded; the url of the image chosen.
 #
-sub pick_image_from_pages($$$$@) {
-  my ($base, $total_hit_count, $unfiltered_link_count, $timeout, @pages) = @_;
-
-  $total_hit_count = "?" unless defined($total_hit_count);
-
-  @pages = depoison (@pages);
-  LOG ($verbose_load,
-       "" . ($#pages+1) . " candidates of $unfiltered_link_count links" .
-       " ($total_hit_count total)");
-
-  return () if ($#pages < 0);
-
-  my $i = int(rand($#pages+1));
-  my $page = $pages[$i];
-
-  LOG ($verbose_load, "picked page $page");
-
-  $suppress_audit = 1;
-
-  my ( $base2, $body2 ) = get_document ($page, $base, $timeout);
-
-  if (!$base2 || !$body2) {
-    $body2 = undef;
-    return ();
-  }
-
-  my $img = pick_image_from_body ($base2, $body2);
-  $body2 = undef;
-
-  if ($img) {
-    return ($base2, $img);
-  } else {
-    return ();
-  }
-}
+#sub pick_image_from_pages($$$$@) {
+#  my ($base, $total_hit_count, $unfiltered_link_count, $timeout, @pages) = @_;
+#
+#  $total_hit_count = "?" unless defined($total_hit_count);
+#
+#  @pages = depoison (@pages);
+#  LOG ($verbose_load,
+#       "" . ($#pages+1) . " candidates of $unfiltered_link_count links" .
+#       " ($total_hit_count total)");
+#
+#  return () if ($#pages < 0);
+#
+#  my $i = int(rand($#pages+1));
+#  my $page = $pages[$i];
+#
+#  LOG ($verbose_load, "picked page $page");
+#
+#  $suppress_audit = 1;
+#
+#  my ( $base2, $body2 ) = get_document ($page, $base, $timeout);
+#
+#  if (!$base2 || !$body2) {
+#    $body2 = undef;
+#    return ();
+#  }
+#
+#  my $img = pick_image_from_body ($base2, $body2);
+#  $body2 = undef;
+#
+#  if ($img) {
+#    return ($base2, $img);
+#  } else {
+#    return ();
+#  }
+#}
 
 \f
 #############################################################################
@@ -2331,6 +2338,54 @@ sub pick_from_flickr_random($) {
   return ($base, $img);
 }
 
+\f
+############################################################################
+#
+# Pick images from the Flickr Commons landing page.
+#
+############################################################################
+
+my $flickr_commons = 'https://commons.flickr.org/';
+
+# flickr_commons
+sub pick_from_flickr_commons($) {
+  my $timeout = shift;
+
+  $last_search = $flickr_commons;
+
+  print STDERR "\n\n" if ($verbose_load);
+  LOG ($verbose_load, "URL: $last_search");
+
+  $suppress_audit = 1;
+
+  my ( $base, $body ) = get_document ($last_search, undef, $timeout);
+  if (!$base || !$body) {
+    $body = undef;
+    return;
+  }
+
+  $body =~ s@^ .* "random_photos" (.*?) </section> .* @$1@six;
+  my @imgs = ();
+  foreach my $a ($body =~ m@ <a .*? </a> @gsix) {
+    my ($url) = ($a =~ m@\bHREF=[\"\']([^\'\"<>]+)@gsi);
+    my ($img) = ($a =~ m@\bSRC=[\"\']([^\'\"<>]+)@gsi);
+    next unless ($img && $url);
+    next unless ($url =~ m@/photos/@s);
+    push @imgs, [ $url, $img ];
+  }
+
+  my $n = @imgs;
+  my $i = int(rand($n));
+  my $P = $imgs[$i];
+  return () unless defined ($P);
+
+  $base   = $P->[0];
+  my $img = $P->[1];
+
+  LOG ($verbose_load, "redirected to: $base");
+  return ($base, $img);
+}
+
 \f
 ############################################################################
 #
index 5ae7941115d56b3a58d465edc1a5873317254917..0d63465d2690a3e032d33550e690db996f6acacf 100755 (executable)
@@ -1,5 +1,5 @@
 #!/usr/bin/perl -w
-# Copyright © 2005-2023 Jamie Zawinski <jwz@jwz.org>
+# Copyright © 2005-2024 Jamie Zawinski <jwz@jwz.org>
 #
 # Permission to use, copy, modify, distribute, and sell this software and its
 # documentation for any purpose is hereby granted without fee, provided that
@@ -38,7 +38,7 @@ BEGIN { eval 'use Text::Wrap qw(wrap);' }
 
 
 my $progname = $0; $progname =~ s@.*/@@g;
-my ($version) = ('$Revision: 1.71 $' =~ m/\s(\d[.\d]+)\s/s);
+my ($version) = ('$Revision: 1.72 $' =~ m/\s(\d[.\d]+)\s/s);
 
 my $verbose = 0;
 my $http_proxy = undef;
@@ -104,11 +104,13 @@ sub utf8_to_latin1($) {
   $text =~ s/\xE2\x80\x99/'/gs;
   $text =~ s/\xE2\x80\x9C/``/gs;
   $text =~ s/\xE2\x80\x9D/'/gs;
-  $text =~ s/\xE2\x80\xA2/&bull;/gs;
+  $text =~ s/\xE2\x80\xA2/\xB7/gs;     # &middot;
+  $text =~ s/\xE2\xC2\x80/\xB7/gs;     # &middot;
   $text =~ s/\xE2\x80\xA6/.../gs;
   $text =~ s/\xE2\x80\xB2/'/gs;
-  $text =~ s/\xE2\x84\xA2/&trade;/gs;
-  $text =~ s/\xE2\x86\x90/ &larr; /gs;
+  $text =~ s/\xE2\x84\xA2/[tm]/gs;     # &trade;
+  $text =~ s/\xE2\x86\x90/<=/gs;       # &larr;
+  $text =~ s/\xE2\x86\x92/=>/gs;       # &rarr;
 
   return $text;
 }
@@ -590,6 +592,7 @@ sub reformat_html($$) {
     s@(<PRE\b[^<>]*>\s*)(.*?)(</PRE)@{
       my ($a, $b, $c) = ($1, $2, $3);
       $b =~ s/[\r\n]/<BR>/gs;
+      $b =~ s/ /\240/gs;  # &nbsp;
       $a . $b . $c;
      }@gsexi;
   }
@@ -597,7 +600,8 @@ sub reformat_html($$) {
   if (! $rss_p) {
     # In HTML, unfold lines.
     # In RSS, assume \n means literal line break.
-    s@[\r\n]@ @gsi;
+    s/\s+/ /gs;
+    s@&nbsp;@\240@gs;
   }
 
   # This right here is the part where I doom us all to inhuman
@@ -607,6 +611,7 @@ sub reformat_html($$) {
   s@<!--.*?-->@@gsi;                            # lose comments
   s@<(STYLE|SCRIPT)\b[^<>]*>.*?</\1\s*>@@gsi;    # lose css and js
 
+  s/(<LI>)/$1&bull; /gsi;
   s@</?(BR|TR|TD|LI|DIV)\b[^<>]*>@\n@gsi; # line break at BR, TD, DIV, etc
   s@</?(P|UL|OL|BLOCKQUOTE)\b[^<>]*>@\n\n@gsi; # two line breaks
 
@@ -617,6 +622,11 @@ sub reformat_html($$) {
   s@<[^<>]*>?@@gs;                # lose all other HTML tags
   $_ = de_entify ($_);            # convert HTML entities
 
+  if (! $rss_p) {
+    s/[ \t]+/ /gs;
+    s/\240/ /gs;
+  }
+
   # For Wikipedia: delete anything inside {{ }} and unwrap [[tags]],
   # among other things.
   #
@@ -665,7 +675,7 @@ sub reformat_html($$) {
 
   # elide any remaining non-Latin1 binary data.
   if ($latin1_p) {
-    utf8::encode ($_);  # Unpack Unicode back to multi-byte UTF-8.
+    $_ = utf8_to_latin1 ($_);
     s/([^\000-\176]+(\s*[^\000-\176]+)[^a-z\d]*)/\xAB...\xBB /g;
   }
 
@@ -693,7 +703,6 @@ sub reformat_html($$) {
     s/^(([^\n]*\n){$truncate_lines}).*$/$1/s;
   }
 
-  $_ = utf8_to_latin1($_) if ($latin1_p);
   y/A-Za-z/N-ZA-Mn-za-m/ if ($nyarlathotep_p);
 
   return $_;
index 3eac567f5ae634525f1796e2449c3ff3e7c0cd34..207ba37a0bd81cc3f26c52b265d0c418bae944df 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 1991-2018 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 1991-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -269,6 +269,40 @@ typedef struct jwxyz_linked_point  linked_point;
 #define XK_F10                 0xFFC7
 #define XK_F11                 0xFFC8
 #define XK_F12                 0xFFC9
+#define XK_F13                 0xFFCA
+#define XK_F14                 0xFFCB
+#define XK_F15                 0xFFCC
+#define XK_F16                 0xFFCD
+#define XK_F17                 0xFFCE
+#define XK_F18                 0xFFCF
+#define XK_F19                 0xFFD0
+#define XK_F20                 0xFFD1
+
+#define XK_KP_F1               0xFF91
+#define XK_KP_F2               0xFF92
+#define XK_KP_F3               0xFF93
+#define XK_KP_F4               0xFF94
+
+#define XK_KP_Home             0xFF95
+#define XK_KP_Left             0xFF96
+#define XK_KP_Up               0xFF97
+#define XK_KP_Right            0xFF98
+#define XK_KP_Down             0xFF99
+#define XK_KP_Prior            0xFF9A
+#define XK_KP_Page_Up          0xFF9A
+#define XK_KP_Next             0xFF9B
+#define XK_KP_Page_Down                0xFF9B
+#define XK_KP_End              0xFF9C
+#define XK_KP_Begin            0xFF9D
+#define XK_KP_Insert           0xFF9E
+#define XK_KP_Delete           0xFF9F
+#define XK_KP_Equal            0xFFBD
+#define XK_KP_Multiply         0xFFAA
+#define XK_KP_Add              0xFFAB
+#define XK_KP_Separator                0xFFAC
+#define XK_KP_Subtract         0xFFAD
+#define XK_KP_Decimal          0xFFAE
+#define XK_KP_Divide           0xFFAF
 
 
 #define GXclear                        0x0             /* 0 */
index e11a6f4519b2300bc13e6f4934cfbb29427f8b4b..afba888482f37a63e04ffdcbf5282fd0ad9d4082 100644 (file)
@@ -3105,6 +3105,24 @@ jwzgles_glTexImage2D (GLenum target,
   if (d2 != data) free (d2);
 }
 
+void
+jwzgles_glTexImage3D (GLenum   target,
+                      GLint    level,
+                      GLint    internalFormat,
+                      GLsizei          width,
+                      GLsizei          height,
+                      GLsizei          depth,
+                      GLint    border,
+                      GLenum   format,
+                      GLenum   type,
+                      const GLvoid *data)
+{
+# ifdef HAVE_GLSL
+  glTexImage3D (target, level, internalFormat, width, height, depth, border,
+                format, type, data);
+# endif
+}
+
 void
 jwzgles_glTexSubImage2D (GLenum target, GLint level,
                          GLint xoffset, GLint yoffset,
index 8a63e0ac6acc5262d7109c7c5cf2b6daeadee741..978d785cd9d0fa538e632214ccbdbe1d33ce1c76 100644 (file)
@@ -283,6 +283,16 @@ extern void jwzgles_glTexImage2D (GLenum target,
                                   GLenum       format,
                                   GLenum       type,
                                   const GLvoid *data);
+extern void jwzgles_glTexImage3D (GLenum target,
+                                  GLint        level,
+                                  GLint        internalFormat,
+                                  GLsizei      width,
+                                  GLsizei      height,
+                                  GLsizei      depth,
+                                  GLint        border,
+                                  GLenum       format,
+                                  GLenum       type,
+                                  const GLvoid *data);
 extern void jwzgles_glTexSubImage2D (GLenum target, GLint level,
                                      GLint xoffset, GLint yoffset,
                                      GLsizei width, GLsizei height,
index 9c0bd21302ce6efd715d26b8626f5ad348c944b0..d5a983343b2500b9e50b4690a286dc36f7dc1051 100644 (file)
@@ -1,4 +1,4 @@
-# Auto-generated: Fri Jun  7 22:01:23 PDT 2024
+# Auto-generated: Mon Apr 28 12:45:42 PDT 2025
 driver/demo-Gtk-conf.c
 driver/demo-Gtk.c
 driver/demo.ui
@@ -74,6 +74,7 @@ hacks/config/distort.xml
 hacks/config/dnalogo.xml
 hacks/config/drift.xml
 hacks/config/droste.xml
+hacks/config/dumpsterfire.xml
 hacks/config/dymaxionmap.xml
 hacks/config/endgame.xml
 hacks/config/energystream.xml
@@ -134,6 +135,7 @@ hacks/config/hextrail.xml
 hacks/config/highvoltage.xml
 hacks/config/hilbert.xml
 hacks/config/hopalong.xml
+hacks/config/hopffibration.xml
 hacks/config/hydrostat.xml
 hacks/config/hyperball.xml
 hacks/config/hypercube.xml
@@ -153,6 +155,7 @@ hacks/config/kaleidescope.xml
 hacks/config/kaleidocycle.xml
 hacks/config/kallisti.xml
 hacks/config/klein.xml
+hacks/config/klondike.xml
 hacks/config/kumppa.xml
 hacks/config/lament.xml
 hacks/config/laser.xml
@@ -198,6 +201,7 @@ hacks/config/photopile.xml
 hacks/config/piecewise.xml
 hacks/config/pinion.xml
 hacks/config/pipes.xml
+hacks/config/platonicfolding.xml
 hacks/config/polyhedra.xml
 hacks/config/polyominoes.xml
 hacks/config/polytopes.xml
index 7ed5e4c1670fe5526f27d8fdffeebea620889a8e..e2bee9ab27ee60cb0797fffbc9cab3f2803deee0 100755 (executable)
@@ -1,5 +1,5 @@
 #!/bin/sh
-# Copyright © 2018 Jamie Zawinski <jwz@jwz.org>
+# Copyright © 2018-2025 Jamie Zawinski <jwz@jwz.org>
 #
 # Permission to use, copy, modify, distribute, and sell this software and its
 # documentation for any purpose is hereby granted without fee, provided that
@@ -28,10 +28,14 @@ NAME=`echo "$OUT" | sed \
   -e 's@^.*/@@' \
   -e 's/\.[^.]*$//' \
   -e 's/[-.]/_/g' \
-  -e 's/^\([^a-z]\)/_\1/'`;
+  -e 's/^\([^a-zA-Z]\)/_\1/'`;
 
 if [ x"$PERL" = "x" ]; then PERL=perl ; fi
 
+# Perl fails if set to run in UTF-8 mode
+unset PERL5OPTS
+unset PERL_UNICODE
+
 # On Linux, we could do this and put the raw image into a .o data segment:
 #      $(LD) -r -b binary $< -o $@
 # but that doesn't work on MacOS.
index 8745cb01ff89e80e5f3c33583de7525a4ef42920..1e9476127d6aa3c04f7ee2b8abd605e86ffd0550 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2012-2018 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 2012-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -798,7 +798,13 @@ textclient_getc (text_data *d)
 
 
 Bool
-textclient_putc (text_data *d, XKeyEvent *k)
+textclient_puts (text_data *d, const char *s)
+{
+  return False;
+}
+
+Bool
+textclient_putc_event (text_data *d, XKeyEvent *k)
 {
   return False;
 }
index 4a4c89573e4d842024ace25dc98d44f4ff87fd9d..59055732afdfd337f80d6df67f04e992623f9750 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright © 2012-2021 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 2012-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -90,7 +90,7 @@ static void
 subproc_cb (XtPointer closure, int *source, XtInputId *id)
 {
   text_data *d = (text_data *) closure;
-# ifdef DEBUG
+# ifdef _DEBUG
   if (! d->input_available_p)
     fprintf (stderr, "%s: textclient: input available\n", progname);
 # endif
@@ -181,7 +181,7 @@ selftest (void)
 }
 
 
-static void start_timer (text_data *d);
+static void start_timer (text_data *, Bool);
 
 static void
 launch_text_generator (text_data *d)
@@ -212,7 +212,7 @@ launch_text_generator (text_data *d)
     {
       if (!d->out_buffer || !*d->out_buffer)
         d->out_buffer = "Can't exec; Gatekeeper problem?\r\n\r\n";
-      start_timer (d);
+      start_timer (d, False);
       free (cmd);
       return;
     }
@@ -350,8 +350,6 @@ launch_text_generator (text_data *d)
           /* This is the child fork. */
           char *av[10];
           int i = 0;
-         if (putenv ("TERM=vt100"))
-            abort();
           av[i++] = "/bin/sh";
           av[i++] = "-c";
           av[i++] = cmd;
@@ -433,7 +431,7 @@ relaunch_generator_timer (XtPointer closure, XtIntervalId *id)
 
 
 static void
-start_timer (text_data *d)
+start_timer (text_data *d, Bool first_time_p)
 {
   XtAppContext app = XtDisplayToApplicationContext (d->dpy);
 
@@ -444,7 +442,9 @@ start_timer (text_data *d)
   if (d->pipe_timer)
     XtRemoveTimeOut (d->pipe_timer);
   d->pipe_timer =
-    XtAppAddTimeOut (app, d->subproc_relaunch_delay,
+    XtAppAddTimeOut (app, (first_time_p
+                           ? 250
+                           : d->subproc_relaunch_delay),
                      relaunch_generator_timer,
                      (XtPointer) d);
 }
@@ -498,6 +498,15 @@ textclient_reshape (text_data *d,
            pix_w, pix_h, char_w, char_h);
 # endif
 
+  {
+    char s1[30];
+    char s2[30];
+    sprintf (s1, "COLUMNS=%d", d->char_w);
+    sprintf (s2, "ROWS=%d",    d->char_h);
+    putenv (strdup (s1));  /* Some versions of putenv copy, some don't */
+    putenv (strdup (s2));  /* so we must leak */
+  }
+
   if (d->pid && d->pipe)
     {
       /* Tell the sub-process that the screen size has changed. */
@@ -526,7 +535,7 @@ textclient_reshape (text_data *d,
 # endif
       close_pipe (d);
       d->input_available_p = False;
-      start_timer (d);
+      start_timer (d, False);
     }
 }
 
@@ -589,7 +598,11 @@ textclient_open (Display *dpy)
   }
 # endif
 
-  start_timer (d);
+  putenv ("TERM=vt100");
+  unsetenv ("TERMCAP");
+  unsetenv ("INSIDE_EMACS");
+
+  start_timer (d, True);
 
   return d;
 }
@@ -655,7 +668,7 @@ textclient_getc (text_data *d)
               d->out_buffer = "\r\n\r\n";
             }
 
-          start_timer (d);
+          start_timer (d, False);
         }
       d->input_available_p = False;
     }
@@ -667,7 +680,7 @@ textclient_getc (text_data *d)
 
 # ifdef DEBUG
   if (ret <= 0)
-    fprintf (stderr, "%s: textclient: getc: %d\n", progname, ret);
+    /* fprintf (stderr, "%s: textclient: getc: %d\n", progname, ret) */;
   else if (ret < ' ')
     fprintf (stderr, "%s: textclient: getc: %03o\n", progname, ret);
   else
@@ -732,12 +745,94 @@ meta_modifier (text_data *d)
 
 
 Bool
-textclient_putc (text_data *d, XKeyEvent *k)
+textclient_puts (text_data *d, const char *s)
+{
+  fputs (s, d->pipe);
+  fflush (d->pipe);
+# ifdef DEBUG
+  fprintf (stderr, "%s: textclient: write: %s\n", progname, s);
+# endif
+  return True;
+}
+
+
+Bool
+textclient_putc_event (text_data *d, XKeyEvent *k)
 {
   KeySym keysym;
   unsigned char c = 0;
+  char *s = 0;
+  char ss[3];
+
+  if (! d->pipe) return False;
+
   XLookupString (k, (char *) &c, 1, &keysym, &d->compose);
-  if (c != 0 && d->pipe)
+
+# define ESC "\x1B"
+# define CSI ESC "["
+# define SS3 ESC "O"
+
+  /* https://vt100.net/docs/vt220-rm/chapter3.html#S3.2
+     https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+   */
+  switch (keysym) {
+  case XK_Up:          s = SS3 "A";   break;
+  case XK_Down:                s = SS3 "B";   break;
+  case XK_Right:       s = SS3 "C";   break;
+  case XK_Left:                s = SS3 "D";   break;
+  case XK_Home:                s = SS3 "H";   break;
+  case XK_End:         s = SS3 "F";   break;
+  case XK_Prior:       s = CSI "5~";  break;
+  case XK_Next:                s = CSI "6~";  break;
+
+  case XK_F1:          s = SS3 "P";   break;
+  case XK_F2:          s = SS3 "Q";   break;
+  case XK_F3:          s = SS3 "R";   break;
+  case XK_F4:          s = SS3 "S";   break;
+  case XK_F5:          s = CSI "15~"; break;
+  case XK_F6:          s = CSI "17~"; break;
+  case XK_F7:          s = CSI "18~"; break;
+  case XK_F8:          s = CSI "19~"; break;
+  case XK_F9:          s = CSI "20~"; break;
+  case XK_F10:         s = CSI "21~"; break;
+  case XK_F11:         s = CSI "23~"; break;
+  case XK_F12:         s = CSI "24~"; break;
+  case XK_F13:         s = CSI "25~"; break;
+  case XK_F14:         s = CSI "26~"; break;
+  case XK_F15:         s = CSI "28~"; break;
+  case XK_F16:         s = CSI "29~"; break;
+  case XK_F17:         s = CSI "31~"; break;
+  case XK_F18:         s = CSI "32~"; break;
+  case XK_F19:         s = CSI "33~"; break;
+  case XK_F20:         s = CSI "34~"; break;
+
+  case XK_KP_F1:       s = SS3 "P";   break;
+  case XK_KP_F2:       s = SS3 "Q";   break;
+  case XK_KP_F3:       s = SS3 "R";   break;
+  case XK_KP_F4:       s = SS3 "S";   break;
+
+  case XK_KP_Up:       s = SS3 "A";   break;
+  case XK_KP_Down:     s = SS3 "B";   break;
+  case XK_KP_Right:    s = SS3 "C";   break;
+  case XK_KP_Left:     s = SS3 "D";   break;
+  case XK_KP_Home:     s = SS3 "H";   break;
+  case XK_KP_End:      s = SS3 "F";   break;
+  case XK_KP_Prior:    s = CSI "5~";  break;
+  case XK_KP_Next:     s = CSI "6~";  break;
+
+  case XK_KP_Begin:    s = CSI "E";   break;
+  case XK_KP_Insert:   s = CSI "2~";  break;
+  case XK_KP_Delete:   s = CSI "3~";  break;
+  case XK_KP_Equal:    s = SS3 "X";   break;
+  case XK_KP_Multiply: s = SS3 "j";   break;
+  case XK_KP_Add:      s = SS3 "k";   break;
+  case XK_KP_Subtract: s = SS3 "m";   break;
+  case XK_KP_Decimal:  s = ESC "?n";  break;
+  case XK_KP_Divide:   s = ESC "?o";  break;
+  default: break;
+  }
+
+  if (c && !s)
     {
       if (!d->swap_bs_del_p) ;
       else if (c == 127) c = 8;
@@ -747,21 +842,33 @@ textclient_putc (text_data *d, XKeyEvent *k)
       if (k->state & meta_modifier (d))
         {
           if (d->meta_sends_esc_p)
-            fputc ('\033', d->pipe);
+            {
+              ss[0] = ESC[0];
+              ss[1] = c;
+              ss[2] = 0;
+            }
           else
-            c |= 0x80;
+            {
+              ss[0] = c | 0x80;
+              ss[1] = 0;
+            }
+        }
+      else
+        {
+          ss[0] = c;
+          ss[1] = 0;
         }
 
-      fputc (c, d->pipe);
-      fflush (d->pipe);
-      k->type = 0;  /* don't interpret this event defaultly. */
-
-# ifdef DEBUG
-      fprintf (stderr, "%s: textclient: putc '%c'\n", progname, (char) c);
-# endif
+      s = ss;
+    }
 
+  if (s)
+    {
+      textclient_puts (d, s);
+      k->type = 0;
       return True;
     }
+
   return False;
 }
 
index 5be8079f143d3776af02dde6d5db7b3a1e614757..35259878db2189c8d7b0cff3c6b19405f891c579 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2012-2016 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 2012-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -27,7 +27,8 @@ extern void textclient_reshape (text_data *,
                                 int char_w, int char_h,
                                 int max_lines);
 extern int textclient_getc (text_data *);
-extern Bool textclient_putc (text_data *, XKeyEvent *);
+extern Bool textclient_puts (text_data *, const char *);
+extern Bool textclient_putc_event (text_data *, XKeyEvent *);
 
 # if defined(HAVE_IPHONE) || defined(HAVE_ANDROID)
 extern char *textclient_mobile_date_string (void);
index 101847c131cb0d9b286e9c6f8bb93ece86bd7776..1a14d22772d953feb0d6bfe2b803349119cb5af1 100644 (file)
@@ -1,4 +1,4 @@
 static const char screensaver_id[] =
-       "@(#)xscreensaver 6.09 (07-Jun-2024), by Jamie Zawinski (jwz@jwz.org)";
-#define XSCREENSAVER_VERSION "6.09"
-#define XSCREENSAVER_RELEASED 1717786800
+       "@(#)xscreensaver 6.10 (28-Apr-2025), by Jamie Zawinski (jwz@jwz.org)";
+#define XSCREENSAVER_VERSION "6.10"
+#define XSCREENSAVER_RELEASED 1745866800
index da8c3ba373ffe4c185cf3017ed16151121877c7d..dd8b994fc82b1c8a4401354d9ac23d311ba52629 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright © 1999-2021 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 1999-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -58,10 +58,11 @@ get_gl_visual (Screen *screen)
   int egl_major = -1, egl_minor = -1;
 
   /* This is re-used, no need to close it. */
-  egl_display = eglGetDisplay ((EGLNativeDisplayType) dpy);
+  egl_display = eglGetPlatformDisplay (EGL_PLATFORM_X11_KHR,
+                                       (void *) dpy, NULL);
   if (!egl_display)
     {
-      fprintf (stderr, "%s: eglGetDisplay failed\n", progname);
+      fprintf (stderr, "%s: eglGetPlatformDisplay failed\n", progname);
       return 0;
     }
 
@@ -324,7 +325,7 @@ describe_gl_visual (FILE *f, Screen *screen, Visual *visual,
 
     /* This is re-used, no need to close it. */
     egl_display = eglGetPlatformDisplay (EGL_PLATFORM_X11_KHR,
-                                         (EGLNativeDisplayType) dpy, NULL);
+                                         (void *) dpy, NULL);
     if (!egl_display)
       {
         fprintf (stderr, "%s: eglGetPlatformDisplay failed\n", progname);
@@ -387,12 +388,15 @@ describe_gl_visual (FILE *f, Screen *screen, Visual *visual,
           else if (fields[i].i == EGL_TRANSPARENT_RED_VALUE && tt != EGL_NONE)
             sprintf (s, "%d, %d, %d", tr, tg, tb);
           else if (fields[i].i == EGL_CONFIG_CAVEAT)
-            strcpy (s, (v == EGL_NONE ? "none" :
-                        v == EGL_SLOW_CONFIG ? "slow" :
+            {
+              const char *s2 = (v == EGL_NONE ? "none" :
+                                v == EGL_SLOW_CONFIG ? "slow" :
 # ifdef EGL_NON_CONFORMANT
-                        v == EGL_NON_CONFORMANT ? "non-conformant" :
+                                v == EGL_NON_CONFORMANT ? "non-conformant" :
 # endif
-                        "???"));
+                                "???");
+              strcpy (s, s2);
+            }
           else if (fields[i].i == EGL_COLOR_BUFFER_TYPE)
             strcpy (s, (v == EGL_RGB_BUFFER ? "RGB" :
                         v == EGL_LUMINANCE_BUFFER ? "luminance" :
index 17c0eaf71817ae9e137326fb9514822132849a0b..50b6931d91874b8d6d0117c5a1a8ed5b9b72c42b 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright © 2014-2021 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 2014-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -360,6 +360,16 @@ XftDrawCreate (Display   *dpy,
 }
 
 
+XftDraw *
+XftDrawCreateBitmap (Display *dpy, Pixmap bitmap)
+{
+  Screen *screen = DefaultScreenOfDisplay (dpy);
+  return XftDrawCreate (dpy, bitmap,
+                        DefaultVisualOfScreen (screen),
+                        DefaultColormapOfScreen (screen));
+}
+
+
 void
 XftDrawDestroy (XftDraw        *draw)
 {
index c5819bb1addf1b334255691f4f3d69800f0d79ee..f5f7bb703e0591e456e41856ae91443fd1152397 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright © 2014-2022 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright © 2014-2025 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -152,6 +152,7 @@ XftDraw *XftDrawCreate (Display   *dpy,
                         Drawable  drawable,
                         Visual    *visual,
                         Colormap  colormap);
+XftDraw *XftDrawCreateBitmap (Display *dpy, Pixmap bitmap);
 Display *XftDrawDisplay (XftDraw *);
 Bool XftDrawSetClipRectangles (XftDraw *, int x, int y, 
                                _Xconst XRectangle *rects, int n);
index 5190550e418288361dc164230893422b01e4f15d..a4cf1d839458a804075ef1fa640c579431988104 100644 (file)
@@ -61,6 +61,7 @@ xft_word_wrap (Display *dpy, XftFont *font, const char *str, int pixels)
           if (nl) in++;
           while (*in == ' ' || *in == '\t') in++;
           in--;
+          if (out < line_out) abort();
 
           XftTextExtentsUtf8 (dpy, font,
                               (FcChar8 *) line_out,
@@ -74,6 +75,7 @@ xft_word_wrap (Display *dpy, XftFont *font, const char *str, int pixels)
               word_out = 0;
               if (done) break;
               *out++ = *in;
+              if (out < line_out) abort();
             }
           else
             {
@@ -82,9 +84,10 @@ xft_word_wrap (Display *dpy, XftFont *font, const char *str, int pixels)
               *out++ = *in;
               if (nl)
                 {
-                  line_out = out + 1;
+                  line_out = out;  /* Why was I adding 1 to this? */
                   word_out = 0;
                 }
+              if (out < line_out) abort();
             }
 
           if (done) break;
@@ -105,6 +108,10 @@ xft_word_wrap (Display *dpy, XftFont *font, const char *str, int pixels)
 /* Like XftTextExtentsUtf8, but handles multi-line strings.
    XGlyphInfo will contain the bounding box that encloses all of the text.
    Return value is the number of lines in the text, >= 1.
+
+   overall->y will be the distance from the top of the ink to the origin of
+   the first character in the string, as usual.  If there are multiple lines,
+   you can think of them as extremely tall descenders below the origin.
  */
 int
 XftTextExtentsUtf8_multi (Display *dpy, XftFont *font,
@@ -132,18 +139,23 @@ XftTextExtentsUtf8_multi (Display *dpy, XftFont *font,
               int nx1, ny1, nx2, ny2;          /* bbox of 'gi' */
               int ux1, uy1, ux2, uy2;          /* union */
 
+              /* Cumulative bbox prior to this line */
               ox1 = overall->x;
               oy1 = overall->y;
               ox2 = ox1 + overall->width;
               oy2 = oy1 + overall->height;
 
-              line_y += font->ascent + font->descent;  /* advance origin */
+              /* Advance the origin of this line downward. */
+              line_y += font->ascent + font->descent;
 
+              /* Bounding box of this line, with origin adjusted to be
+                 the origin of line 0. */
               nx1 = gi.x;
               ny1 = gi.y + line_y;
               nx2 = nx1 + gi.width;
-              ny2 = ny1 + gi.height + line_y;
+              ny2 = ny1 + gi.height;
 
+              /* Find the union of the two same-origin bboxes. */
               ux1 = MIN (ox1, nx1);            /* upper left */
               uy1 = MIN (oy1, ny1);
               ux2 = MAX (ox2, nx2);            /* bottom right */
index 44089e832ef678a0a5947e70b7d78c03ba4691f0..57a02e49d79543403cab288723aacf4568551228 100644 (file)
@@ -1,5 +1,5 @@
 %define        name xscreensaver
-%define        version 6.09
+%define        version 6.10
 
 Summary:       X screen saver and locker
 Name:          %{name}
@@ -74,7 +74,7 @@ Obsoletes: xscreensaver-screensaver-webcollage
 
 %description
 A modular screen saver and locker for the X Window System.
-More than 250 display modes are included in this package.
+More than 260 display modes are included in this package.
 
 %prep
 %setup -q