Back to list

Force

Use d3 force to move particles around. You can change the forces at any point, more info in the d3 docs.

Force.svelte
See on Github
<script>
  import { forceSimulation } from "d3-force"

  // utility function for translating elements
  const move = (x, y) => `transform: translate(${x}px, ${y}px`

  // an array of our particles
  export let dots = []
  // an array of [name, force] pairs
  export let forces = []

  let usedForceNames = []
  let renderedDots = []

  let width = 1200
  $: height = width

  $: simulation = forceSimulation()
    .nodes(dots)
    .on("tick", () => {
      // update the renderedDots reference to trigger an update
      renderedDots = [...dots]
    })

  $: {
    // re-initialize forces when they change
    forces.forEach(([name, force]) => {
      simulation.force(name, force)
    })

    // remove old forces
    const newForceNames = forces.map(([name]) => name)
    let oldForceNames = usedForceNames.filter(
      force => !newForceNames.includes(force),
    )
    oldForceNames.forEach(name => {
      simulation.force(name, null)
    })
    usedForceNames = newForceNames

    // kick our simulation into high gear
    simulation.alpha(1)
    simulation.restart()
  }
</script>

<figure class="c" bind:clientWidth="{width}">
  <svg {width} {height}>
    {#each renderedDots as { x, y }, i}
      <circle style="{move(x, y)}" r="{6}"></circle>
    {/each}
  </svg>
</figure>

<style>
  figure {
    margin: 0;
  }
  svg {
    overflow: visible;
  }
  circle {
    fill: #0b2830;
  }
</style>

Usage example:

ForceWrapper.svelte
See on Github
<script>
  import { forceX, forceY, forceCollide, forceRadial } from "d3-force"

  import Force from "./Force.svelte"

  let element
  let centerPosition = [200, 200]
  let useForceCollide = true
  let useForceRadial = true
  $: activeForceX = forceX().x(centerPosition[0])
  $: activeForceY = forceY().y(centerPosition[1])
  $: activeForceCollide = forceCollide()
    .radius(10)
    .iterations(3)
  $: activeForceRadial = forceRadial()
    .radius(150)
    .x(centerPosition[0])
    .y(centerPosition[1])
  $: forces = [
    ["x", activeForceX],
    ["y", activeForceY],
    useForceCollide && ["collide", activeForceCollide],
    useForceRadial && ["radial", activeForceRadial],
  ].filter(d => d)

  const numberOfDots = 100
  let dots = new Array(numberOfDots).fill(0).map(_ => ({}))

  const onClick = e => {
    if (!element) return
    const bounds = element.getBoundingClientRect()
    const x = e.clientX - bounds.left
    const y = e.clientY - bounds.top
    centerPosition = [x, y]
  }
</script>

<div class="controls">
  <label>
    <input type="checkbox" bind:checked="{useForceCollide}" />
    Collide?
  </label>
  <label>
    <input type="checkbox" bind:checked="{useForceRadial}" />
    Radial?
  </label>
</div>
<div class="note">Click around to update</div>

<div on:click="{onClick}" bind:this="{element}">
  <Force {forces} {dots} />
</div>

<style>
  .controls {
    display: flex;
    align-items: center;
    position: absolute;
    top: 0;
    right: 0;
    font-style: italic;
    color: var(--text-light);
  }
  label + label {
    margin-left: 1em;
  }
  .note {
    position: absolute;
    top: 0;
    font-style: italic;
    color: var(--text-light);
  }
</style>
Click around to update