// The Xmas Demo 2023: Control code
// Authors: Julin and Corneliusen
// More info here: https://www.ignorantus.com/xmasdemo/
// License: https://creativecommons.org/publicdomain/zero/1.0/

// This demo uses the V73D engine by Sjur Julin.
// More info here: https://github.com/sjulin/V73D

// C:\Users\ncorn\src\Xmasdemo> bin\xmasdemo.exe -v -m -o
// C:\Users\ncorn\tmp> ffmpeg -framerate 60 -i out%05d.jpg -i ..\src\xmasdemo\data\hyp6.wav -ar 48000 -ab 384k -c:v libx264 -force_key_frames "expr:gte(t,n_forced/2)" -bf 2 -crf 18 -pix_fmt yuv420p -use_editlist 0 -s 1920x1080 -movflags +faststart test87.mp4

#include <V73D/V7.h>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>

#ifdef _WIN32
#include <Windows.h>
#elif __linux__
#include <unistd.h>
#endif

#include <V73D/font/sdf_kanit.h>
#include <V73D/font/sdf_fradmit.h>
#include <V73D/font/sdf_conflict.h>

#include "vec.h"

// Set by options
static bool fullscreen = true;
static bool vsync = true;
static bool shdebug = false;
static bool msaa = false;
static bool savepics = false;
bool usefaketime = true; // Enables rendering of all frames, even if timing is off

bool mute = false;
double faketime = 0.0;
int framectr = 0;

#ifdef _WIN32
#define SAVEPATH "..\\..\\..\\tmp\\out%05d.jpg"
#else
#define SAVEPATH "../render/out%05d.jpg"
#endif

#define RENDERW 1920
#define RENDERH 1080

#define PIF ((float)PI)

static V7X *ctx;

static Model *bg[2], *fg[2];
static Model *txt_title1, *txt_title2, *txt_info, *txt_llinfo, *txt_scroll, *txt_cred, *txt_debug;
static Model *shtxt_title1, *shtxt_title2, *shtxt_info, *shtxt_llinfo, *shtxt_scroll, *shtxt_cred;
static Model *thebox, *theboxx, *apply, *notsure, *cobrac, *cobransi, *secret, *v73dlogo, *bike, *chrisl, *chrisr, *check, *reticle;
static Texture *openboxtex, *closedboxtex, *applytex, *notsuretex, *cobratex, *cobransitex, *secrettex, *v73dtex, *biketex, *chrisltex, *chrisrtex, *checktex, *reticletex;
static Model *galdance, *jquat, *madbulb, *seascape, *trans, *torus, *citywok, *twofield, *colorful, *cyber, *smoke, *rave;

static vec3 vtx_scroll[16384], shvtx_scroll[16384];

static int part = 0, switchpart, codemax;
static double pstart, pdiff, prest, pnext, pfad=1.2; // fix: 1.5 crash

static v4 h2rgb( float h, const v4 *cols, int colcnt )
{
    h = h - floorf( h );
    float Slice     = (float)(colcnt-1) * h;
    float SliceInt  = floorf( Slice );
    float SliceFrac = Slice - SliceInt;
    int i = (int)SliceInt;
    return v4_mix( cols[i], cols[i + 1], SliceFrac );
}

// for scroller
static const v4 cols4[7] = {
    { 1.0f, 0.0f, 0.0f, 0.0f },
    { 1.0f, 0.0f, 0.0f, 0.0f },
    { 0.0f, 1.0f, 0.0f, 0.0f },
    { 0.0f, 1.0f, 0.0f, 0.0f },
    { 0.0f, 0.5f, 1.0f, 0.0f },
    { 0.0f, 0.5f, 1.0f, 0.0f },
    { 1.0f, 0.0f, 0.0f, 0.0f }
};
#define COLS4CNT ((int)(sizeof(cols4)/sizeof(cols4[0])))

// for quat
static const v4 cols3[5] = {
    { 1.0f, 0.0f, 0.0f, 0.0f },
    { 1.0f, 0.0f, 0.0f, 0.0f },
    { 1.0f, 0.5f, 0.0f, 0.0f },
    { 1.0f, 0.5f, 0.0f, 0.0f },
    { 1.0f, 0.0f, 0.0f, 0.0f }
};
#define COLS3CNT ((int)(sizeof(cols3)/sizeof(cols3[0])))

typedef enum {
    PART_BOX1,
    PART_PRELUDE,
    PART_QUAT,
    PART_TWOFIELD,
    PART_CYBER,
    PART_CITYWOK,
    PART_SEASCAPE,
    PART_TRANS,
    PART_TORUS,
    PART_COLORFUL,
    PART_MAD,
    PART_BOX2,
    PART_CREDITS,
    PART_NADA,
    PART_SNAPSHOT,
    BODYPARTS
} party;

typedef struct Demopart_t {
    party partid;
    float partlen;
    char *fg, *bg;
    vec3 title1pos, title2pos, infopos;
    char *title1txt, *title2txt, *infotxt, *llinfotxt;
    vec4 titlecol, infocol, llinfocol, scrollcol;
    float infostart, infoend;
} Demopart;

typedef struct Keyfrm_t {
    float ts; vec3 s,r,t;
} Keyfrm;

vec3 xaxis = {1.f,.0f,.0f};
vec3 yaxis = {.0f,1.f,.0f};
vec3 zaxis = {.0f,.0f,1.f};

#define TITLE1POS_DFLT {0.0f,  2.570f, 2.11f}
#define TITLE2POS_DFLT {0.0f, -3.210f, 2.11f}
#define INFOPOS_DFLT   {0.0f,  2.32f,  2.75f}
#define COL_DFLT       {-1.0f} // some defaults are cycling, remapped elsewhere
#define INFOSTART_DFLT 10.0f
#define INFOSTOP_DFLT  17.0f
#define SCROLLER_ALPHA  1.25f // Megalpha

