[orx-text-on-contour] Add support for drawing text along contours
This commit is contained in:
10
orx-text-on-contour/README.md
Normal file
10
orx-text-on-contour/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# orx-text-on-contour
|
||||||
|
|
||||||
|
Writing texts on contours.
|
||||||
|
|
||||||
|
<!-- __demos__ -->
|
||||||
|
## Demos
|
||||||
|
### DemoTextWriter01
|
||||||
|
[source code](src/jvmDemo/kotlin/DemoTextWriter01.kt)
|
||||||
|
|
||||||
|

|
||||||
22
orx-text-on-contour/build.gradle.kts
Normal file
22
orx-text-on-contour/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
plugins {
|
||||||
|
org.openrndr.extra.convention.`kotlin-multiplatform`
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.openrndr.shape)
|
||||||
|
implementation(libs.openrndr.draw)
|
||||||
|
implementation(libs.openrndr.application)
|
||||||
|
implementation(project(":orx-shapes"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jvmDemo by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":orx-text-on-contour"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
271
orx-text-on-contour/src/commonMain/kotlin/TextOnContour.kt
Normal file
271
orx-text-on-contour/src/commonMain/kotlin/TextOnContour.kt
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package org.openrndr.extra.textoncontour
|
||||||
|
|
||||||
|
import org.openrndr.draw.*
|
||||||
|
import org.openrndr.extra.shapes.rectify.RectifiedContour
|
||||||
|
import org.openrndr.internal.Driver
|
||||||
|
import org.openrndr.internal.GlyphRectangle
|
||||||
|
import org.openrndr.math.Matrix44
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.math.transforms.buildTransform
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
private fun Vector2.transform(m: Matrix44): Vector2 {
|
||||||
|
return (m * this.xy01).xy
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OnContourImageMapDrawer {
|
||||||
|
var lastPos = Vector2.ZERO
|
||||||
|
|
||||||
|
private val shaderManager: ShadeStyleManager = ShadeStyleManager.fromGenerators(
|
||||||
|
"font-image-map",
|
||||||
|
vsGenerator = Driver.instance.shaderGenerators::fontImageMapVertexShader,
|
||||||
|
fsGenerator = Driver.instance.shaderGenerators::fontImageMapFragmentShader
|
||||||
|
)
|
||||||
|
|
||||||
|
private val maxQuads = 20_000
|
||||||
|
|
||||||
|
private val vertices = VertexBuffer.createDynamic(VertexFormat().apply {
|
||||||
|
textureCoordinate(2)
|
||||||
|
attribute("bounds", VertexElementType.VECTOR4_FLOAT32)
|
||||||
|
position(3)
|
||||||
|
attribute("instance", VertexElementType.FLOAT32)
|
||||||
|
}, 6 * maxQuads)
|
||||||
|
|
||||||
|
private var quadCount = 0
|
||||||
|
|
||||||
|
fun drawTextOnContour(
|
||||||
|
contour: RectifiedContour,
|
||||||
|
context: DrawContext,
|
||||||
|
drawStyle: DrawStyle,
|
||||||
|
text: String,
|
||||||
|
offsetX: Double = 0.0,
|
||||||
|
offsetY: Double = 0.0,
|
||||||
|
tracking: Double = 0.0,
|
||||||
|
scale: Double = 1.0
|
||||||
|
) = drawTextsOnContours(
|
||||||
|
contour,
|
||||||
|
context,
|
||||||
|
drawStyle,
|
||||||
|
listOf(text),
|
||||||
|
listOf(Vector2(offsetX, offsetY)),
|
||||||
|
tracking,
|
||||||
|
scale
|
||||||
|
)
|
||||||
|
|
||||||
|
class SetResult(val cursorT: List<Double>, val glyphRectangles: List<List<GlyphRectangle>>)
|
||||||
|
|
||||||
|
fun drawTextsOnContours(
|
||||||
|
contour: RectifiedContour,
|
||||||
|
context: DrawContext,
|
||||||
|
drawStyle: DrawStyle,
|
||||||
|
texts: List<String>,
|
||||||
|
positions: List<Vector2>,
|
||||||
|
tracking: Double = 0.0,
|
||||||
|
scale: Double = 1.0
|
||||||
|
): SetResult {
|
||||||
|
val fontMap = drawStyle.fontMap as? FontImageMap
|
||||||
|
val cursorTs = mutableListOf<Double>()
|
||||||
|
|
||||||
|
if (fontMap != null) {
|
||||||
|
var instance = 0
|
||||||
|
|
||||||
|
val textAndPositionPairs = texts.zip(positions)
|
||||||
|
for ((text, position) in textAndPositionPairs) {
|
||||||
|
var cursorX = position.x
|
||||||
|
val cursorY = 0.0
|
||||||
|
|
||||||
|
val bw = vertices.shadow.writer()
|
||||||
|
bw.position = vertices.vertexFormat.size * quadCount * 6
|
||||||
|
|
||||||
|
var lastChar: Char? = null
|
||||||
|
text.forEach {
|
||||||
|
val lc = lastChar
|
||||||
|
if (drawStyle.kerning == KernMode.METRIC) {
|
||||||
|
cursorX += if (lc != null) fontMap.kerning(lc, it) else 0.0
|
||||||
|
}
|
||||||
|
val metrics = fontMap.glyphMetrics[it] ?: fontMap.glyphMetrics.getValue(' ')
|
||||||
|
val (dx, _) = insertCharacter(
|
||||||
|
contour,
|
||||||
|
fontMap,
|
||||||
|
bw,
|
||||||
|
it,
|
||||||
|
cursorX,
|
||||||
|
position.y + cursorY,
|
||||||
|
instance,
|
||||||
|
drawStyle.textSetting,
|
||||||
|
scale
|
||||||
|
)
|
||||||
|
cursorX += metrics.advanceWidth + dx + tracking
|
||||||
|
lastChar = it
|
||||||
|
}
|
||||||
|
cursorTs.add(cursorX)
|
||||||
|
instance++
|
||||||
|
}
|
||||||
|
flush(context, drawStyle)
|
||||||
|
}
|
||||||
|
return SetResult(cursorTs, emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
var queuedInstances = 0
|
||||||
|
|
||||||
|
fun flush(context: DrawContext, drawStyle: DrawStyle) {
|
||||||
|
if (quadCount > 0) {
|
||||||
|
vertices.shadow.uploadElements(0, quadCount * 6)
|
||||||
|
val shader = shaderManager.shader(drawStyle.shadeStyle, vertices.vertexFormat)
|
||||||
|
shader.begin()
|
||||||
|
context.applyToShader(shader)
|
||||||
|
|
||||||
|
Driver.instance.setState(drawStyle)
|
||||||
|
drawStyle.applyToShader(shader)
|
||||||
|
(drawStyle.fontMap as FontImageMap).texture.bind(0)
|
||||||
|
Driver.instance.drawVertexBuffer(
|
||||||
|
shader,
|
||||||
|
listOf(vertices),
|
||||||
|
DrawPrimitive.TRIANGLES,
|
||||||
|
0,
|
||||||
|
quadCount * 6,
|
||||||
|
verticesPerPatch = 0
|
||||||
|
)
|
||||||
|
shader.end()
|
||||||
|
quadCount = 0
|
||||||
|
}
|
||||||
|
queuedInstances = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertCharacter(
|
||||||
|
contour: RectifiedContour,
|
||||||
|
fontMap: FontImageMap,
|
||||||
|
bw: BufferWriter,
|
||||||
|
character: Char,
|
||||||
|
cx: Double,
|
||||||
|
cy: Double,
|
||||||
|
instance: Int,
|
||||||
|
textSetting: TextSettingMode,
|
||||||
|
scale: Double
|
||||||
|
): Pair<Double, GlyphRectangle?> {
|
||||||
|
|
||||||
|
val rectangle = fontMap.map[character] ?: fontMap.map[' ']
|
||||||
|
val targetContentScale = RenderTarget.active.contentScale
|
||||||
|
|
||||||
|
val x = if (textSetting == TextSettingMode.PIXEL) round(cx * targetContentScale) / targetContentScale else cx
|
||||||
|
|
||||||
|
val metrics =
|
||||||
|
fontMap.glyphMetrics[character] ?: fontMap.glyphMetrics[' '] ?: error("glyph or space substitute not found")
|
||||||
|
|
||||||
|
val glyphRectangle =
|
||||||
|
if (rectangle != null) {
|
||||||
|
val pad = 2.0f
|
||||||
|
val ushift = 0.0f
|
||||||
|
val xshift = (metrics.xBitmapShift / fontMap.contentScale).toFloat()
|
||||||
|
val yshift = (metrics.yBitmapShift / fontMap.contentScale).toFloat()
|
||||||
|
|
||||||
|
val u0 = (rectangle.x.toFloat() - pad) / fontMap.texture.effectiveWidth + ushift
|
||||||
|
val u1 =
|
||||||
|
(rectangle.x.toFloat() + rectangle.width.toFloat() + pad) / fontMap.texture.effectiveWidth + ushift
|
||||||
|
val v0 = (rectangle.y.toFloat() - pad) / fontMap.texture.effectiveHeight
|
||||||
|
val v1 = v0 + (pad * 2 + rectangle.height.toFloat()) / fontMap.texture.effectiveHeight
|
||||||
|
|
||||||
|
|
||||||
|
val x0 = -pad / fontMap.contentScale.toFloat() + xshift
|
||||||
|
val x1 =
|
||||||
|
(rectangle.width.toFloat() / fontMap.contentScale.toFloat()) + pad / fontMap.contentScale.toFloat() + xshift
|
||||||
|
|
||||||
|
|
||||||
|
val t = (x + (x0 + x1) / 2.0) / (contour.contour.length / scale)
|
||||||
|
|
||||||
|
if (t >= 1.0) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val pose = contour.pose(t)
|
||||||
|
|
||||||
|
val y0 = -pad / fontMap.contentScale.toFloat() + yshift
|
||||||
|
val y1 =
|
||||||
|
rectangle.height.toFloat() / fontMap.contentScale.toFloat() + pad / fontMap.contentScale.toFloat() + yshift
|
||||||
|
|
||||||
|
val transform = buildTransform {
|
||||||
|
multiply(
|
||||||
|
Matrix44(
|
||||||
|
pose.c0r0, pose.c1r0, pose.c2r0, pose.c3r0 / scale,
|
||||||
|
pose.c0r1, pose.c1r1, pose.c2r1, pose.c3r1 / scale,
|
||||||
|
pose.c0r2, pose.c1r2, pose.c2r2, pose.c3r2 / scale,
|
||||||
|
pose.c0r3, pose.c1r3, pose.c2r3, pose.c3r3,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
multiply(
|
||||||
|
Matrix44(
|
||||||
|
-1.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, -1.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0, 1.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
translate(-(x0 + x1) / 2.0, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val p00 = Vector2(x0.toDouble(), y0.toDouble())
|
||||||
|
val p01 = Vector2(x0.toDouble(), y1.toDouble())
|
||||||
|
val p10 = Vector2(x1.toDouble(), y0.toDouble())
|
||||||
|
val p11 = Vector2(x1.toDouble(), y1.toDouble())
|
||||||
|
|
||||||
|
val t00 = p00.transform(transform)
|
||||||
|
val t01 = p01.transform(transform)
|
||||||
|
val t10 = p10.transform(transform)
|
||||||
|
val t11 = p11.transform(transform)
|
||||||
|
|
||||||
|
lastPos = t00
|
||||||
|
|
||||||
|
val s0 = 0.0f
|
||||||
|
val t0 = 0.0f
|
||||||
|
val s1 = 1.0f
|
||||||
|
val t1 = 1.0f
|
||||||
|
|
||||||
|
val w = (x1 - x0)
|
||||||
|
val h = (y1 - y0)
|
||||||
|
val z = quadCount.toFloat()
|
||||||
|
|
||||||
|
val floatInstance = instance.toFloat()
|
||||||
|
|
||||||
|
if (quadCount < maxQuads) {
|
||||||
|
bw.apply {
|
||||||
|
write(u0, v0); write(s0, t0, w, h); write(t00.x.toFloat(), t00.y.toFloat(), z); write(
|
||||||
|
floatInstance
|
||||||
|
)
|
||||||
|
write(u1, v0); write(s1, t0, w, h); write(t10.x.toFloat(), t10.y.toFloat(), z); write(
|
||||||
|
floatInstance
|
||||||
|
)
|
||||||
|
write(u1, v1); write(s1, t1, w, h); write(t11.x.toFloat(), t11.y.toFloat(), z); write(
|
||||||
|
floatInstance
|
||||||
|
)
|
||||||
|
write(u0, v0); write(s0, t0, w, h); write(t00.x.toFloat(), t00.y.toFloat(), z); write(
|
||||||
|
floatInstance
|
||||||
|
)
|
||||||
|
write(u0, v1); write(s0, t1, w, h); write(t01.x.toFloat(), t01.y.toFloat(), z); write(
|
||||||
|
floatInstance
|
||||||
|
)
|
||||||
|
write(u1, v1); write(s1, t1, w, h); write(t11.x.toFloat(), t11.y.toFloat(), z); write(
|
||||||
|
floatInstance
|
||||||
|
)
|
||||||
|
}
|
||||||
|
quadCount++
|
||||||
|
}
|
||||||
|
GlyphRectangle(character, x0.toDouble(), y0.toDouble(), (x1 - x0).toDouble(), (y1 - y0).toDouble())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return Pair(x - cx, glyphRectangle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val onContourImageMapDrawer by lazy { OnContourImageMapDrawer() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a given text along the given contour
|
||||||
|
*
|
||||||
|
* @param text The string to be rendered along the contour.
|
||||||
|
* @param contour The rectified contour along which the text will be rendered.
|
||||||
|
* @param offsetX Optional horizontal offset to shift the starting point of the text along the contour. Default value is 0.0.
|
||||||
|
*/
|
||||||
|
fun Drawer.textOnContour(text: String, contour: RectifiedContour, offsetX: Double = 0.0) {
|
||||||
|
onContourImageMapDrawer.drawTextOnContour(contour, context, drawStyle, text, offsetX)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.draw.loadFont
|
||||||
|
import org.openrndr.extra.shapes.rectify.rectified
|
||||||
|
import org.openrndr.extra.textoncontour.textOnContour
|
||||||
|
import org.openrndr.shape.Circle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demo Functionality includes:
|
||||||
|
* - Loading and applying a specific font (`IBMPlexMono-Regular`) with a size of 32.0.
|
||||||
|
* - Creating a circular contour at the center of the screen with a radius of 200.0.
|
||||||
|
* - Rendering text along the rectified circle's contour.
|
||||||
|
* - Offsetting text positions, enabling repeated text rendering along the same contour.
|
||||||
|
*/
|
||||||
|
fun main() = application {
|
||||||
|
configure {
|
||||||
|
width = 720
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
extend {
|
||||||
|
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 32.0)
|
||||||
|
val c = Circle(drawer.bounds.center, 200.0).contour.rectified()
|
||||||
|
drawer.textOnContour("The wheels of the bus go round and round.", c)
|
||||||
|
drawer.textOnContour("The wheels of the bus go round and round.", c, c.contour.length / 2.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,20 +23,24 @@ import org.openrndr.shape.Rectangle
|
|||||||
* - `drawer` enables isolated operations for drawing elements.
|
* - `drawer` enables isolated operations for drawing elements.
|
||||||
* - `writer` facilitates text rendering with alignment and spacing adjustments.
|
* - `writer` facilitates text rendering with alignment and spacing adjustments.
|
||||||
*/
|
*/
|
||||||
fun main() = application {
|
fun main() {
|
||||||
program {
|
|
||||||
extend {
|
application {
|
||||||
val r = Rectangle.fromCenter(drawer.bounds.center, 200.0, 200.0)
|
|
||||||
drawer.isolated {
|
program {
|
||||||
drawer.fill = null
|
extend {
|
||||||
drawer.stroke = ColorRGBa.WHITE
|
val r = Rectangle.fromCenter(drawer.bounds.center, 200.0, 200.0)
|
||||||
drawer.rectangle(r)
|
drawer.isolated {
|
||||||
}
|
drawer.fill = null
|
||||||
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
|
drawer.stroke = ColorRGBa.WHITE
|
||||||
writer {
|
drawer.rectangle(r)
|
||||||
box = r.offsetEdges(-10.0)
|
}
|
||||||
newLine()
|
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
|
||||||
text("hello world")
|
writer {
|
||||||
|
box = r.offsetEdges(-10.0)
|
||||||
|
newLine()
|
||||||
|
text("hello world")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ include(
|
|||||||
"orx-depth-camera",
|
"orx-depth-camera",
|
||||||
"orx-jvm:orx-depth-camera-calibrator",
|
"orx-jvm:orx-depth-camera-calibrator",
|
||||||
"orx-view-box",
|
"orx-view-box",
|
||||||
|
"orx-text-on-contour",
|
||||||
"orx-text-writer",
|
"orx-text-writer",
|
||||||
"orx-turtle"
|
"orx-turtle"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user