#include "private.h"
#include "sprite.h"
#include "world.h"
#include "group.h"
#include "drawing.h"
struct _Sprite {
double x;
double y;
double rotation;
double width;
double height;
double scale;
double vx;
double vy;
double rotationSpeed;
HeliCollider collider;
double bounciness;
int rotateToDirection;
int mirrorX;
int mirrorY;
double depth;
double lifeTime;
int visible;
int tag;
int debug;
double shapeColor[4];
double tintColor[4];
int playing;
int frame;
HeliArray groups;
HeliAnimation *animation;
};
static HeliArray sprites;
static HeliArray spritesSorted;
static void prepareCollider(Sprite sprite, HeliCollider *collider) {
collider->type = sprite->collider.type;
collider->x = sprite->collider.x + sprite->x;
collider->y = sprite->collider.y + sprite->y;
collider->width = sprite->collider.width;
collider->height = sprite->collider.height;
collider->radius = sprite->collider.radius;
collider->rotation = sprite->collider.rotation + sprite->rotation;
// auto-size
if (collider->type == COLLIDER_RECTANGLE) {
if (collider->width < -HELI_PRECISION) collider->width = sprite->width;
if (collider->height < -HELI_PRECISION) collider->height = sprite->height;
if (collider->radius < -HELI_PRECISION) collider->radius = 0;
} else
if (collider->type == COLLIDER_CIRCLE) {
collider->width = 0;
collider->height = 0;
if (collider->radius < -HELI_PRECISION) collider->radius = 0.5*sprite->width;
}
// scale
collider->width *= sprite->scale;
collider->height *= sprite->scale;
collider->radius *= sprite->scale;
// fix small values
if (collider->width < HELI_PRECISION) collider->width = HELI_PRECISION;
if (collider->height < HELI_PRECISION) collider->height = HELI_PRECISION;
if (collider->radius < HELI_PRECISION) collider->radius = HELI_PRECISION;
// fix round corners
if (collider->type == COLLIDER_RECTANGLE) {
double rmax = 0.5*( collider->width < collider->height
? collider->width : collider->height );
if (collider->radius >= rmax - HELI_PRECISION)
collider->radius = rmax;
collider->width -= 2*collider->radius;
collider->height -= 2*collider->radius;
}
}
Sprite createSpriteEx(double x, double y, double width, double height) {
if (!heliInitialized) return NULL;
Sprite s = calloc(1, sizeof(*s));
s->x = x;
s->y = y;
s->width = width;
s->height = height;
s->scale = 1;
s->collider.type = COLLIDER_RECTANGLE;
s->collider.width = s->collider.height = -1;
s->bounciness = 1;
s->mirrorX = 1;
s->mirrorY = 1;
s->lifeTime = -1;
s->visible = TRUE;
s->shapeColor[0] = s->shapeColor[1] = s->shapeColor[2] = 0.5;
s->shapeColor[3] = 1;
s->tintColor[0] = s->tintColor[1] = s->tintColor[2] = s->tintColor[3] = 1;
heliArrayInsert(&sprites, -1, s, NULL);
heliArrayInsert(&spritesSorted, -1, s, NULL);
heliObjectRegister(s, (HeliFreeCallback)&spriteDestroy);
return s;
}
Sprite createSprite(double x, double y)
{ return createSpriteEx(x, y, 100, 100); }
void spriteDestroy(Sprite sprite) {
spriteSetNoAnimation(sprite);
while(sprite->groups.count > 0)
groupRemove((Group)sprite->groups.items[0].value, sprite);
for(int i = 0; i < sprites.count; ++i)
if (sprites.items[i].value == sprite)
{ heliArrayRemove(&sprites, i); break; }
for(int i = 0; i < spritesSorted.count; ++i)
if (spritesSorted.items[i].value == sprite)
{ heliArrayRemove(&spritesSorted, i); break; }
heliObjectUnregister(sprite);
free(sprite);
}
void spriteDestroyTimer(Sprite sprite, double lifeTime) {
if (lifeTime <= 0) { spriteDestroy(sprite); return; }
sprite->lifeTime = lifeTime;
}
double spriteGetX(Sprite sprite) { return sprite->x; }
void spriteSetX(Sprite sprite, double x) { sprite->x = x; }
double spriteGetY(Sprite sprite) { return sprite->y; }
void spriteSetY(Sprite sprite, double y) { sprite->y = y; }
double spriteGetVelocityX(Sprite sprite) { return sprite->vx; }
void spriteSetVelocityX(Sprite sprite, double x)
{ spriteSetVelocityXY(sprite, x, sprite->vy); }
double spriteGetVelocityY(Sprite sprite) { return sprite->vy; }
void spriteSetVelocityY(Sprite sprite, double y)
{ spriteSetVelocityXY(sprite, sprite->vx, y); }
double spriteGetScale(Sprite sprite) { return sprite->scale; }
void spriteSetScale(Sprite sprite, double scale) { sprite->scale = scale; }
double spriteGetWidth(Sprite sprite) { return sprite->width; }
void spriteSetWidth(Sprite sprite, double width)
{ sprite->width = width > 0 ? width : 0; }
double spriteGetHeight(Sprite sprite) { return sprite->height; }
void spriteSetHeight(Sprite sprite, double height)
{ sprite->height = height > 0 ? height : 0; }
int spriteGetRotateToDirection(Sprite sprite) { return sprite->rotateToDirection; }
void spriteSetRotateToDirection(Sprite sprite, int rotateToDirection) {
if (rotateToDirection) {
sprite->rotateToDirection = TRUE;
sprite->rotation = spriteGetDirection(sprite);
} else {
sprite->rotateToDirection = FALSE;
}
}
double spriteGetRotation(Sprite sprite) { return sprite->rotation; }
void spriteSetRotation(Sprite sprite, double rotation)
{ if (!sprite->rotateToDirection) sprite->rotation = rotation; }
double spriteGetRotationSpeed(Sprite sprite) { return sprite->rotationSpeed; }
void spriteSetRotationSpeed(Sprite sprite, double rotationSpeed)
{ sprite->rotationSpeed = rotationSpeed; }
int spriteGetMirrorX(Sprite sprite) { return sprite->mirrorX; }
void spriteSetMirrorX(Sprite sprite, int mirrorX)
{ sprite->mirrorX = mirrorX < 0 ? -1 : 1; }
int spriteGetMirrorY(Sprite sprite) { return sprite->mirrorY; }
void spriteSetMirrorY(Sprite sprite, int mirrorY)
{ sprite->mirrorY = mirrorY < 0 ? -1 : 1; }
double spriteGetDepth(Sprite sprite) { return sprite->depth; }
void spriteSetDepth(Sprite sprite, double depth)
{ sprite->depth = depth; }
int spriteGetVisible(Sprite sprite) { return sprite->visible; }
void spriteSetVisible(Sprite sprite, int visible)
{ sprite->visible = visible != FALSE; }
int spriteGetTag(Sprite sprite) { return sprite->tag; }
void spriteSetTag(Sprite sprite, int tag)
{ sprite->tag = tag; }
int spriteGetDebug(Sprite sprite) { return sprite->debug; }
void spriteSetDebug(Sprite sprite, int debug)
{ sprite->debug = debug != FALSE; }
int spriteOverlap(Sprite a, Sprite b)
{ return spriteCollideEx(a, b, TRUE, TRUE, 0); }
int spriteCollide(Sprite a, Sprite b, double bounciness)
{ return spriteCollideEx(a, b, FALSE, FALSE, bounciness); }
int spriteBounceOff(Sprite sprite, Sprite other, double bounciness)
{ return spriteCollideEx(sprite, other, FALSE, TRUE, bounciness); }
int spritePush(Sprite sprite, Sprite other, double bounciness)
{ return spriteCollideEx(sprite, other, TRUE, FALSE, bounciness); }
int spriteCollideEx(Sprite a, Sprite b, int keepVelocityA, int keepVelocityB, double bounciness) {
if (a == b) return FALSE;
HeliCollider ca, cb;
prepareCollider(a, &ca);
prepareCollider(b, &cb);
double dx = 0, dy = 0;
double vx = a->vx - b->vx;
double vy = a->vy - b->vy;
bounciness = fabs(bounciness * a->bounciness * b->bounciness);
if (!heliCheckCollision(&ca, &cb, &dx, &dy, &vx, &vy, bounciness))
return FALSE;
if ( keepVelocityA && !keepVelocityB) {
b->x -= dx;
b->y -= dy;
spriteSetVelocityXY(b, a->vx - vx, a->vy - vy);
} else
if (!keepVelocityA && keepVelocityB) {
a->x += dx;
a->y += dy;
spriteSetVelocityXY(a, b->vx + vx, b->vy + vy);
} else
if (!keepVelocityA && !keepVelocityB) {
double vxAvg = 0.5*(a->vx + b->vx);
double vyAvg = 0.5*(a->vy + b->vy);
a->x += 0.5*dx;
a->y += 0.5*dy;
spriteSetVelocityXY(a, vxAvg + 0.5*vx, vyAvg + 0.5*vy);
b->x -= 0.5*dx;
b->y -= 0.5*dy;
spriteSetVelocityXY(b, vxAvg - 0.5*vx, vyAvg - 0.5*vy);
}
return TRUE;
}
double spriteGetBounciness(Sprite sprite) { return sprite->bounciness; }
void spriteSetBounciness(Sprite sprite, double bounciness)
{ sprite->bounciness = bounciness > 0 ? bounciness : 0; }
void spriteSetCollider(Sprite sprite, Collider type, double xOffset, double yOffset, double rotationOffset)
{ spriteSetColliderEx(sprite, type, xOffset, yOffset, rotationOffset, -1, -1, -1); }
void spriteSetColliderCircle(Sprite sprite, double xOffset, double yOffset, double radius)
{ spriteSetColliderEx(sprite, COLLIDER_CIRCLE, xOffset, yOffset, 0, 0, 0, radius); }
void spriteSetColliderRectangle(
Sprite sprite, double xOffset, double yOffset, double rotationOffset,
double width, double height, double cornersRadius )
{
spriteSetColliderEx(
sprite, COLLIDER_RECTANGLE,
xOffset, yOffset, rotationOffset,
width, height, cornersRadius);
}
void spriteSetColliderEx(
Sprite sprite, Collider type,
double xOffset, double yOffset, double rotationOffset,
double width, double height, double radius )
{
sprite->collider.type = type;
sprite->collider.x = xOffset;
sprite->collider.y = yOffset;
sprite->collider.rotation = rotationOffset;
if (type == COLLIDER_CIRCLE) {
sprite->collider.width = 0;
sprite->collider.height = 0;
sprite->collider.radius = radius;
} else
if (type == COLLIDER_RECTANGLE) {
sprite->collider.width = width;
sprite->collider.height = height;
sprite->collider.radius = radius;
} else {
sprite->collider.width = 0;
sprite->collider.height = 0;
sprite->collider.radius = 0;
}
}
void spriteSetAnimation(Sprite sprite, const char *path) {
HeliAnimation *prev = sprite->animation;
sprite->animation = heliAnimationLoad(path);
if (prev) heliAnimationUnref(prev);
spriteSetFrame(sprite, sprite->frame);
}
void spriteSetNoAnimation(Sprite sprite) {
if (sprite->animation) heliAnimationUnref(sprite->animation);
sprite->animation = NULL;
}
void spritePlay(Sprite sprite) { sprite->playing = TRUE; }
void spritePause(Sprite sprite) { sprite->playing = FALSE; }
void spriteNextFrame(Sprite sprite) { spriteSetFrame(sprite, sprite->frame + 1); }
void spriteSetFrame(Sprite sprite, int frame) {
sprite->frame = frame >= 0 && sprite->animation && sprite->animation->frames.count > 0
? frame % sprite->animation->frames.count : 0;
}
void spriteSetShapeColor(Sprite sprite, const char *color)
{ heliParseColor(color, sprite->shapeColor); }
void spriteSetTintColor(Sprite sprite, const char *color)
{ heliParseColor(color, sprite->tintColor); }
void spriteSetVelocityXY(Sprite sprite, double x, double y) {
sprite->vx = x;
sprite->vy = y;
if (sprite->rotateToDirection)
sprite->rotation = spriteGetDirection(sprite);
}
void spriteSetSpeedAndDirection(Sprite sprite, double speed, double angle) {
double a = angle*(PI/180);
spriteSetVelocityXY(sprite, cos(a)*speed, sin(a)*speed);
}
double spriteGetSpeed(Sprite sprite)
{ return sqrt(sprite->vx*sprite->vx + sprite->vy*sprite->vy); }
double spriteGetDirection(Sprite sprite) {
return fabs(sprite->vx) > HELI_PRECISION || fabs(sprite->vy) > HELI_PRECISION
? atan2(sprite->vy, sprite->vx)*(180/PI) : 0;
}
void spritePointTo(Sprite sprite, double x, double y)
{ spriteSetRotation( sprite, atan2(y - sprite->y, x - sprite->x)*(180/PI) ); }
double spriteGetScaledWidth(Sprite sprite)
{ return sprite->width * sprite->scale; }
double spriteGetScaledHeight(Sprite sprite)
{ return sprite->height * sprite->scale; }
int worldGetSpriteCount()
{ return sprites.count; }
Sprite worldGetSprite(int i)
{ return (Sprite)heliArrayGetValue(&sprites, i); }
static void drawSpriteDebug(Sprite s) {
pushDrawingState();
strokeWeight(0.5);
double a = s->rotation*(PI/180);
double sn = sin(a);
double cs = cos(a);
double hw = 0.5 * s->scale * s->mirrorX * s->width;
double hh = 0.5 * s->scale * s->mirrorX * s->height;
double ltx = -hw*cs + hh*sn;
double lty = -hw*sn - hh*cs;
double rtx = hw*cs + hh*sn;
double rty = hw*sn - hh*cs;
fill("transparent");
stroke(rgba(0, 0, 0, 0.75));
// frame
moveTo(s->x + ltx, s->y + lty);
lineTo(s->x + rtx, s->y + rty);
lineTo(s->x - ltx, s->y - lty);
lineTo(s->x - rtx, s->y - rty);
closePath();
// center cross
line( s->x - cs*hw*0.25, s->y - sn*hw*0.25,
s->x + cs*hw*0.25, s->y + sn*hw*0.25 );
line( s->x + sn*hh*0.25, s->y - cs*hh*0.25,
s->x - sn*hh*0.25, s->y + cs*hh*0.25 );
// depth
stroke(rgba(0, 0, 0, 0.75));
char buf[1024];
snprintf(buf, sizeof(buf)-1, "%lf", s->depth);
double s1 = hw*0.25;
double s2 = hh*0.5;
textSize(s1 < s2 ? s1 : s2);
textAlign(HALIGN_CENTER, VALIGN_CENTER);
text(buf, s->x, s->y);
popDrawingState();
}
static void drawSprite(Sprite s, double aaBorder) {
unsigned int texid = s->animation
? (unsigned int)(size_t)heliArrayGetValue(&s->animation->frames, s->frame) : 0u;
double color[4] = {
(texid ? 1.0 : s->shapeColor[0])*s->tintColor[0],
(texid ? 1.0 : s->shapeColor[1])*s->tintColor[1],
(texid ? 1.0 : s->shapeColor[2])*s->tintColor[2],
(texid ? 1.0 : s->shapeColor[3])*s->tintColor[3] };
if (color[3] < HELI_PRECISION) return;
double w = 0.5*s->scale*s->width;
double h = 0.5*s->scale*s->height;
if (w < HELI_PRECISION || h < HELI_PRECISION) return;
glPushMatrix();
glTranslated(s->x, s->y, 0);
glRotated(s->rotation, 0, 0, 1);
if (aaBorder <= HELI_PRECISION) {
// use OpenGL multisample antialiasing
glEnable(GL_MULTISAMPLE);
const int vcnt = 4;
const double vertices[][2] = {
{ -w, -h },
{ w, -h },
{ -w, h },
{ w, h } };
glColor4dv(color);
if (texid) {
const double texCoords[][2] = {
{0, 0},
{1, 0},
{0, 1},
{1, 1} };
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texid);
glBegin(GL_TRIANGLE_STRIP);
for(int i = 0; i < vcnt; ++i) {
glTexCoord2dv(texCoords[i]);
glVertex2dv(vertices[i]);
}
glEnd();
glBindTexture(GL_TEXTURE_2D, 0);
glDisable(GL_TEXTURE_2D);
} else {
glBegin(GL_TRIANGLE_STRIP);
for(int i = 0; i < vcnt; ++i)
glVertex2dv(vertices[i]);
glEnd();
}
glDisable(GL_MULTISAMPLE);
} else {
// make antialiased borde manually
double sideColor[4] = { color[0], color[1], color[2], 0 };
double w0 = w - 0.5*aaBorder;
double w1 = w + 0.5*aaBorder;
if (w0 < HELI_PRECISION) w0 = 0;
double h0 = h - 0.5*aaBorder;
double h1 = h + 0.5*aaBorder;
if (h0 < HELI_PRECISION) h0 = 0;
const double k = (w1 < aaBorder ? w1/aaBorder : 1)
* (h1 < aaBorder ? h1/aaBorder : 1);
color[3] *= k;
w0 *= s->mirrorX;
w1 *= s->mirrorX;
h0 *= s->mirrorY;
h1 *= s->mirrorY;
const int vcnt = 14;
const double vertices[][2] = {
{ -w1, -h1 }, { -w0, -h0 },
{ w1, -h1 }, { w0, -h0 },
{ w1, h1 }, { w0, h0 },
{ -w1, h1 }, { -w0, h0 },
{ -w1, -h1 }, { -w0, -h0 },
{ -w0, -h0 },
{ w0, -h0 },
{ -w0, h0 },
{ w0, h0 } };
const double *colors[] = {
sideColor, color,
sideColor, color,
sideColor, color,
sideColor, color,
sideColor, color,
color,
color,
color,
color };
if (texid) {
const double tw0 = fabs(w0/w)*0.5 - 0.5;
const double tw1 = fabs(w1/w)*0.5 - 0.5;
const double th0 = fabs(h0/h)*0.5 - 0.5;
const double th1 = fabs(h1/h)*0.5 - 0.5;
const double texCoords[][2] = {
{ -tw1, -th1 }, { -tw0, -th0 },
{ 1 + tw1, -th1 }, { 1 + tw0, -th0 },
{ 1 + tw1, 1 + th1 }, { 1 + tw0, 1 + th0 },
{ -tw1, 1 + th1 }, { -tw0, 1 + th0 },
{ -tw1, -th1 }, { -tw0, -th0 },
{ -tw0, -th0 },
{ 1 + tw0, -th0 },
{ -tw0, 1 + th0 },
{ 1 + tw0, 1 + th0 } };
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texid);
glBegin(GL_TRIANGLE_STRIP);
for(int i = 0; i < vcnt; ++i) {
glColor4dv(colors[i]);
glTexCoord2dv(texCoords[i]);
glVertex2dv(vertices[i]);
}
glEnd();
glBindTexture(GL_TEXTURE_2D, 0);
glDisable(GL_TEXTURE_2D);
} else {
glBegin(GL_TRIANGLE_STRIP);
for(int i = 0; i < vcnt; ++i) {
glColor4dv(colors[i]);
glVertex2dv(vertices[i]);
}
glEnd();
}
}
glColor4d(1, 1, 1, 1);
glPopMatrix();
}
void drawSprites() {
// sort
int found = TRUE;
while(found) {
found = FALSE;
// forward
for(int i = 1; i < spritesSorted.count; ++i) {
if ( ((Sprite)spritesSorted.items[i-1].value)->depth
< ((Sprite)spritesSorted.items[i ].value)->depth )
{
void *x = spritesSorted.items[i].value;
spritesSorted.items[i].value = spritesSorted.items[i-1].value;
spritesSorted.items[i-1].value = x;
found = TRUE;
}
}
if (!found) break;
// backward
found = FALSE;
for(int i = spritesSorted.count - 1; i > 0; --i) {
if ( ((Sprite)spritesSorted.items[i-1].value)->depth
< ((Sprite)spritesSorted.items[i ].value)->depth )
{
void *x = spritesSorted.items[i].value;
spritesSorted.items[i].value = spritesSorted.items[i-1].value;
spritesSorted.items[i-1].value = x;
found = TRUE;
}
}
}
// draw
const double aaBorder = heliGLGetAAResolution();
for(int i = 0; i < spritesSorted.count; ++i) {
Sprite s = (Sprite)(spritesSorted.items[i].value);
if (s->visible) drawSprite(s, aaBorder);
}
// draw debug marks
for(int i = 0; i < spritesSorted.count; ++i) {
Sprite s = (Sprite)(spritesSorted.items[i].value);
if (s->debug) drawSpriteDebug(s);
}
}
int mouseIsOver(Sprite sprite) {
HeliCollider collider;
prepareCollider(sprite, &collider);
return heliPointCollision(&collider, mouseX(), mouseY());
}
void heliSpriteUpdate(double dt) {
// auto-remove
for(int i = sprites.count - 1; i > 0; --i) {
Sprite s = (Sprite)sprites.items[i].value;
if (s->lifeTime >= -HELI_PRECISION) {
s->lifeTime -= dt;
if (s->lifeTime <= HELI_PRECISION)
spriteDestroy(s);
}
}
// update
for(int i = 0; i < sprites.count; ++i) {
Sprite s = (Sprite)sprites.items[i].value;
s->x += s->vx*dt;
s->y += s->vy*dt;
if (!s->rotateToDirection)
s->rotation += s->rotationSpeed*dt;
if (s->playing) spriteNextFrame(s);
}
}
HeliArray* heliSpriteGetGroups(Sprite sprite)
{ return &sprite->groups; }
void heliSpriteFinish() {
while(worldGetSpriteCount())
{ spriteDestroy(worldGetSprite(0)); }
heliArrayDestroy(&sprites);
heliArrayDestroy(&spritesSorted);
}