ТИЦЬ · для розробників v1.20.1 ← до редактора

Три способи використати ТИЦЬ у своїх системах

Редактор анотованих колажів можна вбудувати в чужий застосунок, керувати ним програмно через postMessage-API, або віддати агентам як MCP-інструмент, який робить готові картинки з опису.

iframe · postMessage · @3rtools/tyts-embed

Embed

Редактор в iframe на вашій сторінці. Host SDK на один файл без залежностей, стан документа зберігаєте ви, редактор нічого не пише в localStorage.

npm i @3rtools/tyts-embed
const editor = TytsEmbed.create({
  container: '#tyts',
  src: 'https://tyts.vercel.app/embed/',
});
await editor.init();
await editor.loadImage(dataUrl);
const png = await editor.export('png');
SDK · protocol · TytsDocument · React ref

API

Команди loadImage, loadDocument, getDocument, export, markSaved, події change і формат документа з усіма типами фігур. Для host-ів без JS — сирий postMessage-протокол.

await editor.loadDocument(doc);   // JSON з вашого сховища
const doc = await editor.getDocument();
const { dataUrl } = await editor.export('pdf');
Model Context Protocol · @3rtools/tyts-mcp

MCP для агентів

Агент дає картинку й список анотацій у пікселях, отримує PNG/JPEG/PDF і preview для самоперевірки. Рендер піксель-у-піксель як у редакторі, через headless Chrome.

claude mcp add tyts -- npx -y @3rtools/tyts-mcp
tyts_render({
  image: './photo.jpg',
  pins: [
    { type: 'rectangle', x: 130, y: 360, width: 220, height: 130, text: 'Техніка' },
    { type: 'arrow', start: { x: 300, y: 120 }, end: { x: 520, y: 260 }, text: 'Напрямок' }
  ],
  width: 1600
})

Швидкий старт за 3 кроки

  1. Відкрийте пісочницю API: там кожна команда з описом, запитом і відповіддю, а поруч живий редактор.
  2. Підключіть SDK на свою сторінку і вкажіть src на цю збірку редактора: https://tyts.vercel.app/embed/. Безпека (дозволені origin-и, токен) задається при збірці, див. вкладку Embed.
  3. Для агентів додайте MCP-сервер і попросіть: «зроби колаж з photo.jpg, обведи техніку і підпиши». Далі агент сам викличе tyts_image_info і tyts_render.

ТИЦЬ як API: підключення й використання

Як керувати редактором ТИЦЬ програмно з іншої веб-сторінки (iframe + postMessage) або з React-застосунку через ref. Повний довідник методів, подій, помилок і формату документа — довідником API. Інтерактивна пісочниця з живими запитами — https://tyts.vercel.app/embed/api.html, демо host-сторінки — https://tyts.vercel.app/embed/demo.html.

1. Підключення за пʼять хвилин

Потрібні дві речі: SDK на вашій сторінці і редактор, який SDK відкриє в iframe. Редактор уже викладений публічно, збирати нічого не треба.

SDK

npm i @3rtools/tyts-embed
import { TytsEmbed } from '@3rtools/tyts-embed';

Без збирача — класичний скрипт, який створює глобальний TytsEmbed:

<script src="https://unpkg.com/@3rtools/tyts-embed/dist/tyts-embed.js"></script>

Редактор

Публічна збірка: https://tyts.vercel.app/embed/. Вкажіть її в src.

<div id="tyts" style="min-height: 80vh"></div>
<script type="module">
  import { TytsEmbed } from '@3rtools/tyts-embed';

  const editor = TytsEmbed.create({
    container: '#tyts',
    src: 'https://tyts.vercel.app/embed/',
    onChange(doc, { dirty }) { saveButton.disabled = !dirty; },
    onError(err) { console.warn('tyts', err.code, err.message); },
  });

  await editor.init();
  await editor.loadImage(await TytsEmbed.fileToDataUrl(file));
</script>

Контейнер має мати явну висоту або min-height: SDK ставить iframe height: 100%, і в контейнері без висоти iframe буде 150px.

2. Що далі в роботі

Типовий цикл host-сторінки:

// користувач розставив виноски...
const doc = await editor.getDocument();          // JSON документа — зберігаєте у себе
await api.put(`/reports/${id}/collage`, doc);
await editor.markSaved();                        // далі onChange дає dirty:false

// готова картинка або PDF
const { dataUrl } = await editor.export('png', 2);   // 'png' | 'jpeg' | 'pdf'