static Demopart demopart[] = {
    // Hellraiser (1987)
    {   PART_BOX1, 7.0f,  "Empty", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, {-2.025f,  2.32f,  2.75f},
        "\x02""THE BOX\n",
        "\x02""YOU OPENED IT. WE CAME.\n",
        "",
        "",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, INFOSTART_DFLT, INFOSTOP_DFLT
    },

    // Numerous "modern" programming languages enforce references; pointers that cannot have a zero value.
    // They also enforce array indexes that cannot be zero.
    // The Roman numeral system does not have a zero value.
    // Do we have to point out the obvious?
    {   PART_PRELUDE, 15.0f, "Galdance", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "\x02""THE  XMAS  DEMO  2023\n\n\n\n", "", // title2 Roman numerals
        "\x02""\n\n\n\n\n\n"
        "\x02""Corneliusen & Julin Productions\n",
        "\nBased on\n"
        "\"Galactic Dance\" (2015) by Sinuousity\n",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, 0.0f, 15.0f
    },

    // Julia dream, song by Pink Floyd
    {   PART_QUAT, 20.f, "JQuat", "Rave", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "JULIA  DREAM", "",
        "\x02""\n\n\n\n\n"
        "\x02""Shader tweaking\n\n"
        "\x02""Nils Liaaen Corneliusen\n",
        "Based on\n"
        "\"Rave Lasers\" (2021) by R3N\n"
        "\"Julia Quaternions\" (2023) by Corneliusen\n",
        COL_DFLT, COL_DFLT, COL_DFLT, { 0.0f, 1.0f, 0.0f, SCROLLER_ALPHA }, 8.5f, 15.5f
    },

    // Two suns in the sunset, song by PF
    {   PART_TWOFIELD, 20.f, "Twofield", "Rave", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "TWO  FIELDS  IN  THE  SUNSET", "",
        "\x02""\n\n\n\n"
        "\x02""Music\n\n"
        "\x02""\"Hypnotic Groove\"\n\n"
        "\x02""Chris Huelsbeck\n",
        "Based on\n"
        "\"Twofield\" (2014) by w23\n"
        "\"Rave Lasers\" (2021) by R3N\n",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, 12.5f, 19.5f,
    },

    // Welcome to the machine and A new machine, songs by PF
    {   PART_CYBER, 20.f, "Cyber", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "WELCOME  TO  A  NEW  MACHINE", "",
        "\x02""\n\n\n\n\n"
        "\x02""V73D Engine\n\n"
        "\x02""Sjur Julin\n",
        "\nBased on\n"
        "\"Cyberpunk City\" (2022) by Haru86_\n",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, 8.5f, 15.5f
    },

    // Music from the committee, song by PF
    {   PART_CITYWOK, 20.f, "CityWok", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "CITY  FROM  THE  COMMITTEE", "",
        "\x02""\n\n\n\n\n"
        "\x02""Source code\n\n"
        "\x02""ignorantus.com/xmasdemo/\n",
        "\nBased on\n"
        "\"Procedural City\" (2018) by Gr\n",
        COL_DFLT, { 1.0f, 1.0f, 0.0f, 1.0f }, COL_DFLT, { 1.0f, 0.0f, 0.0f, SCROLLER_ALPHA }, 12.5f, 19.5f
    },

    // Goodbye blue sky, song by PF
    {   PART_SEASCAPE, 20.f, "Seascape", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "GOODBYE  BLUE  SKY", "",
        "\x02""\n\n\n\n"
        "\x02""Python uses 76 times more energy than C.\n\n"
        "\x02""Save the planet!\n\n"
        "\x02""Write programs in C!\n",
        "Based on\n"
        "\"Seascape\" (2014) by TDM\n"
        "\"2D Clouds\" (2016) by drift\n",
        COL_DFLT, { 1.0f, 0.0f, 0.0f, 1.0f }, COL_DFLT, { 1.0f, 0.0f, 0.0f, SCROLLER_ALPHA }, INFOSTART_DFLT-1.0f, INFOSTOP_DFLT-1.0f
    },

    // Obscured by clouds, song by PF
    {   PART_TRANS, 20.f, "Trans", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "OBSCURED  BY  GLASS", "",
        "",
        "\nBased on\n"
        "\"Transparent Lattice\" (2016) by Shane\n",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, 12.25f, 19.25f
    },

    // Set the controls for the heart of the sun, song by PF
    {   PART_TORUS, 20.5f, "Torus", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "SET  THE  CONTROLS  FOR  THE  HEART  OF  THE  TORUS", "",
        "\x02""\n\n\n\n\n"
        "\x02""\"I am always wandering around in enigmas.\"\n\n"
        "\x02""- M. C. Escher\n",
        "\nBased on\n"
        "\"Torus Thingy 1\" (2017) by bal-khan\n",
        COL_DFLT, { 0.0f, 0.25f, 1.0f, 1.0f }, COL_DFLT, { 0.0f, 1.0f, 1.0f, SCROLLER_ALPHA }, 12.50f, 17.50f
    },

    // Any colour you like, song by PF
    // We usually write something that resembles American English, so yeah.
    {   PART_COLORFUL, 21.0f, "Colorful", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "ANY  COLOR  YOU  LIKE", "",
        "\x02""\n\n\n\n"
        "\x02""Xmas Demo #5 since 2017\n\n"
        "\x02""C and GLSL only\n\n"
        "\x02""Take no prisoners!\n",
        "\nBased on\n"
        "\"More Colorful Than Average\" (2017) by ollj\n",
        COL_DFLT, { 1.0f, 1.0f, 0.0f, 1.0f }, COL_DFLT, COL_DFLT, 12.0f, 18.0f
    },

    // Shine on you crazy diamond, song by PF
    // Secret message "27B/6" from Brazil (1985 film)
    {   PART_MAD, 18.5f, "Mad", "Smoke", TITLE1POS_DFLT, TITLE2POS_DFLT, {3.35f, -2.525f, 2.75f},
        "\x02""SHINE  ON  YOU  CRAZY  MANDELBULB", "",
        "27B/6\n",
        "Based on\n"
        "\"Lights in Smoke\" (2016) by ehj1\n"
        "\"Mandelbulb Deconstructed\" (2014) by morgan3d\n",
        COL_DFLT, { 1.0f, 1.0f, 1.0f, 1.0f }, COL_DFLT, { 1.0f, 0.5f, 0.0f, SCROLLER_ALPHA }, INFOSTART_DFLT, INFOSTOP_DFLT
    },

    {   PART_BOX2, 7.0f, "Empty", "Smoke", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "\x02""WE  WILL  TEAR\n",
        "\x02""YOUR  CODE  APART\n",
        "",
        "",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, INFOSTART_DFLT, INFOSTOP_DFLT
    },

    // Last quote from Breathe by Pink Floyd
    {   PART_CREDITS, 30.f, "Empty", "Smoke", {0.0f, -5.370f, 2.11f}, TITLE2POS_DFLT, {0.0f, -5.85f, 2.0f},
        "\x02""THE  XMAS  DEMO  2023\n", "",
        "\x02""\n"
        "\x02""for the\n\n"
        "\x02""NVidia Jetson AGX Orin Developer Kit\n\n\n"
        "\x02""Produced by\n\n"
        "\x02""Corneliusen & Julin\n\n\n"

        "\x02""We do this not because it is easy,\n"
        "\x02""but because we thought it would be easy.\n\n\n"

        "\x02""Sound and Music\n\n"
        "\x02""\"Hypnotic Groove\" by Chris Huelsbeck\n"
        "\x02""Royalty-free license\n\n"
        "\x02""\"I Got a Stick Feat James Gavins\" by Kevin MacLeod\n"
        "\x02""CC BY 4.0 license\n\n"
        "\x02""Samples from Hellraiser (1987)\n\n"
        "\x02""Sound and music mixed by Julin\n\n\n"

        "\x02""Graphics\n\n"
        "\x02""\"The Lament Configuration\" by Julin\n"
        "\x02""\"Make Coding Great Again\" by Julin\n"
        "\x02""\"Cobra C\" modified by Julin\n"
        "\x02""\"V73D\" by Julin\n\n\n"

        "\x02""Source Code\n\n"

        "\x02""V73D Engine by Julin\n"
        "\x02""Shader wizardry by Corneliusen\n"
        "\x02""Control code by Julin & Corneliusen\n\n"

        "\x02""The following shaders have been modified:\n\n"
        "\x02""\"Mandelbulb Deconstructed\" by morgan3d\n"
        "\x02""\"More Colorful Than Average\" by ollj\n"
        "\x02""\"Julia Quaternions\" by Corneliusen\n"
        "\x02""\"Transparent Lattice\" by Shane\n"
        "\x02""\"Galactic Dance\" by Sinuousity\n"
        "\x02""\"Cyberpunk City\" by Haru86_\n"
        "\x02""\"Torus Thingy 1\" by bal-khan\n"
        "\x02""\"Lights in Smoke\" by ehj1\n"
        "\x02""\"Procedural City\" by Gr\n"
        "\x02""\"Rave Lasers\" by R3N\n"
        "\x02""\"2D Clouds\" by drift\n"
        "\x02""\"Seascape\" by TDM\n"
        "\x02""\"Twofield\" by w23\n\n"

        "\x02""ignorantus.com/xmasdemo/\n"

        "\x02""\n\n\n\n\n\n\n"
        "\x02""For long you live and high you fly\n"
        "\x02""The smiles you'll give and tears you'll cry\n"
        "\x02""All you touch and all you see\n"
        "\x02""Is all your life will ever be\n",

        "\nBased on\n"
        "\"Lights in Smoke\" (2016) by ehj1\n",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, 0.0f, 30.0f
    },

    {   PART_NADA, 5.0f, "Empty", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "", "",
        "",
        "",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, INFOSTART_DFLT, INFOSTOP_DFLT
    },

    // Used to generate the YouTube snapshot
    {   PART_SNAPSHOT, 5.0f, "Galdance", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, {0.0f, 2.30f, 2.0f },
        "\x02""THE  XMAS  DEMO  2023\n",
        "\x02""NVIDIA JETSON AGX ORIN\n",
        "",
        "",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, 0.0f, 15.0f
    },

    {   PART_NADA, 5.0f, "Empty", "Empty", TITLE1POS_DFLT, TITLE2POS_DFLT, INFOPOS_DFLT,
        "", "",
        "",
        "",
        COL_DFLT, COL_DFLT, COL_DFLT, COL_DFLT, INFOSTART_DFLT, INFOSTOP_DFLT
    },

};

#define NPARTS (sizeof(demopart)/sizeof(demopart[0]))

int quit;

extern float *sndbuf;
extern int sndbufsize;

