@pixelvibing/cli 0.1.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.
- package/dist/pixel-vibing-cli.js +211 -16
- package/package.json +1 -1
- package/skills/pixelvibing/SKILL.md +18 -4
package/dist/pixel-vibing-cli.js
CHANGED
|
@@ -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
|
|
2242
|
-
if (
|
|
2243
|
-
const terrain = scopedTerrain(tileset,
|
|
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
|
|
2279
|
-
if (!
|
|
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
|
|
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 &&
|
|
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 &&
|
|
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
|
|
3040
|
-
if (!
|
|
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
|
|
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: {
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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`.
|