Skip to content

Commit

Permalink
Merge pull request #8 from Ekenstein/markers
Browse files Browse the repository at this point in the history
Markers
  • Loading branch information
Ekenstein authored Dec 27, 2023
2 parents 85fe681 + 76dd049 commit e03c224
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ build
.idea/*
!.idea/codeStyles
*.iml
out
out
.idea
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,30 @@ The animated GIF is highly inspired by the classic game Hayauchi Super Igo for N

```shell
Usage: sgf2gif options_list
Options:
--file, -f -> The SGF-file to convert to a GIF. If not stated, the SGF will be read from the std-in.
--output, -o -> The destination file to write the GIF to. If not stated, the output will be written to std-out.
Options:
--file, -f -> The SGF-file to convert to a GIF. If not stated, the SGF will be read from the std-in.
--output, -o -> The destination file to write the GIF to. If not stated, the output will be written to std-out.
--theme [NES] -> The theme to render the board with { Value should be one of [nes, classic] }
--loop, -l [false] -> Whether the animation should be looped or not
--loop, -l [false] -> Whether the animation should be looped or not
--show-marker [false] -> Whether the last move should be marked or not
--width, -w [1000] -> The width of the image. { Int }
--height, -h [1000] -> The height of the image. { Int }
--move-number, -mn [2147483647] -> The move number up to which the animation will run to. { Int }
--delay, -d [2] -> The delay between frames in seconds. { Int }
--help -> Usage info
--help -> Usage info
```
### NES theme
```shell
java -jar sgf2gif.jar -f ~/game.sgf -o ~/game.gif --theme nes
```
![](https://github.com/Ekenstein/sgf2gif/blob/main/nes.gif?raw=true)
![](./nes.gif)
### Classic theme
```shell
java -jar sgf2gif.jar -f ~/game.sgf -o ~/game.gif --theme classic
```
![](https://github.com/Ekenstein/sgf2gif/blob/main/classic.gif?raw=true)
![](./classic.gif)
### Writing to std-out
By omitting the --output command, the GIF file will be written to std-out.
Expand All @@ -39,4 +40,11 @@ java -jar sgf2gif.jar -f ~/game.sgf > ~/game.gif
By omitting the --file command, the SGF will be taken from the std-in
```shell
java -jar sgf2gif.jar < ~/game.sgf > ~/game.gif
```
```
### Showing markers
By adding the --show-marker flag, each move made in the animation will have a marker on it
```shell
java -jar sgf2gif.jar < ~/game.sgf > ~/game.gif --show-marker
```
![](./nes-with-marker.gif)
12 changes: 6 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ application {

plugins {
application
kotlin("jvm") version "1.8.21"
id("org.jlleitschuh.gradle.ktlint") version "11.3.2"
id("com.github.ben-manes.versions") version "0.46.0"
kotlin("jvm") version "1.9.22"
id("org.jlleitschuh.gradle.ktlint") version "12.0.3"
id("com.github.ben-manes.versions") version "0.50.0"
id("com.github.johnrengelman.shadow") version "8.1.1"
}

group = "com.github.ekenstein"
version = "0.4.1"
version = "0.4.2"

repositories {
mavenCentral()
Expand All @@ -30,7 +30,7 @@ repositories {

dependencies {
implementation("com.github.Ekenstein", "haengma", "2.2.6")
implementation("org.jetbrains.kotlinx", "kotlinx-cli", "0.3.5")
implementation("org.jetbrains.kotlinx", "kotlinx-cli", "0.3.6")
testImplementation(kotlin("test"))
}

Expand Down Expand Up @@ -89,7 +89,7 @@ tasks {
}

ktlint {
version.set("0.45.2")
version.set("0.47.1")
}

class UpgradeToUnstableFilter : ComponentFilter {
Expand Down
Binary file modified dist/lib/sgf2gif.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Binary file added nes-with-marker.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface GifSequenceWriter {

private class GifSequenceWriterImpl(
private val writer: ImageWriter,
private val metaData: IIOMetadata,
private val metaData: IIOMetadata
) : GifSequenceWriter {
override fun addFrame(image: RenderedImage) {
writer.writeToSequence(
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/com/github/ekenstein/sgf2gif/BoardTheme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ data class Stone(val point: SgfPoint, val color: SgfColor)

interface BoardTheme {
fun drawEmptyBoard(g: Graphics2D)
fun drawStone(g: Graphics2D, stone: Stone)
fun drawStone(g: Graphics2D, stone: Stone, drawMarker: Boolean)
fun clearPoint(g: Graphics2D, x: Int, y: Int)
}

Expand All @@ -28,7 +28,7 @@ fun BoardTheme.render(
val boardImage = image(options.width, options.height) { g ->
drawEmptyBoard(g)
board.stones.forEach { (point, color) ->
drawStone(g, Stone(point, color))
drawStone(g, Stone(point, color), false)
}
}

Expand All @@ -41,7 +41,7 @@ fun BoardTheme.render(
val capturedStones = board.stones - updatedBoard.stones.keys

val image = image(options.width, options.height) { g ->
drawStone(g, stone)
drawStone(g, stone, options.showMarker)

capturedStones.forEach { (point, _) ->
clearPoint(g, point.x, point.y)
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/com/github/ekenstein/sgf2gif/Options.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import java.nio.file.InvalidPathException
const val DEFAULT_WIDTH = 1000
const val DEFAULT_HEIGHT = 1000
const val DEFAULT_DELAY_IN_SECONDS = 2
const val DEFAULT_SHOW_MARKER = false
const val DEFAULT_LOOP = false

class Options private constructor(parser: ArgParser) {
Expand Down Expand Up @@ -53,6 +54,12 @@ class Options private constructor(parser: ArgParser) {
description = "Whether the animation should be looped or not"
).default(DEFAULT_LOOP)

val showMarker by parser.option(
type = ArgType.Boolean,
fullName = "show-marker",
description = "Whether the last move should be marked or not"
).default(DEFAULT_SHOW_MARKER)

val width by parser.option(
type = ArgType.Int,
fullName = "width",
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/github/ekenstein/sgf2gif/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package com.github.ekenstein.sgf2gif

enum class Theme {
NES,
Classic,
Classic
}
48 changes: 46 additions & 2 deletions src/main/kotlin/com/github/ekenstein/sgf2gif/themes/Classic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import com.github.ekenstein.sgf.SgfPoint
import com.github.ekenstein.sgf2gif.BoardTheme
import com.github.ekenstein.sgf2gif.Stone
import com.github.ekenstein.sgf2gif.starPoints
import java.awt.BasicStroke
import java.awt.Color
import java.awt.Graphics2D
import java.awt.RenderingHints
import kotlin.math.max
import kotlin.math.min

Expand All @@ -20,6 +22,8 @@ class Classic(
) : BoardTheme {
private val boardColor = Color.WHITE

private var currentMarkedStone: Stone? = null

override fun drawEmptyBoard(g: Graphics2D) {
g.color = boardColor
g.fillRect(0, 0, canvasWidth, canvasHeight)
Expand All @@ -35,7 +39,7 @@ class Classic(
}
}

override fun drawStone(g: Graphics2D, stone: Stone) {
override fun drawStone(g: Graphics2D, stone: Stone, drawMarker: Boolean) {
val middleX = boardX(stone.point.x - 1, canvasWidth, boardWidth)
val middleY = boardY(stone.point.y - 1, canvasHeight, boardHeight)

Expand All @@ -55,9 +59,47 @@ class Classic(
g.fillOval(topLeftX, topLeftY, circleWidth, circleHeight)

g.color = Color.BLACK
g.stroke = BasicStroke(1F)
g.drawOval(topLeftX, topLeftY, circleWidth, circleHeight)
}
}

if (drawMarker) {
drawMarker(g, stone)
}
}

private fun clearMarker(g: Graphics2D, stone: Stone) {
drawStone(g, stone, false)
}

private fun drawMarker(g: Graphics2D, stone: Stone) {
val middleX = boardX(stone.point.x - 1, canvasWidth, boardWidth)
val middleY = boardY(stone.point.y - 1, canvasHeight, boardHeight)

val markerWidthFactor = 0.55
val circleWidth = (intersectionWidth(canvasWidth, boardWidth) * markerWidthFactor).toInt()
val circleHeight = (intersectionHeight(canvasHeight, boardHeight) * markerWidthFactor).toInt()

val topLeftX = middleX - (circleWidth / 2)
val topLeftY = middleY - (circleHeight / 2)

val lineColor = when (stone.color) {
SgfColor.Black -> Color.WHITE
SgfColor.White -> Color.BLACK
}

when (val markedStone = currentMarkedStone) {
null -> { }
else -> clearMarker(g, markedStone)
}

g.color = lineColor
g.stroke = BasicStroke(3F)
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.drawOval(topLeftX, topLeftY, circleWidth, circleHeight)

currentMarkedStone = stone
}

override fun clearPoint(g: Graphics2D, x: Int, y: Int) {
Expand All @@ -82,6 +124,7 @@ class Classic(
drawStarPoint(g, point)
}

g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.color = Color.BLACK
g.drawLine(
max(xOffset(canvasWidth), middleX - (rectangleWidth / 2)),
Expand Down Expand Up @@ -109,6 +152,7 @@ class Classic(
val topLeftY = middleY - (circleHeight / 2)

g.color = Color.BLACK
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.fillOval(topLeftX, topLeftY, circleWidth, circleHeight)
}

Expand All @@ -120,7 +164,7 @@ class Classic(
val intersectionWidth = intersectionWidth(canvasWidth, boardWidth)

g.color = Color.BLACK

g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
repeat(boardWidth) { x ->
val gx = boardX(x, canvasWidth, boardWidth)
g.drawLine(gx, yOffset, gx, yOffset + (intersectionHeight * (boardHeight - 1)))
Expand Down
88 changes: 86 additions & 2 deletions src/main/kotlin/com/github/ekenstein/sgf2gif/themes/Nes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import java.lang.Float.min
private val COLOR_SHADOW = Color.BLACK
private val COLOR_GOBAN = Color(162, 92, 0, 255)
private val COLOR_BACKGROUND = Color(0, 79, 92, 255)
private val COLOR_MARKER = Color(212, 240, 160, 255)
private val COLOR_MARKER_SHADOW = Color(86, 93, 24, 255)

private const val PIXEL_SIZE = 5F
private const val PILLAR_HEIGHT = 7 * PIXEL_SIZE
Expand Down Expand Up @@ -60,6 +62,8 @@ class Nes(
private val stoneWidth = stoneWidthPixels * pixelWidth
private val stoneHeight = stoneHeightPixels * pixelHeight

private var currentMarkedStone: Stone? = null

private fun playAreaX(x: Int): Float {
return playAreaStartX + intersectionWidth * (x - 1)
}
Expand Down Expand Up @@ -202,12 +206,92 @@ class Nes(
g.drawShadowedLine(startX + 2 * PIXEL_SIZE, startY + PIXEL_SIZE * 6, 11)
}

override fun drawStone(g: Graphics2D, stone: Stone) {
override fun drawStone(g: Graphics2D, stone: Stone, drawMarker: Boolean) {
val (x, y) = stone.point
when (stone.color) {
SgfColor.Black -> drawBlackStone(g, x, y)
SgfColor.White -> drawWhiteStone(g, x, y)
}

if (drawMarker) {
drawMarker(g, stone)
}
}

private fun clearMarker(g: Graphics2D, stone: Stone) {
drawStone(g, stone, false)

// remove the last shadow from the marker
g.color = COLOR_GOBAN

val (x, y) = stone.point

val markerHeight = stoneHeight
val markerWidth = stoneWidth

val gx = playAreaX(x)
val gy = playAreaY(y)

val topLeftX = gx - (markerWidth / 2)
val topLeftY = gy - (markerHeight / 2)
val bottomY = topLeftY + markerHeight - pixelHeight
val rightX = topLeftX + markerWidth - pixelWidth

g.fill(Rectangle2D.Float(topLeftX, bottomY + pixelHeight, pixelWidth * 3, pixelHeight))
g.fill(Rectangle2D.Float(rightX - 2 * pixelWidth, bottomY + pixelHeight, pixelWidth * 3, pixelHeight))
}

private fun drawMarker(g: Graphics2D, stone: Stone) {
when (val markedStone = currentMarkedStone) {
null -> { }
else -> clearMarker(g, markedStone)
}

val (x, y) = stone.point
g.color = COLOR_MARKER
val markerHeight = stoneHeight
val markerWidth = stoneWidth

val gx = playAreaX(x)
val gy = playAreaY(y)

val topLeftX = gx - (markerWidth / 2)
val topLeftY = gy - (markerHeight / 2)
val rightX = topLeftX + markerWidth - pixelWidth

// top
g.fill(Rectangle2D.Float(topLeftX, topLeftY, 3 * pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(rightX - 2 * pixelWidth, topLeftY, 3 * pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(topLeftX, topLeftY + pixelHeight, pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(rightX, topLeftY + pixelHeight, pixelWidth, pixelHeight))

// bottom
val bottomY = topLeftY + markerHeight - pixelHeight
g.fill(Rectangle2D.Float(topLeftX, bottomY, 3 * pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(rightX - 2 * pixelWidth, bottomY, 3 * pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(topLeftX, bottomY - pixelHeight, pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(rightX, bottomY - pixelHeight, pixelWidth, pixelHeight))

// middle
val middleX = topLeftX + (markerWidth / 2) - pixelWidth
val middleY = topLeftY + (markerHeight / 2) - pixelHeight

g.fill(Rectangle2D.Float(middleX, middleY, 2 * pixelWidth, 2 * pixelHeight))

g.color = COLOR_MARKER_SHADOW
// shadow

g.fill(Rectangle2D.Float(middleX, middleY + 2 * pixelHeight, 2 * pixelWidth, pixelHeight))

g.fill(Rectangle2D.Float(topLeftX + pixelWidth, topLeftY + pixelHeight, 2 * pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(rightX - 2 * pixelWidth, topLeftY + pixelHeight, 2 * pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(topLeftX, topLeftY + 2 * pixelHeight, pixelWidth, pixelHeight))
g.fill(Rectangle2D.Float(rightX, topLeftY + 2 * pixelHeight, pixelWidth, pixelHeight))

g.fill(Rectangle2D.Float(topLeftX, bottomY + pixelHeight, pixelWidth * 3, pixelHeight))
g.fill(Rectangle2D.Float(rightX - 2 * pixelWidth, bottomY + pixelHeight, pixelWidth * 3, pixelHeight))

currentMarkedStone = stone
}

override fun clearPoint(g: Graphics2D, x: Int, y: Int) {
Expand Down Expand Up @@ -263,7 +347,7 @@ class Nes(
private fun drawWhiteStone(
g: Graphics2D,
x: Int,
y: Int,
y: Int
) {
val gx = playAreaX(x)
val gy = playAreaY(y)
Expand Down
Loading

0 comments on commit e03c224

Please sign in to comment.