Skip to content
Merged
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
47 changes: 32 additions & 15 deletions playground/play.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@
</a>
<a
id="feedback"
href="https://github.com/posecode-dev/posecode/issues/new"
href="https://github.com/posecode-dev/posecode"
class="btn ghost"
target="_blank"
rel="noreferrer"
aria-label="Report an issue on GitHub"
title="Report an issue on GitHub"
aria-label="Open the Posecode repository on GitHub"
title="Open the Posecode repository on GitHub"
>
<span class="ico-github" aria-hidden="true"></span><span class="lbl">GitHub</span>
</a>
Expand All @@ -121,21 +121,38 @@
</button>
<div class="tb-downloads">
<button
id="download-bvh"
id="export-menu-button"
class="btn ghost"
aria-label="Download BVH"
title="Download the movement as a .bvh motion file for Blender and other tools"
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="export-menu"
>
<span class="lbl" aria-live="polite">Download BVH</span>
</button>
<button
id="download-gltf"
class="btn ghost"
aria-label="Download glTF"
title="Download the movement as a .glb glTF asset (rig + animation) for Three.js and web pipelines"
>
<span class="lbl" aria-live="polite">Download glTF</span>
<span class="lbl" aria-live="polite">Export</span>
<span class="export-caret" aria-hidden="true"></span>
</button>
<div id="export-menu" class="export-menu" role="menu" hidden>
<button
id="download-bvh"
class="export-option"
type="button"
role="menuitem"
title="Download the movement as a .bvh motion file for Blender and other tools"
>
<span>BVH</span>
<small>For Blender and animation tools</small>
</button>
<button
id="download-gltf"
class="export-option"
type="button"
role="menuitem"
title="Download the movement as a .glb glTF asset (rig + animation) for Three.js and web pipelines"
>
<span>glTF / GLB</span>
<small>For Three.js and web pipelines</small>
</button>
</div>
</div>
<button
id="copy-prompt"
Expand Down
62 changes: 51 additions & 11 deletions playground/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const floorGuideTravel = $<HTMLSpanElement>("floor-guide-travel");
const floorGuideReset = $<HTMLSpanElement>("floor-guide-reset");
const copyBtn = $<HTMLButtonElement>("copy-prompt");
const shareBtn = $<HTMLButtonElement>("share");
const exportMenuButton = $<HTMLButtonElement>("export-menu-button");
const exportMenu = $<HTMLDivElement>("export-menu");
const downloadBvhBtn = $<HTMLButtonElement>("download-bvh");
const downloadGltfBtn = $<HTMLButtonElement>("download-gltf");
const tabEditor = $<HTMLButtonElement>("tab-editor");
Expand Down Expand Up @@ -702,10 +704,10 @@ async function downloadBvh(): Promise<void> {
const source = editorApi.getValue();
const { ir, errors } = parse(source);
if (!ir || errors.length > 0) {
flash(downloadBvhBtn, "Fix errors first", "error");
flash(exportMenuButton, "Fix errors first", "error");
return;
}
flash(downloadBvhBtn, "Exporting…", "pending", 0);
flash(exportMenuButton, "Exporting…", "pending", 0);
try {
const { exportBVH } = await import("posecode-render");
const bvh = exportBVH(ir);
Expand All @@ -718,12 +720,11 @@ async function downloadBvh(): Promise<void> {
a.click();
a.remove();
URL.revokeObjectURL(url);
flash(downloadBvhBtn, "Downloaded ✓", "success");
flash(exportMenuButton, "Downloaded ✓", "success");
} catch {
flash(downloadBvhBtn, "Export failed", "error");
flash(exportMenuButton, "Export failed", "error");
}
}
downloadBvhBtn.addEventListener("click", downloadBvh);

// --- glTF / GLB export ---
// Bake the current movement into a GLB (rig + animation clip) and download it.
Expand All @@ -732,10 +733,10 @@ async function downloadGltf(): Promise<void> {
const source = editorApi.getValue();
const { ir, errors } = parse(source);
if (!ir || errors.length > 0) {
flash(downloadGltfBtn, "Fix errors first", "error");
flash(exportMenuButton, "Fix errors first", "error");
return;
}
flash(downloadGltfBtn, "Exporting…", "pending", 0);
flash(exportMenuButton, "Exporting…", "pending", 0);
try {
const { exportGLTF } = await import("posecode-render");
const glb = (await exportGLTF(ir, { binary: true })) as ArrayBuffer;
Expand All @@ -748,12 +749,45 @@ async function downloadGltf(): Promise<void> {
a.click();
a.remove();
URL.revokeObjectURL(url);
flash(downloadGltfBtn, "Downloaded ✓", "success");
flash(exportMenuButton, "Downloaded ✓", "success");
} catch {
flash(downloadGltfBtn, "Export failed", "error");
flash(exportMenuButton, "Export failed", "error");
}
}
downloadGltfBtn.addEventListener("click", downloadGltf);

// --- Export menu ---
function setExportMenu(open: boolean): void {
exportMenu.hidden = !open;
exportMenuButton.setAttribute("aria-expanded", String(open));
if (open) downloadBvhBtn.focus();
}
exportMenuButton.addEventListener("click", () => {
setExportMenu(exportMenu.hasAttribute("hidden"));
});
downloadBvhBtn.addEventListener("click", () => {
setExportMenu(false);
exportMenuButton.focus();
void downloadBvh();
});
downloadGltfBtn.addEventListener("click", () => {
setExportMenu(false);
exportMenuButton.focus();
void downloadGltf();
});
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element) || !event.target.closest(".tb-downloads")) {
setExportMenu(false);
}
});
exportMenu.addEventListener("keydown", (event) => {
const options = [downloadBvhBtn, downloadGltfBtn];
const index = options.indexOf(document.activeElement as HTMLButtonElement);
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
const direction = event.key === "ArrowDown" ? 1 : -1;
options[(index + direction + options.length) % options.length]?.focus();
}
});

