Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c409a74
fix: resolve Windows path compatibility and subprocess execution errors
rayanfer32 Jul 17, 2026
92ecb44
feat(video): implement high-performance parallel rendering pipeline
rayanfer32 Jul 17, 2026
2576f14
docs(readme): document headless video rendering features and examples
rayanfer32 Jul 17, 2026
0b8f5b7
feat: add test ai explainer demo
rayanfer32 Jul 18, 2026
8450236
feat(render): support scaling up to 10x, enable SIMD128, and fix Wind…
rayanfer32 Jul 18, 2026
37fd347
feat(host): make base canvas resolution configurable
rayanfer32 Jul 18, 2026
2bce4f2
feat(compiler): support rasterDensity in pocket.config.ts and configu…
rayanfer32 Jul 18, 2026
928be37
feat: support dynamic logical viewport resolutions in WASM core and w…
rayanfer32 Jul 18, 2026
1debb01
feat(ai-explainer): add intro and section 2 compositions, fix runtime…
rayanfer32 Jul 18, 2026
a1aa188
feat(render): log frames/sec in progress reporting
rayanfer32 Jul 18, 2026
6433df0
Merge branch 'upstream/main' into feat/video-rendering
rayanfer32 Jul 26, 2026
8c83882
fix(wasm): remove stale LOGICAL_WIDTH/HEIGHT assignments and add flag…
rayanfer32 Jul 26, 2026
dc04d6f
fix(build): add demos/ directory support in resolveEntry and update c…
rayanfer32 Jul 26, 2026
909951c
fix(build): fix capturing group in outputName regex so demo app artif…
rayanfer32 Jul 26, 2026
b84eb8f
feat(render): default concurrency to CPU count and scale to 1 for cus…
rayanfer32 Jul 26, 2026
1619fb5
refactor: move ai-explainer from demos/ to apps/
rayanfer32 Jul 26, 2026
a7255f6
feat(ai-explainer): add section3.tsx for vector embeddings matching c…
rayanfer32 Jul 26, 2026
d0f2e20
feat(render): add --audio (-au) flag to mix audio track into MP4 output
rayanfer32 Jul 26, 2026
98b8cd6
fix(render): resolve TypeScript type warnings and remove unused varia…
rayanfer32 Jul 26, 2026
e72f9b6
fix(render): resolve all remaining TypeScript types in render.ts and …
rayanfer32 Jul 26, 2026
9831a12
feat(ai-explainer): update section files and add audio assets for nar…
rayanfer32 Jul 26, 2026
bcca3e7
feat(ai-explainer): add section4.tsx for attention mechanism and tran…
rayanfer32 Jul 26, 2026
0360c3e
docs(readme): update video rendering section with examples and CLI op…
rayanfer32 Jul 26, 2026
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
97 changes: 97 additions & 0 deletions .agents/skills/pocketjs_styling/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: pocketjs_styling
description: Guidelines and specifications for compiling Tailwind styles and writing Spec-compliant style objects in PocketJS.
---

# PocketJS Styling & Tailwind Compilation Guidelines

This skill provides the compiler constraints and runtime style property specifications for building user interfaces in PocketJS. Follow these rules to avoid compile-time parser errors and runtime style crashes.

---

## 1. Tailwind Compiler Restrictions (Build-time Class Validation)

The build-time compiler (`compiler/tailwind.ts`) parses static class strings into compiled style records. If a class literal contains any unrecognized utility token, the compiler ignores the entire literal, which leads to a runtime crash (`unknown class ... not in the compiled style table`).

### Text & Font Weights
* **Only `font-bold` is supported.** Do NOT use `font-semibold`, `font-extrabold`, or `font-black`.
* **Only `tracking-wide` is supported.** Do NOT use `tracking-tighter`, `tracking-tight`, etc.
* **Text sizes are restricted to baked font slots:**
* `text-xs` (12px)
* `text-sm` (14px)
* `text-base` (16px)
* `text-lg` (18px)
* `text-xl` (20px)
* `text-2xl` (24px)
* `text-4xl` (36px)

