Skip to content

Commit

Permalink
Merge branch 'develop' into candidate
Browse files Browse the repository at this point in the history
  • Loading branch information
patbeagan1 committed Jan 9, 2023
2 parents 1d296f8 + fa9125c commit 40f386e
Show file tree
Hide file tree
Showing 42 changed files with 925 additions and 170 deletions.
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# ConsoleVision
https://patbeagan.dev/projects/consolevideo
![Screenshot_2022-04-27_23-45-04](https://user-images.githubusercontent.com/10187351/165678333-b3c45bbe-1a8b-49ae-91ab-48feb0337482.png)

# What is this project about?

`ConsoleVision` is a library that will take a bitmap and convert it to a ANSI

With this tool, you'll be able to display images without leaving your terminal. This can be useful when you are just sshing into another machine and don't have GUI access.

To make this more convenient, there is an option to run the tool as a server instead, so the only thing that you need installed is `curl`

# What's included?

There are 2 deliverables from this repo
- **lib** - a library that will take a bitmap and convert it to a ANSI
- **app** - a wrapper that makes the library available via CLI and as a server.
<details>
<summary>As a Library</summary>

## As a library

The library jar file includes an implementation of [ANSI](https://mudhalla.net/tintin/info/ansicolor/) for the JVM. It has some similar content to [Jansi](http://fusesource.github.io/jansi/) (which I was unaware of at the time), but it includes extensions that make it more useful for image processing.

## As an app
</details>

<details>
<summary>As an App</summary>

The app supports a variety of command line flags which will allow for:
- colorspace reduction
Expand All @@ -31,7 +35,12 @@ The app supports a variety of command line flags which will allow for:
|-|-|-|-|
|<img width="705" alt="Screen Shot 2022-02-05 at 8 51 15 AM" src="https://user-images.githubusercontent.com/10187351/152647110-105c8015-f7a7-4a98-aaff-6947722651b6.png">|<img width="700" alt="Screen Shot 2022-02-05 at 8 50 53 AM" src="https://user-images.githubusercontent.com/10187351/152647111-787eeef5-dd59-4ef8-8e0d-47678f44f953.png">|<img width="719" alt="Screen Shot 2022-02-05 at 9 03 37 AM" src="https://user-images.githubusercontent.com/10187351/152647252-c9035db3-a684-4818-8455-f917dade6700.png">|<img width="717" alt="Screen Shot 2022-02-05 at 9 02 59 AM" src="https://user-images.githubusercontent.com/10187351/152647253-1c6b5be0-bb7f-4b58-a98e-ba10c253106a.png">|

It also has the ability to be run as a server, with the `-s` flag. This will allow you to use a limited feature set of the command line tool, in a more convenient way.
</details>

<details>
<summary>As a Server</summary>

Running the tool as a server will allow you to use a limited feature set of the command line tool, in a more convenient way.
- To upload a photo, POST to the `/upload` endpoint. You'll receive an image hash.
- To retrieve a photo, GET to the `/im/{id}` endpoint, using an image hash.
- To retrieve the last photo, GET to the `/last` endpoint
Expand Down Expand Up @@ -62,7 +71,10 @@ I have a server where this is deployed as well, if you just want to test it out.
```bash
curl 3.221.34.94/im/eefbb5b84ef2d8824f3fcaf64c54a63a
```
</details>

---

### Other things to note?
## Other things to note?

Initial functionality that allowed for playing videos has been taken out. The video player that was being used, [humble](https://github.com/artclarke/humble-video/blob/master/humble-video-demos/src/main/java/io/humble/video/demos/DecodeAndPlayVideo.java), was not able to be packaged into a shadow jar.
10 changes: 0 additions & 10 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,7 @@ repositories {
mavenCentral()
}

object Versions {
const val versionKtor = "1.6.7"
}

dependencies {
implementation("ch.qos.logback:logback-classic:1.2.5")
implementation("commons-codec:commons-codec:1.13")
implementation("io.insert-koin:koin-core:3.1.2")
implementation("io.ktor:ktor-html-builder:${Versions.versionKtor}")
implementation("io.ktor:ktor-server-core:${Versions.versionKtor}")
implementation("io.ktor:ktor-server-netty:${Versions.versionKtor}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.mod4j.org.apache.commons:cli:1.0.0")
implementation(project(":lib"))
Expand Down
300 changes: 300 additions & 0 deletions app/src/main/kotlin/dev/patbeagan/consolevision/Lines.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
package dev.patbeagan.consolevision.consolevision.demo

import dev.patbeagan.consolevision.ansi.AnsiColor
import dev.patbeagan.consolevision.ansi.AnsiConstants.HIDE_CURSOR

import dev.patbeagan.consolevision.ansi.AnsiConstants.RIS
import dev.patbeagan.consolevision.style
import dev.patbeagan.consolevision.types.ColorInt
import dev.patbeagan.consolevision.types.CompressedPoint
import dev.patbeagan.consolevision.types.CoordRect
import dev.patbeagan.consolevision.types.List2D
import dev.patbeagan.consolevision.types.coord
import dev.patbeagan.consolevision.types.coordRect
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.random.Random

fun main() {
(1 coord 1).lineByDDATo(3 coord 4)
getCircleByBresenham(1 coord 1, 3)
}

fun CompressedPoint.lineByDDATo(end: CompressedPoint): List<CompressedPoint> {
val dx = end.x - x
val dy = end.y - y
val steps = max(abs(dx), abs(dy))
val stepX = dx / steps.toFloat();
val stepY = dy / steps.toFloat();

var x = x.toFloat()
var y = y.toFloat()

return (0..steps).map {
val ret = x.roundToInt() coord y.roundToInt()
x += stepX
y += stepY
ret
}
}

fun MutableList<CompressedPoint>.drawCircle(x: Int, y: Int, p: Int, q: Int) = addAll(
listOf(
(x + p) coord (y + q),
(x - p) coord (y + q),
(x + p) coord (y - q),
(x - p) coord (y - q),
(x + q) coord (y + p),
(x - q) coord (y + p),
(x + q) coord (y - p),
(x - q) coord (y - p)
)
)

fun circleBres(xc: Int, yc: Int, r: Int): Pair<List<CompressedPoint>, CoordRect> {
var x = 0
var y = r
var d = 3 - 2 * r
val res = mutableListOf<CompressedPoint>()
res.drawCircle(xc, yc, x, y)
while (y >= x) {
// for each pixel we will
// draw all eight pixels
x++

// check for decision parameter
// and correspondingly
// update d, x, y
d = if (d > 0) {
y--
d + 4 * (x - y) + 10
} else d + 4 * x + 6
res.drawCircle(xc, yc, x, y)
}
val topLeft = (xc - r) coord (yc - r)
val botRight = (xc + r) coord (yc + r)
return res.distinct() to (topLeft coordRect botRight)
}

fun List2D<Boolean>.fillPolygon(
rect: CoordRect,
fill: Boolean = true,
): List2D<Boolean> {
var last = false
var isInShape = 0
var entering = true

for (y in (rect.lesser.y + 1)..rect.greater.y) {
for (x in rect.lesser.x..rect.greater.x) {
if (y !in 1 until height) continue
val b = this.at(x, y)
when {
b != last -> {
if (isInShape == 2) entering = false
if (isInShape == 0) entering = true
if (entering) isInShape++ else isInShape--
if (isInShape > 0) assign(x, y, fill)
}

isInShape > 0 -> assign(x, y, fill)
}
last = b
}

isInShape = 0
entering = true
last = false
}
return this
}

fun getCircleByBresenham(center: CompressedPoint, radius: Int) {
// var p = 0
// var q = radius
// var r = radius
// var decision = 3 - 2 * r
// val results = mutableListOf<Coord>()
// while (p < q) {
// results.drawCircle(center.x, center.y, p, q)
// p++
// if (decision < 0) {
// decision += 4 * p + 6
// } else {
// r -= 1
// decision += 4 * (p - q) + 10
// }
// results.drawCircle(center.x, center.y, p, q)
// }
// val sorted = results.sorted()
// (0..results.maxBy { it.y }!!.y).forEach { y ->
// (0..results.maxBy { it.x }!!.x).forEach { x ->
// if (x coord y in sorted) print("x") else print(".")
// }
// println()
// }
// println(sorted)
// println()

previewCircle()

val screen = (Array(40) {
Array(80) { 0 }
}).toList2D()
var tick = 0
forever(1000 / 20) {
println(HIDE_CURSOR + RIS)
(0..5).forEach {
screen.addLayer((it * 192 % 256) shl 16 or 0x00AA88) {
drawCircle(getRandomCircleCoordinate(it), 5)
}
}
screen.addLayer(0xff0000) {
drawCircle(((tick++) % 80) coord 20, 10)
}
screen.also { it.traverseMap { ColorInt(it) }.printScreenColor() }
screen.traverseMutate { x, y, i -> 0 }
}
}

fun forever(limiter: Int? = 100, action: () -> Unit) {
while (true) {
action()
limiter?.let { Thread.sleep(it.toLong()) }
}
}


fun List2D<Boolean>.drawCircle(
center: CompressedPoint,
radius: Int
) {
circleBres(center.x, center.y, radius).also {
val (list, _) = it
traverseAssign(list, true)
}
}

fun List2D<Boolean>.drawLine(
start: CompressedPoint,
end: CompressedPoint
) {
start.lineByDDATo(end).also { traverseAssign(it, true) }
}

fun List2D<Boolean>.drawCircleFilled(
center: CompressedPoint,
radius: Int
) {
circleBres(center.x, center.y, radius).also {
val (list, rect) = it
traverseAssign(list, true)
this.fillPolygon(rect.modifyBy(gy = -1), true)
}
}

private fun previewCircle() {
val screen = (Array(43) {
Array(80) { false }
}).toList2D()
// circleBres(20, 20, 10).also { pair ->
// val (list, rect) = pair
// screen.traverseAssign(list, true)
//
// printScreen(screen)
//
// println()
//
// screen.fillPolygon(rect.modifyBy(gy = -1), true)
// printScreen(screen)
// }

screen.drawCircleFilled(30 coord 30, 10)
screen.drawCircle(20 coord 20, 10)
screen.printScreen()

val screen2 = screen.traverseMap { false }
screen2.drawCircleFilled(35 coord 35, 10)

val merge =
screen.mergeWith(screen2, false) { first: Boolean, second: Boolean -> first xor second }
merge.printScreen()

val screenColor = screen
.traverseMapIndexed { x, y, t -> x shl 16 or y shl 8 }
.also {
it.traverseMap { ColorInt(it) }.printScreenColor()
}

val screenColor2 = screen
.traverseMap { t -> if (t) 0xFF0000 else 0 }

val also = screenColor.mergeWith(screenColor2, 0) { first: Int, second: Int ->
if (second != 0) second else first
}.also {
it.traverseMap { ColorInt(it) }.printScreenColor()
}

val traverseMap = screen.traverseMap { false }
traverseMap.drawLine(23 coord 20, 40 coord 75)
(also to traverseMap).merge(0) { first, second ->
if (second) 0xffffff else first
}.also {
it.traverseMap { ColorInt(it) }.printScreenColor()
}

(0..5).forEach {
also.addLayer((it * 192 % 256) shl 16 or 0x00AA88) {
drawCircle(
getRandomCircleCoordinate(it),
5
)
}
}
also.also {
it.traverseMap { ColorInt(it) }.printScreenColor()
}

// circleBres(40, 30, 20).also {
// val (list, rect) = it
// screen.traverseAssign(list, true)
// screen.fillPolygon(rect.modifyBy(gy = -1), true)
// }
//
// printScreen(screen)
}

private fun getRandomCircleCoordinate(it: Int) =
it * 7 + (Random.nextInt() % 6) coord 32 + (Random.nextInt() % 20) * if (Random.nextBoolean()) 1 else -1

private fun List2D<Boolean>.printScreen() {
traverseMap { t -> if (t) "XX" else ".." }.printAll("")
}

private fun List2D<ColorInt>.printScreenColor() {
traverseMap { t ->
" ".style(
colorBackground = AnsiColor.Custom(t)
)
}.printAll("")
}

inline fun <reified T> Array<Array<T>>.toList2D() = List2D.from(map { rows -> rows.toList() })

inline fun <reified T, reified S, reified R> Pair<List2D<T>, List2D<S>>.merge(
default: R,
crossinline onElement: (first: T, second: S) -> R,
): List2D<R> = first.mergeWith(second, default, onElement)

fun List2D<Int>.addLayer(
color: Int,
config: List2D<Boolean>.() -> Unit,
) {
val other = traverseMap { false }.also(config)
this.traverseMutate { x, y, each ->
if (other.isValidCoordinate(x coord y)) {
if (other.at(x, y)) return@traverseMutate color
}
return@traverseMutate each
}
}
Loading

0 comments on commit 40f386e

Please sign in to comment.