Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 316 additions & 0 deletions src/components/LightEffectPreview.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
---
/**
* LightEffectPreview Component
* Renders a small CSS-animated preview of an ESPHome light effect.
*
* Usage in MDX:
* <LightEffectPreview type="pulse" label="Animated preview of the Pulse effect" />
*/

type EffectType =
| 'pulse'
| 'random'
| 'strobe'
| 'flicker'
| 'lambda'
| 'rainbow'
| 'color-wipe'
| 'scan'
| 'twinkle'
| 'random-twinkle'
| 'fireworks'
| 'addr-flicker'
| 'addr-lambda';

interface Props {
type: EffectType;
label: string;
}

const { type, label } = Astro.props;

const STRIP_TYPES: ReadonlySet<EffectType> = new Set([
'rainbow',
'color-wipe',
'scan',
'twinkle',
'random-twinkle',
'fireworks',
'addr-flicker',
'addr-lambda',
]);

const isStrip = STRIP_TYPES.has(type);
const PIXEL_COUNT = 20;
---

<span class:list={['fx-preview', `fx-${type}`]} role="img" aria-label={label}>
{isStrip ? (
<span class="fx-strip">
{Array.from({ length: PIXEL_COUNT }, () => <span class="fx-pixel" />)}
</span>
) : (
<span class="fx-bulb" />
)}
</span>

<style>
.fx-preview {
display: inline-flex;
align-items: center;
background: #111;
padding: 4px 6px;
border-radius: 4px;
line-height: 0;
vertical-align: middle;
}
.fx-bulb {
width: 24px;
height: 24px;
border-radius: 50%;
background: #ffd24a;
display: inline-block;
}
.fx-strip {
display: inline-flex;
gap: 1px;
}
.fx-pixel {
width: 6px;
height: 14px;
background: #000;
display: inline-block;
}

/* Single-bulb effects */
.fx-pulse .fx-bulb { animation: fx-pulse 1.6s ease-in-out infinite; }
@keyframes fx-pulse { 0%, 100% { opacity: 0.2; } 50% { opacity: 1; } }