### Borders
* **Directional borders are NOT supported in utility classes.** Do NOT use `border-t`, `border-b`, `border-l`, or `border-r`.
* **Only full-border wrappers are allowed:** `border`, `border-2`, `border-4`, `border-8`, or arbitrary `border-[N]`.
* **Implementing single-sided borders:** Render a separate thin `<View>` element to act as a divider line:
```tsx
{/* Horizontal top border separator */}
<View class="absolute" style={{ insetT: 0, insetL: 0, width: 480, height: 2, bgColor: "#38bdf826" }} />
```

### Rounded Corners
* **`rounded-full` needs build-time known size.** You MUST specify both `w-N` and `h-N` in the *same* class literal:
```tsx
{/* Correct */}
<View class="absolute w-[600] h-[600] rounded-full bg-gradient-to-b ..." />

{/* Incorrect (w/h in dynamic style object instead of class) */}
<View class="absolute rounded-full" style={{ width: 600, height: 600 }} />
```

---

## 2. Style Object Rules (Runtime Dynamic Styles)

The dynamic `style` object is parsed at runtime. Keys are matched against the native spec enum (`spec/spec.ts`).

### Positioning Offsets
* **Standard CSS directional keys are NOT supported.** Use their spec-compliant equivalents:
* `left` → **`insetL`**
* `right` → **`insetR`**
* `top` → **`insetT`**
* `bottom` → **`insetB`**

### Visual & Layout Keys
* **Use spec-compliant camelCase style keys:**
* `background-color` → **`bgColor`**
* `border-radius` → **`radius`**
* `border-color` → **`borderColor`**
* `border-width` → **`borderWidth`**
* `z-index` → **`zIndex`**
* **Unsupported Keys:** Properties like `font-size`, `font-family`, and `box-shadow` are unrecognized.

### Values and Units
* **All sizes and dimensions must be raw numbers (representing pixels).** Suffixes like `"px"` are not supported:
```tsx
{/* Correct */}
style={{ width: 320, height: 220, insetL: 50 }}

{/* Incorrect */}
style={{ width: "320px", height: "220px", insetL: "50px" }}
```
* **Colors must be `#rrggbbaa` hex strings.** Standard browser color syntaxes (`rgb()`, `rgba()`) are not supported:
```tsx
{/* Correct */}
style={{ bgColor: "#0f172a66" }}

{/* Helper for dynamic alpha blending */}
function toHexColor(r: number, g: number, b: number, a: number): string {
const toHex = (x: number) => {
const hex = Math.max(0, Math.min(255, Math.round(x))).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(Math.round(a * 255))}`;
}
```

### Dynamic Text Resizing
* Since `fontSize` is not a dynamic runtime property, scale text by setting a standard class size (like `text-2xl`) and modifying the **`scale`** key inside the dynamic `style` object:
```tsx
<Text class="text-2xl font-bold text-white" style={{ scale: 4.0 }} />
```
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,44 @@ fonts, vectors and core masks at Vita's native 960x544 density. Physical
controls, left-analog input, and front-panel multi-touch snapshots are supported;
PSP builds retain their controller-only fallback.

## Headless Video Rendering

PocketJS can render high-performance, high-definition videos (e.g. 1080p) from Solid or Vue Vapor UI components without headless browser automation overhead. It runs inside a single Bun process, using the Rust core's software rasterizer to paint raw framebuffers and piping them directly to FFmpeg.

To maximize performance:
- **Zero Heap Allocations**: Framebuffers are read directly from WASM linear memory and piped to FFmpeg, bypassing JavaScript V8 heap allocations entirely.
- **Multiprocess Concurrency**: Spawns parallel worker processes to render chunks of the video concurrently, stitching them together at the end with FFmpeg's ultra-fast demuxer (zero re-encoding overhead).
- **HD Asset Baking**: Integrates with the compilation pipeline to bake Inter font atlases and SVGs at matching target physical densities (e.g. 4x scale for 1080p).

### Usage

```sh
# Example 1: Full 1080p 60fps video with narration audio track and 4 parallel workers:
bun render -a ai-explainer-main -d 58.68 -f 60 --width 1920 --height 1080 -c 4 --audio apps/ai-explainer/assets/section4.wav

