Shaders
Shaders / Component

Component <fk-shader>

Six styles from the Lab, packed into one small web component for your own pages. It renders an image or video as a live field of dots, squares, lines, dither, ASCII or LCD segments. It reads your , and you can push it around with your finger or cursor.

8 KB gzipped · no dependencies · web component · TypeScript types

Installation

Two ways in. Pick one.

Option A: link it

One line, like loading a web font. This address is version 2.0.0 and never changes, so browsers keep it for a year and the integrity hash proves it is the file you expect.

  1. Add the script once, anywhere on the page.
    <script src="https://shaders.fedekotek.design/v/2.0.0/fk-shader.min.js"
      integrity="sha384-fCGt3+Blw0Kh/IqyNvNRIyswpEU9N9FdJHHZW4i8DaRRz6ZwUngC4HblwxailZLZ"
      crossorigin="anonymous" defer></script>

    Want every fix as it ships instead? This address always serves the latest build, including major versions that may change the API. Pin for production.

    <script src="https://shaders.fedekotek.design/fk-shader.min.js" defer></script>
  2. Give it a size in your CSS. It fills whatever box you put it in.
    <style>
      fk-shader { display: block; width: 100%; aspect-ratio: 3 / 4; }
    </style>
  3. Drop the tag where you want the picture, as in Usage.

Option B: copy it

Own the code, the way shadcn works. Nothing to install, nothing to keep in sync.

  1. Download fk-shader.js (readable) or fk-shader.min.js (about 8 KB gzipped) into your project. Using TypeScript? Take fk-shader.d.ts too.
  2. Load it, with the path relative to your page.
    <script src="js/fk-shader.js" defer></script>
  3. Give it a size and drop the tag, same as in option A.

Open your page from a server, not by double-clicking the file. Opened as file://, the browser will not let the component read pixels and you get a blank box. Any local server works, for example python3 -m http.server.

Coming from <dv-media> 1.x? See Coming from dv-media 1.x.

Usage

<fk-shader
  src="your-video.webm"
  variant="dots"
  interactive
  alt="The team walking through the office">
</fk-shader>

<style>
  fk-shader { display: block; aspect-ratio: 3 / 4; }
</style>

It needs a size: without a height it collapses to 1 px, shows nothing, and says so in the console. Pass a video or an image file, or #id to draw a video or image already on the page (it reads it, and leaves playing and pausing to you). Videos are recognised by their file extension (mp4, webm, mov, m4v, ogv). For any other URL (blob:, streaming, an API), put a <video id="x"> on the page and pass src="#x". Instances with the same source share one decoder.

Knowing when it is on screen, or why not

// run this after the tag, or from a script with defer
const shader = document.querySelector('fk-shader');
shader.addEventListener('ready', () => { /* first frame drawn */ });
shader.addEventListener('error', (e) => console.log(e.detail.reason)); // 'load' or 'cors'
if (shader.error) console.log(shader.error); // it failed before you were listening

In React, Vue or Svelte

It is a plain custom element, so it works in any framework once the script is loaded. With a bundler, import './fk-shader.js' once in your entry; it does nothing on the server, so SSR is safe. Every attribute is also a property, and booleans treat "false" as off, so interactive={false} does what it says.

In React, listen for ready and error through a ref: an onReady or onError prop does not fire in React 18 or 19. In React 18, use class, not className.

// React 18 and 19
import { useEffect, useRef } from 'react';
import './fk-shader.js';

export function Portrait({ clipUrl, isTouch }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    const on = () => { /* first frame drawn */ };
    el.addEventListener('ready', on);
    return () => el.removeEventListener('ready', on);
  }, []);
  return (
    <fk-shader ref={ref} src={clipUrl} variant="lines" interactive={isTouch} alt=""
      style={{ display: 'block', aspectRatio: '3 / 4' }} />
  );
}

In Vue, tell the compiler it is a custom element: set compilerOptions.isCustomElement to (tag) => tag === 'fk-shader'. With TypeScript, fk-shader.d.ts types the element and adds the tag to JSX, for React 18 and 19. Download it into your src/, or add its path to include in tsconfig.json.

Examples

Variants

Six ways to draw the same picture. Each has its own default pitch, shown under it (default sizes are in the API).

dots · pitch 10
squares · pitch 14
lines · pitch 11
dither · pitch 5
ascii · pitch 12
lcd · pitch 16

Theming

Color comes from two CSS custom properties, --fk-shader-bg and --fk-shader-ink, in any CSS color format: hex, rgb, hsl, oklch, a name, even currentColor. Set them on a class, or once at the root so they follow your dark mode. On dark paper the light parts of the picture get the ink. The LCD variant has its own pair, --fk-shader-lcd-bg and --fk-shader-lcd-ink, for the glass and the segments.

riso
print
term

Image source

Stills work too. Once the picture settles it stops drawing entirely, until you touch it.

dither · pitch 4
lcd pixels · pitch 12

Interactive

Add interactive and a finger or cursor pushes the cells aside, then a spring brings them home. On an LCD, pressing bruises the glass dark. On a phone, drag sideways: a vertical drag still scrolls the page.