typedef struct {
    v4 start_pos;
    v4 mu_pos;
    v4 obj_col;
    v4 plane_norm;
    v4 plane_col;
    v4 shadow_col;
    float rot_xz;
    float ax;
    float ay;
    float starttime;
    float shadow;
    float inky,blinky,pinky;
} GPUQuat;
static GPUQuat gpuquat;

typedef struct {
    float starttime;
    float pitch;
    float yaw;
    float zoom;
    float power;
    float x1, x2, x3;
} GPUMadBulb;
static GPUMadBulb gpumadbulb;

typedef struct {
    v4 inori;
    v4 m0, m1, m2;
    float starttime;
    float larry,curly,moe;
} GPUSeascape;
static GPUSeascape gpuseascape;

#define TFNUM   8
#define LIGHTNUM 8
typedef struct {
    v4 pos[LIGHTNUM];
    v4 col[LIGHTNUM];
    v4 b1[TFNUM];
    v4 b2[TFNUM];
    v4 O;
    float scale;
    float starttime;
    float inky,blinky;
} GPUTwofield;
static GPUTwofield gputwofield;

typedef struct {
    v4 ta_in;
    v4 ro_in;
    float scale;
    float starttime;
    float larry,curly;
} GPUCyber;
static GPUCyber gpucyber;

typedef struct {
    float scale;
    float starttime;
    float pinky,clyde;
} GPUTorus;
static GPUTorus gputorus;

typedef struct {
    float starttime;
    float larry,curly,moe;
} GPUGeneric;
static GPUGeneric gpucitywok;
static GPUGeneric gputrans;
static GPUGeneric gpucolorful;
static GPUGeneric gpugaldance;

// Quat positions
typedef struct {
    v4 pos;
    float zoom;
} QPos;
#define QUATCNT 5
static QPos qpostable[QUATCNT] = {
    { {  0.0,     0.0,     0.0,     0.0      }, 11.00f }, // sphere
    { { -0.225362,0.900546,0.000000,0.000000 }, 10.50f }, //
    { { -1.102858,0.220685,0.000000,0.000000 }, 10.75f }, // funkee
    { { -0.462599,0.737401,0.000000,0.000000 }, 11.50f }, // funky
    { {  0.0,     0.0,     0.0,     0.0      }, 11.00f }, // sphere
};

#define OVERSHOOT 1.13f
#define ZOOMSHOOT 1.0f

#define QUAT_ROT_TIME (9*60+30)
#define QUAT_MIX_TIME (2*60)
#define QUAT_HOLD_TIME (5*60-30)
#define QUAT_MU_TIME (QUAT_HOLD_TIME+QUAT_MIX_TIME)

static Model *NewBG(char *name, char *shdname, void *gpudata, size_t gpusize) {
    static int count;
    Model *m = V7NewMod(ctx, name);
    V7Quad(m->msh, 16.f, 9.f);
    m->s = (vec3){7.f,7.f,7.f};
    m->t = (vec3){.0f,.0f,-22.f-count*.1f};
    char *shdsrc = V7Load(0, true, "../shaders/%s", shdname);
    if( shdebug ) {
        char *ptr = strstr( shdsrc, "//#define SHDEBUG" );
        if( ptr ) {
            printf( "Shader debug: %s\n", shdname );
            ptr[0] = ' ';
            ptr[1] = ' ';
        }
    }
    m->prg = V7CustPrg(ctx, name, shdsrc, shdname);
    if(gpudata) V7UniBuf(m, gpudata, gpusize);
//    m->flags|=V7NDEP;
    count++;
    return m;
}

Model *LoadMod(char *name) {
    Model *m = V7GetMod(ctx, name);
    if(m) return m;
    char fn[64];
    snprintf(fn, sizeof(fn), "../data/%s.glb", name);
    size_t glb_size;
    void *glb = V7Load(&glb_size, 0, fn);
    if(!glb) {
        V7Error("Can't load model '%s'", fn);
        return 0;
    }
    m = V7GLB(ctx, name, glb, glb_size);
    m->Update = 0;
    return m;
}

static Model *LoadPicAsMod( Scene *s, char *fname, char *name, Texture **tex, vec3 sc, vec3 t )
{
    char sname[80];

    size_t piclen;
    void *pic = V7Load( &piclen, true, fname );

    snprintf( sname, sizeof(sname), "%sTex", name );
    *tex = V7NewTex(ctx, sname);
    snprintf( sname, sizeof(sname), "%sImg", name );
    (*tex)->img = V7NewImg(ctx, sname);
    size_t len = strlen(fname);
    if( len >= 4 && !strcmp( fname+len-4, ".jpg" ) )
        V7JPGDec((*tex)->img, pic, piclen);
    else
        V7PNGDec((*tex)->img, pic);
    V7ImgTex(*tex);

    snprintf( sname, sizeof(sname), "%sMod", name );
    Model *mod = V7NewMod(ctx, sname);
    mod->prg = V7GetPrg(ctx, "RGB");
    V7Quad(mod->msh, 2.0f*((*tex)->img->w/(float)(*tex)->img->h), 2.0f);
    snprintf( sname, sizeof(sname), "%sMtr", name );
    mod->mtr = V7NewMtr(ctx, sname);
    mod->mtr->coltex = *tex;
    mod->s = sc;
    mod->t = t;

    V7AddMod(s, mod);
    V7Hide( mod );

    return mod;
}

static Model *NewText( char *title, SDFont *fn, vec3 sc, vec3 t, vec4 col )
{
//    txt_scroll->mtr = V7DupMtr(ctx, "ScrolltxtMtr", txt_scroll->mtr);
    Model *mo = V7NewTxt( ctx, title, fn );
    mo->s = sc;
    mo->t = t;
    Material oldmo = *mo->mtr;
    char mtrname[80];
    snprintf( mtrname, sizeof(mtrname), "%s-mtr", title );
    mo->mtr = V7NewMtr( ctx, mtrname );
    *mo->mtr = oldmo;
    mo->mtr->col = col;
    V7AddMod( ctx->scn, mo );
    return mo;
}

static float megafade( float curr, float start, float end, float fadein, float fadeout )
{
    float rv = 1.0f;
         if( curr <  start       || curr >  end )          rv = 0.0f;
    else if( curr >= start       && curr <= start+fadein ) rv = 1.0f - sinf( PIF/2.0f + ((curr-start)        /fadein )*(PIF/2.0f) ); // in  fast..slow
    else if( curr >= end-fadeout && curr <= end          ) rv =        sinf( PIF/2.0f + ((curr-(end-fadeout))/fadeout)*(PIF/2.0f) ); // out slow..fast
    return rv;
}

