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
2 changes: 1 addition & 1 deletion api/mcp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion skills/hypercodemotion/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Use this Skill when asked to create, improve, extend, combine, or troubleshoot e
2. **Brief and inputs** — Identify purpose, audience, distribution format, duration, fps, language, brand, CTA (only when requested), narration and exact data. Analyze user-provided reference images/video, scripts, logos, visual assets and any existing JSON. Distinguish factual content from creative decisions. Ask only for information that blocks progress.
3. **Art direction** — Define visual concept, typography hierarchy, spacing grid, palette, contrast, material, rhythm, transition vocabulary and continuity. Reuse the actual reference's visual intent; do not invent a brand or factual measurements. When the request names a Studio gallery template (an id such as `mix-promo-editorial`), call `get_template` with that id and the video's format, and follow its design: palette, fonts, frame, scene system and motion; replace all its texts.
4. **Storyboard and scenes** — Create sequential beats with one communicative objective each. Decide which objects persist and transform between beats versus which appear only within one scene. Allocate time to reading and transitions; storyboard timing must fit total duration. Join the beats with Studio scene transitions (`scenes[i].transition`: `fade`, `slide`, `push`, `wipe`, `clockWipe`, `iris`, `zoom`, `flip`, `blur`; exact shape in [motion-design](references/motion-design.md#scene-transitions)).
5. **Composition plan** — Default to **HTML scenes**: one HTML document per scene in `docs`, each visible piece marked `data-hcm` and animated by its own `html` layer (see [html-scenes](references/html-scenes.md) and [the example](examples/html-scene-launch.json)). Use native layers (`text`, `rect`, `particles`…) when a piece must be edited as geometry or needs per-property keyframes. Plan layer IDs, order, scene boundaries, assets, effects and audio. For a distinctive look (riso print, springs, camera, audio, UI mockups, per-scene backgrounds, the 13 style looks) or the finish of a talking-head video (`autoedit`: captions with emphasis, zooms, motion screens, sound effects) call `get_motion_recipe` first: no name gives the index, a name gives validated JSON to adapt.
5. **Composition plan** — Default to **HTML scenes**: one HTML document per scene in `docs`, each visible piece marked `data-hcm` and animated by its own `html` layer (see [html-scenes](references/html-scenes.md) and [the example](examples/html-scene-launch.json)). Use native layers (`text`, `rect`, `particles`…) when a piece must be edited as geometry or needs per-property keyframes. Plan layer IDs, order, scene boundaries, assets, effects and audio. For a distinctive look (riso print, springs, camera, audio, UI mockups, per-scene backgrounds, the 13 style looks) or the finish of a talking-head video (`autoedit`: captions with emphasis, zooms, motion screens, sound effects) or any **glitch** effect (call `get_motion_recipe("glitch")` before drawing one: bursts, RGB split, slices, stepped frames, clean resolve; never a plain flicker) call `get_motion_recipe` first: no name gives the index, a name gives validated JSON to adapt.
6. **JSON generation** — Produce **one complete, standalone composition** with `layers` at the root. To change an existing project, return the whole updated composition (keep unchanged layers, IDs and assets as they were), not a patch: a `.hcmotion` file can only carry a full composition. In `code` layers and `components[x].draw`, never declare a name the engine already provides (`ctx`, `S`, `H`, `t`, `lt`, `dur`, `w`, `h`, `p`, `color`, `amount`, `amp`, `beat`, `hit`, `audio`, `c` and the helpers such as `range`, `lerp`, `rand`; full list in [composition.md](references/composition.md#code-layers-and-components)): use `SEG`, `ranges`, `pts`.
7. **MCP validation** — The HyperCodeMotion MCP (`https://mcp.hypercodemotion.com/mcp`) is required. Call `get_capabilities` before selecting layer/effect fields; always run `validate_json` on the finished draft before `create_hcmotion`: fix every error and warning, then go through the checklist it returns and correct the composition (overlaps, hierarchy, motion, audio) until it holds. Quality matters more than saving tokens: a second pass is expected. **Do not claim validation if no call occurred.**
8. **Studio handoff** — Call `create_hcmotion` with the finished JSON. If `created` is false, fix every error and call it again. Give the user the two links it returns: `open` (opens the project in Studio) and `download` (the `.hcmotion` file, to keep or load later with «Cargar proyecto»), and say both expire in 24 hours. **Never paste the composition JSON for the user to copy:** Studio only opens `.hcmotion` files. If the MCP is not connected, stop and ask the user to connect it instead of returning JSON. Never claim a perfect render from MCP results alone.
Expand Down
31 changes: 30 additions & 1 deletion src/recipes.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,35 @@ const autoeditScreens = {
},
};

// ---------- glitch ----------
// Owner feedback 2026-11-02: a glitch that is only flicker/jitter looks amateur. Bursts, RGB split, slices, steps, clean resolve.
const GLITCH_CODE = "const G=[[1,1.08],[1.14,1.26]];let k=-1;for(let i=0;i<G.length;i++)if(lt>=G[i][0]&&lt<G[i][1])k=i;if(k<0)return;const fr=floor(lt*12);for(let i=0;i<6;i++){const sy=floor(rand(i,fr)*h);const sh=20+rand(i+9,fr)*140;const dx=(rand(i+20,fr)-0.5)*120;ctx.fillStyle=i%2?'rgba(0,255,255,0.35)':'rgba(255,0,255,0.35)';ctx.fillRect(dx,sy,w,sh);}if(rand(99,fr)>0.5){ctx.fillStyle=rand(98,fr)>0.5?'#00FFFF':'#FF00FF';ctx.fillRect(rand(97,fr)*w*0.7,rand(96,fr)*h,w*0.3,30);}ctx.fillStyle='rgba(255,255,255,0.12)';for(let y=0;y<h;y+=6)ctx.fillRect(0,y,w,2);";
const hk = (pairs) => ({ kf: pairs.map(([t, v]) => ({ t, v, ease: "hold" })) });
const glitch = {
summary: "Professional glitch: irregular bursts, real RGB split, slice displacement, stepped frames, impact and a clean resolve.",
notes: [
"Bursts, never a loop: 150-400 ms in total, 2-3 irregular hits with gaps (e.g. 80 ms on, 60 ms off, 120 ms on). Never longer than 600 ms.",
"RGB split: duplicate the layer in red (#FF0000) and cyan/blue (#00FFFF) copies with blend \"screen\" under the white one; red and blue move in OPPOSITE directions, 6-20 px, horizontal only, and the offset changes every frame of the burst. Outside the burst offset 0 (copies hidden behind the original).",
"Slice displacement (the key ingredient): 4-8 horizontal strips of random height, each shifted in X on its own by +-10-60 px. In an HTML scene: N copies of the element, each clip-path: inset(top 0 bottom 0) and its own translateX. Over native layers: a code layer drawing offset strips (below).",
"Stepped frames: during the burst animate at 12-15 fps with ease \"hold\" keys every 1/12 s (code layers: floor(lt*12) as the seed); the rest of the video stays fluid.",
"Texture: scanlines at 10-15 % opacity, noise/grain, 1-2 solid cyan/magenta blocks lasting 1-2 frames.",
"Impact: scale jump 1.02-1.05 and 1-2 frames inverted or boosted brightness, synced to a glitch sfx or an audio cut.",
"Intent: use it for entrances, scene changes or to hit a keyword, on the beat or the stressed syllable, not at random.",
"Resolve clean: end on a stable sharp frame (offsets 0, scale 1) with a small settle (1.03 -> 1 spring snappy).",
"Deterministic: seeded randomness only (rand(i,frame), noise); never Math.random, so preview = export.",
"Anti-patterns: only an opacity flicker; the whole layer jittering uniformly; a static RGB offset; a glitch lasting > 600 ms or looping.",
],
fragment: {
background: "#07070C",
layers: [
{ id: "gR", type: "text", name: "Glitch · rojo", start: 0, end: 6, props: { text: "GLITCH", x: hk([[0, 540], [1, 526], [1.04, 552], [1.08, 540], [1.14, 548], [1.18, 530], [1.22, 556], [1.26, 540]]), y: 960, size: 170, weight: 800, color: "#FF0000", blend: "screen" } },
{ id: "gB", type: "text", name: "Glitch · azul", start: 0, end: 6, props: { text: "GLITCH", x: hk([[0, 540], [1, 556], [1.04, 530], [1.08, 540], [1.14, 532], [1.18, 552], [1.22, 526], [1.26, 540]]), y: 960, size: 170, weight: 800, color: "#00FFFF", blend: "screen" } },
{ id: "gW", type: "text", name: "Glitch · texto", start: 0, end: 6, props: { text: "GLITCH", x: hk([[0, 540], [1, 560], [1.04, 510], [1.08, 540], [1.14, 515], [1.18, 570], [1.22, 540]]), y: 960, size: 170, weight: 800, color: "#FFFFFF", scale: { kf: [{ t: 0, v: 1, ease: "hold" }, { t: 1, v: 1.04, ease: "hold" }, { t: 1.08, v: 1, ease: "hold" }, { t: 1.14, v: 1.05, ease: "hold" }, { t: 1.26, v: 1.03 }, { t: 1.5, v: 1 }], spring: "snappy" } } },
{ id: "gSlices", type: "code", name: "Glitch · franjas", start: 0, end: 6, props: { x: 540, y: 960, w: 1080, h: 1920, color: "#FFFFFF", amount: 1 }, code: GLITCH_CODE },
],
},
};

// ---------- looks ----------
/** Title size caps so "Big idea" fits the 928 px safe width of a 1080 canvas (checked by tests/recipes.test.js). */
const TITLE_MAX = { medianoche: 130, maximalista: 130 };
Expand Down Expand Up @@ -327,7 +356,7 @@ function fragmentText(f) {
return out.join("\n");
}

const STATIC = { autoedit, "autoedit-screens": autoeditScreens, riso, choreography, audio, camera, "code-texture": codeTexture, "ui-mockup": uiMockup, "scene-bg": sceneBg };
const STATIC = { autoedit, glitch, "autoedit-screens": autoeditScreens, riso, choreography, audio, camera, "code-texture": codeTexture, "ui-mockup": uiMockup, "scene-bg": sceneBg };

function lookRecipe(look) {
const C = look.colors, F = look.fonts, m = look.motion, s = look.shape;
Expand Down
20 changes: 17 additions & 3 deletions tests/recipes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ const call = async (name, args = {}) => {

test("there are 9 topic recipes, the looks table and one recipe per look", () => {
assert.equal(LOOKS.length, 13);
for (const n of ["autoedit", "autoedit-screens", "riso", "choreography", "audio", "camera", "code-texture", "ui-mockup", "scene-bg", "looks"]) assert.ok(RECIPE_NAMES.includes(n), n);
for (const n of ["glitch", "autoedit", "autoedit-screens", "riso", "choreography", "audio", "camera", "code-texture", "ui-mockup", "scene-bg", "looks"]) assert.ok(RECIPE_NAMES.includes(n), n);
for (const l of LOOKS) assert.ok(RECIPE_NAMES.includes("look:" + l.id), l.id);
assert.equal(RECIPE_NAMES.length, 10 + 13);
assert.equal(RECIPE_NAMES.length, 11 + 13);
});

test("every recipe passes validateJson with no errors and no warning but the contract notice", () => {
Expand All @@ -48,7 +48,7 @@ test("recipe sizes stay on demand: topic recipes under ~800 tokens, looks table
// 1 token ≈ 3.2 characters for JSON/HTML (cl100k measured on these recipes); generous bounds
for (const name of RECIPE_NAMES) {
const chars = recipeText(name).length;
assert.ok(chars < (name === "looks" ? 5200 : name === "riso" || name.startsWith("autoedit") ? 4400 : 3200), name + " " + chars);
assert.ok(chars < (name === "looks" ? 5200 : name === "riso" || name.startsWith("autoedit") || name === "glitch" ? 4400 : 3200), name + " " + chars);
}
});

Expand Down Expand Up @@ -128,3 +128,17 @@ test("create_hcmotion returns the checklist once the file is created, and none w
const bad = await call("create_hcmotion", { json: { version: 1 } });
assert.equal(JSON.parse(bad.content[0].text).checklist, undefined);
});

test("glitch recipe: bursts under 600 ms, opposite RGB split, slices, hold steps, seeded", () => {
const t = recipeText("glitch");
for (const w of ["150-400 ms", "OPPOSITE", "4-8 horizontal strips", "12-15 fps", "Anti-patterns", "Math.random"]) assert.ok(t.includes(w), w);
const L = Object.fromEntries(recipeFragment("glitch").layers.map((l) => [l.id, l]));
const kx = (id) => L[id].props.x.kf;
for (const id of ["gR", "gB", "gW"]) assert.ok(kx(id).every((k, i, a) => i === a.length - 1 || k.ease === "hold"), id);
const span = kx("gW").at(-1).t - kx("gW")[1].t;
assert.ok(span > 0.15 && span <= 0.6, "burst " + span);
for (let i = 1; i < kx("gR").length - 1; i++) assert.ok(Math.sign(kx("gR")[i].v - 540) === -Math.sign(kx("gB")[i].v - 540), "opposite " + i);
assert.equal(L.gR.props.blend, "screen");
assert.ok(!L.gSlices.code.includes("Math.random") && L.gSlices.code.includes("rand("));
assert.ok(motionRecipe().text.includes("glitch:"));
});
Loading