/*
 *  Dynadraw For OpenGL & GLUT
 *  A port of Paul Haeberli's 1989 Dynadraw
 *  By Nicholas Zambetti
 *
 *  left mouse  - used for drawing
 *  up arrow    - wider strokes
 *  down arrow  - narrower strokes
 *  's' key     - saves postscript
 *  'q' key     - exits
 *  'c' key     - clears page
 *
 *  To Compile On Mac OS X
 *  gcc -ansi -Wall dynadraw_2003.c -o dynadraw_2003 -lobjc -framework OpenGL -framework GLUT
 *
 */

#pragma mark ---- Includes ----

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <GLUT/glut.h>
#include <OpenGL/gl.h>

#pragma mark ---- Definitions ----

#define FPS 80
#define WINDOW_WIDTH 560
#define WINDOW_HEIGHT 380
#define MAXPOLYS 50000

#pragma mark ---- Type Declarations ----

typedef struct {
    float x;
    float y;
} vector;

typedef struct {
    float curx, cury;
    float velx, vely, vel;
    float accx, accy, acc;
    float angx, angy;
    float mass, drag;
    float lastx, lasty;
    int fixedangle;
} filter;

#pragma mark ---- Variable Declarations ----

int redrawDelay = 1000 / FPS;
int windowWidth;
int windowHeight;
int leftButtonDown = 0;
vector realMouse;

float initwidth = 1.5;
float width;
float odelx, odely;
float curmass, curdrag;
float polyverts[4 * 2 * MAXPOLYS];
int npolys;
long xsize, ysize;
long xorg, yorg;
filter mouse;

#pragma mark ---- Dynadraw Functions ----

float fgetmousex()
{
    return realMouse.x / (float) windowWidth;
}

float fgetmousey()
{
    return ((float) windowHeight - realMouse.y) / (float) windowHeight;
}

int getmousex()
{
    return (int) realMouse.x;
}

int getmousey()
{
    return windowHeight - (int) realMouse.y;
}

void filtersetpos(filter *f, float x, float y)
{
    f->curx = x;
    f->cury = y;
    f->lastx = x;
    f->lasty = y;
    f->velx = 0.0;
    f->vely = 0.0;
    f->accx = 0.0;
    f->accy = 0.0;
}

float flerp(float f0, float f1, float p)
{
    return ((f0 * (1.0 - p)) + (f1 * p));
}

int filterapply(filter *f, float mx, float my)
{
    float mass, drag;
    float fx, fy;

    /* calculate mass and drag */
    mass = flerp(1.0, 160.0, curmass);
    drag = flerp(0.00, 0.5, curdrag * curdrag);

    /* calculate force and acceleration */
    fx = mx - f->curx;
    fy = my - f->cury;
    f->acc = sqrt(fx * fx + fy * fy);
    if(f->acc < 0.000001){
        return 0;
    }
    f->accx = fx / mass;
    f->accy = fy / mass;

    /* calculate new velocity */
    f->velx += f->accx;
    f->vely += f->accy;
    f->vel = sqrt(f->velx * f->velx + f->vely * f->vely);
    f->angx = -f->vely;
    f->angy = f->velx;
    if(f->vel < 0.000001){
        return 0;
    }

    /* calculate angle of drawing tool */
    f->angx /= f->vel;
    f->angy /= f->vel;
    if(f->fixedangle) {
        f->angx = 0.6;
        f->angy = 0.2;
    }
    /* apply drag */
    f->velx = f->velx * (1.0 - drag);
    f->vely = f->vely * (1.0 - drag);

    /* update position */
    f->lastx = f->curx;
    f->lasty = f->cury;
    f->curx = f->curx + f->velx;
    f->cury = f->cury + f->vely;

    return 1;
}

void savepolys()
{
    FILE *of;
    int i;
    float *fptr;

    of = fopen("dynadraw.ps", "w");
    if(!of) {
        fprintf(stderr, "dynadraw: can't open out put file [dynadraw.ps]\n");
        return;
    }
    fprintf(of, "%%!\n");
    fprintf(of, "%f %f translate\n", 0.25 * 72, (11.0 - 0.75) * 72);
    fprintf(of, "-90 rotate\n");
    fprintf(of, "%f %f scale\n", 8.0 * 72, 8.0 * 72);
    fprintf(of, "0.0 setgray\n");
    printf("npolys %d\n", npolys);
    fptr = polyverts;
    for (i = 0; i < npolys; ++i) {
        fprintf(of, "newpath\n");
        fprintf(of, "%f %f moveto\n", fptr[0], fptr[1]);
        fptr += 2;
        fprintf(of, "%f %f lineto\n", fptr[0], fptr[1]);
        fptr += 2;
        fprintf(of, "%f %f lineto\n", fptr[0], fptr[1]);
        fptr += 2;
        fprintf(of, "%f %f lineto\n", fptr[0], fptr[1]);
        fptr += 2;
        fprintf(of, "closepath\n");
        fprintf(of, "fill\n");
    }
    fprintf(of, "showpage\n");
    fclose(of);
    fprintf(stderr, "PostScript saved to dynadraw.ps\n");
}

