@pixerate/editor 0.191.0 → 0.192.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.
@@ -9346,6 +9346,62 @@ var ImageEditorController = class {
9346
9346
  * text wrapped in `box`. Refused without a text shaper. Returns its id.
9347
9347
  */
9348
9348
  addTextLayer(init) {
9349
+ const layer = this.layoutTypeLayer(init);
9350
+ if (init.opacity !== void 0 && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError("opacity must be between 0 and 1.");
9351
+ if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
9352
+ if (init.opacity !== void 0) layer.opacity = init.opacity;
9353
+ if (init.blend !== void 0) layer.blend = init.blend;
9354
+ const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
9355
+ this.commitLayers(next, () => {
9356
+ this.state.activeLayerId = layer.id;
9357
+ this.state.activeTarget = "pixels";
9358
+ });
9359
+ return layer.id;
9360
+ }
9361
+ /**
9362
+ * Type Mask (horizontal or vertical): a selection in the shape of `text`, laid out and drawn as
9363
+ * `addTextLayer` would place it (font, size, tracking, box, rotation), combined with the current
9364
+ * selection by `mode`. Vertical type stacks the characters top to bottom, centred on `x`. No
9365
+ * layer is added. One undo step. Returns whether anything is selected.
9366
+ */
9367
+ selectTextShape(init) {
9368
+ const vertical = init.orientation === "vertical";
9369
+ if (init.orientation !== void 0 && init.orientation !== "horizontal" && !vertical) throw new LayerOperationError("orientation must be horizontal or vertical.");
9370
+ if (init.feather !== void 0 && !(init.feather >= 0 && init.feather <= 1e3)) throw new LayerOperationError("feather must be from 0 to 1000 px.");
9371
+ let text = normalizeText(init.text);
9372
+ if (!text.trim()) throw new LayerOperationError("The text is empty, so there is nothing to select; give some characters.");
9373
+ let paragraph = init.paragraph;
9374
+ if (vertical) {
9375
+ if (/\n/.test(text)) throw new LayerOperationError("Vertical type masks take one column; select each column separately (mode add) instead of line breaks.");
9376
+ if (init.box) throw new LayerOperationError("Vertical type masks are point text; leave out box.");
9377
+ text = Array.from(text).join("\n");
9378
+ paragraph = { align: "center", ...paragraph ?? {} };
9379
+ }
9380
+ const layer = this.layoutTypeLayer({ ...init, text, paragraph });
9381
+ const { width, height } = this.naturalSize;
9382
+ const b = contentBounds(layer.surface);
9383
+ const x0 = Math.max(0, b?.x ?? 0);
9384
+ const y0 = Math.max(0, b?.y ?? 0);
9385
+ const x1 = Math.min(width, b ? b.x + b.width : 0);
9386
+ const y1 = Math.min(height, b ? b.y + b.height : 0);
9387
+ let mask;
9388
+ if (!b || x1 <= x0 || y1 <= y0) mask = createSelectionMask(width, height);
9389
+ else {
9390
+ const rect = { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
9391
+ const rgba = readRegion(layer.surface, rect);
9392
+ const bytes = new Uint8Array(rect.width * rect.height);
9393
+ const hard = init.antiAlias === false;
9394
+ for (let i = 0; i < bytes.length; i++) {
9395
+ const a = rgba[i * 4 + 3];
9396
+ bytes[i] = hard ? a >= 128 ? 255 : 0 : a;
9397
+ }
9398
+ mask = selectionFromRegion(width, height, rect, bytes);
9399
+ }
9400
+ if ((init.feather ?? 0) > 0 && mask.bounds) mask = modifySelection(mask, "feather", init.feather);
9401
+ return this.setSelection(mask.bounds ? mask : null, init.mode ?? "replace");
9402
+ }
9403
+ /** A type layer laid out and drawn from `init` (not added): the shared part of `addTextLayer` and the type masks. */
9404
+ layoutTypeLayer(init) {
9349
9405
  const text = normalizeText(init.text);
9350
9406
  if (!Number.isFinite(init.x) || !Number.isFinite(init.y)) throw new LayerOperationError("x and y must be numbers (document pixels).");
9351
9407
  const style = patchTextStyle({ ...DEFAULT_TEXT_STYLE, color: this.state.paint.foreground }, init.style ?? {});
@@ -9357,19 +9413,9 @@ var ImageEditorController = class {
9357
9413
  frame = { kind: "box", width, height };
9358
9414
  }
9359
9415
  if (init.rotation !== void 0 && !Number.isFinite(init.rotation)) throw new LayerOperationError("rotation must be a number of degrees.");
9360
- if (init.opacity !== void 0 && !(Number.isFinite(init.opacity) && init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError("opacity must be between 0 and 1.");
9361
- if (init.blend !== void 0 && !isBlendMode(init.blend)) throw new LayerOperationError(`Unknown blend mode "${String(init.blend)}".`);
9362
9416
  const t = [1, 0, 0, 1, init.x, init.y];
9363
9417
  const transform = init.rotation ? multiplyMatrices(t, rotateMatrix(init.rotation, { x: init.x, y: init.y })) : t;
9364
- const layer = createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());
9365
- if (init.opacity !== void 0) layer.opacity = init.opacity;
9366
- if (init.blend !== void 0) layer.blend = init.blend;
9367
- const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));
9368
- this.commitLayers(next, () => {
9369
- this.state.activeLayerId = layer.id;
9370
- this.state.activeTarget = "pixels";
9371
- });
9372
- return layer.id;
9418
+ return createTypeLayer({ text, runs: [{ length: text.length, style }], paragraphs: [{ length: text.length, style: paragraph }], frame, transform, name: init.name ?? typeLayerName(text) }, this.textEnv());
9373
9419
  }
9374
9420
  /**
9375
9421
  * Replaces text [start, end) (default all of it) with `text`; inserted text takes the style at
@@ -11255,7 +11301,7 @@ var DEPTH_MASK_CONTROLS = [
11255
11301
  // src/image-editor/playback.ts
11256
11302
  var IMAGE_PLAYBACK_KIND = "layer";
11257
11303
  var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
11258
- var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection)$/;
11304
+ var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
11259
11305
  var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|convert_text_to_shape)$/;
11260
11306
  var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
11261
11307
  var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
@@ -11376,7 +11422,9 @@ var SELECTION_TOOLS = [
11376
11422
  { id: "column", label: "Single Column" },
11377
11423
  { id: "lasso", label: "Lasso" },
11378
11424
  { id: "polygon", label: "Polygonal Lasso" },
11379
- { id: "range", label: "Color Range" }
11425
+ { id: "range", label: "Color Range" },
11426
+ { id: "type", label: "Type Mask" },
11427
+ { id: "type-vertical", label: "Vertical Type Mask" }
11380
11428
  ];
11381
11429
  var COLOR_RANGE_PRESET_LABELS = {
11382
11430
  reds: "Reds",
@@ -11407,7 +11455,10 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
11407
11455
  feather: 0,
11408
11456
  fuzziness: 40,
11409
11457
  localized: 0,
11410
- rangeInvert: false
11458
+ rangeInvert: false,
11459
+ typeText: "Type",
11460
+ typeSize: 72,
11461
+ typeFamily: "sans-serif"
11411
11462
  };
11412
11463
  function selectionModeFromModifiers(modifiers, fallback) {
11413
11464
  if (modifiers.shiftKey && modifiers.altKey) return "intersect";
@@ -13789,6 +13840,24 @@ var SelectionGesture = class {
13789
13840
  }
13790
13841
  return;
13791
13842
  }
13843
+ if (options.tool === "type" || options.tool === "type-vertical") {
13844
+ this.start = null;
13845
+ try {
13846
+ this.controller.selectTextShape({
13847
+ text: options.typeText || "Type",
13848
+ x: point.x,
13849
+ y: point.y,
13850
+ orientation: options.tool === "type-vertical" ? "vertical" : "horizontal",
13851
+ style: { fontSize: options.typeSize ?? 72, fontFamily: options.typeFamily || "sans-serif" },
13852
+ mode: selectionModeFromModifiers(modifiers, options.mode),
13853
+ feather: options.feather ?? 0,
13854
+ antiAlias: options.antiAlias
13855
+ });
13856
+ } catch (err) {
13857
+ this.error = err instanceof Error ? err.message : String(err);
13858
+ }
13859
+ return;
13860
+ }
13792
13861
  if (options.tool === "wand" || options.tool === "range") {
13793
13862
  this.start = null;
13794
13863
  const common = {
@@ -16333,7 +16402,43 @@ function createImageTypeTools(controller) {
16333
16402
  return { layer: layerSummary(id), shape: controller.getShapeLayer(id), warnings };
16334
16403
  }
16335
16404
  };
16336
- return [listFonts, get, add2, setText, setStyle, setParagraph, setFrame, toShape];
16405
+ const typeMask = {
16406
+ name: "select_text_shape",
16407
+ title: "Select the shape of text",
16408
+ description: "Horizontal / Vertical Type Mask: selects the shape of text laid out as add_text_layer would place it (font family, size, weight, tracking, box, rotation; colour is ignored), combined with the current selection by mode. No layer is added. Vertical stacks the characters top to bottom, centred on x (one column; add more with mode add). Use it to cut text out of an image (layer_via_copy), fill letters with a photo (add_layer_mask), or mask an adjustment. Check fonts with list_fonts first. One undo step. Returns the selection.",
16409
+ inputSchema: {
16410
+ type: "object",
16411
+ properties: {
16412
+ text: { type: "string", minLength: 1, description: "The text; \\n starts a new line (horizontal only)." },
16413
+ x: { type: "number", description: "Document x of the origin, pixels (vertical: the column centre)." },
16414
+ y: { type: "number", description: "Document y of the origin (the first baseline), pixels." },
16415
+ orientation: { type: "string", enum: ["horizontal", "vertical"], description: "horizontal (default) or vertical (characters stacked top to bottom)." },
16416
+ box: {
16417
+ type: "object",
16418
+ description: "Horizontal only: wrap in this box, pixels.",
16419
+ properties: { width: { type: "number", minimum: 1, description: "Box width, pixels." }, height: { type: "number", minimum: 1, description: "Box height, pixels." } },
16420
+ required: ["width", "height"],
16421
+ additionalProperties: false
16422
+ },
16423
+ style: { type: "object", description: "Character style (size in px, family, weight, tracking\u2026; colour is ignored).", properties: STYLE, additionalProperties: false },
16424
+ paragraph: { type: "object", description: "Paragraph style (alignment, leading\u2026).", properties: PARAGRAPH, additionalProperties: false },
16425
+ rotation: { type: "number", description: "Degrees clockwise about the origin (default 0)." },
16426
+ mode: { type: "string", enum: ["replace", "add", "subtract", "intersect"], description: "How it combines with the current selection (default replace)." },
16427
+ feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather radius in px (default 0)." },
16428
+ antiAlias: { type: "boolean", description: "Soft glyph edges (default true)." }
16429
+ },
16430
+ required: ["text", "x", "y"],
16431
+ additionalProperties: false
16432
+ },
16433
+ annotations: { title: "Select the shape of text", ...WRITE9 },
16434
+ handler: (args) => {
16435
+ const init = pick2(args, ["text", "x", "y", "orientation", "box", "style", "paragraph", "rotation", "mode", "feather", "antiAlias"]);
16436
+ controller.selectTextShape(init);
16437
+ const sel = controller.getSelection();
16438
+ return { empty: !sel, bounds: sel?.bounds ?? null, area: Math.round(sel ? selectionArea(sel) : 0) };
16439
+ }
16440
+ };
16441
+ return [listFonts, get, add2, setText, setStyle, setParagraph, setFrame, toShape, typeMask];
16337
16442
  }
16338
16443
 
16339
16444
  // src/image-editor/tools.ts
@@ -2,7 +2,7 @@ import { type SelectionMask, type SelectionMode } from './mask.cjs';
2
2
  import type { ColorDistance, ColorRangePreset } from './wand.cjs';
3
3
  import type { RefineEdgeParams } from './refine.cjs';
4
4
  /** Selection tools of the image editor UI. */
5
- export type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'row' | 'column' | 'lasso' | 'polygon' | 'range';
5
+ export type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'row' | 'column' | 'lasso' | 'polygon' | 'range' | 'type' | 'type-vertical';
6
6
  export declare const SELECTION_TOOLS: readonly {
7
7
  id: SelectionToolKind;
8
8
  label: string;
@@ -31,6 +31,12 @@ export interface SelectionToolOptions {
31
31
  localized?: number;
32
32
  /** Color Range: select everything except the sampled colours. */
33
33
  rangeInvert?: boolean;
34
+ /** Type Mask: the text to select (default "Type"). A click places its first baseline. */
35
+ typeText?: string;
36
+ /** Type Mask: font size in px (default 72). */
37
+ typeSize?: number;
38
+ /** Type Mask: font family (default sans-serif). */
39
+ typeFamily?: string;
34
40
  }
35
41
  export declare const DEFAULT_SELECTION_TOOL_OPTIONS: SelectionToolOptions;
36
42
  /**
@@ -2,7 +2,7 @@ import { type SelectionMask, type SelectionMode } from './mask.js';
2
2
  import type { ColorDistance, ColorRangePreset } from './wand.js';
3
3
  import type { RefineEdgeParams } from './refine.js';
4
4
  /** Selection tools of the image editor UI. */
5
- export type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'row' | 'column' | 'lasso' | 'polygon' | 'range';
5
+ export type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'row' | 'column' | 'lasso' | 'polygon' | 'range' | 'type' | 'type-vertical';
6
6
  export declare const SELECTION_TOOLS: readonly {
7
7
  id: SelectionToolKind;
8
8
  label: string;
@@ -31,6 +31,12 @@ export interface SelectionToolOptions {
31
31
  localized?: number;
32
32
  /** Color Range: select everything except the sampled colours. */
33
33
  rangeInvert?: boolean;
34
+ /** Type Mask: the text to select (default "Type"). A click places its first baseline. */
35
+ typeText?: string;
36
+ /** Type Mask: font size in px (default 72). */
37
+ typeSize?: number;
38
+ /** Type Mask: font family (default sans-serif). */
39
+ typeFamily?: string;
34
40
  }
35
41
  export declare const DEFAULT_SELECTION_TOOL_OPTIONS: SelectionToolOptions;
36
42
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.191.0",
3
+ "version": "0.192.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -307,6 +307,7 @@ in the source frame at the document's bit depth, colour mode and profile. Read
307
307
  | `set_paragraph_style` | Set `align`, `autoLeading`, indents, `spaceBefore` / `spaceAfter` on every paragraph `start`..`end` touches (default all). Returns the text layer. |
308
308
  | `set_text_frame` | `kind: point` (one line per paragraph) or `kind: box` with `width` and `height` (wrapping paragraph text), or resize a box; the first line stays put. Returns the text layer and its lines. |
309
309
  | `convert_text_to_shape` | Type › Convert to Shape: glyph outlines become a shape layer with the same id (text no longer editable; fill from the first run's colour; `warnings` lists losses). Needs an outline font shaper. Destructive. |
310
+ | `select_text_shape` | Horizontal / Vertical Type Mask: select the shape of `text` at `x`, `y` (style, box, rotation as `add_text_layer`; `orientation` `vertical` stacks the letters), by `mode`, with `feather`. No layer is added. Returns the selection. |
310
311
  | `add_shape_layer` | Add a vector shape layer above the active layer (or placed with `aboveId` / `belowId` / `parentId` / `index`) and make it active: `shape` (live rect, ellipse, polygon, star or line, document px), `subpaths` (SVG path data or anchors) or several `components` with path ops; `fill` (colour, gradient or pattern, default the foreground colour; `null` none), `stroke` (`width`, `paint`, `align`, `cap`, `join`, `miterLimit`, `dashes` in widths), `fillRule`, `name`, `opacity`, `blend`. Returns the `layer` and the `shape`. One undo step. |
311
312
  | `update_shape` | Restyle a shape: `fill` (replace, `null` removes), `stroke` (only the settings given change; `null` removes; a new stroke needs `width` and `paint`), `fillRule`. Geometry is unchanged. Returns the shape. |
312
313
  | `set_shape_component` | Change component `componentIndex` (from `get_shape_layer`): `shape` fields (without `kind`, or with the same kind, unset fields keep their values), new `subpaths` (ends the live shape), or `op`. Returns the shape. |
@@ -49,6 +49,8 @@ Detail for the `pixerate-image-editor` skill: how to draw and edit vector shapes
49
49
  refused: rasterize first. Masks, clipping, blend modes and effects work as on any layer.
50
50
  5. **Outlines and pixels**: `convert_text_to_shape` makes glyph outlines a shape layer (needs an
51
51
  outline font shaper; the browser canvas cannot); `rasterize_layer` makes it pixels.
52
+ For a selection in the shape of text (photo-filled letters, text cut out of the image) use
53
+ `select_text_shape`, the Type Mask: it lays the text out the same way but adds no layer.
52
54
 
53
55
  ## Photoshop files
54
56
 
@@ -106,6 +106,7 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
106
106
  | A cutout, shape or text already on its own layer | `select_layer_transparency` (exact, soft where the layer is) |
107
107
  | A dead pixel row or column | `select_line`, then fill or heal it |
108
108
  | Smooth curves you can describe (a logo, a road, a lens) | `path_to_selection` with Bézier `subpaths` or SVG path data |
109
+ | Letters (photo-filled text, text cut out of the image) | `select_text_shape` (check fonts with `list_fonts`), then `add_layer_mask` or `layer_via_copy` |
109
110
 
110
111
  ## Recipes
111
112
 
@@ -203,6 +204,7 @@ This skill is the method. `pixerate-image-editor` has the full tool reference.
203
204
  - `add_vector_mask`, `set_vector_mask_path`, `set_vector_mask_properties`, `get_vector_mask`: sharp, editable path masks
204
205
  - `rasterize_vector_mask`, `remove_vector_mask`: turn a vector mask into pixels, or drop it
205
206
  - `selection_to_path`, `path_to_selection`: a selection's outline as Bézier paths, and a path back into a selection
207
+ - `select_text_shape`: the Type Mask, a selection in the shape of text
206
208
  - `modify_selection`: expand, contract, feather, border, smooth
207
209
  - `refine_selection`: snap the edge to the image
208
210
  - `transform_selection`: move or scale the outline only