#include "private.h"
#include "world.h"
#include "drawing.h"
static double colorBack[4] = {1, 1, 1, 1};
static double *path;
static size_t pathSize;
static size_t pathAllocated;
static HeliDrawingState statesStack[1024] = {{
{0, 0, 0, 1}, // colorStroke
{0.5, 0.5, 0.5, 1}, // colorFill
1, // lineWidth
HALIGN_LEFT, // horAlign
VALIGN_TOP, // vertAlign
NULL, // font
24 // fontSize
}};
static int statesStackIndex = 0;
typedef struct _HeliStrokePoint {
double x, y, dx, dy;
} HeliStrokePoint;
typedef struct _HeliStroke {
HeliStrokePoint *points;
int count;
int allocatedCount;
} HeliStroke;
static void endPath(int close, int stroke, int fill, int fillSimple);
static void closePathSimple()
{ endPath(TRUE, TRUE, FALSE, TRUE); }
void background(const char *color)
{ heliParseColor(color, colorBack); }
void fill(const char *color)
{ heliParseColor(color, heliDrawingGetState()->colorFill); }
void noFill()
{ fill("transparent"); }
void stroke(const char *color)
{ heliParseColor(color, heliDrawingGetState()->colorStroke); }
void noStroke()
{ stroke("transparent"); }
void strokeWeight(double weight)
{ heliDrawingGetState()->lineWidth = weight; }
const char* rgba(double r, double g, double b, double a) {
static char buf[1024];
snprintf(buf, sizeof(buf) - 1, "%f %f %f %f", r, g, b, a);
return buf;
}
const char* rgb(double r, double g, double b)
{ return rgba(r, g, b, 1); }
void translate(double x, double y)
{ glTranslated(x, y, 0); }
void rotate(double angle)
{ glRotated(angle, 0, 0, 1); }
void scale(double x, double y)
{ glScaled(x, y, 1); }
void zoom(double z)
{ scale(z, z); }
void cliprect(double x, double y, double width, double height) {
double eq[4][4] = {
{ 1, 0, 0, -x },
{-1, 0, 0, x+width },
{ 0, 1, 0, -y },
{ 0,-1, 0, y+height } };
for(int i = 0; i < 4; ++i) {
glEnable(GL_CLIP_PLANE0 + i);
glClipPlane(GL_CLIP_PLANE0 + i, eq[i]);
}
}
void noClip()
{ for(int i = 0; i < 4; ++i) glDisable(GL_CLIP_PLANE0 + i); }
void rect(double x, double y, double width, double height) {
resetPath();
moveTo(x, y);
lineTo(x + width, y);
lineTo(x + width, y + height);
lineTo(x, y + height);
closePathSimple();
}
void line(double x1, double y1, double x2, double y2) {
resetPath();
moveTo(x1, y1);
lineTo(x2, y2);
strokePath();
}
void ellipse(double x, double y, double width, double height) {
resetPath();
arcPath(x, y, width, height, 0, 360);
closePathSimple();
}
void point(double x, double y) {
pushDrawingState();
HeliDrawingState *s = heliDrawingGetState();
memcpy(s->colorFill, s->colorStroke, sizeof(s->colorFill));
s->colorStroke[3] = 0;
ellipse(x - s->lineWidth*0.5, y - s->lineWidth*0.5, s->lineWidth, s->lineWidth);
popDrawingState();
}
void arcPath(double x, double y, double w, double h, double start, double stop) {
w *= 0.5; x += w;
h *= 0.5; y += h;
if (fabs(w) < HELI_PRECISION || fabs(h) < HELI_PRECISION) return;
double step = fabs(w) > fabs(h) ? 1/fabs(w) : 1/fabs(h);
if (step > PI/180) step = PI/180;
start *= PI/180;
stop *= PI/180;
double a = start;
if (start < stop) {
while(1) {
if (a > stop) a = stop;
lineTo(x + cos(a)*w, y + sin(a)*h);
if (a == stop) break;
a += step;
}
} else {
while(1) {
if (a < stop) a = stop;
lineTo(x + cos(a)*w, y + sin(a)*h);
if (a == stop) break;
a -= step;
}
}
}
void arc(double x, double y, double w, double h, double start, double stop) {
resetPath();
arcPath(x, y, w, h, start, stop);
strokePath();
}
void regularPolygon(double x, double y, int sides, double size) {
resetPath();
size *= 0.5;
moveTo(x + size, y);
for(int i = 1; i < sides; ++i) {
double a = i*2*PI/sides;
lineTo(x + size*cos(a), y + size*sin(a));
}
closePathSimple();
}
void resetPath()
{ pathSize = 0; }
static void drawFillSimple() {
HeliDrawingState *s = heliDrawingGetState();
if (s->colorFill[3] <= HELI_PRECISION) return;
if (pathSize < 6) return;
glColor4dv(s->colorFill);
glEnable(GL_MULTISAMPLE);
glBegin(GL_TRIANGLE_FAN);
for(int i = 0; i < pathSize; i += 2)
glVertex2dv(&path[i]);
glEnd();
glDisable(GL_MULTISAMPLE);
glColor4d(1, 1, 1, 1);
}
static void drawFill() {
HeliDrawingState *s = heliDrawingGetState();
if (s->colorFill[3] <= HELI_PRECISION) return;
if (pathSize < 6) return;
double l = path[0], r = l;
double t = path[1], b = t;
for(int i = 2; i < pathSize; i += 2) {
if (l > path[i]) l = path[i];
if (r < path[i]) r = path[i];
if (t > path[i + 1]) t = path[i + 1];
if (b < path[i + 1]) b = path[i + 1];
}
if ( r - l <= HELI_PRECISION
|| b - t <= HELI_PRECISION) return;
l -= 1; r += 1;
t -= 1; b += 1;
glEnable(GL_MULTISAMPLE);
glColor4dv(s->colorFill);
glEnable(GL_STENCIL_TEST);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO);
glBegin(GL_QUADS);
glVertex2d(l, t);
glVertex2d(r, t);
glVertex2d(r, b);
glVertex2d(l, b);
glEnd();
if (heliGlStencilOpSeparatePtr) {
heliGlStencilOpSeparatePtr(GL_FRONT, GL_INCR_WRAP, GL_INCR_WRAP, GL_INCR_WRAP);
heliGlStencilOpSeparatePtr(GL_BACK, GL_DECR_WRAP, GL_DECR_WRAP, GL_DECR_WRAP);
glBegin(GL_TRIANGLE_FAN);
for(int i = 0; i < pathSize; i += 2)
glVertex2dv(&path[i]);
glEnd();
} else {
glEnable(GL_CULL_FACE);
glStencilOp(GL_DECR_WRAP, GL_DECR_WRAP, GL_DECR_WRAP);
glCullFace(GL_FRONT);
glBegin(GL_TRIANGLE_FAN);
for(int i = 0; i < pathSize; i += 2)
glVertex2dv(&path[i]);
glEnd();
glStencilOp(GL_INCR_WRAP, GL_INCR_WRAP, GL_INCR_WRAP);
glCullFace(GL_BACK);
glBegin(GL_TRIANGLE_FAN);
for(int i = 0; i < pathSize; i += 2)
glVertex2dv(&path[i]);
glEnd();
glDisable(GL_CULL_FACE);
}
glStencilFunc(GL_NOTEQUAL, 0, -1u);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glBegin(GL_QUADS);
glVertex2d(l, t);
glVertex2d(r, t);
glVertex2d(r, b);
glVertex2d(l, b);
glEnd();
glStencilFunc(GL_ALWAYS, 0, -1u);
glDisable(GL_STENCIL_TEST);
glDisable(GL_MULTISAMPLE);
glColor4d(1, 1, 1, 1);
}
static void strokeAddPoint(HeliStroke *stroke, double x, double y, double dx, double dy) {
if (stroke->allocatedCount < stroke->count + 1) {
stroke->allocatedCount += stroke->allocatedCount/4 + 32;
stroke->points = realloc(stroke->points, stroke->allocatedCount*4*sizeof(*stroke->points));
memset(stroke->points + stroke->count, 0, (stroke->allocatedCount - stroke->count)*sizeof(*stroke->points));
}
HeliStrokePoint *p = &stroke->points[ stroke->count++ ];
p->x = x;
p->y = y;
p->dx = dx;
p->dy = dy;
}
static void strokeAddCornerSub(HeliStroke *stroke, double dx, double dy, double precisionSqr, int level) {
assert(stroke->count > 0);
HeliStrokePoint *prev = stroke->points + stroke->count - 1;
if (level > 0) {
double distx = dx - prev->dx;
double disty = dy - prev->dy;
if (distx*distx + disty*disty > precisionSqr) {
double mx = prev->dx + dx;
double my = prev->dy + dy;
double lsqr = mx*mx + my*my;
if (lsqr > HELI_PRECISION_SQR) {
double kl = 1/sqrt(lsqr);
mx *= kl;
my *= kl;
strokeAddCornerSub(stroke, mx, my, precisionSqr, level-1);
strokeAddCornerSub(stroke, dx, dy, precisionSqr, level-1);
return;
}
}
}
strokeAddPoint(stroke, prev->x, prev->y, dx, dy);
}
static void strokeAddCorner(
HeliStroke *stroke,
double xp, double yp,
double x0, double y0,
double x1, double y1,
double precisionSqr )
{
double dx0 = x0 - xp;
double dy0 = y0 - yp;
double dx1 = x1 - x0;
double dy1 = y1 - y0;
double l0 = sqrt(dx0*dx0 + dy0*dy0);
double l1 = sqrt(dx1*dx1 + dy1*dy1);
assert(l0 > HELI_PRECISION);
assert(l1 > HELI_PRECISION);
double kl0 = 1/l0, kl1 = 1/l1;
double nx0 = dx0*kl0, ny0 = dy0*kl0;
double nx1 = dx1*kl1, ny1 = dy1*kl1;
double dot = nx0*ny1 - ny0*nx1;
if (dot > HELI_PRECISION) {
// round
strokeAddPoint(stroke, x0, y0, ny0, -nx0);
strokeAddCornerSub(stroke, ny1, -nx1, precisionSqr, 8);
} else
if (dot < HELI_PRECISION) {
// corner
double dx = nx1 - nx0;
double dy = ny1 - ny0;
double kl = 1/sqrt(dx*dx + dy*dy);
double nx = dx*kl, ny = dy*kl;
double c = nx*nx1 + ny*ny1;
double cl = 1/sqrt(1 - c*c);
if (cl > 3) cl = 3;
strokeAddPoint(stroke, x0, y0, nx*cl, ny*cl);
} else {
// flat
strokeAddPoint(stroke, x0, y0, ny0, -nx0);
}
}
static void strokeDraw(HeliStroke *stroke, double w, double *color) {
const double aaBorder = heliGLGetAAResolution();
if (aaBorder > HELI_PRECISION) {
double c0[4] = { color[0], color[1], color[2], 0 };
double c1[4] = { color[0], color[1], color[2], color[3] };
double w0 = w + 0.5*aaBorder;
double w1 = w - 0.5*aaBorder;
if (w1 < HELI_PRECISION) {
c1[3] *= w0/aaBorder;
w1 = 0;
}
glBegin(GL_TRIANGLE_STRIP);
for(int i = 0; i < stroke->count; ++i) {
HeliStrokePoint *p = stroke->points + i;
glColor4dv(c0);
glVertex2d(p->x + p->dx*w0, p->y + p->dy*w0);
glColor4dv(c1);
glVertex2d(p->x + p->dx*w1, p->y + p->dy*w1);
}
glEnd();
if (w1 > 0) {
glColor4dv(c1);
glBegin(GL_TRIANGLE_STRIP);
for(int i = 0; i < stroke->count; ++i) {
HeliStrokePoint *p = stroke->points + i;
glVertex2d(p->x, p->y);
glVertex2d(p->x + p->dx*w1, p->y + p->dy*w1);
}
glEnd();
}
glColor4d(1, 1, 1, 1);
} else {
glColor4dv(color);
glEnable(GL_MULTISAMPLE);
glBegin(GL_TRIANGLE_STRIP);
for(int i = 0; i < stroke->count; ++i) {
HeliStrokePoint *p = stroke->points + i;
glVertex2d(p->x, p->y);
glVertex2d(p->x + p->dx*w, p->y + p->dy*w);
}
glEnd();
glDisable(GL_MULTISAMPLE);
glColor4d(1, 1, 1, 1);
}
}
static void drawStroke(int close) {
HeliDrawingState *s = heliDrawingGetState();
if (s->colorStroke[3] <= HELI_PRECISION) return;
if (s->lineWidth < HELI_PRECISION) return;
if (pathSize < 4) return;
HeliStroke stroke = {};
stroke.allocatedCount = pathSize;
stroke.points = calloc(stroke.allocatedCount, sizeof(*stroke.points));
double w = 0.5*s->lineWidth;
double precisionSqr = 1/(w*0.75);
precisionSqr *= precisionSqr;
if (close) {
for(int i = 0; i < pathSize; i += 2) {
int p = i > 0 ? i - 2 : pathSize - 2;
int n = i < pathSize - 2 ? i + 2 : 0;
strokeAddCorner(
&stroke,
path[p], path[p+1],
path[i], path[i+1],
path[n], path[n+1],
precisionSqr );
}
strokeAddPoint(
&stroke,
stroke.points[0].x,
stroke.points[0].y,
stroke.points[0].dx,
stroke.points[0].dy );
strokeDraw(&stroke, w, s->colorStroke);
stroke.count = 0;
for(int i = pathSize - 2; i >= 0; i -= 2) {
int p = i < pathSize - 2 ? i + 2 : 0;
int n = i > 0 ? i - 2 : pathSize - 2;
strokeAddCorner(
&stroke,
path[p], path[p+1],
path[i], path[i+1],
path[n], path[n+1],
precisionSqr );
}
strokeAddPoint(
&stroke,
stroke.points[0].x,
stroke.points[0].y,
stroke.points[0].dx,
stroke.points[0].dy );
strokeDraw(&stroke, w, s->colorStroke);
} else {
double px = 0, py = 0, nx = 0, ny = 0;
for(int i = 0; i < pathSize; i += 2) {
double x = path[i], y = path[i+1];
if (i > 0) { px = path[i-2]; py = path[i-1]; }
if (i < pathSize - 2) { nx = path[i+2]; ny = path[i+3]; }
if (i == 0) { px = x + y - ny; py = y + nx - x; }
if (i == pathSize - 2) { nx = x + py - y; ny = y + x - px; }
strokeAddCorner(&stroke, px, py, x, y, nx, ny, precisionSqr);
}
--stroke.count;
for(int i = pathSize - 2; i >= 0; i -= 2) {
double x = path[i], y = path[i+1];
if (i > 0) { nx = path[i-2]; ny = path[i-1]; }
if (i < pathSize - 2) { px = path[i+2]; py = path[i+3]; }
if (i == 0) { nx = x + py - y; ny = y + x - px; }
if (i == pathSize - 2) { px = x + y - ny; py = y + nx - x; }
strokeAddCorner(&stroke, px, py, x, y, nx, ny, precisionSqr);
}
memcpy(stroke.points + stroke.count - 1, stroke.points, sizeof(*stroke.points));
strokeDraw(&stroke, w, s->colorStroke);
}
free(stroke.points);
}
static void pushPathPoint(double x, double y) {
if (pathAllocated < pathSize + 2) {
pathAllocated += pathAllocated/4 + 32;
path = realloc(path, pathAllocated*sizeof(*path));
memset(&path[pathSize], 0, (pathAllocated - pathSize)*sizeof(*path));
}
path[pathSize++] = x;
path[pathSize++] = y;
}
static void endPath(int close, int stroke, int fill, int fillSimple) {
// remove duplicates
int removed = 0;
for(int i = 2; i < pathSize; i += 2) {
if ( fabs(path[i-removed-2] - path[i]) <= 2*HELI_PRECISION
&& fabs(path[i-removed-1] - path[i+1]) <= 2*HELI_PRECISION )
{
removed += 2;
} else
if (removed > 0) {
path[i-removed] = path[i];
path[i-removed+1] = path[i+1];
}
}
// remove tails duplicates
pathSize -= removed;
if ( pathSize >= 4
&& close
&& fabs(path[pathSize-2] - path[0]) <= 2*HELI_PRECISION
&& fabs(path[pathSize-1] - path[1]) <= 2*HELI_PRECISION )
pathSize -= 2;
// draw
if (pathSize >= 4) {
if (fillSimple) drawFillSimple();
else if (fill) drawFill();
if (stroke) drawStroke(close);
}
resetPath();
}
void closePath()
{ endPath(TRUE, TRUE, TRUE, FALSE); }
void strokePath()
{ endPath(FALSE, TRUE, FALSE, FALSE); }
void lineTo(double x, double y)
{ pushPathPoint(x, y); }
void moveTo(double x, double y)
{ resetPath(); lineTo(x, y); }
HeliDrawingState *heliDrawingGetState()
{ return statesStack + statesStackIndex; }
HeliDrawingState *heliDrawingGetStateStack()
{ return statesStack; }
void pushDrawingState() {
if (statesStackIndex >= sizeof(statesStack)/sizeof(*statesStack) - 1) {
fprintf(stderr, "helianthus: drawing stack overflow\n");
return;
}
++statesStackIndex;
memcpy(statesStack + statesStackIndex, statesStack + statesStackIndex - 1, sizeof(*statesStack));
heliGLGetState(&statesStack[statesStackIndex].glState);
}
void popDrawingState() {
assert(statesStackIndex > 0);
if (statesStackIndex <= 0) {
fprintf(stderr, "helianthus: drawing stack underflow\n");
return;
}
heliGLSetState(&statesStack[statesStackIndex].glState);
--statesStackIndex;
}
void heliDrawingClearFrame() {
glClearColor(colorBack[0], colorBack[1], colorBack[2], colorBack[3]);
glClear(GL_COLOR_BUFFER_BIT);
}
void heliDrawingPrepareFrame() {
resetPath();
heliDrawingClearFrame();
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_MULTISAMPLE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_LINE_SMOOTH);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
}
void heliDrawingFinish() {
resetPath();
free(path);
path = NULL;
pathAllocated = 0;
}