Skip to content

Commit 9adf6ca

Browse files
authored
feat: harness time-lapse GIF capture for the sky-compass card (#114)
Add an npm run capture:timelapse script that drives the dev harness through a simulated day with Playwright and encodes the sky-compass card to an auto-looping GIF via ffmpeg, for the README and HACS panel (which strips <video>, so GIF auto-loops inline everywhere). - harness: ?capture-gated window.__acpCapture bridge to step time deterministically - script: scenario/time-span/format flags, compass-only framing, fixed-clip frames - defaults: north-window preset, Sun Today chart, moon overlay, wide legend-right layout - README: embed images/sky-compass-timelapse.gif + dev note
1 parent 076ec5c commit 9adf6ca

7 files changed

Lines changed: 494 additions & 3 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
node_modules/
33
*.tsbuildinfo
44
harness/dist/
5+
.timelapse-frames/
56

67
# editor / OS
78
.vscode/*

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ Find your `entry_id` on the integration's URL:
9696

9797
## Screenshots
9898

99+
**Sky compass — a full day in motion.** The standalone sky compass card across a simulated day: the sun rises, arcs through the window's field of view, and sets, while the cover-closure wedge tracks it. The dimmed disc is the sun below the horizon at night.
100+
101+
![Sky compass — a day in motion](https://raw.githubusercontent.com/jrhubott/adaptive-cover-pro-card/main/images/sky-compass-timelapse.gif)
102+
99103
**Tile card — five configurations stacked.** Same `custom:adaptive-cover-pro-tile-card` type; each tile reflects a different combination of `show_position`, `show_controls`, `show_badge`, and `show_resume`, plus the live winner-driven badge state (Auto / Manual countdown / etc.):
100104

101105
![Tile card configurations](https://raw.githubusercontent.com/jrhubott/adaptive-cover-pro-card/main/images/tile-card-variants.png)
@@ -115,6 +119,9 @@ npm run test # vitest
115119
npm run lint
116120
```
117121

122+
The sky-compass demo GIF above is regenerated from the dev harness with
123+
`npm run capture:timelapse` (needs `npx playwright install chromium` once and `ffmpeg` on PATH; run `npm run capture:timelapse -- --help` for scenario/time-span/format options).
124+
118125
## Support
119126

120127
[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/jrhubott)

harness/src/harness-app.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { LitElement, css, html, type TemplateResult } from 'lit';
22
import { customElement, state } from 'lit/decorators.js';
33
import type { HomeAssistant } from 'custom-card-helpers';
44

5+
import type { LitElement as LitElementType } from 'lit';
6+
import { SKY_COMPASS_CARD_NAME } from '../../src/const';
57
import { buildMockHass, type ServiceCallEvent } from './mock/hass';
68
import { applyService, type ServiceCall } from './mock/services';
7-
import { defaultScenarioConfig, normalizeConfig } from './scenarios';
9+
import { defaultScenarioConfig, findScenario, normalizeConfig, SCENARIOS } from './scenarios';
810
import { loadConfig, saveConfig } from './persistence';
911
import { setFakeNow } from './fake-clock';
1012
import { zoneForLongitude, zonedNowMs } from './zone';
@@ -14,12 +16,46 @@ import {
1416
readStateFromUrl,
1517
writeStateToUrl,
1618
} from './share-state';
17-
import type { HarnessConfig } from './types';
19+
import type {
20+
HarnessConfig,
21+
RootCardOptions,
22+
SkyCompassCardOptions,
23+
TileCardOptions,
24+
} from './types';
1825
import type { ConfigChangeDetail } from './control-panel';
1926
import './control-panel';
2027
import './card-stage';
2128
import './service-log';
2229

30+
/** A shallow-by-section partial of {@link HarnessConfig} accepted by the capture bridge. */
31+
export type CapturePartial = Partial<Omit<HarnessConfig, 'root' | 'compass' | 'tile'>> & {
32+
root?: Partial<RootCardOptions>;
33+
compass?: Partial<SkyCompassCardOptions>;
34+
tile?: Partial<TileCardOptions>;
35+
};
36+
37+
/**
38+
* Programmatic hook the time-lapse capture script drives via Playwright. Only
39+
* attached when the page URL carries a `?capture` param, so normal harness runs
40+
* are untouched. See `scripts/capture-timelapse.mjs`.
41+
*/
42+
export interface CaptureBridge {
43+
listScenarios(): { id: string; label: string }[];
44+
loadScenario(id: string): Promise<void>;
45+
setConfig(partial: CapturePartial): Promise<void>;
46+
setMinutes(minutes: number): Promise<void>;
47+
}
48+
49+
function mergeCaptureConfig(base: HarnessConfig, p: CapturePartial): HarnessConfig {
50+
return {
51+
...base,
52+
...p,
53+
root: { ...base.root, ...(p.root ?? {}) },
54+
compass: { ...base.compass, ...(p.compass ?? {}) },
55+
tile: { ...base.tile, ...(p.tile ?? {}) },
56+
};
57+
}
58+
2359
const MAX_LOG_ENTRIES = 200;
2460

2561
@customElement('acp-harness-app')
@@ -40,6 +76,7 @@ export class AcpHarnessApp extends LitElement {
4076
super.connectedCallback();
4177
this._rebuildHass();
4278
this._applyTheme();
79+
if (new URLSearchParams(location.search).has('capture')) this._installCaptureBridge();
4380
}
4481

4582
disconnectedCallback(): void {
@@ -99,6 +136,50 @@ export class AcpHarnessApp extends LitElement {
99136
}
100137
}
101138

139+
/** Expose `window.__acpCapture` so the time-lapse script can step time deterministically. */
140+
private _installCaptureBridge(): void {
141+
this._clearPlayInterval();
142+
const bridge: CaptureBridge = {
143+
listScenarios: () => SCENARIOS.map((s) => ({ id: s.id, label: s.label })),
144+
loadScenario: async (id) => {
145+
const sc = findScenario(id);
146+
if (!sc) throw new Error(`unknown scenario: ${id}`);
147+
this._config = normalizeConfig({ ...sc.build(), playing: false });
148+
await this._settle();
149+
},
150+
setConfig: async (partial) => {
151+
this._config = mergeCaptureConfig(this._config, { ...partial, playing: false });
152+
await this._settle();
153+
},
154+
setMinutes: async (minutes) => {
155+
this._config = { ...this._config, timeOfDayMinutes: minutes, playing: false };
156+
await this._settle();
157+
},
158+
};
159+
(window as unknown as { __acpCapture: CaptureBridge }).__acpCapture = bridge;
160+
}
161+
162+
/**
163+
* Wait for the full render chain to settle. Setting `_config` triggers a hass
164+
* rebuild in `updated()`, which schedules a *second* update — `updateComplete`
165+
* resolves `false` while another update is pending, so drain those first, then
166+
* await the nested card-stage and compass card, and finally a paint frame.
167+
*/
168+
private async _settle(): Promise<void> {
169+
for (let i = 0; i < 5; i++) {
170+
if (await this.updateComplete) break;
171+
}
172+
const stage = this.renderRoot.querySelector('acp-harness-card-stage') as LitElementType | null;
173+
if (stage) await stage.updateComplete;
174+
const compass = stage?.renderRoot?.querySelector(
175+
SKY_COMPASS_CARD_NAME,
176+
) as LitElementType | null;
177+
if (compass?.updateComplete) await compass.updateComplete;
178+
await new Promise<void>((resolve) =>
179+
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
180+
);
181+
}
182+
102183
private _onServiceCall = (e: ServiceCallEvent): void => {
103184
const { next, applied } = applyService(this._config, e.domain, e.service, e.data, e.target);
104185
const entry: ServiceCall = {

images/sky-compass-timelapse.gif

938 KB
Loading

package-lock.json

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"harness/**/*.ts\"",
3232
"test": "vitest run",
3333
"test:watch": "vitest",
34-
"typecheck": "tsc --noEmit"
34+
"typecheck": "tsc --noEmit",
35+
"capture:timelapse": "node scripts/capture-timelapse.mjs"
3536
},
3637
"dependencies": {
3738
"custom-card-helpers": "^2.0.0",
@@ -50,6 +51,7 @@
5051
"@typescript-eslint/parser": "^8.59.0",
5152
"eslint": "^10.2.1",
5253
"happy-dom": "^20.9.0",
54+
"playwright": "^1.49.0",
5355
"prettier": "^3.3.2",
5456
"rollup": "^4.18.0",
5557
"rollup-plugin-livereload": "^2.0.5",

0 commit comments

Comments
 (0)