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.atan
import kotlin.math.abs
import kotlin.math.sqrt
fun main() {
// FEEL FREE TO EXPERIMENT BY CHANGING THESE VALUES ************************
val frictionRate = 2.pct // friction rate
val defaultIntensity = 70.pct // default fixed intensity (no intensity decay)
val ripDistance = 20 // if 2 nodes are more than ripDistance away they may break
val ripRisk = 10.pct // the "risk" of ripping 2 nodes
val linkForceIterations = 20 // the more iterations = the more "rigid" the curtains
val collisionForceStrength = 15.pct // less strength = wind pass through the "curtains"
val singleCurtainWidth = 32 // width of a curtain
val curtainsNumber = 1 // # of curtains drawn
val curtainsLength = 30 // length of a curtain
val stitchSpace = 12.0 // size between nodes
val windRadius = 40.0 // size of the "wind" circles for collision
val windSpeed = 6 // speed of the "wind"
val windAngle = 5.deg // angle of the "wind"
val gravityValue = 0.05.pct // higher values = heavier "curtains"
val showWind = false // display the "wind" particles
// *************************************************************************
val curtainsWidth = curtainsNumber * singleCurtainWidth
var totalStitches = curtainsWidth * curtainsLength
val vizSize = 800.0
val movement = Vector(windSpeed * windAngle.cos, windSpeed * windAngle.sin)
val randPos = RandomDistribution.uniform(150.0, 600.0)
val randBreak = RandomDistribution.uniform()
// our domain object, storing a default starting position and if it is "fixed" or not
data class Stitch(val position:Point, val fixed:Boolean = false)
// creating the objects, only the top line is "fixed"
val stitches = (0 until totalStitches).map {
val col = it % curtainsWidth
val row = it / curtainsWidth
Stitch(
point(80.0 + (col * stitchSpace), 50.0 + (row * stitchSpace) - col),
row == 0 && (col % 5 == 0 || col % 5 == 1)
)
}.toMutableList()
// adding 3 more nodes to the simulation, these nodes are used to simulate the wind
// these nodes will have a very different behavior
stitches += (Stitch(point(0, 350), false))
stitches += (Stitch(point(-450, 600), false))
stitches += (Stitch(point(-200, 400), false))
lateinit var viz:Viz
// keeping a reference to our ForceLink, this will allow easy access to the Links
lateinit var forceLinks:ForceLink<Stitch>
val simulation = forceSimulation<Stitch> {
friction = frictionRate
intensity = defaultIntensity
intensityDecay = 0.pct
// if the Stitch is "fixed", we use its current position has a fixed one (node won't move)
initForceNode = {
position = domain.position
fixedX = if (domain.fixed) domain.position.x else null
fixedY = if (domain.fixed) domain.position.y else null
}
// the force that creates links between the nodes
// each node is linked to the next one on the right and next one below
forceLinks = forceLink {
linkGet = {
val links = mutableListOf<Link<Stitch>>()
val currentCol = index % singleCurtainWidth
val wholeCol = index % curtainsWidth
val row = index / curtainsWidth
// only "link" the stitches, not the 3 nodes used for simulating the wind
if (index < totalStitches) {
// check if we had the right-next node
if (currentCol != (singleCurtainWidth - 1) && wholeCol < curtainsWidth - 1) {
links += Link(this, nodes[index + 1], stitchSpace)
}
// check if we had the bottom-next node
if (row < curtainsLength - 1) {
links += Link(this, nodes[index + curtainsWidth], stitchSpace)
}
}
// return the list of links
links
}
iterations = linkForceIterations
}
// create a collision force, only the 3 "wind" nodes have a radius
forceCollision {
radiusGet = { if (index < totalStitches) .0 else windRadius }
strength = collisionForceStrength
iterations = 1
}
// create a "gravity" force, only applies to the "stiches" not the "wind"
forceY {
yGet = { vizSize + 100 }
strengthGet = { if (index < totalStitches) gravityValue else 0.pct }
}
domainObjects = stitches
}
// storing the visuals of links and wind particles
val links = mutableListOf<LineNode?>()
val winds = mutableListOf<CircleNode>()
viz = viz {
size = size(vizSize, vizSize)
// creating the visuals for the wind particles, see below to show them on screen
(totalStitches .. totalStitches+2).forEach {
winds += circle {
radius = windRadius
fill = Colors.Web.lightblue.withAlpha(60.pct)
x = -600.0
y = -600.0
}
}
// creating the visuals for the links, store them in the "links" list
forceLinks.links.forEach {
links += line {
strokeColor = Colors.Web.black
strokeWidth = 1.5
}
}
animation {
// force move the "wind" particles on each frame
(totalStitches .. totalStitches+2).forEach {
val windNode = simulation.nodes[it]
windNode.position += movement
if (windNode.x > vizSize) {
windNode.x = -50.0
windNode.y = randPos()
}
if (showWind) {
winds[it - totalStitches].apply {
x = windNode.x
y = windNode.y
}
}
}
// show the new coordinates of each links to visualize the wind effect
forceLinks.links.forEachIndexed { index, link ->
if (links[index] != null) {
links[index]!!.apply {
x1 = link.source.x
x2 = link.target.x
y1 = link.source.y
y2 = link.target.y
val x = x2 - x1
val y = y2 - y1
val distance = sqrt((x*x)+(y*y))
if (index > 80 && distance > ripDistance && randBreak() < ripRisk.value) {
link.strength = .0
links[index] = null
strokeColor = Colors.Web.black.withAlpha(0.pct)
}
}
}
}
}
}
viz.bindRendererOnNewCanvas()
}