From: Lanius Trolling Date: Sat, 3 Feb 2024 19:24:16 +0000 (-0500) Subject: Add Mechyrdia Sans font sample X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=27e6b163d1ad7da1205a5abc53124741cf7f66f4;p=factbooks Add Mechyrdia Sans font sample --- diff --git a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt b/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt index 36316cd..6974f15 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt @@ -25,6 +25,9 @@ import io.ktor.server.routing.* import io.ktor.server.sessions.* import io.ktor.server.sessions.serialization.* import io.ktor.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext import org.slf4j.event.Level import java.io.File import java.io.IOException @@ -260,12 +263,31 @@ fun Application.factbooks() { // Utilities + post("/mechyrdia-sans") { + val queryString = call.request.queryParameters + + val isBold = "true".equals(queryString["bold"], ignoreCase = true) + val isItalic = "true".equals(queryString["italic"], ignoreCase = true) + + val alignArg = queryString["align"] + val align = MechyrdiaSansFont.Alignment.entries.singleOrNull { + it.name.equals(alignArg, ignoreCase = true) + } ?: MechyrdiaSansFont.Alignment.LEFT + + val text = call.receiveText() + val svg = runInterruptible(Dispatchers.Default) { + MechyrdiaSansFont.renderTextToSvg(text, isBold, isItalic, align) + } + + call.respondText(svg, ContentType.Image.SVG) + } + post("/tylan-lang") { - call.respondText(TylanAlphabet.tylanToFontAlphabet(call.receiveText())) + call.respondText(TylanAlphabetFont.tylanToFontAlphabet(call.receiveText())) } post("/pokhwal-lang") { - call.respondText(PokhwalishAlphabet.pokhwalToFontAlphabet(call.receiveText())) + call.respondText(PokhwalishAlphabetFont.pokhwalToFontAlphabet(call.receiveText())) } post("/preview-comment") { diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt b/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt index 3f8ad93..7bf7da9 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt @@ -500,7 +500,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin td { +"Writes text in the Tylan alphabet: " span(classes = "lang-tylan") { - +TylanAlphabet.tylanToFontAlphabet("rheagda tulasra") + +TylanAlphabetFont.tylanToFontAlphabet("rheagda tulasra") } } } @@ -518,7 +518,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin td { +"Writes text in the Pokhwalish alphabet: " span(classes = "lang-pokhwal") { - +PokhwalishAlphabet.pokhwalToFontAlphabet("pokhvalsqo jargo") + +PokhwalishAlphabetFont.pokhwalToFontAlphabet("pokhvalsqo jargo") } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/fonts.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/fonts.kt new file mode 100644 index 0000000..12e4d0c --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/fonts.kt @@ -0,0 +1,265 @@ +package info.mechyrdia.lore + +import java.awt.Font +import java.awt.RenderingHints +import java.awt.Shape +import java.awt.geom.AffineTransform +import java.awt.geom.GeneralPath +import java.awt.geom.PathIterator +import java.awt.image.BufferedImage + +object MechyrdiaSansFont { + enum class Alignment(val amount: Double) { + LEFT(0.0), CENTER(0.5), RIGHT(1.0), + } + + fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: Alignment, standalone: Boolean = true): String { + val font = if (bold) { + if (italic) + mechyrdiaSansBI + else + mechyrdiaSansB + } else + if (italic) + mechyrdiaSansI + else + mechyrdiaSans + + val shape = layoutText(text, font, align.amount) + return shape.toSvgDocument(standalone) + } + + private const val DEFAULT_FONT_SIZE = 48f + + private fun loadFont(font: String): Font { + return Font + .createFont(Font.TRUETYPE_FONT, javaClass.getResourceAsStream("/fonts/$font.ttf")!!) + .deriveFont(DEFAULT_FONT_SIZE) + } + + private val mechyrdiaSans = loadFont("mechyrdia-sans") + private val mechyrdiaSansB = loadFont("mechyrdia-sans-bold") + private val mechyrdiaSansI = loadFont("mechyrdia-sans-italic") + private val mechyrdiaSansBI = loadFont("mechyrdia-sans-bold-italic") + + private fun layoutText(text: String, font: Font, alignAmount: Double): Shape { + val img = BufferedImage(256, 160, BufferedImage.TYPE_INT_ARGB) + val g2d = img.createGraphics() + try { + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON) + + val fontMetrics = g2d.getFontMetrics(font) + val lines = text.split("\r\n", "\n", "\r") + val width = lines.maxOf { fontMetrics.stringWidth(it) }.toDouble() + var y = 0.0 + + val shape = GeneralPath() + val tf = AffineTransform() + for (line in lines) { + if (line.isNotBlank()) { + val x = (width - fontMetrics.stringWidth(line)) * alignAmount + + val glyphs = font.layoutGlyphVector(g2d.fontRenderContext, line.toCharArray(), 0, line.length, Font.LAYOUT_LEFT_TO_RIGHT) + val textShape = glyphs.outline as GeneralPath + + tf.setToIdentity() + tf.translate(x, y) + shape.append(textShape.getPathIterator(tf), false) + } + + y += fontMetrics.height + } + + return shape + } finally { + g2d.dispose() + } + } + + private fun Shape.toSvgDocument(standalone: Boolean = true): String { + return buildString { + if (standalone) + appendLine("") + + val viewBox = bounds2D + appendLine("") + appendLine(toSvgPath()) + appendLine("") + } + } + + private fun Shape.toSvgPath(): String { + return buildString { + append(" { + append("M ${coords[0]},${coords[1]} ") + } + + PathIterator.SEG_LINETO -> { + append("L ${coords[0]},${coords[1]} ") + } + + PathIterator.SEG_QUADTO -> { + append("Q ${coords[0]},${coords[1]} ${coords[2]},${coords[3]} ") + } + + PathIterator.SEG_CUBICTO -> { + append("C ${coords[0]},${coords[1]} ${coords[2]},${coords[3]} ${coords[4]},${coords[5]} ") + } + + PathIterator.SEG_CLOSE -> { + append("Z ") + } + + else -> error("Invalid segment type $segment") + } + + iterator.next() + } + + append("\" fill-rule=\"") + when (val winding = iterator.windingRule) { + PathIterator.WIND_EVEN_ODD -> append("evenodd") + PathIterator.WIND_NON_ZERO -> append("nonzero") + else -> error("Invalid winding rule $winding") + } + + append("\" />") + } + } +} + +object TylanAlphabetFont { + private val allowedTranslitCharacters = setOf( + ' ', '\r', '\n', '\t', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'x', 'y', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+', + '[', '{', '}', ']', '\\', '|', '/', '<', + '.', ',', ':', ';', '\'', '"', '?', '>', + ) + + private val replacements = listOf( + Regex("([0-9xy]+)(?![\\s0-9xy])") to "$1 ", + Regex("(? + partial.replace(regex, replacement) + } +} + +object PokhwalishAlphabetFont { + private val allowedTranslitCharacters = setOf( + ' ', '\r', '\n', '\t', + 'a', 'b', 'c', 'd', 'e', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'y', 'z', + '.', ',', '\'', '?', '!', + ) + + private val replacements = listOf( + // Vowels + Regex("aa") to "A", + Regex("ae") to "A", + Regex("ee") to "E", + Regex("ei") to "E", + Regex("ey") to "E", + Regex("ie") to "I", + Regex("ii") to "I", + Regex("iy") to "I", + Regex("ao") to "O", + Regex("au") to "O", + Regex("oo") to "O", + Regex("ou") to "U", + Regex("ue") to "U", + Regex("ui") to "U", + Regex("uu") to "U", + // Consonants + Regex("tz") to "C", + Regex("hh") to "K", + Regex("kh") to "K", + Regex("gh") to "G", + Regex("ng(?![aeiouAEIOU])") to "N", + Regex("ng([aeiouAEIOU])") to "Ng$1", + Regex("n'g") to "ng", + Regex("qh") to "Q", + Regex("th") to "T", + + Regex("ck") to "q", + Regex("c") to "", + Regex("k") to "q", + ) + + fun pokhwalToFontAlphabet(pokhwal: String) = replacements.fold(pokhwal.lowercase().filter { it in allowedTranslitCharacters }) { partial, (regex, replacement) -> + partial.replace(regex, replacement) + } +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/languages.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/languages.kt deleted file mode 100644 index 362eccf..0000000 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/languages.kt +++ /dev/null @@ -1,131 +0,0 @@ -package info.mechyrdia.lore - -object TylanAlphabet { - private val allowedTranslitCharacters = setOf( - ' ', '\r', '\n', '\t', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'x', 'y', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+', - '[', '{', '}', ']', '\\', '|', '/', '<', - '.', ',', ':', ';', '\'', '"', '?', '>', - ) - - private val replacements = listOf( - Regex("([0-9xy]+)(?![\\s0-9xy])") to "$1 ", - Regex("(? - partial.replace(regex, replacement) - } -} - -object PokhwalishAlphabet { - private val allowedTranslitCharacters = setOf( - ' ', '\r', '\n', '\t', - 'a', 'b', 'c', 'd', 'e', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'y', 'z', - '.', ',', '\'', '?', '!', - ) - - private val replacements = listOf( - // Vowels - Regex("aa") to "A", - Regex("ae") to "A", - Regex("ee") to "E", - Regex("ei") to "E", - Regex("ey") to "E", - Regex("ie") to "I", - Regex("ii") to "I", - Regex("iy") to "I", - Regex("ao") to "O", - Regex("au") to "O", - Regex("oo") to "O", - Regex("ou") to "U", - Regex("ue") to "U", - Regex("ui") to "U", - Regex("uu") to "U", - // Consonants - Regex("tz") to "C", - Regex("hh") to "K", - Regex("kh") to "K", - Regex("gh") to "G", - Regex("ng(?![aeiouAEIOU])") to "N", - Regex("ng([aeiouAEIOU])") to "Ng$1", - Regex("n'g") to "ng", - Regex("qh") to "Q", - Regex("th") to "T", - - Regex("ck") to "q", - Regex("c") to "", - Regex("k") to "q", - ) - - fun pokhwalToFontAlphabet(pokhwal: String) = replacements.fold(pokhwal.lowercase().filter { it in allowedTranslitCharacters }) { partial, (regex, replacement) -> - partial.replace(regex, replacement) - } -} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt index 0ff8cff..f1dc72c 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/parser_tags.kt @@ -322,7 +322,7 @@ enum class TextParserFormattingTag(val type: TextParserTagType) { TextParserTagType.Indirect { tagParam, content, _ -> if (tagParam?.equals("tylan", ignoreCase = true) == true) { val uncensored = TextParserState.uncensorText(content) - val tylan = TylanAlphabet.tylanToFontAlphabet(uncensored) + val tylan = TylanAlphabetFont.tylanToFontAlphabet(uncensored) val recensored = TextParserState.censorText(tylan) "$recensored" } else if (tagParam?.equals("thedish", ignoreCase = true) == true) @@ -331,7 +331,7 @@ enum class TextParserFormattingTag(val type: TextParserTagType) { "$content" else if (tagParam?.equals("pokhval", ignoreCase = true) == true || tagParam?.equals("pokhwal", ignoreCase = true) == true) { val uncensored = TextParserState.uncensorText(content) - val pokhwal = PokhwalishAlphabet.pokhwalToFontAlphabet(uncensored) + val pokhwal = PokhwalishAlphabetFont.pokhwalToFontAlphabet(uncensored) val recensored = TextParserState.censorText(pokhwal) "$recensored" } else content @@ -339,7 +339,22 @@ enum class TextParserFormattingTag(val type: TextParserTagType) { ), ALPHABET( TextParserTagType.Indirect { _, content, _ -> - if (content.equals("tylan", ignoreCase = true)) { + if (content.equals("mechyrdian", ignoreCase = true)) { + """ + |
+ |

Input Text:

+ | + |

Font options:

+ |
    + |
  • + |
  • + |
  • + |
+ |

Rendered in Mechyrdia Sans:

+ | + |
+ """.trimMargin() + } else if (content.equals("tylan", ignoreCase = true)) { """ |
|

Latin Alphabet:

diff --git a/src/jvmMain/resources/fonts/mechyrdia-sans-bold-italic.ttf b/src/jvmMain/resources/fonts/mechyrdia-sans-bold-italic.ttf new file mode 100644 index 0000000..f1be773 Binary files /dev/null and b/src/jvmMain/resources/fonts/mechyrdia-sans-bold-italic.ttf differ diff --git a/src/jvmMain/resources/fonts/mechyrdia-sans-bold.ttf b/src/jvmMain/resources/fonts/mechyrdia-sans-bold.ttf new file mode 100644 index 0000000..5155f69 Binary files /dev/null and b/src/jvmMain/resources/fonts/mechyrdia-sans-bold.ttf differ diff --git a/src/jvmMain/resources/fonts/mechyrdia-sans-italic.ttf b/src/jvmMain/resources/fonts/mechyrdia-sans-italic.ttf new file mode 100644 index 0000000..89fd2d8 Binary files /dev/null and b/src/jvmMain/resources/fonts/mechyrdia-sans-italic.ttf differ diff --git a/src/jvmMain/resources/fonts/mechyrdia-sans.ttf b/src/jvmMain/resources/fonts/mechyrdia-sans.ttf new file mode 100644 index 0000000..9bdb85c Binary files /dev/null and b/src/jvmMain/resources/fonts/mechyrdia-sans.ttf differ diff --git a/src/jvmMain/resources/static/init.js b/src/jvmMain/resources/static/init.js index 2efe1d9..b444c93 100644 --- a/src/jvmMain/resources/static/init.js +++ b/src/jvmMain/resources/static/init.js @@ -17,6 +17,52 @@ }); } + window.addEventListener("load", function () { + // Mechyrdian font + async function mechyrdianToFont(input, boldOpt, italicOpt, alignOpt, output) { + const inText = input.value; + + await delay(1500); + if (inText !== input.value) return; + + let queryString = "?"; + queryString += boldOpt.checked ? "bold=true&" : ""; + queryString += italicOpt.checked ? "italic=true&" : ""; + queryString += "align=" + alignOpt.value; + + const outBlob = await (await fetch('/mechyrdia-sans' + queryString, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + body: inText, + })).blob(); + + if (inText !== input.value) return; + + const prevObjectUrl = output.src; + if (prevObjectUrl != null && prevObjectUrl.length > 0) + URL.revokeObjectURL(prevObjectUrl); + + output.src = URL.createObjectURL(outBlob); + } + + const mechyrdiaSansBoxes = document.getElementsByClassName("mechyrdia-sans-box"); + for (const mechyrdiaSansBox of mechyrdiaSansBoxes) { + const inputBox = mechyrdiaSansBox.getElementsByClassName("input-box")[0]; + const boldOpt = mechyrdiaSansBox.getElementsByClassName("bold-option")[0]; + const italicOpt = mechyrdiaSansBox.getElementsByClassName("ital-option")[0]; + const alignOpt = mechyrdiaSansBox.getElementsByClassName("align-opts")[0]; + const outputBox = mechyrdiaSansBox.getElementsByClassName("output-img")[0]; + + const inputListener = () => mechyrdianToFont(inputBox, boldOpt, italicOpt, alignOpt, outputBox); + boldOpt.addEventListener("change", inputListener); + italicOpt.addEventListener("change", inputListener); + alignOpt.addEventListener("change", inputListener); + inputBox.addEventListener("input", inputListener); + } + }); + window.addEventListener("load", function () { // Tylan alphabet async function tylanToFont(input, output) {