1 /**
2   The main texit module.
3 */
4 module texit;
5 
6 public import std.datetime.systime, arsd.simpledisplay, arsd.simpleaudio, arsd.vorbis;
7 import arsd.png;
8 
9 /// A single tile in the world
10 struct Tile {
11   float[3] bg = [0, 0, 0];   /// bg color
12   float[3] fg = [1, 1, 1];   /// fg color
13   char ch = ' '; /// character to draw
14 
15   bool opEquals(Tile t) {
16     return bg == t.bg && fg == t.fg && ch == t.ch;
17   }
18 }
19 
20 private Image crop(Image img, int x, int y, int w, int h) {
21   Image ret = new Image(w, h);
22   for(int i = 0; i < w; i++)
23     for(int j = 0; j < h; j++)
24       ret.setPixel(i, j, img.getPixel(x+i, y+j));
25   return ret;
26 }
27 
28 /// Returns a charmap given a directory and a char size
29 bool[charSize][charSize][256] loadCharmap(int charSize)(string dir) {
30   Image charmap = Image.fromMemoryImage(dir.readPng);
31   bool[charSize][charSize][256] chars;
32   for(int i = 0; i < 16; i++) {
33     for(int j = 0; j < 16; j++) {
34       Image tmp = charmap.crop(i*charSize, j*charSize, charSize, charSize);
35       for(int k = 0; k < tmp.width; k++) {
36         for(int l = 0; l < tmp.height; l++) {
37           chars[j*16+i][k][l] = tmp.getPixel(k, l).r != 0;
38         }
39       }
40       destroy(tmp);
41     }
42   }
43   destroy(charmap);
44   return chars;
45 }
46 
47 /// Ease a value given an easing from https://easings.net/ and also easeLinear (which returns the given value)
48 pure nothrow float ease(string easing)(float x) {
49   import std.math.trigonometry : sin, cos;
50   import std.math.algebraic    : sqrt;
51   import std.math.constants    : PI;
52   enum float c1 = 1.70158;
53   enum float c2 = c1*1.525;
54   enum float c3 = c1+1;
55   enum float n1 = 7.5625;
56   enum float d1 = 2.75;
57   static if(easing == "easeLinear")
58     return x;
59   else static if(easing == "easeInSine")
60     return 1-cos((x*PI)/2);
61   else static if(easing == "easeOutSine")
62     return sin((x*PI)/2);
63   else static if(easing == "easeInOutSine")
64     return -(cos(PI*x)-1)/2;
65   else static if(easing == "easeInCubic")
66     return x^^3;
67   else static if(easing == "easeOutCubic")
68     return 1-(1-x)^^3;
69   else static if(easing == "easeInOutCubic")
70     return x < 0.5 
71       ? 4*x^^3
72       : 1-((-2*x+2)^^3)/2;
73   else static if(easing == "easeInQuint")
74     return x^^5;
75   else static if(easing == "easeOutQuint")
76     return 1-(1-x)^^5;
77   else static if(easing == "easeInOutQuint")
78     return x < 0.5 
79       ? 16*x^^5
80       : 1-((-2*x+2)^^5)/2;
81   else static if(easing == "easeInCirc")
82     return 1-sqrt(1-x^^2);
83   else static if(easing == "easeOutCirc")
84     return sqrt(1-(x-1)^^2);
85   else static if(easing == "easeInOutCirc")
86     return x < 0.5
87       ? (1-sqrt(1-(2*x)^^2))/2
88       : (sqrt(1-(-2*x+1)^^2)+1)/2;
89   else static if(easing == "easeInQuad")
90     return x^^2;
91   else static if(easing == "easeOutQuad")
92     return 1-(1-x)^^2;
93   else static if(easing == "easeInOutQuad")
94     return x < 0.5
95       ? 2*x^^2
96       : 1-((-2*x+2)^^2)/2;
97   else static if(easing == "easeInQuart")
98     return x^^4;
99   else static if(easing == "easeOutQuart")
100     return 1-(1-x)^^4;
101   else static if(easing == "easeInOutQuart")
102     return x < 0.5
103       ? 8*x^^4
104       : 1-((-2*x+2)^^4)/2;
105   else static if(easing == "easeInExpo")
106     return x == 0
107       ? 0
108       : 2^^(10*x-10);
109   else static if(easing == "easeOutExpo")
110     return x == 1
111       ? 1
112       : 1-2^^(-10*x);
113   else static if(easing == "easeInOutExpo")
114     return x == 0
115       ? 0
116       : x == 1
117         ? 1
118         : x < 0.5
119           ? 2^^(20*x-10)/2
120           : (2-2^^(-20*x+10))/2;
121   else static if(easing == "easeInBack")
122     return (c3*x^^3)-(c1*x^^2);
123   else static if(easing == "easeOutBack")
124     return 1+c3*(x-1)^^3+c1*(x-1)^^2;
125   else static if(easing == "easeInOutBack")
126     return x < 0.5
127       ? ((2*x)^^2*((c2+1)*2*x-c2))/2
128       : ((2*x-2)^^2*((c2+1)*(x*2-2)+c2)+2)/2;
129   else static if(easing == "easeInBounce")
130     return 1-ease!"easeOutBounce"(1-x);
131   else static if(easing == "easeOutBounce") {
132     if(x < 1/d1)
133       return n1*x^^2;
134     else if(x < 2/d1)
135       return n1*(x -= 1.5/d1)*x+0.75;
136     else if(x < 2.5/d1)
137       return n1*(x -= 2.25/d1)*x+0.9375;
138     else
139       return n1*(x-=2.625/d1)*x+0.984375;
140   }
141   else static if(easing == "easeInOutBounce")
142     return x < 0.5
143       ? (1-ease!"easeOutBounce"(1-2*x)/2)
144       : (1+ease!"easeOutBounce"(2*x-1))/2;
145   else
146     static assert(false, "Unknown easing "~easing);
147 }
148 
149 alias Easing = pure float function(float);
150 
151 /// Takes an easing and returns a function pointer to it
152 Easing easing(string s)() {
153   return &(ease!s);
154 }
155 
156 /// Simple vector struct
157 struct Vector {
158   float x = 0;
159   float y = 0;
160   float z = 0;
161 }
162 
163 Vector translation; /// How much to translate the screen by
164 float zoom = 1;     /// How much to zoom in/out
165 
166 // struct TexitImage {
167 //   int width;
168 //   int height;
169 //   ubyte[] rgbaBytes;
170 // }
171 
172 // TexitImage loadPNG(string dir) {
173 //   Image i = Image.fromMemoryImage(readPng(dir));
174 //   scope(exit)
175 //     destroy(i);
176 //   return TexitImage(i.width, i.height, i.getRgbaBytes);
177 // }
178 
179 /// The main texit declaration
180 mixin template Texit(string charmap, 
181     int charSize, float scale,
182     int worldWidth, int worldHeight, 
183     int width, int height, 
184     string title) {
185   // some constants so the user can access them
186   enum WIDTH = width;
187   enum HEIGHT = height;
188   enum WORLD_WIDTH = worldWidth;
189   alias WW = WORLD_WIDTH;
190   enum WORLD_HEIGHT = worldHeight;
191   alias WH = WORLD_HEIGHT;
192   SimpleWindow window;
193   Tile[worldHeight][worldWidth] world; /// The world
194   bool[charSize][charSize][256] chars; /// Bitmap of each character
195   SysTime start; /// When the program was started
196   AudioOutputThread* aot;
197   float offset = 0; /// Offset to start at
198 
199   /// Represents single thing that can appear or happen on the screen
200   abstract class Event {
201     float start; /// When the event should appear
202     float end; /// When the event should disappear (set to Infinity if the event should always be active)
203     bool triggered; /// Whether this event has been triggered or not yet
204     struct TileChange {
205       Point pos;
206       Tile prev;
207     }
208     TileChange[] changedTiles; /// Tiles changed by this event
209     this(float start, float end) {
210       this.start = start;
211       this.end = end;
212     }
213 
214     /// Called when the event triggers
215     void enable() {}
216 
217     /// Called on the last frame of the event
218     void disable() {}
219 
220     /** Called every frame when the event is active. 
221     
222     `rel` is a value between 0 and 1, 0 being the first frame the event is visible, 1 being the last
223 
224     `abs` is the amount of seconds since enable() has been called.
225 
226     */
227     void time(float rel, float abs) {}
228 
229     /// Changes the tile at (x, y) to the given tile
230     void changeTile(Point p, Tile t) {
231       Tile wt = world[p.x][p.y];
232       if(t == wt)
233         return; // no change needs to be done
234       changedTiles ~= TileChange(p, wt);
235       world[p.x][p.y] = t;
236     }
237 
238     /// Undoes all changed tiles
239     void undoChanges() {
240       foreach(p; changedTiles)
241         world[p.pos.x][p.pos.y] = Tile([0, 0, 0], [0, 0, 0], ' ');
242       // foreach(p; changedTiles)
243       //   world[p.pos.x][p.pos.y] = p.prev;
244     }
245   }
246 
247   Event[] events; /// List of events queued
248 
249   /// Queues an event
250   void queue(Event e) {
251     if(offset >= e.end) {
252       e.disable();
253       return;
254     }
255     if(offset >= e.start) {
256       e.enable();
257       e.triggered = true;
258     }
259     events ~= e;
260   }
261 
262   /// Plays OGG audio
263   void audio(string path) {
264     auto controller = aot.playOgg(path);
265     controller.seek(offset);
266   }
267 
268   /// Puts text onto the screen
269   void puts(bool hasEvent)(Event e, int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
270     int sx = x;
271     foreach(c; text) {
272 			switch(c) {
273 				case '\n':
274           y++;
275           x = sx;
276           break;
277         case '\r':
278           break; // grrr windows
279         default:
280           if(x < 0 || y < 0 || x >= worldWidth || y >= worldHeight)
281             continue;
282           static if(hasEvent) {
283             if(replace || world[x][y].ch == ' ')
284               e.changeTile(Point(x, y), Tile(bg, fg, c));
285           } else {
286             if(replace || world[x][y].ch == ' ')
287               world[x][y] = Tile(bg, fg, c);
288           }
289           x++;
290           break;
291 			}
292     }
293   }
294 
295   /// Puts text with an event
296   void puts(Event e, int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
297     puts!true(e, x, y, bg, fg, text, replace);
298   }
299 
300   /// Puts text without an event
301   void puts(int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
302     puts!false(null, x, y, bg, fg, text, replace);
303   }
304 
305   /// Simple event to place text onto the screen at a given time
306   class TextEvent : Event {
307     string text;
308     float[3] fg = [1, 1, 1];
309     float[3] bg = [0, 0, 0];
310     Point pos;
311     bool replace;
312     this(float start, float end, Point pos, float[3] fg, float[3] bg, string text, bool replace = true) {
313       super(start, end);
314       this.text = text;
315       this.fg = fg;
316       this.bg = bg;
317       this.pos = pos;
318       this.replace = replace;
319     }
320 
321     this(float start, float end, Point pos, float[3] fg, string text, bool replace = true) {
322       super(start, end);
323       this.text = text;
324       this.fg = fg;
325       this.pos = pos;
326       this.replace = replace;
327     }
328 
329     this(float start, float end, Point pos, string text, bool replace = true) {
330       super(start, end);
331       this.pos = pos;
332       this.text = text;
333       this.replace = replace;
334     }
335 
336     override void enable() {
337       puts(this, pos.x, pos.y, bg, fg, text, replace);
338     }
339 
340     override void disable() {
341       undoChanges();
342     }
343   }
344 
345   /// Types text onto the screen. Doesn't play well with easings that go backwards (easeBack, easeBounce)
346   class TypeTextEvent : TextEvent {
347     Easing ease;          /// Easing to use when typing the text. Default: `easeLinear`
348     float typingTime = 1; /// Amount of time (seconds) to type the text
349     this(float start, float end, Point pos, float[3] fg, float[3] bg, string text, Easing e = easing!"easeLinear", float typingTime = 0.5) {
350       super(start, end, pos, fg, bg, text);
351       ease = e;
352       this.typingTime = typingTime;
353     }
354 
355     this(float start, float end, Point pos, string text, Easing e = easing!"easeLinear", float typingTime = 0.5) {
356       super(start, end, pos, text);
357       ease = e;
358       this.typingTime = typingTime;
359     }
360 
361     override void enable() {}
362     override void time(float rel, float abs) {
363       float dif = abs-start;
364       if(dif > typingTime)
365         return;
366       float t = ease(dif/typingTime);
367       import std.math : ceil;
368       int idx = cast(int)ceil(t*text.length);
369       puts(this, pos.x, pos.y, bg, fg, text[0..idx], replace);
370     }
371   }
372 
373   /// Flashing text.
374   class FlashingTextEvent : TextEvent {
375     float flashPeriod = 0.5; /// Period of flashing (in seconds)
376     float[3] fg2; /// Second foreground color
377     float[3] bg2; /// Second background color
378     this(float start, float end, Point pos, float[3] fg, float[3] bg, float[3] fg2, float[3] bg2, string text, float flashPeriod = 0.5) {
379       super(start, end, pos, fg, bg, text);
380       this.fg2 = fg2;
381       this.bg2 = bg2;
382       this.flashPeriod = flashPeriod;
383     }
384 
385     override void time(float rel, float abs) {
386       float dif = abs-start;
387       if(dif%(flashPeriod*2) < flashPeriod)
388         puts(pos.x, pos.y, bg, fg, text, replace);
389       else
390         puts(pos.x, pos.y, bg2, fg2, text, replace);
391     }
392   }
393 
394   /// Creates a box using the +, -, and | characters
395   class BoxEvent : Event {
396     Point tl; /// Top left
397     Point br; /// Bottom right
398     float[3] fg = [1, 1, 1]; /// Foreground color
399     float[3] bg = [0, 0, 0]; /// Background color
400     this(float start, float end, Point topleft, Point bottomright, float[3] fg, float[3] bg) {
401       super(start, end);
402       this.tl = topleft;
403       this.br = bottomright;
404       this.fg = fg;
405       this.bg = bg;
406     }
407 
408     this(float start, float end, Point topleft, Point bottomright, float[3] fg) {
409       super(start, end);
410       this.tl = topleft;
411       this.br = bottomright;
412       this.fg = fg;
413       this.bg = bg;
414     }
415 
416     override void enable() {
417       string rep(string s, int n) {
418         import std.array : appender;
419         auto ap = appender!string;
420         for(int i = 0; i < n; i++)
421           ap ~= s;
422         return ap[];
423       }
424       import std.stdio;
425       // writeln(tl.x+1, " ", tl.y, " ", bg, " ", fg, " ", rep("-", br.x-tl.x-2));
426       puts(this, tl.x+1, tl.y, bg, fg, rep("-", br.x-tl.x-1), true);
427       puts(this, tl.x+1, br.y, bg, fg, rep("-", br.x-tl.x-1), true);
428       puts(this, tl.x, tl.y+1, bg, fg, rep("|\n", br.y-tl.y-1), true);
429       puts(this, br.x, tl.y+1, bg, fg, rep("|\n", br.y-tl.y-1), true);
430       puts(this, tl.x, tl.y, bg, fg, "+", true);
431       puts(this, tl.x, br.y, bg, fg, "+", true);
432       puts(this, br.x, tl.y, bg, fg, "+", true);
433       puts(this, br.x, br.y, bg, fg, "+", true);
434     }
435 
436     override void disable() {
437       undoChanges();
438     }
439   }
440 
441   float mapBetween(float x, float min0, float max0, float min1, float max1) {
442     return (x-min0) / (max0-min0) * (max1-min1) + min1;
443   }
444 
445   /// Translates the screen from an origin to a destination over an amount of time
446   class TranslationEvent : Event {
447     Easing ease;
448     Vector origin;
449     Vector dest;
450 
451     static Vector prevDest; /// The last constructed TranslationEvent's destination
452 
453     this(float start, float end, Vector origin, Vector dest, Easing e = easing!"easeLinear") {
454       super(start, end);
455       ease = e;
456       this.origin = origin;
457       this.dest = dest;
458       prevDest = dest;
459     }
460 
461     /// Origin is assumed to be `prevDest`
462     this(float start, float end, Vector dest, Easing e = easing!"easeLinear") {
463       super(start, end);
464       ease = e;
465       this.origin = prevDest;
466       this.dest = dest;
467       prevDest = dest;
468     }
469 
470     override void enable() {
471       translation = origin;
472     }
473 
474     override void disable() {
475       translation = dest;
476     }
477 
478     override void time(float rel, float abs) {
479       float eased = ease(rel);
480       translation.x = mapBetween(eased, 0, 1, origin.x, dest.x);
481       translation.y = mapBetween(eased, 0, 1, origin.y, dest.y);
482       translation.z = mapBetween(eased, 0, 1, origin.z, dest.z);
483     }
484   }
485 
486   /// Changes zoom level by one value to another over an amount of time
487   class ZoomEvent : Event {
488     Easing ease;
489     float first;
490     float second;
491 
492     static float prevSecond; /// Second of the last constructed ZoomEvent
493 
494     /// 
495     this(float start, float end, float first, float second, Easing e = easing!"easeLinear") {
496       super(start, end);
497       this.first = first;
498       this.second = second;
499       prevSecond = second;
500       ease = e;
501     }
502     
503     /// First is assumed to be prevSecond
504     this(float start, float end, float second, Easing e = easing!"easeLinear") {
505       super(start, end);
506       this.first = prevSecond;
507       this.second = second;
508       prevSecond = second;
509       ease = e;
510     }
511 
512     override void enable() {
513       zoom = first;
514     }
515 
516     override void disable() {
517       zoom = second;
518     }
519 
520     override void time(float rel, float abs) {
521       float eased = ease(rel);
522       zoom = mapBetween(eased, 0, 1, first, second);
523     }
524   }
525 
526   // TexitImage img;
527   uint[1000] textures;
528 
529   void main() {
530     // init audio thread
531     AudioOutputThread aot_ = AudioOutputThread(true);
532     aot = &aot_;
533     // img = loadPNG("dman.png");
534     // init translation
535     translation = Vector(width/4, height/4);
536     // init window
537     window = new SimpleWindow(cast(int)(width*charSize*scale), cast(int)(height*charSize*scale), title, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible);
538     window.redrawOpenGlScene = delegate() {
539       glLoadIdentity();
540       glOrtho(-width*charSize*(zoom/2), width*charSize*(zoom/2), height*charSize*(zoom/2), -height*charSize*(zoom/2), -1.0f, 1.0f);
541       enum css = charSize*scale;
542       glTranslatef(-translation.x*css, -translation.y*css, -translation.z*css);
543       glBegin(GL_QUADS);
544       // draw a giant black rectangle
545       glColor3f(0, 0, 0);
546       glVertex2f( -worldWidth*css,  -worldHeight*css);
547       glVertex2f(2*worldWidth*css,  -worldHeight*css);
548       glVertex2f(2*worldWidth*css, 2*worldHeight*css);
549       glVertex2f( -worldWidth*css, 2*worldHeight*css);
550       // render characters
551       for(int i = 0; i < worldWidth; i++) {
552         for(int j = 0; j < worldHeight; j++) {
553           auto tile = world[i][j];
554           auto ch = chars[tile.ch];
555           float x = i*css;
556           float y = j*css;
557           glColor3f(tile.bg[0], tile.bg[1], tile.bg[2]);
558           glVertex2f(x,     y);
559           glVertex2f(x+css, y);
560           glVertex2f(x+css, y+css);
561           glVertex2f(x,     y+css);
562           glColor3f(tile.fg[0], tile.fg[1], tile.fg[2]);
563           if(tile.ch == ' ')
564             continue; // no point to draw spaces, they're empty
565           for(int k = 0; k < charSize; k++) {
566             for(int l = 0; l < charSize; l++) {
567               if(!ch[k][l])
568                 continue;
569               float ks = k*scale;
570               float ls = l*scale;
571               glVertex2f(x+ks,       y+ls);
572               glVertex2f(x+ks+scale, y+ls);
573               glVertex2f(x+ks+scale, y+ls+scale);
574               glVertex2f(x+ks,       y+ls+scale);
575             }
576           }
577         }
578       }
579       glEnd();
580       // glEnable(GL_TEXTURE_2D);
581       // glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
582       // glBindTexture(GL_TEXTURE_2D, textures[0]);
583       // glBegin(GL_QUADS);
584       // glTexCoord2f(0, 0);
585       // glVertex2f(0, 0);
586       // glTexCoord2f(1, 0);
587       // glVertex2f(100, 0);
588       // glTexCoord2f(1, 1);
589       // glVertex2f(100, 100);
590       // glTexCoord2f(0, 1);
591       // glVertex2f(0, 100);
592       // glEnd();
593       // glDisable(GL_TEXTURE_2D);
594       static if(__traits(compiles, loopGl()))
595         loopGl();
596     };
597     // init some gl things
598     // glGenTextures(1000, textures.ptr);
599     // glBindTexture(GL_TEXTURE_2D, textures[0]);
600     // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
601     // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
602     // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, 
603     //                 GL_NEAREST);
604     // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 
605     //                 GL_NEAREST);
606     // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img.width, img.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.rgbaBytes.ptr);
607     // load charmap
608     chars = loadCharmap!charSize(charmap);
609     // set time
610     start = Clock.currTime;
611     // run start
612     static if(__traits(compiles, setup()))
613       setup();
614     window.eventLoop(1, 
615       delegate() {
616         auto dif = Clock.currTime-start;
617         float time = ((dif.total!"msecs")/1000f)+offset;
618         import std.stdio;
619         foreach_reverse(i, evt; events) {
620           if(time >= evt.start && time <= evt.end) {
621             if(!evt.triggered) {
622               evt.enable();
623               evt.triggered = true;
624             }
625             float rel = (time-evt.start)/(evt.end-evt.start);
626             evt.time(rel, time);
627           }
628           else if(time > evt.end) {
629             evt.disable();
630             import std.algorithm : remove;
631             events = events.remove(i);
632           }
633         }
634         static if(__traits(compiles, loop()))
635           loop();
636         window.redrawOpenGlSceneSoon();
637       }
638     );
639   }
640 }