@pixelvibing/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1077,7 +1077,7 @@ const HUD_PIECE_NAMES = ["health-bar", "mana-bar", "button", "inventory-slot", "
1077
1077
  const ICON_SIZES = [16, 32, 64];
1078
1078
  const DEFAULT_ICON_SIZE = 32;
1079
1079
  const MAX_ICONS = 16;
1080
- const SET_NAMES = { hud: "ui/hud", icons: "ui/icon" };
1080
+ const SET_NAMES = { hud: "ui/hud", icons: "ui/icon", "ui-sketch": "ui/sketch" };
1081
1081
  function setPiecesOf(kind, count, data) {
1082
1082
  if (SET_NAMES[kind] === void 0) return null;
1083
1083
  const pieces = data.pieces;
@@ -2238,9 +2238,9 @@ function terrainTable(names) {
2238
2238
  const byId = /* @__PURE__ */ new Map();
2239
2239
  for (const [id2, name] of names) {
2240
2240
  const { tileset, inner } = splitTileset(name);
2241
- const whole = SOLID.exec(inner);
2242
- if (whole !== null) {
2243
- const terrain = scopedTerrain(tileset, whole[1]);
2241
+ const whole2 = SOLID.exec(inner);
2242
+ if (whole2 !== null) {
2243
+ const terrain = scopedTerrain(tileset, whole2[1]);
2244
2244
  terrains.add(terrain);
2245
2245
  solid.set(terrain, id2);
2246
2246
  byId.set(id2, terrain);
@@ -2275,8 +2275,8 @@ function checked(items, side, gap) {
2275
2275
  for (const item of items) {
2276
2276
  if (seen.has(item.name)) throw new PackError(`two pictures are named ${item.name}`);
2277
2277
  seen.add(item.name);
2278
- const whole = (value) => Number.isInteger(value) && value >= 1;
2279
- if (!whole(item.width) || !whole(item.height))
2278
+ const whole2 = (value) => Number.isInteger(value) && value >= 1;
2279
+ if (!whole2(item.width) || !whole2(item.height))
2280
2280
  throw new PackError(`the picture ${item.name} is ${item.width}×${item.height}, and a picture is whole pixels`);
2281
2281
  if (item.width > largest || item.height > largest) {
2282
2282
  throw new PackError(`the picture ${item.name} is ${item.width}×${item.height}, and a sheet of ${side} holds pictures of at most ${largest} a side`);
@@ -2804,7 +2804,7 @@ function toRgba(samples, header, palette, transparency) {
2804
2804
  const channels = CHANNELS[colorType];
2805
2805
  const out = new Uint8ClampedArray(width * height * 4);
2806
2806
  const high = (index) => samples[wide ? index * 2 : index];
2807
- const whole = (index) => wide ? samples[index * 2] << 8 | samples[index * 2 + 1] : samples[index];
2807
+ const whole2 = (index) => wide ? samples[index * 2] << 8 | samples[index * 2 + 1] : samples[index];
2808
2808
  const keyed = (at) => transparency === null ? null : transparency[at] << 8 | transparency[at + 1];
2809
2809
  const grey = colorType === 0 ? keyed(0) : null;
2810
2810
  const rgb = colorType === 2 && transparency !== null && transparency.length >= 6 ? [keyed(0), keyed(2), keyed(4)] : null;
@@ -2814,14 +2814,14 @@ function toRgba(samples, header, palette, transparency) {
2814
2814
  if (colorType === 6) {
2815
2815
  out.set([high(first), high(first + 1), high(first + 2), high(first + 3)], at);
2816
2816
  } else if (colorType === 2) {
2817
- const clear = rgb !== null && whole(first) === rgb[0] && whole(first + 1) === rgb[1] && whole(first + 2) === rgb[2];
2817
+ const clear = rgb !== null && whole2(first) === rgb[0] && whole2(first + 1) === rgb[1] && whole2(first + 2) === rgb[2];
2818
2818
  out.set([high(first), high(first + 1), high(first + 2), clear ? 0 : 255], at);
2819
2819
  } else if (colorType === 4) {
2820
2820
  const value = high(first);
2821
2821
  out.set([value, value, value, high(first + 1)], at);
2822
2822
  } else if (colorType === 0) {
2823
2823
  const value = high(first);
2824
- out.set([value, value, value, grey !== null && whole(first) === grey ? 0 : 255], at);
2824
+ out.set([value, value, value, grey !== null && whole2(first) === grey ? 0 : 255], at);
2825
2825
  } else {
2826
2826
  const index = samples[first];
2827
2827
  if (palette === null || index * 3 + 2 >= palette.length) throw new PngError("the PNG names a palette entry it does not have");
@@ -3031,13 +3031,142 @@ async function startGeneration(api, project, request) {
3031
3031
  }
3032
3032
  return api.generate(request.kind, { ...request.body, ...ids, project });
3033
3033
  }
3034
+ const SKETCH_CANVASES = [
3035
+ { width: 128, height: 128 },
3036
+ { width: 256, height: 256 },
3037
+ { width: 256, height: 128 },
3038
+ { width: 128, height: 256 }
3039
+ ];
3040
+ const SKETCH_SHAPE_KINDS = ["rect", "ellipse", "polygon"];
3041
+ const MAX_SKETCH_SHAPES = 16;
3042
+ const MIN_SHAPE_SIDE = 4;
3043
+ const POLYGON_SIDES = { min: 3, max: 8 };
3044
+ const MAX_LABEL = 40;
3045
+ const MAX_CAPTION = 24;
3046
+ const MAX_NOTE = 200;
3047
+ const MAX_SKETCH_INSTRUCTION = 600;
3048
+ function polygonCorners(shape) {
3049
+ const n = shape.sides ?? POLYGON_SIDES.min;
3050
+ const cx = shape.x + shape.width / 2;
3051
+ const cy = shape.y + shape.height / 2;
3052
+ return Array.from({ length: n }, (_, i) => {
3053
+ const angle = -Math.PI / 2 + 2 * Math.PI * i / n;
3054
+ return [cx + shape.width / 2 * Math.cos(angle), cy + shape.height / 2 * Math.sin(angle)];
3055
+ });
3056
+ }
3057
+ function inShape(shape, px, py) {
3058
+ const { x, y, width: w, height: h } = shape;
3059
+ if (px < x || py < y || px >= x + w || py >= y + h) return false;
3060
+ if (shape.kind === "ellipse") return ((px - x - w / 2) / (w / 2)) ** 2 + ((py - y - h / 2) / (h / 2)) ** 2 <= 1;
3061
+ if (shape.kind === "polygon") {
3062
+ const corners = polygonCorners(shape);
3063
+ let hit = false;
3064
+ for (let i = 0, j = corners.length - 1; i < corners.length; j = i, i += 1) {
3065
+ const [xi, yi] = corners[i];
3066
+ const [xj, yj] = corners[j];
3067
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) hit = !hit;
3068
+ }
3069
+ return hit;
3070
+ }
3071
+ const r = Math.min(Math.max(shape.radius ?? 0, 0), w / 2, h / 2);
3072
+ const dx = Math.max(x + r - px, 0, px - (x + w - r));
3073
+ const dy = Math.max(y + r - py, 0, py - (y + h - r));
3074
+ return dx * dx + dy * dy <= r * r;
3075
+ }
3076
+ const whole = (value) => typeof value === "number" && Number.isInteger(value);
3077
+ function shapeProblem(shape, index, canvas) {
3078
+ const label = typeof shape.label === "string" ? shape.label.trim() : "";
3079
+ if (label === "") return `shape ${index + 1} has no label`;
3080
+ const named = `"${label}"`;
3081
+ if (label.length > MAX_LABEL) return `${named} is a label of ${label.length} characters; a label is at most ${MAX_LABEL}`;
3082
+ if (!SKETCH_SHAPE_KINDS.includes(shape.kind))
3083
+ return `${named} is a ${String(shape.kind)}; a shape is a rect, an ellipse or a polygon`;
3084
+ if (![shape.x, shape.y, shape.width, shape.height].every(whole)) return `${named} is not on whole pixels`;
3085
+ if (shape.width < MIN_SHAPE_SIDE || shape.height < MIN_SHAPE_SIDE) {
3086
+ return `${named} is ${shape.width} × ${shape.height}; a shape is at least ${MIN_SHAPE_SIDE} pixels on a side`;
3087
+ }
3088
+ if (shape.x < 0 || shape.y < 0 || shape.x + shape.width > canvas.width || shape.y + shape.height > canvas.height) {
3089
+ return `${named} runs off the ${canvas.width} × ${canvas.height} canvas`;
3090
+ }
3091
+ if (shape.radius !== void 0 && (!whole(shape.radius) || shape.radius < 0))
3092
+ return `${named} has a corner radius of ${shape.radius}; a radius is a whole number of pixels`;
3093
+ if (shape.kind === "polygon" && (!whole(shape.sides) || shape.sides < POLYGON_SIDES.min || shape.sides > POLYGON_SIDES.max)) {
3094
+ return `${named} is a polygon of ${shape.sides ?? "no"} sides; a polygon has ${POLYGON_SIDES.min} to ${POLYGON_SIDES.max}`;
3095
+ }
3096
+ if (shape.caption !== void 0 && shape.caption.length > MAX_CAPTION) {
3097
+ return `${named} has a caption of ${shape.caption.length} characters; a caption is at most ${MAX_CAPTION}`;
3098
+ }
3099
+ if (shape.note !== void 0 && shape.note.length > MAX_NOTE) return `${named} has a note of ${shape.note.length} characters; a note is at most ${MAX_NOTE}`;
3100
+ if (shape.border !== void 0) {
3101
+ if (!whole(shape.border) || shape.border < 1) return `${named} has a border of ${shape.border}; a border is at least 1 pixel`;
3102
+ if (2 * shape.border >= Math.min(shape.width, shape.height)) return `${named} has a border of ${shape.border} pixels, which leaves no inside`;
3103
+ }
3104
+ if (shape.hollow === true && shape.border === void 0) return `${named} has a transparent inside but no border width`;
3105
+ return null;
3106
+ }
3107
+ function sketchProblem(sketch) {
3108
+ if (typeof sketch.instruction !== "string" || sketch.instruction.trim() === "") return "the sketch has no description";
3109
+ if (sketch.instruction.length > MAX_SKETCH_INSTRUCTION)
3110
+ return `the description is ${sketch.instruction.length} characters; it is at most ${MAX_SKETCH_INSTRUCTION}`;
3111
+ const { canvas, shapes } = sketch;
3112
+ if (!SKETCH_CANVASES.some((size) => size.width === canvas.width && size.height === canvas.height)) {
3113
+ return `the canvas is ${canvas.width} × ${canvas.height}; a sketch is ${SKETCH_CANVASES.map((size) => `${size.width} × ${size.height}`).join(", ")}`;
3114
+ }
3115
+ if (shapes.length === 0) return "the sketch has no shape";
3116
+ if (shapes.length > MAX_SKETCH_SHAPES) return `the sketch has ${shapes.length} shapes; it holds at most ${MAX_SKETCH_SHAPES}`;
3117
+ const labels = /* @__PURE__ */ new Map();
3118
+ for (const [index, shape] of shapes.entries()) {
3119
+ const problem = shapeProblem(shape, index, canvas);
3120
+ if (problem !== null) return problem;
3121
+ const key = shape.label.trim().toLowerCase();
3122
+ if (labels.has(key)) return `"${shape.label.trim()}" is the label of two shapes`;
3123
+ labels.set(key, shape.label.trim());
3124
+ }
3125
+ for (const [index, shape] of shapes.entries()) {
3126
+ for (let y = shape.y; y < shape.y + shape.height; y += 1) {
3127
+ for (let x = shape.x; x < shape.x + shape.width; x += 1) {
3128
+ if (!inShape(shape, x + 0.5, y + 0.5)) continue;
3129
+ const other = shapes.findIndex((them, at) => at < index && inShape(them, x + 0.5, y + 0.5));
3130
+ if (other >= 0) return `"${shapes[other]?.label.trim()}" and "${shape.label.trim()}" overlap`;
3131
+ }
3132
+ }
3133
+ }
3134
+ return null;
3135
+ }
3136
+ function sentShape(shape) {
3137
+ const caption = shape.caption?.trim() ?? "";
3138
+ const note = shape.note?.trim() ?? "";
3139
+ return {
3140
+ label: shape.label.trim(),
3141
+ kind: shape.kind,
3142
+ x: shape.x,
3143
+ y: shape.y,
3144
+ width: shape.width,
3145
+ height: shape.height,
3146
+ ...shape.kind === "rect" && (shape.radius ?? 0) > 0 ? { radius: shape.radius } : {},
3147
+ ...shape.kind === "polygon" ? { sides: shape.sides } : {},
3148
+ ...caption === "" ? {} : { caption },
3149
+ ...note === "" ? {} : { note },
3150
+ ...shape.border === void 0 ? {} : { border: shape.border },
3151
+ ...shape.hollow === true ? { hollow: true } : {}
3152
+ };
3153
+ }
3154
+ function sentSketch(sketch, description) {
3155
+ return { canvas: { ...sketch.canvas }, shapes: sketch.shapes.map(sentShape), instruction: description.trim() };
3156
+ }
3157
+ function sketchSendProblem(sketch, description) {
3158
+ return sketchProblem(sentSketch(sketch, description));
3159
+ }
3160
+ function sketchBody(sketch, description, quality) {
3161
+ return { ...sentSketch(sketch, description), quality };
3162
+ }
3034
3163
  const LARGEST_FOOTPRINT = {
3035
3164
  cols: Math.min(Math.floor(WINDOW_PX.width / SCALES.at(-1)), Math.floor(MAX_ASSEMBLED_PX / TILE_SIZE)),
3036
3165
  rows: Math.min(Math.floor(WINDOW_PX.height / SCALES.at(-1)), Math.floor(MAX_ASSEMBLED_PX / TILE_SIZE))
3037
3166
  };
3038
3167
  function footprintProblem(footprint) {
3039
- const whole = (value) => Number.isInteger(value) && value >= 1;
3040
- if (!whole(footprint.cols) || !whole(footprint.rows)) return "a footprint is a whole number of tiles wide and tall, each at least one";
3168
+ const whole2 = (value) => Number.isInteger(value) && value >= 1;
3169
+ if (!whole2(footprint.cols) || !whole2(footprint.rows)) return "a footprint is a whole number of tiles wide and tall, each at least one";
3041
3170
  if (footprint.cols > LARGEST_FOOTPRINT.cols || footprint.rows > LARGEST_FOOTPRINT.rows || drawCanvas(footprint) === null) {
3042
3171
  return `a ${footprint.cols}×${footprint.rows} tile footprint is larger than an object is made; at most ${LARGEST_FOOTPRINT.cols}×${LARGEST_FOOTPRINT.rows} tiles`;
3043
3172
  }
@@ -3097,7 +3226,8 @@ const GENERATED = {
3097
3226
  "level-design": { references: 3, words: "a level design" },
3098
3227
  hud: { references: 0, words: "a HUD set" },
3099
3228
  icons: { references: 0, words: "an icon set" },
3100
- "hud-mockup": { references: 3, words: "a HUD mockup" }
3229
+ "hud-mockup": { references: 3, words: "a HUD mockup" },
3230
+ "ui-sketch": { references: 3, words: "a UI sketch" }
3101
3231
  };
3102
3232
  function listOf(given, what) {
3103
3233
  const items = given.split(",").map((item) => item.trim());
@@ -3130,6 +3260,48 @@ function interfaceFields(kind, args) {
3130
3260
  if (!ICON_SIZES.includes(size)) throw new CliError(`an icon's --size is ${ICON_SIZES.join(", ")} px`);
3131
3261
  return { fields: { names, purpose, size }, words: `${names.length} ${purpose} icon${names.length === 1 ? "" : "s"} at ${size} px: ${names.join(", ")}` };
3132
3262
  }
3263
+ const MAX_SKETCH_BYTES = 256 * 1024;
3264
+ async function sketchOf(context, path, instruction) {
3265
+ if (path === void 0) throw new CliError("lay the sketch out in a JSON file and name it with --sketch, such as --sketch hud.json");
3266
+ const file = resolve(context.io.cwd, path);
3267
+ let text2;
3268
+ try {
3269
+ const found = await stat(file);
3270
+ if (!found.isFile()) throw new CliError(`the sketch ${path} is not a file`);
3271
+ if (found.size > MAX_SKETCH_BYTES) throw new CliError(`the sketch ${path} is larger than ${MAX_SKETCH_BYTES} bytes`);
3272
+ text2 = await readFile(file, "utf8");
3273
+ } catch (error) {
3274
+ if (error instanceof CliError) throw error;
3275
+ throw new CliError(`the sketch ${path} cannot be read`);
3276
+ }
3277
+ let parsed2;
3278
+ try {
3279
+ parsed2 = JSON.parse(text2);
3280
+ } catch {
3281
+ throw new CliError(`the sketch ${path} is not JSON`);
3282
+ }
3283
+ const given = parsed2;
3284
+ const canvas = given?.canvas;
3285
+ if (typeof canvas !== "object" || canvas === null || typeof canvas.width !== "number" || typeof canvas.height !== "number") {
3286
+ throw new CliError(`the sketch ${path} has no canvas: { "canvas": { "width": 256, "height": 256 }, "shapes": [...] }`);
3287
+ }
3288
+ if (!Array.isArray(given.shapes)) throw new CliError(`the sketch ${path} has no list of shapes`);
3289
+ const words = (value) => value === void 0 || typeof value === "string";
3290
+ for (const [index, shape] of given.shapes.entries()) {
3291
+ const each = shape;
3292
+ if (typeof each !== "object" || each === null || typeof each.label !== "string" || !words(each.caption) || !words(each.note)) {
3293
+ throw new CliError(`shape ${index + 1} of the sketch ${path} is not a shape: it needs a label, and a caption and a note are text`);
3294
+ }
3295
+ }
3296
+ const sketch = { canvas: { width: canvas.width, height: canvas.height }, shapes: given.shapes };
3297
+ const problem = sketchSendProblem(sketch, instruction);
3298
+ if (problem !== null) throw new CliError(problem);
3299
+ const labels = sketch.shapes.map((shape) => shape.label.trim());
3300
+ return {
3301
+ sketch,
3302
+ words: `a UI sketch of ${labels.length} shape${labels.length === 1 ? "" : "s"} (${labels.join(", ")}) on ${canvas.width} × ${canvas.height}`
3303
+ };
3304
+ }
3133
3305
  const MAX_DESIGN_TILES = 64;
3134
3306
  const MAX_REFERENCE_BYTES = 16 * 1024 * 1024;
3135
3307
  function sizeOf(given, what) {
@@ -3161,6 +3333,20 @@ async function requestOf(context, kind, args) {
3161
3333
  const paths = stringsOf(args, "reference");
3162
3334
  if (paths.length > known.references)
3163
3335
  throw new CliError(known.references === 0 ? `${known.words} takes no reference` : `${known.words} takes at most ${known.references} references`);
3336
+ if (kind === "ui-sketch") {
3337
+ for (const flag of ["pieces", "names", "purpose", "size", "tiles"])
3338
+ if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is not a UI sketch's: its shapes are in the --sketch file`);
3339
+ const { sketch, words: words2 } = await sketchOf(context, stringOf(args, "sketch"), instruction);
3340
+ const styles = [];
3341
+ for (const path of paths) styles.push(await referenceOf(context, path));
3342
+ return {
3343
+ kind,
3344
+ body: sketchBody(sketch, instruction, quality),
3345
+ ...styles.length > 0 ? { images: { references: styles } } : {},
3346
+ words: `${words2}${styles.length > 0 ? `, in the style of ${styles.length} reference${styles.length === 1 ? "" : "s"}` : ""}: ${instruction}`
3347
+ };
3348
+ }
3349
+ if (stringOf(args, "sketch") !== void 0) throw new CliError("--sketch is a UI sketch's");
3164
3350
  if (set) {
3165
3351
  const { fields, words: words2 } = interfaceFields(kind, args);
3166
3352
  if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a level design's");
@@ -3215,10 +3401,14 @@ async function followed(context, platform, jobId) {
3215
3401
  }
3216
3402
  const generate = {
3217
3403
  name: "generate",
3218
- summary: "Generate an object, a creature, a level design, a HUD set, icons or a HUD mockup on the platform, then pull it",
3219
- usage: "<object|creature|level-design|hud|icons|hud-mockup> [--instruction <text>] [--size <c>x<r>|<px>] [--tiles <c>x<r>] [--pieces <list>] [--names <list>] [--purpose skill|item] [--reference <png>]… [--quality <q>] [--yes]",
3404
+ summary: "Generate an object, a creature, a level design, a HUD set, icons, a HUD mockup or a UI sketch on the platform, then pull it",
3405
+ usage: "<object|creature|level-design|hud|icons|hud-mockup|ui-sketch> [--instruction <text>] [--size <c>x<r>|<px>] [--tiles <c>x<r>] [--pieces <list>] [--names <list>] [--purpose skill|item] [--sketch <json>] [--reference <png>]… [--quality <q>] [--yes]",
3220
3406
  options: {
3221
- instruction: { type: "string", value: "<text>", description: "what to make, in your own words; a HUD set's or icon set's theme" },
3407
+ instruction: {
3408
+ type: "string",
3409
+ value: "<text>",
3410
+ description: "what to make, in your own words; a HUD set's or icon set's theme; a UI sketch's set, in a line"
3411
+ },
3222
3412
  size: {
3223
3413
  type: "string",
3224
3414
  value: "<cols>x<rows>|<px>",
@@ -3228,6 +3418,11 @@ const generate = {
3228
3418
  names: { type: "string", value: "<list>", description: `an icon set's icons, comma-separated, at most ${MAX_ICONS}` },
3229
3419
  purpose: { type: "string", value: "skill|item", description: "whether an icon set is skills on badges or items alone; item unless given" },
3230
3420
  tiles: { type: "string", value: "<cols>x<rows>", description: "how many tiles a level design is across, 6x4 unless given" },
3421
+ sketch: {
3422
+ type: "string",
3423
+ value: "<json>",
3424
+ description: "a UI sketch's file: { canvas: { width, height }, shapes: [{ label, kind, x, y, width, height, radius?, sides?, caption?, note?, border?, hollow? }] }"
3425
+ },
3231
3426
  reference: { type: "string", multiple: true, value: "<png>", description: "an image to draw from" },
3232
3427
  quality: { type: "string", value: "<quality>", description: `${EDIT_QUALITIES.join(", ")}; ${DEFAULT_QUALITY} unless given` },
3233
3428
  yes: { type: "boolean", short: "y", description: "start it without asking, once the cost is said" },
@@ -3563,7 +3758,7 @@ const seal = {
3563
3758
  answer(context, { sealed: true, output: result.output }, result.output);
3564
3759
  }
3565
3760
  };
3566
- const version = "0.2.0";
3761
+ const version = "0.3.0";
3567
3762
  const manifest = {
3568
3763
  version
3569
3764
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelvibing/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "PixelVibing: pixel art game assets from the command line, and the Skills that teach coding assistants to make them.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://pixelvibing.com",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pixelvibing
3
- description: Make pixel art for this game with pixel-vibing-cli — objects, creatures, level designs and interface art (a HUD, skill and item icons) generated on the PixelVibing platform, listed and pulled into pixelvibing/out/, and sealed into the game's container. Use when the game needs a sprite, a prop, a creature, a level's art, its HUD or its icons, or art already made for it.
3
+ description: Make pixel art for this game with pixel-vibing-cli — objects, creatures, level designs and interface art (a HUD, skill and item icons, pieces laid out in a sketch) generated on the PixelVibing platform, listed and pulled into pixelvibing/out/, and sealed into the game's container. Use when the game needs a sprite, a prop, a creature, a level's art, its HUD or its icons, or art already made for it.
4
4
  ---
5
5
 
6
6
  # PixelVibing art for this game
@@ -35,6 +35,7 @@ pixel-vibing-cli generate level-design --instruction "<what>" --tiles <cols>x<ro
35
35
  pixel-vibing-cli generate hud [--pieces <list>] [--instruction "<theme>"] --yes --json
36
36
  pixel-vibing-cli generate icons --names "<name>,<name>,..." --purpose <skill|item> [--size <16|32|64>] --yes --json
37
37
  pixel-vibing-cli generate hud-mockup --instruction "<the HUD>" --yes --json
38
+ pixel-vibing-cli generate ui-sketch --sketch <file.json> --instruction "<the set, in a line>" --yes --json
38
39
  ```
39
40
 
40
41
  Each generation spends the account's credits.
@@ -49,7 +50,8 @@ Each generation spends the account's credits.
49
50
  - `--quality` is `low`, `medium` (the default), `high`, `xhigh` or `max`; the higher,
50
51
  the dearer.
51
52
  - `--reference` is a PNG to draw from, repeated for several: at most 8 for an object,
52
- 3 for a creature, a level design or a HUD mockup, none for a HUD set or icons.
53
+ 3 for a creature, a level design, a HUD mockup or a UI sketch, none for a HUD set or icons. A UI
54
+ sketch's references are a style to follow (frames, materials, a way of drawing), never what to draw.
53
55
  - **Interface art.** When the game shows the player something over the world, make it here
54
56
  rather than drawing it in code:
55
57
  - `hud` makes a matching set in one generation: `health-bar`, `mana-bar`, `button`,
@@ -61,7 +63,18 @@ Each generation spends the account's credits.
61
63
  potions or loot (each alone). `--size` is 16, 32 (the default) or 64 px.
62
64
  - `hud-mockup` draws one screen with its whole HUD, to agree on the look first; take
63
65
  its pieces out in the desktop editor.
64
- - The art never carries text or numbers: draw labels, counts and values in the game.
66
+ - `ui-sketch` draws pieces where a sketch lays them out, when the game needs pieces the
67
+ HUD set does not have, or at sizes and places of its own. Write the sketch as JSON:
68
+ `{ "canvas": { "width": 256, "height": 256 }, "shapes": [...] }`, the canvas 128 × 128,
69
+ 256 × 256, 256 × 128 or 128 × 256, and up to 16 shapes that do not overlap, each
70
+ `{ "label", "kind": "rect" | "ellipse" | "polygon", "x", "y", "width", "height" }` in
71
+ the canvas's pixels, with `radius` for a rounded rectangle and `sides` (3 to 8) for a
72
+ polygon. A shape may say more: `note` (what the piece is, such as "a full red health
73
+ bar"), `caption` (text drawn on it, such as "Quit"), `border` (its frame's width in
74
+ pixels, which gives the piece nine-slice insets) and `hollow: true` (only the border
75
+ drawn, its inside transparent). Leave a few pixels between shapes.
76
+ - The art never carries text or numbers, but a UI sketch's captions: draw labels,
77
+ counts and values in the game.
65
78
  - **Success.** The asset is pulled at once. The answer's `files` are where its pictures
66
79
  are, relative to `pixelvibing/`.
67
80
  - **Failure.** Too few credits starts nothing and names the page where the person buys
@@ -79,7 +92,8 @@ pixel-vibing-cli pull <id|name> [--as <name>] --json
79
92
  `creature`, `level-design`, `rotate`, `animate`…
80
93
  - **Pulling.** `pull` writes an asset of one picture as `pixelvibing/out/<name>.png`, and
81
94
  one of several as `pixelvibing/out/<name>/<n>.png`; a HUD set's pieces as
82
- `pixelvibing/out/ui/hud/<piece>.png` and an icon set's as `pixelvibing/out/ui/icon/<name>.png`.
95
+ `pixelvibing/out/ui/hud/<piece>.png`, an icon set's as `pixelvibing/out/ui/icon/<name>.png`
96
+ and a UI sketch's as `pixelvibing/out/ui/sketch/<label>.png`.
83
97
  It downloads again only when the platform's copy changed.
84
98
  - **Collisions.** `pull` never writes over a picture it did not pull. When it refuses,
85
99
  give the asset another name with `--as`.