static void Setup(void) {

    Scene *s = ctx->scn = V7NewScn(ctx, "MainScene");
    s->dat.light[0].pos.x = -3.f;
    s->dat.light[0].pos.y =  2.f;
    s->dat.light[0].pos.z =  9.f;
    s->dat.light[0].val = 1000.f;

    s->dat.eye = (vec3){ .0f, .0f, -5.f };
    ctx->scn->dat.fov = 102.54f;

    bg[0] = V7NewMod(ctx, "BG"); V7AddMod(s, bg[0]);
    bg[1] = V7NewMod(ctx, "BG"); V7AddMod(s, bg[1]);
    fg[0] = V7NewMod(ctx, "FG"); V7AddMod(s, fg[0]);
    fg[1] = V7NewMod(ctx, "FG"); V7AddMod(s, fg[1]);

    (void)LoadMod("Empty");

	thebox = LoadMod("TheBox");
	thebox->t = (vec3){0.f, -5.f, -2.f};
	thebox->s = (vec3){1.5f, 1.5f, 1.5f};
	V7AddMod(s, thebox);
    V7Hide( thebox );
    V7Dissolve( thebox, 1.f );
    theboxx = V7GetMod(ctx, "Cube2");

    galdance = NewBG("Galdance", "galdance.fs", &gpugaldance, sizeof(gpugaldance));
    jquat    = NewBG("JQuat",    "quat.fs",     &gpuquat,     sizeof(gpuquat));
    rave     = NewBG("Rave",     "rave.fs",     0,            0);
    cyber    = NewBG("Cyber",    "cyber.fs",    &gpucyber,    sizeof(gpucyber));
    twofield = NewBG("Twofield", "twofield.fs", &gputwofield, sizeof(gputwofield));
    citywok  = NewBG("CityWok",  "citywok.fs",  &gpucitywok,  sizeof(gpucitywok));
    seascape = NewBG("Seascape", "seascape.fs", &gpuseascape, sizeof(gpuseascape));
    trans    = NewBG("Trans",    "trans.fs",    &gputrans,    sizeof(gputrans));
    colorful = NewBG("Colorful", "colorful.fs", &gpucolorful, sizeof(gpucolorful));
    torus    = NewBG("Torus",    "torus.fs",    &gputorus,    sizeof(gputorus));
    madbulb  = NewBG("Mad",      "madbulb.fs",  &gpumadbulb,  sizeof(gpumadbulb));
    smoke    = NewBG("Smoke",    "smoke.fs",    0,            0);

    SDFont *fnt[3];
    fnt[0] = V7NewFnt(ctx, &sdf_kanit);    // In-demo text
    fnt[1] = V7NewFnt(ctx, &sdf_conflict); // Title/scroller text
    fnt[2] = V7NewFnt(ctx, &sdf_fradmit);  // Lower-left info and credits text

    bike      = LoadPicAsMod( s, "../data/bigbike.png",  "Bike",     &biketex,     (vec3){ 4.00f,   4.00f,  0.0f}, (vec3){ -13.95f,  -1.680f, 0.0f } ); // The Prisoner (1967-1968 TV series)
    cobrac    = LoadPicAsMod( s, "../data/cobra_c.png",  "CobraC",   &cobratex,    (vec3){ 3.00f,   3.00f,  0.0f}, (vec3){   0.00f, -48.000f, 1.0f } ); // Cobra Kai TV series (2018- TV series)
    cobransi  = LoadPicAsMod( s, "../data/mcga.png",     "Cobransi", &cobransitex, (vec3){ 3.00f,   3.00f,  0.0f}, (vec3){   0.00f,   0.000f, 1.0f } ); // Cobra (1986 film)
    secret    = LoadPicAsMod( s, "../data/secret.jpg",   "Secret",   &secrettex,   (vec3){ 4.545f,  4.545f, 0.0f}, (vec3){   0.00f,   0.000f, 1.0f } ); // UL: Brazil (1985 film), UR: District 9 (2009 film), LL: 1984 (1984 film), LR: The Double (2013 film)
    v73dlogo  = LoadPicAsMod( s, "../data/v73d.png",     "V73D",     &v73dtex,     (vec3){ 0.275f,  0.275f, 0.0f}, (vec3){  7.025f,  -4.275f, 1.0f } ); // Great 3D engine
    apply     = LoadPicAsMod( s, "../data/apply.png",    "Apply",    &applytex,    (vec3){ 0.480f,  0.480f, 0.0f}, (vec3){  6.281f,  -4.062f, 1.0f } ); // Breaking Bad (2008-2013 TV series)
    notsure   = LoadPicAsMod( s, "../data/not_sure.png", "NotSure",  &notsuretex,  (vec3){ 0.850f,  0.850f, 0.0f}, (vec3){  6.880f,  -3.695f, 1.0f } ); // Idiocracy (2006 film)
    chrisl    = LoadPicAsMod( s, "../data/chrisl.png",   "ChrisL",   &chrisltex,   (vec3){ 0.600f,  0.600f, 0.0f}, (vec3){  7.470f,  -3.940f, 1.0f } ); // Legendary musician looking left
    chrisr    = LoadPicAsMod( s, "../data/chrisr.png",   "ChrisR",   &chrisrtex,   (vec3){ 0.600f,  0.600f, 0.0f}, (vec3){  7.470f,  -3.940f, 1.0f } ); // Legendary musician looking right
    check     = LoadPicAsMod( s, "../data/check.png",    "Check",    &checktex,    (vec3){ 1.300f,  1.300f, 0.0f}, (vec3){  0.000f, -41.000f, 1.0f } ); // Commodore Amiga logo from 1985
    reticle   = LoadPicAsMod( s, "../data/reticle.png",  "Reticle",  &reticletex,  (vec3){ 1.300f,  1.300f, 0.0f}, (vec3){  0.000f,   0.000f, 1.0f } ); // Actually a shotgun reticle

    shtxt_cred   = NewText( "CredTxtS",   fnt[2], (vec3){ .4f,   .4f, .4f}, (vec3){-2.7f,   -1.f,   2.0f},(vec4){ 0.0f, 0.0f, 0.0f, 1.0f} );
    txt_cred     = NewText( "CredTxt",    fnt[2], (vec3){ .4f,   .4f, .4f}, (vec3){-2.7f,   -1.f,   2.0f},(vec4){ 0.0f, 1.0f, 1.0f, 1.0f} );
    shtxt_llinfo = NewText( "LLInfoTxtS", fnt[2], (vec3){ .2f,   .2f, .2f}, (vec3){-6.055f, -2.96f, 2.0f},(vec4){ 0.0f, 0.0f, 0.0f, 1.0f} );
    txt_llinfo   = NewText( "LLInfoTxt",  fnt[2], (vec3){ .2f,   .2f, .2f}, (vec3){-6.055f, -2.96f, 2.0f},(vec4){ 0.0f, 1.0f, 1.0f, 1.0f} );
    shtxt_info   = NewText( "InfoTxtS",   fnt[0], (vec3){ .4f,   .4f, .4f}, (vec3){-2.7f,   -1.f,   2.0f},(vec4){ 0.0f, 0.0f, 0.0f, 1.0f} );
    txt_info     = NewText( "InfoTxt",    fnt[0], (vec3){ .4f,   .4f, .4f}, (vec3){-2.7f,   -1.f,   2.0f},(vec4){ 0.0f, 1.0f, 1.0f, 1.0f} );
    shtxt_title1 = NewText( "TitleTxt1S", fnt[1], (vec3){1.405f,1.5f,1.4f}, (vec3){-3.f,     2.f,   2.0f},(vec4){ 1.0f, 1.0f, 1.0f, 1.0f} );
    txt_title1   = NewText( "TitleTxt1",  fnt[1], (vec3){1.4f,  1.4f,1.4f}, (vec3){-3.f,     2.f,   2.0f},(vec4){ 1.0f, 1.0f, 0.0f, 1.0f} );
    shtxt_title2 = NewText( "TitleTxt2S", fnt[1], (vec3){1.405f,1.5f,1.4f}, (vec3){-3.f,     2.f,   2.0f},(vec4){ 1.0f, 1.0f, 1.0f, 1.0f} );
    txt_title2   = NewText( "TitleTxt2",  fnt[1], (vec3){1.4f,  1.4f,1.4f}, (vec3){-3.f,     2.f,   2.0f},(vec4){ 1.0f, 1.0f, 0.0f, 1.0f} );
    shtxt_scroll = NewText( "ScrollTxtS", fnt[1], (vec3){1.8f,  1.8f,1.8f}, (vec3){ 0.0f,    2.20f, 2.0f},(vec4){ 0.0f, 0.0f, 0.0f, 1.0f} );
    txt_scroll   = NewText( "ScrollTxt",  fnt[1], (vec3){1.8f,  1.8f,1.8f}, (vec3){ 0.0f,    2.20f, 2.0f},(vec4){ 0.0f, 0.0f, 0.0f, 1.0f} );
    txt_debug    = NewText( "DebugTxt",   fnt[2], (vec3){ .1f,   .1f, .1f}, (vec3){-6.0f,    3.2f,  2.0f},(vec4){ 1.0f, 1.0f, 1.0f, 1.0f} );

    // Twofield static colors
    gputwofield.col[0] = v4_mul3( v4_set( 1.0f, 1.0f, 1.0f, 0.0f ),  80.0f ); gputwofield.col[1] = v4_mul3( v4_set( 0.0f, 1.0f, 1.0f, 0.0f ),  40.0f );
    gputwofield.col[2] = v4_mul3( v4_set( 0.0f, 1.0f, 0.0f, 0.0f ),  40.0f ); gputwofield.col[3] = v4_mul3( v4_set( 1.0f, 0.0f, 0.0f, 0.0f ),  40.0f );
    gputwofield.col[4] = v4_mul3( v4_set( 1.0f, 1.0f, 1.0f, 0.0f ),  80.0f ); gputwofield.col[5] = v4_mul3( v4_set( 0.0f, 1.0f, 0.0f, 0.0f ),  40.0f );
    gputwofield.col[6] = v4_mul3( v4_set( 0.0f, 1.0f, 1.0f, 0.0f ),  40.0f ); gputwofield.col[7] = v4_mul3( v4_set( 1.0f, 0.0f, 0.0f, 0.0f ),  40.0f );

    V7CompShd(ctx);

    pstart = usefaketime ? faketime : V7Time();
}