// при наступному відкритті
await editor.loadDocument(doc);                  // або editor.init({ document: doc })

3. Безпека, дані, власний хостинг

4. Довідник SDK

TytsEmbed.create(options) → editor

Опція Тип Опис
container string | Element Куди вставити iframe (селектор або елемент). Вміст контейнера очищається.
src string URL збірки редактора (тека з index.html). Публічна: https://tyts.vercel.app/embed/.
origin string Origin збірки для перевірки відповідей. За замовчуванням береться з src.
title string title iframe (доступність).
onReady() функція Iframe завантажився і слухає (один раз).
onChange(doc, { dirty }) функція Стан змінився. Приходить після init, з дебаунсом 250 мс, з повним документом.
onError(err) функція Помилка без requestId (наприклад, від сторонньої спроби init).

Методи editor

Метод Результат Опис
init({ token?, document?, timeoutMs? }) — Відкрити сесію. Якщо передано document, він завантажується до відповіді. Можна викликати одразу після create: SDK дочекається готовності iframe.
loadImage(dataUrl) — Нове фонове зображення, піни скидаються. data: URL; файл конвертує TytsEmbed.fileToDataUrl(file).
loadDocument(document) — Відновити документ (див. формат нижче). Приймає також автосейв-чернетку ТИЦЬ v2.
getDocument() TytsDocument | null Поточний стан; null, якщо зображення немає.
export(format, scale) { format, mime, dataUrl } format: 'png' | 'jpeg' | 'pdf'; scale — множник роздільної здатності (для pdf ігнорується).
markSaved() — Поточний стан вважати збереженим.
reset() — Порожній редактор.
destroy() — Прибрати iframe і слухачі; незавершені запити відхиляються з destroyed.

Коди помилок

code Коли
timeout Немає відповіді (60 с; export 120 с, getDocument 30 с).
not_initialized Команда до init.
origin_denied Origin host-а не в списку дозволених або не збігається з origin сесії.
invalid_token Токен не збігається зі збірковим.
protocol_mismatch Версії SDK і збірки різні.
invalid_document Документ не пройшов валідацію.
export_failed, init_failed, handler_error Помилка всередині редактора; message містить деталі.
destroyed Викликано destroy().

5. Протокол postMessage

Для host-ів без JS SDK (інша мова, власна обгортка). Host шле iframe.contentWindow.postMessage(msg, origin), редактор відповідає window.parent.postMessage(...). Повідомлення з requestId отримують відповідь із тим самим requestId. Редактор приймає повідомлення лише від батьківського вікна.

Host → редактор Відповідь
{ type: 'tyts:init', protocol: 1, requestId, token?, document? } { type: 'tyts:initialized', requestId, protocol: 1 }
{ type: 'tyts:loadImage', requestId, dataUrl } { type: 'tyts:done', requestId }
{ type: 'tyts:loadDocument', requestId, document } { type: 'tyts:done', requestId }
{ type: 'tyts:getDocument', requestId } { type: 'tyts:document', requestId, document }
{ type: 'tyts:export', requestId, format, scale? } { type: 'tyts:exportResult', requestId, format, mime, dataUrl }
{ type: 'tyts:markSaved', requestId } { type: 'tyts:done', requestId }
{ type: 'tyts:reset', requestId } { type: 'tyts:done', requestId }

Редактор сам надсилає:

Повні JSON-приклади і мінімальний клієнт без SDK — довіднику API, розділ 6.

6. Формат документа

interface TytsDocument {
  version: 1;
  backgroundImage: string;                  // data: URL
  imageSize: { w: number; h: number };      // логічний розмір полотна
  sourceImageSize: { w: number; h: number } | null;
  pins: Pin[];                              // повний опис полів — розділ 5
  cardPositions: Record<string, CardPosition>; // позиції карток за pin.id; порожньо — редактор розкладе сам
  settings?: { fontId?: 'road-ui' | 'uaf-sans'; strokeWidthScale?: number };
  meta?: { sourceUrl?: string; sourceTitle?: string; capturedAt?: number };
}

Мінімальний пін:

{
  "id": "pin-1", "label": 1, "color": "#E63946",
  "description": "<p>Техніка</p>", "descPos": "below",
  "image": null, "imageAspect": null,
  "anchor": { "type": "point", "x": 320, "y": 180 }
}

anchor.type: point, rectangle, ellipse, polygon, polyline, arrow, text, compass, numbering, standalone, linked — поля кожного з прикладами в довіднику API, розділ 7. Координати — в image space (пікселі imageSize).

Документ, що не проходить валідацію, відхиляється з invalid_document, редактор при цьому не змінюється.

7. React без iframe

import { useRef } from 'react';
import { CollageBuilder } from './components/CollageBuilder';
import type { TytsEditorHandle } from './embed/types';

function Report() {
  const editor = useRef<TytsEditorHandle>(null);
  return (
    <>
      <CollageBuilder
        ref={editor}
        mode="embed"                       // без autosave і без localStorage
        initialDocument={savedDoc}
        onChange={(doc, { dirty }) => setDirty(dirty)}
      />
      <button onClick={async () => download(await editor.current!.exportPdf())}>PDF</button>
    </>
  );
}

TytsEditorHandle має ті самі методи, що й SDK (loadDocument, loadImage, getDocument, exportImage, exportPdf, markSaved, reset). Пакет @3rtools/tyts-editor поки не публікується; пакет @3rtools/tyts-editor поки не публікується.

8. Для агентів

MCP-сервер @3rtools/tyts-mcp дає ті самі можливості LLM-агентам: claude mcp add tyts -- npx -y @3rtools/tyts-mcp. Деталі — довідник API, розділ 11, і сторінка MCP.

9. Як перевірити

ТИЦЬ Embed API — довідник

Описано Клодом

Повний опис програмного інтерфейсу редактора: JS SDK, події, помилки, сирий postMessage-протокол, формат документа і React-handle. Як підключити SDK і викласти редактор — встановлення. Інтерактивна версія з живими запитами — api.html у embed-збірці.

Зміст

  1. Огляд
  2. Життєвий цикл сесії
  3. SDK TytsEmbed
  4. Події
  5. Помилки
  6. Протокол postMessage
  7. Формат документа TytsDocument
  8. React: TytsEditorHandle
  9. Рецепти
  10. Обмеження
  11. MCP для агентів

1. Огляд

Редактор ТИЦЬ можна вбудувати трьома способами, і всі три дають один і той самий набір операцій:

Транспорт Для кого Як
JS SDK tyts-embed.js Будь-яка веб-сторінка Iframe з embed-збіркою, методи повертають проміси
postMessage напряму Host без JS SDK (інша мова, власний клієнт) Повідомлення tyts:* у iframe.contentWindow, відповіді у window
React ref TytsEditorHandle React-застосунок, де редактор живе в тому ж бандлі <CollageBuilder ref={...} mode="embed" />

Операції: init, loadImage, loadDocument, getDocument, export, markSaved, reset. Події від редактора: ready, change, error.

Версія протоколу — 1 (TytsEmbed.PROTOCOL у пакеті @3rtools/tyts-embed). SDK і бандл мають бути однієї версії, інакше init відхиляється з protocol_mismatch.

2. Життєвий цикл сесії

host                                   iframe (редактор)
 │  create() → вставляє <iframe>        │
 │◄──────────── tyts:ready ─────────────│  бандл завантажився, слухає
 │─── tyts:init {token?, document?} ───►│  перевірка origin і токена
 │◄──────── tyts:initialized ───────────│  сесію прив'язано до origin host-а
 │─── tyts:loadImage / loadDocument ───►│
 │◄──────────── tyts:done ──────────────│
 │◄──────── tyts:change {dirty} ────────│  після кожної зміни, з дебаунсом
 │─── tyts:getDocument / export ───────►│
 │◄──── tyts:document / exportResult ───│
 │─── tyts:markSaved ──────────────────►│  стан підтверджено як збережений
 │◄──────── tyts:change {dirty:false} ──│

3. SDK TytsEmbed

