Skip to content

Instantly share code, notes, and snippets.

@MrPowerGamerBR
Created February 27, 2025 14:34
package net.sneakysims.website.frontend.components
import js.typedarrays.toUint8Array
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.perfectdreams.slippyimage.*
import net.sneakysims.floormaker.FloorMaker
import net.sneakysims.floormaker.FloorSound
import net.sneakysims.website.frontend.*
import net.sneakysims.website.frontend.reactcomponents.TSORadioButton
import react.*
import react.dom.client.createRoot
import react.dom.html.ReactHTML.button
import react.dom.html.ReactHTML.canvas
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.input
import react.dom.html.ReactHTML.textarea
import web.blob.Blob
import web.canvas.CanvasRenderingContext2D
import web.cssom.ClassName
import web.dom.document
import web.events.EventHandler
import web.file.File
import web.file.FileReader
import web.html.*
import web.url.URL
class FloorMakerComponentMounter : ComponentMounter("floor-maker") {
override fun mount(element: HTMLElement) {
GlobalScope.launch {
val wallpaperPreviewBase = convertJSImageToImageData(loadImage("/assets/img/floor_base.png"))
val wallpaperMakerComponent = FC<Props> {
var selectedFile by useState<File?>(null)
var wallpaperName by useState<String>("My Floor")
var wallpaperPrice by useState<Int>(1)
var wallpaperDescription by useState<String>("Loritta is so cute!!!")
val canvasRef = useRef<HTMLCanvasElement>(null)
var generatedFloorImage by useState<FloorImage?>(null)
var floorSound by useState<FloorSound>(FloorSound.HARD_FLOOR)
fun updatePreview(generatedFloorImage: FloorImage?) {
if (generatedFloorImage != null) {
val previewImage = SlippyImage.convertToSlippyImage(wallpaperPreviewBase)
// Yes it is 128, not 127, because the bottom tile "connects" to the top tile, it is a bit confusing
previewImage.drawImage(
generatedFloorImage.floor,
0,
32,
127,
32,
257,
240,
)
repeat(2) {
previewImage.drawImage(
generatedFloorImage.floor,
193 + (it * 128),
272 - 32
)
}
repeat(3) {
previewImage.drawImage(
generatedFloorImage.floor,
129 + (it * 128),
304 - 32
)
}
repeat(4) {
previewImage.drawImage(
generatedFloorImage.floor,
65 + (it * 128),
336 - 32
)
}
repeat(5) {
previewImage.drawImage(
generatedFloorImage.floor,
1 + (it * 128),
368 - 32
)
}
repeat(6) {
previewImage.drawImage(
generatedFloorImage.floor,
-63 + (it * 128),
400 - 32
)
}
canvasRef.current!!.width = 640
canvasRef.current!!.height = 400
val ctx = canvasRef.current!!.getContext(CanvasRenderingContext2D.ID)!!
ctx.putImageData(previewImage.toImageData(), 0, 0)
} else {
val previewImage = SlippyImage.convertToSlippyImage(wallpaperPreviewBase)
canvasRef.current!!.width = 640
canvasRef.current!!.height = 400
val ctx = canvasRef.current!!.getContext(CanvasRenderingContext2D.ID)!!
ctx.putImageData(previewImage.toImageData(), 0, 0)
}
}
input {
this.type = InputType.file
this.accept = "image/*"
onChange = {
generatedFloorImage = null
val file = it.target.files?.item(0)!!
val reader = FileReader()
selectedFile = file
reader.onload = EventHandler { a ->
val image = Image()
image.onload = EventHandler { b ->
// When the image is loaded, we will generate EVERYTHING (except the IFF file!)
val sourceImage = SlippyImage.convertToSlippyImage(convertJSImageToImageData(image))
val floorSprite = FloorMaker.createFloorSprite(sourceImage)
val allColors = PaletteCreator.extractColors(floorSprite)
val quantizedPalette = PaletteCreator.kMeansQuantization(allColors, 256) // Create a 256 colors palette
// Apply the palette to the image!
PaletteCreator.applyPalette(floorSprite, quantizedPalette)
val floorImage = FloorImage(
floorSprite,
quantizedPalette
)
generatedFloorImage = floorImage
updatePreview(floorImage)
}
image.src = a.target.result as String
}
reader.readAsDataURL(file) // Convert image file to Data URL
}
}
div {
plainStyle = "display: flex; flex-direction: row; gap: 1em;"
div {
plainStyle =
"display: flex; flex-wrap: wrap; align-content: flex-start; flex-direction: column;"
div {
+"Preview"
}
div {
className = ClassName("content-section-wrapper")
plainStyle = "display: flex; flex-wrap: wrap; align-content: flex-start;"
canvas {
ref = canvasRef
useEffectOnce {
updatePreview(null)
}
}
}
}
div {
plainStyle = "display: flex; flex-wrap: wrap; align-content: flex-start; flex-direction: column;"
val palette = generatedFloorImage?.palette
div {
+"Palette (${palette?.size ?: 0} colors)"
}
div {
className = ClassName("content-section-wrapper")
plainStyle = "display: flex; flex-wrap: wrap; align-content: flex-start;"
if (palette != null) {
for (color in palette) {
div {
plainStyle =
"background-color: rgb(${color.red}, ${color.green}, ${color.blue}); width: 16px; height: 16px; border: 1px solid black;"
}
}
}
}
}
}
div {
plainStyle = "display: flex; gap: 1em; flex-direction: column;"
div {
plainStyle = "display: flex; gap: 1em;"
div {
plainStyle = "flex-grow: 1;"
div {
+"Floor Name"
}
input {
plainStyle = "width: 100%;"
this.type = InputType.text
this.value = wallpaperName
onChange = {
wallpaperName = it.target.value
}
}
}
div {
div {
+"Floor Price"
}
input {
this.type = InputType.number
this.value = wallpaperPrice
onChange = {
wallpaperPrice = it.target.value.toInt()
}
}
}
}
div {
div {
+"Floor Description"
}
textarea {
plainStyle = "width: 100%;"
this.value = wallpaperDescription
onChange = {
wallpaperDescription = it.target.value
}
}
}
div {
div {
+"Floor Step Sound"
}
div {
className = ClassName("content-section-wrapper")
div {
className = ClassName("tso-radio-button-wrapper")
TSORadioButton {
selected = floorSound == FloorSound.SOFT_FLOOR
}
div {
+"Soft Floor Sound"
}
onClick = {
floorSound = FloorSound.SOFT_FLOOR
}
}
div {
className = ClassName("tso-radio-button-wrapper")
TSORadioButton {
selected = floorSound == FloorSound.MEDIUM_FLOOR
}
+"Medium Floor Sound"
onClick = {
floorSound = FloorSound.MEDIUM_FLOOR
}
}
div {
className = ClassName("tso-radio-button-wrapper")
TSORadioButton {
selected = floorSound == FloorSound.HARD_FLOOR
}
+"Hard Floor Sound"
onClick = {
floorSound = FloorSound.HARD_FLOOR
}
}
}
}
if (generatedFloorImage != null) {
// hacky!!! we probably should create a new component for this?
val generatedFloorImage = generatedFloorImage!!
button {
+"Export"
onClick = {
val iff = FloorMaker.createFloorIFF(
wallpaperName,
wallpaperPrice,
wallpaperDescription,
floorSound,
generatedFloorImage.floor,
generatedFloorImage.palette
)
val bytes = iff.write()
// Create a link element
val link = document.createElement("a") as HTMLAnchorElement
// Set the download link with the Blob URL
link.href = URL.createObjectURL(Blob(arrayOf(bytes.toUint8Array())))
val fileName = wallpaperName
.replace(Regex("[^A-Za-z0-9]"), "")
.ifEmpty { "CustomFloor" }
link.download = "$fileName.flr" // Filename
// Append to the document and trigger click
document.body.appendChild(link)
link.click()
// Remove the link after download
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
plausible("Created Custom Floor")
}
}
} else {
button {
disabled = true
+"Export"
}
}
}
}
createRoot(element).render(wallpaperMakerComponent.create())
}
}
data class FloorImage(
val floor: SlippyImage,
val palette: List<Color>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment