Skip to content

Commit 3f729af

Browse files
authored
DPI scaling for JS/HTML (#180)
1 parent c1e3289 commit 3f729af

File tree

5 files changed

+29
-11
lines changed

5 files changed

+29
-11
lines changed

element-view/src/commonMain/kotlin/AdapterState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ internal data class AdapterState<V : Any>(
77
val root: RootElement,
88
val width: Int,
99
val height: Int,
10+
val scale: Float = 1f,
1011
)

element-view/src/jsMain/kotlin/ElementViewAdapter.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ public class ElementViewAdapter<T>(
3535
* The view changed size, so we should re-render the bitmap. When this happens, the RootElement
3636
* will be reset, so the render will have a clean slate.
3737
*/
38-
internal fun onSizeChanged(width: Int, height: Int) {
39-
state.value = state.value.copy(root = RootElement(), width = width, height = height)
38+
internal fun onSizeChanged(width: Int, height: Int, scale: Double) {
39+
state.value = state.value.copy(root = RootElement(), width = width, height = height, scale = scale.toFloat())
4040
}
4141

4242
internal fun onClick(x: Float, y: Float) {
@@ -78,12 +78,13 @@ public class ElementViewAdapter<T>(
7878
state.collectLatest { state ->
7979
if (state.view == null) return@collectLatest
8080
if (state.width == 0 || state.height == 0) return@collectLatest
81-
val canvas = HtmlKanvas(state.view)
8281
dataSource.collect { data ->
8382
updater.update(state.root, state.width.toFloat(), state.height.toFloat(), data)
8483
window.awaitAnimationFrame()
84+
val canvas = HtmlKanvas(state.view, state.scale)
8585
canvas.context.clearRect(0.0, 0.0, state.width.toDouble(), state.height.toDouble())
8686
state.root.draw(canvas)
87+
canvas.context.resetTransform()
8788
}
8889
}
8990
}

element-view/src/jsMain/kotlin/HTMLCanvasElement.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.juul.krayon.element.view
22

3+
import kotlinx.browser.window
34
import org.w3c.dom.Element
45
import org.w3c.dom.HTMLCanvasElement
6+
import kotlin.math.roundToInt
57

68
private external class ResizeObserver(
79
callback: (entries: Array<ResizeObserverEntry>, observer: ResizeObserver) -> Unit,
@@ -15,14 +17,21 @@ private external interface ResizeObserverEntry
1517
private var adapters = mutableMapOf<HTMLCanvasElement, ElementViewAdapter<*>>()
1618
private var observers = mutableMapOf<HTMLCanvasElement, ResizeObserver>()
1719

20+
/**
21+
* Attach an [adapter] to `this` element. When [applyDisplayScale] is `true` (the default), then
22+
* the element will properly handle scaled high-DPI devices. This can be set to `false` to prevent
23+
* an infinite resize loop when the element's size is defined intrinsically by the canvas size.
24+
*/
1825
public fun HTMLCanvasElement.attachAdapter(
1926
adapter: ElementViewAdapter<*>,
27+
applyDisplayScale: Boolean = true,
2028
) {
2129
detachAdapter()
2230
val observer = ResizeObserver { _, _ ->
23-
width = offsetWidth
24-
height = offsetHeight
25-
adapter.onSizeChanged(width, height)
31+
val scale = if (applyDisplayScale) window.devicePixelRatio else 1.0
32+
width = (offsetWidth * scale).roundToInt()
33+
height = (offsetHeight * scale).roundToInt()
34+
adapter.onSizeChanged(offsetWidth, offsetHeight, scale)
2635
}
2736
observers[this] = observer
2837
observer.observe(this)

kanvas/src/jsMain/kotlin/HtmlKanvas.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,23 @@ private val WHITESPACE = Regex("\\s")
2323

2424
public class HtmlKanvas(
2525
element: HTMLCanvasElement,
26+
private val scalingFactor: Float = 1f,
2627
) : Kanvas, IsPointInPath {
2728

2829
/** The raw HTMLCanvas's 2d rendering context. */
2930
public val context: CanvasRenderingContext2D = element.getContext("2d") as CanvasRenderingContext2D
3031

3132
override val width: Float
32-
get() = context.canvas.width.toFloat()
33+
get() = context.canvas.width / scalingFactor
3334

3435
override val height: Float
35-
get() = context.canvas.height.toFloat()
36+
get() = context.canvas.height / scalingFactor
37+
38+
init {
39+
if (scalingFactor != 1f) {
40+
pushTransform(Transform.Scale(scalingFactor, scalingFactor))
41+
}
42+
}
3643

3744
override fun drawArc(
3845
left: Float,

sample/src/jsMain/resources/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
<body>
1010
<h1>Krayon Sample</h1>
1111
<h2>Line Chart</h2>
12-
<canvas id="line-canvas" width="854" height="480"></canvas>
12+
<canvas id="line-canvas" style="height: 480px; width: 854px"></canvas>
1313
<h2>Pie Chart</h2>
1414
<table>
1515
<tr>
1616
<td>
17-
<canvas id="pie-canvas" width="480" height="480"></canvas>
17+
<canvas id="pie-canvas" style="height: 480px; width: 480px"></canvas>
1818
</td>
1919
<td>
2020
<form>
@@ -37,7 +37,7 @@ <h2>Pie Chart</h2>
3737
</tr>
3838
</table>
3939
<h2>Interaction</h2>
40-
<canvas id="interaction-canvas" width="854" height="480"></canvas>
40+
<canvas id="interaction-canvas" style="height: 480px; width: 854px"></canvas>
4141
</body>
4242
<script src="sample.js"></script>
4343
</html>

0 commit comments

Comments
 (0)