Підключення: npm i @3rtools/tyts-embed і import { TytsEmbed } from '@3rtools/tyts-embed', або класичний <script src=".../tyts-embed.js"></script> (з build/embed/ або https://unpkg.com/@3rtools/tyts-embed/dist/tyts-embed.js), який створює глобальний обʼєкт TytsEmbed. Типи — в пакеті (index.d.ts).

TytsEmbed.create(options) → editor

Вставляє iframe у контейнер і починає слухати відповіді.

Опція Тип Обовʼязково Опис
container `string Element` так
src string ні (./) URL теки embed-збірки або її index.html.
origin string ні Origin бандла для перевірки відповідей. За замовчуванням береться з src.
title string ні Атрибут title iframe.
onReady () => void ні Див. події.
onChange (document, { dirty }) => void ні
onError ({ code, message }) => void ні
const editor = TytsEmbed.create({
  container: '#tyts',
  src: 'https://app.example/tyts/embed/',
  onChange(doc, { dirty }) { setDirty(dirty); },
  onError(err) { toast(err.message); },
});

TytsEmbed.fileToDataUrl(file) → Promise<string>

Допоміжна функція: File/Blob → data: URL для loadImage.

editor.init(config?) → Promise<void>

Відкриває сесію. Резолвиться після tyts:initialized.

Поле config Тип Опис
token string Обовʼязковий, якщо бандл зібрано з VITE_TYTS_EMBED_TOKEN.
document TytsDocument Початковий документ; застосовується до відповіді.
timeoutMs number Таймаут очікування (за замовчуванням 60000).

Можливі помилки: origin_denied, invalid_token, protocol_mismatch, invalid_document, init_failed, timeout.

await editor.init({ token: 'change-me', document: savedDoc });

editor.loadImage(dataUrl) → Promise<void>

Нове фонове зображення. Усі піни й картки скидаються, історія undo очищається, стан стає чистим.

Параметр Тип Опис
dataUrl string data:image/...;base64,.... Підтримуються PNG, JPEG, WebP, GIF, SVG (усе, що вміє <img>).

Помилки: handler_error з повідомленням Failed to load image, якщо зображення не декодується.

input.onchange = async () => {
  await editor.loadImage(await TytsEmbed.fileToDataUrl(input.files[0]));
};

editor.loadDocument(document) → Promise<void>

Відновлює документ (див. розділ 7). Замінює все: зображення, піни, картки, settings. Стан стає чистим.

Приймає також файл проєкту .tyts як є (конверт { format: 'tyts-collage', document }, див. розділ 7) і автосейв-чернетку ТИЦЬ (version: 2 з savedAt).

Помилки: invalid_document — документ не пройшов валідацію, редактор не змінився.

editor.getDocument() → Promise<TytsDocument | null>

Поточний стан. null, якщо зображення не завантажене. Це те, що host зберігає у себе.

editor.export(format, scale?) → Promise<{ format, mime, dataUrl }>

Рендерить готовий колаж.

Параметр Тип Опис
format `'png' 'jpeg'
scale number Множник роздільної здатності відносно imageSize (за замовчуванням 1). Для pdf ігнорується: сторінка A4, зображення вписується.

Результат: mime — image/png, image/jpeg або application/pdf; dataUrl — вміст. Таймаут 120 с. Помилки: export_failed.

const { dataUrl } = await editor.export('png', 2);
const blob = await (await fetch(dataUrl)).blob();

editor.markSaved() → Promise<void>

Поточний стан вважати збереженим: наступний onChange прийде з dirty: false, і dirty знову стане true лише після нової зміни. Викликати після того, як host успішно зберіг результат getDocument().

editor.reset() → Promise<void>

Порожній редактор: без зображення, пінів, історії.

editor.destroy()

Знімає слухачі й видаляє iframe. Незавершені проміси відхиляються з destroyed.

Таймаути

Метод Таймаут
getDocument 30 с
export 120 с
решта 60 с (init — timeoutMs)

4. Події

Колбек Повідомлення Коли
onReady() tyts:ready Iframe завантажився. Один раз за життя iframe.
onChange(document, { dirty }) tyts:change Будь-яка зміна стану після init: піни, картки, зображення, налаштування. Дебаунс 250 мс — при перетягуванні приходить остання позиція. Містить повний документ.
onError({ code, message }) tyts:error без requestId Помилка, не привʼязана до запиту host-а (наприклад, сторонній init з іншого origin). Помилки з requestId не потрапляють сюди — вони відхиляють відповідний проміс.

dirty рахується від «базового» стану. Базовим стає:

5. Помилки

Кожна помилка — обʼєкт { code, message }.

code Причина Що робити
timeout Немає відповіді у відведений час Перевірити, що src вказує на embed-збірку, а origin збігається; подивитися консоль iframe.
not_initialized Команда до init Дочекатися init(). SDK сам ставить запити в чергу до ready, але не до init.
origin_denied Origin host-а не в VITE_TYTS_EMBED_ALLOWED_ORIGINS, або відрізняється від origin сесії Додати origin у збірку.
invalid_token Токен не збігається з VITE_TYTS_EMBED_TOKEN Передати правильний token в init.
protocol_mismatch Різні версії SDK і бандла Оновити tyts-embed.js разом із бандлом.
invalid_document Документ не пройшов валідацію Див. правила у розділі 7.
init_failed Виняток під час застосування початкового документа message містить деталі.
export_failed Рендер не вдався (немає зображення тощо) Спочатку loadImage.
handler_error Виняток усередині команди message містить деталі.
destroyed Викликано destroy() —

6. Протокол postMessage

Для host-ів без SDK. Host надсилає у iframe.contentWindow.postMessage(message, editorOrigin), редактор відповідає window.parent.postMessage(message, hostOrigin). Кожен запит із requestId отримує рівно одну відповідь із тим самим requestId: результат або tyts:error.

Правила безпеки з боку редактора:

Host → редактор

// init
{ "type": "tyts:init", "protocol": 1, "requestId": "r1", "token": "change-me", "document": { /* TytsDocument, необовʼязково */ } }
// відповідь
{ "type": "tyts:initialized", "requestId": "r1", "protocol": 1 }

// loadImage
{ "type": "tyts:loadImage", "requestId": "r2", "dataUrl": "data:image/jpeg;base64,..." }
{ "type": "tyts:done", "requestId": "r2" }

// loadDocument
{ "type": "tyts:loadDocument", "requestId": "r3", "document": { /* TytsDocument */ } }
{ "type": "tyts:done", "requestId": "r3" }

// getDocument
{ "type": "tyts:getDocument", "requestId": "r4" }
{ "type": "tyts:document", "requestId": "r4", "document": { /* TytsDocument або null */ } }

// export
{ "type": "tyts:export", "requestId": "r5", "format": "png", "scale": 2 }
{ "type": "tyts:exportResult", "requestId": "r5", "format": "png", "mime": "image/png", "dataUrl": "data:image/png;base64,..." }

// markSaved
{ "type": "tyts:markSaved", "requestId": "r6" }
{ "type": "tyts:done", "requestId": "r6" }

// reset
{ "type": "tyts:reset", "requestId": "r7" }
{ "type": "tyts:done", "requestId": "r7" }

requestId для loadImage, loadDocument, markSaved, reset необовʼязковий; без нього відповіді tyts:done не буде. Для getDocument і export він обовʼязковий.

Редактор → host без запиту

{ "type": "tyts:ready", "protocol": 1 }
{ "type": "tyts:change", "document": { /* TytsDocument */ }, "dirty": true }
{ "type": "tyts:error", "requestId": "r3", "error": { "code": "invalid_document", "message": "Invalid document" } }

Мінімальний клієнт без SDK

const iframe = document.querySelector('iframe');
const editorOrigin = new URL(iframe.src).origin;
const pending = new Map();

window.addEventListener('message', (e) => {
  if (e.source !== iframe.contentWindow || e.origin !== editorOrigin) return;
  const m = e.data;
  if (m.type === 'tyts:change') return render(m.document, m.dirty);
  const p = pending.get(m.requestId);
  if (!p) return;
  pending.delete(m.requestId);
  m.type === 'tyts:error' ? p.reject(m.error) : p.resolve(m);
});

function request(message) {
  const requestId = crypto.randomUUID();
  return new Promise((resolve, reject) => {
    pending.set(requestId, { resolve, reject });
    iframe.contentWindow.postMessage({ ...message, requestId }, editorOrigin);
  });
}

await request({ type: 'tyts:init', protocol: 1 });
await request({ type: 'tyts:loadImage', dataUrl });
const { document: doc } = await request({ type: 'tyts:getDocument' });

7. Формат документа TytsDocument

Типи постачаються з пакетом @3rtools/tyts-embed (index.d.ts): TytsDocument, TytsPin, TytsAnchor.

interface TytsDocument {
  version: 1;
  backgroundImage: string;                       // data: URL фонового зображення
  imageSize: { w: number; h: number };           // логічний розмір полотна, у ньому всі координати
  sourceImageSize: { w: number; h: number } | null; // розмір оригіналу до масштабування
  pins: Pin[];
  cardPositions: Record<string, CardPosition>;   // ключ — pin.id; порожній обʼєкт = авто-розкладка
  settings?: { fontId?: 'road-ui' | 'uaf-sans'; strokeWidthScale?: number };
  meta?: { sourceUrl?: string; sourceTitle?: string; capturedAt?: number };
}

interface CardPosition { x: number; y: number; width: number; height: number }

Файл проєкту .tyts

Редактор зберігає й відкриває колажі як файли («Зберегти у файл» і «Відкрити» зліва в хедері, перетягування у вікно). Це той самий TytsDocument у JSON-конверті:

interface TytsCollageFile {
  format: 'tyts-collage';
  version: 1;
  appVersion: string;   // версія ТИЦЬ, що зберегла файл; на розбір не впливає
  savedAt: number;      // мс
  document: TytsDocument;
}

Host може віддати вміст файлу в loadDocument без розпакування, а getDocument() — загорнути в такий конверт і запропонувати користувачу як завантаження: вебверсія відкриє його. В embed-режимі кнопки файлу в редакторі є, а «Останні проєкти» й чернетка в IndexedDB вимкнені — сховищем керує host.

Pin

Поле Тип Обовʼязково Опис
id string так Унікальний id (UUID або будь-який рядок).
anchor Anchor так Де пін на зображенні, див. нижче.
label number ≥ 1 так Номер виноски. Редактор перенумеровує при видаленні.
color string так CSS-колір маркера й лінії. Палітра UI: #FBD710 #F0462F #FF8D28 #4FD553 #00C0E8 #0088FF #E246A1 #FFFFFF #A0522D #888888 #000000; інші значення теж приймаються.
description string так HTML картки (TipTap): <p>, <strong>, <em>, <u>, <s>, <br>. Порожній рядок — без тексту.
descPos `'above' 'below'` так
image `string null` так
imageAspect `number null` так
strokeStyle `'solid' 'dashed'` ні
fillStyle `'filled' 'none'` ні
strokeWidthScale number ні Множник товщини ліній для цього піна; немає — глобальний із settings.
leaderPoints {x,y}[] ні Точки зламу лінії-виноски.
theme `'dark' 'light'` ні
textBold, textItalic, textUnderline, textStrike boolean ні Стилі всього тексту картки.
textFontSize number ні Кегль у px (типово 16).
textColor string ні Колір тексту.
textBackgroundColor `string null` ні
calloutHidden boolean ні Сховати картку й лінію, лишити лише маркер.
cardHostId string ні Прикріплена фігура (кілька об'єктів на одну виноску): своєї картки немає, лінія йде до картки піна з цим id, номер — його. Ставиться разом із calloutHidden: true.
z number ні Порядок малювання (більше — вище).

Anchor

Усі координати — пікселі в просторі imageSize.

type Поля Приклад
point x, y { "type": "point", "x": 320, "y": 180 }
rectangle x, y, width, height, rotation? { "type": "rectangle", "x": 130, "y": 360, "width": 220, "height": 130 }
ellipse cx, cy, rx, ry, rotation? { "type": "ellipse", "cx": 690, "cy": 290, "rx": 150, "ry": 110 }
polygon points: {x,y}[] (≥3), rotation? { "type": "polygon", "points": [{"x":10,"y":10},{"x":80,"y":20},{"x":40,"y":90}] }
polyline points: {x,y}[] (≥2), rotation? { "type": "polyline", "points": [{"x":10,"y":10},{"x":200,"y":60}] }
arrow start, end, control (усі {x,y}) { "type": "arrow", "start": {"x":50,"y":50}, "end": {"x":200,"y":140}, "control": {"x":125,"y":60} } — control задає вигин; для прямої стрілки поставити на середину
text x, y, width, height, point? { "type": "text", "x": 40, "y": 40, "width": 200, "height": 60 } — текстовий блок на зображенні, текст у description; з point — «точка з текстом»
compass cx, cy, radius, rotation? { "type": "compass", "cx": 100, "cy": 100, "radius": 60, "rotation": 15 }
numbering start, end Нумерована стрілка; має власну нумерацію label.
standalone x, y Картка без лінії й фігури.
image x, y, width, height, src, aspect Вставлена картинка (PNG/SVG) як data: URL.
redact `x, y, width, height, mode: 'blur' 'pixelate'
linked pinId Виноска-супутник: лінія йде до анкера піна pinId, номер успадковується. Супутник супутника заборонений.

Мінімальний повний документ:

{
  "version": 1,
  "backgroundImage": "data:image/jpeg;base64,...",
  "imageSize": { "w": 960, "h": 600 },
  "sourceImageSize": null,
  "pins": [
    {
      "id": "pin-1", "label": 1, "color": "#F0462F",
      "description": "<p>Техніка</p>", "descPos": "below",
      "image": null, "imageAspect": null,
      "anchor": { "type": "rectangle", "x": 130, "y": 360, "width": 220, "height": 130 }
    }
  ],
  "cardPositions": {}
}

Валідація

Документ відхиляється (invalid_document), якщо:

Невідомі додаткові поля ігноруються, документ не змінюється при відхиленні.

8. React: TytsEditorHandle

Для React-застосунку, де редактор у тому ж бандлі (без iframe і postMessage).

import { useRef } from 'react';
import { CollageBuilder } from './components/CollageBuilder';
import type { TytsEditorHandle } from './embed/types';

const editor = useRef<TytsEditorHandle>(null);

<CollageBuilder
  ref={editor}
  mode="embed"                // autosave вимкнено, localStorage не використовується
  initialDocument={doc}       // застосовується один раз при монтуванні
  features={{ autosave: false }}
  onReady={() => {}}          // лише в iframe-режимі
  onChange={(doc, { dirty }) => {}}
  onError={(err) => {}}
/>
Метод Сигнатура
loadDocument (document: TytsDocument) => Promise<void>
loadImage `(source: string
getDocument `() => TytsDocument
exportImage `(format: 'png'
exportPdf `() => Promise<Blob
markSaved () => void
reset () => void

Семантика dirty така сама, як у SDK. Пропи embedConfig (allowedOrigins, token) мають сенс лише для iframe-режиму.

9. Рецепти

Зберегти й відновити

saveButton.onclick = async () => {
  const doc = await editor.getDocument();
  await api.put(`/reports/${id}/collage`, doc);   // ваше сховище
  await editor.markSaved();
};

// при відкритті звіту
const doc = await api.get(`/reports/${id}/collage`);
await editor.init({ document: doc });             // або editor.loadDocument(doc) після init

Попередження про незбережені зміни

let dirty = false;
TytsEmbed.create({ ..., onChange(_, meta) { dirty = meta.dirty; } });
window.addEventListener('beforeunload', (e) => { if (dirty) e.preventDefault(); });

Завантажити PNG як файл

const { dataUrl } = await editor.export('png', 2);
const a = Object.assign(document.createElement('a'), { href: dataUrl, download: 'collage.png' });
a.click();

Надіслати PDF на сервер

const { dataUrl } = await editor.export('pdf');
const blob = await (await fetch(dataUrl)).blob();
await fetch('/upload', { method: 'POST', body: blob, headers: { 'Content-Type': 'application/pdf' } });

Створити виноски програмно

Документ — звичайний JSON, тож host може згенерувати піни сам і віддати їх через loadDocument; cardPositions: {} змусить редактор розкласти картки. Повноцінний API операцій без документа («постав прямокутник тут») — у планах.

Кілька редакторів на сторінці

Кожен TytsEmbed.create — незалежна сесія зі своїм iframe. Обробник message фільтрує за event.source, тож інстанси не перетинаються.

Обробка помилок

try {
  await editor.loadDocument(doc);
} catch (err) {
  if (err.code === 'invalid_document') showValidationHint();
  else if (err.code === 'timeout') reloadEditor();
  else throw err;
}

10. Обмеження

11. MCP для агентів

Пакет @3rtools/tyts-mcp дає LLM-агентам той самий API як MCP-інструменти: tyts_render (картинка + анотації → PNG/JPEG/PDF з preview), tyts_image_info, tyts_validate, ресурс tyts://docs/annotations і промпт annotate-image. Координати анотацій — у пікселях оригінальної картинки; сервер сам переводить їх у простір редактора.

claude mcp add tyts -- npx -y @3rtools/tyts-mcp

Деталі — сторінка MCP.

@3rtools/tyts-mcp

MCP-сервер редактора ТИЦЬ (Tyts): агент дає картинку й список анотацій, отримує готовий PNG/JPEG/PDF з нумерованими виносками. Працює через headless Chrome із вбудованою збіркою редактора, тож результат піксель-у-піксель такий, як у застосунку.

MCP server for the Tyts annotated-collage editor: an agent passes an image plus annotations and gets a rendered PNG/JPEG/PDF back.

Вимоги

Підключення

Claude Code:

claude mcp add tyts -- npx -y @3rtools/tyts-mcp

Будь-який MCP-клієнт (Cursor, Claude Desktop, .mcp.json у проєкті):

{
  "mcpServers": {
    "tyts": {
      "command": "npx",
      "args": ["-y", "@3rtools/tyts-mcp"],
      "env": { "TYTS_OUTPUT_DIR": "./collages" }
    }
  }
}

Змінні середовища: TYTS_OUTPUT_DIR (куди класти результати; типово тимчасова тека), TYTS_CHROME_PATH, TYTS_EDITOR_DIR (своя збірка редактора замість вбудованої).

Інструменти

tyts_render

Головний інструмент. Вхід:

Поле Опис
image шлях до файлу, http(s) URL або data URL
pins анотації; координати в пікселях оригінальної картинки
document замість image+pins — повний TytsDocument (наприклад, з попереднього рендера) або вміст файлу проєкту .tyts як є (конверт { format: 'tyts-collage', document })
crop {x, y, width, height} — кадрувати картинку; координати pins лишаються в оригіналі
stamp {image, corner?, size?, marginX?, marginY?} — лого в кут, як інструмент «Штамп»
strokeWidth товщина ліній для всіх пінів без власної (0.25–4, типово 1)
format png (типово), jpeg, pdf
width або scale цільова ширина в px або множник (1 = полотно × 2)
output шлях для файлу; типово $TYTS_OUTPUT_DIR/tyts-<час>.<ext>
preview повернути зменшений PNG (≤ 1024 px) як картинку для самоперевірки (типово так)

Відповідь: JSON { path, bytes, width, height, imageSize, sourceImageSize, pins, document } і preview.

{
  "image": "./photo.jpg",
  "pins": [
    { "type": "rectangle", "x": 130, "y": 360, "width": 220, "height": 130, "text": "Техніка" },
    { "type": "point", "x": 460, "y": 460, "text": "Дорога", "color": "#0088FF" },
    { "type": "arrow", "start": { "x": 300, "y": 120 }, "end": { "x": 520, "y": 260 }, "text": "Напрямок руху" }
  ],
  "width": 1600
}

Типи фігур — усі, що є в редакторі: point, rectangle, ellipse, polygon, polyline, arrow, text (з point — «точка з текстом»), compass, standalone, numbering, image (картинка поверх фото), redact (розмиття / пікселізація / заливка), linked (ще одна виноска до тієї ж фігури).

Картка-виноска будь-якого піна: text, photo (фото в картці — шлях, URL або data URL), textPosition (текст над / під фото), theme, bold / italic / underline / strike, fontSize, textColor, cardBackground (none — прозорий), card (явна позиція), hideCallout, bends (злами лінії). Фігура: color, strokeStyle, fillStyle, strokeWidth.

Кілька об'єктів на одну виноску: attachTo: "<id>" у фігури (point, polygon, polyline, rectangle, ellipse, arrow) — своєї картки в неї немає, лінія йде до картки піна з цим id.

Картки без card MCP розкладає так само, як редактор (колонки від правого краю), але картка з фото вища — під пропорції фото. Поля кожного типу — у ресурсі tyts://docs/annotations (агент читає його сам).

{
  "image": "./scene.jpg",
  "pins": [
    { "id": "tech", "type": "rectangle", "x": 130, "y": 360, "width": 220, "height": 130, "text": "Техніка", "photo": "./closeup.jpg" },
    { "type": "linked", "to": "tech", "text": "Друга виноска до тієї ж рамки" },
    { "type": "redact", "x": 40, "y": 40, "width": 120, "height": 60, "mode": "pixelate" }
  ],
  "crop": { "x": 0, "y": 100, "width": 1600, "height": 900 },
  "stamp": { "image": "./logo.png", "corner": "se" }
}

Не вміє лише часткове форматування тексту (жирне окреме слово): стиль задається на всю картку.

tyts_image_info

{ image } → { width, height, mime, editorSize }. Викликати першим, щоб знати систему координат.

tyts_validate

{ document } → { ok: true } або { ok: false, code, message } без рендера.

Ресурс і промпт

Як це працює

Сервер піднімає статику з вбудованою embed-збіркою ТИЦЬ на 127.0.0.1, відкриває її в headless Chrome і керує редактором через postMessage-протокол @3rtools/tyts-embed. Браузер стартує ліниво на першому виклику і закривається після 5 хв без роботи. Файли нікуди не надсилаються.

Перевірити, що працює

Без агента — запустити сервер напряму (у stderr має зʼявитися [tyts-mcp] ready):

npx -y @3rtools/tyts-mcp

Подивитися інструменти, ресурс і промпт та викликати їх вручну:

npx @modelcontextprotocol/inspector npx -y @3rtools/tyts-mcp

Через агента: tyts_image_info на будь-якому фото має повернути розмір у пікселях, далі tyts_render з кількома pins — шлях до файлу і preview.

Ліцензія

Apache-2.0. Шрифт Road UI — CC BY-ND 4.0 (див. NOTICE).