Sign in

    moonlight

    Lightweight SVG editor by mizchi/luna, like Excalidraw

    js
    luna
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    9 months ago
    Downloads
    55

    #Moonlight

    A lightweight SVG editor built with MoonBit and the Luna reactive UI library. Inspired by Excalidraw, Moonlight provides a simple yet powerful interface for creating and editing vector graphics.

    #Demo

    Live Demo: https://moonlight.mizchi.workers.dev

    Try the editor directly in your browser. No installation required.

    #Features

    • SVG-native: Works directly with SVG elements (rect, circle, ellipse, line, text)
    • Multiple modes: Full editor, embeddable widget, and Web Component
    • Reactive UI: Built on Luna's signal-based reactivity
    • Undo/Redo: Full command history support
    • Grid snapping: Optional snap-to-grid for precise alignment
    • Connected lines: Lines can connect to element anchor points and follow when moved
    • Dark/Light themes: Toggle between dark and light modes
    • Import/Export: Save and load SVG files

    #Getting Started

    #Prerequisites

    #Installation

    git clone <repository-url> cd moonlight pnpm install

    #Development

    # Start development server pnpm dev # Type checking moon check # Format code moon fmt # Build for production pnpm build

    #Access the Editor

    • Full Editor: http://localhost:5173/
    • Embed Mode: http://localhost:5173/embed.html
    • Web Component: http://localhost:5173/webcomponent.html

    #User Guide

    #Tools

    ToolShortcutDescription
    SelectV or 1Select and move elements
    RectangleR or 2Draw rectangles
    CircleC or 3Draw circles/ellipses
    LineL or 4Draw lines and arrows
    TextT or 5Add text elements

    #Keyboard Shortcuts

    #General

    ShortcutAction
    Ctrl+Z / Cmd+ZUndo
    Ctrl+Shift+Z / Cmd+Shift+ZRedo
    Ctrl+Y / Cmd+YRedo (alternative)
    Delete / BackspaceDelete selected element
    EscapeDeselect / Cancel operation

    ShortcutAction
    Arrow KeysMove selected element (5px)
    Shift + Arrow KeysMove selected element (1px, precise)
    Ctrl + Arrow KeysMove selected element (10px, fast)

    #Element Operations

    ShortcutAction
    Ctrl+D / Cmd+DDuplicate selected element
    [Send backward
    ]Bring forward
    Ctrl+[ / Cmd+[Send to back
    Ctrl+] / Cmd+]Bring to front

    #View

    ShortcutAction
    GToggle grid visibility
    Ctrl+G / Cmd+GToggle grid snapping

    #Mouse Operations

    • Click: Select element
    • Click + Drag: Move element or draw new shape
    • Click on canvas: Deselect all
    • Right-click: Context menu (delete, duplicate, layer ordering)

    #Working with Lines

    Lines can be connected to other elements:

    1. Select the Line tool (L)
    2. Click near an element's anchor point (edges or center)
    3. Drag to another element's anchor point
    4. The line will automatically follow when connected elements are moved

    #Detail Panel

    The right sidebar shows properties of the selected element:

    • Position: X/Y coordinates (editable)
    • Size: Width/Height or radius (editable)
    • Style: Fill color, stroke color, stroke width
    • Line markers: Add arrows to line endpoints

    #Themes

    Toggle between light and dark themes using the theme button in the toolbar.

    #Usage Modes

    #1. Full Editor Mode

    The standalone editor with all features enabled.

    <script type="module" src="./main.ts"></script>

    #2. Embed Mode

    Embed the editor in your application using JavaScript:

    <script type="module" src="./embed.ts"></script> <div id="editor-container"></div> <script> const editor = MoonlightEditor.create( document.getElementById('editor-container'), { width: 800, height: 600, theme: 'light', // or 'dark' readonly: false } ); // API const svg = editor.exportSvg(); editor.importSvg(svgString); editor.clear(); editor.hasFocus(); </script>

    #3. Web Component Mode

    Use as a custom HTML element:

    <script type="module" src="./webcomponent.ts"></script> <moonlight-editor width="800" height="600" theme="light"> </moonlight-editor> <script> const editor = document.querySelector('moonlight-editor'); // API (same as embed mode) const svg = editor.exportSvg(); editor.importSvg(svgString); editor.clear(); editor.hasFocus(); </script>

    #Attributes

    AttributeTypeDefaultDescription
    widthnumber400Canvas width in pixels
    heightnumber300Canvas height in pixels
    themestring"light"Theme: "light" or "dark"
    readonlybooleanfalseDisable editing

    #4. CDN Distribution (Copy & Paste)

    Embed the editor in any HTML page with just 2 lines:

    #Minimal Example

    <moonlight-editor width="800" height="600"></moonlight-editor> <script src="https://moonlight.mizchi.workers.dev/moonlight-editor.component.js" async></script>

    #With Initial SVG (using template)

    <moonlight-editor width="800" height="500"> <template> <svg viewBox="0 0 800 500"> <rect x="100" y="100" width="120" height="80" fill="#4CAF50" stroke="#2E7D32" stroke-width="2"/> <circle cx="400" cy="200" r="50" fill="#2196F3" stroke="#1565C0" stroke-width="2"/> <text x="300" y="350" font-size="24" fill="#333">Hello Moonlight!</text> </svg> </template> </moonlight-editor> <script src="https://moonlight.mizchi.workers.dev/moonlight-editor.component.js" async></script>

    #With API Usage

    <button onclick="alert(editor.exportSvg())">Export SVG</button> <button onclick="editor.clear()">Clear</button> <moonlight-editor id="editor" width="800" height="600" theme="dark"></moonlight-editor> <script src="https://moonlight.mizchi.workers.dev/moonlight-editor.component.js" async></script> <script> const editor = document.getElementById('editor'); // editor.exportSvg() - Get SVG string // editor.importSvg(svg) - Load SVG // editor.clear() - Clear canvas // editor.hasFocus() - Check focus state </script>

    #Attributes

    AttributeDefaultDescription
    width400Canvas width (px)
    height300Canvas height (px)
    theme"light""light" or "dark"
    readonly-Add to disable editing

    #Self-hosting with Cloudflare Workers

    pnpm build:all # Build component pnpm deploy # Deploy to Workers

    The worker serves files with CORS headers, enabling cross-origin usage.

    #API Reference

    #Editor Handle

    Both embed and web component modes return an editor handle with these methods:

    interface EditorHandle { // Export current canvas as SVG string exportSvg(): string; // Import SVG string to canvas importSvg(svg: string): void; // Clear all elements clear(): void; // Check if editor has focus hasFocus(): boolean; // Register change callback onChange(callback: () => void): void; }

    #Architecture

    src/ ├── main.mbt # Full editor entry point ├── ui.mbt # UI components (sidebar, toolbar) ├── render.mbt # SVG rendering ├── core/ │ └── scene.mbt # Editor state management ├── model/ │ ├── types.mbt # Data models (Element, Style, etc.) │ ├── command.mbt # Undo/Redo commands │ └── element_ops.mbt # Pure element operations ├── lib/ # Shared library code ├── embed/ # Embed mode entry point ├── webcomponent/ # Web Component entry point └── preview/ # Preview mode

    #Tech Stack

    • MoonBit: Systems programming language that compiles to WebAssembly/JavaScript
    • Luna: Signal-based reactive UI library for MoonBit
    • Vite: Build tool with hot module replacement
    • vite-plugin-moonbit: Vite plugin for MoonBit integration

    #License

    MIT

    EmbedOptions

    pub struct EmbedOptions {
    width : Int
    height : Int
    doc_width : Int
    doc_height : Int
    zoom : Double
    theme : String
    is_readonly : Bool
    initial_svg : String?
    }

    エディタオプション(JavaScript から渡される)

    EmbedOptions::default

    fn EmbedOptions::default() -> EmbedOptions

    デフォルトオプション

    EmbedOptions::from_js

    JavaScript オブジェクトからオプションを作成

    ImportResult

    pub struct ImportResult {
    elements : Array[
    Element
    ]
    is_moonlight : Bool
    plain_svg_ids : Array[String]
    }

    インポート結果(要素と検証結果)

    add_keyboard_listener

    fn add_keyboard_listener(handler : (
    Any
    ) -> Unit) -> Unit

    window に keydown イベントリスナーを追加

    add_keydown_listener

    fn add_keydown_listener(handler : (
    Any
    ) -> Unit) -> Unit

    keydown リスナー

    add_keyup_listener

    fn add_keyup_listener(handler : (
    Any
    ) -> Unit) -> Unit

    keyup リスナー

    add_sample_shapes

    fn add_sample_shapes(state :
    EditorState
    , new_id : () -> String) -> Unit

    サンプル図形を追加

    app_main

    fn app_main() -> Unit

    アプリケーションのエントリポイント(外部から呼び出し可能)

    clear_children_ffi

    fn clear_children_ffi(el :
    Any
    ) -> Unit

    SVG コンテナの子要素をクリア

    copy_svg_as_image

    fn copy_svg_as_image(svg_str : String, width : Int, height : Int, on_done : () -> Unit, on_error : () -> Unit) -> Unit

    SVGをPNG画像としてクリップボードにコピー

    copy_text_to_clipboard

    fn copy_text_to_clipboard(text : String, on_done : () -> Unit, on_error : () -> Unit) -> Unit

    テキストをクリップボードにコピー

    create_arrow_defs

    fn create_arrow_defs() ->
    Any

    SVG 定義(マーカー)を作成

    create_arrow_defs_node

    fn create_arrow_defs_node() ->
    DomNode

    SVG defs を DomNode として作成

    create_doc_shadow_filter

    fn create_doc_shadow_filter(is_dark_theme : Bool) ->
    Any

    ドキュメントシャドウフィルタを defs に追加

    create_doc_shadow_filter_node

    fn create_doc_shadow_filter_node(is_dark_theme : Bool) ->
    DomNode

    ドキュメントシャドウフィルタを defs Node として作成

    create_embedded_app

    埋め込みモード用のアプリを作成(外部から呼び出し可能)

    create_js_editor

    fn create_js_editor(container :
    Any
    , options : EmbedOptions) ->
    Any

    JavaScript 埋め込み用エディタを作成 container: DOM コンテナ要素 options: エディタオプション 戻り値: エディタハンドル

    create_textarea_ffi

    fn create_textarea_ffi(style : String, initial_value : String, on_commit : (String) -> Unit, on_escape : (String) -> Unit, on_input : (String) -> Unit) ->
    Any

    テキストエリアを作成(IMEコンポジション対応)

    debug_log

    fn debug_log(msg : String) -> Unit

    デバッグログ

    download_file

    fn download_file(content : String, filename : String, mime_type : String) -> Unit

    ファイルをダウンロード

    element_to_moonlight_svg

    fn element_to_moonlight_svg(el :
    Element
    ) -> String

    要素を Moonlight SVG 文字列に変換(メタデータ付き)

    element_to_svg_string

    fn element_to_svg_string(el :
    Element
    ) -> String

    要素を SVG 文字列に変換(エクスポート用)

    elements_to_moonlight_svg

    fn elements_to_moonlight_svg(elements : Array[
    Element
    ], width : Int, height : Int, bg_color : String) -> String

    全要素を Moonlight SVG 形式で出力

    elements_to_svg

    fn elements_to_svg(elements : Array[
    Element
    ], width : Int, height : Int) -> String

    全要素を SVG 文字列に変換

    get_attribute

    fn get_attribute(el :
    Any
    , name : String) ->
    Any

    要素の属性を取得(存在しない場合はnull)

    get_child_at_ffi

    fn get_child_at_ffi(parent :
    Any
    , index : Int) ->
    Any

    子要素を取得

    get_closest_data_id_ffi

    fn get_closest_data_id_ffi(el :
    Any
    ) ->
    Any

    要素から data-id を持つ最も近い親要素の data-id を取得

    get_connected_points

    選択要素に接続しているラインの接続ポイントを収集

    get_current_time

    fn get_current_time() -> Double

    現在時刻を取得(ミリ秒)

    get_event_target_value

    fn get_event_target_value(e :
    Any
    ) -> String

    イベントから target.value を取得

    get_svg_children

    fn get_svg_children(svg :
    Any
    ) ->
    Any

    SVG要素の子要素リストを取得

    get_tag_name

    fn get_tag_name(el :
    Any
    ) -> String

    要素のタグ名を取得

    get_text_content

    fn get_text_content(el :
    Any
    ) -> String

    要素のテキスト内容を取得

    get_url_search_ffi

    fn get_url_search_ffi() -> String

    URL の search パラメータを取得

    hide_popover

    fn hide_popover(popover_id : String) -> Unit

    Popover を非表示

    import_svg_elements

    fn import_svg_elements(svg_str : String, new_id : () -> String) -> Array[
    Element
    ]

    SVG文字列をパースして Element 配列を返す

    import_svg_validated

    fn import_svg_validated(svg_str : String, new_id : () -> String) -> ImportResult

    SVG文字列をパースして検証付きでインポート

    insert_before_ffi

    fn insert_before_ffi(parent :
    Any
    , new_child :
    Any
    , ref_child :
    Any
    ) -> Unit

    指定した子要素の前に挿入

    is_composing_ffi

    fn is_composing_ffi() -> Bool

    IMEコンポジション中かどうかを取得

    is_input_focused_ffi

    fn is_input_focused_ffi() -> Bool

    入力要素にフォーカスがあるかチェック

    load_from_indexeddb

    fn load_from_indexeddb(db_name : String, store_name : String, key : String, on_success : (String) -> Unit, on_not_found : () -> Unit, on_error : () -> Unit) -> Unit

    IndexedDB から文字列を読み込み

    observe_resize_ffi

    fn observe_resize_ffi(el :
    Any
    , callback : (Double, Double) -> Unit) -> Unit

    ResizeObserver で要素のサイズ変更を監視

    observe_window_resize_ffi

    fn observe_window_resize_ffi(callback : (Int, Int) -> Unit) -> Unit

    ウィンドウサイズを監視

    open_file_dialog

    fn open_file_dialog(accept : String, on_file : (String) -> Unit) -> Unit

    ファイル選択ダイアログを開く

    parse_double

    fn parse_double(s : String) -> Double?

    文字列を Double にパース

    parse_svg_string

    fn parse_svg_string(svg_str : String) ->
    Any

    SVG文字列をパースしてDOM要素を返す

    read_clipboard_text

    fn read_clipboard_text(on_success : (String) -> Unit, on_error : () -> Unit) -> Unit

    クリップボードからテキストを読み込み

    remove_child_safe_ffi

    fn remove_child_safe_ffi(parent :
    Any
    , child :
    Any
    ) -> Unit

    子要素を安全に削除(存在しない場合は何もしない)

    remove_element_ffi

    fn remove_element_ffi(el :
    Any
    ) -> Unit

    要素を親から削除

    render_all_anchor_points

    fn render_all_anchor_points(elements : Array[
    Element
    ], exclude_id : String) -> Array[
    Any
    ]

    全てのアンカーポイントを描画(ドラッグ中の可視化用)

    render_all_anchors_vnode

    全アンカーポイントを VNode として描画(ライン編集中、Effect ベース)

    render_box_select_vnode

    矩形選択ボックスを VNode として描画(Effect ベース)

    render_canvas_background

    fn render_canvas_background(doc_width : Double, doc_height : Double, viewport :
    Viewport
    , screen_width : Int, screen_height : Int, is_dark_theme : Bool) -> Array[
    Any
    ]

    Canvas 背景レイヤーを描画
    • 境界外のグレー背景(テーマ連動)
    • ドキュメント境界(白/黒 rect + ドロップシャドウ)

    render_canvas_background_vnode

    Canvas 背景を VNode として描画(Effect ベース)
    • 境界外のグレー背景(テーマ連動)
    • ドキュメント境界(白/黒 rect + ドロップシャドウ)

    render_connected_anchor_highlights

    fn render_connected_anchor_highlights(elements : Array[
    Element
    ], selected_el :
    Element
    ) -> Array[
    Any
    ]

    選択要素に接続しているラインのアンカーポイントを黄色でハイライト描画

    render_connected_point

    fn render_connected_point(point :
    Point
    ) ->
    Any

    接続済みポイントを描画(選択要素に接続しているラインの端点)

    render_connection_highlight

    fn render_connection_highlight(point :
    Point
    ) ->
    Any

    接続ポイントのハイライトを描画(スナップ先)

    render_connection_highlight_node

    接続ポイントハイライトを DomNode として作成

    render_connection_highlight_vnode

    接続ハイライトを VNode として描画(Effect ベース)

    render_context_menu

    コンテキストメニューをレンダリング

    render_delete_button

    fn render_delete_button(state :
    EditorState
    , history :
    History
    , element_id : String, close_menu : () -> Unit) ->
    DomNode

    削除ボタンコンポーネント

    render_editor_modal

    エディタモーダルをレンダリング(フルエディタを別ウィンドウ風に表示)

    render_element

    要素を SVG ノードに変換 parent: テキスト子要素の場合、親要素を渡すと中央に配置

    render_element_anchors

    選択中の図形のアンカーポイントを描画(ドラッグでライン生成用)

    render_element_anchors_nodes

    アンカーポイントを DomNode として作成

    render_element_node

    要素を DomNode に変換(Luna VNode ベース)

    render_element_vnode

    要素を VNode として描画(Effect で直接 DOM 更新)

    render_floating_panel

    フローティングパネルをレンダリング

    render_grid

    fn render_grid(width : Int, height : Int, grid_size : Int, viewport :
    Viewport
    ) -> Array[
    Any
    ]

    グリッド線を描画

    render_grid_nodes

    fn render_grid_nodes(width : Int, height : Int, grid_size : Int, viewport :
    Viewport
    ) -> Array[
    DomNode
    ]

    グリッド線を DomNode として作成

    render_grid_vnode

    グリッド線を VNode として描画

    render_inline_panel

    インラインパネルをレンダリング(モーダル内で使用、position: fixed なし)

    render_layer_operations

    fn render_layer_operations(state :
    EditorState
    , history :
    History
    , element_id : String, close_menu : () -> Unit) ->
    DomNode

    Layer 操作コンポーネント

    render_resize_handles

    リサイズハンドルを描画

    render_resize_handles_nodes

    リサイズハンドルを DomNode として作成

    render_selection_box

    矩形選択ボックスを描画

    render_selection_box_node

    矩形選択ボックスを DomNode として作成

    render_selection_overlay

    選択オーバーレイ(ハンドル・アンカー)を描画(Effect ベース)

    render_sidebar

    サイドバーをレンダリング(後方互換性のため残す)

    render_style_editor

    fn render_style_editor(state :
    EditorState
    , history :
    History
    , element_id : String, close_menu : () -> Unit) ->
    DomNode

    共通スタイルエディタコンポーネント(コンテキストメニュー・詳細パネル両方で使用) close_menu: メニューを閉じるコールバック(コンテキストメニュー用、詳細パネルでは空の関数を渡す) ShapeConfig を使用して、シェープごとに表示する項目を制御

    render_svg_container

    fn render_svg_container(width : Int, height : Int, on_pointerdown : (
    Any
    ) -> Unit, on_pointermove : (
    Any
    ) -> Unit, on_pointerup : (
    Any
    ) -> Unit, on_contextmenu : (
    Any
    ) -> Unit, children : Array[
    Any
    ]) ->
    Any

    SVG コンテナを作成(イベントハンドラ付き)

    render_svg_container_with_viewport

    fn render_svg_container_with_viewport(width : Int, height : Int, viewport :
    Viewport
    , on_pointerdown : (
    Any
    ) -> Unit, on_pointermove : (
    Any
    ) -> Unit, on_pointerup : (
    Any
    ) -> Unit, on_contextmenu : (
    Any
    ) -> Unit, on_wheel : (
    Any
    ) -> Unit, children : Array[
    Any
    ]) ->
    Any

    SVG コンテナを作成(ビューポート対応)

    render_text_input

    fn render_text_input(state :
    EditorState
    , commit : (String) -> Unit, cancel : () -> Unit) ->
    DomNode

    テキスト入力オーバーレイをレンダリング

    save_to_indexeddb

    fn save_to_indexeddb(db_name : String, store_name : String, key : String, value : String, on_done : () -> Unit, on_error : () -> Unit) -> Unit

    IndexedDB に文字列を保存

    schedule_focus_with_value_ffi

    fn schedule_focus_with_value_ffi(selector : String, value : String) -> Int

    指定セレクタの要素にフォーカスを遅延設定(初期値付き)

    schedule_textarea_focus_ffi

    fn schedule_textarea_focus_ffi(selector : String, value : String) -> Int

    指定セレクタのテキストエリアにフォーカスを遅延設定(IMEコンポジション対応)

    set_multiline_text_ffi

    fn set_multiline_text_ffi(text_el :
    Any
    , content : String, x : Double, font_size : Double) -> Unit

    SVG text 要素に複数行テキストを設定(tspan使用)

    show_popover_at_button

    fn show_popover_at_button(popover_id : String, e :
    Any
    ) -> Unit

    Popover を表示してボタン位置に配置

    svg_to_dom_node

    SVG を DomNode にラップ

    update_svg_background_ffi

    fn update_svg_background_ffi(svg :
    Any
    , background : String) -> Unit

    SVG の背景スタイルを更新

    update_svg_css_vars_ffi

    fn update_svg_css_vars_ffi(svg :
    Any
    , stroke : String, fill : String, text_color : String) -> Unit

    SVG の CSS 変数を更新

    update_viewbox_ffi

    fn update_viewbox_ffi(svg :
    Any
    , viewbox : String) -> Unit

    SVG の viewBox を更新