ditherkit
Integrations

Svelte / Vue / Solid

No framework component yet — use @ditherkit/core directly with about 20 lines of glue.

Status: 🛠 Use core directly. No framework wrapper package exists or is planned. @ditherkit/core has no framework dependencies and works anywhere — this page shows how to wire it into Svelte, Vue, and Solid.

The @ditherkit/core algorithms are pure functions that take a Uint8ClampedArray of RGBA pixels. That means any framework can use them — all you need is a way to:

  1. Load an image into a canvas
  2. Extract ImageData from the canvas
  3. Call the dithering functions
  4. Put the result back into the canvas

That's ~20 lines of glue code, and the glue doesn't care which framework you're in. The same pattern works in Svelte, Vue, Solid, Qwik, and any other framework. This page shows Svelte explicitly; the Vue and Solid versions are structurally identical.

What works

  • @ditherkit/core — all seven algorithms (Floyd-Steinberg, Atkinson, Jarvis-Judice-Ninke, Stucki, Burkes, Bayer, Threshold) plus colour/image helpers.
  • Canvas rendering — every framework supports refs/bindings to DOM elements, which is all you need.
  • Web Worker offloading — you can write your own tiny worker that calls into @ditherkit/core if you need to keep the main thread free. It's ~30 lines.

What doesn't work

  • No <DitheredImage /> Svelte/Vue/Solid component. You write your own, usually in less time than it would take to install a wrapper package.
  • No shared worker singleton. The @ditherkit/react worker management is React-specific. You'd write your own or just create a worker per component instance.
  • No server-side caching. Same as React SPA — there's no server to cache against. Use build-time dithering (like the Astro Pattern 2) or a small backend.

Quick start — Svelte

pnpm add @ditherkit/core
<!-- src/lib/DitheredImage.svelte -->
<script lang="ts">
  import { onMount } from 'svelte'
  import {
    floydSteinbergDither,
    grayscale,
    type Color,
  } from '@ditherkit/core'

  export let src: string
  export let width: number
  export let height: number
  export let algorithm: 'Floyd-Steinberg' = 'Floyd-Steinberg'

  let canvas: HTMLCanvasElement

  const palette: Color[] = [
    { r: 0, g: 0, b: 0 },
    { r: 255, g: 255, b: 255 },
  ]

  onMount(async () => {
    const image = new Image()
    image.crossOrigin = 'Anonymous'
    await new Promise((resolve, reject) => {
      image.onload = resolve
      image.onerror = reject
      image.src = src
    })

    const ctx = canvas.getContext('2d')!
    ctx.drawImage(image, 0, 0, width, height)
    const imageData = ctx.getImageData(0, 0, width, height)

    grayscale(imageData.data)
    floydSteinbergDither(imageData.data, width, height, palette)

    ctx.putImageData(imageData, 0, 0)
  })
</script>

<canvas bind:this={canvas} {width} {height}></canvas>

Use it:

<script>
  import DitheredImage from '$lib/DitheredImage.svelte'
</script>

<DitheredImage src="/portrait.jpg" width={400} height={600} />

Same shape works in Vue (with <script setup> and a <canvas ref>) and Solid (with createEffect and a <canvas ref>).

Adding a Web Worker (optional)

The component above runs dithering on the main thread, which will block the UI for a few milliseconds on small images and longer on large ones. To offload to a worker:

// src/lib/dither.worker.ts
import {
  floydSteinbergDither,
  grayscale,
  type Color,
} from '@ditherkit/core'

const palette: Color[] = [
  { r: 0, g: 0, b: 0 },
  { r: 255, g: 255, b: 255 },
]

self.onmessage = (e) => {
  const { imageData, width, height } = e.data
  grayscale(imageData.data)
  floydSteinbergDither(imageData.data, width, height, palette)
  self.postMessage({ imageData }, [imageData.data.buffer])
}

Then in your component:

const worker = new Worker(
  new URL('./dither.worker.ts', import.meta.url),
  { type: 'module' }
)

worker.postMessage({ imageData, width, height })
worker.onmessage = (e) => {
  ctx.putImageData(e.data.imageData, 0, 0)
  worker.terminate()
}

This is roughly what @ditherkit/react does internally, minus the shared-singleton optimisation and the AbortSignal cancellation. For most use cases, the simpler per-component worker is fine.

Vue variant

Same pattern, idiomatic Vue:

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { floydSteinbergDither, grayscale, type Color } from '@ditherkit/core'

const props = defineProps<{ src: string; width: number; height: number }>()
const canvas = ref<HTMLCanvasElement | null>(null)

const palette: Color[] = [
  { r: 0, g: 0, b: 0 },
  { r: 255, g: 255, b: 255 },
]

onMounted(async () => {
  if (!canvas.value) return
  const image = new Image()
  image.crossOrigin = 'Anonymous'
  await new Promise((resolve) => {
    image.onload = resolve
    image.src = props.src
  })

  const ctx = canvas.value.getContext('2d')!
  ctx.drawImage(image, 0, 0, props.width, props.height)
  const imageData = ctx.getImageData(0, 0, props.width, props.height)

  grayscale(imageData.data)
  floydSteinbergDither(imageData.data, props.width, props.height, palette)
  ctx.putImageData(imageData, 0, 0)
})
</script>

<template>
  <canvas ref="canvas" :width="width" :height="height"></canvas>
</template>

Solid variant

import { onMount, createSignal } from 'solid-js'
import { floydSteinbergDither, grayscale, type Color } from '@ditherkit/core'

export function DitheredImage(props: {
  src: string
  width: number
  height: number
}) {
  let canvas: HTMLCanvasElement | undefined

  const palette: Color[] = [
    { r: 0, g: 0, b: 0 },
    { r: 255, g: 255, b: 255 },
  ]

  onMount(async () => {
    if (!canvas) return
    const image = new Image()
    image.crossOrigin = 'Anonymous'
    await new Promise((resolve) => {
      image.onload = resolve
      image.src = props.src
    })

    const ctx = canvas.getContext('2d')!
    ctx.drawImage(image, 0, 0, props.width, props.height)
    const imageData = ctx.getImageData(0, 0, props.width, props.height)

    grayscale(imageData.data)
    floydSteinbergDither(imageData.data, props.width, props.height, palette)
    ctx.putImageData(imageData, 0, 0)
  })

  return <canvas ref={canvas} width={props.width} height={props.height} />
}

Will there ever be framework-specific packages?

Not planned. The glue code is short enough that maintaining four more packages would cost more than it saves. If you write a nice reusable component in your own codebase, consider publishing it as a community package — we'd link to it from here.

On this page