Forces - balls + events

import io.data2viz.color.* import io.data2viz.geom.* import io.data2viz.math.* import io.data2viz.timer.* import io.data2viz.force.* import io.data2viz.viz.* import io.data2viz.random.* import kotlin.math.sqrt import kotlin.math.max fun main() { val vizSize = 600.0 val randPos = RandomDistribution.uniform(.0, vizSize) val randRadius = RandomDistribution.uniform(6.0, 35.0) val randAngle = RandomDistribution.uniform(.0, 360.0) data class ColoredParticle(var position:Point, val radius:Double, val color:Color) val items = (0..100).map { ColoredParticle( point(randPos(), randPos()), randRadius(), Colors.hsl(randAngle().deg, 100.pct, 50.pct) ) } var drag = false lateinit var viz:Viz var clickedNode:ForceNode<ColoredParticle>? = null var clickedNodePos = point(0,0) val viewCenter = point(vizSize / 2, vizSize / 2) val simulation = forceSimulation<ColoredParticle> { // initForceNode use the pre-populated domain object to set starting position initForceNode = { // initForceNode: ForceNode<D>.() -> Unit position = domain.position } // radiusGet use the domain object to retrieve properties (ie. the radius of the particle) forceCollision { // radiusGet: ForceNode<D>.() -> Double radiusGet = { domain.radius } iterations = 6 } // target position and force strength are the same for all nodes, use constants forcePoint { // pointGet: ForceNode<D>.() -> Point pointGet = { viewCenter } // strengthGet: ForceNode<D>.() -> Percent strengthGet = { 1.pct } } domainObjects = items intensityMin = 1.pct intensityDecay = 0.pct on(SimulationEvent.END, "End of simulation", { viz.stopAnimations() }) } val particles = mutableListOf<CircleNode>() viz = viz { size = size(vizSize, vizSize) text { textContent = "Click and drag!" x = 10.0 y = 20.0 } simulation.nodes.forEach { forceNode -> particles += circle { // use the domain object contained in the node to set properties radius = forceNode.domain.radius fill = forceNode.domain.color } } animation { if (drag) { clickedNode!!.position = clickedNodePos } simulation.nodes.forEach { forceNode -> particles[forceNode.index].apply { x = forceNode.x y = forceNode.y radius = forceNode.domain.radius fill = forceNode.domain.color } } } on(KMouseDown) { event -> if (!drag) { // find the clicked node clickedNode = simulation.nodes.firstOrNull { node -> val diffX = event.pos.x - node.x val diffY = event.pos.y - node.y sqrt((diffX * diffX) + (diffY * diffY)) < items[node.index].radius } clickedNodePos = event.pos drag = clickedNode != null } } on(KMouseUp) { drag = false } on(KMouseMove) { event -> if (drag) { clickedNodePos = event.pos clickedNode!!.position = clickedNodePos } } } viz.bindRendererOnNewCanvas() }
pierre avatar

Sketch created by

pierre

Simple simulation with just 2 forces, a ForceCollision to avoid overlapping of the bubbles, and a forcePoint to push them to the center. You can click and drag a bubble to move it. Note that the "intensitydecay" value of the simulation is set to zero, to avoid ending in a stable state where the simulation stops.

comments

Pierre Mariac
Gaetan Zoritchak