.fx-random .fx-bulb { animation: fx-random 2.4s steps(1) infinite; }
@keyframes fx-random {
0% { background: #ff3860; }
25% { background: #23d160; }
50% { background: #209cee; }
75% { background: #ffdd57; }
100% { background: #ff3860; }
}

.fx-strobe .fx-bulb { animation: fx-strobe 0.4s steps(1) infinite; }
@keyframes fx-strobe { 0% { opacity: 1; } 50% { opacity: 0.05; } 100% { opacity: 1; } }

.fx-flicker .fx-bulb {
background: #ffb04a;
animation: fx-flicker 1.3s linear infinite;
}
@keyframes fx-flicker {
0% { opacity: 0.9; }
13% { opacity: 1.0; }
21% { opacity: 0.65; }
34% { opacity: 0.95; }
47% { opacity: 0.75; }
61% { opacity: 1.0; }
73% { opacity: 0.7; }
88% { opacity: 0.9; }
100% { opacity: 0.85; }
}

.fx-lambda .fx-bulb {
background: #444;
display: inline-flex;
align-items: center;
justify-content: center;
color: #fff;
font: bold 14px/1 monospace;
}
.fx-lambda .fx-bulb::before { content: "\03BB"; }

/* Strip effects */
.fx-rainbow .fx-pixel { animation: fx-rainbow 2.2s linear infinite; }
.fx-rainbow .fx-pixel:nth-child(1) { animation-delay: 0s; }
.fx-rainbow .fx-pixel:nth-child(2) { animation-delay: -0.11s; }
.fx-rainbow .fx-pixel:nth-child(3) { animation-delay: -0.22s; }
.fx-rainbow .fx-pixel:nth-child(4) { animation-delay: -0.33s; }
.fx-rainbow .fx-pixel:nth-child(5) { animation-delay: -0.44s; }
.fx-rainbow .fx-pixel:nth-child(6) { animation-delay: -0.55s; }
.fx-rainbow .fx-pixel:nth-child(7) { animation-delay: -0.66s; }
.fx-rainbow .fx-pixel:nth-child(8) { animation-delay: -0.77s; }
.fx-rainbow .fx-pixel:nth-child(9) { animation-delay: -0.88s; }
.fx-rainbow .fx-pixel:nth-child(10) { animation-delay: -0.99s; }
.fx-rainbow .fx-pixel:nth-child(11) { animation-delay: -1.10s; }
.fx-rainbow .fx-pixel:nth-child(12) { animation-delay: -1.21s; }
.fx-rainbow .fx-pixel:nth-child(13) { animation-delay: -1.32s; }
.fx-rainbow .fx-pixel:nth-child(14) { animation-delay: -1.43s; }
.fx-rainbow .fx-pixel:nth-child(15) { animation-delay: -1.54s; }
.fx-rainbow .fx-pixel:nth-child(16) { animation-delay: -1.65s; }
.fx-rainbow .fx-pixel:nth-child(17) { animation-delay: -1.76s; }
.fx-rainbow .fx-pixel:nth-child(18) { animation-delay: -1.87s; }
.fx-rainbow .fx-pixel:nth-child(19) { animation-delay: -1.98s; }
.fx-rainbow .fx-pixel:nth-child(20) { animation-delay: -2.09s; }
@keyframes fx-rainbow {
0% { background: hsl( 0, 100%, 50%); }
17% { background: hsl( 60, 100%, 50%); }
33% { background: hsl(120, 100%, 50%); }
50% { background: hsl(180, 100%, 50%); }
67% { background: hsl(240, 100%, 50%); }
83% { background: hsl(300, 100%, 50%); }
100% { background: hsl(360, 100%, 50%); }
}

.fx-color-wipe .fx-pixel {
animation: fx-wipe 4s steps(1) infinite;
background: #000;
}
.fx-color-wipe .fx-pixel:nth-child(1) { animation-delay: 0s; }
.fx-color-wipe .fx-pixel:nth-child(2) { animation-delay: 0.10s; }
.fx-color-wipe .fx-pixel:nth-child(3) { animation-delay: 0.20s; }
.fx-color-wipe .fx-pixel:nth-child(4) { animation-delay: 0.30s; }
.fx-color-wipe .fx-pixel:nth-child(5) { animation-delay: 0.40s; }
.fx-color-wipe .fx-pixel:nth-child(6) { animation-delay: 0.50s; }
.fx-color-wipe .fx-pixel:nth-child(7) { animation-delay: 0.60s; }
.fx-color-wipe .fx-pixel:nth-child(8) { animation-delay: 0.70s; }
.fx-color-wipe .fx-pixel:nth-child(9) { animation-delay: 0.80s; }
.fx-color-wipe .fx-pixel:nth-child(10) { animation-delay: 0.90s; }
.fx-color-wipe .fx-pixel:nth-child(11) { animation-delay: 1.00s; }
.fx-color-wipe .fx-pixel:nth-child(12) { animation-delay: 1.10s; }
.fx-color-wipe .fx-pixel:nth-child(13) { animation-delay: 1.20s; }
.fx-color-wipe .fx-pixel:nth-child(14) { animation-delay: 1.30s; }
.fx-color-wipe .fx-pixel:nth-child(15) { animation-delay: 1.40s; }
.fx-color-wipe .fx-pixel:nth-child(16) { animation-delay: 1.50s; }
.fx-color-wipe .fx-pixel:nth-child(17) { animation-delay: 1.60s; }
.fx-color-wipe .fx-pixel:nth-child(18) { animation-delay: 1.70s; }
.fx-color-wipe .fx-pixel:nth-child(19) { animation-delay: 1.80s; }
.fx-color-wipe .fx-pixel:nth-child(20) { animation-delay: 1.90s; }
@keyframes fx-wipe {
0%, 50% { background: #209cee; }
50.01%, 100% { background: #000; }
}

.fx-scan .fx-strip { position: relative; }
.fx-scan .fx-pixel { background: #000; }
.fx-scan .fx-strip::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 100%;
background: #ffd24a;
animation: fx-scan 1.6s ease-in-out infinite alternate;
}
@keyframes fx-scan {
from { transform: translateX(0); }
to { transform: translateX(133px); }
}

.fx-twinkle .fx-pixel {
background: #000;
animation: fx-twinkle 1.8s ease-in-out infinite;
}
.fx-twinkle .fx-pixel:nth-child(3n) { animation-delay: 0s; }
.fx-twinkle .fx-pixel:nth-child(3n+1) { animation-delay: 0.6s; }
.fx-twinkle .fx-pixel:nth-child(3n+2) { animation-delay: 1.2s; }
.fx-twinkle .fx-pixel:nth-child(5n) { animation-delay: 0.3s; }
.fx-twinkle .fx-pixel:nth-child(7n) { animation-delay: 0.9s; }
@keyframes fx-twinkle {
0%, 100% { background: #000; }
10% { background: #fff; }
20% { background: #000; }
}

.fx-random-twinkle .fx-pixel {
background: #000;
animation: fx-rtwinkle 1.8s ease-in-out infinite;
}
.fx-random-twinkle .fx-pixel:nth-child(3n) { animation-delay: 0s; }
.fx-random-twinkle .fx-pixel:nth-child(3n+1) { animation-delay: 0.6s; }
.fx-random-twinkle .fx-pixel:nth-child(3n+2) { animation-delay: 1.2s; }
.fx-random-twinkle .fx-pixel:nth-child(4n) { animation-name: fx-rtwinkle-alt; }
.fx-random-twinkle .fx-pixel:nth-child(5n) { animation-name: fx-rtwinkle-warm; }
@keyframes fx-rtwinkle {
0%, 100% { background: #000; }
10% { background: #209cee; }
20% { background: #000; }
}
@keyframes fx-rtwinkle-alt {
0%, 100% { background: #000; }
10% { background: #ff3860; }
20% { background: #000; }
}
@keyframes fx-rtwinkle-warm {
0%, 100% { background: #000; }
10% { background: #ffd24a; }
20% { background: #000; }
}

.fx-fireworks .fx-pixel {
background: #000;
animation: fx-fireworks 2.4s ease-out infinite;
}
.fx-fireworks .fx-pixel:nth-child(4n) { animation-delay: 0s; }
.fx-fireworks .fx-pixel:nth-child(4n+1) { animation-delay: 0.6s; }
.fx-fireworks .fx-pixel:nth-child(4n+2) { animation-delay: 1.2s; }
.fx-fireworks .fx-pixel:nth-child(4n+3) { animation-delay: 1.8s; }
@keyframes fx-fireworks {
0% { background: #000; }
5% { background: #fff; }
100% { background: #000; }
}

.fx-addr-flicker .fx-pixel {
background: #ffb04a;
animation: fx-flicker 1.3s linear infinite;
}
.fx-addr-flicker .fx-pixel:nth-child(2n) { animation-delay: -0.4s; }
.fx-addr-flicker .fx-pixel:nth-child(3n) { animation-delay: -0.8s; }
.fx-addr-flicker .fx-pixel:nth-child(5n) { animation-delay: -0.2s; }

.fx-addr-lambda .fx-strip {
background: #444;
color: #fff;
font: bold 12px/14px monospace;
text-align: center;
padding: 0 6px;
width: 140px;
letter-spacing: 4px;
}
.fx-addr-lambda .fx-strip::before { content: "\03BB \03BB \03BB"; }
.fx-addr-lambda .fx-pixel { display: none; }

@media (prefers-reduced-motion: reduce) {
.fx-preview *, .fx-preview { animation: none !important; transition: none !important; }
.fx-pulse .fx-bulb { opacity: 0.7; }
.fx-strobe .fx-bulb { opacity: 1; }
.fx-random .fx-bulb { background: #23d160; }
.fx-flicker .fx-bulb { opacity: 0.9; }
.fx-rainbow .fx-pixel:nth-child(1) { background: hsl( 0, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(2) { background: hsl( 18, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(3) { background: hsl( 36, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(4) { background: hsl( 54, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(5) { background: hsl( 72, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(6) { background: hsl( 90, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(7) { background: hsl(108, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(8) { background: hsl(126, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(9) { background: hsl(144, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(10) { background: hsl(162, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(11) { background: hsl(180, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(12) { background: hsl(198, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(13) { background: hsl(216, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(14) { background: hsl(234, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(15) { background: hsl(252, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(16) { background: hsl(270, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(17) { background: hsl(288, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(18) { background: hsl(306, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(19) { background: hsl(324, 100%, 50%); }
.fx-rainbow .fx-pixel:nth-child(20) { background: hsl(342, 100%, 50%); }
.fx-color-wipe .fx-pixel:nth-child(-n+10) { background: #209cee; }
.fx-scan .fx-strip::after { display: none; }
.fx-scan .fx-pixel:nth-child(5),
.fx-scan .fx-pixel:nth-child(6) { background: #ffd24a; }
.fx-twinkle .fx-pixel:nth-child(3n) { background: #fff; }
.fx-random-twinkle .fx-pixel:nth-child(3n) { background: #209cee; }
.fx-random-twinkle .fx-pixel:nth-child(3n+1) { background: #ff3860; }
.fx-random-twinkle .fx-pixel:nth-child(3n+2) { background: #ffd24a; }
.fx-fireworks .fx-pixel:nth-child(4n) { background: #fff; }
.fx-addr-flicker .fx-pixel { opacity: 0.9; }
}
</style>
39 changes: 39 additions & 0 deletions src/content/docs/components/light/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ sidebar:
import APIClass from '@components/APIClass.astro';
import APIStruct from '@components/APIStruct.astro';
import APIRef from '@components/APIRef.astro';
import LightEffectPreview from '@components/LightEffectPreview.astro';

In ESPHome, `light` components allow you to create lights usable from Home Assistant's frontend and have many
features such as colors, transitions and even effects.
Expand Down Expand Up @@ -498,6 +499,44 @@ light:
> After setting a light effect, it is possible to reset the in-use effect back to a static light by setting the
> `effect` to `none` when it is being called through Home Assistant or directly on the device.

### Basic Animations

These effects work on any light platform.

| Preview | Effect | Description | Parameters |
| --- | --- | --- | --- |
| <LightEffectPreview type="pulse" label="Animated preview of the Pulse effect" /> | [Pulse](#pulse-effect) | Smooth brightness pulse | `name`, `transition_length`, `update_interval`, `min_brightness`, `max_brightness` |
| <LightEffectPreview type="random" label="Animated preview of the Random effect" /> | [Random](#random-effect) | Random colors at fixed intervals | `name`, `transition_length`, `update_interval` |
| <LightEffectPreview type="strobe" label="Animated preview of the Strobe effect" /> | [Strobe](#strobe-effect) | Cycle through a list of colors with set durations | `name`, `colors` |
| <LightEffectPreview type="flicker" label="Animated preview of the Flicker effect" /> | [Flicker](#flicker-effect) | Candle-like brightness variation | `name`, `alpha`, `intensity` |
| <LightEffectPreview type="lambda" label="Lambda effect runs user code; no canonical visual" /> | [Lambda](#lambda-effect) | Run custom C++ on each update | `name`, `update_interval`, `lambda` |

### Strip Animations

These effects require an addressable light (LED strip).

| Preview | Effect | Description | Parameters |
| --- | --- | --- | --- |
| <LightEffectPreview type="rainbow" label="Animated preview of the Addressable Rainbow effect" /> | [Addressable Rainbow](#addressable-rainbow-effect) | Hue sweeps the length of the strip | `name`, `speed`, `width` |
| <LightEffectPreview type="color-wipe" label="Animated preview of the Addressable Color Wipe effect" /> | [Addressable Color Wipe](#addressable-color-wipe-effect) | New colors shift in at the start of the strip | `name`, `colors`, `add_led_interval`, `reverse` |
| <LightEffectPreview type="scan" label="Animated preview of the Addressable Scan effect" /> | [Addressable Scan](#addressable-scan-effect) | Single dot slides back and forth across the strip | `name`, `move_interval`, `scan_width` |
| <LightEffectPreview type="twinkle" label="Animated preview of the Addressable Twinkle effect" /> | [Addressable Twinkle](#addressable-twinkle-effect) | Random pixels brighten and fade | `name`, `twinkle_probability`, `progress_interval` |
| <LightEffectPreview type="random-twinkle" label="Animated preview of the Addressable Random Twinkle effect" /> | [Addressable Random Twinkle](#addressable-random-twinkle-effect) | Twinkle, but each pixel gets a random color | `name`, `twinkle_probability`, `progress_interval` |
| <LightEffectPreview type="fireworks" label="Animated preview of the Addressable Fireworks effect" /> | [Addressable Fireworks](#addressable-fireworks-effect) | Sparks burst from random pixels | `name`, `update_interval`, `spark_probability`, `use_random_color`, `fade_out_rate` |
| <LightEffectPreview type="addr-flicker" label="Animated preview of the Addressable Flicker effect" /> | [Addressable Flicker](#addressable-flicker-effect) | Per-pixel candle flicker | `name`, `update_interval`, `intensity` |
| <LightEffectPreview type="addr-lambda" label="Addressable Lambda effect runs user code; no canonical visual" /> | [Addressable Lambda](#addressable-lambda-effect) | Run custom C++ per LED | `name`, `update_interval`, `lambda` |

### External Control

These effects receive frames from an external source instead of generating animation locally.

| Effect | Description | Parameters |
| --- | --- | --- |
| [Automation](#automation-light-effect) | Run ESPHome automation actions on a loop | `name`, `sequence` |
| [E1.31](#e131-effect) | Receive sACN / E1.31 frames over the network | `universe`, `channels` |
| [Adalight](#adalight-effect) | Receive Adalight protocol over UART | `uart_id` |
| [WLED](#wled-effect) | Receive WLED UDP sync protocol | `port`, `blank_on_start`, `sync_group_mask` |

### Pulse Effect

This effect makes a pulsating light. The period can be defined by `update_interval`, the transition length with
Expand Down