⏳ This skill is pending AI review.
Scores will appear once the review pipeline completes.
alpha-cutout
Turns an AI-generated (or filmed) clip of a character or product into a transparent, web-ready animation by shooting it on a flat chroma plate and keying it with per-frame plate measurement and edge unmixing. Delivers stacked-alpha MP4 that plays with transparency in every browser including Safari, VP9 alpha WebM, or keyed WebP frame sequences for scroll scrubbing. Use when an animated subject has to sit over page content, between layers of type, or over a 3D scene, when someone asks for "transparent video", "video with alpha", "remove the background from this clip", or when a generated video has a background that should not be there.
Choose how to use this skill
You do not need every option. Choose the path your AI client supports. The stable page stays the same; versioned files are immutable.
1. Native installer
This listing has no registered native installer command. Use the complete package or source fallback below, depending on what your client supports.
Do not guess an installer command or replace an existing version without reviewing the diff.
2. Complete package recommended
Download the ZIP when available. It includes SKILL.md plus the references, security notes and version metadata.
No complete ProSkills package is published for this listing yet.3. Prompt-only
Copy the prompt above when the agent can read the stable page or when you want to adopt the workflow without installing a skill.
Need only the instruction file?
Download SKILL.md only if your client requires a single file. The complete ZIP is safer for a full installation because it preserves the references and release context.
No path installs or executes anything by itself. Your agent still needs access to the project files. Before updating, compare the installed version and review the diff.
// RATINGS
Not yet listed on ClawHub or SkillsMP
// README
Open 3D Skills are eight agent skills for building 3D and animated websites: real-time three.js scenes built from code, scroll-scrubbed films, transparent video that plays in Safari, characters that follow the cursor, exploded views of machines, and the AI image and video work behind them. They also teach the agent to check its work in real browsers and to record it as a smooth 60 fps video.
Every technique here was used to build a real site first, and every script was run on real footage before it went in.
The pavilion is the starter scene in threejs-scene; the motor is the demo in exploded-view. Both are built entirely from code: no model files, no textures to download.
Install
npx skills add Moh4696/open-3d-skills
That installs with the npx skills CLI, the open installer for agent skills. It asks which of the eight skills you want, which agents to install them for (Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Antigravity and dozens more) and whether they are for this project or every project. Start a new agent session afterwards.
npx skills add Moh4696/open-3d-skills -g -y # all skills, all your agents, every project, no questions
npx skills add Moh4696/open-3d-skills --skill threejs-scene # just one skill
npx skills update # pull new versions later
npx skills needs Node 22.20 or newer.
Inside Claude Code:
/plugin marketplace add Moh4696/open-3d-skills
/plugin install open-3d-skills@open-3d-skills
The skills are namespaced as open-3d-skills:threejs-scene and so on, so they never collide with other skills. /plugin update upgrades them.
codex plugin marketplace add Moh4696/open-3d-skills
codex plugin add open-3d-skills@open-3d-skills
Start a new Codex session afterwards.
The one-line installer links the skills into ~/.cursor/skills, where Cursor finds personal skills. To use the repo as a Cursor plugin instead, clone it into ~/.cursor/plugins/local/open-3d-skills and run Developer: Reload Window. Teams can import the repo URL as a marketplace under Dashboard → Plugins & MCPs → Add Marketplace.
gemini extensions install https://github.com/Moh4696/open-3d-skills
gemini extensions update pulls new versions.
curl -fsSL https://raw.githubusercontent.com/Moh4696/open-3d-skills/main/install.sh | bash
It installs all eight skills for every coding agent it finds (Claude Code, Codex, Cursor, Gemini CLI, and OpenCode, which also reads Claude Code's folder). It clones the repo to ~/.open-3d-skills and links the skills from there, so running it again updates them. From a clone you can choose:
git clone https://github.com/Moh4696/open-3d-skills && cd open-3d-skills
./install.sh --claude --codex # only these agents (also --cursor --gemini --opencode --agents)
./install.sh --dir path/to/skills # any agent that reads a skills folder
./install.sh threejs-scene frame-capture # only some skills
./install.sh --copy # copy instead of symlink (Windows, synced folders)
./install.sh --uninstall # remove what it installed
Each skill is a plain folder with a SKILL.md, so any agent that supports Agent Skills can load it.
The skills
| Skill | What it does | Ships with |
|---|---|---|
threejs-scene | Real-time 3D sites built from code: buildings, vehicles, machines, landscapes. Sky and sun presets that blend, soft fitted shadows, ambient occlusion, bloom, instancing, profile sweeps, shader patches, camera flights, quality tiers | A complete 60 fps starter scene, and notes on lighting, geometry, shaders and performance |
scroll-film | Scroll-scrubbed films from generated clips: chained with start and end frames, holds for captions, phone versions, loading order, reduced motion | scroll-film.js engine, a prep script for frames or scrub-ready video, a seam checker |
motion-assets | Planning and generating AI images and clips without wasting money: asset lists with prompts and costs up front, one look across assets, model choice by capability, rate limits, logging | A loop checker that finds and fixes loop points, and a paste-back tool for outpainted images |
alpha-cutout | Transparent video from clips shot on a key plate: per-frame plate measurement, edge unmixing, outputs for every browser including Safari | Plate builder, keyer (stacked-alpha MP4, VP9 alpha, WebP frames), WebGL alpha video player |
cursor-rig | A painted character whose head follows the cursor, built as a grid of poses on one fixed body, with live blinks | Rig builder (alignment, even steps, flow in-betweens) and a spring-driven canvas player |
exploded-view | Interactive explainers of how machines work: staged explode, x-ray, section cut, labels with leader lines, picking | explode.js and the motor demo above |
site-check | Checks a 3D or animated page in headless Chromium and WebKit at three sizes: frame time, draw calls, errors, blank canvases, overflow, reduced motion | A checker that writes a report and a contact sheet, and a Range-capable local server |
frame-capture | Frame-exact 60 fps video of any page by running it on a virtual clock; labelled side-by-side or stacked comparisons | Recorder, comparison builder, local server |
How they work together
- *Plan.
// HOW IT'S BUILT
// FILES
6 files shipped with this skill. Inline text is the catalog copy. The upstream link is the source repository.
skills/alpha-cutout/SKILL.md6.8 KB96a355a528a7upstream ↗
--- name: alpha-cutout description: Turns an AI-generated (or filmed) clip of a character or product into a transparent, web-ready animation by shooting it on a flat chroma plate and keying it with per-frame plate measurement and edge unmixing. Delivers stacked-alpha MP4 that plays with transparency in every browser including Safari, VP9 alpha WebM, or keyed WebP frame sequences for scroll scrubbing. Use when an animated subject has to sit over page content, between layers of type, or over a 3D scene, when someone asks for "transparent video", "video with alpha", "remove the background from this clip", or when a generated video has a background that should not be there. --- # Alpha cutout Image-to-video models never return alpha. A character that should stand between a giant headline and a sun disc, or a bottle that should float over a page, needs its background removed from every frame, cleanly enough that no fringe shows on any page colour. This skill does it the way a film studio does: shoot the subject on a flat key colour, then pull a matte per frame. ## Tools | File | What it does | | --- | --- | | `scripts/plate.py` | Places a cut-out subject on a key plate with headroom; picks the key colour the subject clashes with least | | `scripts/key.py` | Keys a clip or still into stacked MP4, VP9 alpha WebM, RGBA WebP frames or PNGs | | `assets/alpha-video.js` | Plays a stacked-alpha MP4 with transparency on a canvas (WebGL), with `seek()` for scrubbing | Needs Python 3.9+, `pip install numpy opencv-python-headless pillow`, and ffmpeg on PATH. ## Pipeline 1. **Cut the subject out of the still.** Any background remover works. Check what it dropped: thin objects (a ball in the air, racket strings, hair) often go missing. Put them back by hand. 2. **Make the plate.** ```bash python3 scripts/plate.py subject.png plate.png --size 1350x1800 --fit 0.8 ``` The script reports, for magenta, green and blue, how much of the subject sits close to that key, and picks the cleanest. Magenta is usually right: almost nothing in skin, clothing, product or foliage is pure magenta. Green fails on plants and green products; blue fails on denim and sky. The subject is pasted with a hard edge on purpose: a soft edge pre-mixed with the plate colour teaches the model to draw a fringe. Leave room for the motion. A serve needs space above the head; a turn needs space to the side. Match the plate's aspect to the video model's output aspect so nothing gets cropped. 3. **Generate on the plate.** Use the plate as the start frame (and as the end frame for a seamless loop; see `motion-assets`). The prompt must name the plate, or the model invents a background: > The background must remain a completely flat, uniform, pure chroma-key magenta (exact RGB 255, 0, 255) filling the entire frame, with no shadows, no floor, no gradient, no vignette and no scenery. Locked-off camera. **The prompt wins over the image.** A leftover "on a white background" in the prompt gets you a white background whatever the plate was. Re-read the prompt after every edit. 4. **Key it.** ```bash python3 scripts/key.py clip.mp4 --png check/ --frame-list 0,40,80,120 # inspect first python3 scripts/key.py clip.mp4 --stacked hero-1080.mp4 --width 1080 python3 scripts/key.py clip.mp4 --stacked hero-720.mp4 --width 720 # phones python3 scripts/key.py clip.mp4 --frames serve/lg --width 960 --start 0 --end 78 ``` `--png` prints the plate colour it measured in each frame. Models drift the plate: a pure magenta can come back pale pink (250, 178, 243) halfway through while the subject keeps its colours. That is why the key colour is measured per frame from the frame's own border instead of being set once. If the subject touches the top edge, the measurement breaks and the script says so. Then, per pixel: alpha comes from how far the pixel's key excess is from the plate's, edge pixels are unmixed against the plate (`F = (C - (1 - a) K) / a`), and surviving spill is clamped. Unmixing matters. A plain colour-key or a clamp toward neutral turns magenta edge blends tan, which reads as a salmon outline on a white shirt; unmixing returns the real edge colour. 5. **Check on three backgrounds**: near-black, the page's light colour, and its loudest brand colour. Composite the `--png` output and look at the edges at 100% and 400%. Look hardest at hair, fingers, thin props, motion blur and the frame where the plate drifted most. ## Fixes | You see | Do | | --- | --- | | A thin outline that follows the subject | `--erode 1` (rarely 2). The model copied a fringe into the artwork | | Holes in light or shiny parts | That colour is too close to the key. Regenerate on a different key (`plate.py --key`) | | Pink or green wash over the whole background | The plate gradient is stronger than usual; check the measured plate colours and raise `--solid` slightly | | A detail changed colour mid-clip (a cap turned green) | Model drift, not a keying fault. Recolour that hue in those frames, or regenerate | | Coloured line round the subject after compression | Already handled: colour under transparent pixels is the subject's own edge colour pushed outward, so chroma subsampling has nothing foreign to bleed in. `--fill` only sets the colour far from the subject | ## Which output | Output | Plays in | Use for | | --- | --- | --- | | `--stacked` MP4 + `assets/alpha-video.js` | Every browser, Safari and iOS included | Looping heroes, characters, anything that plays | | `--webm` VP9 alpha | Chrome, Edge, Firefox. Not Safari | A plain `<video>` tag where Safari support is not needed | | `--frames` WebP sequence | Everywhere | Scroll-scrubbed motion (see `scroll-film`): exact frame per scroll position, no seeking lag | Stacked alpha doubles the frame height (colour on top, matte below) and a shader recombines them, so it is plain H.264: it decodes in hardware everywhere and costs about 1.3 times the bytes of the colour alone. Ship two widths (1080 and 720) and choose by viewport. ```js import { AlphaVideo } from "./alpha-video.js"; const hero = new AlphaVideo(document.querySelector("canvas.hero"), innerWidth > 900 ? "hero-1080.mp4" : "hero-720.mp4"); hero.onready = () => document.body.classList.add("hero-ready"); function frame() { hero.draw(); requestAnimationFrame(frame); } requestAnimationFrame(frame); ``` The player pauses itself off screen and uploads a texture only when the video has moved on. For scrubbing, create it with `{ loop: false, autoplay: false }` and call `hero.seek(seconds)` from your scroll handler. Serve videos from a server that answers HTTP Range requests (the `frame-capture` skill's serve.py does; `python3 -m http.server` does not), or seeking will not work. ## Reduced motion With `prefers-reduced-motion: reduce`, show one keyed still (`--png` a good frame, convert to WebP) instead of the loop.skills/alpha-cutout/scripts/key.py10 KB63ef7f07ed91upstream ↗
#!/usr/bin/env python3 """Key a generated clip shot on a flat colour plate into real alpha, frame by frame. python3 key.py clip.mp4 --stacked out.mp4 [--width 1080] # H.264 colour over matte, any browser python3 key.py clip.mp4 --webm out.webm [--width 720] # VP9 with alpha (Chrome, Firefox, Edge) python3 key.py clip.mp4 --frames out_dir [--width 960 --step 2] # RGBA WebP sequence + manifest.json python3 key.py clip.mp4 --png out_dir --frame-list 0,30,60 # full-size RGBA PNGs to inspect python3 key.py image.png --png out_dir # stills work too --key magenta (default) | green | blue. The plate colour is measured per frame from the frame's own border (top band and upper side columns, where the subject rarely reaches), because video models drift it: a #FF00FF plate can come back pale pink halfway through. Matte: the key's "excess" e (magenta: min(R,B) - G; green: G - max(R,B); blue: B - max(R,G)) is high on the plate and low on the subject. With K the frame's plate colour: a = (0.97 e_K - e) / (0.97 e_K - solid) clipped to 0..1 Edges are then unmixed against K, F = (C - (1 - a) K) / a, which recovers the real edge colour instead of a tinted fringe; whatever spill survives is clamped. Colour under fully transparent pixels is the subject's own edge colour pushed outward. Video and lossy WebP subsample chroma, so whatever sits next to an edge bleeds into it: a flat fill there draws a coloured line round the subject. Needs: pip install numpy opencv-python-headless pillow; ffmpeg on PATH. """ import argparse import json import subprocess from pathlib import Path import cv2 import numpy as np from PIL import Image EXCESS = { "magenta": lambda f: np.minimum(f[..., 0], f[..., 2]) - f[..., 1], "green": lambda f: f[..., 1] - np.maximum(f[..., 0], f[..., 2]), "blue": lambda f: f[..., 2] - np.maximum(f[..., 0], f[..., 1]), } # channel a surviving spill shows up in, and the channels whose max caps it at the edge SPILL = {"magenta": (2, (1, 1)), "green": (1, (0, 2)), "blue": (2, (0, 1))} def frames(path): if Path(path).suffix.lower() in (".png", ".jpg", ".jpeg", ".webp"): yield np.array(Image.open(path).convert("RGB")) return cap = cv2.VideoCapture(str(path)) while True: ok, bgr = cap.read() if not ok: break yield cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) cap.release() def fps_of(path): cap = cv2.VideoCapture(str(path)) fps = cap.get(cv2.CAP_PROP_FPS) or 24 cap.release() return fps def plate_colour(rgb): h, w, _ = rgb.shape b = max(4, w // 100) ring = np.concatenate([rgb[:b].reshape(-1, 3), rgb[: int(h * 0.7), :b].reshape(-1, 3), rgb[: int(h * 0.7), -b:].reshape(-1, 3)]) return np.median(ring, axis=0) def key(rgb, opts): excess = EXCESS[opts.key] k = plate_colour(rgb) k_ex = float(excess(k[None, None].astype(np.float32))[0, 0]) if k_ex < opts.solid + 8: raise SystemExit(f"plate colour {k.round().tolist()} is not a {opts.key} plate (excess {k_ex:.0f}); " f"check --key, or the subject touches the top edge") high = 0.97 * k_ex f = rgb.astype(np.float32) a = np.clip((high - excess(f)) / max(high - opts.solid, 1.0), 0.0, 1.0) ka = np.maximum(a, 0.25)[..., None] # floor keeps compression noise from exploding f = np.where((a < 0.999)[..., None], (f - (1 - ka) * k.astype(np.float32)) / ka, f) f = np.clip(f, 0, 255) a8 = (a * 255).astype(np.uint8) a8 = cv2.morphologyEx(a8, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) a8 = cv2.morphologyEx(a8, cv2.MORPH_CLOSE, np.ones((3, 3), np.uint8)) if opts.erode: a8 = cv2.erode(a8, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)), iterations=opts.erode) a = cv2.GaussianBlur(a8, (0, 0), 0.6).astype(np.float32) / 255.0 # spill that survives the unmix: magenta shows as excess blue (red is left alone so red # subjects keep their edges), green and blue as excess of their own channel c, others = SPILL[opts.key] cap = np.maximum(f[..., others[0]], f[..., others[1]]) + opts.solid * (2 if opts.key == "magenta" else 1) f[..., c] = np.where(a < 0.999, np.minimum(f[..., c], cap), f[..., c]) return f.astype(np.uint8), a, k def under_colour(col, a, fill): f = col.astype(np.float32) w = a.astype(np.float32) out = np.empty_like(f) out[:] = np.array(fill, np.float32) have = np.zeros(a.shape, bool) for sigma in (3, 10): den = cv2.GaussianBlur(w, (0, 0), sigma) num = cv2.GaussianBlur(f * w[..., None], (0, 0), sigma) ok = (den > 1e-3) & ~have out[ok] = num[ok] / den[ok][..., None] have |= ok near = cv2.dilate((a > 0.02).astype(np.uint8), np.ones((33, 33), np.uint8)) > 0 out[~near] = np.array(fill, np.float32) return np.where((a > 0.02)[..., None], f, out).astype(np.uint8) def resize(img, width): if not width or img.shape[1] == width: return img h = round(img.shape[0] * width / img.shape[1] / 2) * 2 return cv2.resize(img, (width, h), interpolation=cv2.INTER_AREA) def size_for(clip, width): first = next(frames(clip)) w = (width or first.shape[1]) // 2 * 2 return w, round(first.shape[0] * w / first.shape[1] / 2) * 2 def pipe(cmd): return subprocess.Popen(cmd, stdin=subprocess.PIPE) def write_stacked(clip, out, opts): w, h = size_for(clip, opts.width) fps = opts.fps or fps_of(clip) proc = pipe(["ffmpeg", "-y", "-loglevel", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h * 2}", "-r", str(fps), "-i", "-", "-c:v", "libx264", "-profile:v", "high", "-pix_fmt", "yuv420p", "-crf", str(opts.crf), "-preset", "slow", "-movflags", "+faststart", "-an", str(out)]) n = 0 for rgb in frames(clip): col, a, _ = key(rgb, opts) col = cv2.resize(under_colour(col, a, opts.fill_rgb), (w, h), interpolation=cv2.INTER_AREA) m = cv2.resize((a * 255).astype(np.uint8), (w, h), interpolation=cv2.INTER_AREA) proc.stdin.write(np.vstack([col, np.repeat(m[..., None], 3, axis=2)]).tobytes()) n += 1 proc.stdin.close() proc.wait() print(json.dumps({"out": str(out), "kind": "stacked", "frames": n, "width": w, "height": h, "fps": fps})) def write_webm(clip, out, opts): w, h = size_for(clip, opts.width) fps = opts.fps or fps_of(clip) proc = pipe(["ffmpeg", "-y", "-loglevel", "error", "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", str(fps), "-i", "-", "-c:v", "libvpx-vp9", "-pix_fmt", "yuva420p", "-b:v", "0", "-crf", str(opts.crf + 10), "-auto-alt-ref", "0", "-row-mt", "1", "-an", str(out)]) n = 0 for rgb in frames(clip): col, a, _ = key(rgb, opts) rgba = np.dstack([under_colour(col, a, opts.fill_rgb), (a * 255).astype(np.uint8)]) proc.stdin.write(cv2.resize(rgba, (w, h), interpolation=cv2.INTER_AREA).tobytes()) n += 1 proc.stdin.close() proc.wait() print(json.dumps({"out": str(out), "kind": "webm", "frames": n, "width": w, "height": h, "fps": fps})) def write_frames(clip, out_dir, opts): out_dir = Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) picks = {int(x) for x in opts.picks.split(",")} if opts.picks else None picked = [] for i, rgb in enumerate(frames(clip)): if picks is not None: if i not in picks: continue elif i < opts.start or (opts.end is not None and i > opts.end) or (i - opts.start) % opts.step: continue col, a, _ = key(rgb, opts) rgba = resize(np.dstack([under_colour(col, a, opts.fill_rgb), (a * 255).astype(np.uint8)]), opts.width) Image.fromarray(rgba).save(out_dir / f"{len(picked):03d}.webp", "WEBP", quality=opts.quality, method=6, alpha_quality=90) picked.append(i) w, h = Image.open(out_dir / "000.webp").size (out_dir / "manifest.json").write_text(json.dumps({"count": len(picked), "width": w, "height": h, "pattern": "{i:03d}.webp", "source_frames": picked})) print(json.dumps({"out": str(out_dir), "kind": "frames", "count": len(picked), "width": w, "height": h})) def write_png(clip, out_dir, opts): out_dir = Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) want = {int(x) for x in opts.frame_list.split(",")} for i, rgb in enumerate(frames(clip)): if i in want: col, a, k = key(rgb, opts) Image.fromarray(np.dstack([col, (a * 255).astype(np.uint8)])).save(out_dir / f"f{i:03d}.png") print(f"frame {i}: plate {k.round().astype(int).tolist()}, {100 * (a > 0.5).mean():.1f}% subject") def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("clip") ap.add_argument("--stacked", help="H.264 mp4, colour on top, matte below") ap.add_argument("--webm", help="VP9 webm with an alpha channel") ap.add_argument("--frames", help="directory for an RGBA WebP sequence") ap.add_argument("--png", help="directory for full-size RGBA PNGs") ap.add_argument("--key", choices=list(EXCESS), default="magenta") ap.add_argument("--frame-list", default="0") ap.add_argument("--width", type=int) ap.add_argument("--fps", type=float) ap.add_argument("--start", type=int, default=0) ap.add_argument("--end", type=int) ap.add_argument("--step", type=int, default=1) ap.add_argument("--picks", help="comma list of source frames for --frames") ap.add_argument("--quality", type=int, default=82, help="WebP quality") ap.add_argument("--crf", type=int, default=20) ap.add_argument("--solid", type=float, default=12, help="key excess at or below this is solid subject") ap.add_argument("--erode", type=int, default=0, help="shave N px off the matte edge") ap.add_argument("--fill", default="808080", help="hex colour far from the subject under alpha 0") opts = ap.parse_args() opts.fill_rgb = [int(opts.fill[i:i + 2], 16) for i in (0, 2, 4)] if not any([opts.stacked, opts.webm, opts.frames, opts.png]): ap.error("choose at least one of --stacked, --webm, --frames, --png") if opts.stacked: write_stacked(opts.clip, opts.stacked, opts) if opts.webm: write_webm(opts.clip, opts.webm, opts) if opts.frames: write_frames(opts.clip, opts.frames, opts) if opts.png: write_png(opts.clip, opts.png, opts) if __name__ == "__main__": main()skills/alpha-cutout/scripts/plate.py3.6 KBc45b69536fc9upstream ↗
#!/usr/bin/env python3 """Put a cut-out subject on a flat key plate, ready to animate with an image-to-video model. python3 plate.py subject.png plate.png [--key auto|magenta|green|blue] [--size 1350x1800] [--fit 0.82] [--anchor bottom|center] [--margin 0.04] subject.png must have alpha (cut it out first: rembg, Apple Vision, Photoshop, any tool). The subject is scaled to --fit of the plate height and placed on a pure key colour with room to move: a serve, a jump or a head turn needs headroom the still never had. --key auto picks the key colour the subject collides with least, and every run prints the collision report: the share of the subject's opaque pixels whose own key excess is close to the plate's. Above about 0.5% the key will eat into the subject; pick another key or recolour those details before generating anything. """ import argparse import numpy as np from PIL import Image PLATES = {"magenta": (255, 0, 255), "green": (0, 255, 0), "blue": (0, 0, 255)} EXCESS = { "magenta": lambda f: np.minimum(f[..., 0], f[..., 2]) - f[..., 1], "green": lambda f: f[..., 1] - np.maximum(f[..., 0], f[..., 2]), "blue": lambda f: f[..., 2] - np.maximum(f[..., 0], f[..., 1]), } def collisions(rgba): f = rgba[..., :3].astype(np.float32) opaque = rgba[..., 3] > 200 out = {} for name, ex in EXCESS.items(): e = ex(f)[opaque] out[name] = (float((e > 60).mean() * 100), float(np.percentile(e, 99.5)) if e.size else 0.0) return out def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("subject") ap.add_argument("out") ap.add_argument("--key", choices=["auto", *PLATES], default="auto") ap.add_argument("--size", default="1350x1800", help="plate size WxH; match the video model's aspect") ap.add_argument("--fit", type=float, default=0.82, help="subject height as a share of the plate") ap.add_argument("--anchor", choices=["bottom", "center"], default="bottom") ap.add_argument("--margin", type=float, default=0.04, help="gap below the subject, share of height") args = ap.parse_args() im = Image.open(args.subject) if im.mode != "RGBA" or im.getchannel("A").getextrema()[0] == 255: raise SystemExit("subject has no transparency; cut it out first") im = im.crop(im.getchannel("A").getbbox()) rgba = np.array(im) report = collisions(rgba) for name, (share, p995) in report.items(): print(f"{name:8s} {share:5.2f}% of the subject would key out (99.5th pct excess {p995:.0f})") key = min(report, key=lambda k: report[k][0]) if args.key == "auto" else args.key if report[key][0] > 0.5: print(f"warning: {report[key][0]:.2f}% of the subject is close to {key}; the key will bite into it") W, H = (int(v) for v in args.size.lower().split("x")) s = args.fit * H / im.height if im.width * s > W * 0.94: s = W * 0.94 / im.width im = im.resize((round(im.width * s), round(im.height * s)), Image.LANCZOS) x = (W - im.width) // 2 y = H - im.height - round(args.margin * H) if args.anchor == "bottom" else (H - im.height) // 2 plate = Image.new("RGBA", (W, H), PLATES[key] + (255,)) # hard-edged alpha: soft edges pre-blended with the plate colour teach the model a fringe a = np.array(im.getchannel("A")) im.putalpha(Image.fromarray(np.where(a > 127, 255, 0).astype(np.uint8))) plate.alpha_composite(im, (x, y)) plate.convert("RGB").save(args.out) print(f"key {key}: wrote {args.out} {W}x{H}, subject {im.width}x{im.height} at ({x},{y})") if __name__ == "__main__": main()skills/alpha-cutout/assets/alpha-video.js4.6 KBd5a355f904a5upstream ↗
// Plays a "stacked alpha" MP4 (colour in the top half, matte as grey in the bottom half) with // real transparency on a <canvas>. Plain H.264 plus a two-line shader works in every browser, // Safari included, where VP9 alpha does not. // // import { AlphaVideo } from "./alpha-video.js"; // const hero = new AlphaVideo(document.querySelector("canvas.hero"), "hero-1080.mp4"); // function frame() { hero.draw(); requestAnimationFrame(frame); } // or call draw() from your loop // // For scroll scrubbing, pass { loop: false, autoplay: false } and call hero.seek(seconds). // Give the canvas its size with CSS; resize() matches the backing store to it (DPR capped at 2). const VERT = ` attribute vec2 p; varying vec2 uv; void main() { uv = vec2(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5); gl_Position = vec4(p, 0.0, 1.0); }`; const FRAG = ` precision mediump float; uniform sampler2D tex; varying vec2 uv; void main() { vec3 c = texture2D(tex, vec2(uv.x, uv.y * 0.5)).rgb; float a = texture2D(tex, vec2(uv.x, 0.5 + uv.y * 0.5)).g; gl_FragColor = vec4(c * a, a); }`; export class AlphaVideo { constructor(canvas, src, { loop = true, autoplay = true } = {}) { this.canvas = canvas; this.ready = false; this.visible = true; const gl = canvas.getContext("webgl", { premultipliedAlpha: true, alpha: true, antialias: false }); this.gl = gl; if (!gl) return; const sh = (type, source) => { const s = gl.createShader(type); gl.shaderSource(s, source); gl.compileShader(s); return s; }; const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, VERT)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, FRAG)); gl.linkProgram(prog); gl.useProgram(prog); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); const loc = gl.getAttribLocation(prog, "p"); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); this.tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.tex); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); const v = document.createElement("video"); Object.assign(v, { muted: true, loop, playsInline: true, autoplay, preload: "auto", crossOrigin: "anonymous" }); v.setAttribute("muted", ""); v.setAttribute("playsinline", ""); v.src = src; this.video = v; this.lastTime = -1; v.addEventListener("loadeddata", () => { this.ready = true; this.onready && this.onready(); }, { once: true }); if (autoplay) v.play().catch(() => {}); this.resize(); new ResizeObserver(() => this.resize()).observe(canvas); // a looping hero off screen still decodes every frame; pause it new IntersectionObserver(([e]) => { this.visible = e.isIntersecting; if (!autoplay) return; this.visible ? this.play() : this.pause(); }, { rootMargin: "100px" }).observe(canvas); } resize() { const dpr = Math.min(window.devicePixelRatio || 1, 2); const w = Math.round(this.canvas.clientWidth * dpr), h = Math.round(this.canvas.clientHeight * dpr); if (w && h && (this.canvas.width !== w || this.canvas.height !== h)) { this.canvas.width = w; this.canvas.height = h; this.lastTime = -1; } } play() { this.video && this.video.paused && this.video.play().catch(() => {}); } pause() { this.video && !this.video.paused && this.video.pause(); } // scrubbing: seconds into the clip; draw() picks the new frame up once it has decoded seek(t) { const v = this.video; if (!v || !v.duration) return; const target = Math.min(Math.max(t, 0), v.duration - 0.001); if (Math.abs(v.currentTime - target) > 0.001 && !v.seeking) v.currentTime = target; } draw() { const gl = this.gl; if (!gl || !this.ready || !this.visible || this.video.readyState < 2) return; // upload only when the video moved on: texImage2D from video is the expensive part if (this.video.currentTime === this.lastTime && this.video.paused) return; this.lastTime = this.video.currentTime; gl.viewport(0, 0, this.canvas.width, this.canvas.height); gl.bindTexture(gl.TEXTURE_2D, this.tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.video); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } }skills/alpha-cutout/agents/openai.yaml136 B69b5830aaaabupstream ↗
interface: display_name: "Alpha Cutout" short_description: "Key generated clips into transparent video that plays in every browser"README.mdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
Plain-text copy, no login: /skill-bundles/moh4696-open-3d-skills-skills-alpha-cutout.md