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
<fk-shader
src="your-video.webm"
variant="dots"
size="0.74"
interactive
alt="A man lying on a sofa, drawn in dots">
</fk-shader>
<style>
fk-shader { display: block; aspect-ratio: 4 / 5; }
</style>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.
- 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> - 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> - 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.
- Download
fk-shader.js(readable) orfk-shader.min.js(about 8 KB gzipped) into your project. Using TypeScript? Takefk-shader.d.tstoo. - Load it, with the path relative to your page.
<script src="js/fk-shader.js" defer></script> - 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 listeningIn 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).
<fk-shader src="your-video.webm" variant="dots" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="squares" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="lines" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="dither" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="ascii" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="lcd" alt=""></fk-shader>
<style>
fk-shader { display: block; aspect-ratio: 3 / 4; }
</style>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.
<fk-shader class="t-riso" src="your-video.webm"
variant="dots" alt=""></fk-shader>
<fk-shader class="t-print" src="your-video.webm"
variant="squares" alt=""></fk-shader>
<fk-shader class="t-term" src="your-video.webm"
variant="ascii" pitch="8" alt=""></fk-shader>
<style>
.t-riso {
--fk-shader-bg: #F3EFE3;
--fk-shader-ink: #FF4D2E;
}
.t-print {
--fk-shader-bg: #0B2A5B;
--fk-shader-ink: #DCE8FF;
}
.t-term {
--fk-shader-bg: #060A06;
--fk-shader-ink: #5BF08A;
}
fk-shader { display: block; aspect-ratio: 3 / 4; }
/* or once for the whole site, following dark mode */
:root {
--fk-shader-bg: hsl(50 17% 91%);
--fk-shader-ink: hsl(52 21% 20%);
}
@media (prefers-color-scheme: dark) {
:root {
--fk-shader-bg: hsl(240 7% 4%);
--fk-shader-ink: hsl(50 12% 83%);
--fk-shader-lcd-bg: #121409; /* LCD glass and segments */
--fk-shader-lcd-ink: #98A07A;
}
}
</style>Image source
Stills work too. Once the picture settles it stops drawing entirely, until you touch it.
<fk-shader src="portrait.jpg" variant="dither" pitch="4"
interactive alt="Portrait, dithered"></fk-shader>
<fk-shader src="portrait.jpg" variant="lcd" lcd="pixels" pitch="12"
interactive alt="Portrait on an LCD"></fk-shader>
<style>
fk-shader { display: block; aspect-ratio: 3 / 4; }
</style>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.
<fk-shader src="your-video.webm" variant="dots" pitch="12" interactive alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="lcd" lcd="pixels" pitch="12" interactive alt=""></fk-shader>
<style>
fk-shader { display: block; aspect-ratio: 3 / 4; }
</style>Tuning
The same clip, first as it comes, then with one attribute changed each time.
<fk-shader src="your-video.webm" variant="dots" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="dots" pitch="6" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="dots" pitch="16" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="dots" size="0.6" alt=""></fk-shader>
<fk-shader src="your-video.webm" variant="dots" invert alt=""></fk-shader>
<!-- a portrait clip in a 3:4 box: contain shows all of it, with bars at the sides -->
<fk-shader src="your-video.webm" variant="dots" fit="contain" alt=""></fk-shader>
<style>
fk-shader { display: block; aspect-ratio: 3 / 4; }
</style>API reference
Attributes
Every attribute is also a property (el.variant = 'lcd'). Booleans are on when present, and "false" counts as off.
| Attribute | Type | Default | Description |
|---|---|---|---|
src | string | none | Video 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". |
variant | dots squares lines dither ascii lcd | dots | How each cell is drawn. |
pitch | number, px | dots 10, squares 14, lines 11, dither 5, ascii 12, lcd 16 | Size 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. |
size | number, 0 to 2 | dots 1, squares 0.95, lines 1.35 | Largest mark relative to the cell, for dots, squares and lines. 1 is as wide as the cell. |
contrast | number | 1 | Multiplies the variant's tone curve. Above 1 thins the mid-tones, below 1 fills them. Useful range 0.5 to 2. |
exposure | number | 0 | Shifts the whole picture lighter or darker. Useful range -0.5 to 0.5. |
fit | cover contain | cover | Crop to fill, or show the whole frame. |
invert | boolean | false | Swap which end of the tone scale gets the ink. |
interactive | boolean | false | Cells 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. |
lcd | digits pixels | digits | For the LCD variant: seven-segment numbers or plain pixels. |
paused | boolean | false | Freezes the picture, and it stops reacting to touch. |
alt | string | "" | 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.
| Property | Default | Description |
|---|---|---|
--fk-shader-bg | transparent | Paper. 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-ink | the element's color | The marks. |
--fk-shader-lcd-bg, --fk-shader-lcd-ink | #A6AA80 #1F241B | Glass and segments for the LCD variant. |
Methods, events and parts
| Name | Description |
|---|---|
play(), pause() | Same as removing or setting paused. |
ready event, el.ready | Fires 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.error | The 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.FkShader | The 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:
- 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.jsin its place. - Rename the tag,
<dv-media>to<fk-shader>, in your HTML and in any CSS or script that selects it. - Rename the color variables,
--dv-bgto--fk-shader-bg, and the same for--dv-ink,--dv-lcd-bgand--dv-lcd-ink. Attributes, events and variants stay the same. window.DvMediais nowwindow.FkShader. With TypeScript, swapdv-media.d.tsforfk-shader.d.tsand renameDvMediaElement,DvMediaVariant,DvMediaErrorReasonandDvMediaAttributestoFkShaderElement,FkShaderVariant,FkShaderErrorReasonandFkShaderAttributes.
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.