Skip to content

Commit f8b729a

Browse files
committed
Add the ability to record Grid algorithms and save it as an animated GIF
Just in black & white for the first step.
1 parent 0d89c2a commit f8b729a

File tree

5 files changed

+178
-0
lines changed

5 files changed

+178
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package de.ronny_h.aoc.extensions.animation
2+
3+
import de.ronny_h.aoc.extensions.asList
4+
import java.io.File
5+
6+
class AnimationRecorder {
7+
private val frames = mutableListOf<List<String>>()
8+
9+
fun record(frame: String) {
10+
frames.add(frame.asList())
11+
}
12+
13+
fun saveTo(fileName: String) = frames
14+
.map(List<String>::createImage)
15+
.writeToGifFile(File(fileName))
16+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package de.ronny_h.aoc.extensions.animation
2+
3+
import java.awt.Font
4+
import java.awt.Font.MONOSPACED
5+
import java.awt.Font.PLAIN
6+
import java.awt.Graphics2D
7+
import java.awt.RenderingHints.KEY_TEXT_ANTIALIASING
8+
import java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON
9+
import java.awt.image.BufferedImage
10+
import java.awt.image.BufferedImage.TYPE_INT_RGB
11+
import java.awt.image.RenderedImage
12+
import java.io.File
13+
import javax.imageio.*
14+
import javax.imageio.metadata.IIOMetadata
15+
import javax.imageio.metadata.IIOMetadataNode
16+
import javax.imageio.stream.FileImageOutputStream
17+
import javax.imageio.stream.ImageOutputStream
18+
19+
20+
private const val fontSize = 20
21+
private const val drawStartX = 3
22+
private const val rowHeight = fontSize + 1
23+
private const val colWidth = 3 / 5.0 * fontSize
24+
private val font = Font(MONOSPACED, PLAIN, fontSize)
25+
26+
fun List<String>.createImage(): BufferedImage {
27+
val width = 2 * drawStartX + (this.first().length * colWidth).toInt()
28+
val height = size * rowHeight - 13
29+
val bufferedImage = BufferedImage(width, height, TYPE_INT_RGB)
30+
val graphics2D = bufferedImage.graphics as Graphics2D
31+
graphics2D.font = font
32+
graphics2D.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON)
33+
forEachIndexed { rowNo, row ->
34+
graphics2D.drawString(row, drawStartX, rowNo * rowHeight)
35+
}
36+
return bufferedImage
37+
}
38+
39+
class GifSequenceWriter(
40+
outputStream: ImageOutputStream,
41+
imageType: Int,
42+
timeBetweenFramesMS: Int,
43+
loopContinuously: Boolean,
44+
) : AutoCloseable {
45+
46+
private val gifWriter: ImageWriter = getWriter()
47+
private val imageWriteParam: ImageWriteParam = gifWriter.defaultWriteParam
48+
private val imageMetaData: IIOMetadata
49+
50+
init {
51+
val imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType)
52+
imageMetaData = gifWriter.getDefaultImageMetadata(imageTypeSpecifier, imageWriteParam)
53+
54+
val metaFormatName = imageMetaData.getNativeMetadataFormatName()
55+
val root = imageMetaData.getAsTree(metaFormatName) as IIOMetadataNode
56+
57+
val graphicsControl: IIOMetadataNode = root.getNode("GraphicControlExtension")
58+
graphicsControl["disposalMethod"] = "none"
59+
graphicsControl["userInputFlag"] = "FALSE"
60+
graphicsControl["transparentColorFlag"] = "FALSE"
61+
graphicsControl["delayTime"] = (timeBetweenFramesMS / 10).toString()
62+
graphicsControl["transparentColorIndex"] = "0"
63+
64+
root.getNode("CommentExtensions")["CommentExtension"] = "Created by rhaendel for Advent of Code in Kotlin"
65+
66+
val loop = if (loopContinuously) 0 else 1
67+
val appAttributes = IIOMetadataNode("ApplicationExtension")
68+
appAttributes["applicationID"] = "NETSCAPE"
69+
appAttributes["authenticationCode"] = "2.0"
70+
appAttributes.userObject = byteArrayOf(0x1, (loop and 0xFF).toByte(), 0.toByte())
71+
72+
root.getNode("ApplicationExtensions").appendChild(appAttributes)
73+
74+
imageMetaData.setFromTree(metaFormatName, root)
75+
76+
gifWriter.setOutput(outputStream)
77+
gifWriter.prepareWriteSequence(null)
78+
}
79+
80+
fun write(img: RenderedImage) = gifWriter.writeToSequence(
81+
IIOImage(img, null, imageMetaData),
82+
imageWriteParam
83+
)
84+
85+
override fun close() {
86+
gifWriter.endWriteSequence()
87+
gifWriter.dispose()
88+
}
89+
90+
companion object {
91+
private fun getWriter(): ImageWriter {
92+
val iter = ImageIO.getImageWritersBySuffix("gif")
93+
if (!iter.hasNext()) {
94+
throw IIOException("No GIF Image Writers exist")
95+
} else {
96+
return iter.next()
97+
}
98+
}
99+
100+
private fun IIOMetadataNode.getNode(nodeName: String): IIOMetadataNode {
101+
for (i in 0..<length) {
102+
if (item(i).nodeName.equals(nodeName, ignoreCase = true)) {
103+
return item(i) as IIOMetadataNode
104+
}
105+
}
106+
return IIOMetadataNode(nodeName).also { node ->
107+
appendChild(node)
108+
}
109+
}
110+
111+
}
112+
}
113+
114+
fun List<BufferedImage>.writeToGifFile(file: File) {
115+
FileImageOutputStream(file).use { out ->
116+
GifSequenceWriter(out, first().type, 200, true).use { gifWriter ->
117+
forEach { image ->
118+
gifWriter.write(image)
119+
}
120+
}
121+
}
122+
}
123+
124+
private operator fun IIOMetadataNode.set(name: String, value: String) = setAttribute(name, value)

