diff --git a/api/mcp.js b/api/mcp.js index 19925c4..1803136 100644 --- a/api/mcp.js +++ b/api/mcp.js @@ -20,7 +20,7 @@ async function createHcmotion(args){ } const tools=[ {name:"get_capabilities",title:"HyperCodeMotion Capabilities",description:"List the HyperCodeMotion JSON formats, layer types and properties, and effect presets that HyperCodeMotion Studio supports, how to reference files the user keeps in Studio by id, the screen rule that decides type size (field screen: the canvas wins over a requested size), a runtime layer that keeps an existing Lottie, glTF or GSAP file and seeks it, scene transitions (field transitions), a short start guide (field start), the names code layers and components must not redeclare (field reservedCodeNames), and the Studio contract revision they come from. Call it before designing.",annotations:{title:"HyperCodeMotion Capabilities",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{},additionalProperties:false},outputSchema:{type:"object",properties:{contractVersion:{type:"string"},source:{type:"string"},sourceSchemaSha:{type:"string"},sourcePresetsSha:{type:"string"},formats:{type:"array",items:{type:"string"}},commonProperties:{type:"array",items:{type:"string"}},layerProperties:{type:"object",additionalProperties:{type:"array",items:{type:"string"}}},layerPropertiesNote:{type:"string"},start:{type:"string"},transitions:{type:"object"},presetSlots:{type:"object",additionalProperties:{type:"array",items:{type:"string"}}},userMaterials:{type:"string"},reservedCodeNames:{type:"object",properties:{rule:{type:"string"},codeLayer:{type:"array",items:{type:"string"}},component:{type:"array",items:{type:"string"}}}},screen:{type:"string"},fullyValidated:{type:"boolean"}},required:["contractVersion","formats","layerProperties","presetSlots","screen","fullyValidated"]}}, -{name:"get_motion_recipe",title:"Get motion recipe",description:"Real, validated JSON/HTML/CSS patterns for looks the basics do not give: riso print, choreography, audio, camera, code textures, UI mockups, per-scene backgrounds, 13 style looks. Without name: the index.",annotations:{title:"Get motion recipe",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{name:{type:"string",description:"Recipe from the index, e.g. riso or look:cartel."}},additionalProperties:false}}, +{name:"get_motion_recipe",title:"Get motion recipe",description:"Real, validated JSON/HTML/CSS patterns for looks the basics do not give: riso print, choreography, audio, camera, code textures, UI mockups, per-scene backgrounds, 13 style looks, professional glitch. Without name: the index.",annotations:{title:"Get motion recipe",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{name:{type:"string",description:"Recipe from the index, e.g. riso or look:cartel."}},additionalProperties:false}}, {name:"create_hcmotion",title:"Create HyperCodeMotion project file",description:"Validate a finished HyperCodeMotion composition and package it as a .hcmotion project file, the format HyperCodeMotion Studio opens. If the composition has problems, returns created: false with the errors to fix and stores nothing. On success, stores the file temporarily and returns an `open` link (opens the project in HyperCodeMotion Studio) and a `download` link; both expire 24 hours after creation. It does not render video or generate media.",annotations:{title:"Create HyperCodeMotion project file",readOnlyHint:false,destructiveHint:false,idempotentHint:false,openWorldHint:false},inputSchema:{type:"object",properties:{json:{description:"The full composition (version, name, width, height, fps, duration, background, layers…), as an object or JSON-encoded string.",anyOf:[{type:"object"},{type:"string"}]},name:{type:"string",description:"Optional project name."}},required:["json"],additionalProperties:false},outputSchema:{type:"object",properties:{created:{type:"boolean"},download:{type:"string"},open:{type:"string"},expiresAt:{type:"string"},bytes:{type:"number"},errors:{type:"array",items:{type:"object",properties:{path:{type:"string"},code:{type:"string"},message:{type:"string"}}}},warnings:{type:"array",items:{type:"object",properties:{path:{type:"string"},code:{type:"string"},message:{type:"string"}}}}},required:["created","errors","warnings"]}}, {name:"get_template",title:"Get HyperCodeMotion template",description:"Return the full design of a template from the HyperCodeMotion Studio gallery, by its id and format: brief (palette, fonts, frame, rhythm), scenes with timings, and the composition with its HTML scene documents and layers. Use it as the design reference when asked to follow a template; it does not create or store anything.",annotations:{title:"Get HyperCodeMotion template",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{template:{type:"string",description:"Template id, such as \"mix-promo-editorial\"."},format:{type:"string",enum:TEMPLATE_FORMATS,description:"Aspect ratio of the video being made. Defaults to 16:9."}},required:["template"],additionalProperties:false},outputSchema:{type:"object",properties:{found:{type:"boolean"},template:{type:"string"},name:{type:"string"},format:{type:"string"},brief:{type:"string"},scenes:{type:"array",items:{type:"object"}},composition:{type:"object"},error:{type:"string"}},required:["found"]}}, {name:"validate_json",title:"Validate HyperCodeMotion JSON",description:"Check a composition (layers), design (elements), or improvement patch (add/change/remove/sfx) and return actionable errors and warnings. Rejects measurable text that cannot fit the canvas safe area, text that crosses the canvas, and line-height below 1. Warns when a code layer or component declares a name the engine already provides (RESERVED_NAME_DECLARED). This is not proof of a successful render.",annotations:{title:"Validate HyperCodeMotion JSON",readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:"object",properties:{json:{description:"HyperCodeMotion JSON object or JSON-encoded string to validate.",anyOf:[{type:"object"},{type:"string"}]},composition:{type:"object",description:"Optional original composition used to resolve patch references and timings."}},required:["json"],additionalProperties:false},outputSchema:{type:"object",properties:{valid:{type:"boolean"},format:{type:"string"},fullyValidated:{type:"boolean"},mode:{type:"string"},contractVersion:{type:"string"},errors:{type:"array",items:{type:"object",properties:{path:{type:"string"},code:{type:"string"},message:{type:"string"}}}},warnings:{type:"array",items:{type:"object",properties:{path:{type:"string"},code:{type:"string"},message:{type:"string"}}}}},required:["valid","format","fullyValidated","mode","errors","warnings","contractVersion"]}}, diff --git a/skills/hypercodemotion/SKILL.md b/skills/hypercodemotion/SKILL.md index 5e3e064..9153a02 100644 --- a/skills/hypercodemotion/SKILL.md +++ b/skills/hypercodemotion/SKILL.md @@ -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. diff --git a/src/recipes.js b/src/recipes.js index 420cee7..ba0e34b 100644 --- a/src/recipes.js +++ b/src/recipes.js @@ -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[i][0]&<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 ({ 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 }; @@ -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; diff --git a/tests/recipes.test.js b/tests/recipes.test.js index f22ad14..36d548d 100644 --- a/tests/recipes.test.js +++ b/tests/recipes.test.js @@ -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", () => { @@ -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); } }); @@ -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:")); +});