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 }