void drawsegment(filter *f)
{
    float delx, dely;
    float wid, *fptr;
    float px, py, nx, ny;

    wid = 0.04 - f->vel;
    wid = wid * width;
    if(wid < 0.00001){
        wid = 0.00001;
    }
    delx = f->angx * wid;
    dely = f->angy * wid;

    px = f->lastx;
    py = f->lasty;
    nx = f->curx;
    ny = f->cury;

    fptr = polyverts + 8 * npolys;
    glBegin(GL_POLYGON);
    fptr[0] = px + odelx;
    fptr[1] = py + odely;
    glVertex2fv(fptr);
    fptr += 2;
    fptr[0] = px - odelx;
    fptr[1] = py - odely;
    glVertex2fv(fptr);
    fptr += 2;
    fptr[0] = nx - delx;
    fptr[1] = ny - dely;
    glVertex2fv(fptr);
    fptr += 2;
    fptr[0] = nx + delx;
    fptr[1] = ny + dely;
    glVertex2fv(fptr);
    fptr += 2;
    glEnd();
    npolys++;
    if(npolys >= MAXPOLYS) {
        fprintf(stderr, "out of polys - increase the define MAXPOLYS\n");
        npolys--;
    }
    fptr -= 8;
    glBegin(GL_LINE_LOOP);
    glVertex2fv(fptr);
    fptr += 2;
    glVertex2fv(fptr);
    fptr += 2;
    glVertex2fv(fptr);
    fptr += 2;
    glVertex2fv(fptr);
    fptr += 2;
    glEnd();
    odelx = delx;
    odely = dely;
}

void clearWindow()
{
    npolys = 0;
    glClear(GL_COLOR_BUFFER_BIT);
    glutSwapBuffers();
}

#pragma mark ---- GLUT Callbacks ----

void onDisplay()
{
    float mx, my;
    if(leftButtonDown) {
        mx = 1.25 * fgetmousex();
        my = fgetmousey();
        if(filterapply(&mouse, mx, my)) {
            drawsegment(&mouse);
        }
    }
    glutSwapBuffers();
}

void onResize(int width, int height)
{
    windowWidth = width;
    windowHeight = height;
    glViewport(0, 0, windowWidth, windowHeight);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, 1.25, 0.0, 1.0, -1.0, 1.0);
    glMatrixMode(GL_MODELVIEW);
    clearWindow();
}

void onMouseButton(int button, int state, int xPos, int yPos)
{
    float mx, my;
    if(button == GLUT_LEFT_BUTTON) {
        switch(state) {
            case GLUT_UP:
                leftButtonDown = 0;
                break;
            case GLUT_DOWN:
                mx = 1.25 * fgetmousex();
                my = fgetmousey();
                filtersetpos(&mouse, mx, my);
                odelx = 0.0;
                odely = 0.0;
                leftButtonDown = 1;
                break;
        }
    }
}

void onMouseMove(int x, int y)
{
    realMouse.x = (float) x;
    realMouse.y = (float) y;
}

void onTick(int timerId)
{
    glutPostRedisplay();
    glutTimerFunc(redrawDelay, onTick, 0);
}

void onKeyChar(unsigned char inKey, int x, int y)
{
    switch(inKey) {
        case 27:
        case 'q':
        case 'Q':
            exit(0);
            break;
        case 'S':
        case 's':
            savepolys();
            break;
        case 'c':
        case 'C':
            clearWindow();
            break;
        case 'f':
        case 'F':
            mouse.fixedangle = 1 - mouse.fixedangle;
            break;
    }
}

void onKeySpecial(int inKey, int x, int y)
{
    switch(inKey) {
        case GLUT_KEY_DOWN:
            initwidth /= 1.414213;
            width = initwidth;
            break;
        case GLUT_KEY_UP:
            initwidth *= 1.414213;
            width = initwidth;
            break;
    }
}

#pragma mark ---- main ----

int main(int argc, const char *argv[])
{
    /* dynadraw init */
    curmass = 0.5;
    curdrag = 0.15;
    mouse.fixedangle = 1;
    width = initwidth;

    /* glut init */
    glutInit(&argc, (char **) argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
    glutInitWindowPosition(50, 50);
    glutInitWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT);
    glutCreateWindow("Draw");

    /* gl init */
    glShadeModel(GL_SMOOTH);
    glClearColor(0.6, 0.2, 0.0, 1.0);
    onResize(WINDOW_WIDTH, WINDOW_HEIGHT);
    glColor3f(1.0, 1.0, 1.0);

    /* register glut event callbacks */
    glutReshapeFunc(onResize);
    glutDisplayFunc(onDisplay);
    glutKeyboardFunc(onKeyChar);
    glutSpecialFunc(onKeySpecial);
    glutMouseFunc(onMouseButton);
    glutMotionFunc(onMouseMove);
    glutPassiveMotionFunc(onMouseMove);
    glutTimerFunc(redrawDelay, onTick, 0);

    /* begin event loop */
    glutMainLoop();

    return 0;
}