static v4 rotate( float frac, float tilt, float radius )
{
    float yv = sinf( tilt );
    float xv = cosf( tilt );
    float sinxy = radius * sinf( frac * (PIF * 2.0f) );

    return v4_set( sinxy * xv,
                   sinxy * yv,
                  -radius * cosf( frac * (PIF * 2.0f) ),
                   0.0f );
}

#define MAXX(a,b) ((a)>(b)?(a):(b))
#define MIIN(a,b) ((a)<(b)?(a):(b))

// Windows get remapped keycodes. Quick workaround:
#ifdef _WIN32
#define _SPACE_ ' '
#define _L_ 'L'
#define _M_ 'M'
#define _R_ 'R'
#endif

///////////////////////////////////////////////////////////////////// LOOP //
static void Loop(void) {
    faketime = framectr/60.0;
    if( usefaketime ) ctx->dat.time = (float)faketime;

    v4 cc = h2rgb( ctx->dat.time*0.165f, cols4, COLS4CNT );
    vec4 colly = (vec4){ cc.x, cc.y, cc.z, SCROLLER_ALPHA };

#ifndef __ANDROID__
    if(ctx->key[_R_])     { ctx->key[_R_]     = 0; V7ReloadShd(ctx); V7CompShd(ctx); V7List(ctx); }
#endif
    if(ctx->key[_ESC_])   { ctx->key[_ESC_]   = 0; quit=1; }
    if(ctx->key[_L_])     { ctx->key[_L_]     = 0; V7List(ctx); }
    if(ctx->key[_M_])     { ctx->key[_M_]     = 0; V7PrtMtx(ctx->scn); }
    if(ctx->key[_SPACE_]) { ctx->key[_SPACE_] = 0; switchpart=part+1; }

    double t = usefaketime ? faketime : V7Time();

    Demopart *dp = &demopart[part];
    party pid = dp->partid;

    pdiff = t-pstart;
    prest = pnext-t;

    if(pdiff > dp->partlen) switchpart = part+1;

    if(switchpart!=-1) {
        part = switchpart%NPARTS;
        dp = &demopart[part];
        pid = dp->partid;
        switchpart = -1;
        pstart = t;
        pnext = t + dp->partlen;

        if( pid == PART_BOX1 || pid == PART_PRELUDE || pid == PART_CREDITS || pid == PART_BOX2 || pid == PART_SNAPSHOT ) {
            // Parts that use title text
            txt_title1->t   = dp->title1pos; V7Txt(txt_title1, dp->title1txt);
            txt_title2->t   = dp->title2pos; V7Txt(txt_title2, dp->title2txt);
            shtxt_title1->t = dp->title1pos; shtxt_title1->t.x += 0.005f; shtxt_title1->t.y -= 0.000f; shtxt_title1->t.z -= 0.00001f;
            shtxt_title2->t = dp->title2pos; shtxt_title2->t.x += 0.005f; shtxt_title2->t.y -= 0.000f; shtxt_title2->t.z -= 0.00001f;
            V7Txt(shtxt_title1, dp->title1txt); V7Show(shtxt_title1 ); V7Show(txt_title1 );
            V7Txt(shtxt_title2, dp->title2txt); V7Show(shtxt_title2 ); V7Show(txt_title2 );
        } else {
            // Rest uses scroller
            V7Hide( txt_title1 ); V7Hide( shtxt_title1 );
            V7Hide( txt_title2 ); V7Hide( shtxt_title2 );
            V7Txt( txt_scroll,   dp->title1txt );
            V7Txt( shtxt_scroll, dp->title1txt );
            memcpy(vtx_scroll, txt_scroll->msh->vvtx.arr, txt_scroll->msh->vvtx.num*sizeof(vec3));
            memcpy(shvtx_scroll, shtxt_scroll->msh->vvtx.arr, shtxt_scroll->msh->vvtx.num*sizeof(vec3));
            V7Show(txt_scroll);
            V7Show(shtxt_scroll);
        }

        shtxt_llinfo->t = txt_llinfo->t;
        shtxt_llinfo->t.x += 0.015f; shtxt_llinfo->t.y -= 0.015f; shtxt_llinfo->t.z -= 0.00001f;
        V7Txt(shtxt_llinfo, dp->llinfotxt);

        V7Txt(txt_llinfo, dp->llinfotxt);

        if( pid == PART_CREDITS ) {
            shtxt_cred->t = dp->infopos;
            shtxt_cred->t.x += 0.025f; shtxt_cred->t.y -= 0.025f; shtxt_cred->t.z -= 0.00001f;
            V7Txt(shtxt_cred, dp->infotxt);
            txt_cred->t = dp->infopos;
            V7Txt(txt_cred, dp->infotxt);
        } else {
            shtxt_info->t = dp->infopos;
            shtxt_info->t.x += 0.025f; shtxt_info->t.y -= 0.025f; shtxt_info->t.z -= 0.00001f;
            V7Txt(shtxt_info, dp->infotxt);
            txt_info->t = dp->infopos;
            V7Txt(txt_info, dp->infotxt);
        }

        *bg[1] = *bg[0]; bg[1]->t.z = -21.9; *bg[0] = *V7GetMod(ctx, dp->bg); bg[0]->t.z = -22.0f;
        *fg[1] = *fg[0]; fg[1]->t.z = -19.9; *fg[0] = *V7GetMod(ctx, dp->fg); fg[0]->t.z = -20.0f;

        // It's considered wise to hide all PNGs here
        V7Hide( thebox ); V7Hide( apply );    V7Hide( notsure ); V7Hide( bike );
        V7Hide( cobrac ); V7Hide( cobransi ); V7Hide( secret );  V7Hide( v73dlogo ); V7Hide( chrisl ); V7Hide( chrisr ); V7Hide( check );

        switch( pid ) {
        case PART_BOX1:     break; // check code below for secret frame
        case PART_PRELUDE:  gpugaldance.starttime = ctx->dat.time; V7Show( v73dlogo ); break;
        case PART_QUAT:	    gpuquat.starttime     = ctx->dat.time; V7Show( apply    ); apply->dissolve   = 1.0f; break;
        case PART_CYBER:    gpucyber.starttime    = ctx->dat.time; V7Show( notsure  ); notsure->dissolve = 1.0f; break;
        case PART_TWOFIELD: gputwofield.starttime = ctx->dat.time; V7Show( chrisl   ); break;
        case PART_CITYWOK:  gpucitywok.starttime  = ctx->dat.time; V7Show( bike     ); break;
        case PART_SEASCAPE: gpuseascape.starttime = ctx->dat.time; break;
        case PART_TRANS:    gputrans.starttime    = ctx->dat.time; V7Show( cobransi ); break;
        case PART_COLORFUL: gpucolorful.starttime = ctx->dat.time; break;
        case PART_TORUS:    gputorus.starttime    = ctx->dat.time; break;
        case PART_MAD:	    gpumadbulb.starttime  = ctx->dat.time; break;

        case PART_CREDITS:  apply->t   = (vec3){ -4.58f, -11.960f, 1.0f }; apply->dissolve   = 0.0f; V7Show( apply );
                            notsure->t = (vec3){  4.00f, -11.960f, 1.0f }; notsure->dissolve = 0.0f; V7Show( notsure );
                            chrisr->t  = (vec3){ -5.28f, -17.060f, 1.0f }; chrisr->dissolve  = 0.0f; V7Show( chrisr );
                            V7Show( cobrac ); V7Show( v73dlogo ); V7Show( check ); break;
        case PART_BOX2:     V7Show( thebox ); break;
        case PART_SNAPSHOT: gputrans.starttime    = ctx->dat.time - 3.66667f; V7Show( cobransi ); break;
        }

        V7Hide(txt_info); // avoid flash on change
    } else {
        V7Show(txt_info); // show after change
    }

    float parttime = (float)(ctx->dat.time - pstart);

    if( pid == PART_SNAPSHOT ) gpugaldance.starttime    = ctx->dat.time - 3.66667f;

    // Text coloring
    txt_title1->mtr->col  = dp->titlecol.x  != -1.0f ? dp->titlecol  : (vec4){ 1.0f, 0.0f, 0.0f, 1.0f };
    txt_title2->mtr->col  = dp->titlecol.x  != -1.0f ? dp->titlecol  : (vec4){ 1.0f, 0.0f, 0.0f, 1.0f };
    txt_info->mtr->col    = dp->infocol.x   != -1.0f ? dp->infocol   : (vec4){ 0.0f, 1.0f, 1.0f, 1.0f };
    txt_llinfo->mtr->col  = dp->llinfocol.x != -1.0f ? dp->llinfocol : (vec4){ 0.0f, 1.0f, 1.0f, 1.0f };
    txt_scroll->mtr->col  = dp->scrollcol.x != -1.0f ? dp->scrollcol : colly;

    // *** Torus control code
    {
        float tt = ctx->dat.time - gputorus.starttime;
             if( tt <= 2.0f )  gputorus.scale = 2.0f - sinf(tt/2.0f*PIF/2.0f)*1.5f;
        else if( tt >= 19.5f ) gputorus.scale = 0.5f + (1.0f-sinf((tt-19.5f)/2.0f+PIF/2.0f))*2.0f;
        else                   gputorus.scale = 0.5f;
    }

    // *** Cyber control code
    {
//        vec3 ta  = vec3(sin(iTime*0.6f*0.8571f)*2.0f, cos(iTime*0.5f*0.8571f)*2.0f,0.0); // Cam direction
//        vec3 ro  = vec3(0.0, 0.0, -5.2f+iTime*0.140f ); // Move forwards, not backwards
        float tt = ctx->dat.time - gpucyber.starttime;
        float wt = tt >= 15.0f ? fmaxf( 1.0f-(tt-15.0f)*0.29f, 0.0f ) : 1.0f;
        gpucyber.ta_in = v4_set( sinf(tt*0.6f*1.14f*0.8571f)*1.8f*wt, cosf(tt*0.5f*1.14f*0.8571f)*1.8f*wt, 0.0f, 0.0f);
        gpucyber.ro_in = v4_set( 0.0f, 0.0f, -5.2f+tt*0.160f, 0.0f );

        if( pid == PART_CYBER && parttime >= 18.5f-4*0.0166667f )
            V7Show( reticle );
        if( pid == PART_CITYWOK && parttime >= 0.75f )
            V7Hide( reticle );

        gpucyber.scale = tt >= 19.0f ? fmaxf( 0.05f, 1.0f-(tt-19.0f)*0.50f ) : 1.0f;
   }

    // *** Quat control code
    {
        // This turned into a load of rubbish. Cleanup on aisle 14.
        gpuquat.ax  = 10.0f * 16.0f / 9.0f;
        gpuquat.ay  = 10.0f;
        gpuquat.start_pos = v4_set( gpuquat.ax  * -0.5f - 0.0f, gpuquat.ay  * -0.5f - 0.0f, 12.0f, 0.0f );
        gpuquat.plane_col = v4_set( 1.0f, 0.0f, 0.0f, 1.0f );
        gpuquat.shadow = 1.0f;
        gpuquat.shadow_col = v4_set( 0.0f, 0.0f, 0.0f, 1.0f );
        gpuquat.plane_norm = v4_normalize3( v4_set( 0.0f, -1.0f, 0.0f, 6.5f ) );

        int frs = (int)(roundf((ctx->dat.time-gpuquat.starttime)*60.0f));

        int smu = (frs/QUAT_MU_TIME)%(QUATCNT-1);
        int qfr = frs%QUAT_MU_TIME;
        static v4 topmu;
        static float topz;
        if( qfr < QUAT_MIX_TIME/2 ) {
            float wt = sinf( (qfr%QUAT_MIX_TIME)/(float)(QUAT_MIX_TIME) * PIF );
            if( smu == QUATCNT-2 )
                gpuquat.mu_pos = v4_mix( qpostable[smu].pos, v4_mul4( qpostable[smu+1].pos, 1.0f), wt );
            else
                gpuquat.mu_pos = v4_mix( qpostable[smu].pos, v4_mul4( qpostable[smu+1].pos, OVERSHOOT), wt );
            gpuquat.start_pos.z = qpostable[smu].zoom*(1.0f-wt) + qpostable[smu+1].zoom*wt*ZOOMSHOOT;
            topmu = gpuquat.mu_pos;
            topz = gpuquat.start_pos.z;
        } else if( qfr < QUAT_MIX_TIME ) {
            float wt = (sinf( ( (((qfr-QUAT_MIX_TIME/2)%QUAT_MIX_TIME)/(float)QUAT_MIX_TIME) * (PIF*2.0f) + PIF/2.0f ))+ 1.0f)/2.0f;
            gpuquat.mu_pos = v4_mix( qpostable[smu+1].pos, topmu, wt );
            gpuquat.start_pos.z = qpostable[smu+1].zoom*(1.0f-wt) + topz*(wt);
        } else {
            gpuquat.mu_pos = qpostable[smu+1].pos;
            gpuquat.start_pos.z = qpostable[smu+1].zoom;
        }
        int rfr = frs%QUAT_ROT_TIME;
        gpuquat.rot_xz = sinf( rfr/(float)(QUAT_ROT_TIME) * 2.0f*(PIF) ) * PIF;
        gpuquat.obj_col = h2rgb( (ctx->dat.time-gpuquat.starttime)*60.0f/(QUAT_ROT_TIME*3.0f), cols3, COLS3CNT );
        gpuquat.plane_norm = v4_normalize3( v4_set( 0.0f, -1.0f, 0.0f, 6.5f ) );

        float tt = ctx->dat.time-gpuquat.starttime;
        if( tt >= 19.5f ) gpuquat.start_pos.z -= (tt-19.5f)*4.75f;
    }

    // *** Twofield control code
    {
        // Calculate metaball trajectories
        float tt = ctx->dat.time-gputwofield.starttime;
        float ttt = tt * 0.45f;

       float wa = 1.0f;
       if( tt <= 2.0f )
           wa = 2.0f*sinf01( PIF*0.5f + (tt*(1.0f/2.0f)*PIF ) ) + 1.0f;
       else if( tt >= 20.5f )
           wa = 0.000f;
       else if( tt >= 18.5f )
           wa = 1.0f - sinf( (tt-18.5f)*(1.0f/2.0f)*PIF*0.5f );

        for( int i = 0; i < TFNUM; ++i ) {
            float fi;
            fi = i*0.7f; gputwofield.b1[i] = v4_mul4( v4_set( 3.7f*sinf(ttt     +fi), 1.0f+10.0f*cosf(ttt*1.1f+fi),  2.3f*sinf(ttt*2.3f+fi), 0.0f ), wa );
            fi = i*1.2f; gputwofield.b2[i] = v4_mul4( v4_set( 4.4f*cosf(ttt*0.4f+fi),-1.0f-10.0f*cosf(ttt*0.7f+fi), -2.1f*sinf(ttt*1.3f+fi), 0.0f ), wa );
        }
        // Move lights
        float fw = ttt*0.325f;
        float xadj[LIGHTNUM] = {  0.000f,  0.125f,  0.250f,  0.375f, 0.500f, 0.625f, 0.750f, 0.875f };
        float yadj[LIGHTNUM] = { -7.000f, -5.000f, -3.000f, -1.000f, 1.000f, 3.000f, 5.000f, 7.000f };
        for( int i = 0; i < LIGHTNUM; i++ ) {
            gputwofield.pos[i] = rotate( fract( fw + xadj[i] ), 0.0f, 9.5f );
            gputwofield.pos[i].y = yadj[i];
        }

        gputwofield.O     = v4_set( 0.0f, 0.0f, 20.0f, 0.0f ); // rotation disabled... or is it?

        gputwofield.scale = tt >= 18.5f ? 1.0f-(tt-18.5f)*0.30f : 1.0f;
    }

    // *** Seascape control code
    {
        float time = ctx->dat.time-gpuseascape.starttime;
        float ttime = (ctx->dat.time-gpuseascape.starttime) * 0.5f + 2.1f;
        gpuseascape.inori = v4_set( 0.0f, 3.5f, 0.0f, 0.0f );
        v4 ang = v4_set( sinf(ttime*3.0f)*0.1f, sinf(ttime)*0.190f+0.3f, ttime, 0.5f );
        float m = 1.0f - sinf01( PIF/2.0f + (time-18.5f)*0.50f*PIF );
        if( time >= 20.5f ) m = 1.0f;
        if( time >= 18.5f ) ang.y = mix( ang.y, -0.7f, m );
        v2 a1 = v2_set( sinf( ang.x ), cosf( ang.x ) );
        v2 a2 = v2_set( sinf( ang.y ), cosf( ang.y ) );
        v2 a3 = v2_set( sinf( ang.z ), cosf( ang.z ) );
        gpuseascape.m0 = v4_set(  a1.y*a3.y+a1.x*a2.x*a3.x, a1.y*a2.x*a3.x+a3.y*a1.x, -a2.y*a3.x, 0.0f );
        gpuseascape.m1 = v4_set( -a2.y*a1.x, a1.y*a2.y, a2.x, 0.0f );
        gpuseascape.m2 = v4_set(  a3.y*a1.x*a2.x+a1.y*a3.x, a1.x*a3.x-a1.y*a3.y*a2.x, a2.y*a3.y, 0.0f );
    }

    // *** Madbulb control code
    {
        // This is what happens when you try to fix a glitch. Don't try this at home, kids!
        float minpower =  3.5f, maxpower = 10.0f, powerlen = 20.0f;
        float zoommax  = 2.1f, zoomtime = 2.0f, waittime = zoomtime-0.75f;
        float tt  =  ctx->dat.time - gpumadbulb.starttime;
        float zf = tt >= 17.75f ? sinf01( fmaxf( PIF*0.5f + (tt-17.75f)*1.15f, 0.001f ) ) : 1.0f;
        float zv = sinf(PIF/2.0f + (tt-zoomtime)*1.6f);
        gpumadbulb.zoom = tt <= zoomtime ? (sinf( (tt/zoomtime)*PIF+1.5f*PIF )+1.0f)*0.5f*zoommax : (1.77f + 0.4f*sinf( zv ))*zf;
        gpumadbulb.power = minpower + (tt/powerlen)*(maxpower-minpower);
        static float fy = 0.0f;
        float tt2 = tt <= waittime ? 0.0f : (tt-waittime)*fy;
        if( (pid == PART_MAD || pid == PART_BOX2) && tt > waittime ) fy += 1.0f/((tt-waittime+1.00f)*330.0f);
        gpumadbulb.pitch = sinf( tt2*2.0f )*0.25f;
        gpumadbulb.yaw   = sinf( tt2      )*0.50f;
    }

    if( pid == PART_CREDITS ) {
        txt_cred->t.y     += 0.0220f;
        shtxt_cred->t.y   += 0.0220f;
        if( txt_title1->t.y < 2.570f ) {
            txt_title1->t.y   += 0.02125f;
            shtxt_title1->t.y += 0.02125f;
        }
        apply->t.y   += 0.0295f;
        notsure->t.y += 0.0295f;
        chrisr->t.y  += 0.0295f;
        check->t.y   += 0.0295f;
        if( cobrac->t.y < -0.25f ) cobrac->t.y += 0.0295f;
    }

    if( pid == PART_CITYWOK && parttime > 4.75f ) bike->t.x += 0.16f;

    float tdis = 1.0f;
    float bfdis = 1.0f;
    if( ctx->dat.time + pfad >= pnext ) {
        // fade text out during last 2 secs
        tdis = (float)((pnext - ctx->dat.time)/pfad);
        bfdis = 1.0f;
    } else if( ctx->dat.time - pstart <= pfad ) {
        // fade text in and fg+bg out/in first 2 secs
        tdis = (float)((ctx->dat.time - pstart)/pfad);
        bfdis = tdis;
    }

    // fade shaders and texts
    fg[0]->dissolve = bg[0]->dissolve = (1.0f-bfdis);
    fg[1]->dissolve = bg[1]->dissolve = bfdis;
    txt_title1->mtr->col.w   = tdis;
    txt_title2->mtr->col.w   = tdis;
    txt_llinfo->mtr->col.w   = tdis;
    shtxt_title1->mtr->col.w = tdis;
    shtxt_title2->mtr->col.w = tdis;
    shtxt_llinfo->mtr->col.w = tdis;

    cobrac->dissolve    = 1.0f-tdis;

    float mf  = megafade( parttime, dp->infostart,      dp->infoend, 1.00f, 1.0f );
    float mfs = megafade( parttime, dp->infostart+0.5f, dp->infoend, 0.25f, 1.0f );
    txt_info->mtr->col.w   = mf;
    shtxt_info->mtr->col.w = mfs;

    // Fade logos as text
    if( pid == PART_CYBER   )  notsure->dissolve  = 1.0f - mf;
    if( pid == PART_TWOFIELD ) chrisl->dissolve   = 1.0f - mf;
    if( pid == PART_QUAT     ) apply->dissolve    = 1.0f - mf;
    if( pid == PART_TRANS )    cobransi->dissolve = 1.0f - mf;
    if( pid == PART_PRELUDE ||
        pid == PART_CREDITS )  v73dlogo->dissolve = 1.0f - mf;

    // *** Prelude control code
    if( pid == PART_PRELUDE ) {
        static char *numerals = "\x02""X IX VIII VII VI V IV III II I ? ";
        char buf[80];
        strcpy( buf, numerals );
        int cnt = (int)floorf((float)(ctx->dat.time-pstart)*1.07f) - 3;
        cnt = MIIN( MAXX( cnt, 0 ), 11 );
        char *p = buf + (cnt == 0 ? 1 : 0);
        for( int i = 0; i < cnt; i++ )
            p = strstr( p+1, " " );
        *p++ = '\n'; *p = 0;
        V7Txt(txt_title2, buf);
        V7Txt(shtxt_title2, buf);

        float mfa, mfas;
        if( parttime >= 7.5f ) {
            char *txt = "\x02""\n\n\n\n"
                        "\x02""Running on\n\n"
                        "\x02""NVidia Jetson AGX Orin\n\n"
                        "\x02""1080p60\n";
            V7Txt( txt_info, txt );
            V7Txt( shtxt_info, txt );
            mfa  = megafade( parttime, 7.5f,      15.0f, 1.00f, 1.0f );
            mfas = megafade( parttime, 7.5f+0.5f, 15.0f, 0.25f, 1.0f );
        } else {
            mfa  = megafade( parttime, 0.0f,      7.5f, 1.00f, 1.0f );
            mfas = megafade( parttime, 0.0f+0.5f, 7.5f, 0.25f, 1.0f );
        }
        txt_info->mtr->col.w = mfa;
        shtxt_info->mtr->col.w = mfas;
    }

    if( pid == PART_BOX1 ) {
        // Confusing line
        txt_info->mtr->col.w = 0.0f;
        if( framectr > 1 &&  parttime < 2.0f ) {
            //                                    ...................
            const char *fu = "\n\n\n\n\n\n""\x01""8100000A.48454C50"; // Amiga guru meditation code: Bogus Exception, HELP
            char str[80];
            strcpy( str, fu ); // Thank you, msvc
            int cnt = MIIN( (int)strlen(str), (int)(7 + parttime*20.0f) );
            str[cnt] = '\n';
            str[cnt+1] = 0;
            txt_info->mtr->col = (vec4){ 0.0f, 1.0f, 1.0f, 1.0f };
            V7Txt( txt_info, str );
        }

        // Delay fade in
        V7Show( thebox );
        float mfa = megafade( parttime, 2.0f, 7.0f, 1.0f, 1.0f );
        txt_title1->mtr->col.w = mfa;
        txt_title2->mtr->col.w = mfa;
        shtxt_title1->mtr->col.w = mfa;
        shtxt_title2->mtr->col.w = mfa;

        V7Dissolve( thebox, 1.0f-mfa );

        Keyfrm anim[] = {
        {   0.0f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,  .0f,  0.f} },
        {   3.1f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,  .01f, 0.f} },
        {   3.8f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,  .1f,  0.f} },
        {   4.1f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 2.f,   0.f} },
        {   4.5f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,PI/4, 0.f}, (vec3){0.f, 2.f,   0.f} },
        {   5.0f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,PI/4, 0.f}, (vec3){0.f,  .05f, 0.f} },
        {   5.7f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,PI/4, 0.f}, (vec3){0.f,  .02f, 0.f} },
        {   7.0f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,PI/4, 0.f}, (vec3){0.f,  .01f, 0.f} }
        };

