Forces - Ripped Curtains

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() }
pierre avatar

Sketch created by

pierre

Simulates a ripped curtain using a link network (ForceLink), collisions (ForceCollision), and gravitation (ForceY), when 2 nodes are too far apart, they may tear themselves, this is "simulated" by setting the force of the link between the nodes to zero.

comments

Gaetan Zoritchak gaetan 4 years ago
Very hypnotic!!

Gaetan Zoritchak