# Example 2: 720p 30fps quick preview:
bun render -a ai-explainer-main -d 10.0 -f 30 --width 1280 --height 720

# Example 3: 1080p render using default 480x272 viewport at 4x scaling:
bun render -a music-main -d 60 -f 30 -s 4

# Example 4: Custom output path for 5-second 540p clip:
bun render -a motions-main -d 5.0 -f 30 -s 2 -o dist/render/motions-preview.mp4

# CLI Options:
# -a, --app <name> Name of the app/demo target to render (required)
# -o, --output <path> Output MP4 path (default: dist/render/<app>.mp4)
# -d, --duration <secs> Duration of the video in seconds (default: 5.0)
# -f, --fps <number> Frame rate of the video (default: 60)
# -s, --scale <1..10> Scaling factor (default: 4 for stock 480x272, 1 for custom width/height)
# -w, --width <pixels> Logical width of layout viewport (default: 480)
# --height <pixels> Logical height of layout viewport (default: 272)
# -c, --concurrency <n> Number of parallel worker processes (default: CPU core count)
# --audio, -au <path> Path to audio track to mix into output MP4 (WAV/MP3/AAC)
# --crf <number> x264 quality factor (default: 18)
# --preset <string> x264 speed preset (default: faster)
```

## The Pocket Launcher (on-device app switching)

One PSP EBOOT or Vita VPK can embed every app admitted by that target plus a
Expand All @@ -259,6 +297,7 @@ bun run launcher build --target vita -- --release # -> dist/vita/launcher-main.v
bun run e2e:launcher # PPSSPPHeadless 3-swap journey
bun run e2e:launcher:vita # Vita3K 7-swap/resource-reuse journey
```
```

## DevTools + time travel

Expand Down
Binary file added apps/ai-explainer/assets/section0.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section1.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section10.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section11.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section12.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section13.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section14.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section15.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section16.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section17.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section18.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section19.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section2.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section20.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section21.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section22.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section3.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section4.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section5.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section6.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section7.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section8.wav
Binary file not shown.
Binary file added apps/ai-explainer/assets/section9.wav
Binary file not shown.
121 changes: 121 additions & 0 deletions apps/ai-explainer/compositions/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1920, height=1080">
<title>AI Engineering Explained for Developers</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #040814;
}
body {
font-family: "Inter", sans-serif;
}
/* Sub-composition slots take up full bleed overlay placement */
#root > div[data-composition-src] {
position: absolute;
inset: 0;
}
</style>
</head>
<body>
<!-- Standalone Composition Root -->
<div data-hf-id="hf-e3jm" id="root" data-composition-id="root" data-start="0" data-duration="1166.20" data-width="1920" data-height="1080">

<!-- Sub-compositions for each scene -->
<div data-hf-id="hf-bpzx" id="el-intro" data-composition-id="scene-intro" data-composition-src="compositions/intro.html" data-start="0" data-duration="46.64" data-track-index="1"></div>
<div data-hf-id="hf-34jh" id="el-sec1" data-composition-id="scene-sec1" data-composition-src="compositions/section1.html" data-start="46.64" data-duration="48.68" data-track-index="1" data-hidden=""></div>

<div data-hf-id="hf-ylra" id="el-sec2" data-composition-id="scene-sec2" data-composition-src="compositions/section2.html" data-start="95.32" data-duration="48.76" data-track-index="1"></div>

<div data-hf-id="hf-sec3" id="el-sec3" data-composition-id="scene-sec3" data-composition-src="compositions/section3.html" data-start="144.08" data-duration="59.20" data-track-index="1"></div>

<div data-hf-id="hf-sec4" id="el-sec4" data-composition-id="scene-sec4" data-composition-src="compositions/section4.html" data-start="203.28" data-duration="58.68" data-track-index="1"></div>

<div data-hf-id="hf-sec5" id="el-sec5" data-composition-id="scene-sec5" data-composition-src="compositions/section5.html" data-start="261.96" data-duration="47.52" data-track-index="1"></div>

<div data-hf-id="hf-sec6" id="el-sec6" data-composition-id="scene-sec6" data-composition-src="compositions/section6.html" data-start="309.48" data-duration="46.52" data-track-index="1"></div>

<div data-hf-id="hf-sec7" id="el-sec7" data-composition-id="scene-sec7" data-composition-src="compositions/section7.html" data-start="356.00" data-duration="46.76" data-track-index="1"></div>

<div data-hf-id="hf-sec8" id="el-sec8" data-composition-id="scene-sec8" data-composition-src="compositions/section8.html" data-start="402.76" data-duration="46.52" data-track-index="1"></div>

<div data-hf-id="hf-sec9" id="el-sec9" data-composition-id="scene-sec9" data-composition-src="compositions/section9.html" data-start="449.28" data-duration="48.16" data-track-index="1"></div>

<div data-hf-id="hf-sec10" id="el-sec10" data-composition-id="scene-sec10" data-composition-src="compositions/section10.html" data-start="497.44" data-duration="42.56" data-track-index="1"></div>

<div data-hf-id="hf-sec11" id="el-sec11" data-composition-id="scene-sec11" data-composition-src="compositions/section11.html" data-start="540.00" data-duration="50.13" data-track-index="1"></div>

<div data-hf-id="hf-sec12" id="el-sec12" data-composition-id="scene-sec12" data-composition-src="compositions/section12.html" data-start="590.13" data-duration="54.49" data-track-index="1"></div>

<div data-hf-id="hf-sec13" id="el-sec13" data-composition-id="scene-sec13" data-composition-src="compositions/section13.html" data-start="644.62" data-duration="54.69" data-track-index="1"></div>

<div data-hf-id="hf-sec14" id="el-sec14" data-composition-id="scene-sec14" data-composition-src="compositions/section14.html" data-start="699.31" data-duration="57.77" data-track-index="1"></div>

<div data-hf-id="hf-sec15" id="el-sec15" data-composition-id="scene-sec15" data-composition-src="compositions/section15.html" data-start="757.08" data-duration="41.77" data-track-index="1"></div>

<div data-hf-id="hf-sec16" id="el-sec16" data-composition-id="scene-sec16" data-composition-src="compositions/section16.html" data-start="798.85" data-duration="40.41" data-track-index="1"></div>

<div data-hf-id="hf-sec17" id="el-sec17" data-composition-id="scene-sec17" data-composition-src="compositions/section17.html" data-start="839.26" data-duration="47.81" data-track-index="1"></div>

<div data-hf-id="hf-sec18" id="el-sec18" data-composition-id="scene-sec18" data-composition-src="compositions/section18.html" data-start="887.07" data-duration="46.61" data-track-index="1"></div>

<div data-hf-id="hf-sec19" id="el-sec19" data-composition-id="scene-sec19" data-composition-src="compositions/section19.html" data-start="933.68" data-duration="42.69" data-track-index="1"></div>

<div data-hf-id="hf-sec20" id="el-sec20" data-composition-id="scene-sec20" data-composition-src="compositions/section20.html" data-start="976.37" data-duration="58.33" data-track-index="1"></div>

<div data-hf-id="hf-sec21" id="el-sec21" data-composition-id="scene-sec21" data-composition-src="compositions/section21.html" data-start="1034.70" data-duration="62.65" data-track-index="1"></div>

<div data-hf-id="hf-sec22" id="el-sec22" data-composition-id="scene-sec22" data-composition-src="compositions/section22.html" data-start="1097.35" data-duration="68.85" data-track-index="1"></div>

<!-- Voiceover audio tracks -->
<audio data-hf-id="hf-fj65" id="audio-intro" src="assets/intro.wav" data-start="0.00" data-duration="46.64" data-track-index="10"></audio>
<audio data-hf-id="hf-89gb" id="audio-section1" src="assets/section1.wav" data-start="46.64" data-duration="48.68" data-track-index="11"></audio>
<audio data-hf-id="hf-bykp" id="audio-section2" src="assets/section2.wav" data-start="95.32" data-duration="48.76" data-track-index="10"></audio>
<audio data-hf-id="hf-sec3-aud" id="audio-section3" src="assets/section3.wav" data-start="144.08" data-duration="59.20" data-track-index="11"></audio>
<audio data-hf-id="hf-sec4-aud" id="audio-section4" src="assets/section4.wav" data-start="203.28" data-duration="58.68" data-track-index="10"></audio>
<audio data-hf-id="hf-sec5-aud" id="audio-section5" src="assets/section5.wav" data-start="261.96" data-duration="47.52" data-track-index="11"></audio>
<audio data-hf-id="hf-sec6-aud" id="audio-section6" src="assets/section6.wav" data-start="309.48" data-duration="46.52" data-track-index="10"></audio>
<audio data-hf-id="hf-sec7-aud" id="audio-section7" src="assets/section7.wav" data-start="356.00" data-duration="46.76" data-track-index="11"></audio>
<audio data-hf-id="hf-sec8-aud" id="audio-section8" src="assets/section8.wav" data-start="402.76" data-duration="46.52" data-track-index="10"></audio>
<audio data-hf-id="hf-sec9-aud" id="audio-section9" src="assets/section9.wav" data-start="449.28" data-duration="48.16" data-track-index="11"></audio>
<audio data-hf-id="hf-sec10-aud" id="audio-section10" src="assets/section10.wav" data-start="497.44" data-duration="42.56" data-track-index="10"></audio>
<audio data-hf-id="hf-sec11-aud" id="audio-section11" src="assets/section11.wav" data-start="540.00" data-duration="50.13" data-track-index="11"></audio>
<audio data-hf-id="hf-sec12-aud" id="audio-section12" src="assets/section12.wav" data-start="590.13" data-duration="54.49" data-track-index="10"></audio>
<audio data-hf-id="hf-sec13-aud" id="audio-section13" src="assets/section13.wav" data-start="644.62" data-duration="54.69" data-track-index="11"></audio>
<audio data-hf-id="hf-sec14-aud" id="audio-section14" src="assets/section14.wav" data-start="699.31" data-duration="57.77" data-track-index="10"></audio>
<audio data-hf-id="hf-sec15-aud" id="audio-section15" src="assets/section15.wav" data-start="757.08" data-duration="41.77" data-track-index="11"></audio>
<audio data-hf-id="hf-sec16-aud" id="audio-section16" src="assets/section16.wav" data-start="798.85" data-duration="40.41" data-track-index="10"></audio>
<audio data-hf-id="hf-sec17-aud" id="audio-section17" src="assets/section17.wav" data-start="839.26" data-duration="47.81" data-track-index="11"></audio>
<audio data-hf-id="hf-sec18-aud" id="audio-section18" src="assets/section18.wav" data-start="887.07" data-duration="46.61" data-track-index="10"></audio>
<audio data-hf-id="hf-sec19-aud" id="audio-section19" src="assets/section19.wav" data-start="933.68" data-duration="42.69" data-track-index="11"></audio>
<audio data-hf-id="hf-sec20-aud" id="audio-section20" src="assets/section20.wav" data-start="976.37" data-duration="58.33" data-track-index="10"></audio>
<audio data-hf-id="hf-sec21-aud" id="audio-section21" src="assets/section21.wav" data-start="1034.70" data-duration="62.65" data-track-index="11"></audio>
<audio data-hf-id="hf-sec22-aud" id="audio-section22" src="assets/section22.wav" data-start="1097.35" data-duration="68.85" data-track-index="10"></audio>

</div>

<script>
window.__timelines = window.__timelines || {};
// Root timeline is empty; sub-compositions manage their own timeline playback
window.__timelines["root"] = gsap.timeline({ paused: true });
</script>
<script type="application/hyperframes-slideshow+json">
{
"version": 1,
"slides": []
}
</script>
</body>
</html>
Loading