/FloorMakerComponentMounter.kt Secret
Created
February 27, 2025 14:34
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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