Menu

Brush.html.svelte component

Adds a brush component to create a range between 0 and 1. Bind to the min and max props to use them in other components. See the brushable example for use.

Param Default Required Description
min (number | null) None
yes
The brush's min value. Useful to bind to.
max (number | null) None
yes
The brush's max value. Useful to bind to.

Used in these examples:

SSR Examples:

<!--
  @component
  Adds a brush component to create a range between 0 and 1. Bind to the `min` and `max` props to use them in other components. See the [brushable example](https://layercake.graphics/example/Brush) for use.
 -->
<script>
  import { clamp } from 'yootils';

  /**
   * @typedef {Object} Props
   * @property {number|null} min - The brush's min value. Useful to bind to.
   * @property {number|null} max - The brush's max value. Useful to bind to.
   */

  /** @type {Props} */
  let { min = $bindable(), max = $bindable() } = $props();

  let brush = $state();

  const p = x => {
    const { left, right } = brush.getBoundingClientRect();
    return clamp((x - left) / (right - left), 0, 1);
  };

  const handler = fn => {
    return e => {
      e.stopPropagation();
      e.preventDefault(); // Prevent default drag behavior
      if (e.type === 'touchstart') {
        if (e.touches.length !== 1) return;
        e = e.touches[0];
      }

      const id = e.identifier;
      const start = { min, max, p: p(e.clientX) };

      const handle_move = e => {
        e.preventDefault(); // Prevent default drag behavior during move
        if (e.type === 'touchmove') {
          if (e.changedTouches.length !== 1) return;
          e = e.changedTouches[0];
          if (e.identifier !== id) return;
        }

        fn(start, p(e.clientX));
      };

      const handle_end = e => {
        if (e.type === 'touchend') {
          if (e.changedTouches.length !== 1) return;
          if (e.changedTouches[0].identifier !== id) return;
        } else if (e.target === brush) {
          clear();
        }

        window.removeEventListener('mousemove', handle_move);
        window.removeEventListener('mouseup', handle_end);

        window.removeEventListener('touchmove', handle_move);
        window.removeEventListener('touchend', handle_end);
      };

      window.addEventListener('mousemove', handle_move);
      window.addEventListener('mouseup', handle_end);

      window.addEventListener('touchmove', handle_move);
      window.addEventListener('touchend', handle_end);
    };
  };

  const clear = () => {
    min = null;
    max = null;
  };

  const reset = handler((start, p) => {
    min = clamp(Math.min(start.p, p), 0, 1);
    max = clamp(Math.max(start.p, p), 0, 1);
  });

  const move = handler((start, p) => {
    const d = clamp(p - start.p, -start.min, 1 - start.max);
    min = start.min + d;
    max = start.max + d;
  });

  const adjust_min = handler((start, p) => {
    min = p > start.max ? start.max : p;
    max = p > start.max ? p : start.max;
  });

  const adjust_max = handler((start, p) => {
    min = p < start.min ? p : start.min;
    max = p < start.min ? start.min : p;
  });

  let left = $derived(min !== null ? 100 * min : null);
  let right = $derived(max !== null ? 100 * (1 - max) : null);
</script>

<!-- TODO Add keyboard accessibility. See https://github.com/mhkeller/layercake/pull/258 -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div bind:this={brush} class="brush-outer" onmousedown={reset} ontouchstart={reset}>
  {#if min !== null}
    <div
      class="brush-inner"
      draggable="false"
      onmousedown={move}
      ontouchstart={move}
      style="left: {left}%; right: {right}%"
    ></div>
    <div
      class="brush-handle"
      draggable="false"
      onmousedown={adjust_min}
      ontouchstart={adjust_min}
      style="left: {left}%"
    ></div>
    <div
      class="brush-handle"
      draggable="false"
      onmousedown={adjust_max}
      ontouchstart={adjust_max}
      style="right: {right}%"
    ></div>
  {/if}
</div>

<style>
  .brush-outer {
    position: relative;
    width: 100%;
    height: calc(100% + 5px);
    top: -5px;
  }

  .brush-inner {
    position: absolute;
    height: 100%;
    cursor: move;
    /* mix-blend-mode: difference; */
    background-color: #cccccc90;
    /* border: 1px solid #000; */
  }

  .brush-handle {
    position: absolute;
    width: 0;
    height: 100%;
    cursor: ew-resize;
  }

  .brush-handle::before {
    position: absolute;
    content: '';
    width: 8px;
    left: -4px;
    height: 100%;
    background: transparent;
  }
</style>