diff --git a/projects/jigsaw/build.sh b/projects/jigsaw/build.sh new file mode 100755 index 0000000..910a159 --- /dev/null +++ b/projects/jigsaw/build.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +if [ -z "$1" ] || [ "$1" == "release" ]; then + echo "build release" + FLAGS="-O3 -DNDEBUG" +elif [ "$1" == "debug" ]; then + echo "build debug" + FLAGS="-g -O0" +else + echo "usage: $0 [debug|release]" + exit 1 +fi + +LIBS="$(pkg-config --cflags --libs helianthus) -lm" +FLAGS="-Wall $FLAGS $LIBS" + +cc $FLAGS *.c icons.S $LIBS -lm -o jigsaw + +echo "build success" diff --git a/projects/jigsaw/common.c b/projects/jigsaw/common.c new file mode 100644 index 0000000..181446c --- /dev/null +++ b/projects/jigsaw/common.c @@ -0,0 +1,76 @@ + +#include "common.h" + + + +void* alloc(size_t size) { + void *p = calloc(1, size); + if (!p) DIE("cannot allocate %llu bytes of memory", (unsigned long long)size); + return p; +} + + +void* alloc2d(size_t cnt, size_t size) { + if (cnt < 0) return NULL; + size_t ps = cnt*sizeof(void*); + char **p = (char**)alloc(ps + cnt*size); + char *d = ((char*)p) + ps; + if (size) for(size_t i = 0; i < cnt; ++i) p[i] = d + i*size; + return p; +} + + + +char* vstrprintf(const char *fmt, va_list args) { + va_list copy; + va_copy(copy, args); + int size = vsnprintf(NULL, 0, fmt, copy); + va_end(copy); + if (size < 0) size = 0; + ++size; + + char *buf = alloc(size+1); + vsnprintf(buf, size, fmt, args); + return buf; +} + + +char* strprintf(const char *format, ...) { + va_list args; + va_start(args,format); + char* s = vstrprintf(format, args); + va_end(args); + return s; +} + + +char* unquote(char *s, char prefix, char suffix) { + char *s0 = NULL, *s1 = s; + for(char *c = s; *c; ++c) { + if (isspace(*c)) continue; + if (!s0) s0 = c; + s1 = c+1; + } + if (!s0) s0 = s; + if (s0 < s1 && *s0 == prefix) ++s0; + if (s0 < s1 && *(s1-1) == suffix) --s1; + memmove(s, s0, s1 - s0); + s[s1-s0] = 0; + return s; +} + + + +void cubicto(double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, int levels) { + if (--levels <= 0) { lineTo(x3, y3); return; } + double xx0 = (x0 + x1)/2, yy0 = (y0 + y1)/2; + double xx1 = (x1 + x2)/2, yy1 = (y1 + y2)/2; + double xx2 = (x2 + x3)/2, yy2 = (y2 + y3)/2; + double xxx0 = (xx0 + xx1)/2, yyy0 = (yy0 + yy1)/2; + double xxx1 = (xx1 + xx2)/2, yyy1 = (yy1 + yy2)/2; + double xxxx = (xxx0 + xxx1)/2, yyyy = (yyy0 + yyy1)/2; + cubicto(x0, y0, xx0, yy0, xxx0, yyy0, xxxx, yyyy, levels); + cubicto(xxxx, yyyy, xxx1, yyy1, xx2, yy2, x3, y3, levels); +} + + diff --git a/projects/jigsaw/common.h b/projects/jigsaw/common.h new file mode 100644 index 0000000..bfb9ccf --- /dev/null +++ b/projects/jigsaw/common.h @@ -0,0 +1,90 @@ +#ifndef COMMON_H +#define COMMON_H + + +#include +#include +#include +#include +#include +#include + +#include + +#include + + +#include "geometry.h" + + +// types + +typedef unsigned int Flags; + + +// log + +static inline int dummy(int x) { return x; } + +#define PREQUOTE(...) #__VA_ARGS__ +#define QUOTE(...) PREQUOTE(__VA_ARGS__) + +#define LOG(f, r, p0, p1, ...) ( fprintf((f), "%s%s: ", (p0), (p1)), fprintf((f), __VA_ARGS__), fprintf((f), "\n"), fflush(f), dummy(r) ) +#define INF(...) LOG(stdout, 1, "INFO", "", __VA_ARGS__) +#define WRN(...) LOG(stderr, 1, "WARNING", "", __VA_ARGS__) +#define ERR(...) LOG(stderr, 0, "ERROR", "", __VA_ARGS__) + +#define DIE(...) (ERR(__VA_ARGS__), exit(1), dummy(0)) + +#ifdef NDEBUG +#define DBG(...) (dummy(1)) +#else +#define DBG(...) LOG(stdout, 1, "DEBUG: " __FILE__ ":" QUOTE(__LINE__) ": ", __func__, __VA_ARGS__) +#endif +#define TRC DBG("trace") + + +// alloc + +#define alignof __alignof__ +#define ALIGN(s, a) ((((s)-1)/(a)+1)*(a)) +#define ASIZEOF(t) align(sizeof(t), alignof(t)) +#define COUNTOF(a) (sizeof(a)/ASIZEOF(*a)) +static inline size_t align(size_t s, size_t a) { return ALIGN(s, a); } +void* alloc(size_t size); +void* alloc2d(size_t cnt, size_t size); + + +// str + +char* vstrprintf(const char *fmt, va_list args); +char* strprintf(const char *fmt, ...); +char* unquote(char *s, char prefix, char suffix); +static inline char* trim(char *s) { return unquote(s, 0, 0); } + + +// arithm + +static inline double mind(double a, double b) { return a < b ? a : b; } +static inline double maxd(double a, double b) { return a < b ? b : a; } +static inline double lintr(double a, double b, double k) { return a*(1-k) + b*k; } + + +// rand + +static inline int randSign() { return rand()%2*2 - 1; } +static inline double randOne() { return rand()/(double)RAND_MAX; } +static inline double randTwo() { return randOne()*2 - 1; } +static inline Vec vrandOne() { return vec(randOne(), randOne()); } +static inline Vec vrandTwo() { return vec(randTwo(), randTwo()); } + +static inline unsigned int randColor() { return colorByRGB(randOne(), randOne(), randOne()); } +static inline unsigned int randColorPtr(const void *ptr) { int s = rand(); srand((size_t)ptr); unsigned int c = randColor(); srand(s); return c; } + + +// spline + +void cubicto(double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, int levels); + + +#endif diff --git a/projects/jigsaw/data/.gitignore b/projects/jigsaw/data/.gitignore new file mode 100644 index 0000000..ef68233 --- /dev/null +++ b/projects/jigsaw/data/.gitignore @@ -0,0 +1,2 @@ +/images/ +/saves/ diff --git a/projects/jigsaw/data/icons/back.png b/projects/jigsaw/data/icons/back.png new file mode 100644 index 0000000..3684a5a Binary files /dev/null and b/projects/jigsaw/data/icons/back.png differ diff --git a/projects/jigsaw/data/icons/count1.png b/projects/jigsaw/data/icons/count1.png new file mode 100644 index 0000000..0e55fde Binary files /dev/null and b/projects/jigsaw/data/icons/count1.png differ diff --git a/projects/jigsaw/data/icons/count2.png b/projects/jigsaw/data/icons/count2.png new file mode 100644 index 0000000..2c53c4a Binary files /dev/null and b/projects/jigsaw/data/icons/count2.png differ diff --git a/projects/jigsaw/data/icons/count3.png b/projects/jigsaw/data/icons/count3.png new file mode 100644 index 0000000..33582fd Binary files /dev/null and b/projects/jigsaw/data/icons/count3.png differ diff --git a/projects/jigsaw/data/icons/count4.png b/projects/jigsaw/data/icons/count4.png new file mode 100644 index 0000000..c9d843b Binary files /dev/null and b/projects/jigsaw/data/icons/count4.png differ diff --git a/projects/jigsaw/data/icons/noturn.png b/projects/jigsaw/data/icons/noturn.png new file mode 100644 index 0000000..b64f499 Binary files /dev/null and b/projects/jigsaw/data/icons/noturn.png differ diff --git a/projects/jigsaw/data/icons/turn.png b/projects/jigsaw/data/icons/turn.png new file mode 100644 index 0000000..a9f2bc8 Binary files /dev/null and b/projects/jigsaw/data/icons/turn.png differ diff --git a/projects/jigsaw/game.c b/projects/jigsaw/game.c new file mode 100644 index 0000000..65f38c2 --- /dev/null +++ b/projects/jigsaw/game.c @@ -0,0 +1,266 @@ + +#include "game.h" + + + +void gameFree(Game *gm) { + puzzleFree(&gm->pz); + if (gm->pz.image) animationDestroy(gm->pz.image); + free(gm->path); + free(gm->imgname); + memset(gm, 0, sizeof(*gm)); +} + + +int gameCreate(Game *gm, const char *path, const char *imgname, int rows, int cols, int turn, int seed) { + gameFree(gm); + char* imgpath = findImage(imgname); + if (!imgpath) return ERR("gameCreate: cannot find image: %s", imgname); + + gm->pz.image = createAnimationEx(imgpath, TRUE, FALSE, FALSE); + free(imgpath); + + int iw = animationGetOrigWidth(gm->pz.image); + int ih = animationGetOrigHeight(gm->pz.image); + + if (rows < 1 || cols < 1 || rows*cols < 2) { + int count = rows; + if (count < 4) count = 4; + double s = sqrt(iw*ih/(double)count); + cols = (int)round(iw/s); + if (cols < 1) cols = 1; + rows = (int)round(ih/s); + if (rows < 1) rows = 1; + } + + double cw = iw/(double)cols; + double ch = ih/(double)rows; + if (cw > ch) { ch /= cw; cw = 1; } + else { cw /= ch; ch = 1; } + + int px = (int)exp2(round(log2(1024/cols))); + if (px < 8) px = 8; + if (px > 1024) px = 1024; + DBG("iw %d, ih %d, cols %d, rows %d, px %d, cw %g, ch %g", iw, ih, cols, rows, px, cw , ch); + + int prevseed = rand(); + gm->pz.turn = !!turn; + gm->path = strprintf("%s", path); + gm->imgname = strprintf("%s", imgname); + gm->seed = seed; + while(!gm->seed) gm->seed = rand(); + srand(gm->seed); + puzzleAlloc(&gm->pz, rows, cols); + puzzleGenLines(&gm->pz, cw, ch, 0.05, 1/5.0); + puzzleGenChunks(&gm->pz); + puzzleRenderChunks(&gm->pz, px, 0.3); + srand(prevseed); + + gameSave(gm); + return 1; +} + + +int gameSave(Game *gm) { + char *tmp = strprintf("%s.tmp", gm->path); + FILE *f = fopen(tmp, "w"); + if (!f) { + ERR("gameSave: cannot open file for write: %s", tmp); + free(tmp); + return 0; + } + + puzzleRecalcGroups(&gm->pz); + + // puzzle1 + // [[img-file]] + // [rows] [cols] [groups] [seed] + // [gid] [x] [y] [angle]... + fprintf(f, "puzzle1\n[%s]\n%d %d %d %d %d\n", gm->imgname, gm->pz.rows, gm->pz.cols, !!gm->pz.turn, gm->pz.groups, gm->seed); + Chunk *chunks = gm->pz.chunks[0]; + int cnt = gm->pz.rows * gm->pz.cols; + for(int i = 0; i < cnt; ++i) { + Chunk *c = &chunks[i]; + PhGroup *g = c->parent; + Chunk *p = (Chunk*)((char*)g - offsetof(Chunk, group)); + int gid = p - chunks; + if (p == c) fprintf(f, "%d %g %g %g\n", gid, g->o.x, g->o.y, vangled(g->dx)); + else fprintf(f, "%d\n", gid); + } + + fflush(f); + int err = ferror(f); + if (err) ERR("gameSave: cannot write to file: %s", tmp); + fclose(f); + + if (!err) { + err = rename(tmp, gm->path); + if (err) ERR("gameSave: cannot rename file: [%s] -> [%s]", tmp, gm->path); + } + if (err) remove(tmp); + free(tmp); + return !err; +} + + +static FILE* internalLoadGameInfo(GameInfo *gi, const char *path) { + FILE *f = fopen(path, "r"); + if (!f) { + ERR("loadGameInfo: cannot open file for read: %s", path); + return NULL; + } + + size_t ls = 1; + char *l = (char*)alloc(ls); + getline(&l, &ls, f); trim(l); + if (strcmp(l, "puzzle1")) { + ERR("loadGameInfo: wrong file version (expected: puzzle1): %s", l); + free(l); fclose(f); return NULL; + } + + getline(&l, &ls, f); unquote(l, '[', ']'); + fscanf(f, "%d%d%d%d%d", &gi->rows, &gi->cols, &gi->turn, &gi->groups, &gi->seed); + if ( gi->rows < 1 || gi->cols < 1 || gi->rows*gi->cols < 2 + || gi->turn < 0 || gi->turn > 1 + || gi->groups < 1 || gi->groups > gi->rows*gi->cols ) + { + ERR("loadGameInfo: bad data in file: %s, [%dx%d, %d], %d", path, gi->rows, gi->cols, gi->turn, gi->groups); + free(l); fclose(f); return NULL; + } + + if (ferror(f)) { + ERR("loadGameInfo: cannot read from file: %s", path); + free(l); fclose(f); return NULL; + } + + gi->imgname = l; + return f; +} + + +GameInfo* loadGameInfo(const char *path) { + GameInfo gi = { .t = filetime(path) }; + FILE *f = internalLoadGameInfo(&gi, path); + if (!f) return NULL; + fclose(f); + + GameInfo *r = alloc(ASIZEOF(*r) + strlen(gi.imgname) + 1); + memcpy(r, &gi, sizeof(*r)); + r->imgname = strcpy((char*)(r+1), gi.imgname); + return r; +} + + +int gameLoad(Game *gm, const char *path) { + gameFree(gm); + + GameInfo gi = {}; + FILE *f = internalLoadGameInfo(&gi, path); + if (!f) return ERR("gameLoad: cannot load game info: %s", path); + if (!gameCreate(gm, path, gi.imgname, gi.rows, gi.cols, gi.turn, gi.seed)) { + ERR("gameLoad: cannot create game: %s, %s, %dx%d, seed %d", path, gi.imgname, gi.rows, gi.cols, gi.seed); + free(gi.imgname); + fclose(f); + return 0; + } + + int cnt = gm->pz.rows * gm->pz.cols; + Chunk *chunks = gm->pz.chunks[0]; + double* coords = (double*)alloc(cnt*3*ASIZEOF(double)); + for(int i = 0; i < cnt && !feof(f); ++i) { + int gid = -1; + fscanf(f, "%d", &gid); + if (gid < 0 || gid >= cnt) break; + + Chunk *c = &chunks[i]; + Chunk *p = &chunks[gid]; + if (i == gid) { + double v[3] = {}; + fscanf(f, "%lg%lg%lg", v+0, v+1, v+2); + if (!(v[0] == v[0]) || !(v[1] == v[1]) || !(v[2] == v[2])) { + WRN("gameLoad: invalid chunks positions"); + break; + } + memcpy(coords + i*3, v, sizeof(v)); + } else { + chunkMerge(p, c); + } + } + fclose(f); + + for(int i = 0; i < cnt; ++i) { + Chunk *c = &chunks[i]; + if (c->parent != &c->group) continue; + PhGroup *g = c->parent; + g->o.x = coords[3*i + 0]; + g->o.y = coords[3*i + 1]; + g->dx = gm->pz.turn ? vcossind(coords[3*i + 2]) : vec(1,0); + phGroupPlaceNodes(g); + } + free(coords); + + return 1; +} + + +void gameDraw(Game *gm) { + int w = windowGetWidth(); + int h = windowGetHeight(); + Vec s = { w, h }; + saveState(); + + translate(w/2, h/2); + + double pw = gm->pz.cols*gm->pz.cs.x; + double ph = gm->pz.rows*gm->pz.cs.y; + + double k = mind(w/pw, h/ph); + double kwin = k*0.9; + k /= 1.5; + + if (gameWon(gm)) { + double dt = windowGetFrameTime(); + gm->winTransition += dt; + if (gm->winTransition > 1) gm->winTransition = 1; + + PhGroup *g0 = gm->pz.ph.first; + k = lintr(k, kwin, gm->winTransition); + Vec o = vlintr(g0->o, vzero(), gm->winTransition); + double a = lintr(vangled(g0->dx), 0, gm->winTransition); + + zoom(k); + strokeWidth(3/k); + translate(o.x, o.y); + rotate(a); + rectTextured(gm->pz.image, -pw/2, -ph/2, pw, ph); + } else { + zoom(k); + strokeWidth(3/k); + + Vec mouse = { mouseTransformedX(), mouseTransformedY() }; + if (mouseWentDown("left")) + puzzleChooseChunks(&gm->pz, mouse); + if (mouseWentUp("left")) + puzzleReleaseChunks(&gm->pz); + puzzleUpdate(&gm->pz, vdiv(s, 2*k), mouse); + + int cnt = gm->pz.cols*gm->pz.rows; + for(int i = cnt-1; i >= 0; --i) { + chunkDraw(gm->pz.chunksOrder[i]); + //chunkDrawDebug(gm->pz.chunksOrder[i]); + } + //physicsDrawDebug(&gm->pz.ph); + } + + restoreState(); + + /*saveState(); + noFill(); + stroke(COLOR_WHITE); + textf(10, 10, "fps: %f", 1/windowGetFrameTime()); + restoreState();*/ +} + + + + diff --git a/projects/jigsaw/game.h b/projects/jigsaw/game.h new file mode 100644 index 0000000..1fadbb5 --- /dev/null +++ b/projects/jigsaw/game.h @@ -0,0 +1,37 @@ +#ifndef GAME_H +#define GAME_H + + +#include "path.h" +#include "puzzle.h" + + +typedef struct { + char *path; + char *imgname; + int seed; + Puzzle pz; + + double winTransition; +} Game; + + +typedef struct { + char *imgname; + int rows, cols, turn, groups, seed; + time_t t; +} GameInfo; + + +void gameFree(Game *gm); +int gameCreate(Game *gm, const char *path, const char *imgname, int rows, int cols, int turn, int seed); +int gameSave(Game *gm); +int gameLoad(Game *gm, const char *path); +GameInfo* loadGameInfo(const char *path); +void gameDraw(Game *gm); + +static inline int gameStarted(Game *gm) { return !!gm->path; } +static inline int gameWon(Game *gm) { return gameStarted(gm) && gm->pz.ph.first && !gm->pz.ph.first->next; } + + +#endif diff --git a/projects/jigsaw/geometry.h b/projects/jigsaw/geometry.h new file mode 100644 index 0000000..18ff7c3 --- /dev/null +++ b/projects/jigsaw/geometry.h @@ -0,0 +1,66 @@ +#ifndef GEOMETRY_H +#define GEOMETRY_H + + +#include "math.h" + + +#ifndef PI +#define PI 3.14159265358979323846 +#endif + + +#define PRECISION 1e-5 +#define PRECISION2 1e-10 + + +typedef struct { double x, y; } Vec; + + +static inline Vec vec(double x, double y) { return (Vec){x, y}; } +static inline Vec vecxy(double xy) { return vec(xy, xy); } +static inline Vec vzero() { return vecxy(0); } +static inline Vec vone() { return vecxy(1); } + +static inline Vec vadd(Vec a, Vec b) { return vec(a.x + b.x, a.y + b.y); } +static inline Vec vsub(Vec a, Vec b) { return vec(a.x - b.x, a.y - b.y); } +static inline Vec vmul(Vec v, double k) { return vec(v.x*k, v.y*k); } +static inline Vec vdiv(Vec v, double k) { return vec(v.x/k, v.y/k); } +static inline Vec vmulv(Vec a, Vec b) { return vec(a.x*b.x, a.y*b.y); } +static inline Vec vdivv(Vec a, Vec b) { return vec(a.x/b.x, a.y/b.y); } +static inline Vec vlintr(Vec a, Vec b, double k) { return vadd(vmul(a, 1-k), vmul(b, k)); } +static inline Vec vlintrv(Vec a, Vec b, Vec k) { return vadd(vmulv(a, vsub(vone(), k)), vmulv(b, k)); } + +static inline Vec vneg(Vec v) { return vec(-v.x, -v.y); } +static inline Vec vnegx(Vec v) { return vec(-v.x, v.y); } +static inline Vec vnegy(Vec v) { return vec(v.x, -v.y); } +static inline Vec vabs(Vec v) { return vec(fabs(v.x), fabs(v.y)); } +static inline Vec vabsx(Vec v) { return vec(fabs(v.x), v.y); } +static inline Vec vabsy(Vec v) { return vec(v.x, fabs(v.y)); } + +static inline double vdot(Vec a, Vec b) { return a.x*b.x + a.y*b.y; } +static inline double vlen2(Vec v) { return vdot(v, v); } +static inline double vlen(Vec v) { return sqrt(vlen2(v)); } +static inline double vdist2(Vec a, Vec b) { return vlen2(vsub(b, a)); } +static inline double vdist(Vec a, Vec b) { return sqrt(vdist2(b, a)); } + +static inline Vec vrot90(Vec v) { return vec(-v.y, v.x); } +static inline Vec vrot180(Vec v) { return vneg(v); } +static inline Vec vrot270(Vec v) { return vec(v.y, -v.x); } +static inline Vec vtranspose(Vec v) { return vec(v.y, v.x); } + +static inline Vec vturn(Vec v, Vec dx) { return vec(vdot(v, vnegy(dx)), vdot(v, vtranspose(dx))); } +static inline Vec vunturn(Vec v, Vec dx) { return vec(vdot(v, dx), vdot(v, vrot90(dx))); } +static inline Vec vtrans(Vec v, Vec dx, Vec o) { return vadd(vturn(v, dx), o); } +static inline Vec vuntrans(Vec v, Vec dx, Vec o) { return vunturn(vsub(v, o), dx); } + +static inline Vec vcossin(double rad) { return vec(cos(rad), sin(rad)); } +static inline Vec vcossind(double deg) { return vcossin(180/PI*deg); } +static inline double vangle(Vec v) { return atan2(v.y, v.x); } +static inline double vangled(Vec v) { return 180/PI*vangle(v); } + +static inline Vec vrot(Vec v, double rad) { return vturn(v, vcossin(rad)); } +static inline Vec vrotd(Vec v, double deg) { return vturn(v, vcossind(deg)); } + + +#endif diff --git a/projects/jigsaw/icons.S b/projects/jigsaw/icons.S new file mode 100644 index 0000000..7b70199 --- /dev/null +++ b/projects/jigsaw/icons.S @@ -0,0 +1,46 @@ + .global dataIconBack + .global dataIconBackSize + .global dataIconCount1 + .global dataIconCount1Size + .global dataIconCount2 + .global dataIconCount2Size + .global dataIconCount3 + .global dataIconCount3Size + .global dataIconCount4 + .global dataIconCount4Size + .global dataIconNoturn + .global dataIconNoturnSize + .global dataIconTurn + .global dataIconTurnSize + .section .rodata +dataIconBack: + .incbin "data/icons/back.png" +dataIconBackSize: + .int dataIconBackSize - dataIconBack +dataIconCount1: + .incbin "data/icons/count1.png" +dataIconCount1Size: + .int dataIconCount1Size - dataIconCount1 +dataIconCount2: + .incbin "data/icons/count2.png" +dataIconCount2Size: + .int dataIconCount2Size - dataIconCount2 +dataIconCount3: + .incbin "data/icons/count3.png" +dataIconCount3Size: + .int dataIconCount3Size - dataIconCount3 +dataIconCount4: + .incbin "data/icons/count4.png" +dataIconCount4Size: + .int dataIconCount4Size - dataIconCount4 +dataIconNoturn: + .incbin "data/icons/noturn.png" +dataIconNoturnSize: + .int dataIconNoturnSize - dataIconNoturn +dataIconTurn: + .incbin "data/icons/turn.png" +dataIconTurnSize: + .int dataIconTurnSize - dataIconTurn +#if defined(__linux__) && defined(__ELF__) + .section .note.GNU-stack, "", @progbits +#endif diff --git a/projects/jigsaw/icons.h b/projects/jigsaw/icons.h new file mode 100644 index 0000000..7e53436 --- /dev/null +++ b/projects/jigsaw/icons.h @@ -0,0 +1,21 @@ +#ifndef ICONS_H +#define ICONS_H + +#include + +extern const unsigned char dataIconBack[]; +extern const size_t dataIconBackSize; +extern const unsigned char dataIconCount1[]; +extern const size_t dataIconCount1Size; +extern const unsigned char dataIconCount2[]; +extern const size_t dataIconCount2Size; +extern const unsigned char dataIconCount3[]; +extern const size_t dataIconCount3Size; +extern const unsigned char dataIconCount4[]; +extern const size_t dataIconCount4Size; +extern const unsigned char dataIconNoturn[]; +extern const size_t dataIconNoturnSize; +extern const unsigned char dataIconTurn[]; +extern const size_t dataIconTurnSize; + +#endif diff --git a/projects/jigsaw/line.c b/projects/jigsaw/line.c new file mode 100644 index 0000000..0c8b38a --- /dev/null +++ b/projects/jigsaw/line.c @@ -0,0 +1,96 @@ + + +#include "line.h" + + + +void lineTranspose(Vertex *l, int cols) { + int cnt = cols*LN_SEGS+1; + for(int i = 0; i < cnt; ++i) { + Vertex *v = &l[i]; + v->p = vtranspose(v->p); + v->t = vtranspose(v->t); + } +} + + +void lineGenH(Vertex *l, int cols, double y, double cellw, double cellh, double jitter, double depth) { + if (cols <= 0) return; + + double kx = cellh < cellw ? cellh/cellw : 1.0; + double ky = cellw < cellh ? cellw/cellh : 1.0; + + int cnt = cols*LN_SEGS+1; + for(int i = 0; i < cnt; ++i) + l[i] = (Vertex){ {i*cellw/LN_SEGS, y}, {}, 1 }; + + if (jitter) for(int i = 1; i < cnt-1; ++i) { + l[i].p.x += randTwo()*kx*jitter*cellw/2; + l[i].p.y += randTwo()*ky*jitter*cellh; + l[i].w += randOne()*0.25; + } + + for(int c = 0; c < cols; ++c) { + double j = jitter ? randTwo()*(1-kx)/3 : 0; + l[c*LN_SEGS+1].p.x += ( 0.3 - kx/6 + j)*cellw; + l[c*LN_SEGS+2].p.x += ( 0.1 - kx/6 + j)*cellw; + l[c*LN_SEGS+3].p.x += (-0.1 + kx/6 + j)*cellw; + l[c*LN_SEGS+4].p.x += (-0.3 + kx/6 + j)*cellw; + double d = randSign()*depth*ky*cellh; + l[c*LN_SEGS+2].p.y += d; + l[c*LN_SEGS+3].p.y += d; + } + + Vec b = l[0].p, e = l[cnt-1].p; + b.x -= 1/3.0; e.x += 1/3.0; + for(int i = 0; i < cnt; ++i) { + Vertex *v = &l[i]; + Vec p0 = i ? (v-1)->p : b; + Vec p1 = i+1p : e; + v->t = vmul(vsub(p1, p0), v->w/2); + } +} + + +void lineGenV(Vertex *l, int rows, double x, double cellw, double cellh, double jitter, double depth) { + lineGenH(l, rows, x, cellh, cellw, jitter, depth); + lineTranspose(l, rows); +} + + +void linePut(Vertex *l, int cells, int levels) { + int cnt = cells*LN_SEGS; + if (cnt > 0) { + for(int i = 1; i <= cnt; ++i) { + Vertex v0 = l[i-1]; + Vertex v1 = l[i]; + cubicto( + v0.p.x, v0.p.y, + v0.p.x + v0.t.x/3, v0.p.y + v0.t.y/3, + v1.p.x - v1.t.x/3, v1.p.y - v1.t.y/3, + v1.p.x, v1.p.y, + levels ); + } + } else { + for(int i = -1; i >= cnt; --i) { + Vertex v0 = l[i+1]; + Vertex v1 = l[i]; + cubicto( + v0.p.x, v0.p.y, + v0.p.x - v0.t.x/3, v0.p.y - v0.t.y/3, + v1.p.x + v1.t.x/3, v1.p.y + v1.t.y/3, + v1.p.x, v1.p.y, + levels ); + } + } +} + + +void lineDraw(Vertex *l, int cells, int levels) { + if (!cells) return; + moveTo(l->p.x, l->p.y); + linePut(l, cells, levels); + strokePath(); +} + + diff --git a/projects/jigsaw/line.h b/projects/jigsaw/line.h new file mode 100644 index 0000000..17b8f14 --- /dev/null +++ b/projects/jigsaw/line.h @@ -0,0 +1,24 @@ +#ifndef LINE_H +#define LINE_H + + +#include "common.h" + + +#define LN_SEGS 5 + + +typedef struct { + Vec p, t; + double w; +} Vertex; + + +void lineTranspose(Vertex *l, int cols); +void lineGenH(Vertex *l, int cols, double y, double cellw, double cellh, double jitter, double depth); +void lineGenV(Vertex *l, int rows, double x, double cellw, double cellh, double jitter, double depth); +void linePut(Vertex *l, int cells, int levels); +void lineDraw(Vertex *l, int cells, int levels); + + +#endif diff --git a/projects/jigsaw/main.c b/projects/jigsaw/main.c new file mode 100644 index 0000000..223cf1a --- /dev/null +++ b/projects/jigsaw/main.c @@ -0,0 +1,44 @@ + +// todo: +// recursive merging (corner merge) +// improve ordering + + +#include "menu.h" + + +// app + +Menu menu; + + +void init() { + background(colorByRGBA(0.1, 0.1, 0.1, 1)); + menuInit(&menu); + menuLoad(&menu); + if (!gameStarted(&menu.gm)) menuUpdateItems(&menu); +} + + +void deinit() { + if (gameStarted(&menu.gm) && !gameWon(&menu.gm)) gameSave(&menu.gm); + menuSave(&menu); + menuFree(&menu); +} + + +void draw() { + menuDraw(&menu); +} + + +int main() { + windowSetVariableFrameRate(); + windowSetResizable(TRUE); + windowSetInit(&init); + windowSetDeinit(&deinit); + windowSetDraw(&draw); + windowRun(); + return 0; +} + diff --git a/projects/jigsaw/menu.c b/projects/jigsaw/menu.c new file mode 100644 index 0000000..c8b7d84 --- /dev/null +++ b/projects/jigsaw/menu.c @@ -0,0 +1,547 @@ + +#include "menu.h" +#include "icons.h" + + +static Animation loadThumb(const char *imgname, Framebuffer fb, int w, int h) { + char *imgpath = findImage(imgname); + if (!imgpath) + { ERR("loadThumb: image not found: %s", imgname); return NULL; } + + if (w < 1) w = 1; + if (h < 1) h = 1; + + char *tpath = strprintf("%s/%s", thumbsPath(), filename(imgpath)); + if (fileExists(tpath) && filetime(imgpath) < filetime(tpath)) { + Animation thumb = createAnimationEx(tpath, TRUE, FALSE, FALSE); + if (abs(animationGetOrigWidth(thumb) - w) <= 2 || abs(animationGetOrigHeight(thumb) - h) <= 2) { + free(tpath); + free(imgpath); + return thumb; + } + animationDestroy(thumb); + } + + INF("loadThumb: create thumbnail: %s -> %s", imgpath, tpath); + + Animation full = createAnimationEx(imgpath, TRUE, FALSE, FALSE); + int fw = animationGetOrigWidth(full); + int fh = animationGetOrigHeight(full); + + int tw = w, th = h; + if (fw*(double)h > fh*(double)w) th = (int)round(w/(double)fw*fh); + else tw = (int)round(h/(double)fh*fw); + if (tw < 1) tw = 1; else if (tw > w) tw = w; + if (th < 1) th = 1; else if (th > h) th = h; + + saveState(); + resetState(); + target(fb); + background(COLOR_TRANSPARENT); + clear(); + noStroke(); + rectTextured(full, 0, 0, tw, th); + + int iw, ih; + unsigned char *pixels; + Animation thumb = NULL; + if (imageFromViewport(&iw, &ih, &pixels)) { + if (ih > th) ih = th; + if (iw > tw) { + for(int i = 1; i < ih; ++i) memmove(pixels + i*tw*4, pixels + i*iw*4, tw*4); + iw = tw; + } + if (iw != tw || ih != th) { + WRN("loadThumb: thumbnail was truncated: %s, [%dx%d] -> [%dx%d]", imgpath, tw, th, iw, ih); + } else + if (!imageSave(tpath, iw, ih, pixels)) { + WRN("loadThumb: cannot save thumbnail to: %s", tpath); + } + thumb = createAnimationFromImageEx(iw, ih, pixels, FALSE, FALSE, TRUE, TRUE); + free(pixels); + } else { + WRN("loadThumb: cannot capture vieport: %s", imgpath); + unsigned char px[4*4*4] = {}; + thumb = createAnimationFromImageEx(4, 4, px, FALSE, FALSE, TRUE, TRUE); + } + + restoreState(); + free(tpath); + free(imgpath); + return thumb; +} + + +int menuItemInit(MenuItem *mi, const char *path, const char *imgname, Vec p0, Vec p1, Framebuffer fb, int w, int h) { + memset(mi, 0, sizeof(*mi)); + + DBG("%s, %s", path ? path : "null", imgname ? imgname : "null"); + + Vec d = vsub(p1, p0); + if (!(fabs(d.x) > PRECISION) || !(fabs(d.y) > PRECISION)) return 0; + + if (path) { + GameInfo *gi = loadGameInfo(path); + if (!gi) return 0; + mi->image = loadThumb(gi->imgname, fb, w, h); + if (!mi->image) { free(gi); return 0; } + + int cnt = gi->rows*gi->cols; + mi->path = strprintf("%s", path); + mi->imgname = strprintf("%s", gi->imgname); + mi->percent = (cnt - gi->groups)/(cnt - 1.0); + mi->t = filetime(path); + free(gi); + } else + if (imgname) { + mi->image = loadThumb(imgname, fb, w, h); + if (!mi->image) return 0; + mi->imgname = strprintf("%s", imgname); + } else { + return 0; + } + + Vec is = { animationGetOrigWidth(mi->image), animationGetOrigHeight(mi->image) }; + if (is.x < 1) is.x = 1; + if (is.y < 1) is.y = 1; + Vec k = vabs(vdivv(is, d)); + + if (k.x > k.y) { + double l = (1 - k.y/k.x)/2; + mi->p0 = vec(p0.x, lintr(p0.y, p1.y, l)); + mi->p1 = vec(p1.x, lintr(p0.y, p1.y, 1-l)); + } else { + double l = (1 - k.x/k.y)/2; + mi->p0 = vec(lintr(p0.x, p1.x, l), p0.y); + mi->p1 = vec(lintr(p0.x, p1.x, 1-l), p1.y); + } + + return 1; +} + + +void menuItemFree(MenuItem *mi) { + free(mi->path); + free(mi->imgname); + if (mi->image) animationDestroy(mi->image); + memset(mi, 0, sizeof(*mi)); +} + + +static void splitRect(Vec p0, Vec p1, double percent, Vec *v0, int *v0c, Vec *v1, int *v1c) { + Vec s = vabs(vsub(p1, p0)); + if (s.x < s.y) { + splitRect(vtranspose(p0), vtranspose(p1), percent, v0, v0c, v1, v1c); + for(int i = 0; i < *v0c; ++i) v0[i] = vtranspose(v0[i]); + for(int i = 0; i < *v1c; ++i) v1[i] = vtranspose(v1[i]); + return; + } + if (percent > 0.5) { + splitRect(p1, p0, 1 - percent - PRECISION, v1, v1c, v0, v0c); + return; + } + + if (!(percent > PRECISION) || !(s.x > PRECISION) || !(s.y > PRECISION)) + { v0[0] = p0; v0[1] = vec(p1.x, p0.y); v0[2] = p1; v0[3] = vec(p0.x, p1.y); *v0c = 4; *v1c = 0; return; } + if (!(percent < 1-PRECISION)) + { v1[0] = p1; v1[1] = vec(p0.x, p1.y); v1[2] = p0; v1[3] = vec(p1.x, p0.y); *v0c = 0; *v1c = 4; return; } + + *v0c = *v1c = 0; + v0[(*v0c)++] = p0; + v1[(*v1c)++] = p1; + + double af = s.x*s.y; + double ap = af*percent; + double ac = s.y*s.y/2; + if (ap < ac) { + double ky = sqrt(ap/ac); + Vec a = { lintr(p0.x, p1.x, ky*s.y/s.x), p0.y }; + Vec b = { p0.x, lintr(p0.y, p1.y, ky) }; + + v0[(*v0c)++] = a; + v0[(*v0c)++] = b; + + v1[(*v1c)++] = vec(p0.x, p1.y); + v1[(*v1c)++] = b; + v1[(*v1c)++] = a; + v1[(*v1c)++] = vec(p1.x, p0.y); + } else { + double kx = (ap-ac)/(af-2*ac)*(s.x-s.y)/s.x; + Vec b = { lintr(p0.x, p1.x, kx), p1.y }; + Vec a = { b.x + p1.y - p0.y, p0.y }; + + v0[(*v0c)++] = a; + v0[(*v0c)++] = b; + v0[(*v0c)++] = vec(p0.x, p1.y); + + v1[(*v1c)++] = b; + v1[(*v1c)++] = a; + v1[(*v1c)++] = vec(p1.x, p0.y); + } +} + + +void menuItemDraw(MenuItem *mi) { + saveState(); + Vec d = vsub(mi->p1, mi->p0); + if (mi->path) { + noStroke(); + Vec v[2][5] = {}; + int vc[2] = {}; + splitRect(mi->p0, mi->p1, mi->percent, v[0], &vc[0], v[1], &vc[1]); + noStroke(); + for(int i = 0; i < 2; ++i) { + fill(colorByRGBA(1, 1, 1, (2-i)*0.5)); + fillTexture(mi->image, mi->p0.x, mi->p0.y, d.x, d.y, FALSE); + moveTo(v[i][0].x, v[i][0].y); + for(int j = 1; j < vc[i]; ++j) lineTo(v[i][j].x, v[i][j].y); + closePath(); + } + noFill(); + stroke(COLOR_BLUE); + rect(mi->p0.x, mi->p0.y, d.x, d.y); + } else { + stroke(COLOR_BLACK); + fill(COLOR_WHITE); + rectTextured(mi->image, mi->p0.x, mi->p0.y, d.x, d.y); + } + restoreState(); +} + + +static Animation loadIcon(const void *data, size_t size) { + unsigned char *p = NULL; int w = 0, h = 0; + if (!imageLoadFromMemory(data, size, &w, &h, &p)) + return createAnimationEmpty(); + Animation a = createAnimationFromImageEx(w, h, p, FALSE, FALSE, TRUE, TRUE); + free(p); + return a; +} + + +void menuInit(Menu *m) { + memset(m, 0, sizeof(*m)); + m->contentSize = vone(); + #define LOADICON(x) loadIcon(dataIcon ## x, dataIcon ## x ## Size) + m->iconBack = LOADICON(Back); + m->iconCount[0] = LOADICON(Count1); + m->iconCount[1] = LOADICON(Count2); + m->iconCount[2] = LOADICON(Count3); + m->iconCount[3] = LOADICON(Count4); + m->iconTurn[0] = LOADICON(Noturn); + m->iconTurn[1] = LOADICON(Turn); + #undef LOADICON +} + + +void menuFree(Menu *m) { + if (gameStarted(&m->gm)) gameFree(&m->gm); + for(int i = 0; i < m->itemsCnt; ++i) menuItemFree(&m->items[i]); + free(m->items); + if (m->iconBack) animationDestroy(m->iconBack); + for(int i = 0; i < COUNTOF(m->iconCount); ++i) + if (m->iconCount[i]) animationDestroy(m->iconCount[i]); + for(int i = 0; i < COUNTOF(m->iconTurn); ++i) + if (m->iconTurn[i]) animationDestroy(m->iconTurn[i]); + memset(m, 0, sizeof(*m)); +} + + +void menuUpdateItems(Menu* m) { + // remove old items, but copy images to keep them in memory + Animation animCopy = createAnimationEmpty(); + for(int i = 0; i < m->itemsCnt; ++i) { + animationInsert(animCopy, -1, m->items[i].image); + menuItemFree(&m->items[i]); + } + free(m->items); + m->itemsCnt = 0; + + Vec border = {1, 1}; + Vec spacing = {1, 1}; + Vec thumb = {16, 12}; + int cols = 4; + int tw = 256; + int th = tw/4*3; + + Framebuffer fb = createFramebuffer(tw, tw); + + PathList *pls = saveFiles(); + PathList *pli = imageFiles(); + m->items = alloc((pls->cnt + pli->cnt)*ASIZEOF(*m->items)); + + int c = 0; + double y = border.y, ly = y; + for(int i = 0; i < pls->cnt; ++i) { + Vec p0 = { border.x + (thumb.x + spacing.x)*c, y }; + Vec p1 = vadd(p0, thumb); + if (menuItemInit(&m->items[m->itemsCnt], pls->l[i], NULL, p0, p1, fb, tw, th)) { + if (++c >= cols) { c = 0; y += thumb.y + spacing.y; } + ++m->itemsCnt; ly = p1.y; + } + } + + if (c) { c = 0; y += thumb.y + spacing.y; } + if (m->itemsCnt) y += spacing.y; + + for(int i = 0; i < pli->cnt; ++i) { + Vec p0 = { border.x + (thumb.x + spacing.x)*c, y }; + Vec p1 = vadd(p0, thumb); + char *imgname = filebase(pli->l[i]); + if (menuItemInit(&m->items[m->itemsCnt], NULL, imgname, p0, p1, fb, tw, th)) { + if (++c >= cols) { c = 0; y += thumb.y + spacing.y; } + ++m->itemsCnt; ly = p1.y; + } + free(imgname); + } + + animationDestroy(animCopy); + free(pls); + free(pli); + + framebufferDestroy(fb); + m->contentSize = vec( 2*border.x + thumb.x*cols + spacing.x*(cols-1), ly + border.y ); +} + + +int menuSave(Menu *m) { + makeDirectoryRecursive(savesPath()); + const char *fn = settingsFile(); + FILE *f = fopen(fn, "w"); + if (!f) return ERR("menuSave: cannnot open file for write: %s", fn); + fprintf(f, "puzzle1\n[%s]\n%d %d\n", m->gm.path ? m->gm.path : "", m->countMode, m->turnMode); + fflush(f); + if (ferror(f)) ERR("menuSave: cannnot write to file: %s", fn); + fclose(f); + return 1; +} + + +int menuLoad(Menu *m) { + const char *fn = settingsFile(); + FILE *f = fopen(fn, "r"); + if (!f) return 0; + + size_t ls = 2; + char *l = (char*)alloc(ls); + getline(&l, &ls, f); trim(l); + if (strcmp(l, "puzzle1")) { + ERR("menuLoad: wrong file version (expected: puzzle1): %s", l); + free(l); fclose(f); return 0; + } + + getline(&l, &ls, f); unquote(l, '[', ']'); + int cm = -1, tm = -1; + fscanf(f, "%d %d", &cm, &tm); + fclose(f); + + if ( cm < 0 || cm > COUNTOF(m->iconCount) || tm < 0 || tm > COUNTOF(m->iconTurn)) + { ERR("menuLoad: cannot parse file: %s", fn); free(l); return 0; } + + m->countMode = cm; + m->turnMode = tm; + if (*l) gameLoad(&m->gm, l); + free(l); + return 1; +} + + +void menuClick(Menu *m, Vec screenSize, double cornerSize) { + double cs2 = cornerSize*cornerSize; + if (vlen2(vsub(vec(screenSize.x, 0), m->mouseDownPos)) <= cs2) + { m->countMode = (m->countMode+1) % COUNTOF(m->iconCount); return; } + if (vlen2(vsub(screenSize, m->mouseDownPos)) <= cs2) + { m->turnMode = (m->turnMode+1) % COUNTOF(m->iconTurn); return; } + + Vec p = m->mouseDownPos; + p.y += m->scrollPos; + for(int i = 0; i < m->itemsCnt; ++i) { + MenuItem *mi = &m->items[i]; + if ( !(p.x >= mi->p0.x) || !(p.x <= mi->p1.x) + || !(p.y >= mi->p0.y) || !(p.y <= mi->p1.y) ) continue; + + if (mi->path) { + gameLoad(&m->gm, mi->path); + return; + } + + int cnt = m->countMode == 0 ? 12 + : m->countMode == 1 ? 24 + : m->countMode == 2 ? 48 : 96; + + char *path = NULL; + for(int j = 0; j < 10000; ++j) { + path = strprintf("%s/%s_%04d.puzzle", savesPath(), mi->imgname, j); + if (fileExists(path)) { free(path); continue; } + gameCreate(&m->gm, path, mi->imgname, cnt, 0, m->turnMode, 0); + free(path); + return; + } + } +} + + +void menuDraw(Menu *m) { + int w = windowGetWidth(); + int h = windowGetHeight(); + double dt = windowGetFrameTime(); + double k = w/m->contentSize.x; + Vec screenSize = { w/k, h/k }; + + double maxPath = m->contentSize.x*0.01; + double scrollMax = m->contentSize.y - screenSize.y; + double scrollBase = m->contentSize.x*dt*2; + double scrollDecc = m->contentSize.x*dt*2; + double scrollOver = m->contentSize.x*0.2; + if (!(scrollMax > 0)) scrollMax = 0; + + saveState(); + zoom(k); + strokeWidth(1/k); + + double cornerSizeX = screenSize.x/8; + double cornerSizeY = screenSize.y/4; + double cornerSize = cornerSizeX < cornerSizeY ? cornerSizeX : cornerSizeY; + + Vec mouse = { mouseTransformedX(), mouseTransformedY() }; + + if (mouseWentDown("left")) { + m->mouseDownPos = mouse; + m->mouseDownTime = 0; + m->mousePath = 0; + m->mouseScroll = m->scrollPos; + if (vlen2(mouse) <= cornerSize*cornerSize || gameWon(&m->gm)) + { + if (gameStarted(&m->gm) && !gameWon(&m->gm)) gameSave(&m->gm); + m->closing = 1; + } + } + + if (mouseWentUp("left")) m->closing = 0; + + if (mouseDown("left")) { + m->mousePath += vlen(vsub(mouse, m->mouse)); + m->mouseDownTime += dt; + if (!(m->mousePath < maxPath)) m->closing = 0; + } + + if (!gameStarted(&m->gm)) { + if (mouseWentDown("left")) + memset(m->scrollHist, 0, sizeof(m->scrollHist)); + + if (mouseWentUp("left")) { + if ( m->mousePath < maxPath + && m->mouseDownTime < 0.5 + && vlen(vsub(mouse, m->mouseDownPos)) < maxPath ) + { + m->scrollVel = 0; + menuClick(m, screenSize, cornerSize); + } else { + double p1 = m->scrollPos, p0 = p1, t = 0; + for(int i = 0; i < MS_CNT; ++i) { + if (m->scrollHist[i][1] > PRECISION) { + p0 = m->scrollHist[i][0]; + t += m->scrollHist[i][1]; + if (i >= 3 && t > 0.2) break; + } + } + m->scrollVel = t > PRECISION ? (p1 - p0)/t : 0; + } + } + + if (mouseDown("left")) { + m->scrollPos = m->mouseScroll + m->mouseDownPos.y - mouse.y; + memmove(m->scrollHist + 1, m->scrollHist, (MS_CNT-1)*sizeof(*m->scrollHist)); + m->scrollHist[0][0] = m->scrollPos; + m->scrollHist[0][1] = dt; + } else { + if (m->scrollPos < -scrollOver && m->scrollVel < 0) m->scrollVel = 0; + if (m->scrollPos > scrollMax+scrollOver && m->scrollVel > 0) m->scrollVel = 0; + m->scrollPos += m->scrollVel*dt; + + if (m->scrollVel < 0) { + m->scrollVel += scrollDecc; + if (!(m->scrollVel < 0)) m->scrollVel = 0; + } else { + m->scrollVel -= scrollDecc; + if (!(m->scrollVel > 0)) m->scrollVel = 0; + } + + if (m->scrollPos < 0) { + m->scrollPos += scrollBase; + if (!(m->scrollPos < 0)) m->scrollPos = 0; + } else + if (m->scrollPos > scrollMax) { + m->scrollPos -= scrollBase; + if (!(m->scrollPos > scrollMax)) m->scrollPos = scrollMax; + } + } + + saveState(); + translate(0, -m->scrollPos); + for(int i = 0; i < m->itemsCnt; ++i) + menuItemDraw(&m->items[i]); + restoreState(); + } + + m->mouse = mouse; + + if (gameStarted(&m->gm)) { + int wasGroups = m->gm.pz.groups; + int wasWon = gameWon(&m->gm); + + saveState(); + zoom(1/k); + gameDraw(&m->gm); + restoreState(); + + if (wasGroups != m->gm.pz.groups && !gameWon(&m->gm)) + gameSave(&m->gm); + if (wasWon && gameWon(&m->gm) && fileExists(m->gm.path) && remove(m->gm.path)) + ERR("cannot remove save-file for the won game: %s", m->gm.path); + } + + saveState(); + noStroke(); + fill(colorByRGBA(1, 1, 1, 0.25)); + rectTextured(m->iconBack, 0, 0, cornerSize, cornerSize); + if (!gameStarted(&m->gm)) { + rectTextured(m->iconCount[m->countMode], screenSize.x-cornerSize, 0, cornerSize, cornerSize); + rectTextured(m->iconTurn[m->turnMode], screenSize.x-cornerSize, screenSize.y-cornerSize, cornerSize, cornerSize); + } + restoreState(); + + if (m->closing) { + m->closingTransition += dt; + if (m->closingTransition > 1) { + if (gameStarted(&m->gm)) { + gameFree(&m->gm); + m->closing = 0; + m->closingTransition = 0; + menuUpdateItems(m); + } else { + m->closingTransition = 1; + windowStop(); + } + } + } else { + m->closingTransition -= dt; + if (!(m->closingTransition > 0)) m->closingTransition = 0; + } + + if (m->closingTransition > 0) { + saveState(); + noStroke(); + fill(gameStarted(&m->gm) ? colorByRGBA(0.05, 0.05, 0.05, 1) : COLOR_BLACK); + Vec c = m->mouseDownPos; + double r0 = vlen(vsub(screenSize, c)) * (1 - m->closingTransition); + double r1 = r0 + vlen(screenSize); + arcPath(c.x-r0, c.y-r0, 2*r0, 2*r0, 180, -180); + arcPath(c.x-r1, c.y-r1, 2*r1, 2*r1, -180, 180); + closePath(); + restoreState(); + } + + restoreState(); +} + diff --git a/projects/jigsaw/menu.h b/projects/jigsaw/menu.h new file mode 100644 index 0000000..7067352 --- /dev/null +++ b/projects/jigsaw/menu.h @@ -0,0 +1,58 @@ +#ifndef MENU_H +#define MENU_H + + +#include "game.h" + + +#define MS_CNT 16 + +typedef struct { + char *path; + char *imgname; + double percent; + Animation image; + Vec p0, p1; + time_t t; +} MenuItem; + + +typedef struct { + Game gm; + MenuItem *items; + int itemsCnt; + Vec contentSize; + + double scrollPos; + double scrollVel; + double scrollHist[MS_CNT][2]; + Vec mouse, mouseDownPos; + double mouseDownTime; + double mouseScroll; + double mousePath; + + Animation iconBack; + Animation iconCount[4]; + Animation iconTurn[2]; + + int countMode; + int turnMode; + int closing; + double closingTransition; +} Menu; + + +int menuItemInit(MenuItem *mi, const char *path, const char *imgname, Vec p0, Vec p1, Framebuffer fb, int w, int h); +void menuItemFree(MenuItem *mi); +void menuItemDraw(MenuItem *mi); + +void menuInit(Menu *m); +void menuFree(Menu *m); +void menuUpdateItems(Menu *m); +int menuSave(Menu *m); +int menuLoad(Menu *m); +void menuClick(Menu *m, Vec screenSize, double cornerSize); +void menuDraw(Menu *m); + + +#endif diff --git a/projects/jigsaw/path.c b/projects/jigsaw/path.c new file mode 100644 index 0000000..9276d89 --- /dev/null +++ b/projects/jigsaw/path.c @@ -0,0 +1,250 @@ + +#include "path.h" + +#include +#include + + + +#ifndef APPNAME +#define APPNAME "jigsaw" +#endif + +#ifndef SUBDIR +#define SUBDIR "games/" APPNAME +#endif + +#ifndef PREFIX +#define PREFIX "/usr" +#endif + +#ifndef PATHENV_DATA +#define PATHENV_DATA "JIGSAW_DATA" +#endif + +#ifndef PATHENV_SAVE +#define PATHENV_SAVE "JIGSAW_SAVE" +#endif + + + +const char* filename(const char *path) { + const char *r = path; + for(const char *c = r; *c; ++c) if (*c == '/' || *c == '\\') r = c+1; + return r; +} + + +const char* fileext(const char *path) { + const char* r = path = filename(path), *c = r; + for(;*c; ++c) if (*c == '.') r = c; + return r == path ? c : r; +} + + +char* filebase(const char *path) { + const char* c0 = filename(path); + const char* c1 = fileext(c0); + char* r = (char*)alloc(c1 - c0 + 1); + return strncpy(r, c0, c1 - c0); +} + + +char* filedir(const char *path) { + const char* c = filename(path); + while(c > path && (*(c-1) == '/' || *(c-1) == '\\')) --c; + char* r = (char*)alloc(c - path + 1); + return strncpy(r, path, c - path); +} + + + + +time_t filetime(const char *path) { + struct stat st = {}; + return stat(path, &st) ? 0 : st.st_mtime; +} + + +int makeDirectoryRecursive(const char *path) { + if (directoryExists(path)) return 1; + char *parent = filedir(path); + if (*parent) makeDirectoryRecursive(parent); + free(parent); + return makeDirectory(path); +} + + + +PathList* mergePaths(PathList a, PathList b) { + int cnt = 0; + size_t size = ASIZEOF(PathList); + for(int i = 0, j = 0; i < a.cnt || j < b.cnt; ++cnt) { + int r = i < a.cnt && j < b.cnt ? strcmp(filename(a.l[i]), filename(b.l[i])) + : i < a.cnt ? -1 : 1; + if (r < 0) { size += strlen(a.l[i++]) + 1; } + else { size += strlen(b.l[j++]) + 1; i += !r; } + } + size += cnt*ASIZEOF(char*); + + PathList *pl = alloc(size); + char **l = (char**)(pl+1); + char *c = (char*)(l+cnt); + pl->l = (StrList)l; + pl->cnt = cnt; + for(int i = 0, j = 0; i < a.cnt || j < b.cnt; ++cnt) { + int r = i < a.cnt && j < b.cnt ? strcmp(filename(a.l[i]), filename(b.l[i])) + : i < a.cnt ? -1 : 1; + if (r < 0) { *l++ = strcpy(c, a.l[i++]); } + else { *l++ = strcpy(c, b.l[j++]); i += !r; } + c += strlen(c) + 1; + } + + return pl; +} + + +PathList* readDir(const char *path, const char *ext) { + Directory d = openDirectory(path); + if (!d) return alloc(sizeof(PathList)); + + int cnt = directoryGetCount(d); + size_t size = ASIZEOF(PathList) + cnt*ASIZEOF(char*); + for(int i = 0; i < cnt; ++i) { + const char *p = directoryGetFull(d, i); + if (!fileExists(p) || strcmp(fileext(p), ext)) continue; + size += strlen(p) + 1; + } + + PathList *pl = alloc(size); + char **l = (char**)(pl+1); + char *c = (char*)(l+cnt); + pl->l = (StrList)l; + for(int i = 0; i < cnt; ++i) { + const char *p = directoryGetFull(d, i); + if (!fileExists(p) || strcmp(fileext(p), ext)) continue; + l[pl->cnt++] = strcpy(c, p); + c += strlen(c) + 1; + } + + closeDirectory(d); + return pl; +} + + +PathList* readDirs(PathList paths, const char *ext) { + PathList *a = alloc(sizeof(PathList)); + for(int i = 0; i < paths.cnt; ++i) { + PathList *b = readDir(paths.l[i], ext); + PathList *c = mergePaths(*a, *b); + free(a); free(b); a = c; + } + return a; +} + + +char* findFile(PathList paths, const char *name) { + for(int i = paths.cnt-1; i >= 0; --i) { + char *fn = strprintf("%s/%s", paths.l[i], name); + if (fileExists(fn)) return fn; + free(fn); + } + return NULL; +} + + + +PathList imagePaths() { + static PathList pl; + if (pl.l) return pl; + + const char *sys = PREFIX "/local/share:" PREFIX "/share"; + const char *xdd = getenv("XDG_DATA_DIRS"); + if (!xdd) xdd = ""; + const char *home = getenv("HOME"); + + char *paths = home && *home ? strprintf("%s/.local/share:%s:%s", home, xdd, sys) + : strprintf("%s:%s", xdd, sys); + int cnt = 3; // :tail + wd + ud + for(char* c = paths; *c; ++c) if (*c == ':') ++cnt; + + char **l = (char**)alloc(cnt*ASIZEOF(*l)); + pl.l = (StrList)l; + + while(pl.cnt < cnt) { + char* c = strrchr(paths, ':'); + if (c) *c++ = 0; else c = paths; + if (*c) l[pl.cnt++] = strprintf("%s/%s", c, SUBDIR); + if (c == paths) break; + } + + const char *wd = getcwd(NULL, 0); + const char *ud = getenv(PATHENV_DATA); + if (wd && *wd) l[pl.cnt++] = strprintf("%s/data/images", wd); + if (ud && *ud) l[pl.cnt++] = strprintf("%s", ud); + + for(int i = 0; i < pl.cnt; ++i) INF("images directory: %s", l[i]); + return pl; +} + + +const char* savesPath() { + static char *path; + if (path) return path; + + if (!path) { + const char *p = getenv(PATHENV_SAVE); + if (p && *p) path = strprintf("%s", p); + } + if (!path) { + const char *p = getenv("HOME"); + if (p && *p) path = strprintf("%s/.local/share/%s/saves", p, SUBDIR); + } + if (!path) { + const char *p = getcwd(NULL, 0); + if (p && *p) path = strprintf("%s/data/saves", p); + } + if (!path) + path = strprintf("./data/saves"); + + INF("saves directory: %s", path); + if (!makeDirectoryRecursive(path)) + WRN("cannot create saves directory: %s", path); + return path; +} + + +const char* thumbsPath() { + static char *path; + if (!path) { + path = strprintf("%s/thumbs", savesPath()); + INF("thumbs directory: %s", path); + if (!makeDirectoryRecursive(path)) + WRN("cannot create thumbs directory: %s", path); + } + return path; +} + + +const char* settingsFile() { + static char *path; + if (!path) { + path = strprintf("%s/settings.ini", savesPath()); + INF("settings file: %s", path); + } + return path; +} + + +char* findImage(const char *name) { + char* fn = strprintf("%s.png", name); + char* r = findFile(imagePaths(), fn); + free(fn); + return r; +} + + +PathList* imageFiles() { return readDirs(imagePaths(), ".png"); } +PathList* saveFiles() { return readDir(savesPath(), ".puzzle"); } + + diff --git a/projects/jigsaw/path.h b/projects/jigsaw/path.h new file mode 100644 index 0000000..80a85b5 --- /dev/null +++ b/projects/jigsaw/path.h @@ -0,0 +1,38 @@ +#ifndef PATH_H +#define PATH_H + + +#include "common.h" + + +typedef const char* const* StrList; +typedef struct { + StrList l; + int cnt; +} PathList; + + +const char* filename(const char *path); +const char* fileext(const char *path); +char* filebase(const char *path); +char* filedir(const char *path); + +time_t filetime(const char *path); +int makeDirectoryRecursive(const char *path); + +PathList* pathsMerge(PathList a, PathList b); +PathList* readDir(const char *path, const char *ext); +PathList* readDirs(PathList paths, const char *ext); +char* findFile(PathList paths, const char *name); + +PathList imagePaths(); +const char* savesPath(); +const char* thumbsPath(); +const char* settingsFile(); +char* findImage(const char *name); +PathList* imageFiles(); +PathList* saveFiles(); + + + +#endif diff --git a/projects/jigsaw/phisics.c b/projects/jigsaw/phisics.c new file mode 100644 index 0000000..3cf9725 --- /dev/null +++ b/projects/jigsaw/phisics.c @@ -0,0 +1,71 @@ + +#include "phisics.h" + + + +void physicsAlloc(Physics *ph, int hw, int hh, double cs) { + ph->cs = maxd(PRECISION, cs); + ph->hw = hw > 2 ? hw : 2; + ph->hh = hh > 2 ? hh : 2; + ph->cells = (PhCell*)alloc(4*ph->hh*ph->hw*ASIZEOF(*ph->cells)); +} + + +void physicsFree(Physics *ph) { + free(ph->cells); + memset(ph, 0, sizeof(*ph)); +} + + +PhCell* physicsCell(Physics *ph, Vec p) { + int cx = (int)floor(p.x/ph->cs + ph->hw - 0.5); + int cy = (int)floor(p.y/ph->cs + ph->hh - 0.5); + if (cx > 2*(ph->hw-1)) cx = 2*(ph->hw-1); + if (cy > 2*(ph->hh-1)) cy = 2*(ph->hh-1); + if (cx < 0) cx = 0; + if (cy < 0) cy = 0; + return ph->cells + 2*ph->hw*cy + cx; +} + + +void physicsClear(Physics *ph) { + while(ph->first) { + while(ph->first->first) phNodeUnreg(ph->first->first); + phGroupUnreg(ph->first); + } +} + + +int physicsUpdate(Physics *ph) { + int res = 0; + // collision + //for(PhGroup *g = ph->first; g; g = g->next) + // for(PhNode *n = g->first; n; n = n->next) + // res += phNodeCollision(n); + // bounds + for(PhGroup *g = ph->first; g; g = g->next) + for(PhNode *n = g->first; n; n = n->next) + res += phNodeBound(n, ph->b0, ph->b1); + // fix + for(PhGroup *g = ph->first; g; g = g->next) + phGroupFix(g, ph->fixangle); + return res; +} + + +void physicsDrawDebug(Physics *ph) { + int seed = rand(); + srand(0); + saveState(); + noFill(); + for(PhGroup *g = ph->first; g; g = g->next) { + stroke(randColorPtr(g)); + for(PhNode *n = g->first; n; n = n->next) + circle(n->p.x, n->p.y, n->r); + } + restoreState(); + srand(seed); +} + + + diff --git a/projects/jigsaw/phisics.group.c b/projects/jigsaw/phisics.group.c new file mode 100644 index 0000000..a346631 --- /dev/null +++ b/projects/jigsaw/phisics.group.c @@ -0,0 +1,128 @@ + +#include "phisics.h" + + + +void phGroupUpdateCells(PhGroup *g) + { for(PhNode *n = g->first; n; n = n->next) phNodeUpdateCell(n); } + + +void phGroupUnreg(PhGroup *g) { + if (!g->ph) return; + phGroupUpdateCells(g); + *(g->prev ? &g->prev->next : &g->ph->first) = g->next; + *(g->next ? &g->next->prev : &g->ph->last) = g->prev; + g->prev = g->next = NULL; + g->ph = NULL; +} + + +void phGroupReg(PhGroup *g, Physics *ph) { + if (g->ph == ph) return; + phGroupUnreg(g); + g->ph = ph; + g->prev = ph->last; + *(g->prev ? &g->prev->next : &ph->first) = ph->last = g; + phGroupUpdateCells(g); +} + + +void phGroupRecalc(PhGroup *g) { + g->cnt = 0; + g->go.x = g->go.y = 0; + for(PhNode *n = g->first; n; n = n->next) + { g->go = vadd(g->go, n->gp); ++g->cnt; } + if (!g->cnt) return; + g->go = vdiv(g->go, g->cnt); + + for(PhNode *n = g->first; n; n = n->next) { + Vec d = vsub(n->gp, g->go); + double d2 = vlen2(d); + n->gk = d2 > PRECISION2 ? vdiv(d, d2) : vzero(); + } + + g->dx = vec(1, 0); +} + + +void phGroupPlaceNodes(PhGroup* g) { + for(PhNode *n = g->first; n; n = n->next) { + n->p = phGroupTrans(g, n->gp); + phNodeUpdateCell(n); + } +} + + +void phGroupFix(PhGroup *g, int fixangle) { + if (!g->cnt) return; + + g->o = vzero(); + for(PhNode *n = g->first; n; n = n->next) + g->o = vadd(g->o, n->p); + g->o = vdiv(g->o, g->cnt); + + if (!fixangle) { + Vec dx = {}; + for(PhNode *n = g->first; n; n = n->next) + dx = vadd(dx, vuntrans(n->p, n->gk, g->o)); + double l = vlen(dx); + if (l > PRECISION) g->dx = vdiv(dx, l); + } + + phGroupPlaceNodes(g); +} + + +void phGroupMove(PhGroup *g, Vec gp, Vec p, int fixangle) { + if (fixangle) { + g->o = vadd(g->o, vsub(p, phGroupTrans(g, gp))); + phGroupPlaceNodes(g); + return; + } + + Vec d = vsub(p, g->o); + Vec gd = vsub(gp, g->go); + double l = vlen(d); + double gl = vlen(gd); + + if (l > PRECISION) { + g->o = vsub(p, vmul(d, gl/l)); + if (!(gl > PRECISION)) return; + } else { + g->o.x = p.x - gl; + g->o.y = p.y; + return; + } + + g->dx = vunturn(d, vdiv(gd, l*gl)); + phGroupPlaceNodes(g); +} + + +void phGroupMerge(PhGroup *a, PhGroup *b) { + if (a == b) return; + + PhNode *afirst = a->first, *bfirst = b->first; + if (afirst && bfirst) { + a->last->next = bfirst; + b->first->prev = a->last; + a->last = b->last; + } else + if (!afirst) { a->first = bfirst; a->last = b->last; } + + b->first = b->last = NULL; + + for(PhNode *n = bfirst; n; n = n->next) { + n->group = a; + if (a->ph != b->ph) phNodeUpdateCell(n); + } + + if (!afirst) { a->cnt = b->cnt; a->go = b->go; } else + if (bfirst) phGroupRecalc(a); + + phGroupRecalc(b); + phGroupUnreg(b); +} + + + diff --git a/projects/jigsaw/phisics.h b/projects/jigsaw/phisics.h new file mode 100644 index 0000000..866f77d --- /dev/null +++ b/projects/jigsaw/phisics.h @@ -0,0 +1,85 @@ +#ifndef PHISICS_H +#define PHISICS_H + + +#include "common.h" + + +typedef struct PhLink PhLink; +typedef struct PhNode PhNode; +typedef struct PhGroup PhGroup; +typedef struct Physics Physics; +typedef PhLink *PhCell; + +struct PhLink { + PhNode *node; + PhCell *cell; + PhLink *prev, *next; +}; + +struct PhNode { + PhGroup *group; + PhNode *prev, *next; + PhLink l[4]; + double r; + Vec p, gp, gk; + size_t mark; // temporary mark + PhNode *chain; // temporary chain +}; + +struct PhGroup { + Physics *ph; + PhGroup *prev, *next; + PhNode *first, *last; + int cnt; + Vec go, o, dx; +}; + +struct Physics { + PhGroup *first, *last; + + double cs; + int hw, hh; + PhCell *cells; + size_t lastMark; + + Vec b0, b1; + int fixangle; +}; + + +void phLinkUnreg(PhLink *l); +void phLinkReg(PhLink *l, PhCell *c); + +void phNodeUnregCell(PhNode *n); +void phNodeRegCell(PhNode *n, PhCell *c, int stride); +void phNodeUpdateCell(PhNode *n); +void phNodeUnreg(PhNode *n); +void phNodeReg(PhNode *n, PhGroup *g); +int phNodeBound(PhNode *n, Vec b0, Vec b1); +int phNodeCollision(PhNode *n); + + +static inline Vec phGroupTrans(PhGroup *g, Vec gp) { return vtrans(vsub(gp, g->go), g->dx, g->o); } +static inline Vec phGroupUntrans(PhGroup *g, Vec p) { return vadd(vuntrans(p, g->dx, g->o), g->go); } + +void phGroupUpdateCells(PhGroup *g); +void phGroupUnreg(PhGroup *g); +void phGroupReg(PhGroup *g, Physics *ph); +void phGroupRecalc(PhGroup *g); +void phGroupPlaceNodes(PhGroup *g); +void phGroupFix(PhGroup *g, int fixangle); +void phGroupMove(PhGroup *g, Vec gp, Vec p, int fixangle); +void phGroupMerge(PhGroup *a, PhGroup *b); + + +void physicsAlloc(Physics *ph, int hw, int hh, double cs); +void physicsFree(Physics *ph); +PhCell* physicsCell(Physics *ph, Vec p); +void physicsClear(Physics *ph); +int physicsUpdate(Physics *ph); + +void physicsDrawDebug(Physics *ph); + + +#endif diff --git a/projects/jigsaw/phisics.node.c b/projects/jigsaw/phisics.node.c new file mode 100644 index 0000000..fcff6c9 --- /dev/null +++ b/projects/jigsaw/phisics.node.c @@ -0,0 +1,126 @@ + +#include "phisics.h" + + + +void phLinkUnreg(PhLink *l) { + if (!l->cell) return; + l->next->prev = l->prev; l->prev->next = l->next; + if (l->next == l) *l->cell = NULL; else + if (*l->cell == l) *l->cell = l->next; + l->cell = NULL; + l->prev = l->next = NULL; +} + + +void phLinkReg(PhLink *l, PhCell *c) { + if (l->cell == c) return; + phLinkUnreg(l); + if (*c) { + l->next = *c; + l->prev = l->next->prev; + l->next->prev = l; + l->prev->next = l; + } else { + *c = l; + l->next = l->prev = l; + } + l->cell = c; +} + + + +void phNodeUnregCell(PhNode *n) + { for(int i = 0; i < 4; ++i) phLinkUnreg(n->l+i); } + + +void phNodeRegCell(PhNode *n, PhCell *c, int stride) { + if (n->l->cell == c) return; + phLinkReg(n->l+0, c); + phLinkReg(n->l+1, c+1); + phLinkReg(n->l+2, c+stride); + phLinkReg(n->l+3, c+stride+1); +} + + +void phNodeUpdateCell(PhNode *n) { + if (n->group && n->group->ph) + phNodeRegCell(n, physicsCell(n->group->ph, n->p), n->group->ph->hw*2); + else + phNodeUnregCell(n); +} + + +void phNodeUnreg(PhNode *n) { + if (!n->group) return; + phNodeUnregCell(n); + *(n->prev ? &n->prev->next : &n->group->first) = n->next; + *(n->next ? &n->next->prev : &n->group->last) = n->prev; + n->prev = n->next = NULL; + n->group = NULL; +} + + +void phNodeReg(PhNode *n, PhGroup *g) { + if (n->group == g) return; + phNodeUnreg(n); + for(int i = 0; i < 4; ++i) n->l[i].node = n; + n->group = g; + n->prev = g->last; + *(n->prev ? &n->prev->next : &g->first) = g->last = n; + phNodeUpdateCell(n); +} + + +int phNodeBound(PhNode *n, Vec b0, Vec b1) { + double d; + int res = 0; + d = n->p.x - b0.x - n->r; if (d < 0) n->p.x -= d, res = 1; + d = b1.x - n->p.x - n->r; if (d < 0) n->p.x += d, res = 1; + d = n->p.y - b0.y - n->r; if (d < 0) n->p.y -= d, res = 1; + d = b1.y - n->p.y - n->r; if (d < 0) n->p.y += d, res = 1; + if (res) phNodeUpdateCell(n); + return res; +} + + +int phNodeCollision(PhNode *n) { + if (!n->group || !n->group->ph) return 0; + PhNode *chain = n; + unsigned int mark = ++n->group->ph->lastMark; + for(int i = 0; i < 4; ++i) { + PhLink *link = n->l+i; + for(PhLink *l = link->next; l && l != link; l = l->next) { + PhNode *m = l->node; + if (m >= n || m->mark == mark) continue; + m->mark = mark; + + if (n->group == m->group) continue; + + double r = n->r + m->r; + Vec d = vsub(m->p, n->p); + double l2 = vlen2(d); + if (!(l2 < r*r)) continue; + + double l = sqrt(l2); + d = vmul(d, (r*1.001 - l)/l/2); + n->p = vsub(n->p, d); + m->p = vadd(m->p, d); + m->chain = chain; + chain = m; + } + } + if (chain == n) return 0; + + int res = 0; + while(chain) { + phNodeUpdateCell(chain); + PhNode *nn = chain->chain; + chain->chain = NULL; + chain = nn; + ++res; + } + return res; +} + + diff --git a/projects/jigsaw/puzzle.c b/projects/jigsaw/puzzle.c new file mode 100644 index 0000000..9dcd04b --- /dev/null +++ b/projects/jigsaw/puzzle.c @@ -0,0 +1,184 @@ + +#include "puzzle.h" + + +void puzzleAlloc(Puzzle *pz, int rows, int cols) { + pz->rows = rows < 1 ? 1 : rows; + pz->cols = cols < 1 ? 1 : cols; + pz->cs = vone(); + pz->hlines = (Vertex**)alloc2d(pz->rows+1, (pz->cols*LN_SEGS+1)*ASIZEOF(**pz->hlines)); + pz->vlines = (Vertex**)alloc2d(pz->cols+1, (pz->rows*LN_SEGS+1)*ASIZEOF(**pz->vlines)); + pz->chunks = (Chunk**)alloc2d(pz->rows, pz->cols*ASIZEOF(**pz->chunks)); + pz->chunksOrder = (Chunk**)alloc(pz->rows*pz->cols*ASIZEOF(*pz->chunksOrder)); + physicsAlloc(&pz->ph, pz->rows*2, pz->cols*2, 1); +} + + +void puzzleFree(Puzzle *pz) { + free(pz->chunksOrder); + free(pz->chunks); + physicsFree(&pz->ph); + tileFree(&pz->tm); + free(pz->hlines); + free(pz->vlines); + memset(pz, 0, sizeof(*pz)); +} + + +void puzzleClearImages(Puzzle *pz) { + for(int r = 0; r < pz->rows; ++r) + for(int c = 0; c < pz->cols; ++c) + pz->chunks[r][c].t = (Tile){}; + tileClear(&pz->tm); +} + + +void puzzleGenLines(Puzzle *pz, double cellw, double cellh, double jitter, double depth) { + puzzleClearImages(pz); + pz->cs.x = maxd(PRECISION, cellw); + pz->cs.y = maxd(PRECISION, cellh); + for(int r = 0; r <= pz->rows; ++r) { + double j = r && r < pz->rows ? jitter : 0.0; + double d = r && r < pz->rows ? depth : 0.0; + lineGenH(pz->hlines[r], pz->cols, r*pz->cs.y, pz->cs.x, pz->cs.y, j, d); + } + for(int c = 0; c <= pz->cols; ++c) { + double j = c && c < pz->cols ? jitter : 0.0; + double d = c && c < pz->cols ? depth : 0.0; + lineGenV(pz->vlines[c], pz->rows, c*pz->cs.x, pz->cs.x, pz->cs.y, j, d); + } + for(int r = 1; r < pz->rows; ++r) + for(int c = 1; c < pz->cols; ++c) + pz->vlines[c][r*LN_SEGS].p = pz->hlines[r][c*LN_SEGS].p; +} + + +void puzzleRecalcGroups(Puzzle *pz) { + pz->groups = 0; + for(PhGroup *g = pz->ph.first; g; g = g->next) ++pz->groups; +} + + +void puzzleGenChunks(Puzzle *pz) { + pz->ph.fixangle = 1; + double nr = mind(pz->cs.x, pz->cs.y)/4; + Vec nd = { pz->cs.x/2 - nr, pz->cs.y/2 - nr }; + + Vec s = vmul( vmulv(vec(pz->cols, pz->rows), pz->cs), 1.5/2 ); + + physicsClear(&pz->ph); + pz->ph.cs = maxd(pz->cs.x, pz->cs.y); + Vec nds[] = { vneg(nd), vnegy(nd), vnegx(nd), nd, vzero() }; + double nrs[] = { nr, nr, nr, nr, 2*nr }; + + for(int r = 0; r < pz->rows; ++r) + for(int c = 0; c < pz->cols; ++c) { + Chunk *a = &pz->chunks[r][c]; + *a = (Chunk){ pz, r, c }; + a->parent = &a->group; + a->flags |= PF_HANDLES; + + Vec o = vmulv(s, vrandTwo()); + Vec go = chunkGroupPos(a); + + for(int i = 0; i < 5; ++i) { + PhNode *n = &a->nodes[i]; + n->gp = vadd(go, nds[i]); + n->r = nrs[i]; + n->p = vadd(o, vrandTwo()); + phNodeReg(n, a->parent); + } + phGroupReg(a->parent, &pz->ph); + phGroupRecalc(a->parent); + phGroupFix(a->parent, !pz->turn); + + pz->chunksOrder[r*pz->cols + c] = a; + } + + int cnt = pz->rows*pz->cols; + for(int i = 0; i < cnt; ++i) { + int j = rand()%(cnt-i) + i; + Chunk *c = pz->chunksOrder[j]; + pz->chunksOrder[j] = pz->chunksOrder[i]; + pz->chunksOrder[i] = c; + } + + puzzleRecalcGroups(pz); +} + + +void puzzleRenderChunks(Puzzle *pz, int size, double m) { + puzzleClearImages(pz); + int cnt = pz->rows*pz->cols; + double s = maxd(pz->cs.x, pz->cs.y) + 2*m; + for(int i = 0; i < cnt; ++i) { + Chunk *c = &pz->chunks[0][i]; + c->t = tileAdd(&pz->tm, size, s); + chunkRender(c); + } +} + + +int puzzleChooseChunks(Puzzle *pz, Vec mouse) { + pz->activeChunks = 0; + Chunk *choosen = NULL; + Chunk *choosenE = NULL; + int cnt = pz->rows*pz->cols; + for(int i = cnt-1; i >= 0; --i) { + Chunk *c = pz->chunksOrder[i]; + Flags f = chunkCheckPoint(c, mouse); + if (f&PCP_BODY) { choosen = c; pz->mousefix = !pz->turn || !(f&PCP_EDGE); } + if (f&PCP_EDGE) choosenE = c; + } + if (!choosen) { choosen = choosenE; pz->mousefix = !pz->turn; } + if (!choosen) return 0; + + pz->gmouse = phGroupUntrans(choosen->parent, mouse); + for(int i = 0; i < cnt; ++i) { + Chunk *c = pz->chunksOrder[i]; + if (c->parent != choosen->parent) continue; + memmove(pz->chunksOrder + pz->activeChunks + 1, pz->chunksOrder + pz->activeChunks, (i - pz->activeChunks)*sizeof(*pz->chunksOrder)); + pz->chunksOrder[pz->activeChunks++] = c; + } + return pz->activeChunks; +} + + +void puzzleReleaseChunks(Puzzle *pz) { + if (!pz->activeChunks) return; + while(pz->activeChunks > 0) { + Chunk *c = pz->chunksOrder[--pz->activeChunks]; + chunkTryMerge(c, -1, 0); + chunkTryMerge(c, 1, 0); + chunkTryMerge(c, 0, -1); + chunkTryMerge(c, 0, 1); + } + puzzleRecalcGroups(pz); +} + + +void puzzleUpdate(Puzzle *pz, Vec hs, Vec mouse) { + Vec mmin = vdiv(pz->cs, 2); + Vec bmax = vmulv(vec(pz->ph.hw, pz->ph.hh), pz->cs); + Vec b1 = vec( maxd(mmin.x, mind(hs.x, bmax.x)), + maxd(mmin.y, mind(hs.y, bmax.y)) ); + + if ( (b1.x != pz->ph.b1.x || b1.y != pz->ph.b1.y) + && b1.x > PRECISION && pz->ph.b1.x > PRECISION + && b1.y > PRECISION && pz->ph.b1.y > PRECISION ) + { + Vec k = vdivv(b1, pz->ph.b1); + for(PhGroup *g = pz->ph.first; g; g = g->next) { + g->o = vmulv(g->o, k); + phGroupPlaceNodes(g); + } + } + + pz->ph.b1 = b1; + pz->ph.b0 = vneg(b1); + + physicsUpdate(&pz->ph); + if (pz->activeChunks) + phGroupMove(pz->chunksOrder[0]->parent, pz->gmouse, mouse, pz->mousefix); +} + diff --git a/projects/jigsaw/puzzle.chunk.c b/projects/jigsaw/puzzle.chunk.c new file mode 100644 index 0000000..ba98354 --- /dev/null +++ b/projects/jigsaw/puzzle.chunk.c @@ -0,0 +1,231 @@ + +#include "puzzle.h" + + + +unsigned int chunkCalcEdges(Chunk *c) { + unsigned int edges = 0; + if (c->c <= 0 || c->pz->chunks[c->r][c->c-1].parent != c->parent) edges |= PF_BORDER_L; + if (c->c+1 >= c->pz->cols || c->pz->chunks[c->r][c->c+1].parent != c->parent) edges |= PF_BORDER_R; + if (c->r <= 0 || c->pz->chunks[c->r-1][c->c].parent != c->parent) edges |= PF_BORDER_T; + if (c->r+1 >= c->pz->rows || c->pz->chunks[c->r+1][c->c].parent != c->parent) edges |= PF_BORDER_B; + return edges; +} + + +void chunkDrawBase(Chunk *c, unsigned int edges) { + int levels = 4; + Vec gp = chunkGroupPos(c); + + saveState(); + noFill(); + strokeWidth( mind(c->pz->cs.x, c->pz->cs.y)*0.025 ); + translate(-gp.x, -gp.y); + if (edges & PF_BORDER_T) lineDraw(c->pz->hlines[c->r + 0] + (c->c + 0)*LN_SEGS, 1, levels); + if (edges & PF_BORDER_R) lineDraw(c->pz->vlines[c->c + 1] + (c->r + 0)*LN_SEGS, 1, levels); + if (edges & PF_BORDER_B) lineDraw(c->pz->hlines[c->r + 1] + (c->c + 1)*LN_SEGS, -1, levels); + if (edges & PF_BORDER_L) lineDraw(c->pz->vlines[c->c + 0] + (c->r + 1)*LN_SEGS, -1, levels); + restoreState(); + + saveState(); + translate(-gp.x, -gp.y); + /*strokeWidth( mind(c->pz->cs.x, c->pz->cs.y)*0.1 ); + stroke(COLOR_WHITE); + strokeTexture(c->pz->image, 0, 0, c->pz->cols*c->pz->cs.x, c->pz->rows*c->pz->cs.x, FALSE); + if (edges & PF_BORDER_T) lineDraw(c->pz->hlines[c->r + 0] + (c->c + 0)*LN_SEGS, 1, levels); + if (edges & PF_BORDER_R) lineDraw(c->pz->vlines[c->c + 1] + (c->r + 0)*LN_SEGS, 1, levels); + if (edges & PF_BORDER_B) lineDraw(c->pz->hlines[c->r + 1] + (c->c + 1)*LN_SEGS, -1, levels); + if (edges & PF_BORDER_L) lineDraw(c->pz->vlines[c->c + 0] + (c->r + 1)*LN_SEGS, -1, levels);*/ + noStroke(); + fill(COLOR_WHITE); + fillTexture(c->pz->image, 0, 0, c->pz->cols*c->pz->cs.x, c->pz->rows*c->pz->cs.y, FALSE); + linePut(c->pz->hlines[c->r + 0] + (c->c + 0)*LN_SEGS, 1, levels); + linePut(c->pz->vlines[c->c + 1] + (c->r + 0)*LN_SEGS, 1, levels); + linePut(c->pz->hlines[c->r + 1] + (c->c + 1)*LN_SEGS, -1, levels); + linePut(c->pz->vlines[c->c + 0] + (c->r + 1)*LN_SEGS, -1, levels); + closePath(); + restoreState(); +} + + +void chunkDraw(Chunk *c) { + saveState(); + Vec p = chunkWorldPos(c); + translate(p.x, p.y); + rotate(vangled(c->parent->dx)); + if (c->t.tex && c->t.ts > 1e-5 && (c->flags & PF_RENDERED)) { + translate(-c->t.s/2, -c->t.s/2); + noStroke(); + double k = c->t.s/c->t.ts; + fillTexture(c->t.tex, -c->t.tp.x*k, -c->t.tp.y*k, k, k, FALSE); + rect(0, 0, c->t.s, c->t.s); + } else { + chunkDrawBase(c, chunkCalcEdges(c)); + } + restoreState(); +} + + +void chunkDrawDebug(Chunk *c) { + saveState(); + Vec p = chunkWorldPos(c); + translate(p.x, p.y); + rotate(vangled(c->parent->dx)); + stroke(randColorPtr(c->parent)); + Vec hs = vdiv(c->pz->cs, 2); + if (c->flags & PF_HANDLE_L) line(-hs.x, -hs.y, -hs.x, hs.y); + if (c->flags & PF_HANDLE_R) line( hs.x, -hs.y, hs.x, hs.y); + if (c->flags & PF_HANDLE_T) line(-hs.x, -hs.y, hs.x, -hs.y); + if (c->flags & PF_HANDLE_B) line(-hs.x, hs.y, hs.x, hs.y); + restoreState(); +} + + +void chunkRender(Chunk *c) { + if (!c->t.fb || !(c->t.ts > 1e-5)) return; + + unsigned int edges = PF_RENDERED | chunkCalcEdges(c); + if ((c->flags & PF_RENDERING) == edges) return; + + saveState(); + target(c->t.fb); + resetStateEx(STATE_TRANSFORM); + + int ix = (int)round(c->t.tp.x*TM_SIZE); + int iy = (int)round(c->t.tp.y*TM_SIZE); + int s = (int)round(c->t.ts*TM_SIZE); + background(COLOR_TRANSPARENT); + glEnable(GL_SCISSOR_TEST); + glScissor(ix, iy, s, s); + clear(); + glScissor(0, 0, TM_SIZE, TM_SIZE); + glDisable(GL_SCISSOR_TEST); + + double k = c->t.s/c->t.ts; + zoom(TM_SIZE/k); + translate(c->t.tp.x*k + c->t.s/2, c->t.tp.y*k + c->t.s/2); + cliprect(-c->t.s/2, -c->t.s/2, c->t.s, c->t.s); + chunkDrawBase(c, edges); + c->flags |= edges; + + restoreState(); +} + +#include +void chunkMerge(Chunk *a, Chunk *b) { + if (a->parent == b->parent) return; + Puzzle *pz = a->pz; + PhGroup *pa = a->parent, *pb = b->parent; + phGroupMerge(pa, pb); + + for(int r = 0; r < pz->rows; ++r) + for(int c = 0; c < pz->cols; ++c) { + Chunk *chunk = &pz->chunks[r][c]; + if (chunk->parent == pa || chunk->parent == pb) chunk->flags &= ~PF_HANDLES; + if (chunk->parent != pb) continue; + chunk->parent = pa; + chunkRender(chunk); + if (c > 0 ) chunkRender(&pz->chunks[r][c-1]); + if (c+1 < pz->cols) chunkRender(&pz->chunks[r][c+1]); + if (r > 0 ) chunkRender(&pz->chunks[r-1][c]); + if (r+1 < pz->rows) chunkRender(&pz->chunks[r+1][c]); + } + + int c0 = pz->cols-1, c1 = 0; + for(int r = 0; r < pz->rows; ++r) { + Chunk *first = NULL, *last = NULL; + for(int c = 0; c < pz->cols; ++c) { + Chunk *chunk = &pz->chunks[r][c]; + if (chunk->parent != pa) continue; + if (c <= c0) { first = chunk; c0 = c; } + if (c >= c1) { last = chunk; c1 = c; } + } + if (first) first->flags |= PF_HANDLE_L; + if (last) last->flags |= PF_HANDLE_R; + } + + c0 = pz->cols-1, c1 = 0; + for(int r = pz->rows-1; r >= 0; --r) { + Chunk *first = NULL, *last = NULL; + for(int c = 0; c < pz->cols; ++c) { + Chunk *chunk = &pz->chunks[r][c]; + if (chunk->parent != pa) continue; + if (c <= c0) { first = chunk; c0 = c; } + if (c >= c1) { last = chunk; c1 = c; } + } + if (first) first->flags |= PF_HANDLE_L; + if (last) last->flags |= PF_HANDLE_R; + } + + int r0 = pz->rows-1, r1 = 0; + for(int c = 0; c < pz->cols; ++c) { + Chunk *first = NULL, *last = NULL; + for(int r = 0; r < pz->rows; ++r) { + Chunk *chunk = &pz->chunks[r][c]; + if (chunk->parent != pa) continue; + if (r <= r0) { first = chunk; r0 = r; } + if (r >= r1) { last = chunk; r1 = r; } + } + if (first) first->flags |= PF_HANDLE_T; + if (last) last->flags |= PF_HANDLE_B; + } + + r0 = pz->rows-1, r1 = 0; + for(int c = pz->cols-1; c >= 0; --c) { + Chunk *first = NULL, *last = NULL; + for(int r = 0; r < pz->rows; ++r) { + Chunk *chunk = &pz->chunks[r][c]; + if (chunk->parent != pa) continue; + if (r <= r0) { first = chunk; r0 = r; } + if (r >= r1) { last = chunk; r1 = r; } + } + if (first) first->flags |= PF_HANDLE_T; + if (last) last->flags |= PF_HANDLE_B; + } +} + + +void chunkTryMerge(Chunk *a, int dr, int dc) { + int r = a->r + dr; + int c = a->c + dc; + Puzzle *pz = a->pz; + if (r < 0 || c < 0 || r >= pz->rows || c >= pz->cols) return; + + Chunk *b = &pz->chunks[r][c]; + if (a->parent == b->parent) return; + + Vec gp = vadd( chunkGroupPos(a), vdiv(vec(dc,dr), 2)); + Vec pa = phGroupTrans(a->parent, gp); + Vec pb = phGroupTrans(b->parent, gp); + double d2 = vdist2(pb, pa); + + if (d2 < pz->cs.x*pz->cs.y/16) + chunkMerge(a, b); +} + + +Flags chunkCheckPoint(Chunk *c, Vec p) { + Vec hs = vdiv(c->pz->cs, 2); + double hbs = mind(hs.x, hs.y); + Vec hs0 = vsub(hs, vecxy(hbs*0.25)); + Vec hs1 = vadd(hs, vecxy(hbs*0.50)); + Vec gp = vsub(phGroupUntrans(c->parent, p), chunkGroupPos(c)); + int l = c->flags & (gp.x < 0 ? PF_HANDLE_L : PF_HANDLE_R); + int t = c->flags & (gp.y < 0 ? PF_HANDLE_T : PF_HANDLE_B); + gp = vabs(gp); + + int inx = gp.x < hs0.x; + int iny = gp.y < hs0.y; + if (!(gp.x < hs1.x) || !(gp.y < hs1.y)) return 0; + if (inx && iny) return PCP_BODY; + + Flags res = 0; + if (gp.x < hs.x && gp.y < hs.y) res |= PCP_BODY; + double r = hs1.x - hs.x; + if ( (l && !inx && gp.y < hs.y) + || (t && !iny && gp.x < hs.x) + || (l && t && !inx && !iny && vdist2(gp, hs) < r*r) ) res |= PCP_EDGE; + return res; +} + + diff --git a/projects/jigsaw/puzzle.h b/projects/jigsaw/puzzle.h new file mode 100644 index 0000000..b8090cd --- /dev/null +++ b/projects/jigsaw/puzzle.h @@ -0,0 +1,87 @@ +#ifndef PUZZLE_H +#define PUZZLE_H + + +#include "line.h" +#include "tile.h" +#include "phisics.h" + + +#define PF_RENDERED (1 << 0) +#define PF_BORDER_L (1 << 1) +#define PF_BORDER_R (1 << 2) +#define PF_BORDER_T (1 << 3) +#define PF_BORDER_B (1 << 4) +#define PF_HANDLE_L (1 << 5) +#define PF_HANDLE_R (1 << 6) +#define PF_HANDLE_T (1 << 7) +#define PF_HANDLE_B (1 << 8) + +#define PF_BORDERS (PF_BORDER_L|PF_BORDER_R|PF_BORDER_T|PF_BORDER_B) +#define PF_RENDERING (PF_BORDERS|PF_RENDERED) +#define PF_HANDLES (PF_HANDLE_L|PF_HANDLE_R|PF_HANDLE_T|PF_HANDLE_B) + +#define PCP_BODY 1 +#define PCP_EDGE 2 + +typedef struct Puzzle Puzzle; + +typedef struct { + Puzzle *pz; + int r, c; + Tile t; + PhGroup *parent; + PhGroup group; + PhNode nodes[5]; + Flags flags; +} Chunk; + +struct Puzzle { + int rows, cols, turn; + Vertex **hlines; + Vertex **vlines; + + Physics ph; + Chunk **chunks; + int groups; + + Animation image; + TileMap tm; + Vec cs; + + Chunk **chunksOrder; + int activeChunks; + Vec gmouse; + int mousefix; +}; + + +static inline Vec chunkGroupPos(Chunk *c) + { return vmulv( vadd(vec(c->c, c->r), vecxy(0.5)), c->pz->cs ); } +static inline Vec chunkWorldPos(Chunk *c) + { return phGroupTrans(c->parent, chunkGroupPos(c)); } + +unsigned int chunkCalcEdges(Chunk *c); +void chunkDrawBase(Chunk *c, unsigned int edges); +void chunkDraw(Chunk *c); +void chunkDrawDebug(Chunk *c); +void chunkRender(Chunk *c); +void chunkMerge(Chunk *a, Chunk *b); +void chunkTryMerge(Chunk *a, int dr, int dc); +Flags chunkCheckPoint(Chunk *c, Vec p); +int chunkCheckEdge(Chunk *c, Vec p); + + +void puzzleAlloc(Puzzle *pz, int rows, int cols); +void puzzleFree(Puzzle *pz); +void puzzleClearImages(Puzzle *pz); +void puzzleGenLines(Puzzle *pz, double cellw, double cellh, double jitter, double depth); +void puzzleRecalcGroups(Puzzle *pz); +void puzzleGenChunks(Puzzle *pz); +void puzzleRenderChunks(Puzzle *pz, int size, double m); +int puzzleChooseChunks(Puzzle *pz, Vec mouse); +void puzzleReleaseChunks(Puzzle *pz); +void puzzleUpdate(Puzzle *pz, Vec hs, Vec mouse); + + +#endif diff --git a/projects/jigsaw/tile.c b/projects/jigsaw/tile.c new file mode 100644 index 0000000..f79f2c1 --- /dev/null +++ b/projects/jigsaw/tile.c @@ -0,0 +1,41 @@ + + +#include "tile.h" + + + +Tile tileAdd(TileMap *tm, int ts, double s) { + if (ts <= 0) return (Tile){}; + if (ts > TM_SIZE) ts = TM_SIZE; + if (ts > TM_SIZE - tm->x) { tm->x = 0; tm->y0 = tm->y1; } + if (ts > TM_SIZE - tm->y0) { + TileMap *ntm = (TileMap*)alloc(sizeof(*ntm)); + *ntm = *tm; + memset(tm, 0, sizeof(*tm)); + tm->next = ntm; + } + + if (!tm->fb) tm->fb = createFramebufferEx(TM_SIZE, TM_SIZE, NULL, FALSE, FALSE, TRUE); + if (!tm->tex) tm->tex = createAnimationFromFramebuffer(tm->fb); + + Tile t = { {tm->x/(double)TM_SIZE, tm->y0/(double)TM_SIZE}, ts/(double)TM_SIZE, s, tm->fb, tm->tex }; + tm->x += ts; + if (tm->y1 < tm->y0 + ts) tm->y1 = tm->y0 + ts; + return t; +} + + +void tileClear(TileMap *tm) { + if (tm->next) tileClear(tm->next); + tm->x = tm->y0 = tm->y1 = 0; +} + + +void tileFree(TileMap *tm) { + if (tm->next) { tileFree(tm->next); free(tm->next); } + if (tm->tex) animationDestroy(tm->tex); + if (tm->fb) framebufferDestroy(tm->fb); + memset(tm, 0, sizeof(*tm)); +} + + diff --git a/projects/jigsaw/tile.h b/projects/jigsaw/tile.h new file mode 100644 index 0000000..7dcb4c3 --- /dev/null +++ b/projects/jigsaw/tile.h @@ -0,0 +1,31 @@ +#ifndef TILE_H +#define TILE_H + + +#include "common.h" + + +#define TM_SIZE 1024 + + +typedef struct { + Vec tp; + double ts, s; + Framebuffer fb; + Animation tex; +} Tile; + +typedef struct TileMap { + Framebuffer fb; + Animation tex; + int x, y0, y1, err; + struct TileMap *next; +} TileMap; + + +Tile tileAdd(TileMap *tm, int ts, double s); +void tileClear(TileMap *tm); +void tileFree(TileMap *tm); + + +#endif