diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..05f2f03
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+out/*
+bin/*
+hs_err_pid*
+assets/*
+2ndreal_CGA.png_132x74_C4_A13.aarec.zip
+2ndreal_CGA.png_132x74_C4_A23_fullcp.aarec.zip
+2ndreal_CGA.png_132x74_C16_A23_fullcp.aarec.zip
+Bad_Apple_CGA.png_76x43_C4_A13.aarec.zip
+Bad_Apple_CGA.png_132x74_C4_A11.aarec.zip
+assets.zip
diff --git a/.idea/artifacts/BadAppleAA_jar.xml b/.idea/artifacts/BadAppleAA_jar.xml
new file mode 100644
index 0000000..3685a66
--- /dev/null
+++ b/.idea/artifacts/BadAppleAA_jar.xml
@@ -0,0 +1,20 @@
+
+
+ $PROJECT_DIR$/out/artifacts/BadAppleAA_jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..96cc43e
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..e7bedf3
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..97626ba
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/KotlinJavaRuntime.xml b/.idea/libraries/KotlinJavaRuntime.xml
new file mode 100644
index 0000000..b0b18f4
--- /dev/null
+++ b/.idea/libraries/KotlinJavaRuntime.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/lib.xml b/.idea/libraries/lib.xml
new file mode 100644
index 0000000..00a52d3
--- /dev/null
+++ b/.idea/libraries/lib.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..c6d8fb7
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..89e2f73
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..e96534f
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/BaAA.iml b/BaAA.iml
new file mode 100644
index 0000000..96191a9
--- /dev/null
+++ b/BaAA.iml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..5347d79
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,9 @@
+BaAA
+
+Copyright (c) 2016 Torvald (minjaesong)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..45aadf1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,162 @@
+# BA-AA #
+
+## About ##
+
+This is yet another crudely-written ascii-art generator and demo player that is highly configurable. Default setting of this demo emulates EGA text mode, with four shades of black/white, and a Sound Blaster.
+
+The video playback can be scaled to any console text mode size, can support any video framerate and any length.
+
+You can alter the font set to whatever you want, the AA engine will adjust itself accordingly.
+
+You can also make this demo to play completely different video and audio. Please refer to the ```config.properties``` file.
+
+The demo is distributed under the MIT License. Please refer to ```COPYING``` for the copy of the license.
+
+One last note: don’t expect much about the performance, and don’t try to find out what “BA” means.
+
+
+## How to run ##
+
+### First run ###
+
+Before you play the demo, please unzip the ```assets.zip```. There should be ```assets``` directory created.
+
+If you don't see the zip file, get one from latest __[release](https://github.com/minjaesong/Ba-AA) (github)__
+
+### Execution ###
+
+Execute __BaAA.jar__ by double-clicking on it or using terminal. Ignore anything else; they’re just bunch of libraries, natives and resources. If you’re not a programmer, just leave them as is.
+
+Java SE version 8 or higher is required.
+
+
+### Player control ###
+
+- SPACE: pause/resume (frequent pausing will make audio off-sync)
+- F1: remove colour filter
+- F2: Colour filter IBM (green)
+- F3: Colour filter AMBER
+- F4: User-defined colour filter
+- H: Capture current screen into PNG
+- T: Capture current screen into TXT (```bSingleTone=true``` only)
+
+
+## Configuration ##
+
+If you’re done with the default configuration, it’s time to play with it!
+
+When playing the demo, you can see __STRM__ or __PRLD__ on the window title. This indicates whether the video is streamed or preloaded. If streaming video is too choppy, you can try preload by setting ```bPreloadFrames``` to ```true```, or pre-record everything beforehand. See section __Record and play__ for instruction.
+
+Note that the audio is always streamed from disk.
+
+
+### How to read window title for information ###
+
+ AA Player — S: 61(60) — M: 216M/1820M — F: 4932/13159 STRM — C: 4 — A: 12
+
+- __S__ indicates the current framerate: drawing framerate and video framerate. In this line, 61 is the drawing framerate and 60 is the video’s framerate.
+- __M__ indicates the memory usage information. Former is currently used by program (NOT by the Java applet), latter is the maximum amount of memory JVM can use in this applet.
+- __F__ indicates the index of frame currently playing. STRM indicates that each frame is streamed from the disk, calculated on the fly, PRLD indicates every frame is pre-loaded and pre-calculated, RECD indicates the frame is loaded from the record file.
+- __C__ indicates the number of greyscale tones currently using
+- __A__ indicates the current algorithms using. First number represents antialiasing, second is dithering
+
+
+To changed the font colour, you’ll need to edit font files with your image editor.
+
+- ```font.png```, ```font.3.png```: full brightness (0xFF)
+- ```font.1.png```: 33.3% (0x55)
+- ```font.2.png```: 66.7% (0xAA)
+
+- ```font.4.png```: (0x11)
+- ```font.5.png```: (0x22)
+- ```font.6.png```: (0x33)
+- ```font.7.png```: (0x44)
+- ```font.8.png```: (0x66)
+- ```font.9.png```: (0x77)
+- ```font.10.png```: (0x88)
+- ```font.11.png```: (0x99)
+- ```font.12.png```: (0xBB)
+- ```font.13.png```: (0xCC)
+- ```font.14.png```: (0xDD)
+- ```font.15.png```: (0xEE)
+
+(fonts are pre-coloured to make the demo run faster)
+
+
+## About Ascii Art Library ##
+
+Source: ```ImageToAA*.kt```
+
+The library receives current font (as a SpriteSheet), apply colours, then calculate the luminosity of each glyph. No hand-crafted things other than certain glyphs are go unused.
+
+The object must be initialised with ```setProp()``` function before use, and ```precalcFont()``` must be called before you can actually play with ```toAscii()```.
+
+The library supports Gamma Correction, but not the inverted mode (white background, black text)
+
+
+## Trivia ##
+
+* The demo plays real 60-frame Bad Apple footage which was generated with SVPFlow.
+
+* Included audio faithfully emulates what the real Sound Blaster would do, sampling rate of 23 kHz, 8-bit, monoaural, then encoded with Vorbis ```-q 0```
+
+* You can use your own series of images, frame size unlimited. (disk space and your GPU texture size has limit, however)
+
+* Built-in font “terminal.png” was from OSX’s single mode console. Didn’t dumped; it was a manual labour of tracing dot by dot. Non-ASCII letters are my own drawing.
+
+* Supported formats for frame: png (rgb or indexed), jpg, bmp, tga (uncompressed)
+
+* Supported formats for audio: ogg, mod, xm (please note that MOD/XM playback of IBXM is suck)
+
+
+## Disclaimer ##
+
+* I claim no copyright to the included assets of:
+ - The audio “Bad Apple!! (feat. nomico)” by Alstroemeria Records, original composition by ZUN
+ - The video, frame-doubled version of ```www.nicovideo.jp/watch/sm8628149```
+
+These assets are copyrighted to the its copyright owner.
+
+* Included OGG audio has low sound quality, which is intentional to achieve the retro vibe (Sound Blaster). If you don’t like it, get your own high quality audio and encode with OGG/Vorbis ```-q 10```.
+
+## (dropped features) ##
+
+* Play from mp4: Tried JCodec, playback was very jittery
+
+* Inverted colour (white background): I’m exhausted. You clone it and try it ;)
+
+## How to use my own video and audio ##
+
+To play your own video on the demo player, you’ll need to prepare them yourself.
+
+* Requirements: Your video file, FFmpeg, framerate for the video, Audacity
+
+1. Open FFmpeg, type ```ffmpeg -i input_file_name -vf fps=video_framerate out output_directory/prefix_of_your_own%08d.png```
+ e.g. If I’m generating from Test.mp4, ```ffmpeg -i test.mp4 -vf fps=30 out test%08d.png```
+2. Extract audio stream from your video file. Google ```video_container demux audio``` (e.g. ```mp4 demux audio```) for instruction.
+3. Transcode extracted audio to OGG/Vorbis. You cau use Audacity for the job.
+4. Move directory that contains thousands of PNGs and converted OGG to demo’s ```assets``` directory.
+5. Configure the demo accordingly. You’ll need to change ```iVideoFramerate```, ```sFramesDir```, ```sFramesPrefix``` and ```sAudioFileName```.
+
+## Record and replay ##
+
+If ```bIsRecordMode``` is set to ```true```, the demo will record the frame into your hard disk.
+
+Name for the file is auto-generated in the format of ```[framename]_[fontname]_[width]x[height]_C[colours]_A[algorithm].aarec```
+e.g. ```Bad_Apple_CGA.png_132x74_C4_A11.aarec```
+
+To play from the record, specify ```sRecordFileName``` as the filename of the aarec file. Note that you still have to give valid values in the config to the following:
+
+- sAudioFileName
+- sFontFamilyName
+- sFontSize
+
+### Pre-recorded files ###
+
+The distribution of the demo will contain 5 aarec files which are:
+
+- ```2ndreal_CGA.png_132x74_C4_A23_fullcp.aarec``` : the demo _Second Reality_ by Future Crew, in four greyscales, full code page. Use audio ```2ndreal.ogg```
+- ```2ndreal_CGA.png_132x74_C16_A23_fullcp.aarec``` : the demo _Second Reality_ by Future Crew, in sixteen greyscales, full code page. Use audio ```2ndreal.ogg```
+- ```2ndreal_CGA.png_132x74_C4_A13.aarec``` : the demo _Second Reality_ by Future Crew, in four greyscales, only using ASCII glyphs. Use audio ```2ndreal.ogg```
+- ```Bad_Apple_CGA.png_76x43_C4_A13.aarec``` : pre-recorded Bad Apple in 76x43, CGA font
+- ```Bad_Apple_CGA.png_132x74_C4_A11.aarec``` : pre-recorded Bad Apple in 132x74, CGA font
diff --git a/config.properties b/config.properties
new file mode 100644
index 0000000..0b01bdb
--- /dev/null
+++ b/config.properties
@@ -0,0 +1,114 @@
+# Whether or not pre-calculate all the frames before play
+# Note that pre-calculating can take a while
+# "true" : pre-load, pre-process everything before playback. Will hog quite an amount of RAM (~500M+ for Bad Apple)
+# "false" : stream frames from the disk and process them on the fly
+bPreloadFrames=false
+
+# Set "true" to record the pre-loading to a binary file.
+# This option overrides any playback mode.
+# filename for the recorded file will be generated accordingly to the framesDir, font settings, etc.
+# default: false
+bIsRecordMode=false
+
+# Fill in the option to play from the recording.
+# This option overrides any playback mode.
+# extension: .aarec
+# if "bIsRecordMode=true" and this option is specified, the application will quit with error (superposition!)
+# default: (blank)
+sRecordFileName=
+
+# Framerate of the video you provided.
+# It will also limit the FPS of the application
+# valid range: 0 - 255
+# default: 60
+iVideoFramerate=60
+
+# Play video from series of images.
+# default: Bad_Apple
+sFramesDir=Bad_Apple
+# default: ba
+sFramesPrefix=ba
+
+# supported formats: OGG, MOD, XM
+# default: Touhou_bad_apple_feat_nomico.ogg
+sAudioFileName=Touhou_bad_apple_feat_nomico.ogg
+
+# different font can have different effect on the video,
+# default: CGA.png - 8x8, MDA.png - 9x14, terminal.png - 8x16
+sFontFamilyName=CGA.png
+sFontSize=8x8
+
+# Note: Bad Apple footage has aspect ratio of 16:9
+# default-SVGA : 132x74 for CGA.png, 132x48 for MDA.png, 132x37 for terminal.png
+# default-oldskool: 76x43 for EGA (CGA.png), 69x25 for MDA (MDA.png)
+sTerminalSize=76x43
+
+# Monitor color type, if you want more RETRO
+# 0: White (LCD white)
+# 1: Green (P39 phosphor -- IBM 5151)
+# 2: Amber (P3 phosphor)
+# 3: custom colour given in "sCustomFilterColour"
+# You can also change the filter by using F1-F4 on the player
+# default: 0
+iMonitorType=0
+# usage: like 0,239,255
+sCustomFilterColour=0,239,255
+
+# Use only single colour
+# if b16Tones and bSingleCol are both true, the application will quit with error (superposition!)
+# default: false
+bSingleTone=false
+# mark as true and the demo will use 16 shades of grey
+# MDA font can't utilise this feature; you'll have to extend the font family by yourself
+# to use the feature with MDA.
+# Note: No known oldskool computers are capable of display text mode in this fashion.
+# default: false
+b16Tones=false
+
+# Use full 255 characters
+# Note that since we can't really change the background, it won't look like the "8088 Corruption".
+# Use with "iAsciiAlgo=2" for better quality.
+# default: false
+bFullCodePage=false
+
+# Gamma function for luminosity calculation
+# it's here just for the proof that the demo can gamma-correct. Just leave the knob alone.
+# default: 220
+iGamma=220
+
+# dither image to increase quality and decrease framerate
+# 0: No dither --FASTEST
+# 1: Floyd-Steinberg
+# 2: Sierra lite --FASTEST dithering
+# 3: Sierra-2
+# test dithering with sTestDisplayImage=gradient.png
+# default: 1
+iDitherAlgo=1
+# Antialiasing algorithms
+# 2 -- SubGlyph9: a pixel corresponds to 3x3 pixels. Best for pictures and/or "bFullCodePage=true"
+# 1 -- SubGlyph4: a pixel corresponds to 2x2 pixels. Best for high contrast images (like Bad Apple).
+# different font has different effect on AA; if unsure, stick to default, which is guaranteed to work.
+# default: 1
+iAsciiAlgo=1
+
+# Faster machine might need higher value. (not tested)
+# Using too low value will make pre-calculation needlessly too long
+# If the progress display lags, it's a sign that you hit the limit of your processing power.
+# Even if you did, however, it doesn't hurt as long as the display on the Window title is all good.
+# If unsure, just leave it to the default: 41
+iPreCalcRate=41
+
+#sTestDisplayImage=gradient.png
+#sTestDisplayImage=arendelle_drawing_bg.png
+#sTestDisplayImage=drawitself_cga.png
+
+# Display credit at the end of the play
+# I know, it's sort of, meh. Just set it false so you wouldn't see it.
+# default: false
+bDemoCredit=true
+
+# Antialiasing algorithms -- advanced
+# 3 -- SubGlyph16 (4x4)
+# 4 -- SubGlyph25 (5x5)
+# 5 -- SubGlyph64 (8x8)
+# 0 -- None
diff --git a/config_default.properties b/config_default.properties
new file mode 100644
index 0000000..575d1e2
--- /dev/null
+++ b/config_default.properties
@@ -0,0 +1,114 @@
+# Whether or not pre-calculate all the frames before play
+# Note that pre-calculating can take a while
+# "true" : pre-load, pre-process everything before playback. Will hog quite an amount of RAM (~500M+ for Bad Apple)
+# "false" : stream frames from the disk and process them on the fly
+bPreloadFrames=false
+
+# Set "true" to record the pre-loading to a binary file.
+# This option overrides any playback mode.
+# filename for the recorded file will be generated accordingly to the framesDir, font settings, etc.
+# default: false
+bIsRecordMode=false
+
+# Fill in the option to play from the recording.
+# This option overrides any playback mode.
+# extension: .aarec
+# if "bIsRecordMode=true" and this option is specified, the application will quit with error (superposition!)
+# default: (blank)
+sRecordFileName=
+
+# Framerate of the video you provided.
+# It will also limit the FPS of the application
+# valid range: 0 - 255
+# default: 60
+iVideoFramerate=60
+
+# Play video from series of images.
+# default: Bad_Apple
+sFramesDir=Bad_Apple
+# default: ba
+sFramesPrefix=ba
+
+# supported formats: OGG, MOD, XM
+# default: Touhou_bad_apple_feat_nomico.ogg
+sAudioFileName=Touhou_bad_apple_feat_nomico.ogg
+
+# different font can have different effect on the video,
+# default: CGA.png - 8x8, MDA.png - 9x14, terminal.png - 8x16
+sFontFamilyName=CGA.png
+sFontSize=8x8
+
+# Note: Bad Apple footage has aspect ratio of 16:9
+# default-SVGA : 132x74 for CGA.png, 132x48 for MDA.png, 132x37 for terminal.png
+# default-oldskool: 76x43 for EGA (CGA.png), 69x25 for MDA (MDA.png)
+sTerminalSize=132x74
+
+# Monitor color type, if you want more RETRO
+# 0: White (LCD white)
+# 1: Green (P39 phosphor -- IBM 5151)
+# 2: Amber (P3 phosphor)
+# 3: custom colour given in "sCustomFilterColour"
+# You can also change the filter by using F1-F4 on the player
+# default: 0
+iMonitorType=0
+# usage: like 0,239,255
+sCustomFilterColour=0,239,255
+
+# Use only single colour
+# if b16Tones and bSingleCol are both true, the application will quit with error (superposition!)
+# default: false
+bSingleTone=false
+# mark as true and the demo will use 16 shades of grey
+# MDA font can't utilise this feature; you'll have to extend the font family by yourself
+# to use the feature with MDA.
+# Note: No known oldskool computers are capable of display text mode in this fashion.
+# default: false
+b16Tones=false
+
+# Use full 255 characters
+# Note that since we can't really change the background, it won't look like the "8088 Corruption".
+# Use with "iAsciiAlgo=2" for better quality.
+# default: false
+bFullCodePage=false
+
+# Gamma function for luminosity calculation
+# it's here just for the proof that the demo can gamma-correct. Just leave the knob alone.
+# default: 220
+iGamma=220
+
+# dither image to increase quality and decrease framerate
+# 0: No dither --FASTEST
+# 1: Floyd-Steinberg
+# 2: Sierra lite --FASTEST dithering
+# 3: Sierra-2
+# test dithering with sTestDisplayImage=gradient.png
+# default: 1
+iDitherAlgo=1
+# Antialiasing algorithms
+# 2 -- SubGlyph9: a pixel corresponds to 3x3 pixels. Best for pictures and/or "bFullCodePage=true"
+# 1 -- SubGlyph4: a pixel corresponds to 2x2 pixels. Best for high contrast images (like Bad Apple).
+# different font has different effect on AA; if unsure, stick to default, which is guaranteed to work.
+# default: 1
+iAsciiAlgo=1
+
+# Faster machine might need higher value. (not tested)
+# Using too low value will make pre-calculation needlessly too long
+# If the progress display lags, it's a sign that you hit the limit of your processing power.
+# Even if you did, however, it doesn't hurt as long as the display on the Window title is all good.
+# If unsure, just leave it to the default: 41
+iPreCalcRate=41
+
+#sTestDisplayImage=gradient.png
+#sTestDisplayImage=arendelle_drawing_bg.png
+#sTestDisplayImage=drawitself_cga.png
+
+# Display credit at the end of the play
+# I know, it's sort of, meh. Just set it false so you wouldn't see it.
+# default: false
+bDemoCredit=true
+
+# Antialiasing algorithms -- advanced
+# 3 -- SubGlyph16 (4x4)
+# 4 -- SubGlyph25 (5x5)
+# 5 -- SubGlyph64 (8x8)
+# 0 -- None
diff --git a/lib/ibxm.jar b/lib/ibxm.jar
new file mode 100644
index 0000000..619d26e
Binary files /dev/null and b/lib/ibxm.jar differ
diff --git a/lib/jinput.jar b/lib/jinput.jar
new file mode 100644
index 0000000..7c2b6b0
Binary files /dev/null and b/lib/jinput.jar differ
diff --git a/lib/jogg-0.0.7.jar b/lib/jogg-0.0.7.jar
new file mode 100644
index 0000000..ecb0260
Binary files /dev/null and b/lib/jogg-0.0.7.jar differ
diff --git a/lib/jorbis-0.0.15.jar b/lib/jorbis-0.0.15.jar
new file mode 100644
index 0000000..4cf51f9
Binary files /dev/null and b/lib/jorbis-0.0.15.jar differ
diff --git a/lib/kotlin-reflect.jar b/lib/kotlin-reflect.jar
new file mode 100644
index 0000000..62340e2
Binary files /dev/null and b/lib/kotlin-reflect.jar differ
diff --git a/lib/kotlin-runtime-sources.jar b/lib/kotlin-runtime-sources.jar
new file mode 100644
index 0000000..e661b30
Binary files /dev/null and b/lib/kotlin-runtime-sources.jar differ
diff --git a/lib/kotlin-runtime.jar b/lib/kotlin-runtime.jar
new file mode 100644
index 0000000..4d7a927
Binary files /dev/null and b/lib/kotlin-runtime.jar differ
diff --git a/lib/lwjgl.jar b/lib/lwjgl.jar
new file mode 100644
index 0000000..e2fa096
Binary files /dev/null and b/lib/lwjgl.jar differ
diff --git a/lib/lwjgl_util.jar b/lib/lwjgl_util.jar
new file mode 100644
index 0000000..237a1d7
Binary files /dev/null and b/lib/lwjgl_util.jar differ
diff --git a/lib/natives/OpenAL32.dll b/lib/natives/OpenAL32.dll
new file mode 100644
index 0000000..1f69e94
Binary files /dev/null and b/lib/natives/OpenAL32.dll differ
diff --git a/lib/natives/OpenAL64.dll b/lib/natives/OpenAL64.dll
new file mode 100644
index 0000000..6f2a2fe
Binary files /dev/null and b/lib/natives/OpenAL64.dll differ
diff --git a/lib/natives/jinput-dx8.dll b/lib/natives/jinput-dx8.dll
new file mode 100644
index 0000000..6d27ad5
Binary files /dev/null and b/lib/natives/jinput-dx8.dll differ
diff --git a/lib/natives/jinput-dx8_64.dll b/lib/natives/jinput-dx8_64.dll
new file mode 100644
index 0000000..6730589
Binary files /dev/null and b/lib/natives/jinput-dx8_64.dll differ
diff --git a/lib/natives/jinput-raw.dll b/lib/natives/jinput-raw.dll
new file mode 100644
index 0000000..ce1d162
Binary files /dev/null and b/lib/natives/jinput-raw.dll differ
diff --git a/lib/natives/jinput-raw_64.dll b/lib/natives/jinput-raw_64.dll
new file mode 100644
index 0000000..3d2b3ad
Binary files /dev/null and b/lib/natives/jinput-raw_64.dll differ
diff --git a/lib/natives/libjinput-linux.so b/lib/natives/libjinput-linux.so
new file mode 100644
index 0000000..3cdc439
Binary files /dev/null and b/lib/natives/libjinput-linux.so differ
diff --git a/lib/natives/libjinput-linux64.so b/lib/natives/libjinput-linux64.so
new file mode 100644
index 0000000..de1ee5f
Binary files /dev/null and b/lib/natives/libjinput-linux64.so differ
diff --git a/lib/natives/libjinput-osx.dylib b/lib/natives/libjinput-osx.dylib
new file mode 100644
index 0000000..59a3eab
Binary files /dev/null and b/lib/natives/libjinput-osx.dylib differ
diff --git a/lib/natives/liblwjgl.dylib b/lib/natives/liblwjgl.dylib
new file mode 100644
index 0000000..a6083b9
Binary files /dev/null and b/lib/natives/liblwjgl.dylib differ
diff --git a/lib/natives/liblwjgl.so b/lib/natives/liblwjgl.so
new file mode 100644
index 0000000..ba6e7f6
Binary files /dev/null and b/lib/natives/liblwjgl.so differ
diff --git a/lib/natives/liblwjgl64.so b/lib/natives/liblwjgl64.so
new file mode 100644
index 0000000..8ed0992
Binary files /dev/null and b/lib/natives/liblwjgl64.so differ
diff --git a/lib/natives/libopenal.so b/lib/natives/libopenal.so
new file mode 100644
index 0000000..0a3a619
Binary files /dev/null and b/lib/natives/libopenal.so differ
diff --git a/lib/natives/libopenal64.so b/lib/natives/libopenal64.so
new file mode 100644
index 0000000..e0693c0
Binary files /dev/null and b/lib/natives/libopenal64.so differ
diff --git a/lib/natives/lwjgl.dll b/lib/natives/lwjgl.dll
new file mode 100644
index 0000000..b26da56
Binary files /dev/null and b/lib/natives/lwjgl.dll differ
diff --git a/lib/natives/lwjgl64.dll b/lib/natives/lwjgl64.dll
new file mode 100644
index 0000000..ac5aecd
Binary files /dev/null and b/lib/natives/lwjgl64.dll differ
diff --git a/lib/natives/natives-linux.jar b/lib/natives/natives-linux.jar
new file mode 100644
index 0000000..a543ee0
Binary files /dev/null and b/lib/natives/natives-linux.jar differ
diff --git a/lib/natives/natives-mac.jar b/lib/natives/natives-mac.jar
new file mode 100644
index 0000000..220012b
Binary files /dev/null and b/lib/natives/natives-mac.jar differ
diff --git a/lib/natives/natives-windows.jar b/lib/natives/natives-windows.jar
new file mode 100644
index 0000000..2fb036f
Binary files /dev/null and b/lib/natives/natives-windows.jar differ
diff --git a/lib/natives/openal.dylib b/lib/natives/openal.dylib
new file mode 100644
index 0000000..3c6d0f7
Binary files /dev/null and b/lib/natives/openal.dylib differ
diff --git a/lib/slick.jar b/lib/slick.jar
new file mode 100644
index 0000000..1d3d075
Binary files /dev/null and b/lib/slick.jar differ
diff --git a/repo_logo.png b/repo_logo.png
new file mode 100644
index 0000000..88d47c6
Binary files /dev/null and b/repo_logo.png differ
diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..20af0ed
--- /dev/null
+++ b/src/META-INF/MANIFEST.MF
@@ -0,0 +1,8 @@
+Manifest-Version: 1.0
+Class-Path: ibxm.jar lwjgl.jar slick.jar jinput.jar jogg-0.0.7.jar lwj
+ gl_util.jar jorbis-0.0.15.jar kotlin-reflect.jar kotlin-runtime.jar k
+ otlin-runtime-sources.jar ibxm.jar lwjgl.jar slick.jar jinput.jar jog
+ g-0.0.7.jar lwjgl_util.jar jorbis-0.0.15.jar kotlin-reflect.jar kotli
+ n-runtime.jar kotlin-runtime-sources.jar
+Main-Class: net.torvald.aa.demoplayer.BaAA
+
diff --git a/src/net/torvald/aa/AAFrame.kt b/src/net/torvald/aa/AAFrame.kt
new file mode 100644
index 0000000..235da67
--- /dev/null
+++ b/src/net/torvald/aa/AAFrame.kt
@@ -0,0 +1,93 @@
+package net.torvald.aa
+
+import net.torvald.aa.demoplayer.toUint
+import org.newdawn.slick.*
+
+import java.util.Random
+
+/**
+ * Created by minjaesong on 16-08-10.
+ */
+class AAFrame @Throws(SlickException::class)
+constructor(var width: Int, var height: Int) {
+
+ /**
+ * xx_000000_00000000
+
+ * Upper bit: Colour 0 black 1 dark grey 2 grey 3 white || 0-31 arbitrary index
+ * Lower 8 bits: ASCII
+ */
+ internal val frameBuffer: CharArray
+
+ private val testrng = Random()
+
+ val sizeof = 2 * width * height // magic number 2: indicator that we're using char
+
+ init {
+ frameBuffer = CharArray(width * height)
+ }
+
+ fun drawBuffer(x: Int, y: Int, c: Char, brightness: Int) {
+ frameBuffer[y * width + x] = (c.toInt() or (brightness and 0x1F shl 8)).toChar()
+ }
+
+ fun drawBuffer(x: Int, y: Int, raw: Char): Boolean =
+ if (checkOOB(x, y))
+ false
+ else {
+ frameBuffer[y * width + x] = raw
+ true
+ }
+
+ fun drawFromBytes(other: ByteArray) {
+ for (i in 0..other.size - 1 step 2) {
+ val char = (other[i].toUint().shl(8) or other[i + 1].toUint()).toChar()
+ frameBuffer[i.ushr(1)] = char
+ }
+ }
+
+ fun getColorKey(x: Int, y: Int): Int {
+ return frameBuffer[y * width + x].toInt().ushr(8) and 0x1F
+ }
+
+ fun getChar(x: Int, y: Int): Char {
+ return (frameBuffer[y * width + x].toInt() and 0xFF).toChar()
+ }
+
+ fun getRaw(x: Int, y: Int): Char? =
+ if (checkOOB(x, y))
+ null
+ else
+ frameBuffer[y * width + x]
+
+ fun clear() {
+ for (y in 0..height - 1) {
+ for (x in 0..width - 1) {
+ frameBuffer[y * width + x] = 0.toChar()
+ }
+ }
+ }
+
+ fun drawString(s: String, x: Int, y: Int, col: Int) {
+ if (checkOOB(x, y)) return
+
+ for (i in 0..s.length - 1) {
+ if (checkOOB(x + i, y)) return
+
+ frameBuffer[y * width + x + i] =
+ (s[i].toInt() or
+ ((if (s[i] == 0.toChar() || s[i] == 32.toChar()) 0 else col) shl 8)).toChar()
+ }
+ }
+
+ fun drawFromOther(other: AAFrame) {
+ //this.framebuffer = other.getFrameBuffer();
+ for (y in 0..height - 1) {
+ for (x in 0..width - 1) {
+ frameBuffer[y * width + x] = other.getRaw(x, y)!!
+ }
+ }
+ }
+
+ private fun checkOOB(x: Int, y: Int) = (x < 0 || y < 0 || x >= width || y >= height)
+}
diff --git a/src/net/torvald/aa/AsciiAlgo.kt b/src/net/torvald/aa/AsciiAlgo.kt
new file mode 100644
index 0000000..d77bac6
--- /dev/null
+++ b/src/net/torvald/aa/AsciiAlgo.kt
@@ -0,0 +1,13 @@
+package net.torvald.aa
+
+import org.newdawn.slick.Image
+
+/**
+ * Created by minjaesong on 16-08-12.
+ */
+interface AsciiAlgo {
+ fun precalcFont()
+ fun toAscii(rawImage: Image, aaframe: AAFrame)
+ var fontW: Int
+ var fontH: Int
+}
\ No newline at end of file
diff --git a/src/net/torvald/aa/ColouredFastFont.kt b/src/net/torvald/aa/ColouredFastFont.kt
new file mode 100644
index 0000000..8db1b32
--- /dev/null
+++ b/src/net/torvald/aa/ColouredFastFont.kt
@@ -0,0 +1,101 @@
+package net.torvald.aa
+
+import net.torvald.aa.demoplayer.BaAA
+import org.lwjgl.opengl.GL11
+import org.newdawn.slick.Color
+import org.newdawn.slick.Font
+import org.newdawn.slick.Image
+import org.newdawn.slick.SpriteSheet
+import java.util.*
+
+/**
+ * Based on multisheet slick spritesheef font (net.torvald.imagefont.GameFontBase) of my game project.
+ *
+ * Created by minjaesong on 16-08-12.
+ */
+class ColouredFastFont(fontRef: String, val fontW: Int, val fontH: Int) : Font {
+
+ val colouredSheet = ArrayList() // index zero: dark grey
+ private var sheetW = 0
+ private var sheetH = 0
+
+
+ private lateinit var sheetImageBuffer: Image
+
+ init {
+ val getSizeImg = Image(fontRef)
+ sheetW = getSizeImg.width
+ sheetH = getSizeImg.height
+
+ getSizeImg.destroy()
+
+ sheetImageBuffer = Image(sheetW, sheetH)
+
+ for (i in 1..BaAA.colors.size - 1) {
+ val sheet = SpriteSheet("$fontRef.$i.png", fontW, fontH)
+ colouredSheet.add(sheet)
+
+ //sheetImageBuffer.graphics.clear()
+ }
+
+ sheetImageBuffer.destroy()
+ }
+
+ private fun getIndexX(ch: Char) = ch.toInt() % (sheetW / fontW)
+ private fun getIndexY(ch: Char) = ch.toInt() / (sheetW / fontW)
+
+ override fun getHeight(p0: String): Int = fontH
+
+ override fun getWidth(p0: String): Int {
+ throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
+ }
+
+ override fun getLineHeight(): Int = fontH
+
+ override fun drawString(p0: Float, p1: Float, p2: String) {
+ throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
+ }
+
+
+ override fun drawString(p0: Float, p1: Float, p2: String, p3: Color) {
+ //search colour
+ var colourIndex = -1
+ for (i in 0..BaAA.colors.size - 1) {
+ if (BaAA.getColor(i) == p3) {
+ colourIndex = i - 1
+ break
+ }
+ }
+
+ if (colourIndex >= 0) {
+
+
+ colouredSheet[colourIndex].startUse()
+
+
+ for (i in 0..p2.length - 1) {
+ val ch = p2[i]
+
+ colouredSheet[colourIndex].renderInUse(
+ p0.floorInt() + (i * fontW),
+ p1.floorInt(),
+ getIndexX(ch),
+ getIndexY(ch)
+ )
+ }
+
+
+ colouredSheet[colourIndex].endUse()
+ }
+ else {
+ //System.err.println("[ColouredFastFont] unmatched colour! $p3")
+ }
+
+ }
+
+ override fun drawString(p0: Float, p1: Float, p2: String, p3: Color, p4: Int, p5: Int) {
+ throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
+ }
+
+ fun Float.floorInt() = this.toInt()
+}
diff --git a/src/net/torvald/aa/ImageToAA.kt b/src/net/torvald/aa/ImageToAA.kt
new file mode 100644
index 0000000..beea8bb
--- /dev/null
+++ b/src/net/torvald/aa/ImageToAA.kt
@@ -0,0 +1,416 @@
+package net.torvald.aa
+
+import net.torvald.aa.demoplayer.BaAA
+import org.newdawn.slick.*
+import java.util.*
+
+/**
+ * Created by minjaesong on 16-08-10.
+ */
+class ImageToAA : AsciiAlgo {
+
+ private var w = 0
+ private var h = 0
+ private lateinit var fontSheet: SpriteSheet
+ override var fontW = 0
+ override var fontH = 0
+ private var inverted = false
+ private var gamma = 0.0
+ private var ditherAlgo = 0
+ private var colourAlgo = 0
+
+ private lateinit var fontRange: IntRange
+
+ private lateinit var colourMap: Array
+
+ fun setProp(
+ w: Int, h: Int,
+ font: SpriteSheet, fontW: Int, fontH: Int,
+ inverted: Boolean, gamma: Double, dither: Int, fullCodePage: Boolean, colourAlgo: Int) {
+ this.w = w
+ this.h = h
+ this.fontW = fontW
+ this.fontH = fontH
+ this.fontSheet = font
+ this.inverted = inverted
+ this.gamma = gamma
+ ditherAlgo = dither
+ this.colourAlgo = colourAlgo
+
+ if (fullCodePage)
+ fontRange = 1..255
+ else
+ fontRange = 32..126
+
+ imageBuffer = Image(w, h)
+
+ // re-invert colourmap so that the calculation stay normal when inverted
+ colourMap = Array(BaAA.colors.size, { 0.0 })
+
+ ditherBuffer = Array(h, { IntArray(w, { 0 }) })
+
+ if (BaAA.colors.size == BaAA.RANGE_CGA) {
+ //if (!inverted)
+ BaAA.colcga.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance(colourAlgo, gamma) }
+ //else
+ // BadAppleDemo.colcgaInv.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance() }
+ }
+ else if (BaAA.colors.size == BaAA.RANGE_EXT) {
+ //if (!inverted)
+ BaAA.hexadecaGrey.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance(colourAlgo, gamma) }
+ //else
+ // BadAppleDemo.hexadecaGreyInv.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance() }
+ }
+ else if (BaAA.colors.size == 2) {
+ if (!inverted) {
+ colourMap[0] = BaAA.getColor(0).getLuminance(colourAlgo, gamma)
+ colourMap[1] = BaAA.getColor(1).getLuminance(colourAlgo, gamma)
+ }
+ else {
+ colourMap[0] = BaAA.getColor(1).getLuminance(colourAlgo, gamma)
+ colourMap[1] = BaAA.getColor(0).getLuminance(colourAlgo, gamma)
+ }
+ }
+ else {
+ throw IllegalStateException("Colour mode unknown; unexpected colour range: ${BaAA.colors.size}")
+ }
+ }
+
+ // Brightness, Raw frame (0bXXXXXXX_00_0000000)
+ private val brightnessMap = ArrayList>()
+ private val glyphColMap = HashMap()
+ private val sameLumStartIndices = HashMap()
+ private val sameLumEndIndices = HashMap()
+ private val lumMap = ArrayList()
+
+ private lateinit var imageBuffer: Image
+
+ private var precalcDone = false
+
+ private var fontLumMin = 0
+ private var fontLumMax = 0
+
+ private val exclude = arrayOf(
+ '_'.getAscii(),
+ '\\'.getAscii(),
+ '/'.getAscii()
+ )
+
+ /**
+ * (' ' - '\') * number of colours
+ */
+ private var totalGlyphCount = 0
+
+ override fun precalcFont() {
+ if (!precalcDone) {
+
+ val fontSheetW = fontSheet.width / fontW
+
+ for (bKey in 0..colourMap.size - 1) {
+ for (i in fontRange) {
+ if (exclude.contains(i)) continue
+
+ val glyph = fontSheet.getSubImage(i % fontSheetW, i / fontSheetW).copy()
+ totalGlyphCount++
+
+ var glyphBrightness = 0
+ var perturb = 0
+ val perturbMod = 8
+
+ if (fontRange.endInclusive < 128 && i == 32) { // exclude ' ' if in ASCII mode
+ glyphBrightness = glyph.height * glyph.width * colourMap[0].oneTo256()
+ }
+ else {
+ for (fy in 0..glyph.height - 1) {
+ for (fx in 0..glyph.width - 1) {
+ val pixel = glyph.getColor(fx, fy).a
+ val b: Int
+ if (pixel == 0f)
+ b = colourMap[0].oneTo256()
+ else {
+ b = colourMap[bKey].oneTo256()
+ perturb = (perturb xor (fx xor fy)) % perturbMod
+ }
+
+ glyphBrightness += b.toInt()// - perturb
+ }
+ }
+ }
+
+ brightnessMap.add(
+ Pair(
+ glyphBrightness,
+ (bKey.shl(8) or i.and(0xFF)).toChar()
+ ))
+ glyphColMap.put(
+ (bKey.shl(8) or i.and(0xFF)).toChar(),
+ glyphBrightness
+ )
+ }
+ }
+
+ brightnessMap.sortBy { it.first }
+
+ fontLumMin = brightnessMap.first().first
+
+ // fix for negative brightness
+ for (i in 0..brightnessMap.size - 1) {
+ val first = brightnessMap[i].first - fontLumMin
+ val second = brightnessMap[i].second
+
+ brightnessMap[i] = Pair(first, second)
+ }
+
+ fontLumMin = brightnessMap.first().first
+ fontLumMax = brightnessMap.last().first
+
+ // test print
+ brightnessMap.forEachIndexed { index, it ->
+ println("Brightness ${it.first}\t" +
+ "glyph ${it.second.getAscii().toChar()} " +
+ "col ${it.second.getColourKey()}" +
+ " [$index]")
+ }
+
+ // make sameLumStartIndices and LumMap
+ // define starting point
+ sameLumStartIndices.put(brightnessMap.first().first, 0)
+ sameLumEndIndices.put(brightnessMap.last().first, brightnessMap.size - 1)
+ for (i in 0..brightnessMap.size - 2) {
+ // if (this != next), mark 'this' as endpoint, 'next' as start point
+ val thisLum = brightnessMap[i].first
+ val nextLum = brightnessMap[i + 1].first
+ if (thisLum != nextLum) {
+ sameLumEndIndices.put(thisLum, i)
+ sameLumStartIndices.put(nextLum, i + 1)
+ lumMap.add(thisLum)
+ }
+ }
+ // for edge case where there's only one brightest glyph
+ if (!lumMap.contains(fontLumMax)) lumMap.add(fontLumMax)
+
+ println("Total glyphs: $totalGlyphCount")
+
+ // test print
+ //sameLumStartIndices.forEach { lum, index -> println("Lum $lum, starts at $index") }
+ //sameLumEndIndices.forEach { lum, index -> println("Lum $lum, ends at $index") }
+ //lumMap.forEach { println(it) }
+
+ println("Max brightness: $fontLumMax")
+
+ precalcDone = true
+ }
+ }
+
+ lateinit var ditherBuffer: Array
+
+ val FLOYD_STEINBERG = 1
+ val SIERRA_LITE = 2
+ val SIERRA_2 = 3
+ val SIERRA_3 = 4
+
+ val bayer4Map = arrayOf(1, 9, 3, 11, 13, 5, 15, 7, 4, 12, 2, 10, 16, 8, 14, 6)
+ val bayer4Divisor = bayer4Map.size - 1
+ val bayer4MapSize = Math.sqrt(bayer4Map.size.toDouble()).toInt()
+
+ private fun getBayer4(x: Int, y: Int) = bayer4Map[y * bayer4MapSize + (x % bayer4MapSize)]
+
+ override fun toAscii(rawImage: Image, aaframe: AAFrame) {
+ // draw scale-flagged (getScaledCopy) image to the buffer
+ imageBuffer.graphics.drawImage(rawImage.getScaledCopy(w, h), 0f, 0f)
+
+ // fill buffer
+ for (y in 0..h - 1) {
+ for (x in 0..w - 1) {
+ if (ditherAlgo == 0) {
+ aaframe.drawBuffer(x, y, pickRandomGlyphByLumNoQnt(quantiseLum(bufferPixelFontLum(x, y))))
+ }
+ else {
+ ditherBuffer.set(x, y, bufferPixelFontLum(x, y))
+ }
+ }
+ }
+
+ // dither
+ // ref: http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/
+ if (ditherAlgo > 0) {
+ for (y in 0..h - 1) {
+ for (x in 0..w - 1) {
+ val oldPixel = ditherBuffer.get(x, y)
+ val newPixel = quantiseLum(oldPixel.toInt())
+
+ //println("$oldPixel, $newPixel")
+
+ ditherBuffer.set(x, y, newPixel)
+
+ val error = oldPixel - newPixel
+
+ // floyd-steinberg
+ if (ditherAlgo == FLOYD_STEINBERG) { // no ushr or else it won't work
+ ditherBuffer.set(x + 1, y , ditherBuffer.get(x + 1, y ) + (error.times(7).shr(4)))
+ ditherBuffer.set(x - 1, y + 1, ditherBuffer.get(x - 1, y + 1) + (error.times(3).shr(4)))
+ ditherBuffer.set(x , y + 1, ditherBuffer.get(x , y + 1) + (error.times(5).shr(4)))
+ ditherBuffer.set(x + 1, y + 1, ditherBuffer.get(x + 1, y + 1) + (error.times(1).shr(4)))
+ }
+ // sierra lite
+ else if (ditherAlgo == SIERRA_LITE) {
+ ditherBuffer.set(x + 1, y , ditherBuffer.get(x + 1, y ) + (error.times(2).shr(2)))
+ ditherBuffer.set(x - 1, y + 1, ditherBuffer.get(x - 1, y + 1) + (error.times(1).shr(2)))
+ ditherBuffer.set(x , y + 1, ditherBuffer.get(x , y + 1) + (error.times(1).shr(2)))
+ }
+ // sierra-2
+ else if (ditherAlgo == SIERRA_2) {
+ ditherBuffer.set(x + 1, y , ditherBuffer.get(x + 1, y ) + (error.times(4).shr(4)))
+ ditherBuffer.set(x + 2, y , ditherBuffer.get(x + 2, y ) + (error.times(3).shr(4)))
+ ditherBuffer.set(x - 2, y + 1, ditherBuffer.get(x - 2, y + 1) + (error.times(1).shr(4)))
+ ditherBuffer.set(x - 1, y + 1, ditherBuffer.get(x - 1, y + 1) + (error.times(2).shr(4)))
+ ditherBuffer.set(x , y + 1, ditherBuffer.get(x , y + 1) + (error.times(3).shr(4)))
+ ditherBuffer.set(x + 1, y + 1, ditherBuffer.get(x + 1, y + 1) + (error.times(2).shr(4)))
+ ditherBuffer.set(x + 2, y + 1, ditherBuffer.get(x + 2, y + 1) + (error.times(1).shr(4)))
+ }
+ // sierra-3
+ else if (ditherAlgo == SIERRA_3) {
+ ditherBuffer.set(x + 1, y , ditherBuffer.get(x + 1, y ) + (error.times(5).shr(5)))
+ ditherBuffer.set(x + 2, y , ditherBuffer.get(x + 2, y ) + (error.times(3).shr(5)))
+ ditherBuffer.set(x - 2, y + 1, ditherBuffer.get(x - 2, y + 1) + (error.times(2).shr(5)))
+ ditherBuffer.set(x - 1, y + 1, ditherBuffer.get(x - 1, y + 1) + (error.times(4).shr(5)))
+ ditherBuffer.set(x , y + 1, ditherBuffer.get(x , y + 1) + (error.times(5).shr(5)))
+ ditherBuffer.set(x + 1, y + 1, ditherBuffer.get(x + 1, y + 1) + (error.times(4).shr(5)))
+ ditherBuffer.set(x + 2, y + 1, ditherBuffer.get(x + 2, y + 1) + (error.times(2).shr(5)))
+ ditherBuffer.set(x - 1, y + 2, ditherBuffer.get(x - 1, y + 2) + (error.times(2).shr(5)))
+ ditherBuffer.set(x , y + 2, ditherBuffer.get(x , y + 2) + (error.times(3).shr(5)))
+ ditherBuffer.set(x + 1, y + 2, ditherBuffer.get(x + 1, y + 2) + (error.times(2).shr(5)))
+ }
+ else {
+ throw IllegalArgumentException("Unknown dithering algorithm: $ditherAlgo")
+ }
+ }
+ }
+
+ // ...and draw
+ for (y in 0..h - 1) {
+ for (x in 0..w - 1) {
+ aaframe.drawBuffer(x, y, pickRandomGlyphByLumNoQnt(quantiseLum(ditherBuffer.get(x, y))))
+ }
+ }
+ }
+
+ // clear buffer
+ imageBuffer.graphics.flush()
+ }
+
+ fun Array.set(x: Int, y: Int, value: Int) {
+ if (x >= 0 && y >= 0 && x < w && y < h)
+ this[y][x] = value
+ }
+
+ fun Array.get(x: Int, y: Int): Int {
+ if (x >= 0 && y >= 0 && x < w && y < h)
+ return this[y][x]
+ else return 0
+ }
+
+ fun bufferPixelFontLum(x: Int, y: Int): Int {
+ val lum = // [0.0 - 1.0]
+ imageBuffer.getColor(x, y).getLuminance(colourAlgo, gamma)
+
+ val delta = fontLumMax - fontLumMin
+ return (fontLumMin + delta * lum).roundInt()
+ }
+
+ fun Double.toFontLum(): Int {
+ val delta = fontLumMax - fontLumMin
+ return (fontLumMin + delta * this).roundInt()
+ }
+
+ /**
+ * @param fontLum : int ranged 0..fontLumMax
+ * @return indexed lum 0..fontLumMax
+ */
+ private fun quantiseLum(fontLum: Int): Int {
+ val interval = binarySearchInterval(fontLum)
+
+ if (interval.first == interval.second)
+ return lumMap[interval.first]
+ else {
+ // compare two and return closest
+ if (fontLum - lumMap[interval.first] < lumMap[interval.second] - fontLum)
+ return lumMap[interval.first]
+ else
+ return lumMap[interval.second]
+ }
+ }
+
+ fun Int.prevLum() = lumMap[binarySearchInterval(this).first]
+
+ fun Int.nextLum() = lumMap[binarySearchInterval(this).second]
+
+ /**
+ * e.g.
+ *
+ * 0 1 4 5 7 , find 3
+ *
+ * will return (1, 2), which corresponds value (1, 4) of which input value 3 is in between.
+ */
+ private fun binarySearchInterval(lum: Int): Pair {
+ var low: Int = 0
+ var high: Int = lumMap.size - 1
+
+ while (low <= high) {
+ val mid = (low + high).ushr(1)
+ val midVal = lumMap[mid]
+
+ if (lum < midVal)
+ high = mid - 1
+ else if (lum > midVal)
+ low = mid + 1
+ else
+ return Pair(mid, mid)
+ }
+
+ return Pair(Math.max(high, 0), Math.min(low, lumMap.size - 1))
+ }
+
+ private fun pickRandomGlyphByLumNoQnt(fontLum: Int): Char {
+ if ((fontLum == 0 && !inverted) || (fontLum == fontLumMax && inverted))
+ return 0x20.toChar() // ' ' with colour index 'zero'
+
+ val indexStart = sameLumStartIndices[fontLum]!!
+ val indexEnd = sameLumEndIndices[fontLum]!!
+
+ val index = Random().nextInt(indexEnd - indexStart + 1) + indexStart
+ return brightnessMap[index].second
+ }
+
+ fun Char.getColourKey() = this.toInt().ushr(8).and(0x1F)
+ fun Char.getAscii() = this.toInt().and(0xFF)
+ fun Double.oneTo256() = this.times(255).roundInt()
+}
+
+
+
+
+fun Double.ceilInt() = Math.ceil(this).toInt()
+fun Double.floorInt() = Math.floor(this).toInt()
+fun Double.roundInt() = Math.round(this).toInt()
+fun Float.sqr() = this * this
+fun Double.sqrt() = Math.sqrt(this)
+fun Int.sqrt() = Math.sqrt(this.toDouble())
+fun Int.abs() = Math.abs(this)
+fun Double.powerOf(exp: Double) = Math.pow(this, exp)
+fun Float.powerOf(exp: Double) = Math.pow(this.toDouble(), exp)
+fun Int.sqr() = this * this
+infix fun Int.min(other: Int) = Math.min(this, other)
+infix fun Int.max(other: Int) = Math.max(this, other)
+fun Color.getLuminance(colourAlgo: Int, gamma: Double): Double =
+ if (redByte > 0xF8 && greenByte >= 0xF8 && blueByte >= 0xF8) // mask irregularity in white colour
+ 1.0
+ else if (redByte < 0x8 && greenByte < 0x8 && blueByte < 0x8)
+ 0.0
+ else
+ if (colourAlgo == 1)
+ (0.299 * r.sqr() + 0.587 * g.sqr() + 0.114 * b.sqr()).powerOf(0.5 * gamma)
+ else if (colourAlgo == 0)
+ Math.pow((r + g + b) / 3.0, gamma)
+ else
+ throw IllegalArgumentException("Unknown luminance algorithm: $colourAlgo")
diff --git a/src/net/torvald/aa/ImageToAASubGlyph4.kt b/src/net/torvald/aa/ImageToAASubGlyph4.kt
new file mode 100644
index 0000000..c5c097b
--- /dev/null
+++ b/src/net/torvald/aa/ImageToAASubGlyph4.kt
@@ -0,0 +1,523 @@
+package net.torvald.aa
+
+import net.torvald.aa.demoplayer.BaAA
+import org.newdawn.slick.*
+import java.util.*
+
+/**
+ * How it works:
+ *
+ * Divide the single glyph into 2 by 2, pre-calculate the luminosity of each cell and try to find closest matching
+ * one from the 2x2 pixels in input image.
+ *
+ * Pre-calculation:
+ *
+ * Scan each glyph on the spritesheet provided, divide the single glyph into 2x2 and calculate the luminosity.
+ *
+ * Created by minjaesong on 16-08-10.
+ */
+class ImageToAASubGlyph4 : AsciiAlgo {
+
+ private var w = 0
+ private var h = 0
+ private lateinit var fontSheet: SpriteSheet
+ override var fontW = 0
+ override var fontH = 0
+ private var inverted = false
+ private var gamma = 0.0
+ private var ditherAlgo = 0
+ private var colourAlgo = 0
+
+ private lateinit var fontRange: IntRange
+
+ private lateinit var colourMap: Array
+
+ fun setProp(
+ width: Int, height: Int,
+ font: SpriteSheet, fontW: Int, fontH: Int,
+ inverted: Boolean, gamma: Double, dither: Int, fullCodePage: Boolean, colourAlgo: Int) {
+ w = width * 2
+ h = height * 2
+ this.fontW = fontW
+ this.fontH = fontH
+ this.fontSheet = font
+ this.inverted = inverted
+ this.gamma = gamma
+ ditherAlgo = dither
+ this.colourAlgo = colourAlgo
+
+ if (fullCodePage)
+ fontRange = 1..255
+ else
+ fontRange = 32..126
+
+ imageBuffer = Image(w, h)
+
+ // re-invert colourmap so that the calculation stay normal when inverted
+ colourMap = Array(BaAA.colors.size, { 0.0 })
+
+ ditherBuffer = Array(h, { IntArray(w, { 0 }) })
+
+ if (BaAA.colors.size == BaAA.RANGE_CGA) {
+ if (!inverted)
+ BaAA.colcga.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance(colourAlgo, gamma) }
+ else
+ BaAA.colcgaInv.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance(colourAlgo, gamma) }
+ }
+ else if (BaAA.colors.size == BaAA.RANGE_EXT) {
+ if (!inverted)
+ BaAA.hexadecaGrey.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance(colourAlgo, gamma) }
+ else
+ BaAA.hexadecaGreyInv.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance(colourAlgo, gamma) }
+ }
+ else if (BaAA.colors.size == 2) {
+ if (!inverted) {
+ colourMap[0] = BaAA.getColor(0).getLuminance(colourAlgo, gamma)
+ colourMap[1] = BaAA.getColor(1).getLuminance(colourAlgo, gamma)
+ }
+ else {
+ colourMap[0] = BaAA.getColor(1).getLuminance(colourAlgo, gamma)
+ colourMap[1] = BaAA.getColor(0).getLuminance(colourAlgo, gamma)
+ }
+ }
+ else {
+ throw IllegalStateException("Colour mode unknown; unexpected colour range: ${BaAA.colors.size}")
+ }
+ }
+
+ private val brightnessMap = ArrayList>() // Long: stored TL-TR-BL-BR
+ private val glyphColMap = HashMap()
+ private val sameLumStartIndices = HashMap()
+ private val sameLumEndIndices = HashMap()
+ private val lumMapMatrix = Array>(4, { ArrayList() }) // stores unique elems, by position lumMapMatrix[position][index]
+ private val lumMap = ArrayList() // Long: stored TL-TR-BL-BR; stores unique elems, regardless of position
+ private val lumMapAll = ArrayList() // each element of Luminosity, unique values regardless of position
+
+ private lateinit var imageBuffer: Image
+
+ private var precalcDone = false
+
+ private var lumMin = 0x7FFFFFFF
+ private var lumMax = 0
+
+ private val exclude = arrayOf(
+ '_'.getAscii(),
+ '-'.getAscii(),
+ '|'.getAscii(),
+ 8, 9, 10
+ )
+
+ /**
+ * (' ' - '\') * number of colours
+ */
+ private var totalGlyphCount = 0
+
+ override fun precalcFont() {
+ if (!precalcDone) {
+
+ val fontSheetW = fontSheet.width / fontW
+
+ for (bKey in 0..colourMap.size - 1) {
+ for (i in fontRange) {
+ if (exclude.contains(i)) continue
+
+ val glyph = fontSheet.getSubImage(i % fontSheetW, i / fontSheetW).copy()
+ totalGlyphCount++
+
+ var glyphLumTopLeft = 0
+ var glyphLumTopRight = 0
+ var glyphLumBottomLeft = 0
+ var glyphLumBottomRight = 0
+
+ if (fontRange.endInclusive < 128 && i == 32) { // exclude ' ' if in ASCII mode
+ glyphLumTopLeft = if (!inverted) 0 else glyph.height * glyph.width * colourMap[0].oneTo256()
+
+ glyphLumTopRight = glyphLumTopLeft
+ glyphLumBottomLeft = glyphLumTopLeft
+ glyphLumBottomRight = glyphLumTopLeft
+ }
+ else {
+ // TODO more weighting on clustered pixels
+
+ // top-left part
+ for (fx in 0..glyph.width / 2 - 1) {
+ for (fy in 0..glyph.height / 2 - 1) {
+ val pixel = glyph.getColor(fx, fy).a
+ val b: Int
+ if (pixel == 0f)
+ b = colourMap[0].oneTo256()
+ else {
+ b = colourMap[bKey].oneTo256()
+ }
+
+ glyphLumTopLeft += b
+ }
+ }
+ // bottom-left part
+ for (fx in 0..glyph.width / 2 - 1) {
+ for (fy in glyph.height / 2..glyph.height - 1) {
+ val pixel = glyph.getColor(fx, fy).a
+ val b: Int
+ if (pixel == 0f)
+ b = colourMap[0].oneTo256()
+ else {
+ b = colourMap[bKey].oneTo256()
+ }
+
+ glyphLumBottomLeft += b
+ }
+ }
+ // top-right part
+ for (fx in glyph.width / 2..glyph.width - 1) {
+ for (fy in 0..glyph.height / 2 - 1) {
+ val pixel = glyph.getColor(fx, fy).a
+ val b: Int
+ if (pixel == 0f)
+ b = colourMap[0].oneTo256()
+ else {
+ b = colourMap[bKey].oneTo256()
+ }
+
+ glyphLumTopRight += b
+ }
+ }
+ // bottom-right part
+ for (fx in glyph.width / 2..glyph.width - 1) {
+ for (fy in glyph.height / 2..glyph.height - 1) {
+ val pixel = glyph.getColor(fx, fy).a
+ val b: Int
+ if (pixel == 0f)
+ b = colourMap[0].oneTo256()
+ else {
+ b = colourMap[bKey].oneTo256()
+ }
+
+ glyphLumBottomRight += b
+ }
+ }
+ }
+
+ val luminosityPacked: Long =
+ glyphLumTopLeft.toLong().shl(48) or
+ glyphLumTopRight.toLong().shl(32) or
+ glyphLumBottomLeft.toLong().shl(16) or
+ glyphLumBottomRight.toLong()
+
+ brightnessMap.add(
+ Pair(
+ luminosityPacked,
+ (bKey.shl(8) or i.and(0xFF)).toChar()
+ ))
+ glyphColMap.put(
+ (bKey.shl(8) or i.and(0xFF)).toChar(),
+ luminosityPacked
+ )
+ }
+ }
+
+ brightnessMap.sortBy { it.first } // will be sorted by TL then TR then BL then BR
+
+ // fill lumMapX(top)/Y(bottom)
+ brightnessMap.forEach {
+ if (!lumMap.contains(it.first)) lumMap.add(it.first)
+
+ if (!lumMapMatrix[POS_TL].contains(it.first.getTopLeft())) lumMapMatrix[POS_TL].add(it.first.getTopLeft())
+ if (!lumMapMatrix[POS_TR].contains(it.first.getTopRight())) lumMapMatrix[POS_TR].add(it.first.getTopRight())
+ if (!lumMapMatrix[POS_BL].contains(it.first.getBottomLeft())) lumMapMatrix[POS_BL].add(it.first.getBottomLeft())
+ if (!lumMapMatrix[POS_BR].contains(it.first.getBottomRight())) lumMapMatrix[POS_BR].add(it.first.getBottomRight())
+
+ if (!lumMapAll.contains(it.first.getTopLeft())) lumMapAll.add(it.first.getTopLeft())
+ if (!lumMapAll.contains(it.first.getTopRight())) lumMapAll.add(it.first.getTopRight())
+ if (!lumMapAll.contains(it.first.getBottomLeft())) lumMapAll.add(it.first.getBottomLeft())
+ if (!lumMapAll.contains(it.first.getBottomRight())) lumMapAll.add(it.first.getBottomRight())
+ }
+
+ // sort everything required
+ lumMapMatrix.forEach { it.sort() }
+ lumMapAll.sort()
+ lumMap.sort()
+
+ // get min/max
+ lumMin = lumMapAll.first()
+ lumMax = lumMapAll.last()
+
+ brightnessMap.forEachIndexed { index, it ->
+ println("Brightness " +
+ "${it.first.getTopLeft()}+${it.first.getTopRight()}+" +
+ "${it.first.getBottomLeft()}+${it.first.getBottomRight()}\t" +
+ "glyph ${it.second.getAscii().toChar()} " +
+ "col ${it.second.getColourKey()}" +
+ " [$index]")
+ }
+
+ println("Min: $lumMin, Max: $lumMax")
+
+ // make sameLumStartIndices and LumMap
+ // define starting point
+ sameLumStartIndices.put(brightnessMap.first().first, 0)
+ sameLumEndIndices.put(brightnessMap.last().first, brightnessMap.size - 1)
+ for (i in 0..brightnessMap.size - 2) {
+ // if (this != next), mark 'this' as endpoint, 'next' as start point
+ val thisLum = brightnessMap[i].first
+ val nextLum = brightnessMap[i + 1].first
+ if (thisLum != nextLum) {
+ sameLumEndIndices.put(thisLum, i)
+ sameLumStartIndices.put(nextLum, i + 1)
+ }
+ }
+
+ precalcDone = true
+ }
+ }
+
+ private fun Long.getTopLeft() = this.ushr(48).toInt() and 0xFFFF
+ private fun Long.getTopRight() = this.ushr(32).toInt() and 0xFFFF
+ private fun Long.getBottomLeft() = this.ushr(16).toInt() and 0xFFFF
+ private fun Long.getBottomRight() = this.toInt() and 0xFFFF
+
+ lateinit var ditherBuffer: Array
+
+ val FLOYD_STEINBERG = 1
+ val SIERRA_LITE = 2
+ val SIERRA_2 = 3
+
+ private val ditherHack = 2
+
+ override fun toAscii(rawImage: Image, aaframe: AAFrame) {
+ // draw scale-flagged (getScaledCopy) image to the buffer
+ imageBuffer.graphics.drawImage(rawImage.getScaledCopy(w, h), 0f, 0f)
+
+ // fill buffer (scan size: W*H of aaframe)
+ for (y in 0..h - 1 step 2) {
+ for (x in 0..w - 1 step 2) {
+ if (ditherAlgo == 0) {
+ val lumTL = bufferPixelFontLum(x, y)
+ val lumTR = bufferPixelFontLum(x + 1, y)
+ val lumBL = bufferPixelFontLum(x, y + 1)
+ val lumBR = bufferPixelFontLum(x + 1, y + 1)
+ val qnt = findNearestLum(lumTL, lumTR, lumBL, lumBR)
+ val char = pickRandomGlyphByLumNoQnt(qnt)
+
+ aaframe.drawBuffer(x.shr(1), y.shr(1), char)
+ }
+ else {
+ ditherBuffer.set(x , y , bufferPixelFontLum(x , y ))
+ ditherBuffer.set(x , y + 1, bufferPixelFontLum(x , y + 1))
+ ditherBuffer.set(x + 1, y , bufferPixelFontLum(x + 1, y ))
+ ditherBuffer.set(x + 1, y + 1, bufferPixelFontLum(x + 1, y + 1))
+ }
+ }
+ }
+
+ // dither
+ // ref: http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/
+ if (ditherAlgo > 0) {
+ // scan for ditherBuffer that is strecthed to Y
+ for (y in 0..h - 1 step ditherHack) {
+ for (x in 0..w - 1 step ditherHack) {
+ val oldPixel = ditherBuffer.get(x, y)
+ val newPixel = findNearest(oldPixel)
+
+ ditherBuffer.set(x, y, newPixel)
+
+ val error = oldPixel - newPixel
+
+ // dither glyph-wise rather than pixel-wise
+ for (haxX in 0..ditherHack - 1) {
+ for (haxY in 0..ditherHack - 1) {
+ // floyd-steinberg
+ if (ditherAlgo == FLOYD_STEINBERG) {
+ ditherBuffer.set(x + 1 + haxX, y + haxY, ditherBuffer.get(x + 1 + haxX, y + haxY) + (error.times(7).shr(4)))
+ ditherBuffer.set(x - 1 + haxX, y + 1 + haxY, ditherBuffer.get(x - 1 + haxX, y + 1 + haxY) + (error.times(3).shr(4)))
+ ditherBuffer.set(x + haxX, y + 1 + haxY, ditherBuffer.get(x + haxX, y + 1 + haxY) + (error.times(5).shr(4)))
+ ditherBuffer.set(x + 1 + haxX, y + 1 + haxY, ditherBuffer.get(x + 1 + haxX, y + 1 + haxY) + (error.times(1).shr(4)))
+ }
+ // sierra lite
+ else if (ditherAlgo == SIERRA_LITE) {
+ ditherBuffer.set(x + 1 + haxX, y + haxY, ditherBuffer.get(x + 1 + haxX, y + haxY) + (error.times(2).shr(2)))
+ ditherBuffer.set(x - 1 + haxX, y + 1 + haxY, ditherBuffer.get(x - 1 + haxX, y + 1 + haxY) + (error.times(1).shr(2)))
+ ditherBuffer.set(x + haxX, y + 1 + haxY, ditherBuffer.get(x + haxX, y + 1 + haxY) + (error.times(1).shr(2)))
+ }
+ // sierra-2
+ else if (ditherAlgo == SIERRA_2) {
+ ditherBuffer.set(x + 1 + haxX, y + haxY, ditherBuffer.get(x + 1 + haxX, y + haxY) + (error.times(4).shr(4)))
+ ditherBuffer.set(x + 2 + haxX, y + haxY, ditherBuffer.get(x + 2 + haxX, y + haxY) + (error.times(3).shr(4)))
+ ditherBuffer.set(x - 2 + haxX, y + 1 + haxY, ditherBuffer.get(x - 2 + haxX, y + 1 + haxY) + (error.times(1).shr(4)))
+ ditherBuffer.set(x - 1 + haxX, y + 1 + haxY, ditherBuffer.get(x - 1 + haxX, y + 1 + haxY) + (error.times(2).shr(4)))
+ ditherBuffer.set(x + haxX, y + 1 + haxY, ditherBuffer.get(x + haxX, y + 1 + haxY) + (error.times(3).shr(4)))
+ ditherBuffer.set(x + 1 + haxX, y + 1 + haxY, ditherBuffer.get(x + 1 + haxX, y + 1 + haxY) + (error.times(2).shr(4)))
+ ditherBuffer.set(x + 2 + haxX, y + 1 + haxY, ditherBuffer.get(x + 2 + haxX, y + 1 + haxY) + (error.times(1).shr(4)))
+ }
+ else {
+ throw IllegalArgumentException("Unknown dithering algorithm: $ditherAlgo")
+ }
+ }
+ }
+ }
+ }
+
+ // ...and draw
+ for (y in 0..h - 1 step 2) {
+ for (x in 0..w - 1 step 2) {
+ val lumTL = ditherBuffer.get(x, y)
+ val lumTR = ditherBuffer.get(x + 1, y)
+ val lumBL = ditherBuffer.get(x, y + 1)
+ val lumBR = ditherBuffer.get(x + 1, y + 1)
+ val char = pickRandomGlyphByLumNoQnt(findNearestLum(lumTL, lumTR, lumBL, lumBR))
+
+ aaframe.drawBuffer(x.shr(1), y.shr(1), char)
+ }
+ }
+ }
+
+ // clear buffer
+ imageBuffer.graphics.flush()
+ }
+
+ fun Array.set(x: Int, y: Int, value: Int) {
+ if (x >= 0 && y >= 0 && x < w && y < h)
+ this[y][x] = value
+ }
+
+ fun Array.get(x: Int, y: Int): Int {
+ if (x >= 0 && y >= 0 && x < w && y < h)
+ return this[y][x]
+ else return 0
+ }
+
+ fun bufferPixelFontLum(x: Int, y: Int): Int {
+ val lum = // [0.0 - 1.0]
+ imageBuffer.getColor(x, y).getLuminance(colourAlgo, gamma)
+
+ val delta = lumMax - lumMin
+ return (lumMin + delta * lum).roundInt()
+ }
+
+ /**
+ * @param fontLum : int ranged 0..fontLumMax
+ * @return indexed lum 0..fontLumMax
+ */
+ private fun findNearestLum(lumTopLeft: Int, lumTopRight: Int, lumBottomLeft: Int, lumBottomRight: Int): Long {
+ // find closest: "Closest pair of points problem"
+ // TODO better algorithm
+ // brute force
+ // for some reason, lambda expression is actually slower
+ var distMin = 0x7FFFFFFF
+ var lum = 0L
+ var dist: Int = 0
+ var otherLum: Long
+
+ //println("Lum: $lumTopLeft+$lumTopRight+$lumBottomLeft+$lumBottomRight")
+
+ //println("$argMin, $argMax")
+
+ for (cnt in 0..lumMap.size - 1) {
+ otherLum = lumMap[cnt]
+ dist = // euclidean norm on 2D, squared
+ (lumTopLeft - otherLum.getTopLeft() ) * (lumTopLeft - otherLum.getTopLeft() ) +
+ (lumTopRight - otherLum.getTopRight() ) * (lumTopRight - otherLum.getTopRight() ) +
+ (lumBottomLeft - otherLum.getBottomLeft() ) * (lumBottomLeft - otherLum.getBottomLeft() ) +
+ (lumBottomRight - otherLum.getBottomRight()) * (lumBottomRight - otherLum.getBottomRight())
+
+ //println("dist: $dist")
+
+ if (dist < distMin) {
+ distMin = dist
+ lum = lumMap[cnt]
+ }
+ }
+
+ return lum
+ }
+
+ private val POS_TL = 0
+ private val POS_TR = 1
+ private val POS_BL = 2
+ private val POS_BR = 3
+
+ private fun findNearest(lum: Int): Int {
+ val interval = binarySearchInterval(lumMapAll, lum)
+
+ if (interval.first == interval.second)
+ return lumMapAll[interval.first]
+ else {
+ // compare two and return closest
+ if (lum - lumMapAll[interval.first] < lumMapAll[interval.second] - lum)
+ return lumMapAll[interval.first]
+ else
+ return lumMapAll[interval.second]
+ }
+ }
+
+ private fun findNearest(sortedList: List, element: Int): Int {
+ val interval = binarySearchInterval(sortedList, element)
+
+ if (interval.first == interval.second)
+ return sortedList[interval.first]
+ else {
+ // compare two and return closest
+ if (element - sortedList[interval.first] < sortedList[interval.second] - element)
+ return sortedList[interval.first]
+ else
+ return sortedList[interval.second]
+ }
+ }
+
+ /**
+ * e.g.
+ *
+ * 0 1 4 5 7 , find 3
+ *
+ * will return (1, 2), which corresponds value (1, 4) of which input value 3 is in between.
+ */
+ private fun binarySearchInterval(list: List, lum: Int): Pair {
+ var low: Int = 0
+ var high: Int = list.size - 1
+
+ while (low <= high) {
+ val mid = (low + high).ushr(1)
+ val midVal = list[mid]
+
+ if (lum < midVal)
+ high = mid - 1
+ else if (lum > midVal)
+ low = mid + 1
+ else
+ return Pair(mid, mid)
+ }
+
+ return Pair(Math.max(high, 0), Math.min(low, list.size - 1))
+ }
+
+ private fun pickRandomGlyphByLumNoQnt(fontLum: Long): Char {
+ if (fontLum == 0L && !inverted)
+ return 0x20.toChar() // ' ' with colour index 'zero'
+
+ val indexStart = sameLumStartIndices[fontLum]
+ val indexEnd = sameLumEndIndices[fontLum]
+
+ if (indexStart == null && indexEnd == null)
+ System.err.println("kotlin.KotlinNullPointerException: indexStart and indexEnd is null.")
+ else if (indexStart == null)
+ System.err.println("kotlin.KotlinNullPointerException: indexStart is null.")
+ else if (indexEnd == null)
+ System.err.println("kotlin.KotlinNullPointerException: indexEnd is null.")
+
+ if (indexStart == null || indexEnd == null) {
+ System.err.println(" fontLum: ${fontLum.getTopLeft()}+${fontLum.getTopRight()}+${fontLum.getBottomLeft()}+${fontLum.getBottomRight()}")
+ }
+
+ if (indexStart != null && indexEnd != null) {
+ val index = Random().nextInt(indexEnd - indexStart + 1) + indexStart
+ return brightnessMap[index].second
+ }
+
+ throw NullPointerException()
+ }
+
+ fun Char.getColourKey() = this.toInt().ushr(8).and(0x1F)
+ fun Char.getAscii() = this.toInt().and(0xFF)
+ fun Double.oneTo256() = this.times(255).roundInt()
+ fun Long.toShorts() = arrayOf(this.getTopLeft(), this.getTopRight(), this.getBottomLeft(), this.getBottomRight())
+
+}
diff --git a/src/net/torvald/aa/ImageToAASubGlyphArb.kt b/src/net/torvald/aa/ImageToAASubGlyphArb.kt
new file mode 100644
index 0000000..dc0cfc1
--- /dev/null
+++ b/src/net/torvald/aa/ImageToAASubGlyphArb.kt
@@ -0,0 +1,481 @@
+package net.torvald.aa
+
+import net.torvald.aa.demoplayer.BaAA
+import org.newdawn.slick.*
+import java.util.*
+
+/**
+ * Generalised version of ImageToAASubGlyph4
+ * @param divW horizontal division
+ * @param divH vertical division
+ *
+ *
+ *
+ * Note: weird enough, if divW >= 4 || divH >= 4, it has tendency of pixelating picture instead of proper antialias.
+ *
+ *
+ * Created by minjaesong on 16-08-10.
+ */
+class ImageToAASubGlyphArb(val divW: Int, val divH: Int) : AsciiAlgo {
+
+ private var w = 0
+ private var h = 0
+ private lateinit var fontSheet: SpriteSheet
+ override var fontW = 0
+ override var fontH = 0
+ private var inverted = false
+ private var gamma = 0.0
+ private var ditherAlgo = 0
+ private var colourAlgo = 0
+
+ private val divSize: Int get() = divW * divH
+
+ private lateinit var fontRange: IntRange
+
+ private lateinit var colourMap: Array
+
+ fun setProp(
+ width: Int, height: Int,
+ font: SpriteSheet, fontW: Int, fontH: Int,
+ inverted: Boolean, gamma: Double, dither: Int, fullCodePage: Boolean, colourAlgo: Int) {
+ w = width * divW
+ h = height * divH
+ this.fontW = fontW
+ this.fontH = fontH
+ this.fontSheet = font
+ this.inverted = inverted
+ this.gamma = gamma
+ ditherAlgo = dither
+ this.colourAlgo = colourAlgo
+
+ if (fullCodePage)
+ fontRange = 1..255
+ else
+ fontRange = 32..126
+
+ imageBuffer = Image(w, h)
+
+ // re-invert colourmap so that the calculation stay normal when inverted
+ colourMap = Array(BaAA.colors.size, { 0.0 })
+
+ ditherBuffer = Array(h, { IntArray(w, { 0 }) })
+
+ if (BaAA.colors.size == BaAA.RANGE_CGA) {
+ //if (!inverted)
+ BaAA.colcga.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance(colourAlgo, gamma) }
+ //else
+ // BadAppleDemo.colcgaInv.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance() }
+ }
+ else if (BaAA.colors.size == BaAA.RANGE_EXT) {
+ //if (!inverted)
+ BaAA.hexadecaGrey.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance(colourAlgo, gamma) }
+ //else
+ // BadAppleDemo.hexadecaGreyInv.forEachIndexed { i, c -> colourMap[i] = Color(c, c, c).getLuminance() }
+ }
+ else if (BaAA.colors.size == 2) {
+ if (!inverted) {
+ colourMap[0] = BaAA.getColor(0).getLuminance(colourAlgo, gamma)
+ colourMap[1] = BaAA.getColor(1).getLuminance(colourAlgo, gamma)
+ }
+ else {
+ colourMap[0] = BaAA.getColor(1).getLuminance(colourAlgo, gamma)
+ colourMap[1] = BaAA.getColor(0).getLuminance(colourAlgo, gamma)
+ }
+ }
+ else {
+ throw IllegalStateException("Colour mode unknown; unexpected colour range: ${BaAA.colors.size}")
+ }
+ }
+
+ private val brightnessMap = ArrayList>()
+ private val glyphColMap = HashMap()
+ private val sameLumStartIndices = HashMap()
+ private val sameLumEndIndices = HashMap()
+ private val lumMapMatrix = Array>(divSize, { ArrayList() }) // stores unique elems, by position lumMapMatrix[position][index]
+ private val lumMap = ArrayList() // stores unique elems, regardless of position
+ private val lumMapAll = ArrayList() // each element of Luminosity, unique values regardless of position
+
+ private lateinit var imageBuffer: Image
+
+ private var precalcDone = false
+
+ private var lumMin = 0x7FFFFFFF
+ private var lumMax = 0
+
+ private val exclude = arrayOf(
+ '|'.getAscii(),
+ '_'.getAscii(),
+ 8, 9, 10
+ )
+
+ /**
+ * (' ' - '\') * number of colours
+ */
+ private var totalGlyphCount = 0
+
+ override fun precalcFont() {
+ if (!precalcDone) {
+
+ val fontSheetW = fontSheet.width / fontW
+ val rangesX = Array(divW, {
+ getStartAndEndInclusivePoints(fontW, divW, it) })
+ val rangesY = Array(divH, {
+ getStartAndEndInclusivePoints(fontH, divH, it) })
+
+
+ rangesX.forEach { println("range x: $it") }
+ rangesY.forEach { println("range y: $it") }
+
+ for (bKey in 0..colourMap.size - 1) {
+ for (i in fontRange) {
+ if (exclude.contains(i)) continue
+
+ val glyph = fontSheet.getSubImage(i % fontSheetW, i / fontSheetW).copy()
+ totalGlyphCount++
+
+ var lumCalcBuffer = Luminosity(divSize, { 0 })
+
+ if (fontRange.endInclusive < 128 && i == 32) { // exclude ' ' if in ASCII mode
+ if (!inverted)
+ lumCalcBuffer = Luminosity(divSize, { 0 })
+ else
+ lumCalcBuffer = Luminosity(divSize, { glyph.height * glyph.width * colourMap[0].oneTo256() })
+ }
+ else {
+ for (area in 0..divSize - 1) {
+ for (fx in rangesX[area % divW]) {
+ for (fy in rangesY[area / divW]) {
+ val pixel = glyph.getColor(fx, fy).a
+ val b: Int
+ if (pixel == 0f)
+ b = colourMap[0].oneTo256()
+ else {
+ b = colourMap[bKey].oneTo256()
+ }
+
+ lumCalcBuffer[area] += b
+ }
+ }
+ }
+ }
+
+ brightnessMap.add(
+ Pair(
+ lumCalcBuffer,
+ (bKey.shl(8) or i.and(0xFF)).toChar()
+ ))
+ glyphColMap.put(
+ (bKey.shl(8) or i.and(0xFF)).toChar(),
+ lumCalcBuffer
+ )
+ }
+ }
+
+ brightnessMap.sortBy { it.first } // will be sorted by TL then TR then BL then BR
+
+ // fill lumMapX(top)/Y(bottom)
+ brightnessMap.forEach {
+ if (!lumMap.contains(it.first)) lumMap.add(it.first)
+
+ it.first.forEachIndexed { i, value ->
+ if (!lumMapMatrix[i].contains(value)) lumMapMatrix[i].add(value)
+ }
+
+ it.first.forEach { if (!lumMapAll.contains(it)) lumMapAll.add(it) }
+ }
+
+ // sort everything required
+ lumMapMatrix.forEach { it.sort() }
+ lumMapAll.sort()
+ lumMap.sort()
+
+ // get min/max
+ lumMin = lumMapAll.first()
+ lumMax = lumMapAll.last()
+
+ brightnessMap.forEachIndexed { index, it ->
+ println("${it.first}\tglyph ${it.second.getAscii().toChar()}" +
+ " col ${it.second.getColourKey()}" +
+ " [$index]")
+ }
+ //lumMap.forEach { println(it) }
+
+ println("Min: $lumMin, Max: $lumMax")
+
+ // make sameLumStartIndices and LumMap
+ // define starting point
+ sameLumStartIndices.put(brightnessMap.first().first, 0)
+ sameLumEndIndices.put(brightnessMap.last().first, brightnessMap.size - 1)
+ for (i in 0..brightnessMap.size - 2) {
+ // if (this != next), mark 'this' as endpoint, 'next' as start point
+ val thisLum = brightnessMap[i].first
+ val nextLum = brightnessMap[i + 1].first
+ if (thisLum != nextLum) {
+ sameLumEndIndices.put(thisLum, i)
+ sameLumStartIndices.put(nextLum, i + 1)
+ }
+ }
+
+ //sameLumStartIndices.forEach { luminosity, i -> println("$luminosity, starts from $i") }
+ //sameLumEndIndices.forEach { luminosity, i -> println("$luminosity, ends at $i") }
+
+ precalcDone = true
+ }
+ }
+
+ lateinit var ditherBuffer: Array
+
+ val FLOYD_STEINBERG = 1
+ val SIERRA_LITE = 2
+ val SIERRA_2 = 3
+
+ override fun toAscii(rawImage: Image, aaframe: AAFrame) {
+ // draw scale-flagged (getScaledCopy) image to the buffer
+ imageBuffer.graphics.drawImage(rawImage.getScaledCopy(w, h), 0f, 0f)
+
+ // fill buffer (scan size: W*H of aaframe)
+ for (y in 0..h - 1 step divH) {
+ for (x in 0..w - 1 step divW) {
+ if (ditherAlgo == 0) {
+ val lum = Luminosity(divSize, { bufferPixelFontLum(x + it % divW, y + it / divW) })
+ val qntLum = findNearestLum(lum)
+ //val qntLum = brightnessMap.last().first
+ val char = pickRandomGlyphByLumNoQnt(qntLum)
+
+ aaframe.drawBuffer(x / divW, y / divH, char)
+ }
+ else {
+ for (ySub in 0..divH - 1)
+ for (xSub in 0..divW - 1)
+ ditherBuffer.set(x + xSub, y + ySub, bufferPixelFontLum(x + xSub, y + ySub))
+ }
+ }
+ }
+
+ // dither
+ // ref: http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/
+ if (ditherAlgo > 0) {
+ // scan for ditherBuffer that is strecthed to Y
+ for (y in 0..h - 1 step divH) {
+ for (x in 0..w - 1 step divW) {
+ val oldPixel = ditherBuffer.get(x, y)
+ val newPixel = findNearest(oldPixel)
+
+ ditherBuffer.set(x, y, newPixel)
+
+ val error = oldPixel - newPixel
+
+ // dither glyph-wise rather than pixel-wise
+ for (haxX in 0..divW - 1) {
+ for (haxY in 0..divH - 1) {
+ // floyd-steinberg
+ if (ditherAlgo == FLOYD_STEINBERG) {
+ ditherBuffer.set(x + 1 + haxX, y + haxY, ditherBuffer.get(x + 1 + haxX, y + haxY) + (error.times(7).shr(4)))
+ ditherBuffer.set(x - 1 + haxX, y + 1 + haxY, ditherBuffer.get(x - 1 + haxX, y + 1 + haxY) + (error.times(3).shr(4)))
+ ditherBuffer.set(x + haxX, y + 1 + haxY, ditherBuffer.get(x + haxX, y + 1 + haxY) + (error.times(5).shr(4)))
+ ditherBuffer.set(x + 1 + haxX, y + 1 + haxY, ditherBuffer.get(x + 1 + haxX, y + 1 + haxY) + (error.times(1).shr(4)))
+ }
+ // sierra lite
+ else if (ditherAlgo == SIERRA_LITE) {
+ ditherBuffer.set(x + 1 + haxX, y + haxY, ditherBuffer.get(x + 1 + haxX, y + haxY) + (error.times(2).shr(2)))
+ ditherBuffer.set(x - 1 + haxX, y + 1 + haxY, ditherBuffer.get(x - 1 + haxX, y + 1 + haxY) + (error.times(1).shr(2)))
+ ditherBuffer.set(x + haxX, y + 1 + haxY, ditherBuffer.get(x + haxX, y + 1 + haxY) + (error.times(1).shr(2)))
+ }
+ // sierra-2
+ else if (ditherAlgo == SIERRA_2) {
+ ditherBuffer.set(x + 1 + haxX, y + haxY, ditherBuffer.get(x + 1 + haxX, y + haxY) + (error.times(4).shr(4)))
+ ditherBuffer.set(x + 2 + haxX, y + haxY, ditherBuffer.get(x + 2 + haxX, y + haxY) + (error.times(3).shr(4)))
+ ditherBuffer.set(x - 2 + haxX, y + 1 + haxY, ditherBuffer.get(x - 2 + haxX, y + 1 + haxY) + (error.times(1).shr(4)))
+ ditherBuffer.set(x - 1 + haxX, y + 1 + haxY, ditherBuffer.get(x - 1 + haxX, y + 1 + haxY) + (error.times(2).shr(4)))
+ ditherBuffer.set(x + haxX, y + 1 + haxY, ditherBuffer.get(x + haxX, y + 1 + haxY) + (error.times(3).shr(4)))
+ ditherBuffer.set(x + 1 + haxX, y + 1 + haxY, ditherBuffer.get(x + 1 + haxX, y + 1 + haxY) + (error.times(2).shr(4)))
+ ditherBuffer.set(x + 2 + haxX, y + 1 + haxY, ditherBuffer.get(x + 2 + haxX, y + 1 + haxY) + (error.times(1).shr(4)))
+ }
+ else {
+ throw IllegalArgumentException("Unknown dithering algorithm: $ditherAlgo")
+ }
+ }
+ }
+ }
+ }
+
+ // ...and draw
+ for (y in 0..h - 1 step divH) {
+ for (x in 0..w - 1 step divW) {
+ val lum = Luminosity(divSize, { bufferPixelFontLum(x + it % divW, y + it / divW) })
+ val char = pickRandomGlyphByLumNoQnt(findNearestLum(lum))
+
+ aaframe.drawBuffer(x / divW, y / divH, char)
+ }
+ }
+ }
+
+ // clear buffer
+ imageBuffer.graphics.flush()
+ }
+
+ fun Array.set(x: Int, y: Int, value: Int) {
+ if (x >= 0 && y >= 0 && x < w && y < h)
+ this[y][x] = value
+ }
+
+ fun Array.get(x: Int, y: Int): Int {
+ if (x >= 0 && y >= 0 && x < w && y < h)
+ return this[y][x]
+ else return 0
+ }
+
+ fun bufferPixelFontLum(x: Int, y: Int): Int {
+ val lum = // [0.0 - 1.0]
+ imageBuffer.getColor(x, y).getLuminance(colourAlgo, gamma)
+
+ val delta = lumMax - lumMin
+ return (lumMin + delta * lum).roundInt()
+ }
+
+ /**
+ * @param fontLum : int ranged 0..fontLumMax
+ * @return indexed lum 0..fontLumMax
+ */
+ private fun findNearestLum(inputLum: Luminosity): Luminosity {
+ // find closest: "Closest pair of points problem"
+ // TODO better algorithm
+ // brute force
+ var distMin = 0x7FFFFFFF
+ var lum = Luminosity(divSize, { 0 }) // initial lum
+ var dist: Int
+ var otherLum: Luminosity
+ for (i in 0..lumMap.size - 1) {
+ otherLum = lumMap[i]
+ dist = otherLum.getDistSqr(inputLum)
+ if (dist < distMin) {
+ distMin = dist
+ lum = otherLum // redefine lum to currently nearest
+ }
+ }
+
+ return lum
+ }
+
+ private fun findNearest(lum: Int): Int {
+ val interval = binarySearchInterval(lumMapAll, lum)
+
+ if (interval.first == interval.second)
+ return lumMapAll[interval.first]
+ else {
+ // compare two and return closest
+ if (lum - lumMapAll[interval.first] < lumMapAll[interval.second] - lum)
+ return lumMapAll[interval.first]
+ else
+ return lumMapAll[interval.second]
+ }
+ }
+
+ private fun findNearest(sortedList: List, element: Int): Int {
+ val interval = binarySearchInterval(sortedList, element)
+
+ if (interval.first == interval.second)
+ return sortedList[interval.first]
+ else {
+ // compare two and return closest
+ if (element - sortedList[interval.first] < sortedList[interval.second] - element)
+ return sortedList[interval.first]
+ else
+ return sortedList[interval.second]
+ }
+ }
+
+ /**
+ * e.g.
+ *
+ * 0 1 4 5 7 , find 3
+ *
+ * will return (1, 2), which corresponds value (1, 4) of which input value 3 is in between.
+ */
+ private fun binarySearchInterval(list: List, lum: Int): Pair {
+ var low: Int = 0
+ var high: Int = list.size - 1
+
+ while (low <= high) {
+ val mid = (low + high).ushr(1)
+ val midVal = list[mid]
+
+ if (lum < midVal)
+ high = mid - 1
+ else if (lum > midVal)
+ low = mid + 1
+ else
+ return Pair(mid, mid)
+ }
+
+ return Pair(Math.max(high, 0), Math.min(low, list.size - 1))
+ }
+
+ private fun pickRandomGlyphByLumNoQnt(fontLum: Luminosity): Char {
+ if (fontLum.isZero() && !inverted)
+ return 0x20.toChar() // ' ' with colour index 'zero'
+
+ //println("Errenous call of $fontLum")
+
+ val indexStart = sameLumStartIndices[fontLum]!!
+ val indexEnd = sameLumEndIndices[fontLum]!!
+
+ val index = Random().nextInt(indexEnd - indexStart + 1) + indexStart
+ return brightnessMap[index].second
+ }
+
+ fun Char.getColourKey() = this.toInt().ushr(8).and(0x1F)
+ fun Char.getAscii() = this.toInt().and(0xFF)
+ fun Double.oneTo256() = this.times(255).roundInt()
+ fun getStartAndEndInclusivePoints(divideFrom: Int, divideBy: Int, i: Int) = (
+ ((divideFrom.toDouble() / divideBy) * i).roundInt()
+ ..
+ ((divideFrom.toDouble() / divideBy) * i.plus(1)).roundInt() - 1
+ )
+}
+
+class Luminosity(size: Int, init: (Int) -> Int): Comparable {
+ private var luminosity: Array = Array(size, init)// = lum.toTypedArray()
+ //constructor(size: Int, init: (Int) -> Int): this() { luminosity = Array(size, init) }
+ operator fun get(i: Int) = luminosity[i]
+ operator fun set(x: Int, value: Int) { luminosity[x] = value }
+ val size: Int get() = luminosity.size
+ fun contains(element: Int) = luminosity.contains(element)
+ fun forEach(action: (Int) -> Unit) = luminosity.forEach(action)
+ fun forEachIndexed(action: (Int, Int) -> Unit) = luminosity.forEachIndexed(action)
+ override fun compareTo(other: Luminosity): Int {
+ luminosity.forEachIndexed { i, lum -> if (lum != other[i]) return lum - other[i] }
+ return 0 // this and the other is equal
+ }
+ override fun equals(other: Any?) = this.compareTo(other as Luminosity) == 0
+ fun isZero(): Boolean {
+ luminosity.forEach { if (it != 0) return false }
+ return true
+ }
+ override fun toString(): String {
+ val stringBuilder = StringBuilder()
+ stringBuilder.append("Luma ")
+ this.forEachIndexed { i, value ->
+ if (i > 0) stringBuilder.append("+$value")
+ else stringBuilder.append("$value")
+ }
+ return stringBuilder.toString()
+ }
+
+ // from http://stackoverflow.com/questions/2351087/what-is-the-best-32bit-hash-function-for-short-strings-tag-names
+ override fun hashCode(): Int {
+ var h = 0
+ for (elem in luminosity)
+ h = 37 * h + elem
+ return h
+ }
+ /**
+ * (euclidean norm on 2D) ^ 2
+ */
+ fun getDistSqr(other: Luminosity): Int {
+ var dist = 0
+ for (i in 0..luminosity.size - 1)
+ dist += (luminosity[i] - other.luminosity[i]) * (luminosity[i] - other.luminosity[i])
+ return dist
+ }
+}
diff --git a/src/net/torvald/aa/demoplayer/BaAA.java b/src/net/torvald/aa/demoplayer/BaAA.java
new file mode 100644
index 0000000..23adb63
--- /dev/null
+++ b/src/net/torvald/aa/demoplayer/BaAA.java
@@ -0,0 +1,618 @@
+package net.torvald.aa.demoplayer;
+
+
+import net.torvald.aa.*;
+import org.lwjgl.opengl.GL11;
+import org.newdawn.slick.*;
+import org.newdawn.slick.imageout.ImageOut;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.time.LocalDateTime;
+import java.util.Properties;
+
+/**
+ * Crudely-written Ascii Art video player for Bad Apple
+ *
+ *
+ *
+ * Created by minjaesong on 16-08-10.
+ */
+public class BaAA extends BasicGame {
+
+ static int w;
+ static int h;
+
+ static AppGameContainer appgc;
+ private FrameFetcher frameLoader;
+ static AAFrame aaframe;
+
+ static String appname;
+ static final String appnameDefault = "AA Player";
+
+ private boolean precalcDone = false;
+ private boolean displayedLoading = false;
+ private boolean playbackComplete = false;
+ private boolean musicFired = false;
+
+ private Music bamusic;
+ private boolean preloadMode;
+ private String filePrefix;
+ private static String fontFileName;
+ private static String audioFileName;
+ private static boolean inverted;
+ private int preCalcRate;
+ private String framesDir;
+ private static String framename;
+ private double gamma;
+ private static boolean moreGrey;
+ private static int ditherAlgo;
+ private String testImageRef;
+ private Image testImage;
+ private static boolean singleColour;
+ private static boolean fullCodePage;
+ private static int algorithm;
+ private int monitorCol;
+ private boolean showCredit;
+ private int colourAlgo;
+ private Color customFilter;
+ private boolean recordMode;
+ private boolean replayMode = false;
+ private String replayFileRef;
+
+ private static Image screenBuffer;
+ private static Graphics screenG;
+
+ public static Color[] colors;
+
+ public static final int[] hexadecaGrey = {
+ 0x00, 0xFF, 0x55, 0xAA, 0x11, 0x22, 0x33, 0x44,
+ 0x66, 0x77, 0x88, 0x99, 0xBB, 0xCC, 0xDD, 0xEE
+ };
+ public static final int[] hexadecaGreyInv = {
+ 0xFF, 0x00, 0xAA, 0x55, 0xEE, 0xDD, 0xCC, 0xBB,
+ 0x99, 0x88, 0x77, 0x66, 0x44, 0x33, 0x22, 0x11
+ };
+
+ public static final int [] colcga = {
+ 0x00, 0xFF, 0x55, 0xAA
+ };
+
+ public static final int [] colcgaInv = {
+ 0xFF, 0x00, 0xAA, 0x55
+ };
+
+ public static final int RANGE_CGA = colcga.length;
+ public static final int RANGE_EXT = hexadecaGrey.length;
+
+ private static int fontW, fontH;
+
+ public static int framerate;
+ private static double frameLen;
+
+ private Font font;
+ private Font drawFont;
+
+ public static AsciiAlgo imageToAA;
+
+ public BaAA() {
+ super(appname);
+ }
+
+ @Override
+ public void init(GameContainer gameContainer) throws SlickException {
+ try {
+ Properties prop = new Properties();
+ prop.load(new FileInputStream("./config.properties"));
+
+ w = new Integer(decodeAxB(prop.getProperty("sTerminalSize"))[0]);
+ h = new Integer(decodeAxB(prop.getProperty("sTerminalSize"))[1]);
+ framerate = new Integer(prop.getProperty("iVideoFramerate"));
+ preloadMode = new Boolean(prop.getProperty("bPreloadFrames"));
+ filePrefix = prop.getProperty("sFramesPrefix");
+ fontFileName = prop.getProperty("sFontFamilyName");
+ audioFileName = prop.getProperty("sAudioFileName");
+ fontW = new Integer(decodeAxB(prop.getProperty("sFontSize"))[0]);
+ fontH = new Integer(decodeAxB(prop.getProperty("sFontSize"))[1]);
+ inverted = new Boolean(prop.getProperty("bInverted"));
+ preCalcRate = new Integer(prop.getProperty("iPreCalcRate"));
+ framename = prop.getProperty("sFramesDir");
+ framesDir = "./assets/" + framename;
+ gamma = intNullSafe(prop.getProperty("iGamma"), 220) * 0.01;
+ moreGrey = booleanNullSafe(prop.getProperty("b16Tones"), false);
+ ditherAlgo = new Integer(prop.getProperty("iDitherAlgo"));
+ testImageRef = prop.getProperty("sTestDisplayImage");
+ singleColour = booleanNullSafe(prop.getProperty("bSingleTone"), false);
+ fullCodePage = booleanNullSafe(prop.getProperty("bFullCodePage"), false);
+ algorithm = new Integer(prop.getProperty("iAsciiAlgo"));
+ monitorCol = new Integer(prop.getProperty("iMonitorType"));
+ showCredit = new Boolean(prop.getProperty("bDemoCredit"));
+ //colourAlgo = new Integer(prop.getProperty("iColourMode"));
+ colourAlgo = 1;
+ recordMode = booleanNullSafe(prop.getProperty("bIsRecordMode"), false);
+ replayFileRef = prop.getProperty("sRecordFileName");
+ if (recordMode && replayFileRef != null && replayFileRef.length() > 0)
+ throw new IllegalStateException("Cannot record and play from the same file! " +
+ "Please check your configuration.");
+ if (replayFileRef != null && replayFileRef.length() > 0) replayMode = true;
+
+ String customCol = prop.getProperty("sCustomFilterColour");
+ if (customCol == null || customCol.length() < 5) customCol = "255,79,0";
+ String[] customColSplit = customCol.split("[,.]");
+ customFilter = new Color(
+ new Integer(customColSplit[0]),
+ new Integer(customColSplit[1]),
+ new Integer(customColSplit[2]));
+
+ if (testImageRef != null && testImageRef.length() > 0)
+ testImage = new Image("./assets/" + testImageRef);
+
+ updateFramerate(framerate);
+
+ if (moreGrey && singleColour)
+ throw new IllegalStateException("Cannot be both 16-tone and black-white mode! " +
+ "Please check your configuration.");
+ if (replayMode) {
+ moreGrey = true;
+ singleColour = false;
+ }// they're somewhat compatible 'cause I made them to be, just fix it to 16 so that
+ // the user don't have to care about config adjustment
+
+ updateColours((moreGrey) ? 16 : (singleColour) ? 2 : 4);
+
+ System.out.println("Current pallet: ");
+ for (Color color : colors) {
+ System.out.println(" " + color);
+ }
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ SpriteSheet fontSheet = new SpriteSheet("./assets/" + fontFileName, fontW, fontH);
+ font = new SpriteSheetFont(fontSheet, (char) 0);
+
+ drawFont = new ColouredFastFont("./assets/" + fontFileName, fontW, fontH);
+
+ updateDisplayMode(w, h, fontW, fontH);
+
+ bamusic = new Music("./assets/" + audioFileName);
+
+ // it doesn't have to be square (3x3, 4x4, 5x5, ...), but the quality generally sucks if not.
+ if (!replayMode) {
+ if (algorithm == 0) {
+ imageToAA = new ImageToAA();
+ ((ImageToAA) imageToAA).setProp(w, h, fontSheet, fontW, fontH, inverted, gamma,
+ ditherAlgo, fullCodePage, colourAlgo);
+ }
+ else if (algorithm == 1) {
+ imageToAA = new ImageToAASubGlyph4();
+ ((ImageToAASubGlyph4) imageToAA).setProp(w, h, fontSheet, fontW, fontH, inverted, gamma,
+ ditherAlgo, fullCodePage, colourAlgo);
+ }
+ else if (algorithm == 2) {
+ imageToAA = new ImageToAASubGlyphArb(3, 3);
+ ((ImageToAASubGlyphArb) imageToAA).setProp(w, h, fontSheet, fontW, fontH, inverted, gamma,
+ ditherAlgo, fullCodePage, colourAlgo);
+ }
+ else if (algorithm == 3) {
+ imageToAA = new ImageToAASubGlyphArb(4, 4);
+ ((ImageToAASubGlyphArb) imageToAA).setProp(w, h, fontSheet, fontW, fontH, inverted, gamma,
+ ditherAlgo, fullCodePage, colourAlgo);
+ }
+ else if (algorithm == 4) {
+ imageToAA = new ImageToAASubGlyphArb(5, 5);
+ ((ImageToAASubGlyphArb) imageToAA).setProp(w, h, fontSheet, fontW, fontH, inverted, gamma,
+ ditherAlgo, fullCodePage, colourAlgo);
+ }
+ else if (algorithm == 5) {
+ imageToAA = new ImageToAASubGlyphArb(8, 8);
+ ((ImageToAASubGlyphArb) imageToAA).setProp(w, h, fontSheet, fontW, fontH, inverted, gamma,
+ ditherAlgo, fullCodePage, colourAlgo);
+ }
+ else {
+ throw new IllegalStateException("Unknown antialiasing option: " + algorithm);
+ }
+ }
+
+ if (recordMode) {
+ frameLoader = new FrameRecorder(framesDir, w, h, filePrefix, preCalcRate);
+ }
+ else if (replayMode) {
+ frameLoader = new FrameRecordPreloader(new File("./" + replayFileRef).getAbsoluteFile());
+ }
+ else if (preloadMode) {
+ frameLoader = new FramePreloader(framesDir, w, h, filePrefix, preCalcRate);
+ }
+ else {
+ frameLoader = new FrameStreamer(framesDir, w, h, filePrefix);
+ }
+
+ frameLoader.init();
+ }
+
+ private int frameCount = 0;
+ private int deltaCount = 0;
+
+ private String loadingmsg = "Precalculating . . . .";
+
+ private String nowplayng = "Now playing . . . . ";
+ private String howtopause = "Hit SPACE to pause/resume video.";
+ private String protip = "- Protip: it always looks better on CGA";
+
+ private boolean isPaused = false;
+
+ @Override
+ public void update(GameContainer gameContainer, int delta) throws SlickException {
+ if (!precalcDone) {
+ aaframe.drawString(loadingmsg, 3, 2, colors.length - 1);
+ aaframe.drawString(howtopause, 3, 7, colors.length - 1);
+ aaframe.drawString(protip, 3, 9, colors.length - 1);
+
+ if (!replayMode) imageToAA.precalcFont();
+
+ //preload frames (if applicable)
+ frameLoader.preJob(aaframe);
+ precalcDone = frameLoader.preJobDone();
+
+ if (precalcDone) {
+ if (recordMode) { // record done
+ System.gc();
+ playbackComplete = true;
+ }
+ else {
+ aaframe.drawString(nowplayng, 3, 2, colors.length - 1);
+ }
+ }
+ }
+ else if (!playbackComplete) {
+ // count up frame
+ if (!isPaused) {
+ deltaCount += delta;
+ frameCount = (int) Math.floor(deltaCount / frameLen) - 2 * framerate;
+ }
+
+ appgc.setTitle(
+ appname
+ + " — S: " + String.valueOf(gameContainer.getFPS())
+ + "("
+ + String.valueOf(framerate)
+ + ") — M: "
+ + String.valueOf(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() >>> 20)
+ + "M/"
+ + String.valueOf(Runtime.getRuntime().maxMemory() >>> 20)
+ + "M"
+ + " — F: "
+ + frameCount
+ + "/"
+ + frameLoader.getFrameCount()
+ + " "
+ + ((replayMode) ? "RECD" : ((preloadMode) ? "PRLD" : "STRM"))
+ + " — C: " + ((!replayMode) ? String.valueOf(colors.length)
+ : String.valueOf(((FrameRecordPreloader) frameLoader).getNColour()))
+ + " — A: " + ((!replayMode) ? String.valueOf(algorithm) + String.valueOf(ditherAlgo)
+ : String.valueOf(((FrameRecordPreloader) frameLoader).getNAlgo()))
+ );
+
+ if (frameCount >= frameLoader.getFrameCount() - 1 && testImage == null)
+ playbackComplete = true;
+ }
+ else {
+ if (recordMode) { // record done
+ appgc.setTitle(appname + " — Record done");
+ aaframe.clear();
+ aaframe.drawString("Record done. You may close the application.", 3, 2, colors.length - 1);
+ }
+ else {
+ appgc.setTitle(appname + " — Playback complete");
+ aaframe.clear();
+ aaframe.drawString("Playback completed.", 3, 2, colors.length - 1);
+ if (bamusic.playing()) bamusic.stop();
+
+ if (showCredit) displayCredits();
+ }
+ }
+ }
+
+ @Override
+ public void render(GameContainer gc, Graphics g) throws SlickException {
+ screenG.clear();
+
+ if (precalcDone && !playbackComplete) {
+ if (testImage != null) {
+ imageToAA.toAscii(testImage.getScaledCopy(w, h), aaframe);
+ }
+ else {
+ if (frameCount >= 0)
+ frameLoader.setFrameBuffer(aaframe, (frameCount) < 0 ? 0 : frameCount);
+
+ // fire music if not
+ if (!musicFired && frameCount > 8) { // 8: audio sync hack
+ bamusic.play();
+ musicFired = true;
+ }
+ }
+ }
+
+ renderFrame(g);
+ }
+
+ private final int IBM_GREEN = 1;
+ private final int IBM_AMBER = 2;
+
+ private final Color IBMGREEN = new Color(74, 255, 0);
+ private final Color IBMAMBER = new Color(255, 183, 0);
+
+ private void renderFrame(Graphics gg) {
+ //g.setFont(font);
+ screenG.setFont(drawFont);
+ screenG.setBackground(colors[0]);
+
+ blendNormal();
+
+ for (int y = 0; y < h; y++) {
+ for (int x = 0; x < w; x++) {
+ char ch = aaframe.getChar(x, y);
+
+ // regular char
+ if (ch != 0 && ch != 32) {
+ screenG.setColor(getColor(aaframe.getColorKey(x, y)));
+ screenG.drawString(
+ Character.toString(ch),
+ fontW * x, fontH * y
+ );
+ }
+ else {
+ screenG.setColor(getColor(aaframe.getColorKey(x, y)));
+ screenG.fillRect(fontW * x, fontH * y, fontW, fontH);
+ }
+ }
+ }
+
+ // colour overlay
+ if (monitorCol > 0) {
+ if (monitorCol == 1)
+ screenG.setColor(IBMGREEN);
+ else if (monitorCol == 2)
+ screenG.setColor(IBMAMBER);
+ else if (monitorCol == 3)
+ screenG.setColor(customFilter);
+ else
+ throw new IllegalArgumentException("Unknown monitor mode: " + String.valueOf(monitorCol));
+
+ blendMul();
+
+ screenG.fillRect(0f, 0f, fontW * w, fontH * h);
+
+ }
+
+ blendNormal();
+
+ gg.drawImage(screenBuffer, 0, 0);
+
+ screenG.flush();
+ }
+
+ public static void main(String[] args) {
+ appname = (args.length > 0 && args[0] != null && args[0].length() > 0) ? args[0] : appnameDefault;
+
+ try {
+ appgc = new AppGameContainer(new BaAA());
+ appgc.setShowFPS(false);
+ appgc.setAlwaysRender(true);
+ appgc.setUpdateOnlyWhenVisible(false);
+
+ appgc.start();
+ }
+ catch (SlickException e) {
+ e.printStackTrace();
+ }
+ }
+
+ static int getBrightness(Color col) {
+ return Math.max(Math.max(col.getRedByte(), col.getGreenByte()), col.getBlueByte());
+ }
+
+ public static Color getColor(int i) {
+ return colors[i];
+ }
+
+ @Override
+ public void keyPressed(int key, char c) {
+ // pause
+ if (key == 57 && precalcDone && !playbackComplete) { // SPACE
+ isPaused = !isPaused;
+
+ // pause music
+ if (musicFired && isPaused) {
+ bamusic.pause();
+ }
+ // resume paused music
+ else if (musicFired) {
+ bamusic.resume();
+ }
+ }
+
+ // colour filters
+ if (key == 59) // F1
+ monitorCol = 0;
+ else if (key == 60) // F2
+ monitorCol = 1;
+ else if (key == 61) // F3
+ monitorCol = 2;
+ else if (key == 62) // F4
+ monitorCol = 3;
+
+ if (!inverted)
+ colors[0] = new Color(colcga[0], colcga[0], colcga[0]);
+ else
+ colors[1] = new Color(colcgaInv[1], colcgaInv[1], colcgaInv[1]);
+
+ // capture
+ if (key == 35) { // H
+ try {
+ ImageOut.write(screenBuffer, "./" + LocalDateTime.now().toString() + ".png");
+ System.out.println("Hardcopy exported as " + LocalDateTime.now().toString() + ".png");
+ }
+ catch (Exception e) {
+ System.err.print("An error occured while exporting hardcopy: ");
+ e.printStackTrace();
+ }
+ }
+ else if (key == 20 && singleColour) { // T
+ try {
+ String filename = "./" + LocalDateTime.now().toString() + ".txt";
+
+ FileWriter writer = new FileWriter(new File(filename).getAbsoluteFile());
+ for (int i = 0; i < aaframe.getSizeof() >>> 1; i++) {
+ if (i % w == 0 && i > 0){
+ writer.write("\n");
+ writer.flush();
+ }
+
+ writer.write(aaframe.getChar(i % w, i / w) & 0xFF);
+ writer.flush();
+ }
+
+ writer.close();
+
+ System.out.println("Hardcopy (text) exported as " + LocalDateTime.now().toString() + ".txt");
+ }
+ catch (Exception e) {
+ System.err.print("An error occured while exporting hardcopy (text): ");
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static String getFontFileName() {
+ return fontFileName;
+ }
+
+ public static String getAudioFileName() {
+ return audioFileName;
+ }
+
+ public static String getFramename() {
+ return framename;
+ }
+
+ public static int getColorsCount() {
+ return colors.length;
+ }
+
+ public static int getDitherAlgo() {
+ return ditherAlgo;
+ }
+
+ public static int getAlgorithm() {
+ return algorithm;
+ }
+
+ public static boolean isFullCodePage() {
+ return fullCodePage;
+ }
+
+ private void blendNormal() {
+ // blend: NORMAL
+ GL11.glEnable(GL11.GL_BLEND);
+ GL11.glColorMask(true, true, true, true);
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ }
+
+ private void blendMul() {
+ // blend: MULTIPLY
+ // (protip: do NOT use "g.setDrawMode(Graphics.MODE_COLOR_MULTIPLY)"; it NEVER work as it should be!)
+ GL11.glEnable(GL11.GL_BLEND);
+ GL11.glColorMask(true, true, true, true);
+ GL11.glBlendFunc(GL11.GL_DST_COLOR, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ }
+
+ private int intNullSafe(String string, int i) {
+ try {
+ return new Integer(string);
+ }
+ catch (NumberFormatException e) {
+ return i;
+ }
+ }
+
+ private boolean booleanNullSafe(String string, boolean i) {
+ if (string == null || string.length() < 1) return i;
+ else return new Boolean(string);
+ }
+
+ private void displayCredits() {
+ aaframe.drawString("BA-AA", 3, 5, colors.length - 1);
+ aaframe.drawString("Code by Torvald, 2016", 3, 7, colors.length - 1);
+ aaframe.drawString("Please refer to ABOUT.md for the information.", 3, 8, colors.length - 1);
+ aaframe.drawString("...and don't try to find out what \"BA\" means.", 3, 10, colors.length - 1);
+ }
+
+ public static void updateFramerate(int newrate) {
+ framerate = newrate;
+ frameLen = 1000.0 / framerate;
+ appgc.setTargetFrameRate(newrate);
+ }
+
+ public static void updateDisplayMode(int width, int height, int glyphWidth, int glyphHeight) throws SlickException {
+ appgc.setDisplayMode(width * glyphWidth, height * glyphHeight, false);
+ aaframe = new AAFrame(width, height);
+ w = width;
+ h = height;
+ fontW = glyphWidth;
+ fontH = glyphHeight;
+ screenBuffer = new Image(w * fontW, h * fontH);
+ screenG = screenBuffer.getGraphics();
+
+ appgc.getGraphics().clear(); // clear graphics context so that the frame draws on the correct position
+ // (to future myself: actually I have no idea how it works, it just does the job)
+ }
+
+ public static void updateColours(int colourCount) {
+ if (colourCount == 16) {
+ singleColour = false;
+ moreGrey = true;
+ int size = hexadecaGrey.length;
+ colors = new Color[size];
+ for (int c = 0; c < size; c++) {
+ if (!inverted)
+ colors[c] = new Color(hexadecaGrey[c], hexadecaGrey[c], hexadecaGrey[c]);
+ else
+ colors[c] = new Color(hexadecaGreyInv[c], hexadecaGreyInv[c], hexadecaGreyInv[c]);
+ }
+ }
+ else if (colourCount == 2) {
+ singleColour = true;
+ moreGrey = false;
+ int size = 2;
+ colors = new Color[size];
+ for (int c = 0; c < size; c++) {
+ if (!inverted)
+ colors[c] = new Color(colcga[c], colcga[c], colcga[c]);
+ else
+ colors[c] = new Color(colcgaInv[c], colcgaInv[c], colcgaInv[c]);
+ }
+ }
+ else {
+ singleColour = false;
+ moreGrey = false;
+ int size = colcga.length;
+ colors = new Color[size];
+ for (int c = 0; c < size; c++) {
+ if (!inverted)
+ colors[c] = new Color(colcga[c], colcga[c], colcga[c]);
+ else
+ colors[c] = new Color(colcgaInv[c], colcgaInv[c], colcgaInv[c]);
+ }
+ }
+ }
+
+ private String[] decodeAxB(String string) {
+ if (!string.contains("x")) throw new IllegalArgumentException("Wrong format! valid representation: 1337x6969");
+ return string.split("x");
+ }
+}
diff --git a/src/net/torvald/aa/demoplayer/FrameFetcher.kt b/src/net/torvald/aa/demoplayer/FrameFetcher.kt
new file mode 100644
index 0000000..21cb4ae
--- /dev/null
+++ b/src/net/torvald/aa/demoplayer/FrameFetcher.kt
@@ -0,0 +1,15 @@
+package net.torvald.aa.demoplayer
+
+import net.torvald.aa.AAFrame
+
+/**
+ * Created by minjaesong on 16-08-11.
+ */
+interface FrameFetcher {
+ val frameCount: Int
+
+ fun init()
+ fun setFrameBuffer(framebuffer: AAFrame, frameNo: Int)
+ fun preJob(framebuffer: AAFrame)
+ fun preJobDone(): Boolean
+}
\ No newline at end of file
diff --git a/src/net/torvald/aa/demoplayer/FramePreloader.kt b/src/net/torvald/aa/demoplayer/FramePreloader.kt
new file mode 100644
index 0000000..d9bc12d
--- /dev/null
+++ b/src/net/torvald/aa/demoplayer/FramePreloader.kt
@@ -0,0 +1,85 @@
+package net.torvald.aa.demoplayer
+
+import net.torvald.aa.AAFrame
+import net.torvald.aa.ImageToAA
+import org.newdawn.slick.Image
+import java.io.File
+import java.lang.management.ManagementFactory
+
+/**
+ * Created by minjaesong on 16-08-11.
+ */
+class FramePreloader(
+ private val ref: String,
+ private val w: Int,
+ private val h: Int,
+ private val prefix: String,
+ private val preCalcRate: Int) : FrameFetcher {
+
+
+ private val framesList: Array
+ override val frameCount: Int
+
+ private var loadCount = 0
+
+ private val asciiFrames: Array
+
+ init {
+ val f = File(ref)
+ framesList = f.listFiles { dir, name ->
+ name.startsWith(prefix) && (name.endsWith(".jpg") || name.endsWith(".bmp")
+ || name.endsWith("" + ".png") || name.endsWith("" + ".tga"))
+ }
+
+ frameCount = framesList.size
+
+ asciiFrames = Array(frameCount, { AAFrame(w, h) })
+ }
+
+ private lateinit var frameInput: Image
+
+ override fun init() {
+ }
+
+ override fun preJob(framebuffer: AAFrame) {
+ for (k in 0..preCalcRate - 1) {
+ frameInput = Image(framesList[loadCount].absolutePath)
+ // note that getScaleCopy() does not actually resize image as Photoshop would do. It does in the OpenGL context.
+
+ // convert image to ascii and store it
+ BaAA.imageToAA.toAscii(frameInput.copy(), asciiFrames[loadCount])
+
+ // message
+ val msgConsole = "Precalculating frame ${loadCount + 1} of $frameCount"
+ val msgWindow = "${BaAA.appname} — MEM: " +
+ "${ManagementFactory.getMemoryMXBean().heapMemoryUsage.used ushr 20}" +
+ "M/${Runtime.getRuntime().maxMemory() ushr 20}M — " +
+ "$msgConsole"
+
+ // printout message
+ framebuffer.drawString(msgConsole, 3, 2, BaAA.colors.size - 1)
+
+ BaAA.appgc.setTitle(msgWindow)
+
+ // progress bar
+ val barwidth = w - 6
+ for (i in 0..barwidth - 1) {
+ if (i < Math.round(loadCount.toFloat() / frameCount * barwidth)) {
+ framebuffer.drawString("#", 3 + i, 4, BaAA.colors.size - 1)
+ } else {
+ framebuffer.drawString(".", 3 + i, 4, BaAA.colors.size - 1)
+ }
+ }
+
+ loadCount++
+
+ if (preJobDone()) break
+ }
+ }
+
+ override fun preJobDone() = loadCount >= frameCount
+
+ override fun setFrameBuffer(framebuffer: AAFrame, frameNo: Int) {
+ framebuffer.drawFromOther(asciiFrames[frameNo])
+ }
+}
\ No newline at end of file
diff --git a/src/net/torvald/aa/demoplayer/FrameRecordPreloader.kt b/src/net/torvald/aa/demoplayer/FrameRecordPreloader.kt
new file mode 100644
index 0000000..29b6053
--- /dev/null
+++ b/src/net/torvald/aa/demoplayer/FrameRecordPreloader.kt
@@ -0,0 +1,176 @@
+package net.torvald.aa.demoplayer
+
+import net.torvald.aa.AAFrame
+import java.io.*
+import java.lang.management.ManagementFactory
+
+/**
+ * Created by minjaesong on 16-08-24.
+ */
+
+class FrameRecordPreloader(file: File) : FrameFetcher {
+
+ override val frameCount: Int
+ private val inputStream = BufferedInputStream(FileInputStream(file))
+ private val frameDataBuffer: ByteArray
+ private val sizeofFrame: Int
+
+ var glyphW = -1; private set
+ var glyphH = -1; private set
+ var framerate = -1; private set
+ var nColour = -1; private set
+ var nAlgo = -1; private set
+ var width = -1; private set
+ var height = -1; private set
+
+ private val asciiFrames: Array
+ private var loadCount = 0
+
+ private val preloadRate = 517
+
+
+
+ init {
+ val byteBuffer = ByteArray(8)
+
+ println("Reading record ${file.path}")
+ println("Filesize: ${file.length()} bytes")
+
+ inputStream.readRelative(byteBuffer, 0x00, 8)
+ if (!byteBuffer.assertMagic(FrameRecorder.magic)) throw RuntimeException("Not a valid record file!")
+
+ inputStream.readRelative(byteBuffer, 0x08, 1); glyphW = byteBuffer.toInt(1)
+ inputStream.readRelative(byteBuffer, 0x09, 1); glyphH = byteBuffer.toInt(1)
+ inputStream.readRelative(byteBuffer, 0x0A, 1); framerate = byteBuffer.toInt(1)
+ inputStream.readRelative(byteBuffer, 0x0B, 1); nColour = byteBuffer.toInt(1)
+ inputStream.readRelative(byteBuffer, 0x0C, 1); nAlgo = byteBuffer.toInt(1)
+ inputStream.readRelative(byteBuffer, 0x0D, 2); width = byteBuffer.toInt(2)
+ inputStream.readRelative(byteBuffer, 0x0F, 2); height = byteBuffer.toInt(2)
+ inputStream.readRelative(byteBuffer, 0x11, 4); frameCount = byteBuffer.toInt(4)
+
+ sizeofFrame = width * height * 2
+
+ println("glyph dimension: $glyphW x $glyphH")
+ println("framerate: $framerate")
+ println("colours: $nColour")
+ println("algorithm: $nAlgo")
+ println("dimension: $width x $height")
+ println("frame data length: $sizeofFrame")
+ println("# of frames: $frameCount")
+
+ frameDataBuffer = ByteArray(sizeofFrame)
+
+ asciiFrames = Array(frameCount, { AAFrame(width, height) })
+ }
+
+ override fun setFrameBuffer(framebuffer: AAFrame, frameNo: Int) {
+ framebuffer.drawFromOther(asciiFrames[frameNo])
+ }
+
+ override fun init() {
+ BaAA.updateColours(nColour)
+ BaAA.updateFramerate(framerate)
+ BaAA.updateDisplayMode(width, height, glyphW, glyphH)
+ }
+
+ override fun preJob(framebuffer: AAFrame) {
+ for (k in 0..preloadRate - 1) {
+ inputStream.readRelative(frameDataBuffer, 0x15 + sizeofFrame * loadCount, sizeofFrame)
+ asciiFrames[loadCount].drawFromBytes(frameDataBuffer)
+
+
+ // message
+ val msgConsole = "Loading frame data to RAM ${loadCount + 1} of $frameCount"
+ val msgWindow = "${BaAA.appname} — MEM: " +
+ "${ManagementFactory.getMemoryMXBean().heapMemoryUsage.used ushr 20}" +
+ "M/${Runtime.getRuntime().maxMemory() ushr 20}M — " +
+ "$msgConsole"
+
+ // printout message
+ framebuffer.drawString(msgConsole, 3, 2, BaAA.colors.size - 1)
+
+ BaAA.appgc.setTitle(msgWindow)
+
+ // progress bar
+ val barwidth = width - 6
+ for (i in 0..barwidth - 1) {
+ if (i < Math.round(loadCount.toFloat() / frameCount * barwidth)) {
+ framebuffer.drawString("#", 3 + i, 4, BaAA.colors.size - 1)
+ } else {
+ framebuffer.drawString(".", 3 + i, 4, BaAA.colors.size - 1)
+ }
+ }
+
+ loadCount++
+
+ if (preJobDone()) break
+ }
+ }
+
+ override fun preJobDone() = loadCount >= frameCount
+ /**
+ * Big endian
+ */
+ private fun ByteArray.toInt(nBytes: Int) =
+ if (nBytes == 1)
+ this[0].toUint()
+ else if (nBytes == 2)
+ this[0].toUint().shl(8) or this[1].toUint()
+ else if (nBytes == 4)
+ this[0].toUint().shl(24) or
+ this[1].toUint().shl(16) or
+ this[2].toUint().shl(8) or
+ this[3].toUint()
+ else
+ throw IllegalArgumentException("Incompatible data size: $nBytes")
+ private fun ByteArray.assertMagic(magic: ByteArray): Boolean {
+ for (i in 0..magic.size - 1)
+ if (this[i] != magic[i]) return false
+ return true
+ }
+
+ /**
+ * Still have to read from head to tail
+ */
+ fun InputStream.readRelative(b: ByteArray, off: Int, len: Int): Int {
+ if (b == null) {
+ throw NullPointerException()
+ } else if (off < 0 || len < 0 || len > b.size) {
+ throw IndexOutOfBoundsException()
+ } else if (len == 0) {
+ return 0
+ }
+
+ var c = read()
+ if (c == -1) {
+ return -1
+ }
+ b[0] = c.toByte()
+
+ var i = 1
+ try {
+ while (i < len) {
+ c = read()
+ if (c == -1) {
+ break
+ }
+ b[i] = c.toByte()
+ i++
+ }
+ } catch (ee: IOException) {
+ }
+
+ return i
+ }
+
+ fun ByteArray.toByteStrings(): String {
+ val sb = StringBuilder()
+ for (i in 0..this.size - 1) {
+ sb.append(Integer.toHexString(this[i].toUint()).toUpperCase())
+ sb.append(" ")
+ }
+ return sb.toString()
+ }
+}
+
+fun Byte.toUint() = java.lang.Byte.toUnsignedInt(this)
\ No newline at end of file
diff --git a/src/net/torvald/aa/demoplayer/FrameRecorder.kt b/src/net/torvald/aa/demoplayer/FrameRecorder.kt
new file mode 100644
index 0000000..78de625
--- /dev/null
+++ b/src/net/torvald/aa/demoplayer/FrameRecorder.kt
@@ -0,0 +1,212 @@
+package net.torvald.aa.demoplayer
+
+import net.torvald.aa.AAFrame
+import net.torvald.aa.roundInt
+import org.newdawn.slick.Image
+import java.io.File
+import java.lang.management.ManagementFactory
+
+/**
+ * Created by minjaesong on 16-08-24.
+ */
+class FrameRecorder(
+ private val ref: String,
+ private val w: Int,
+ private val h: Int,
+ private val prefix: String,
+ private val preCalcRate: Int) : FrameFetcher {
+
+ private val framesList: Array
+ override val frameCount: Int
+
+ private var loadCount = 0
+
+ private val asciiBuffer = AAFrame(w, h)
+
+ private lateinit var frameInput: Image
+
+ private lateinit var outfile: File
+
+ private val estmSize: Int
+
+ init {
+ val f = File(ref)
+ framesList = f.listFiles { dir, name ->
+ name.startsWith(prefix) && (name.endsWith(".jpg") || name.endsWith(".bmp")
+ || name.endsWith("" + ".png") || name.endsWith("" + ".tga"))
+ }
+
+ frameCount = framesList.size
+ estmSize = (w * h * 2 * frameCount + 13).ushr(20)
+
+ /******************************************
+ * Generate file to write, write metadata *
+ ******************************************/
+
+ val filename =
+ "${BaAA.getFramename()}_" +
+ "${BaAA.getFontFileName()}_" +
+ "${w}x${h}_" +
+ "C${BaAA.getColorsCount()}_" +
+ "A${BaAA.getAlgorithm()}${BaAA.getDitherAlgo()}" +
+ (if (BaAA.isFullCodePage()) "_fullcp" else "") +
+ ".aarec"
+
+ outfile = File("./$filename")
+
+ println("Recording frame to $outfile")
+
+ serialiseMetadata(outfile)
+ }
+
+ override fun setFrameBuffer(framebuffer: AAFrame, frameNo: Int) {
+ throw UnsupportedOperationException("Record only; TODO: quit applet with message")
+ }
+
+ private var startNano: Long = 0
+
+ override fun init() {
+ startNano = System.nanoTime()
+ }
+
+ override fun preJob(framebuffer: AAFrame) {
+ /*************************************************************
+ * Write image to AA framebuffer, serialise THAT framebuffer *
+ *************************************************************/
+
+ try {
+ for (k in 0..preCalcRate - 1) {
+ frameInput = Image(framesList[loadCount].absolutePath)
+ // note that getScaleCopy() does not actually resize image as Photoshop would do. It does in the OpenGL context.
+
+ // convert image to ascii and store it
+ BaAA.imageToAA.toAscii(frameInput.copy(), asciiBuffer)
+
+ // serialise the frame
+ serialiseFrame(outfile, asciiBuffer)
+
+
+
+ // message
+ val msgConsole = "Recording frame ${loadCount + 1} of $frameCount"
+ val msgWindow = "${BaAA.appname} — MEM: " +
+ "${ManagementFactory.getMemoryMXBean().heapMemoryUsage.used ushr 20}" +
+ "M/${Runtime.getRuntime().maxMemory() ushr 20}M — " +
+ "$msgConsole"
+
+ // printout message
+ framebuffer.drawString(msgConsole, 3, 2, BaAA.colors.size - 1)
+
+ BaAA.appgc.setTitle(msgWindow)
+
+ // progress bar
+ val barwidth = w - 6
+ for (i in 0..barwidth - 1) {
+ if (i < Math.round(loadCount.toFloat() / frameCount * barwidth)) {
+ framebuffer.drawString("#", 3 + i, 4, BaAA.colors.size - 1)
+ } else {
+ framebuffer.drawString(".", 3 + i, 4, BaAA.colors.size - 1)
+ }
+ }
+
+ // some more information
+ framebuffer.drawString("Filename: ${outfile.name}", 3, 12, BaAA.colors.size - 1)
+ framebuffer.drawString("Estimated size: $estmSize Mbytes", 3, 14, BaAA.colors.size - 1)
+
+ if (loadCount > 0) {
+ val elapsedTime = System.nanoTime() - startNano
+ val remainingTime =
+ ((frameCount.minus(loadCount).toDouble() / loadCount) * elapsedTime)
+ .div(1000).div(1000).div(1000).roundInt()
+
+ framebuffer.drawString("Estimated remaining: ${getEstimated(remainingTime)} ",
+ 3, 16, BaAA.colors.size - 1)
+ }
+
+ loadCount++
+ }
+ } catch (e1: ArrayIndexOutOfBoundsException) {
+ // a fluke; do nothing
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun getEstimated(seconds: Int): String {
+ fun pluralise(i: Int) = "${if (i != 1) "s" else ""}"
+
+ if (seconds >= 3600) {
+ val hours = seconds.div(3600.0).roundInt()
+ return "$hours hour${pluralise(hours)}"
+ }
+ else if (seconds >= 60) {
+ val minutes = seconds.div(60.0).roundInt()
+ return "${Math.max(1, minutes)} minute${pluralise(minutes)}"
+ }
+ else {
+ return "about a minute"
+ }
+ }
+
+ override fun preJobDone() = loadCount >= frameCount
+
+
+ /*
+ Format documentation
+ * Big endian.
+ - "BaAArecD"(magic)
+ - int8 Glyph width
+ - int8 Glyph height
+ - int8 Playback rate (FPS)
+ - int8 # of colours
+ - int8 algorithm index
+ - int16 Terminal width
+ - int16 Terminal height
+ - int32 # of frames
+ - Array Array of data (aaFrame.framebuffer)
+ */
+
+ fun serialiseMetadata(file: File) {
+ val i8s = byteArrayOf(
+ BaAA.imageToAA.fontW.toByte(),
+ BaAA.imageToAA.fontH.toByte(),
+ BaAA.framerate.toByte(),
+ BaAA.getColorsCount().toByte(),
+ BaAA.getAlgorithm().times(10).plus(BaAA.getDitherAlgo()).toByte()
+ )
+ val i16s = byteArrayOf(
+ asciiBuffer.width.ushr(8).and(0xFF).toByte(), // msb
+ asciiBuffer.width.and(0xFF).toByte(), // lsb
+ asciiBuffer.height.ushr(8).and(0xFF).toByte(), // msb
+ asciiBuffer.height.and(0xFF).toByte() // lsb
+ )
+ val nFramesBigEndian = byteArrayOf(
+ frameCount.ushr(24).and(0xFF).toByte(),
+ frameCount.ushr(16).and(0xFF).toByte(),
+ frameCount.ushr(8).and(0xFF).toByte(),
+ frameCount.and(0xFF).toByte()
+ )
+
+ // make sure it starts with magic
+ file.writeBytes(magic)
+
+ file.appendBytes(i8s)
+ file.appendBytes(i16s)
+ file.appendBytes(nFramesBigEndian)
+ }
+
+ fun serialiseFrame(file: File, aaFrame: AAFrame) {
+ val frameBytes = ByteArray(aaFrame.sizeof)
+ for (i in 0..frameBytes.size - 1 step 2) {
+ val glyph = aaFrame.frameBuffer[i.shr(1)].toInt()
+ frameBytes[i] = glyph.ushr(8).and(0xFF).toByte() // msb
+ frameBytes[i + 1] = glyph.and(0xFF).toByte() // lsb
+ }
+ file.appendBytes(frameBytes)
+ }
+
+ companion object {
+ /** ASCII bytes of "BaAArecD" */
+ val magic = byteArrayOf(0x42, 0x61, 0x41, 0x41, 0x72, 0x65, 0x63, 0x44)
+ }
+}
\ No newline at end of file
diff --git a/src/net/torvald/aa/demoplayer/FrameStreamer.kt b/src/net/torvald/aa/demoplayer/FrameStreamer.kt
new file mode 100644
index 0000000..650dc14
--- /dev/null
+++ b/src/net/torvald/aa/demoplayer/FrameStreamer.kt
@@ -0,0 +1,45 @@
+package net.torvald.aa.demoplayer
+
+import net.torvald.aa.AAFrame
+import net.torvald.aa.ImageToAA
+import org.newdawn.slick.Image
+import java.io.File
+
+/**
+ * Created by minjaesong on 16-08-11.
+ */
+class FrameStreamer(
+ private val ref: String,
+ private val w: Int,
+ private val h: Int,
+ private val prefix: String) : FrameFetcher {
+
+
+ private val framesList: Array
+ override val frameCount: Int
+
+ init {
+ val f = File(ref)
+ framesList = f.listFiles { dir, name ->
+ name.startsWith(prefix) && (name.endsWith(".jpg") || name.endsWith(".bmp")
+ || name.endsWith("" + ".png") || name.endsWith("" + ".tga"))
+ }
+
+ frameCount = framesList.size
+ }
+
+ override fun init() {
+ }
+
+ override fun setFrameBuffer(framebuffer: AAFrame, frameNo: Int) {
+ if (frameNo < frameCount) {
+ val frameInput = Image(framesList[frameNo].absolutePath)
+ BaAA.imageToAA.toAscii(frameInput, framebuffer)
+ }
+ }
+
+ override fun preJob(framebuffer: AAFrame) {
+ }
+
+ override fun preJobDone(): Boolean = true
+}
\ No newline at end of file