Back to list

tweened-staggered

A tweaked version of the official svelte

tweened
method, with two updates: a: it takes an
iDelay
configuration option, which staggers the transition of individual items in a tweened array, and b: it uses d3.js's interpolation function, which handles interpolation of colors.

tweened-staggered.js
See on Github
import { writable } from "svelte/store";
import { assign, loop, now } from "svelte/internal";
import { linear } from "svelte/easing";
import { interpolate } from "d3-interpolate";
import { scaleLinear } from "d3-scale";

function get_interpolator(a, b, iDelay = 0, length = 0, delay = 0) {
  if (a === b || a !== a) return () => a;
  const type = typeof a;
  if (type !== typeof b || Array.isArray(a) !== Array.isArray(b)) {
    throw new Error("Cannot interpolate values of different type");
  }
  if (Array.isArray(a)) {
    const arr = b.map((bi, i) => {
      return get_interpolator(a[i], bi, iDelay, b.length, i * iDelay);
    });
    return (t) => arr.map((fn) => fn(t));
  }
  if (type === "object") {
    if (!a || !b) throw new Error("Object cannot be null");
    const keys = Object.keys(b);
    const interpolators = {};
    keys.forEach((key) => {
      interpolators[key] = get_interpolator(
        a[key],
        b[key],
        iDelay,
        length,
        delay
      );
    });
    return (t) => {
      const result = {};
      keys.forEach((key) => {
        result[key] = interpolators[key](t);
      });
      return result;
    };
  }
  if (["string", "number"].includes(type)) {
    const allDelays = iDelay * length;
    const iDuration = 1 - allDelays;
    const scale = scaleLinear()
      .domain([delay, delay + iDuration])
      .range([0, 1])
      .clamp(true);
    const interpolationFunction = interpolate(a, b);
    // console.log()
    return (t) => {
      return interpolationFunction(scale(t));

      // a + Math.max(0, t * maxT - delay) * delta;
    };
  }
  throw new Error(`Cannot interpolate ${type} values`);
}

export function tweened(value, defaults = {}) {
  const store = writable(value);
  let task;
  let target_value = value;
  function set(new_value, opts) {
    if (value == null) {
      store.set((value = new_value));
      return Promise.resolve();
    }
    target_value = new_value;
    let previous_task = task;
    let started = false;
    let {
      delay = 0,
      duration = 400,
      iDelay = 10,
      easing = linear,
      interpolate = get_interpolator,
    } = assign(assign({}, defaults), opts);
    if (duration === 0) {
      if (previous_task) {
        previous_task.abort();
        previous_task = null;
      }
      store.set((value = target_value));
      return Promise.resolve();
    }
    const start = now() + delay;
    let fn;
    task = loop((now) => {
      if (now < start) return true;
      if (!started) {
        fn = interpolate(value, new_value, iDelay / duration);
        if (typeof duration === "function")
          duration = duration(value, new_value);
        started = true;
      }
      if (previous_task) {
        previous_task.abort();
        previous_task = null;
      }
      const elapsed = now - start;
      if (elapsed > duration) {
        store.set((value = new_value));
        return false;
      }

      store.set((value = fn(easing(elapsed / duration))));
      return true;
    });
    return task.promise;
  }
  return {
    set,
    update: (fn, opts) => set(fn(target_value, value), opts),
    subscribe: store.subscribe,
  };
}

Usage example:

TweenedStaggeredWrapper.svelte
See on Github
<script>
  import { scaleLinear } from "d3-scale"
  import { interpolateHcl } from "d3-interpolate"
  import SimplexNoise from "simplex-noise"

  import { tweened } from "./tweened-staggered"
  import move from "./move"

  const simplex = new SimplexNoise(0)

  const height = 8
  const width = 10

  const colorScale = scaleLinear()
    .domain([0, 1])
    .range(["#C3B6DF", "#0B2830"])
    .interpolate(interpolateHcl)

  let iteration = 3
  const createData = () => {
    iteration = iteration + 1
    return new Array(150).fill(0).map((_, i) => {
      const x = (i * 10) / 150
      const y = Math.random() * height
      const r = Math.max(0, simplex.noise2D(x, y))
      return {
        x: x - r,
        y: y - r,
        r,
        color: colorScale(Math.random()),
      }
    })
  }

  let dots = tweened(createData(), {
    duration: 2000,
    iDelay: 6,
  })
</script>

<svg viewBox="{[-1, -1, width + 2, height + 2].join(' ')}">
  {#each $dots as { x, y, r, color }}
    <rect
      style="{move(x, y)}"
      width="{r * 2}"
      height="{r * 2}"
      fill="{color}"></rect>
  {/each}
</svg>

<div class="note">Click to update</div>
<svelte:window
  on:click="{() => dots.set(createData())}"
  on:touchend="{() => dots.set(createData())}" />

<style>
  svg {
    width: 100%;
  }
  rect {
    mix-blend-mode: multiply;
  }
  .note {
    position: absolute;
    top: 0;
    font-style: italic;
    color: var(--text-light);
  }
</style>
Click to update