빠른 시작
플러그인은 파일 세 개를 담은 zip입니다. 화면은 평범한 HTML 문서 하나 — 에디터가 스크립트보다 먼저 SSHOWPlugin SDK를 주입하고 샌드박스 패널에 띄우며, 아래가 그 화면이 닿을 수 있는 표면 전부입니다.
my-plugin.sshowplugin (zip)
├─ plugin.json — manifest
├─ ui.html — the plugin screen (manifest.main)
└─ icon.svg — listing icon (optional)
plugin.json은 신원과 진입 문서를 선언합니다:
{
"id": "com.example.hello",
"name": "Hello",
"version": "1.0.0",
"api": 1,
"main": "ui.html",
"description": "Inserts a greeting card.",
"author": "SSHOW",
"icon": "icon.svg"
}
ui.html이 곧 플러그인입니다 — 연결한 뒤 핸들로 읽고 씁니다:
<!doctype html>
<button id="insert">Insert</button>
<script>
(async () => {
const api = await SSHOWPlugin.connect();
api.ui.resize(160);
document.querySelector('#insert').addEventListener('click', async () => {
await api.document.applyActions([{
op: 'create_object', type: 'text', config: {
name: 'greeting',
data: { text: 'Hello, SSHOW!', fontSize: 48, autoSize: true },
transform: { x: 200, y: 200 }
}
}], 'Hello plugin');
});
})();
</script>
바로 실행해 보기: 세 파일을 my-plugin.sshowplugin 으로 zip해 에디터 플러그인 패널의 + 버튼으로 임포트하세요. 수정할 때마다 다시 임포트하거나, 아래 데스크톱 개발 루프를 쓰면 됩니다.
매니페스트 레퍼런스
필수 5개, 선택 3개. 모르는 필드는 무시되므로 최신 매니페스트도 구버전 에디터에서 로드됩니다.
| 필드 | 설명 |
|---|---|
id |
고유 id — 소문자 역도메인 형식 (필수) |
name |
목록과 패널 제목에 보이는 이름 (필수) |
version |
x.y.z 버전 (필수) |
api |
플러그인 API 버전 — 현재 1 (필수) |
main |
화면 문서 파일명, 예: ui.html (필수) |
description |
목록에 보이는 한 줄 설명 (선택) |
author |
목록에 보이는 제작자 이름 (선택) |
icon |
패키지 안의 아이콘 파일명 — png/svg/jpg/webp (선택) |
제약 — id: 소문자 역도메인([a-z0-9.-], 최대 100자, 최초 제출자가 영구 소유) · version: 정확한 x.y.z · api: 현재 1 · name ≤ 100자 · description ≤ 2000자 · author ≤ 100자 · icon: 패키지 안의 png / svg / jpg / webp. 카탈로그의 제작자 표시는 항상 검증된 제출 계정을 따릅니다.
연결
화면 문서 안에서 SSHOWPlugin.connect()를 호출하면 API 핸들이 반환됩니다. 핸들에는 apiVersion(현재 1), engineVersion, 그리고 아래의 document·events·ui 네임스페이스가 들어 있습니다.
const api = await SSHOWPlugin.connect();
api.apiVersion; // 1 — the bridge contract this editor speaks
api.engineVersion; // engine build, for display only
api.document; // getState() · getObject(id) · getSelection() · setSelection(ids)
// setActiveScene(sceneId) · getTimelineTime() · applyActions(actions, label)
api.assets; // get(uri) · register(bytes, { mimeType, originalName })
api.events; // on(type, callback) · off(type, callback)
api.ui; // resize(size) · getTheme()
읽기
document.getState()- 문서 전체의 스냅샷 — 캔버스 크기, 활성 씬 id, 씬 목록(활성 씬은 오브젝트 전체, 나머지는 요약), 폰트 목록.
document.getObject(id)- 오브젝트 하나의 스냅샷, 없으면 null.
document.getSelection()- 현재 선택된 오브젝트들의 스냅샷 배열.
전형적인 읽기 흐름:
const { canvas, activeSceneId, scenes } = await api.document.getState();
const selection = await api.document.getSelection();
// selection[0] → { id, type, name, transform, size, style, data, … }
반환값은 모두 스냅샷(사본)입니다. 값을 고쳐도 문서는 바뀌지 않습니다 — 변경은 반드시 액션으로 보냅니다.
아주 큰 필드는 스냅샷에서 생략됩니다 — 긴 data.src는 실제 값 대신 <src len=…> 마커로 옵니다. 마커를 set에 다시 넣지 마세요. 실제 바이트는 assets.get으로 읽습니다.
에디터 상태
읽기를 보완하는 UI 상태 세터 두 개와 에디터 상태 읽기 하나입니다. 모두 문서나 undo 히스토리를 건드리지 않습니다.
document.setSelection(ids)- 활성 씬의 오브젝트 id들을 에디터에서 선택합니다 — 방금 만든 오브젝트를 선택된 상태로 사용자에게 넘기세요. 없는 id는 조용히 제외됩니다.
document.setActiveScene(sceneId)- 활성 씬을 전환합니다. 모르는 id는 거부되므로 잘못된 씬에 계속 쓰는 일이 없습니다.
document.getTimelineTime()- 에디터의 애니메이션 시계(ms) — Animation 모드에서는 플레이헤드, Design 모드에서는 0(문서 포즈)입니다. 타임라인 작업(베이크·프리셋)의 시작점으로 쓰고, 쓰기 직전에 다시 읽으세요.
쓰기 — applyActions
document.applyActions(actions, label)이 문서를 편집하는 유일한 방법입니다. 액션 배열이 하나의 편집으로 묶여 실행취소 한 번에 전부 되돌아가고, 결과로 { applied, skipped }가 반환됩니다. 깨진 액션은 전체를 막지 않고 skipped로 보고됩니다.
각 액션은 op와 대상, 바꿀 내용으로 구성됩니다. 오브젝트는 id, 씬은 sceneId로 지정하고(생략하면 활성 씬), update_* 액션은 set 객체에 바꿀 필드만 담습니다.
19개 op 전부 같은 actions 배열에 담겨 순서대로 커밋됩니다. 괄호 안 필드는 선택이며, sceneId를 생략하면 어디서든 활성 씬을 대상으로 합니다.
오브젝트
| Op | 필드 (+ 선택) | 비고 |
|---|---|---|
create_object |
type, config (+ sceneId, options) |
type은 rect · circle · path · text · image · video · audio · group · frame 중 하나입니다. config.id에 직접 id를 주면 같은 배치의 뒤 액션에서 그 오브젝트를 겨냥할 수 있고, options.parentObjectId는 그룹/프레임 안에 생성, options.index는 목록 위치를 지정합니다. |
update_object |
id, set (+ sceneId) |
set 안에 담은 키만 바뀝니다 — 아래 유효한 set 키를 참조하세요. |
delete_object |
id (+ sceneId) |
유효하지 않은 id는 배치를 실패시키지 않고 skipped로 빠집니다. |
duplicate_object |
id (+ sceneId, options) |
|
move_object |
id (+ sceneId, options) |
options.parentObjectId는 컨테이너로 재부모화, options.index는 그 안에서 순서를 바꿉니다. |
group_objects |
ids (+ config, sceneId) |
id 2개 이상이 필요하며, 같은 배치에서 앞서 만든 config.id 값도 포함할 수 있습니다. |
ungroup |
id (+ sceneId) |
|
convert_to_path |
id (+ sceneId) |
씬
| Op | 필드 (+ 선택) | 비고 |
|---|---|---|
create_scene |
config (+ options) |
여기서도 config.id 직접 지정이 동작합니다. |
update_scene |
set (+ sceneId) |
|
delete_scene |
(+ sceneId) |
|
duplicate_scene |
(+ sceneId) |
|
move_scene |
sceneId, newIndex |
|
set_scene_size |
size: { width, height } |
캔버스 크기는 문서 전역입니다 — 모든 씬에 적용됩니다. |
변수
| Op | 필드 (+ 선택) | 비고 |
|---|---|---|
create_variable |
config (+ options) |
|
update_variable |
variableId, set |
|
delete_variable |
variableId |
|
move_variable |
variableId, newIndex |
문서
| Op | 필드 (+ 선택) | 비고 |
|---|---|---|
set_document |
set |
문서 메타데이터 — 아래 유효한 set 키를 참조하세요. |
유효한 set 키
set 안에 모르는 키가 있으면 그 액션 전체가 건너뛰어집니다(skipped에 이유와 함께 담김):
update_object.set — name · description · size · transform · distort · layout · style ·
opacity · blendMode · locked · visible · motion · interaction · data
update_scene.set — name · description · notes · style · visible · motion · interaction · data · clip
set_document.set — name · description · notes
호출 하나가 undo 한 스텝 — 타깃은 스냅샷에서 얻은 오브젝트 id입니다:
const { applied, skipped } = await api.document.applyActions([
{
op: 'create_object', type: 'rect', config: {
name: 'bar',
size: { width: 120, height: 240 },
transform: { x: 400, y: 300, anchorX: 0.5, anchorY: 0 },
style: { fills: [{ type: 'solid', color: '#8A8A8E' }], strokes: [], effects: [] }
}
},
{ op: 'update_object', id: selection[0].id, set: { transform: { rotateZ: 15 } } },
{ op: 'delete_object', id: obsoleteId }
], 'My plugin edit');
// applied — actions committed as ONE undo step · skipped — malformed entries with reasons
set 병합 규칙
- transform · size · data · layout — 보낸 키만 바뀌고 나머지는 유지됩니다.
- style · distort — 통째로 교체됩니다. style은 항상 fills·strokes·effects 전체를 보내세요.
- motion — 서브컨테이너 단위로 병합됩니다. animations만 보내면 transitions는 유지됩니다(반대도 동일). 키프레임 하나를 고칠 때는 스냅샷에서 서브컨테이너 전체를 읽어 수정한 뒤 통째로 되돌려 보내세요.
정규화 브리지
- transform.rotateX / rotateY / rotateZ(도 단위, 레거시 transform.rotate는 rotateZ)는 라디안으로 자동 변환됩니다. 모션 트랙의 transform.rotate* 값은 라디안(엔진 단위)입니다.
- color만 있고 type이 없는 style 페인트는 'solid'로 기본 처리되고, 잘못된 effects 항목은 버려집니다.
- data.text 안의 리터럴 \n 과 \t 는 실제 줄바꿈과 탭이 됩니다.
에셋
assets.get(uri)- asset:// 주소의 원본 바이트와 MIME 타입·파일명을 반환합니다. 없으면 null. 선택한 이미지의 data.src로 원본을 읽어 캔버스에서 가공할 수 있습니다.
assets.register(bytes, { mimeType, originalName })- 바이트를 프로젝트 에셋으로 등록하고 asset:// 주소를 반환합니다. 같은 내용은 같은 주소로 중복 제거되며, 개당 10MB까지 허용됩니다(기본 요금제의 파일 업로드 한도와 동일).
선택한 이미지의 픽셀 편집 풀사이클:
const [image] = await api.document.getSelection(); // an image object
const { bytes, mimeType } = await api.assets.get(image.data.src);
const edited = await process(bytes); // your pixel work
const uri = await api.assets.register(edited, { mimeType, originalName: 'edited.png' });
await api.document.applyActions([
{ op: 'update_object', id: image.id, set: { data: { src: uri } } }
], 'Edit image');
등록한 주소는 곧바로 액션(예: 이미지의 data.src)으로 참조하세요. 아무 곳에서도 참조되지 않는 에셋은 정리 대상이 됩니다.
이벤트
events.on(type, callback) / events.off(type, callback)로 구독합니다. 콜백에는 인자가 없습니다 — 다시 조회하라는 신호이므로 읽기 API로 재조회합니다. 이벤트 타입은 아래 세 가지가 전부이며, 그 밖의 구독은 거부됩니다. 플러그인이 닫히면 구독은 자동으로 해제됩니다.
- history:update — 문서가 바뀔 때(편집·실행취소·다시실행).
- ui:modes:edit:changeSelectedObjects — 선택이 바뀔 때.
- motion:animation:timeUpdate — 애니메이션 시계가 움직일 때(시크, 재생 중엔 매 프레임 — 디바운스 권장).
await api.events.on('ui:modes:edit:changeSelectedObjects', async () => {
const selection = await api.document.getSelection(); // re-query — events carry no payload
render(selection);
});
테마
모든 플러그인 문서에 에디터 디자인 토큰이 CSS 커스텀 프로퍼티로 주입되며, 에디터와 같은 라이트/다크 미디어쿼리에 연결됩니다 — var(--sshow-…)로 스타일하면 테마 전환 시 패널이 코드 없이 자동으로 따라갑니다.
--sshow-primary /* accent (#2196F3) */
--sshow-primary-strong /* filled active/selected surface */
--sshow-primary-soft /* its hover */
--sshow-primary-foreground /* text over the accent */
--sshow-secondary
--sshow-foreground /* body text — follows light/dark */
--sshow-background /* panel surface (translucent) */
--sshow-background-solid
--sshow-border-color
--sshow-radius /* 13px */
--sshow-font-size /* 12px — matches native panel text */
--sshow-scrollbar-size /* 6px */
--sshow-scrollbar-radius /* 3px */
문서의 스크롤바도 에디터와 같은 것을 씁니다 — 운영체제 기본 스크롤바(윈도우는 15px) 대신 패널이 쓰는 6px 슬림 바입니다. ::-webkit-scrollbar 규칙을 직접 선언하면 덮어쓸 수 있습니다.
스크립트 로직(예: 캔버스 드로잉)에서는 활성 모드와 해석된 색을 읽으세요:
const { mode, colors } = await api.ui.getTheme();
// mode → 'light' | 'dark'
// colors → { primary, primaryForeground, secondary,
// foreground, background, backgroundSolid, borderColor }
// React to a theme flip in JS (styles via var() follow automatically):
matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => render());
패널 UI
ui.resize(size) — 화면 크기를 요청합니다. 숫자는 높이 요청이고, { width, height }로 두 축을 지정할 수 있습니다. 패널이 요청한 화면 크기에 맞춰 리사이즈되며(패널 자체 한계로 제한), 요청하지 않으면 기본 패널 크기를 가득 채웁니다.
키보드 — 화면이 포커스를 쥔 동안에도 에디터 단축키는 그대로 동작합니다. 플러그인이 처리하지 않은 키는 에디터로 전달됩니다(Tab은 디자인/애니메이션 전환, Ctrl+Z는 실행 취소, Space는 팬). 키를 직접 쓰려면 window에 닿기 전에 preventDefault() 또는 stopPropagation()을 호출하세요. 텍스트 입력란에 포커스가 있는 동안에는 아무것도 전달되지 않습니다.
제약과 버전
- 화면 문서는 자기완결이어야 합니다 — 네트워크가 차단되므로 스크립트·스타일은 인라인으로, 이미지는 data:/blob:만 동작합니다. WebAssembly는 컴파일되지만 eval·new Function은 막힙니다.
- 패키지는 zip 엔트리 64개, 파일당 압축 해제 5MB, 패키지당 10MB까지 허용됩니다. 등록하는 에셋도 개당 10MB가 상한입니다.
- plugin.json의 api가 에디터의 API 버전(1)과 다르면 로드가 거부됩니다. API 확장은 기존 플러그인이 깨지지 않는 방향으로만 이뤄집니다.
- 실패한 호출은 이유 문자열과 함께 reject됩니다.
개발 루프
게시 전 반복 작업 두 가지 방법:
- 재임포트 (웹 + 데스크톱)
- 에디터 플러그인 패널의 + 버튼이 .sshowplugin 을 임포트합니다. 같은 id는 중복으로 거부되므로, 행의 − 버튼으로 제거한 뒤 다시 임포트하세요.
- devPath 핫 리로드 (데스크톱)
- plugins.devPath 설정을 플러그인 폴더(plugin.json + main + 아이콘)로 지정하세요. 저장할 때마다 열린 에디터에 재등록되고 실행 중이던 플러그인은 다시 열립니다 — 반복 중엔 zip이 필요 없습니다.
카탈로그에 게시하기
누구나 /developers 개발자 콘솔에서 플러그인을 제출할 수 있습니다. 모든 버전은 게시 전 사람이 검수하며, 결과는 인앱 알림과 이메일로 전달됩니다.
- 1 plugin.json, 메인 문서, 아이콘을 .sshowplugin 패키지(zip)로 묶으세요.
- 2 /developers 에서 업로드하세요 — 서버가 패키지를 검증하고 manifest 를 추출합니다. 다시 입력할 것은 없습니다.
- 3 콘솔에서 검수 상태를 확인하세요. 승인된 버전은 즉시 카탈로그에 게시됩니다.
- 버전은 불변이며 항상 증가해야 합니다(x.y.z) — 거절을 고치려면 더 높은 버전을 제출하세요.
- manifest id 는 최초 제출자가 영구 소유합니다. 직접 관리하는 역도메인 id 를 사용하세요.
- 검수된 zip 이 사용자가 받는 파일 그대로입니다 — 서버는 패키지를 절대 재작성하지 않습니다.