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 }