src/main/kotlin/de/ronny_h/aoc/extensions/grids/Grid.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package de.ronny_h.aoc.extensions.grids
22

3+
import de.ronny_h.aoc.extensions.animation.AnimationRecorder
34
import de.ronny_h.aoc.extensions.graphs.shortestpath.Graph
45
import de.ronny_h.aoc.extensions.graphs.shortestpath.ShortestPath
56
import de.ronny_h.aoc.extensions.graphs.shortestpath.aStarAllPaths
@@ -25,6 +26,8 @@ abstract class Grid<T>(
2526
val height: Int get() = grid.height
2627
val width: Int get() = grid.width
2728

29+
var recorder: AnimationRecorder? = null
30+
2831
/**
2932
* A function that maps each `Char` that may occur in the `input: List<String>` to a value of type [T].
3033
*/
@@ -79,12 +82,14 @@ abstract class Grid<T>(
7982
operator fun set(x: Int, y: Int, value: T) {
8083
preSet(Coordinates(x, y), value)
8184
grid[x, y] = value
85+
recorder?.record(toString())
8286
}
8387

8488
operator fun get(position: Coordinates) = grid.getOrNull(position) ?: fallbackElement
8589
operator fun set(position: Coordinates, element: T) {
8690
preSet(position, element)
8791
grid[position] = element
92+
recorder?.record(toString())
8893
}
8994

9095
operator fun set(x: Int, yRange: IntRange, value: T) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package de.ronny_h.aoc.extensions.animation
2+
3+
import io.kotest.core.spec.style.FunSpec
4+
import io.kotest.engine.spec.tempfile
5+
import io.kotest.matchers.shouldBe
6+
import javax.imageio.ImageIO
7+
8+
9+
class GifSequenceWriterTest : FunSpec({
10+
11+
val file = tempfile()
12+
13+
test("a GIF can be rendered from a list of Strings and be written to a file") {
14+
val frames = List(3) { i ->
15+
val text = buildList { repeat(5) { add("$i".repeat(5)) } }
16+
text.createImage()
17+
}
18+
frames.writeToGifFile(file)
19+
20+
val newImage = ImageIO.read(file)
21+
val resource = this.javaClass.getResource("/reference.gif")
22+
val referenceImage = ImageIO.read(resource)
23+
24+
newImage.height shouldBe referenceImage.height
25+
newImage.width shouldBe referenceImage.width
26+
27+
for (x in 0..<newImage.width) {
28+
for (y in 0..<newImage.height) {
29+
newImage.getRGB(x, y) shouldBe referenceImage.getRGB(x, y)
30+
}
31+
}
32+
}
33+
})

src/test/resources/reference.gif

68.2 KB
Loading

0 commit comments

Comments
 (0)