diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/FunCommands.kt b/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/FunCommands.kt index 3a38048..3a50e4a 100644 --- a/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/FunCommands.kt +++ b/common/src/main/kotlin/dev/erdragh/astralbot/commands/discord/FunCommands.kt @@ -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 @@ -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 = 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() @@ -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() + } } \ No newline at end of file diff --git a/common/src/main/kotlin/dev/erdragh/astralbot/util/GifWriter.kt b/common/src/main/kotlin/dev/erdragh/astralbot/util/GifWriter.kt new file mode 100644 index 0000000..1b61f5a --- /dev/null +++ b/common/src/main/kotlin/dev/erdragh/astralbot/util/GifWriter.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/headpat.png b/common/src/main/resources/headpat.png deleted file mode 100644 index 1e27634..0000000 Binary files a/common/src/main/resources/headpat.png and /dev/null differ diff --git a/common/src/main/resources/headpat/pet0.gif b/common/src/main/resources/headpat/pet0.gif new file mode 100644 index 0000000..09c40d4 Binary files /dev/null and b/common/src/main/resources/headpat/pet0.gif differ diff --git a/common/src/main/resources/headpat/pet1.gif b/common/src/main/resources/headpat/pet1.gif new file mode 100644 index 0000000..ee33972 Binary files /dev/null and b/common/src/main/resources/headpat/pet1.gif differ diff --git a/common/src/main/resources/headpat/pet2.gif b/common/src/main/resources/headpat/pet2.gif new file mode 100644 index 0000000..4d09c16 Binary files /dev/null and b/common/src/main/resources/headpat/pet2.gif differ diff --git a/common/src/main/resources/headpat/pet3.gif b/common/src/main/resources/headpat/pet3.gif new file mode 100644 index 0000000..09dedb3 Binary files /dev/null and b/common/src/main/resources/headpat/pet3.gif differ diff --git a/common/src/main/resources/headpat/pet4.gif b/common/src/main/resources/headpat/pet4.gif new file mode 100644 index 0000000..f9fb14c Binary files /dev/null and b/common/src/main/resources/headpat/pet4.gif differ