//        int len = sizeof(anim)/sizeof(Keyfrm);
        static int idx = 0; if( parttime > anim[idx+1].ts ) idx++;
        float t0 = anim[idx].ts;
        float t1 = anim[idx+1].ts;
        float tX = (parttime - t0) / (t1 - t0);

        vec3 rot;
        vec3_combine( &theboxx->t, 1.f-tX, &anim[idx].t, tX, &anim[idx+1].t );
        vec3_combine( &rot,        1.f-tX, &anim[idx].r, tX, &anim[idx+1].r );
    	quat_from_rot(&theboxx->r, &yaxis, rot.y);

    	vec3 axis = {-.1f, 1.f, .5f};
	    vec3_normalize(&axis, &axis);
	    quat_from_rot(&thebox->r, &axis, 2.2f+parttime/5.f);

        thebox->t.x = 1.f-parttime/7.f;
        thebox->t.y = -1.75f+parttime/5.f;
        thebox->t.z = -3.3f+parttime/2.f;
    }

    if( pid == PART_BOX2 ) {
        // Delay fade in
        float mfa = megafade( parttime, 1.0f, 7.0f, 1.0f, 1.0f );
        txt_title1->mtr->col.w = mfa;
        txt_title2->mtr->col.w = mfa;
        shtxt_title1->mtr->col.w = mfa;
        shtxt_title2->mtr->col.w = mfa;

        float mfb = megafade( parttime, 0.0f, 7.0f, 1.0f, 2.5f );
        V7Dissolve( thebox, 1.0f-mfb );

        Keyfrm anim[] = {
        {   0.0f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,PI/4, 0.f}, (vec3){0.f,  .0f,  0.f} },
        {   2.5f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,PI/4, 0.f}, (vec3){0.f,  .0f,  0.f} },
        {   2.9f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,PI/4, 0.f}, (vec3){0.f, 2.f,   0.f} },
        {   3.3f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 2.f,   0.f} },
        {   3.9f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,  .1f,  0.f} },
        {   4.3f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,  .05f, 0.f} },
        {   7.0f, (vec3){0.f, 0.f, 0.f}, (vec3){0.f, 0.f, 0.f}, (vec3){0.f,  .0f,  0.f} }
        };

