Lens Distortion
Installation
npm i @paper-design/shaders-react
Code
import { LensDistortion } from '@paper-design/shaders-react'; import './demo.css'; <LensDistortion spread={0.15} angle={28} perspective={1} count={50} dispersionColor={0.75} focusCenter={0.5} focusEdges={0} swirl={0} lensBulge={0.1} > <div className="demo" data-recording="on" ref={(element) => { const timecode = element.querySelector('time'); let frames = 0; const interval = setInterval(() => { if (element.dataset.recording === 'off') return; frames += 1; const parts = [frames / 86400, (frames / 1440) % 60, (frames / 24) % 60, frames % 24]; timecode.textContent = parts.map((part) => String(Math.floor(part)).padStart(2, '0')).join(':'); }, 1000 / 24); return () => clearInterval(interval); }} > <div className="scene" /> <div className="tint" /> <div className="shade" /> <header> <time>00:00:00:00</time> <button className="rec" onClick={(event) => { const demo = event.currentTarget.closest('.demo'); demo.dataset.recording = demo.dataset.recording === 'off' ? 'on' : 'off'; }} > rec </button> </header> <footer onInput={(event) => { const input = event.target; if (!input.dataset.property) return; input.closest('.demo').style.setProperty(input.dataset.property, input.value); const value = input.valueAsNumber.toFixed(Number(input.dataset.digits ?? 1)); input.parentElement.querySelector('output').textContent = input.dataset.format.replace('%', value); }} > <label className="dial"> <span>ev</span> <input type="range" min={-2} max={2} step={0.1} defaultValue={0} data-property="--ev" data-format="%" /> <output>0.0</output> </label> <label className="dial"> <span>zoom</span> <input type="range" min={1} max={3} step={0.1} defaultValue={1} data-property="--zoom" data-format="%×" /> <output>1.0×</output> </label> <label className="dial"> <span>wb</span> <input type="range" min={3200} max={8000} step={100} defaultValue={5600} data-property="--wb" data-format="%K" data-digits={0} /> <output>5600K</output> </label> <label className="dial"> <span>color</span> <input type="range" min={0} max={1} step={0.01} value={0.75} onChange={(event) => setDispersionColor(event.currentTarget.valueAsNumber)} /> <output>0.75</output> </label> </footer> </div> </LensDistortion>
demo.css
.demo { --ev: 0; --zoom: 1; --wb: 5600; --warmth: calc((var(--wb) - 5600) / 2400); --panel: 38cqw; --gap: 1.6cqw; --inset: 2.2cqw; position: relative; height: 100%; container-type: inline-size; overflow: hidden; color: #f4f3ec; background: #121212; font: 22px ui-monospace, monospace; text-transform: uppercase; user-select: none; text-shadow: 0 0.1cqw 0.4cqw rgb(0 0 0 / 50%) } .demo .scene { position: absolute; inset: 0; background: url(/images/image-filters/003.webp) center / cover; scale: var(--zoom); filter: brightness(pow(2, var(--ev))); transition: scale 200ms cubic-bezier(0.22, 1, 0.36, 1), filter 200ms } .demo .tint { position: absolute; inset: 0; background: color-mix(in oklab, #3d7bff, #ff9a3d calc((var(--warmth) + 1) * 50%)); mix-blend-mode: soft-light; opacity: calc(max(var(--warmth), -1 * var(--warmth)) * 0.8); transition: opacity 200ms } .demo .shade { position: absolute; inset: 0; background: linear-gradient(rgb(0 0 0 / 55%), transparent 25% 55%, rgb(0 0 0 / 75%)) } .demo header, .demo footer { position: absolute; left: 50%; width: calc(2 * var(--panel) + var(--gap)); translate: -50% 0 } .demo header { top: 5cqw; display: flex; justify-content: space-between; align-items: center } .demo footer { bottom: 5cqw; display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap) } .demo header time { padding-left: var(--inset) } .demo .rec { padding: 1.4cqw var(--inset); border: 0.2cqw solid rgb(255 255 255 / 30%); border-radius: 1.4cqw; background: rgb(0 0 0 / 45%); font: inherit; text-transform: inherit; color: inherit; cursor: pointer; transition: background-color 150ms cubic-bezier(0.685, 0.89, 0.315, 0.995) } .demo .rec:hover { background: rgb(255 255 255 / 14%) } .demo .rec:active { background: rgb(255 255 255 / 24%) } .demo .rec::before { content: ''; display: inline-block; width: 1.5cqw; height: 1.5cqw; margin-right: 1cqw; border-radius: 50%; background: #ff3b30; animation: blink 1s infinite } .demo[data-recording="off"] .rec::before { background: transparent; box-shadow: inset 0 0 0 0.2cqw currentColor; animation: none } .demo .dial { display: grid; grid-template-columns: 5ch 1fr 5ch; align-items: center; gap: 1.6cqw; padding: 1.2cqw var(--inset) } .demo .dial span { opacity: 0.75 } .demo .dial output { text-align: right } .demo input { appearance: none; min-width: 0; height: 4.4cqw; margin: 0; background: linear-gradient(transparent calc(50% - 0.15cqw), rgb(255 255 255 / 55%) 0 calc(50% + 0.15cqw), transparent 0); outline: none; cursor: pointer } .demo input::-webkit-slider-thumb { appearance: none; width: 1.4cqw; height: 3.6cqw; border-radius: 0.7cqw; background: #f4f3ec; box-shadow: 0 0.1cqw 0.6cqw rgb(0 0 0 / 50%); transition: background-color 150ms } .demo input:focus-visible::-webkit-slider-thumb { background: #ffd84d } @keyframes blink { 50% { opacity: 0.15 } }
Shader Props
Common Props
Description
Lens Distortion image filter separates an image into shifting color layers (recreating the chromatic aberration of a lens) and warps the image geometry itself, curving it outward or inward like barrel and pincushion distortion.