Median Cut Palette Plate
Pulls a genuinely distinct palette out of a photograph by median cut — splitting on channel spread, not on population, sorted by relative luminance.
Canvas 2D0 deps1 file · 10 KB TSX
Props
"use client";
import { useEffect, useRef, useState } from "react";
type Box = { pixels: number[][] };
//
// MEDIAN CUT, NOT A HISTOGRAM.
//
// Taking the N most frequent colours returns N near-duplicates of whatever
// fills the most area — five shades of the same sky, and the one brilliant
// accent that made the photograph worth using never appears.
//
// Median cut puts every pixel in one box, then repeatedly takes the box with
// the widest single-channel SPREAD and splits it at the MEDIAN of that
// channel. Splitting on spread rather than on population is the whole trick:
// a small vivid region has a wide range and survives; a huge flat background
// has a narrow one and collapses into a single entry.
// ---------------------------------------------------------------
// Full source available with a viberdy Pro subscription.
// https://viberdy.dev/pricing
// ---------------------------------------------------------------
}Details
About this component
Extracting a palette from an image is usually done by counting the most frequent colours, and that is why so many extracted palettes are five shades of the same sky: population favours whatever fills the most area, so the small brilliant accent that made the photograph worth using never survives. Median cut inverts the criterion. Every pixel starts in one box; the box with the widest single-channel spread is split at the median of that channel, repeatedly, and each final box is averaged. Splitting on spread rather than count is the whole idea — a small vivid region has a wide range and survives, a huge flat background has a narrow one and collapses into one entry. Three practical details decide whether it ships. The crossOrigin attribute has to be assigned before the src, because reading pixels from an image the browser did not fetch with CORS taints the canvas permanently, and setting the attribute afterwards has no effect at all; that version reads perfectly and fails only later at getImageData, which is why it is the one usually written. The analysis runs on a small offscreen thumbnail, since a palette needs a few thousand samples rather than the four million in a full-size photograph. And the result is sorted by relative luminance on linearised channels rather than by population or HSL lightness, which is the difference between a designed ramp and a bug report.
Curator’s note
Assigning crossOrigin after src reads perfectly and does nothing — the request already went out, and the failure only surfaces later at getImageData.

