Skip to content

Commit

Permalink
Curve on livemap (#1021)
Browse files Browse the repository at this point in the history
* Curve on livemap.

* Update test notebooks.

* Cancel irrelevant changes (optimize imports).

* Move the curve drawing curve by control points to Context2d.

* Fix after rebase.

* Rename the curve drawing function.
  • Loading branch information
OLarionova-HORIS committed Feb 20, 2024
1 parent 537806c commit b63f2b5
Show file tree
Hide file tree
Showing 9 changed files with 818 additions and 270 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,41 @@ interface Context2d {
fun setTransform(m11: Double, m12: Double, m21: Double, m22: Double, dx: Double, dy: Double)
fun setLineDash(lineDash: DoubleArray)
fun measureText(str: String): Double

// https://github.com/d3/d3/blob/9364923ee2b35ec2eb80ffc4bdac12a7930097fc/src/svg/line.js#L236
fun drawBezierCurve(points: List<Vec<*>>) {
fun lineDot4(a: List<Double>, b: List<Double>): Double {
// returns the dot product of the given four-element vectors
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
}
// Matrix to transform basis (b-spline) control points to bezier control points.
val lineBasisBezier1 = listOf(0.0, 2.0 / 3.0, 1.0 / 3.0, 0.0)
val lineBasisBezier2 = listOf(0.0, 1.0 / 3.0, 2.0 / 3.0, 0.0)
val lineBasisBezier3 = listOf(0.0, 1.0 / 6.0, 2.0 / 3.0, 1.0 / 6.0)

val px = arrayListOf(points[0].x, points[0].x, points[0].x, points[1].x)
val py = arrayListOf(points[0].y, points[0].y, points[0].y, points[1].y)

moveTo(points[0].x, points[0].y)
lineTo(
lineDot4(lineBasisBezier3, px),
lineDot4(lineBasisBezier3, py)
)
for (i in 2..points.size) {
val curPoint = if (i < points.size) points[i] else points.last()
px.removeFirst(); px.add(curPoint.x)
py.removeFirst(); py.add(curPoint.y)
bezierCurveTo(
lineDot4(lineBasisBezier1, px),
lineDot4(lineBasisBezier1, py),
lineDot4(lineBasisBezier2, px),
lineDot4(lineBasisBezier2, py),
lineDot4(lineBasisBezier3, px),
lineDot4(lineBasisBezier3, py)
)
}
lineTo(points.last().x, points.last().y)
}
}

fun Context2d.drawImage(snapshot: Snapshot, p: Vec<*>) = drawImage(snapshot, p.x, p.y)
Expand Down
550 changes: 500 additions & 50 deletions docs/dev/notebooks/geom_curve.ipynb

Large diffs are not rendered by default.

135 changes: 68 additions & 67 deletions docs/dev/notebooks/geom_curve_to_annotate.ipynb

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.jetbrains.letsPlot.livemap.chart.GrowingPathEffect.GrowingPathEffectC
import org.jetbrains.letsPlot.livemap.chart.GrowingPathEffect.GrowingPathRenderer
import org.jetbrains.letsPlot.livemap.chart.IndexComponent
import org.jetbrains.letsPlot.livemap.chart.LocatorComponent
import org.jetbrains.letsPlot.livemap.chart.path.CurveRenderer
import org.jetbrains.letsPlot.livemap.chart.path.PathLocator
import org.jetbrains.letsPlot.livemap.chart.path.PathRenderer
import org.jetbrains.letsPlot.livemap.chart.path.PathRenderer.ArrowSpec
Expand Down Expand Up @@ -91,6 +92,7 @@ class PathEntityBuilder(
var animation: Int = 0
var speed: Double = 0.0
var flow: Double = 0.0
var isCurve: Boolean = false

// Arrow specification
var arrowAngle: Double? = null
Expand Down Expand Up @@ -151,7 +153,7 @@ class PathEntityBuilder(
+IndexComponent(layerIndex!!, index!!)
}
+RenderableComponent().apply {
renderer = PathRenderer()
renderer = if (isCurve) CurveRenderer() else PathRenderer()
}
+ChartElementComponent().apply {
sizeScalingRange = this@PathEntityBuilder.sizeScalingRange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import org.jetbrains.letsPlot.livemap.core.ecs.EcsEntity
import org.jetbrains.letsPlot.livemap.geometry.WorldGeometryComponent
import org.jetbrains.letsPlot.livemap.mapengine.RenderHelper
import org.jetbrains.letsPlot.livemap.mapengine.Renderer
import org.jetbrains.letsPlot.livemap.mapengine.lineTo
import org.jetbrains.letsPlot.livemap.mapengine.moveTo
import kotlin.math.*

class PathRenderer : Renderer {
open class PathRenderer : Renderer {
override fun render(entity: EcsEntity, ctx: Context2d, renderHelper: RenderHelper) {
val geometry = entity.get<WorldGeometryComponent>().geometry.multiLineString
val chartElement = entity.get<ChartElementComponent>()
Expand All @@ -37,10 +39,7 @@ class PathRenderer : Renderer {
val adjustedGeometry = padLineString(lineString, startPadding, endPadding)

ctx.beginPath()

adjustedGeometry[0].let { ctx.moveTo(it.x, it.y) }
adjustedGeometry.drop(1).forEach { ctx.lineTo(it.x, it.y) }

drawPath(adjustedGeometry, ctx)
ctx.restore()

ctx.setStrokeStyle(color)
Expand All @@ -58,6 +57,11 @@ class PathRenderer : Renderer {
}
}

open fun drawPath(points: List<WorldPoint>, ctx: Context2d) {
points[0].let(ctx::moveTo)
points.drop(1).forEach(ctx::lineTo)
}

class ArrowSpec private constructor(
val angle: Double,
val length: Double,
Expand Down Expand Up @@ -222,4 +226,15 @@ class PathRenderer : Renderer {
return lineString.subList(0, lineString.size - index) + adjustedEndPoint
}
}
}

class CurveRenderer : PathRenderer() {
override fun drawPath(points: List<WorldPoint>, ctx: Context2d) {
if (points.size < 3) {
// linear
super.drawPath(points, ctx)
} else {
ctx.drawBezierCurve(points)
}
}
}
Loading

0 comments on commit b63f2b5

Please sign in to comment.