//        int len = sizeof(anim)/sizeof(Keyfrm);
        static int idx = 0; if( parttime > anim[idx+1].ts ) idx++;
        float t0 = anim[idx].ts;
        float t1 = anim[idx+1].ts;
        float tX = (parttime - t0) / (t1 - t0);

        vec3 rot;
        vec3_combine( &theboxx->t, 1.f-tX, &anim[idx].t, tX, &anim[idx+1].t );
        vec3_combine( &rot,        1.f-tX, &anim[idx].r, tX, &anim[idx+1].r );
        quat_from_rot(&theboxx->r, &yaxis, rot.y);

        vec3 axis = {.6f, .9f, .4f};
        vec3_normalize(&axis, &axis);
        quat_from_rot(&thebox->r, &axis, parttime/3.f);

        thebox->t.y = -2.5f+parttime/3.f;
        thebox->t.z = -3.f+parttime/2.f;

    }

    // don't wrap demo, some black frames at end
    if( pid == PART_NADA && parttime >= 3.793f ) { quit = 1; return; }

    // Hide secret frame
    if( framectr == 1 && pid == PART_BOX1 ) V7Show( secret );
    if( framectr == 2 && pid == PART_BOX1 ) V7Hide( secret );

    if( pid != PART_PRELUDE && pid != PART_CREDITS && pid != PART_BOX1 && pid != PART_BOX2 ) {
        // Sine scroller, shadow slightly behind
        vec3 *v0 = (vec3*)txt_scroll->msh->vvtx.arr;
        vec3 *v1 = (vec3*)shtxt_scroll->msh->vvtx.arr;
        for(int i=0; i<txt_scroll->msh->vvtx.num; i++) {
            float ti = (float)(0.5f + ctx->dat.time - pstart);
            float tii = ti+4.0f/60.0f;
            v0[i].y = vtx_scroll[i].y   - sinf((v0[i].x+ti )/2.f + ti *2.f)/4.5f;
            v1[i].y = shvtx_scroll[i].y - sinf((v1[i].x+tii)/2.f + tii*2.f)/4.5f;
            txt_scroll->t.x   = 12.50f        - fmodf(ti*3.50f, 100.f);
            shtxt_scroll->t.x = 12.50f+0.055f - fmodf(ti*3.50f, 100.f);
            shtxt_scroll->t.z = txt_scroll->t.z - 0.00001f;
        }
        V7MeshUp(txt_scroll->msh);
        V7MeshUp(shtxt_scroll->msh);
    }
    V7Hide( thebox );