// --- Slide-over panels (how-to, movement library) sharing one scrim ---
const howto = $<HTMLElement>("howto");
Expand Down Expand Up @@ -788,7 +822,13 @@ $<HTMLButtonElement>("howto-copy").addEventListener("click", (e) =>
copyPrompt(e.currentTarget as HTMLButtonElement),
);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closePanels();
if (e.key === "Escape") {
closePanels();
if (!exportMenu.hidden) {
setExportMenu(false);
exportMenuButton.focus();
}
}
});

// --- Intro strip ---
Expand Down
58 changes: 53 additions & 5 deletions playground/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,60 @@ select:hover {
align-items: center;
}

/* The two motion-export buttons are grouped so mobile can lay them out as one
balanced row. On desktop the wrapper is transparent (`display: contents`),
so the buttons stay direct children of the flex toolbar and the desktop row
is unchanged. */
/* Desktop motion export lives behind one compact menu; mobile hides the whole
desktop-pipeline action below. */
.tb-downloads {
display: contents;
position: relative;
}
.export-caret {
width: 7px;
height: 7px;
margin-left: 2px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: translateY(-2px) rotate(45deg);
transition: transform 0.16s var(--ease);
}
#export-menu-button[aria-expanded="true"] .export-caret {
transform: translateY(2px) rotate(225deg);
}
.export-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 30;
width: 260px;
padding: 6px;
background:
var(--sheen),
var(--panel-2);
border: 1px solid var(--border-2);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-float);
}
.export-option {
display: grid;
width: 100%;
gap: 2px;
padding: 10px 11px;
color: var(--text);
text-align: left;
font: inherit;
background: transparent;
border: 0;
border-radius: 5px;
cursor: pointer;
}
.export-option:hover,
.export-option:focus-visible {
background: var(--panel-3);
}
.export-option > span {
font-weight: 700;
}
.export-option small {
color: var(--text-2);
font-size: 11px;
}

/* Movement-library trigger — the one content control in the topbar. Reads like
Expand Down