feat(core): add readiness adapters for map and visualization libraries (#1548)

Add readiness-only runtime adapters for Mapbox GL JS, Leaflet, Google
Maps, MapLibre GL JS, and D3. Each adapter gates `__renderReady` until
the library's async initialization completes, preventing the renderer
from capturing blank or half-loaded frames.

Built on the `getReadyPromise` adapter contract from #1543. A shared
`createReadinessAdapter()` helper in `_readiness.ts` owns the
settled-tracking WeakSet, promise-identity stability, and
`Promise.allSettled` gate — each adapter provides only its type, window
global name, and `waitFor` callback.

Readiness signals per library:
- Mapbox / MapLibre: `map.loaded()` + `map.on('load', ...)`
- Leaflet: `map.whenReady(cb)`
- Google Maps: `map.addListener('tilesloaded', cb)` with handle cleanup
- D3: `transition.end()` promise

50 unit tests across 5 test files covering happy path, no-instances,
stable promise identity, post-settle drain, loaded-before-subscribe
race, and listener cleanup. 5 producer regression tests with
Docker-generated baselines for end-to-end render verification.
This commit is contained in:
Miguel Ángel
2026-06-18 01:21:27 -04:00
committed by GitHub
parent c8195799bc
commit ea6783c8ac
33 changed files with 2294 additions and 0 deletions
@@ -0,0 +1,36 @@
import type { RuntimeDeterministicAdapter } from "../types";
export function createReadinessAdapter<T extends object>(opts: {
name: string;
getInstances: () => T[];
waitFor: (instance: T) => PromiseLike<void>;
}): RuntimeDeterministicAdapter {
let pendingPromise: PromiseLike<void> | null = null;
const settled = new WeakSet<T>();
return {
name: opts.name,
discover: () => {},
seek: () => {},
pause: () => {},
play: () => {},
revert: () => {},
getReadyPromise: () => {
const instances = opts.getInstances();
if (instances.length === 0) return null;
const unsettled = instances.filter((i) => !settled.has(i));
if (unsettled.length === 0) return null;
if (pendingPromise) return pendingPromise;
pendingPromise = Promise.allSettled(
unsettled.map((i) =>
opts.waitFor(i).then(() => {
settled.add(i);
}),
),
).then(() => {
pendingPromise = null;
});
return pendingPromise;
},
};
}
@@ -0,0 +1,100 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createD3Adapter } from "./d3";
type D3TransitionLike = {
end: () => PromiseLike<void>;
};
const d3Window = window as Window & { __hfD3?: D3TransitionLike[] };
function createMockTransition(opts?: { resolved?: boolean }): D3TransitionLike {
let resolver: (() => void) | null = null;
return {
end: vi.fn(
() =>
new Promise<void>((resolve) => {
if (opts?.resolved) {
resolve();
} else {
resolver = resolve;
}
}),
),
_resolve() {
resolver?.();
},
} as D3TransitionLike & { _resolve: () => void };
}
describe("d3 adapter", () => {
beforeEach(() => {
delete d3Window.__hfD3;
});
afterEach(() => {
delete d3Window.__hfD3;
});
it("has correct name", () => {
expect(createD3Adapter().name).toBe("d3");
});
describe("getReadyPromise", () => {
it("returns null when no transitions registered", () => {
const adapter = createD3Adapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("returns null when __hfD3 is empty", () => {
d3Window.__hfD3 = [];
const adapter = createD3Adapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves when transition ends", async () => {
const t = createMockTransition() as D3TransitionLike & { _resolve: () => void };
d3Window.__hfD3 = [t];
const adapter = createD3Adapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
t._resolve();
await promise;
});
it("resolves immediately for already-resolved transition", async () => {
const t = createMockTransition({ resolved: true });
d3Window.__hfD3 = [t];
const adapter = createD3Adapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
await promise;
});
it("returns same promise on repeated calls (stable identity)", () => {
const t = createMockTransition();
d3Window.__hfD3 = [t];
const adapter = createD3Adapter();
const p1 = adapter.getReadyPromise!();
const p2 = adapter.getReadyPromise!();
expect(p1).toBe(p2);
});
it("returns null after all transitions have settled", async () => {
const t = createMockTransition({ resolved: true });
d3Window.__hfD3 = [t];
const adapter = createD3Adapter();
await adapter.getReadyPromise!();
expect(adapter.getReadyPromise!()).toBeNull();
});
});
it("discover is a no-op", () => {
const adapter = createD3Adapter();
expect(() => adapter.discover()).not.toThrow();
});
it("seek is a no-op", () => {
const adapter = createD3Adapter();
expect(() => adapter.seek({ time: 1 })).not.toThrow();
});
});
+17
View File
@@ -0,0 +1,17 @@
import { createReadinessAdapter } from "./_readiness";
type D3TransitionLike = {
end: () => PromiseLike<void>;
};
export function createD3Adapter() {
return createReadinessAdapter<D3TransitionLike>({
name: "d3",
getInstances: () => {
if (typeof window === "undefined") return [];
const arr = (window as { __hfD3?: D3TransitionLike[] }).__hfD3;
return Array.isArray(arr) ? arr : [];
},
waitFor: (t) => t.end(),
});
}
@@ -0,0 +1,111 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createGoogleMapsAdapter } from "./google-maps";
type GoogleMapLike = {
addListener: (event: string, cb: () => void) => { remove: () => void };
};
const mapWindow = window as Window & { __hfGoogleMaps?: GoogleMapLike[] };
function createMockMap(): GoogleMapLike & { _fire: (e: string) => void } {
const listeners: Record<string, { cb: () => void; handle: { remove: () => void } }[]> = {};
return {
addListener: vi.fn((event: string, cb: () => void) => {
const entry = { cb, handle: { remove: vi.fn() } };
(listeners[event] ??= []).push(entry);
return entry.handle;
}),
_fire(event: string) {
for (const entry of listeners[event] ?? []) entry.cb();
},
};
}
describe("google-maps adapter", () => {
beforeEach(() => {
delete mapWindow.__hfGoogleMaps;
});
afterEach(() => {
delete mapWindow.__hfGoogleMaps;
});
it("has correct name", () => {
expect(createGoogleMapsAdapter().name).toBe("google-maps");
});
describe("getReadyPromise", () => {
it("returns null when no maps registered", () => {
const adapter = createGoogleMapsAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("returns null when __hfGoogleMaps is empty", () => {
mapWindow.__hfGoogleMaps = [];
const adapter = createGoogleMapsAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves when map fires tilesloaded", async () => {
const map = createMockMap();
mapWindow.__hfGoogleMaps = [map];
const adapter = createGoogleMapsAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
map._fire("tilesloaded");
await promise;
});
it("returns same promise on repeated calls (stable identity)", () => {
const map = createMockMap();
mapWindow.__hfGoogleMaps = [map];
const adapter = createGoogleMapsAdapter();
const p1 = adapter.getReadyPromise!();
const p2 = adapter.getReadyPromise!();
expect(p1).toBe(p2);
});
it("returns null after all maps have settled", async () => {
const map = createMockMap();
mapWindow.__hfGoogleMaps = [map];
const adapter = createGoogleMapsAdapter();
const promise = adapter.getReadyPromise!();
map._fire("tilesloaded");
await promise;
expect(adapter.getReadyPromise!()).toBeNull();
});
it("removes listener after first tilesloaded fire", async () => {
const map = createMockMap();
mapWindow.__hfGoogleMaps = [map];
const adapter = createGoogleMapsAdapter();
const promise = adapter.getReadyPromise!();
map._fire("tilesloaded");
await promise;
const handle = (map.addListener as ReturnType<typeof vi.fn>).mock.results[0].value;
expect(handle.remove).toHaveBeenCalled();
});
it("handles multiple maps", async () => {
const map1 = createMockMap();
const map2 = createMockMap();
mapWindow.__hfGoogleMaps = [map1, map2];
const adapter = createGoogleMapsAdapter();
const promise = adapter.getReadyPromise!();
map1._fire("tilesloaded");
map2._fire("tilesloaded");
await promise;
expect(adapter.getReadyPromise!()).toBeNull();
});
});
it("discover is a no-op", () => {
const adapter = createGoogleMapsAdapter();
expect(() => adapter.discover()).not.toThrow();
});
it("seek is a no-op", () => {
const adapter = createGoogleMapsAdapter();
expect(() => adapter.seek({ time: 1 })).not.toThrow();
});
});
@@ -0,0 +1,23 @@
import { createReadinessAdapter } from "./_readiness";
type GoogleMapLike = {
addListener: (event: string, cb: () => void) => { remove: () => void };
};
export function createGoogleMapsAdapter() {
return createReadinessAdapter<GoogleMapLike>({
name: "google-maps",
getInstances: () => {
if (typeof window === "undefined") return [];
const arr = (window as { __hfGoogleMaps?: GoogleMapLike[] }).__hfGoogleMaps;
return Array.isArray(arr) ? arr : [];
},
waitFor: (m) =>
new Promise<void>((resolve) => {
const handle = m.addListener("tilesloaded", () => {
handle.remove();
resolve();
});
}),
});
}
@@ -0,0 +1,93 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createLeafletAdapter } from "./leaflet";
type LeafletMapLike = {
whenReady: (cb: () => void) => void;
};
const mapWindow = window as Window & { __hfLeaflet?: LeafletMapLike[] };
function createMockMap(opts?: { ready?: boolean }): LeafletMapLike {
return {
whenReady: vi.fn((cb: () => void) => {
if (opts?.ready ?? false) cb();
}),
_fireReady() {
const calls = (this.whenReady as ReturnType<typeof vi.fn>).mock.calls;
for (const [cb] of calls) cb();
},
} as LeafletMapLike & { _fireReady: () => void };
}
describe("leaflet adapter", () => {
beforeEach(() => {
delete mapWindow.__hfLeaflet;
});
afterEach(() => {
delete mapWindow.__hfLeaflet;
});
it("has correct name", () => {
expect(createLeafletAdapter().name).toBe("leaflet");
});
describe("getReadyPromise", () => {
it("returns null when no maps registered", () => {
const adapter = createLeafletAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("returns null when __hfLeaflet is empty", () => {
mapWindow.__hfLeaflet = [];
const adapter = createLeafletAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves when map fires whenReady callback", async () => {
const map = createMockMap() as LeafletMapLike & { _fireReady: () => void };
mapWindow.__hfLeaflet = [map];
const adapter = createLeafletAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
map._fireReady();
await promise;
});
it("resolves immediately for already-ready map", async () => {
const map = createMockMap({ ready: true });
mapWindow.__hfLeaflet = [map];
const adapter = createLeafletAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
await promise;
});
it("returns same promise on repeated calls (stable identity)", () => {
const map = createMockMap();
mapWindow.__hfLeaflet = [map];
const adapter = createLeafletAdapter();
const p1 = adapter.getReadyPromise!();
const p2 = adapter.getReadyPromise!();
expect(p1).toBe(p2);
});
it("returns null after all maps have settled", async () => {
const map = createMockMap({ ready: true });
mapWindow.__hfLeaflet = [map];
const adapter = createLeafletAdapter();
await adapter.getReadyPromise!();
expect(adapter.getReadyPromise!()).toBeNull();
});
});
it("discover is a no-op", () => {
const adapter = createLeafletAdapter();
expect(() => adapter.discover()).not.toThrow();
});
it("seek is a no-op", () => {
const adapter = createLeafletAdapter();
expect(() => adapter.seek({ time: 1 })).not.toThrow();
});
});
@@ -0,0 +1,17 @@
import { createReadinessAdapter } from "./_readiness";
type LeafletMapLike = {
whenReady: (cb: () => void) => void;
};
export function createLeafletAdapter() {
return createReadinessAdapter<LeafletMapLike>({
name: "leaflet",
getInstances: () => {
if (typeof window === "undefined") return [];
const arr = (window as { __hfLeaflet?: LeafletMapLike[] }).__hfLeaflet;
return Array.isArray(arr) ? arr : [];
},
waitFor: (m) => new Promise<void>((resolve) => m.whenReady(resolve)),
});
}
@@ -0,0 +1,120 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createMapboxAdapter } from "./mapbox";
type MapboxMapLike = {
loaded: () => boolean;
on: (event: string, cb: () => void) => void;
};
const mapWindow = window as Window & { __hfMapbox?: MapboxMapLike[] };
function createMockMap(opts?: { loaded?: boolean }): MapboxMapLike {
const listeners: Record<string, (() => void)[]> = {};
return {
loaded: vi.fn(() => opts?.loaded ?? false),
on: vi.fn((event: string, cb: () => void) => {
(listeners[event] ??= []).push(cb);
}),
_fire(event: string) {
for (const cb of listeners[event] ?? []) cb();
},
} as MapboxMapLike & { _fire: (e: string) => void };
}
describe("mapbox adapter", () => {
beforeEach(() => {
delete mapWindow.__hfMapbox;
});
afterEach(() => {
delete mapWindow.__hfMapbox;
});
it("has correct name", () => {
expect(createMapboxAdapter().name).toBe("mapbox");
});
describe("getReadyPromise", () => {
it("returns null when no maps registered", () => {
const adapter = createMapboxAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("returns null when __hfMapbox is empty", () => {
mapWindow.__hfMapbox = [];
const adapter = createMapboxAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves when map fires load event", async () => {
const map = createMockMap() as MapboxMapLike & { _fire: (e: string) => void };
mapWindow.__hfMapbox = [map];
const adapter = createMapboxAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
map._fire("load");
await promise;
});
it("resolves immediately for already-loaded map", async () => {
const map = createMockMap({ loaded: true });
mapWindow.__hfMapbox = [map];
const adapter = createMapboxAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
await promise;
});
it("returns same promise on repeated calls (stable identity)", () => {
const map = createMockMap();
mapWindow.__hfMapbox = [map];
const adapter = createMapboxAdapter();
const p1 = adapter.getReadyPromise!();
const p2 = adapter.getReadyPromise!();
expect(p1).toBe(p2);
});
it("returns null after all maps have settled", async () => {
const map = createMockMap({ loaded: true });
mapWindow.__hfMapbox = [map];
const adapter = createMapboxAdapter();
await adapter.getReadyPromise!();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("handles mix of loaded and unloaded maps", async () => {
const loaded = createMockMap({ loaded: true });
const unloaded = createMockMap() as MapboxMapLike & { _fire: (e: string) => void };
mapWindow.__hfMapbox = [loaded, unloaded];
const adapter = createMapboxAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
unloaded._fire("load");
await promise;
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves via loaded() check when map finishes loading before subscribe", async () => {
const racyMap = {
loaded: vi.fn(() => true),
on: vi.fn(),
};
mapWindow.__hfMapbox = [racyMap];
const adapter = createMapboxAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
await promise;
expect(racyMap.on).not.toHaveBeenCalled();
});
});
it("discover is a no-op", () => {
const adapter = createMapboxAdapter();
expect(() => adapter.discover()).not.toThrow();
});
it("seek is a no-op", () => {
const adapter = createMapboxAdapter();
expect(() => adapter.seek({ time: 1 })).not.toThrow();
});
});
@@ -0,0 +1,26 @@
import { createReadinessAdapter } from "./_readiness";
type MapboxMapLike = {
loaded: () => boolean;
on: (event: string, cb: () => void) => void;
};
// 'load' = style + sources ready (not tile-level 'idle'); sufficient for render-ready gate
export function createMapboxAdapter() {
return createReadinessAdapter<MapboxMapLike>({
name: "mapbox",
getInstances: () => {
if (typeof window === "undefined") return [];
const arr = (window as { __hfMapbox?: MapboxMapLike[] }).__hfMapbox;
return Array.isArray(arr) ? arr : [];
},
waitFor: (m) =>
new Promise<void>((resolve) => {
if (m.loaded()) {
resolve();
return;
}
m.on("load", resolve);
}),
});
}
@@ -0,0 +1,122 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createMaplibreAdapter } from "./maplibre";
type MaplibreMapLike = {
loaded: () => boolean;
on: (event: string, cb: () => void) => void;
};
const mapWindow = window as Window & { __hfMaplibre?: MaplibreMapLike[] };
function createMockMap(opts?: {
loaded?: boolean;
}): MaplibreMapLike & { _fire: (e: string) => void } {
const listeners: Record<string, (() => void)[]> = {};
return {
loaded: vi.fn(() => opts?.loaded ?? false),
on: vi.fn((event: string, cb: () => void) => {
(listeners[event] ??= []).push(cb);
}),
_fire(event: string) {
for (const cb of listeners[event] ?? []) cb();
},
};
}
describe("maplibre adapter", () => {
beforeEach(() => {
delete mapWindow.__hfMaplibre;
});
afterEach(() => {
delete mapWindow.__hfMaplibre;
});
it("has correct name", () => {
expect(createMaplibreAdapter().name).toBe("maplibre");
});
describe("getReadyPromise", () => {
it("returns null when no maps registered", () => {
const adapter = createMaplibreAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("returns null when __hfMaplibre is empty", () => {
mapWindow.__hfMaplibre = [];
const adapter = createMaplibreAdapter();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves when map fires load event", async () => {
const map = createMockMap();
mapWindow.__hfMaplibre = [map];
const adapter = createMaplibreAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
map._fire("load");
await promise;
});
it("resolves immediately for already-loaded map", async () => {
const map = createMockMap({ loaded: true });
mapWindow.__hfMaplibre = [map];
const adapter = createMaplibreAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
await promise;
});
it("returns same promise on repeated calls (stable identity)", () => {
const map = createMockMap();
mapWindow.__hfMaplibre = [map];
const adapter = createMaplibreAdapter();
const p1 = adapter.getReadyPromise!();
const p2 = adapter.getReadyPromise!();
expect(p1).toBe(p2);
});
it("returns null after all maps have settled", async () => {
const map = createMockMap({ loaded: true });
mapWindow.__hfMaplibre = [map];
const adapter = createMaplibreAdapter();
await adapter.getReadyPromise!();
expect(adapter.getReadyPromise!()).toBeNull();
});
it("handles mix of loaded and unloaded maps", async () => {
const loaded = createMockMap({ loaded: true });
const unloaded = createMockMap();
mapWindow.__hfMaplibre = [loaded, unloaded];
const adapter = createMaplibreAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
unloaded._fire("load");
await promise;
expect(adapter.getReadyPromise!()).toBeNull();
});
it("resolves via loaded() check when map finishes loading before subscribe", async () => {
const racyMap = {
loaded: vi.fn(() => true),
on: vi.fn(),
};
mapWindow.__hfMaplibre = [racyMap];
const adapter = createMaplibreAdapter();
const promise = adapter.getReadyPromise!();
expect(promise).not.toBeNull();
await promise;
expect(racyMap.on).not.toHaveBeenCalled();
});
});
it("discover is a no-op", () => {
const adapter = createMaplibreAdapter();
expect(() => adapter.discover()).not.toThrow();
});
it("seek is a no-op", () => {
const adapter = createMaplibreAdapter();
expect(() => adapter.seek({ time: 1 })).not.toThrow();
});
});
@@ -0,0 +1,26 @@
import { createReadinessAdapter } from "./_readiness";
type MaplibreMapLike = {
loaded: () => boolean;
on: (event: string, cb: () => void) => void;
};
// 'load' = style + sources ready; same contract as Mapbox GL JS
export function createMaplibreAdapter() {
return createReadinessAdapter<MaplibreMapLike>({
name: "maplibre",
getInstances: () => {
if (typeof window === "undefined") return [];
const arr = (window as { __hfMaplibre?: MaplibreMapLike[] }).__hfMaplibre;
return Array.isArray(arr) ? arr : [];
},
waitFor: (m) =>
new Promise<void>((resolve) => {
if (m.loaded()) {
resolve();
return;
}
m.on("load", resolve);
}),
});
}
+10
View File
@@ -6,6 +6,11 @@ import { createGsapAdapter } from "./adapters/gsap";
import { createAnimeJsAdapter } from "./adapters/animejs";
import { createLottieAdapter } from "./adapters/lottie";
import { createThreeAdapter } from "./adapters/three";
import { createMapboxAdapter } from "./adapters/mapbox";
import { createLeafletAdapter } from "./adapters/leaflet";
import { createGoogleMapsAdapter } from "./adapters/google-maps";
import { createMaplibreAdapter } from "./adapters/maplibre";
import { createD3Adapter } from "./adapters/d3";
import { createTypegpuAdapter } from "./adapters/typegpu";
import {
patchVideoTextureCompat,
@@ -1913,6 +1918,11 @@ export function initSandboxRuntimeModular(): void {
createAnimeJsAdapter(),
createLottieAdapter(),
createThreeAdapter(),
createMapboxAdapter(),
createLeafletAdapter(),
createGoogleMapsAdapter(),
createMaplibreAdapter(),
createD3Adapter(),
createTypegpuAdapter(),
createGsapAdapter({ getTimeline: () => state.capturedTimeline }),
] as RuntimeDeterministicAdapter[];
+30
View File
@@ -102,6 +102,36 @@ declare global {
* window.__hfLottie.push(anim);
*/
__hfLottie?: unknown[];
/**
* Mapbox GL JS map instances. Push your map here after creating it:
* window.__hfMapbox = window.__hfMapbox || [];
* window.__hfMapbox.push(map);
*/
__hfMapbox?: unknown[];
/**
* Leaflet map instances. Push your map here after creating it:
* window.__hfLeaflet = window.__hfLeaflet || [];
* window.__hfLeaflet.push(map);
*/
__hfLeaflet?: unknown[];
/**
* Google Maps instances. Push your map here after creating it:
* window.__hfGoogleMaps = window.__hfGoogleMaps || [];
* window.__hfGoogleMaps.push(map);
*/
__hfGoogleMaps?: unknown[];
/**
* MapLibre GL JS map instances. Push your map here after creating it:
* window.__hfMaplibre = window.__hfMaplibre || [];
* window.__hfMaplibre.push(map);
*/
__hfMaplibre?: unknown[];
/**
* D3 transition instances. Push your transition here after creating it:
* window.__hfD3 = window.__hfD3 || [];
* window.__hfD3.push(transition);
*/
__hfD3?: unknown[];
/**
* Render-time variable overrides injected by the engine when the user
* passes `hyperframes render --variables '<json>'`. Read indirectly via
@@ -0,0 +1,13 @@
{
"name": "d3-adapter",
"description": "Regression guard for the D3.js runtime adapter. Verifies that D3 transitions with an SVG bar chart register on window.__hfD3 and render deterministically via the adapter bridge.",
"tags": ["regression", "adapter"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0693b24a6fad50609f05c4b5ea103a4e533113703725e64d9244435a1b0dbdcc
size 250658
@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; background: #111827; }
[data-composition-id="d3-test"] { position: relative; overflow: hidden; font-family: 'Segoe UI', system-ui, sans-serif; }
#chart-container {
position: absolute; top: 0; left: 0; width: 1920px; height: 1080px;
display: flex; align-items: center; justify-content: center;
}
#label {
position: absolute; top: 36px; left: 48px; z-index: 10;
color: #f9fafb; font-size: 32px; font-weight: 700;
background: rgba(17, 24, 39, 0.9); padding: 12px 24px; border-radius: 10px;
border: 2px solid #8b5cf6; opacity: 0;
}
#subtitle {
position: absolute; top: 100px; left: 48px; z-index: 10;
color: #9ca3af; font-size: 20px;
opacity: 0;
}
.axis text { fill: #9ca3af; font-size: 14px; }
.axis line, .axis path { stroke: #374151; }
.grid line { stroke: #1f2937; stroke-dasharray: 2,4; }
</style>
</head>
<body>
<div id="root" data-composition-id="d3-test" data-width="1920" data-height="1080" data-start="0" data-duration="4">
<div id="chart-container"></div>
<div id="label">D3 adapter test</div>
<div id="subtitle">Animated bar chart with transitions</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
window.__hfD3 = window.__hfD3 || [];
// Chart dimensions
var margin = { top: 160, right: 120, bottom: 100, left: 120 };
var width = 1920 - margin.left - margin.right;
var height = 1080 - margin.top - margin.bottom;
// Data
var data = [
{ label: 'React', value: 92, color: '#61dafb' },
{ label: 'Vue', value: 78, color: '#42b883' },
{ label: 'Svelte', value: 65, color: '#ff3e00' },
{ label: 'Angular', value: 58, color: '#dd0031' },
{ label: 'Solid', value: 45, color: '#4f88c6' },
{ label: 'Qwik', value: 38, color: '#18b6f6' },
{ label: 'Astro', value: 72, color: '#bc52ee' },
{ label: 'Next', value: 85, color: '#fff' },
];
// Create SVG
var svg = d3.select('#chart-container')
.append('svg')
.attr('width', 1920)
.attr('height', 1080)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Gradient definitions
var defs = svg.append('defs');
data.forEach(function(d, i) {
var grad = defs.append('linearGradient')
.attr('id', 'grad-' + i)
.attr('x1', '0%').attr('y1', '100%')
.attr('x2', '0%').attr('y2', '0%');
grad.append('stop').attr('offset', '0%').attr('stop-color', d.color).attr('stop-opacity', 0.6);
grad.append('stop').attr('offset', '100%').attr('stop-color', d.color).attr('stop-opacity', 1);
});
// Scales
var x = d3.scaleBand()
.domain(data.map(function(d) { return d.label; }))
.range([0, width])
.padding(0.25);
var y = d3.scaleLinear()
.domain([0, 100])
.range([height, 0]);
// Grid lines
svg.append('g')
.attr('class', 'grid')
.selectAll('line')
.data(y.ticks(5))
.enter().append('line')
.attr('x1', 0).attr('x2', width)
.attr('y1', function(d) { return y(d); })
.attr('y2', function(d) { return y(d); });
// X axis
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x).tickSize(0))
.selectAll('text')
.style('font-size', '18px')
.style('fill', '#d1d5db');
// Y axis
svg.append('g')
.attr('class', 'axis')
.call(d3.axisLeft(y).ticks(5).tickSize(-8))
.selectAll('text')
.style('font-size', '16px');
// Bars — start at zero height, transition to final values
var bars = svg.selectAll('.bar')
.data(data)
.enter().append('rect')
.attr('class', 'bar')
.attr('x', function(d) { return x(d.label); })
.attr('y', height)
.attr('width', x.bandwidth())
.attr('height', 0)
.attr('rx', 6)
.attr('fill', function(d, i) { return 'url(#grad-' + i + ')'; });
// Value labels — start hidden
var labels = svg.selectAll('.value-label')
.data(data)
.enter().append('text')
.attr('class', 'value-label')
.attr('x', function(d) { return x(d.label) + x.bandwidth() / 2; })
.attr('y', height)
.attr('text-anchor', 'middle')
.attr('fill', '#f9fafb')
.attr('font-size', '20px')
.attr('font-weight', '600')
.attr('opacity', 0)
.text(function(d) { return d.value; });
// D3 transition: bars grow from bottom to their value
var barTransition = bars.transition()
.duration(1200)
.delay(function(d, i) { return i * 80; })
.attr('y', function(d) { return y(d.value); })
.attr('height', function(d) { return height - y(d.value); })
.ease(d3.easeCubicOut);
// D3 transition: value labels appear
var labelTransition = labels.transition()
.duration(600)
.delay(function(d, i) { return 600 + i * 80; })
.attr('y', function(d) { return y(d.value) - 12; })
.attr('opacity', 1)
.ease(d3.easeCubicOut);
// Register transitions for the adapter
// D3 transitions have .end() returning a Promise
window.__hfD3 = [barTransition, labelTransition];
// GSAP timeline for overlay labels
var compId = 'd3-test';
var tl = gsap.timeline({ paused: true });
tl.to('#label', { opacity: 1, duration: 0.5, ease: 'power3.out' }, 0.2)
.from('#label', { y: -20, duration: 0.5, ease: 'power3.out' }, 0.2)
.to('#subtitle', { opacity: 1, duration: 0.4, ease: 'power2.out' }, 0.6)
.from('#subtitle', { x: -15, duration: 0.4, ease: 'power2.out' }, 0.6);
window.__timelines[compId] = tl;
</script>
</div>
</body>
</html>
@@ -0,0 +1,13 @@
{
"name": "google-maps-adapter",
"description": "Regression guard for the Google Maps runtime adapter. Uses a mock Google Maps object with procedural canvas visualization to verify the __hfGoogleMaps adapter bridge without requiring an API key.",
"tags": ["regression", "adapter"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5a9fe32ca9852e9702e387332f9bc871314d2048fc19cd544db73fb169d18ae0
size 155771
@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; background: #f0f4f8; }
[data-composition-id="google-maps-test"] { position: relative; overflow: hidden; font-family: 'Segoe UI', system-ui, sans-serif; }
#map-canvas { position: absolute; top: 0; left: 0; }
#label {
position: absolute; top: 36px; left: 48px; z-index: 10;
color: #1a73e8; font-size: 32px; font-weight: 700;
background: rgba(255, 255, 255, 0.95); padding: 12px 24px; border-radius: 10px;
border: 2px solid #1a73e8; opacity: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
#badge {
position: absolute; bottom: 36px; left: 48px; z-index: 10;
color: #fff; font-size: 16px; font-weight: 600; font-family: monospace;
background: #34a853; padding: 8px 16px; border-radius: 20px;
opacity: 0;
}
</style>
</head>
<body>
<div id="root" data-composition-id="google-maps-test" data-width="1920" data-height="1080" data-start="0" data-duration="4">
<canvas id="map-canvas" width="1920" height="1080"></canvas>
<div id="label">Google Maps adapter test</div>
<div id="badge">tilesloaded</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
window.__hfGoogleMaps = window.__hfGoogleMaps || [];
// Draw procedural road-network visualization
var canvas = document.getElementById('map-canvas');
var ctx = canvas.getContext('2d');
// Background — light map style
ctx.fillStyle = '#e8f0e8';
ctx.fillRect(0, 0, 1920, 1080);
// Water body
ctx.fillStyle = '#aad3df';
ctx.beginPath();
ctx.moveTo(1400, 0);
ctx.bezierCurveTo(1300, 200, 1500, 400, 1350, 600);
ctx.bezierCurveTo(1200, 800, 1450, 900, 1500, 1080);
ctx.lineTo(1920, 1080);
ctx.lineTo(1920, 0);
ctx.closePath();
ctx.fill();
// Parks / green areas
var parks = [
{ x: 200, y: 150, w: 240, h: 180 },
{ x: 700, y: 500, w: 300, h: 200 },
{ x: 100, y: 700, w: 180, h: 150 },
];
ctx.fillStyle = '#c8e6c9';
for (var p = 0; p < parks.length; p++) {
ctx.beginPath();
ctx.roundRect(parks[p].x, parks[p].y, parks[p].w, parks[p].h, 8);
ctx.fill();
}
// Major roads (thick)
ctx.strokeStyle = '#fff';
ctx.lineWidth = 12;
ctx.lineCap = 'round';
// Horizontal highways
var highways = [
[[0, 300], [500, 300], [800, 280], [1100, 320], [1350, 300]],
[[0, 700], [400, 680], [700, 720], [1000, 700], [1350, 710]],
];
for (var h = 0; h < highways.length; h++) {
ctx.beginPath();
ctx.moveTo(highways[h][0][0], highways[h][0][1]);
for (var s = 1; s < highways[h].length; s++) {
ctx.lineTo(highways[h][s][0], highways[h][s][1]);
}
ctx.stroke();
}
// Vertical roads
ctx.lineWidth = 8;
ctx.strokeStyle = '#f5f5f5';
var vertRoads = [300, 600, 900, 1200];
for (var v = 0; v < vertRoads.length; v++) {
ctx.beginPath();
ctx.moveTo(vertRoads[v], 0);
ctx.lineTo(vertRoads[v] + (v % 2 === 0 ? 30 : -20), 540);
ctx.lineTo(vertRoads[v], 1080);
ctx.stroke();
}
// Secondary roads (thin grid)
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 3;
for (var gy = 100; gy < 1080; gy += 120) {
ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(1350, gy); ctx.stroke();
}
// Road outlines on highways
ctx.strokeStyle = '#fbbc04';
ctx.lineWidth = 2;
for (var h2 = 0; h2 < highways.length; h2++) {
ctx.beginPath();
ctx.moveTo(highways[h2][0][0], highways[h2][0][1]);
for (var s2 = 1; s2 < highways[h2].length; s2++) {
ctx.lineTo(highways[h2][s2][0], highways[h2][s2][1]);
}
ctx.stroke();
}
// POI markers (Google Maps red pins)
var pins = [
[450, 450], [750, 350], [350, 600], [950, 500], [1100, 200], [550, 800]
];
for (var i = 0; i < pins.length; i++) {
// Pin shadow
ctx.beginPath();
ctx.ellipse(pins[i][0], pins[i][1] + 18, 8, 4, 0, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fill();
// Pin body
ctx.beginPath();
ctx.arc(pins[i][0], pins[i][1] - 8, 12, 0, Math.PI * 2);
ctx.fillStyle = '#ea4335';
ctx.fill();
ctx.strokeStyle = '#b31412';
ctx.lineWidth = 2;
ctx.stroke();
// Pin point
ctx.beginPath();
ctx.moveTo(pins[i][0] - 7, pins[i][1] + 2);
ctx.lineTo(pins[i][0], pins[i][1] + 18);
ctx.lineTo(pins[i][0] + 7, pins[i][1] + 2);
ctx.fillStyle = '#ea4335';
ctx.fill();
// Pin dot
ctx.beginPath();
ctx.arc(pins[i][0], pins[i][1] - 8, 5, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
}
// Canvas label
ctx.font = 'bold 20px sans-serif';
ctx.fillStyle = 'rgba(26, 115, 232, 0.5)';
ctx.fillText('MOCK GOOGLE MAPS', 1500, 1050);
// Mock Google Maps object
var tilesCallbacks = [];
var tilesLoaded = false;
var mockGoogleMap = {
addListener: function(event, cb) {
if (event === 'tilesloaded') {
if (tilesLoaded) { cb(); }
else { tilesCallbacks.push(cb); }
}
return { remove: function() {} };
},
getCenter: function() { return { lat: function() { return 37.7749; }, lng: function() { return -122.4194; } }; },
getZoom: function() { return 12; }
};
// Simulate async tilesloaded
setTimeout(function() {
tilesLoaded = true;
for (var k = 0; k < tilesCallbacks.length; k++) {
tilesCallbacks[k]();
}
}, 150);
// Register for adapter
window.__hfGoogleMaps = [mockGoogleMap];
// GSAP timeline
var compId = 'google-maps-test';
var tl = gsap.timeline({ paused: true });
tl.to('#label', { opacity: 1, duration: 0.5, ease: 'power3.out' }, 0.3)
.from('#label', { scale: 0.9, duration: 0.5, ease: 'power3.out' }, 0.3)
.to('#badge', { opacity: 1, duration: 0.4, ease: 'back.out(1.5)' }, 1.0)
.from('#badge', { scale: 0, duration: 0.4, ease: 'back.out(1.5)' }, 1.0);
window.__timelines[compId] = tl;
</script>
</div>
</body>
</html>
@@ -0,0 +1,13 @@
{
"name": "leaflet-adapter",
"description": "Regression guard for the Leaflet runtime adapter. Verifies that a real Leaflet map with OSM tiles initializes and registers on window.__hfLeaflet for deterministic frame capture.",
"tags": ["regression", "adapter"],
"minPsnr": 25,
"maxFrameFailures": 10,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11742cc93d05dde833246b259fe25fe93cde7f3d15c9907fa6838a57852822df
size 86363
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<style>
body { margin: 0; background: #0a1628; }
[data-composition-id="leaflet-test"] { position: relative; overflow: hidden; font-family: 'Segoe UI', system-ui, sans-serif; }
#map { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; }
#label {
position: absolute; top: 36px; left: 48px; z-index: 1000;
color: #fff; font-size: 32px; font-weight: 700;
background: rgba(10, 22, 40, 0.85); padding: 12px 24px; border-radius: 10px;
border: 2px solid #4ecdc4; opacity: 0;
}
#coords {
position: absolute; bottom: 36px; left: 48px; z-index: 1000;
color: #a0d2db; font-size: 20px; font-family: monospace;
background: rgba(10, 22, 40, 0.8); padding: 8px 16px; border-radius: 6px;
opacity: 0;
}
</style>
</head>
<body>
<div id="root" data-composition-id="leaflet-test" data-width="1920" data-height="1080" data-start="0" data-duration="4">
<div id="map"></div>
<div id="label">Leaflet adapter test</div>
<div id="coords">37.7749, -122.4194 — San Francisco</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script>
window.__timelines = window.__timelines || {};
window.__hfLeaflet = window.__hfLeaflet || [];
// Initialize Leaflet map
var map = L.map('map', {
center: [37.7749, -122.4194],
zoom: 12,
zoomControl: false,
attributionControl: false
});
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19
}).addTo(map);
// Marker at San Francisco center
L.marker([37.7749, -122.4194]).addTo(map);
// Register for the adapter
window.__hfLeaflet = [map];
// GSAP timeline for label animations
var compId = 'leaflet-test';
var tl = gsap.timeline({ paused: true });
tl.to('#label', { opacity: 1, y: 0, duration: 0.6, ease: 'power3.out' }, 0.3)
.from('#label', { y: -20, duration: 0.6, ease: 'power3.out' }, 0.3)
.to('#coords', { opacity: 1, duration: 0.5, ease: 'power2.out' }, 0.8)
.from('#coords', { y: 10, duration: 0.5, ease: 'power2.out' }, 0.8);
window.__timelines[compId] = tl;
</script>
</div>
</body>
</html>
@@ -0,0 +1,13 @@
{
"name": "mapbox-adapter",
"description": "Regression guard for the Mapbox GL JS runtime adapter. Uses a mock Mapbox map with procedural canvas visualization to verify the __hfMapbox adapter bridge without requiring an API key.",
"tags": ["regression", "adapter"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf5c77b49eb2a7d0396b2798e872a4a6708dbe8c24be6c81afa896d86a1f7886
size 213883
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; background: #1a1a2e; }
[data-composition-id="mapbox-test"] { position: relative; overflow: hidden; font-family: 'Segoe UI', system-ui, sans-serif; }
#map-canvas { position: absolute; top: 0; left: 0; }
#label {
position: absolute; top: 36px; left: 48px; z-index: 10;
color: #fff; font-size: 32px; font-weight: 700;
background: rgba(26, 26, 46, 0.9); padding: 12px 24px; border-radius: 10px;
border: 2px solid #e94560; opacity: 0;
}
#status {
position: absolute; bottom: 36px; right: 48px; z-index: 10;
color: #16213e; font-size: 18px; font-weight: 600; font-family: monospace;
background: #0f3460; padding: 8px 16px; border-radius: 6px;
opacity: 0; color: #e94560;
}
</style>
</head>
<body>
<div id="root" data-composition-id="mapbox-test" data-width="1920" data-height="1080" data-start="0" data-duration="4">
<canvas id="map-canvas" width="1920" height="1080"></canvas>
<div id="label">Mapbox adapter test</div>
<div id="status">mock: loaded</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
window.__hfMapbox = window.__hfMapbox || [];
// Draw procedural map visualization on canvas
var canvas = document.getElementById('map-canvas');
var ctx = canvas.getContext('2d');
// Background gradient
var grad = ctx.createLinearGradient(0, 0, 1920, 1080);
grad.addColorStop(0, '#16213e');
grad.addColorStop(0.5, '#0f3460');
grad.addColorStop(1, '#1a1a2e');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 1920, 1080);
// Grid lines (like a map grid)
ctx.strokeStyle = 'rgba(233, 69, 96, 0.15)';
ctx.lineWidth = 1;
for (var x = 0; x < 1920; x += 60) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 1080); ctx.stroke();
}
for (var y = 0; y < 1080; y += 60) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(1920, y); ctx.stroke();
}
// Colored regions (simulating map zones)
var regions = [
{ x: 120, y: 180, w: 360, h: 300, color: 'rgba(233, 69, 96, 0.25)' },
{ x: 540, y: 120, w: 420, h: 240, color: 'rgba(15, 52, 96, 0.5)' },
{ x: 1020, y: 300, w: 480, h: 360, color: 'rgba(233, 69, 96, 0.15)' },
{ x: 300, y: 540, w: 540, h: 300, color: 'rgba(22, 33, 62, 0.6)' },
{ x: 900, y: 660, w: 600, h: 240, color: 'rgba(233, 69, 96, 0.2)' },
{ x: 1500, y: 120, w: 300, h: 480, color: 'rgba(15, 52, 96, 0.4)' },
];
for (var i = 0; i < regions.length; i++) {
var r = regions[i];
ctx.fillStyle = r.color;
ctx.fillRect(r.x, r.y, r.w, r.h);
ctx.strokeStyle = 'rgba(233, 69, 96, 0.4)';
ctx.lineWidth = 2;
ctx.strokeRect(r.x, r.y, r.w, r.h);
}
// Dots simulating POIs
var pois = [
[240, 330], [480, 240], [720, 200], [1200, 480], [1080, 420],
[600, 660], [840, 780], [1380, 300], [1560, 360], [360, 720]
];
for (var j = 0; j < pois.length; j++) {
ctx.beginPath();
ctx.arc(pois[j][0], pois[j][1], 8, 0, Math.PI * 2);
ctx.fillStyle = '#e94560';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
// Label on canvas
ctx.font = 'bold 24px monospace';
ctx.fillStyle = 'rgba(233, 69, 96, 0.6)';
ctx.fillText('MOCK MAPBOX GL', 1400, 1040);
// Mock Mapbox map object
var loadCallbacks = [];
var isLoaded = false;
var mockMap = {
loaded: function() { return isLoaded; },
on: function(event, cb) {
if (event === 'load') {
if (isLoaded) { cb(); }
else { loadCallbacks.push(cb); }
}
},
getCenter: function() { return { lng: -122.4194, lat: 37.7749 }; },
getZoom: function() { return 12; }
};
// Simulate async load
setTimeout(function() {
isLoaded = true;
for (var k = 0; k < loadCallbacks.length; k++) {
loadCallbacks[k]();
}
}, 100);
// Register for adapter
window.__hfMapbox = [mockMap];
// GSAP timeline
var compId = 'mapbox-test';
var tl = gsap.timeline({ paused: true });
tl.to('#label', { opacity: 1, duration: 0.5, ease: 'power2.out' }, 0.2)
.from('#label', { y: -25, duration: 0.5, ease: 'power2.out' }, 0.2)
.to('#status', { opacity: 1, duration: 0.4, ease: 'power2.out' }, 0.9)
.from('#status', { x: 20, duration: 0.4, ease: 'power2.out' }, 0.9);
window.__timelines[compId] = tl;
</script>
</div>
</body>
</html>
@@ -0,0 +1,13 @@
{
"name": "maplibre-adapter",
"description": "Regression guard for the MapLibre GL JS runtime adapter. Verifies that a real MapLibre map with demo tiles initializes and registers on window.__hfMaplibre for deterministic frame capture.",
"tags": ["regression", "adapter"],
"minPsnr": 25,
"maxFrameFailures": 10,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:67f116ec5e15cb460d2926fd55ba1195cde6d05e519c6fe55fecc098ea966325
size 98887
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" />
<style>
body { margin: 0; background: #0d1117; }
[data-composition-id="maplibre-test"] { position: relative; overflow: hidden; font-family: 'Segoe UI', system-ui, sans-serif; }
#map { position: absolute; top: 0; left: 0; width: 1920px; height: 1080px; }
#label {
position: absolute; top: 36px; right: 48px; z-index: 10;
color: #fff; font-size: 32px; font-weight: 700;
background: rgba(13, 17, 23, 0.9); padding: 12px 24px; border-radius: 10px;
border: 2px solid #58a6ff; opacity: 0;
}
#coords {
position: absolute; bottom: 36px; right: 48px; z-index: 10;
color: #8b949e; font-size: 20px; font-family: monospace;
background: rgba(13, 17, 23, 0.85); padding: 8px 16px; border-radius: 6px;
opacity: 0;
}
</style>
</head>
<body>
<div id="root" data-composition-id="maplibre-test" data-width="1920" data-height="1080" data-start="0" data-duration="4">
<div id="map"></div>
<div id="label">MapLibre adapter test</div>
<div id="coords">51.5074, -0.1278 — London</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script>
<script>
window.__timelines = window.__timelines || {};
window.__hfMaplibre = window.__hfMaplibre || [];
// Initialize MapLibre map
var map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [-0.1278, 51.5074],
zoom: 10,
attributionControl: false
});
// Register for adapter
window.__hfMaplibre = [map];
// GSAP timeline
var compId = 'maplibre-test';
var tl = gsap.timeline({ paused: true });
tl.to('#label', { opacity: 1, duration: 0.6, ease: 'power3.out' }, 0.4)
.from('#label', { x: 30, duration: 0.6, ease: 'power3.out' }, 0.4)
.to('#coords', { opacity: 1, duration: 0.5, ease: 'power2.out' }, 1.0)
.from('#coords', { y: 10, duration: 0.5, ease: 'power2.out' }, 1.0);
window.__timelines[compId] = tl;
</script>
</div>
</body>
</html>