Skip to content

Commit

Permalink
Make headpats animated :3
Browse files Browse the repository at this point in the history
  • Loading branch information
femmeromantic authored and Erdragh committed Oct 7, 2024
1 parent 34f99b9 commit 416ff16
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package dev.erdragh.astralbot.commands.discord

import dev.erdragh.astralbot.util.GifWriter
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData
import net.dv8tion.jda.api.utils.FileUpload
import java.awt.Color
import java.awt.Graphics2D
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.net.URL
Expand All @@ -15,7 +17,13 @@ object HeadpatCommand : HandledSlashCommand {
private const val USER_OPTION = "user"
override val command: SlashCommandData = Commands.slash("headpat", "Headpats a user")
.addOption(OptionType.USER, USER_OPTION, "The user whose avatar will be headpat.", true)
private val headpatBaseImage = ImageIO.read(this.javaClass.getResource("/headpat.png"))

private val ANIMATION = floatArrayOf(-.05f, .1f, .2f, .19f, .1f)
private val FRAMES: Array<BufferedImage> = Array(5) { ImageIO.read(this::class.java.getResourceAsStream("/headpat/pet$it.gif")) }
private val RENDERING_HINTS = RenderingHints(mapOf(
RenderingHints.KEY_ANTIALIASING to RenderingHints.VALUE_ANTIALIAS_ON,
RenderingHints.KEY_RENDERING to RenderingHints.VALUE_RENDER_QUALITY
))

override fun handle(event: SlashCommandInteractionEvent) {
event.deferReply(false).queue()
Expand All @@ -28,35 +36,40 @@ object HeadpatCommand : HandledSlashCommand {

val url = URL(user.effectiveAvatarUrl)
val avatar = ImageIO.read(url)
val headpatImage = BufferedImage(headpatBaseImage.width, headpatBaseImage.height, BufferedImage.TYPE_INT_ARGB)

val graphics = headpatImage.createGraphics()

val xOffset = 20
val yOffset = 20
graphics.drawImage(
avatar,
xOffset,
yOffset,
headpatImage.width - xOffset,
headpatImage.height - yOffset,
Color(0, 0, 0, 0),
null
)
graphics.drawImage(
headpatBaseImage,
0,
0,
headpatBaseImage.width,
headpatBaseImage.height,
Color(0, 0, 0, 0),
null
)

graphics.dispose()
val byteStream = ByteArrayOutputStream()
ImageIO.write(headpatImage, "png", byteStream)
event.hook.sendFiles(FileUpload.fromData(byteStream.toByteArray(), "headpat.png")).queue()
}

val stream: ByteArrayOutputStream

ByteArrayOutputStream().use { output ->
ImageIO.createImageOutputStream(output).use { out ->
GifWriter(
out, BufferedImage.TYPE_INT_ARGB,
timeBetweenFramesMS = 50, loopContinuously = true, transparent = true
).use { gifWriter ->
for (i in FRAMES.indices) {
val frame = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB)
val graphics = frame.graphics as Graphics2D
// set rendering hints to slightly improve quality
graphics.setRenderingHints(RENDERING_HINTS)

val offset1 = Math.round(ANIMATION[i] * 64)
val offset2 = Math.round((-ANIMATION[i]) * 64)

// draw avatar
graphics.drawImage(avatar, 2, 32 + offset1, 128 - offset2, 128 - 32 - offset1, null)
// draw hand
graphics.drawImage(FRAMES[i], 0, 0, 128, 128, null)

gifWriter.write(frame)

graphics.dispose()
}
}
out.flush()

stream = output
}
}

event.hook.sendFiles(FileUpload.fromData(stream.toByteArray(), "headpat.gif")).queue()
}
}
70 changes: 70 additions & 0 deletions common/src/main/kotlin/dev/erdragh/astralbot/util/GifWriter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dev.erdragh.astralbot.util

import java.awt.image.RenderedImage
import java.io.IOException
import javax.imageio.*
import javax.imageio.metadata.IIOMetadata
import javax.imageio.metadata.IIOMetadataNode
import javax.imageio.stream.ImageOutputStream

/**
* A simple utility for creating gifs.
*
* @author femmeromantic
*/
class GifWriter(
os: ImageOutputStream?, imageType: Int,
timeBetweenFramesMS: Int, loopContinuously: Boolean, transparent: Boolean
) : AutoCloseable {
private val writer: ImageWriter =
ImageIO.getImageWritersBySuffix("gif").next() ?: throw IOException("No GIF Image Writers Exist!")
private val imageWriteParam: ImageWriteParam = writer.defaultWriteParam
private val metadata: IIOMetadata = writer.getDefaultImageMetadata(
ImageTypeSpecifier.createFromBufferedImageType(imageType),
imageWriteParam
)

init {
val root = metadata.getAsTree(metadata.nativeMetadataFormatName) as IIOMetadataNode
setGifAttributes(root, timeBetweenFramesMS, transparent, loopContinuously)
metadata.setFromTree(metadata.nativeMetadataFormatName, root)
writer.output = os
writer.prepareWriteSequence(null)
}

fun write(img: RenderedImage) {
writer.writeToSequence(IIOImage(img, null, metadata), imageWriteParam)
}

override fun close() {
writer.endWriteSequence()
}

private fun getOrCreate(root: IIOMetadataNode, name: String): IIOMetadataNode =
(0 until root.length)
.map { root.item(it) as IIOMetadataNode }
.firstOrNull { it.nodeName == name }
?: IIOMetadataNode(name).also { root.appendChild(it) }

private fun setGifAttributes(
root: IIOMetadataNode, timeBetweenFramesMS: Int, transparent: Boolean, loopContinuously: Boolean
) {
getOrCreate(root, "GraphicControlExtension").apply {
setAttribute("disposalMethod", "restoreToBackgroundColor")
setAttribute("userInputFlag", "FALSE")
setAttribute("transparentColorFlag", if (transparent) "TRUE" else "FALSE")
setAttribute("delayTime", (timeBetweenFramesMS / 10).toString())
setAttribute("transparentColorIndex", "0")
}
getOrCreate(root, "CommentExtensions").setAttribute("CommentExtension", "Test Comment")

val appEN = getOrCreate(root, "ApplicationExtensions")
val loop = if (loopContinuously) 0 else 1
IIOMetadataNode("ApplicationExtension").apply {
setAttribute("applicationID", "NETSCAPE")
setAttribute("authenticationCode", "2.0")
userObject = byteArrayOf(0x1, loop.toByte(), 0)
appEN.appendChild(this)
}
}
}
Binary file removed common/src/main/resources/headpat.png
Binary file not shown.
Binary file added common/src/main/resources/headpat/pet0.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added common/src/main/resources/headpat/pet1.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added common/src/main/resources/headpat/pet2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added common/src/main/resources/headpat/pet3.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added common/src/main/resources/headpat/pet4.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 416ff16

Please sign in to comment.