drag across · dots
drag across · lcd

Tuning

The same clip, first as it comes, then with one attribute changed each time.

default
pitch="6"
pitch="16"
size="0.6"
invert
fit="contain"

API reference

Attributes

Every attribute is also a property (el.variant = 'lcd'). Booleans are on when present, and "false" counts as off.

AttributeTypeDefaultDescription
srcstringnoneVideo or image URL, or #id of a video or image already on the page. A borrowed video is read, never played or paused. Videos are recognised by their file extension (mp4, webm, mov, m4v, ogv). For any other URL (blob:, streaming, an API), put a <video id="x"> on the page and pass src="#x".
variantdots squares lines dither ascii lcddotsHow each cell is drawn.
pitchnumber, pxdots 10, squares 14, lines 11, dither 5, ascii 12, lcd 16Size of the grid cell. Minimum 3. Past a cap the grid opens up by itself, so a big box never freezes the page: 9,000 cells for lines, 12,000 for lcd, 16,000 for ascii, 30,000 for the others.
sizenumber, 0 to 2dots 1, squares 0.95, lines 1.35Largest mark relative to the cell, for dots, squares and lines. 1 is as wide as the cell.
contrastnumber1Multiplies the variant's tone curve. Above 1 thins the mid-tones, below 1 fills them. Useful range 0.5 to 2.
exposurenumber0Shifts the whole picture lighter or darker. Useful range -0.5 to 0.5.
fitcover containcoverCrop to fill, or show the whole frame.
invertbooleanfalseSwap which end of the tone scale gets the ink.
interactivebooleanfalseCells react to touch and cursor. Off under reduced motion and while paused, and then swipes are not captured either. Only an interactive instance holds sideways drags; the rest leave swipes and pinch-zoom to the page.
lcddigits pixelsdigitsFor the LCD variant: seven-segment numbers or plain pixels.
pausedbooleanfalseFreezes the picture, and it stops reacting to touch.
altstring""Accessible description. Leave empty when decorative, and assistive tech skips it.

CSS custom properties

Any CSS color format works, including currentColor and light-dark(). A value that is not a color logs a warning and falls back to the default.

PropertyDefaultDescription
--fk-shader-bgtransparentPaper. Its lightness decides which end of the picture gets ink. Left transparent, the ink decides instead: light ink is read as a dark page.
--fk-shader-inkthe element's colorThe marks.
--fk-shader-lcd-bg, --fk-shader-lcd-ink#A6AA80 #1F241BGlass and segments for the LCD variant.

Methods, events and parts

NameDescription
play(), pause()Same as removing or setting paused.
ready event, el.readyFires when a source draws its first frame, once the element is on screen, and again after each src change. Bubbles. el.ready stays true until the source changes, so a listener added late can check it.
error event, el.errorThe source could not be loaded (detail.reason is load), or it is on another origin and failed or cannot be read (cors). Also logged once as a warning that says what to do. Fires a tick later so a listener added right after the tag hears it, and does not bubble. el.error holds the reason, or null.
::part(canvas)Style the internal canvas from outside.
window.FkShaderThe element class, for instanceof checks.

Accessibility and performance

With an alt the canvas is an image with that label. With an empty alt it is hidden from assistive tech as decoration. With reduced motion on, the picture is drawn once without easing, a video it loaded stays paused, and interactive is off. The page's Pause button above does the same for everyone.

It only draws when something changes: a new video frame, cells settling, a finger on it. Offscreen instances stop, and a still image stops drawing once it settles. Cost grows with the number of cells, and halving the pitch makes four times as many, so a small pitch on a big box is the expensive combination.

Media from another domain needs CORS headers, or the browser will not let the component read its pixels (you get an error event with cors, and a console warning that names the missing header). It works under a strict Content Security Policy: its styles use a constructed stylesheet, not an inline style tag.

Coming from dv-media 1.x

This component used to be called <dv-media>. Nothing breaks if you leave it: the old tag and its pinned links keep working as they are. The rolling /dv-media.min.js is frozen at 1.2.0 and gets no more fixes. To move to 2.0.0, do it in this order:

  1. Replace the whole script line with the new one from Installation. Do this first, and not with a find-and-replace of "dv-media": that would also rewrite the old address, and point it at a file that does not exist. If you copied the file, download fk-shader.js in its place.
  2. Rename the tag, <dv-media> to <fk-shader>, in your HTML and in any CSS or script that selects it.
  3. Rename the color variables, --dv-bg to --fk-shader-bg, and the same for --dv-ink, --dv-lcd-bg and --dv-lcd-ink. Attributes, events and variants stay the same.
  4. window.DvMedia is now window.FkShader. With TypeScript, swap dv-media.d.ts for fk-shader.d.ts and rename DvMediaElement, DvMediaVariant, DvMediaErrorReason and DvMediaAttributes to FkShaderElement, FkShaderVariant, FkShaderErrorReason and FkShaderAttributes.

Lab

The playground has 30 styles, layers and brushes. Tune a look there, then copy it as a tag with Copy as <fk-shader>, in More, Component. Looks that use a style outside these six copy as the closest variant.

Open the Lab