Три способи використати ТИЦЬ у своїх системах
Редактор анотованих колажів можна вбудувати в чужий застосунок, керувати ним програмно через postMessage-API, або віддати агентам як MCP-інструмент, який робить готові картинки з опису.
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');
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');
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 кроки
- Відкрийте пісочницю API: там кожна команда з описом, запитом і відповіддю, а поруч живий редактор.
- Підключіть SDK на свою сторінку і вкажіть
srcна цю збірку редактора:https://tyts.vercel.app/embed/. Безпека (дозволені origin-и, токен) задається при збірці, див. вкладку Embed. - Для агентів додайте 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 })
onChange(doc, { dirty })приходить після кожної зміни з дебаунсом 250 мс;dirtyкаже, чи є незбережені зміни відносно того, що ви завантажили або підтвердили черезmarkSaved.- Усі методи повертають проміси, які резолвляться відповіддю редактора або відхиляються
{ code, message }(timeout,invalid_document,origin_denied, …). - Документ містить картинку як base64, тож зберігайте його за
dirty, а не на коженonChange. - Рецепти (попередження про незбережене, завантаження файлу, PDF на сервер, кілька редакторів) — довіднику API, розділ 9.
3. Безпека, дані, власний хостинг
Документ і картинка ходять лише між вашою сторінкою та iframe у браузері користувача (postMessage). На сервер ТИЦЬ нічого не надсилається; публічна збірка на Vercel — статичні файли.
Публічний редактор приймає
initз будь-якого origin без токена. Перший успішнийinitпривʼязує сесію до origin вашої сторінки, повідомлення з інших вікон і origin-ів ігноруються.Якщо потрібно обмежити, хто може вбудовувати редактор, або працювати офлайн, зберіть і викладіть його самі (потрібен доступ до репозиторію):
git clone <repo> tyts && cd tyts && npm install VITE_TYTS_EMBED_ALLOWED_ORIGINS=https://app.example,https://staging.example \ VITE_TYTS_EMBED_TOKEN=change-me \ npm run build:embed # → build/embed/ — статичні файли, викласти будь-деVITE_TYTS_EMBED_ALLOWED_ORIGINS— origin-и, з яких дозволеноinit;VITE_TYTS_EMBED_TOKEN— секрет дляinit({ token }). Шляхи в збірці відносні, тека може лежати на іншому origin, ніж host-сторінка. Тоді вsrcвказуєте свою адресу замістьhttps://tyts.vercel.app/embed/.SDK і збірка мають однакову версію протоколу (
TytsEmbed.PROTOCOL); при невідповідностіinitвідхиляється зprotocol_mismatch.
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 } |
Редактор сам надсилає:
{ type: 'tyts:ready', protocol: 1 }— один раз після завантаження;init, надісланий раніше, host має відкласти до цього моменту.{ type: 'tyts:change', document, dirty }— післяinit, з дебаунсом.{ type: 'tyts:error', requestId?, error: { code, message } }.
Повні 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. Як перевірити
- Пісочниця: https://tyts.vercel.app/embed/api.html — кожна команда з описом, запитом, відповіддю й логом повідомлень.
- Демо host-сторінки: https://tyts.vercel.app/embed/demo.html.
- Свій host без збірки: скопіюйте приклад із демо і замініть
srcна свій.
ТИЦЬ Embed API — довідник
Описано Клодом
Повний опис програмного інтерфейсу редактора: JS SDK, події, помилки, сирий postMessage-протокол,
формат документа і React-handle. Як підключити SDK і викласти редактор — встановлення.
Інтерактивна версія з живими запитами — api.html у embed-збірці.
Зміст
- Огляд
- Життєвий цикл сесії
- SDK
TytsEmbed - Події
- Помилки
- Протокол postMessage
- Формат документа
TytsDocument - React:
TytsEditorHandle - Рецепти
- Обмеження
- 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} ──│
- До
initредактор відповідає на будь-яку команду помилкоюnot_initialized. - Після
initприймаються лише повідомлення від батьківського вікна і з того ж origin, що йinit. readyприходить один раз. SDK відкладає всі запити до нього, тож методи можна викликати одразу післяcreate().- Повторний
initу тій самій сесії дозволений (наприклад, щоб передати новий документ).
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 рахується від «базового» стану. Базовим стає:
- стан одразу після
loadImage,loadDocument,init({ document })(плюс автоматична розкладка карток, яку редактор робить у перші 150 мс); - стан на момент
markSaved().
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.
Правила безпеки з боку редактора:
- приймаються лише повідомлення, у яких
event.source === window.parent; - до
init— лишеtyts:init, і лише з дозволеного origin; - після
init— лише з origin, що зробивinit; tyts:readyіде зtargetOrigin: '*'(без даних) або на кожен дозволений origin; усе інше — лише на origin сесії.
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), якщо:
versionне1(або не1/2для автосейв-чернетки зsavedAt);backgroundImageне рядок, що починається зdata:;imageSizeне пара скінченних чисел;sourceImageSizeне пара і неnull;- будь-який пін без обовʼязкового поля з таблиці вище,
label < 1, абоanchorневідомого типу чи без потрібних полів; cardPositionsне обʼєкт, або якась позиція не маєx, y, width, height.
Невідомі додаткові поля ігноруються, документ не змінюється при відхиленні.
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. Обмеження
- Документ містить зображення як base64, тож
getDocument/onChangeможуть важити мегабайти. Для великих знімків зберігайте документ рідше (заdirty, а не на коженonChange). onChangeіде з дебаунсом 250 мс і лише післяinit; зміни доinitприходять однією подією після нього.- Координати після кропу в редакторі перераховуються редактором; host отримує вже нові
imageSizeі піни. export('pdf')— одна сторінка A4 з вписаним зображенням; масштаб не застосовується.- Налаштування
settings(шрифт, товщина) в embed-режимі зберігаються в localStorage origin-у бандла так само, як у вебверсії; документ, переданий черезloadDocument, їх перезаписує. - Історія undo не переживає
loadDocument/loadImage/reset.
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.
Вимоги
- Node.js ≥ 20
- Google Chrome, Chromium або Edge на машині (пакет не завантажує Chromium). Якщо браузер у
нестандартному місці —
TYTS_CHROME_PATH=/шлях/до/chrome.
Підключення
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 } без рендера.
Ресурс і промпт
tyts://docs/annotations— формат анотацій, палітра, поради.- Промпт
annotate-image(image, task?)— покрокова інструкція агенту: розмір → рендер → перевірка preview.
Як це працює
Сервер піднімає статику з вбудованою 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).