//  V7Txt( txt_debug, "%.2ffps", ctx->fps );

    V7Render(ctx);

    if( savepics ) {
        char fn[256];
        snprintf( fn, sizeof(fn), SAVEPATH, ctx->dat.frame );
        if( ctx->dat.frame%60 == 0 ) puts(fn);
        V7GrabFrame( ctx, fn );
    }

	framectr++;
}

static void SetCWD(char *rel) {
    char dir[260], div;
    size_t len;
#ifdef __linux__
    len = readlink("/proc/self/exe", dir, sizeof(dir)-1);
    div = '/';
#elif _WIN32
    len = GetModuleFileNameA(NULL, dir, sizeof(dir)-1);
    div = '\\';
#endif
    while(len>0)
    if(dir[--len]==div) {
        if(rel==0) dir[len] = 0; // use exe directory
        else strcpy(&dir[++len], rel); // relative path
        break;
    }
#ifdef __linux__
    int ret = chdir(dir);
#elif _WIN32
    SetCurrentDirectoryA(dir);
#endif
}

int main(int argc, char **argv)
{
    printf( "The Xmas Demo 2023 by Julin & Corneliusen\n" );
    printf( "More info here: https://www.ignorantus.com/xmasdemo/\n" );

    for( int i = 1; i < argc; i++ ) {
        if( !strcmp( argv[i], "-h" ) ) {
            printf( "Usage: %s [-w] [-v] [-m] [-f] [-o] [-d]\n", argv[0] );
            printf( "  -w: Run in window\n" );
            printf( "  -v: Disable vsync\n" );
            printf( "  -m: Enable 16x MSAA\n" );
            printf( "  -f: Turn off fake timer\n" );
            printf( "  -o: Save frames to %s\n", SAVEPATH );
            printf( "  -d: Set shader debug flag\n" );
            printf( "  -p <x>: Start at part x\n" );
            return 0;
        }
        if( !strcmp( argv[i], "-w" ) ) { fullscreen  = false; printf( "Windowed mode enabled\n" );         continue; }
        if( !strcmp( argv[i], "-v" ) ) { vsync       = false; printf( "Vsync disabled\n" );                continue; }
        if( !strcmp( argv[i], "-m" ) ) { msaa        = true;  printf( "16x MSAA on\n" );                   continue; }
        if( !strcmp( argv[i], "-f" ) ) { usefaketime = false; printf( "Fake time turned off\n" );          continue; }
        if( !strcmp( argv[i], "-o" ) ) { savepics    = true;  printf( "Saving frames to %s\n", SAVEPATH ); continue; }
        if( !strcmp( argv[i], "-d" ) ) { shdebug     = true;  printf( "Shader debug flag enabled\n" );     continue; }
        if( !strcmp( argv[i], "-p" ) && i < argc-1 ) {
            switchpart = atoi( argv[i+1] );
            printf( "Start at part %d\n", switchpart );
            i++;
            continue;
        }
        printf( "Unknown argument %s!\n", argv[i] );
        return 1;
    }

    SetCWD(0);

    ctx = V7Create(L"A M00se once bit my sister...", 0, 0, RENDERW, RENDERH, fullscreen, vsync?1:0, msaa?16:0);
    if(!ctx) return -1;

    glClearColor( 0, 0, 0, 0 );

    if( usefaketime ) V7SET( ctx->flags, V7TOFF );

    ctx->dat.width  = RENDERW;
    ctx->dat.height = RENDERH;
    ctx->dat.aspect = ctx->dat.width / (float)ctx->dat.height;

    Setup();

    double t0 = V7Time();
    while(!quit)
        Loop();
    double t1 = V7Time();

    printf( "%d frames rendered in %f seconds: %f fps\n\n", framectr, t1-t0, framectr/(t1-t0) );

    V7Destroy(ctx);

    return 0;
}
