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   enum WORLD_HEIGHT = worldHeight;
190   SimpleWindow window;
191   Tile[worldHeight][worldWidth] world; /// The world
192   bool[charSize][charSize][256] chars; /// Bitmap of each character
193   SysTime start; /// When the program was started
194   AudioOutputThread* aot;
195   float offset = 0; /// Offset to start at
196 
197   /// Represents single thing that can appear or happen on the screen
198   abstract class Event {
199     float start; /// When the event should appear
200     float end; /// When the event should disappear (set to Infinity if the event should always be active)
201     bool triggered; /// Whether this event has been triggered or not yet
202     struct TileChange {
203       Point pos;
204       Tile prev;
205     }
206     TileChange[] changedTiles; /// Tiles changed by this event
207     this(float start, float end) {
208       this.start = start;
209       this.end = end;
210     }
211 
212     /// Called when the event triggers
213     void enable() {}
214 
215     /// Called on the last frame of the event
216     void disable() {}
217 
218     /** Called every frame when the event is active. 
219     
220     `rel` is a value between 0 and 1, 0 being the first frame the event is visible, 1 being the last
221 
222     `abs` is the amount of seconds since enable() has been called.
223 
224     */
225     void time(float rel, float abs) {}
226 
227     /// Changes the tile at (x, y) to the given tile
228     void changeTile(Point p, Tile t) {
229       Tile wt = world[p.x][p.y];
230       if(t == wt)
231         return; // no change needs to be done
232       changedTiles ~= TileChange(p, wt);
233       world[p.x][p.y] = t;
234     }
235 
236     /// Undoes all changed tiles
237     void undoChanges() {
238       foreach(p; changedTiles)
239         world[p.pos.x][p.pos.y] = p.prev;
240     }
241   }
242 
243   Event[] events; /// List of events queued
244 
245   /// Queues an event
246   void queue(Event e) {
247     events ~= e;
248   }
249 
250   /// Plays OGG audio
251   void audio(string path) {
252     auto controller = aot.playOgg(path);
253     controller.seek(offset);
254   }
255 
256   /// Puts text onto the screen
257   void puts(bool hasEvent)(Event e, int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
258     int sx = x;
259     foreach(c; text) {
260       if(x < 0 || y < 0 || x >= worldWidth || y >= worldHeight)
261         continue;
262       if(c == '\n') {
263         y++;
264         x = sx;
265       } else {
266         static if(hasEvent) {
267           if(!replace || world[x][y].ch == ' ')
268             e.changeTile(Point(x, y), Tile(bg, fg, c));
269         } else {
270           if(!replace || world[x][y].ch == ' ')
271             world[x][y] = Tile(bg, fg, c);
272         }
273         x++;
274       }
275     }
276   }
277 
278   /// Puts text with an event
279   void puts(Event e, int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
280     puts!true(e, x, y, bg, fg, text, replace);
281   }
282 
283   /// Puts text without an event
284   void puts(int x, int y, float[3] bg, float[3] fg, string text, bool replace) {
285     puts!false(null, x, y, bg, fg, text, replace);
286   }
287 
288   /// Simple event to place text onto the screen at a given time
289   class TextEvent : Event {
290     string text;
291     float[3] fg = [1, 1, 1];
292     float[3] bg = [0, 0, 0];
293     Point pos;
294     bool replace;
295     this(float start, float end, Point pos, float[3] fg, float[3] bg, string text, bool replace = true) {
296       super(start, end);
297       this.text = text;
298       this.fg = fg;
299       this.bg = bg;
300       this.pos = pos;
301       this.replace = replace;
302     }
303 
304     this(float start, float end, Point pos, string text, bool replace = true) {
305       super(start, end);
306       this.pos = pos;
307       this.text = text;
308       this.replace = replace;
309     }
310 
311     override void enable() {
312       puts(this, pos.x, pos.y, bg, fg, text, replace);
313     }
314 
315     override void disable() {
316       undoChanges();
317     }
318   }
319 
320   /// Types text onto the screen. Doesn't play well with easings that go backwards (easeBack, easeBounce)
321   class TypeTextEvent : TextEvent {
322     Easing ease;          /// Easing to use when typing the text. Default: `easeLinear`
323     float typingTime = 1; /// Amount of time (seconds) to type the text
324     this(float start, float end, Point pos, float[3] fg, float[3] bg, string text, Easing e = easing!"easeLinear", float typingTime = 0.5) {
325       super(start, end, pos, fg, bg, text);
326       ease = e;
327       this.typingTime = typingTime;
328     }
329 
330     this(float start, float end, Point pos, string text, Easing e = easing!"easeLinear", float typingTime = 0.5) {
331       super(start, end, pos, text);
332       ease = e;
333       this.typingTime = typingTime;
334     }
335 
336     override void enable() {}
337     override void time(float rel, float abs) {
338       float dif = abs-start;
339       if(dif > typingTime)
340         return;
341       float t = ease(dif/typingTime);
342       import std.math : ceil;
343       int idx = cast(int)ceil(t*text.length);
344       puts(this, pos.x, pos.y, bg, fg, text[0..idx], replace);
345     }
346   }
347 
348   /// Flashing text.
349   class FlashingTextEvent : TextEvent {
350     float flashPeriod = 0.5; /// Period of flashing (in seconds)
351     float[3] fg2; /// Second foreground color
352     float[3] bg2; /// Second background color
353     this(float start, float end, Point pos, float[3] bg, float[3] fg, float[3] bg2, float[3] fg2, string text, float flashPeriod = 0.5) {
354       super(start, end, pos, fg, bg, text);
355       this.fg2 = fg2;
356       this.bg2 = bg2;
357       this.flashPeriod = flashPeriod;
358     }
359 
360     override void time(float rel, float abs) {
361       float dif = abs-start;
362       if(dif%(flashPeriod*2) < flashPeriod)
363         puts(pos.x, pos.y, bg, fg, text, replace);
364       else
365         puts(pos.x, pos.y, bg2, fg2, text, replace);
366     }
367   }
368 
369   float mapBetween(float x, float min0, float max0, float min1, float max1) {
370     return (x-min0) / (max0-min0) * (max1-min1) + min1;
371   }
372 
373   /// Translates the screen from an origin to a destination over an amount of time
374   class TranslationEvent : Event {
375     Easing ease;
376     Vector origin;
377     Vector dest;
378     this(float start, float end, Vector origin, Vector dest, Easing e = easing!"easeLinear") {
379       super(start, end);
380       ease = e;
381       this.origin = origin;
382       this.dest = dest;
383     }
384 
385     override void enable() {
386       translation = origin;
387     }
388 
389     override void disable() {
390       translation = dest;
391     }
392 
393     override void time(float rel, float abs) {
394       float eased = ease(rel);
395       translation.x = mapBetween(eased, 0, 1, origin.x, dest.x);
396       translation.y = mapBetween(eased, 0, 1, origin.y, dest.y);
397       translation.z = mapBetween(eased, 0, 1, origin.z, dest.z);
398     }
399   }
400 
401   /// Changes zoom level by one value to another over an amount of time
402   class ZoomEvent : Event {
403     Easing ease;
404     float first;
405     float second;
406     this(float start, float end, float first, float second, Easing e = easing!"easeLinear") {
407       super(start, end);
408       this.first = first;
409       this.second = second;
410       ease = e;
411     }
412 
413     override void enable() {
414       zoom = first;
415     }
416 
417     override void disable() {
418       zoom = second;
419     }
420 
421     override void time(float rel, float abs) {
422       float eased = ease(rel);
423       zoom = mapBetween(eased, 0, 1, first, second);
424     }
425   }
426 
427   // TexitImage img;
428   uint[1000] textures;
429 
430   void main() {
431     // init audio thread
432     AudioOutputThread aot_ = AudioOutputThread(true);
433     aot = &aot_;
434     // img = loadPNG("dman.png");
435     // init translation
436     translation = Vector(width/4, height/4);
437     // init window
438     window = new SimpleWindow(cast(int)(width*charSize*scale), cast(int)(height*charSize*scale), title, OpenGlOptions.yes, Resizability.automaticallyScaleIfPossible);
439     window.redrawOpenGlScene = delegate() {
440       glLoadIdentity();
441       glOrtho(-width*charSize*(zoom/2), width*charSize*(zoom/2), height*charSize*(zoom/2), -height*charSize*(zoom/2), -1.0f, 1.0f);
442       enum css = charSize*scale;
443       glTranslatef(-translation.x*css, -translation.y*css, -translation.z*css);
444       glBegin(GL_QUADS);
445       // draw a giant black rectangle
446       glColor3f(0, 0, 0);
447       glVertex2f( -worldWidth*css,  -worldHeight*css);
448       glVertex2f(2*worldWidth*css,  -worldHeight*css);
449       glVertex2f(2*worldWidth*css, 2*worldHeight*css);
450       glVertex2f( -worldWidth*css, 2*worldHeight*css);
451       // render characters
452       for(int i = 0; i < worldWidth; i++) {
453         for(int j = 0; j < worldHeight; j++) {
454           auto tile = world[i][j];
455           auto ch = chars[tile.ch];
456           float x = i*css;
457           float y = j*css;
458           glColor3f(tile.bg[0], tile.bg[1], tile.bg[2]);
459           glVertex2f(x,     y);
460           glVertex2f(x+css, y);
461           glVertex2f(x+css, y+css);
462           glVertex2f(x,     y+css);
463           glColor3f(tile.fg[0], tile.fg[1], tile.fg[2]);
464           if(tile.ch == ' ')
465             continue; // no point to draw spaces, they're empty
466           for(int k = 0; k < charSize; k++) {
467             for(int l = 0; l < charSize; l++) {
468               if(!ch[k][l])
469                 continue;
470               float ks = k*scale;
471               float ls = l*scale;
472               glVertex2f(x+ks,       y+ls);
473               glVertex2f(x+ks+scale, y+ls);
474               glVertex2f(x+ks+scale, y+ls+scale);
475               glVertex2f(x+ks,       y+ls+scale);
476             }
477           }
478         }
479       }
480       glEnd();
481       // glEnable(GL_TEXTURE_2D);
482       // glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
483       // glBindTexture(GL_TEXTURE_2D, textures[0]);
484       // glBegin(GL_QUADS);
485       // glTexCoord2f(0, 0);
486       // glVertex2f(0, 0);
487       // glTexCoord2f(1, 0);
488       // glVertex2f(100, 0);
489       // glTexCoord2f(1, 1);
490       // glVertex2f(100, 100);
491       // glTexCoord2f(0, 1);
492       // glVertex2f(0, 100);
493       // glEnd();
494       // glDisable(GL_TEXTURE_2D);
495       
496     };
497     // init some gl things
498     // glGenTextures(1000, textures.ptr);
499     // glBindTexture(GL_TEXTURE_2D, textures[0]);
500     // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
501     // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
502     // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, 
503     //                 GL_NEAREST);
504     // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 
505     //                 GL_NEAREST);
506     // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img.width, img.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.rgbaBytes.ptr);
507     // load charmap
508     chars = loadCharmap!charSize(charmap);
509     // set time
510     start = Clock.currTime;
511     // run start
512     static if(__traits(compiles, setup()))
513       setup();
514     window.eventLoop(1, 
515       delegate() {
516         auto dif = Clock.currTime-start;
517         float time = ((dif.total!"msecs")/1000f)+offset;
518         import std.stdio;
519         foreach_reverse(i, evt; events) {
520           if(time >= evt.start && time <= evt.end) {
521             if(!evt.triggered) {
522               evt.enable();
523               evt.triggered = true;
524             }
525             float rel = (time-evt.start)/(evt.end-evt.start);
526             evt.time(rel, time);
527           }
528           else if(time > evt.end) {
529             evt.disable();
530             import std.algorithm : remove;
531             events = events.remove(i);
532           }
533         }
534         static if(__traits(compiles, loop()))
535           loop();
536         window.redrawOpenGlSceneSoon();
537       }
538     );
539   }
540 }