├── CNAME ├── favicon.ico ├── assets ├── card.png ├── apple-touch-icon.png ├── icon.svg ├── prism.js ├── style.css └── canvas.js ├── .gitignore ├── Gemfile ├── .layouts ├── nav.html ├── docs.html ├── head.html ├── 404.html └── canvas.html ├── _config.yml ├── sample.canvas ├── readme.md ├── LICENSE ├── 404.md ├── docs └── apps.md ├── spec └── 1.0.md ├── Gemfile.lock └── logo.svg /CNAME: -------------------------------------------------------------------------------- 1 | jsoncanvas.org -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obsidianmd/jsoncanvas/HEAD/favicon.ico -------------------------------------------------------------------------------- /assets/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obsidianmd/jsoncanvas/HEAD/assets/card.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | .obsidian 6 | vendor 7 | .DS_Store -------------------------------------------------------------------------------- /assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obsidianmd/jsoncanvas/HEAD/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "github-pages", group: :jekyll_plugins 4 | # If you have any plugins, put them here! 5 | group :jekyll_plugins do 6 | gem "jekyll-feed", "~> 0.12" 7 | end 8 | 9 | gem "webrick", "~> 1.8" 10 | -------------------------------------------------------------------------------- /.layouts/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | name: 'JSON Canvas' 2 | title: 'JSON Canvas' 3 | url: 'https://jsoncanvas.org' 4 | description: 'An open file format for infinite canvas data.' 5 | exclude: ['.obsidian'] 6 | 7 | github: [metadata] 8 | 9 | baseurl: '' 10 | 11 | layouts_dir: .layouts 12 | includes_dir: .layouts 13 | 14 | use_html_extension: false 15 | 16 | permalink: pretty 17 | relative_permalinks: false 18 | 19 | defaults: 20 | - scope: 21 | path: "**/*" 22 | values: 23 | layout: "canvas" 24 | - scope: 25 | path: "spec/**/*.md" 26 | values: 27 | layout: "docs" 28 | - scope: 29 | path: "docs/**/*.md" 30 | values: 31 | layout: "docs" -------------------------------------------------------------------------------- /sample.canvas: -------------------------------------------------------------------------------- 1 | { 2 | "nodes":[ 3 | {"id":"754a8ef995f366bc","type":"group","x":-300,"y":-460,"width":610,"height":200,"label":"JSON Canvas"}, 4 | {"id":"8132d4d894c80022","type":"file","file":"readme.md","x":-280,"y":-200,"width":570,"height":560,"color":"6"}, 5 | {"id":"7efdbbe0c4742315","type":"file","file":"_site/logo.svg","x":-280,"y":-440,"width":217,"height":80}, 6 | {"id":"59e896bc8da20699","type":"text","text":"Learn more:\n\n- [Apps](/docs/apps.md)\n- [Spec](spec/1.0.md)\n- [Github](https://github.com/obsidianmd/jsoncanvas)","x":40,"y":-440,"width":250,"height":160}, 7 | {"id":"0ba565e7f30e0652","type":"file","file":"spec/1.0.md","x":360,"y":-400,"width":400,"height":400} 8 | ], 9 | "edges":[ 10 | {"id":"6fa11ab87f90b8af","fromNode":"7efdbbe0c4742315","fromSide":"right","toNode":"59e896bc8da20699","toSide":"left"} 11 | ] 12 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # An open file format for infinite canvas data. 2 | 3 | Infinite canvas tools are a way to view and organize information spatially, like a digital whiteboard. Infinite canvases encourage freedom and exploration, and have become a popular interface pattern across many apps. 4 | 5 | The JSON Canvas format was created to provide longevity, readability, interoperability, and extensibility to data created with infinite canvas apps. The format is designed to be easy to parse and give users [ownership over their data](https://stephango.com/file-over-app). JSON Canvas files use the `.canvas` extension. 6 | 7 | JSON Canvas was originally created for [Obsidian](https://obsidian.md/blog/json-canvas/). JSON Canvas can be implemented freely as an import, export, and storage format for any [app or tool](/docs/apps.md). This site, and all the resources associated with JSON Canvas are [open source](https://github.com/obsidianmd/jsoncanvas) under the MIT license. 8 | -------------------------------------------------------------------------------- /.layouts/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | 6 | {% include nav.html %} 7 | 8 |
9 | {{ content }} 10 |
11 | 12 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Obsidian.md 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Not found 3 | permalink: /404.html 4 | layout: 404 5 | --- 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |

404

15 | 16 |

Whoops. You've found an unknown part of this infinite canvas. Head back home.

17 |
18 | -------------------------------------------------------------------------------- /.layouts/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ site.title }} — {{ page.title }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/apps.md: -------------------------------------------------------------------------------- 1 | # Apps and tools 2 | 3 | JSON Canvas is supported by the following apps and tools. If you would like to add an app or tool to this list, please submit a pull request on [GitHub](https://github.com/obsidianmd/jsoncanvas). 4 | 5 | ## Apps 6 | 7 | | Name | Storage | Import | Export | 8 | | ----------------------------------------------- | :-----: | :----: | :----: | 9 | | [Obsidian](https://obsidian.md/) | ✓ | ✓ | ✓ | 10 | | [Kinopio](https://kinopio.club/) | | ✓ | ✓ | 11 | | [Flowchart Fun](https://flowchart.fun/) | | ✓ | ✓ | 12 | | [hi-canvas](https://hi-canvas.marknoteapp.com/) | | ✓ | ✓ | 13 | | [OrgPad](https://orgpad.info/) | | ✓ | ✓ | 14 | | [Charkoal](https://charkoal.dev/) | ✓ | ✓ | ✓ | 15 | 16 | ## Tools 17 | 18 | To convert from other formats to JSON Canvas: 19 | 20 | - [Heptabase to JSON Canvas](https://github.com/link-ding/Heptabase-Export) 21 | 22 | To convert from JSON Canvas to other formats: 23 | 24 | - [Mermaid](https://alexwiench.github.io/json-canvas-to-mermaid-demo/) 25 | - [Property Graph Exchange Format](https://www.npmjs.org/package/pgraphs) 26 | 27 | ## Libraries 28 | 29 | - [Dart library](https://pub.dev/packages/json_canvas/) 30 | - [Go library](https://github.com/supersonicpineapple/go-jsoncanvas) 31 | - [Python library](https://pypi.org/project/PyJSONCanvas/) 32 | - [React library](https://github.com/Digital-Tvilling/react-jsoncanvas) 33 | - [Ruby library](https://github.com/ongaeshi/json_canvas) 34 | - [Rust crate](https://crates.io/crates/jsoncanvas) 35 | - [TypeScript library](https://npmjs.com/package/@trbn/jsoncanvas) 36 | - [Rehype Rendering Library (inline)](https://github.com/lovettbarron/rehype-jsoncanvas) 37 | - [Vue library](https://github.com/wujieli0207/vue-json-canvas) 38 | - [TypeScript viewer library](https://github.com/Hesprs/JSON-Canvas-Viewer) 39 | -------------------------------------------------------------------------------- /.layouts/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | 6 |
7 | 8 |
9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 26 | 27 | 28 |
404
29 | {{ content }} 30 |
31 | 32 | 33 |
34 |
35 |

Learn more:

36 | 41 |
42 |
43 |
44 | 45 | 58 | 59 |
60 |
61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 | 69 |
70 | 71 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /.layouts/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | 6 |
7 | 8 |
9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
spec/1.0
24 | 29 |
30 | 31 | 32 |
readme
33 | {{ content }} 34 |
35 | 36 | 37 |
38 |
39 |

Learn more:

40 | 45 |
46 |
47 | 48 | 52 |
53 | 54 | 67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 |
75 |
76 |
77 | 78 |
79 | 80 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /spec/1.0.md: -------------------------------------------------------------------------------- 1 | # JSON Canvas Spec 2 | 3 | Version 1.0 — 2024-03-11 4 | 5 | ## Top level 6 | 7 | The top level of JSON Canvas contains two arrays: 8 | 9 | - `nodes` (optional, array of nodes) 10 | - `edges` (optional, array of edges) 11 | 12 | ## Nodes 13 | 14 | Nodes are objects within the canvas. Nodes may be text, files, links, or groups. 15 | 16 | Nodes are placed in the array in ascending order by z-index. The first node in the array should be displayed below all other nodes, and the last node in the array should be displayed on top of all other nodes. 17 | 18 | ### Generic node 19 | 20 | All nodes include the following attributes: 21 | 22 | - `id` (required, string) is a unique ID for the node. 23 | - `type` (required, string) is the node type. 24 | - `text` 25 | - `file` 26 | - `link` 27 | - `group` 28 | - `x` (required, integer) is the `x` position of the node in pixels. 29 | - `y` (required, integer) is the `y` position of the node in pixels. 30 | - `width` (required, integer) is the width of the node in pixels. 31 | - `height` (required, integer) is the height of the node in pixels. 32 | - `color` (optional, `canvasColor`) is the color of the node, see the Color section. 33 | 34 | ### Text type nodes 35 | 36 | Text type nodes store text. Along with generic node attributes, text nodes include the following attribute: 37 | 38 | - `text` (required, string) in plain text with Markdown syntax. 39 | 40 | ### File type nodes 41 | 42 | File type nodes reference other files or attachments, such as images, videos, etc. Along with generic node attributes, file nodes include the following attributes: 43 | 44 | - `file` (required, string) is the path to the file within the system. 45 | - `subpath` (optional, string) is a subpath that may link to a heading or a block. Always starts with a `#`. 46 | 47 | ### Link type nodes 48 | 49 | Link type nodes reference a URL. Along with generic node attributes, link nodes include the following attribute: 50 | 51 | - `url` (required, string) 52 | 53 | ### Group type nodes 54 | 55 | Group type nodes are used as a visual container for nodes within it. Along with generic node attributes, group nodes include the following attributes: 56 | 57 | - `label` (optional, string) is a text label for the group. 58 | - `background` (optional, string) is the path to the background image. 59 | - `backgroundStyle` (optional, string) is the rendering style of the background image. Valid values: 60 | - `cover` fills the entire width and height of the node. 61 | - `ratio` maintains the aspect ratio of the background image. 62 | - `repeat` repeats the image as a pattern in both x/y directions. 63 | 64 | ## Edges 65 | 66 | Edges are lines that connect one node to another. 67 | 68 | - `id` (required, string) is a unique ID for the edge. 69 | - `fromNode` (required, string) is the node `id` where the connection starts. 70 | - `fromSide` (optional, string) is the side where this edge starts. Valid values: 71 | - `top` 72 | - `right` 73 | - `bottom` 74 | - `left` 75 | - `fromEnd` (optional, string) is the shape of the endpoint at the edge start. Defaults to `none` if not specified. Valid values: 76 | - `none` 77 | - `arrow` 78 | - `toNode` (required, string) is the node `id` where the connection ends. 79 | - `toSide` (optional, string) is the side where this edge ends. Valid values: 80 | - `top` 81 | - `right` 82 | - `bottom` 83 | - `left` 84 | - `toEnd` (optional, string) is the shape of the endpoint at the edge end. Defaults to `arrow` if not specified. Valid values: 85 | - `none` 86 | - `arrow` 87 | - `color` (optional, `canvasColor`) is the color of the line, see the Color section. 88 | - `label` (optional, string) is a text label for the edge. 89 | 90 | 91 | ## Color 92 | 93 | The `canvasColor` type is used to encode color data for nodes and edges. Colors attributes expect a string. Colors can be specified in hex format e.g. `"#FF0000"`, or using one of the preset colors, e.g. `"1"` for red. Six preset colors exist, mapped to the following numbers: 94 | 95 | - `"1"` red 96 | - `"2"` orange 97 | - `"3"` yellow 98 | - `"4"` green 99 | - `"5"` cyan 100 | - `"6"` purple 101 | 102 | Specific values for the preset colors are intentionally not defined so that applications can tailor the presets to their specific brand colors or color scheme. 103 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (7.1.3.2) 5 | base64 6 | bigdecimal 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | connection_pool (>= 2.2.5) 9 | drb 10 | i18n (>= 1.6, < 2) 11 | minitest (>= 5.1) 12 | mutex_m 13 | tzinfo (~> 2.0) 14 | addressable (2.8.6) 15 | public_suffix (>= 2.0.2, < 6.0) 16 | base64 (0.2.0) 17 | bigdecimal (3.1.6) 18 | coffee-script (2.4.1) 19 | coffee-script-source 20 | execjs 21 | coffee-script-source (1.12.2) 22 | colorator (1.1.0) 23 | commonmarker (0.23.10) 24 | concurrent-ruby (1.2.3) 25 | connection_pool (2.4.1) 26 | dnsruby (1.70.0) 27 | simpleidn (~> 0.2.1) 28 | drb (2.2.0) 29 | ruby2_keywords 30 | em-websocket (0.5.3) 31 | eventmachine (>= 0.12.9) 32 | http_parser.rb (~> 0) 33 | ethon (0.16.0) 34 | ffi (>= 1.15.0) 35 | eventmachine (1.2.7) 36 | execjs (2.9.1) 37 | faraday (2.9.0) 38 | faraday-net_http (>= 2.0, < 3.2) 39 | faraday-net_http (3.1.0) 40 | net-http 41 | ffi (1.16.3) 42 | forwardable-extended (2.6.0) 43 | gemoji (4.1.0) 44 | github-pages (231) 45 | github-pages-health-check (= 1.18.2) 46 | jekyll (= 3.9.5) 47 | jekyll-avatar (= 0.8.0) 48 | jekyll-coffeescript (= 1.2.2) 49 | jekyll-commonmark-ghpages (= 0.4.0) 50 | jekyll-default-layout (= 0.1.5) 51 | jekyll-feed (= 0.17.0) 52 | jekyll-gist (= 1.5.0) 53 | jekyll-github-metadata (= 2.16.1) 54 | jekyll-include-cache (= 0.2.1) 55 | jekyll-mentions (= 1.6.0) 56 | jekyll-optional-front-matter (= 0.3.2) 57 | jekyll-paginate (= 1.1.0) 58 | jekyll-readme-index (= 0.3.0) 59 | jekyll-redirect-from (= 0.16.0) 60 | jekyll-relative-links (= 0.6.1) 61 | jekyll-remote-theme (= 0.4.3) 62 | jekyll-sass-converter (= 1.5.2) 63 | jekyll-seo-tag (= 2.8.0) 64 | jekyll-sitemap (= 1.4.0) 65 | jekyll-swiss (= 1.0.0) 66 | jekyll-theme-architect (= 0.2.0) 67 | jekyll-theme-cayman (= 0.2.0) 68 | jekyll-theme-dinky (= 0.2.0) 69 | jekyll-theme-hacker (= 0.2.0) 70 | jekyll-theme-leap-day (= 0.2.0) 71 | jekyll-theme-merlot (= 0.2.0) 72 | jekyll-theme-midnight (= 0.2.0) 73 | jekyll-theme-minimal (= 0.2.0) 74 | jekyll-theme-modernist (= 0.2.0) 75 | jekyll-theme-primer (= 0.6.0) 76 | jekyll-theme-slate (= 0.2.0) 77 | jekyll-theme-tactile (= 0.2.0) 78 | jekyll-theme-time-machine (= 0.2.0) 79 | jekyll-titles-from-headings (= 0.5.3) 80 | jemoji (= 0.13.0) 81 | kramdown (= 2.4.0) 82 | kramdown-parser-gfm (= 1.1.0) 83 | liquid (= 4.0.4) 84 | mercenary (~> 0.3) 85 | minima (= 2.5.1) 86 | nokogiri (>= 1.13.6, < 2.0) 87 | rouge (= 3.30.0) 88 | terminal-table (~> 1.4) 89 | github-pages-health-check (1.18.2) 90 | addressable (~> 2.3) 91 | dnsruby (~> 1.60) 92 | octokit (>= 4, < 8) 93 | public_suffix (>= 3.0, < 6.0) 94 | typhoeus (~> 1.3) 95 | html-pipeline (2.14.3) 96 | activesupport (>= 2) 97 | nokogiri (>= 1.4) 98 | http_parser.rb (0.8.0) 99 | i18n (1.14.1) 100 | concurrent-ruby (~> 1.0) 101 | jekyll (3.9.5) 102 | addressable (~> 2.4) 103 | colorator (~> 1.0) 104 | em-websocket (~> 0.5) 105 | i18n (>= 0.7, < 2) 106 | jekyll-sass-converter (~> 1.0) 107 | jekyll-watch (~> 2.0) 108 | kramdown (>= 1.17, < 3) 109 | liquid (~> 4.0) 110 | mercenary (~> 0.3.3) 111 | pathutil (~> 0.9) 112 | rouge (>= 1.7, < 4) 113 | safe_yaml (~> 1.0) 114 | jekyll-avatar (0.8.0) 115 | jekyll (>= 3.0, < 5.0) 116 | jekyll-coffeescript (1.2.2) 117 | coffee-script (~> 2.2) 118 | coffee-script-source (~> 1.12) 119 | jekyll-commonmark (1.4.0) 120 | commonmarker (~> 0.22) 121 | jekyll-commonmark-ghpages (0.4.0) 122 | commonmarker (~> 0.23.7) 123 | jekyll (~> 3.9.0) 124 | jekyll-commonmark (~> 1.4.0) 125 | rouge (>= 2.0, < 5.0) 126 | jekyll-default-layout (0.1.5) 127 | jekyll (>= 3.0, < 5.0) 128 | jekyll-feed (0.17.0) 129 | jekyll (>= 3.7, < 5.0) 130 | jekyll-gist (1.5.0) 131 | octokit (~> 4.2) 132 | jekyll-github-metadata (2.16.1) 133 | jekyll (>= 3.4, < 5.0) 134 | octokit (>= 4, < 7, != 4.4.0) 135 | jekyll-include-cache (0.2.1) 136 | jekyll (>= 3.7, < 5.0) 137 | jekyll-mentions (1.6.0) 138 | html-pipeline (~> 2.3) 139 | jekyll (>= 3.7, < 5.0) 140 | jekyll-optional-front-matter (0.3.2) 141 | jekyll (>= 3.0, < 5.0) 142 | jekyll-paginate (1.1.0) 143 | jekyll-readme-index (0.3.0) 144 | jekyll (>= 3.0, < 5.0) 145 | jekyll-redirect-from (0.16.0) 146 | jekyll (>= 3.3, < 5.0) 147 | jekyll-relative-links (0.6.1) 148 | jekyll (>= 3.3, < 5.0) 149 | jekyll-remote-theme (0.4.3) 150 | addressable (~> 2.0) 151 | jekyll (>= 3.5, < 5.0) 152 | jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) 153 | rubyzip (>= 1.3.0, < 3.0) 154 | jekyll-sass-converter (1.5.2) 155 | sass (~> 3.4) 156 | jekyll-seo-tag (2.8.0) 157 | jekyll (>= 3.8, < 5.0) 158 | jekyll-sitemap (1.4.0) 159 | jekyll (>= 3.7, < 5.0) 160 | jekyll-swiss (1.0.0) 161 | jekyll-theme-architect (0.2.0) 162 | jekyll (> 3.5, < 5.0) 163 | jekyll-seo-tag (~> 2.0) 164 | jekyll-theme-cayman (0.2.0) 165 | jekyll (> 3.5, < 5.0) 166 | jekyll-seo-tag (~> 2.0) 167 | jekyll-theme-dinky (0.2.0) 168 | jekyll (> 3.5, < 5.0) 169 | jekyll-seo-tag (~> 2.0) 170 | jekyll-theme-hacker (0.2.0) 171 | jekyll (> 3.5, < 5.0) 172 | jekyll-seo-tag (~> 2.0) 173 | jekyll-theme-leap-day (0.2.0) 174 | jekyll (> 3.5, < 5.0) 175 | jekyll-seo-tag (~> 2.0) 176 | jekyll-theme-merlot (0.2.0) 177 | jekyll (> 3.5, < 5.0) 178 | jekyll-seo-tag (~> 2.0) 179 | jekyll-theme-midnight (0.2.0) 180 | jekyll (> 3.5, < 5.0) 181 | jekyll-seo-tag (~> 2.0) 182 | jekyll-theme-minimal (0.2.0) 183 | jekyll (> 3.5, < 5.0) 184 | jekyll-seo-tag (~> 2.0) 185 | jekyll-theme-modernist (0.2.0) 186 | jekyll (> 3.5, < 5.0) 187 | jekyll-seo-tag (~> 2.0) 188 | jekyll-theme-primer (0.6.0) 189 | jekyll (> 3.5, < 5.0) 190 | jekyll-github-metadata (~> 2.9) 191 | jekyll-seo-tag (~> 2.0) 192 | jekyll-theme-slate (0.2.0) 193 | jekyll (> 3.5, < 5.0) 194 | jekyll-seo-tag (~> 2.0) 195 | jekyll-theme-tactile (0.2.0) 196 | jekyll (> 3.5, < 5.0) 197 | jekyll-seo-tag (~> 2.0) 198 | jekyll-theme-time-machine (0.2.0) 199 | jekyll (> 3.5, < 5.0) 200 | jekyll-seo-tag (~> 2.0) 201 | jekyll-titles-from-headings (0.5.3) 202 | jekyll (>= 3.3, < 5.0) 203 | jekyll-watch (2.2.1) 204 | listen (~> 3.0) 205 | jemoji (0.13.0) 206 | gemoji (>= 3, < 5) 207 | html-pipeline (~> 2.2) 208 | jekyll (>= 3.0, < 5.0) 209 | kramdown (2.4.0) 210 | rexml 211 | kramdown-parser-gfm (1.1.0) 212 | kramdown (~> 2.0) 213 | liquid (4.0.4) 214 | listen (3.8.0) 215 | rb-fsevent (~> 0.10, >= 0.10.3) 216 | rb-inotify (~> 0.9, >= 0.9.10) 217 | mercenary (0.3.6) 218 | mini_portile2 (2.8.5) 219 | minima (2.5.1) 220 | jekyll (>= 3.5, < 5.0) 221 | jekyll-feed (~> 0.9) 222 | jekyll-seo-tag (~> 2.1) 223 | minitest (5.22.2) 224 | mutex_m (0.2.0) 225 | net-http (0.4.1) 226 | uri 227 | nokogiri (1.16.2) 228 | mini_portile2 (~> 2.8.2) 229 | racc (~> 1.4) 230 | nokogiri (1.16.2-aarch64-linux) 231 | racc (~> 1.4) 232 | nokogiri (1.16.2-arm-linux) 233 | racc (~> 1.4) 234 | nokogiri (1.16.2-arm64-darwin) 235 | racc (~> 1.4) 236 | nokogiri (1.16.2-x86-linux) 237 | racc (~> 1.4) 238 | nokogiri (1.16.2-x86_64-darwin) 239 | racc (~> 1.4) 240 | nokogiri (1.16.2-x86_64-linux) 241 | racc (~> 1.4) 242 | octokit (4.25.1) 243 | faraday (>= 1, < 3) 244 | sawyer (~> 0.9) 245 | pathutil (0.16.2) 246 | forwardable-extended (~> 2.6) 247 | public_suffix (5.0.4) 248 | racc (1.7.3) 249 | rb-fsevent (0.11.2) 250 | rb-inotify (0.10.1) 251 | ffi (~> 1.0) 252 | rexml (3.2.6) 253 | rouge (3.30.0) 254 | ruby2_keywords (0.0.5) 255 | rubyzip (2.3.2) 256 | safe_yaml (1.0.5) 257 | sass (3.7.4) 258 | sass-listen (~> 4.0.0) 259 | sass-listen (4.0.0) 260 | rb-fsevent (~> 0.9, >= 0.9.4) 261 | rb-inotify (~> 0.9, >= 0.9.7) 262 | sawyer (0.9.2) 263 | addressable (>= 2.3.5) 264 | faraday (>= 0.17.3, < 3) 265 | simpleidn (0.2.1) 266 | unf (~> 0.1.4) 267 | terminal-table (1.8.0) 268 | unicode-display_width (~> 1.1, >= 1.1.1) 269 | typhoeus (1.4.1) 270 | ethon (>= 0.9.0) 271 | tzinfo (2.0.6) 272 | concurrent-ruby (~> 1.0) 273 | unf (0.1.4) 274 | unf_ext 275 | unf_ext (0.0.9.1) 276 | unicode-display_width (1.8.0) 277 | uri (0.13.0) 278 | webrick (1.8.1) 279 | 280 | PLATFORMS 281 | aarch64-linux 282 | aarch64-linux-android 283 | aarch64-linux-musl 284 | arm-linux 285 | arm-linux-androideabi 286 | arm-linux-musleabihf 287 | arm64-darwin 288 | x86-linux 289 | x86-linux-android 290 | x86-linux-musl 291 | x86_64-darwin 292 | x86_64-linux 293 | x86_64-linux-android 294 | x86_64-linux-musl 295 | 296 | DEPENDENCIES 297 | github-pages 298 | jekyll-feed (~> 0.12) 299 | webrick (~> 1.8) 300 | 301 | BUNDLED WITH 302 | 2.5.6 303 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism&languages=clike+javascript+json */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 5 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; 6 | Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; 7 | -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-content: -apple-system, BlinkMacSystemFont, "Inter", "IBM Plex Sans", Segoe UI, Helvetica, Arial, sans-serif; 3 | --font-mono: ui-monospace, SFMono-Regular, "Cascadia Code", "Roboto Mono", "Source Code Pro", "DejaVu Sans Mono", "Liberation Mono", "Consolas", Menlo, Monaco, monospace; 4 | --font-small: 0.875em; 5 | --font-smaller: 0.8em; 6 | --wrap-wide: 1000px; 7 | --wrap-normal: 37em; 8 | --line-height: 1.5; 9 | 10 | --color-bg-1: #fff; 11 | --color-bg-2: #fafafa; 12 | --color-tx-1: #3F062D; 13 | --color-tx-2: #999; 14 | --color-ui-1: #ddd; 15 | --color-ui-2: #bbb; 16 | --color-ui-3: #5E0641; 17 | --color-ax-1: #8B0A5F; 18 | 19 | --color-selection: rgba(139,10,95,0.15); 20 | } 21 | 22 | .theme-dark { 23 | --color-bg-1: #1e0516; 24 | --color-bg-2: #140310; 25 | --color-tx-1: #fff; 26 | --color-tx-2: #a28397; 27 | --color-ui-1: #3F062D; 28 | --color-ui-2: #68154C; 29 | --color-ui-3: #b40e7a; 30 | --color-ax-1: #8B0A5F; 31 | 32 | --color-selection: rgba(139,10,95,0.5); 33 | } 34 | 35 | *, *:before, *:after { 36 | box-sizing:inherit;} 37 | 38 | ::selection { 39 | background: var(--color-selection); 40 | } 41 | 42 | ::-moz-selection { 43 | background: var(--color-selection); 44 | } 45 | 46 | html, body { 47 | touch-action: manipulation; 48 | } 49 | 50 | html { 51 | box-sizing: border-box; 52 | width: 100%; 53 | height: 100%; 54 | font-size: 62.5%; 55 | } 56 | 57 | body { 58 | color-scheme: light dark; 59 | -webkit-font-smoothing: antialiased; 60 | text-rendering: optimizeLegibility; 61 | background-color: var(--color-bg-1); 62 | font-family: var(--font-content); 63 | margin: 0 auto 0 auto; 64 | line-height: var(--line-height); 65 | padding: 0; 66 | font-size: 1.6rem; 67 | color: var(--color-tx-1); 68 | } 69 | 70 | /* Canvas area */ 71 | #home { 72 | overflow: hidden; 73 | } 74 | #container { 75 | touch-action: none; 76 | display: flex; 77 | flex: 1; 78 | flex-direction: row; 79 | height: 100%; 80 | width: 100%; 81 | position: fixed; 82 | top: 0; 83 | left: 0; 84 | } 85 | #canvas-container { 86 | width: 100%; 87 | height: 100%; 88 | padding: 0; 89 | position: relative; 90 | background-color: var(--color-bg-2); 91 | background-image: radial-gradient(var(--color-ui-1) calc(var(--scale)*0.5px + 0.5px), transparent 0); 92 | background-size: calc(var(--scale) * 20px) calc(var(--scale) * 20px); 93 | overflow: hidden; 94 | background-position: calc(var(--pan-x) - 19px) calc(var(--pan-y) - 19px); 95 | } 96 | #canvas-edges, 97 | #canvas-nodes { 98 | opacity: 0; 99 | transform: translate(var(--pan-x), var(--pan-y)) scale(var(--scale)); 100 | transform-origin: left top; 101 | } 102 | #canvas-edges { 103 | z-index: 150; 104 | pointer-events: none; 105 | user-select: none; 106 | overflow:visible; 107 | position: absolute; 108 | top: 0; 109 | left: 0; 110 | width: 100%; 111 | height: 100%; 112 | } 113 | #canvas-edges path { 114 | stroke: var(--color-ui-3); 115 | stroke-width: 2; 116 | fill: none; 117 | } 118 | #arrowhead { 119 | fill: var(--color-ui-3); 120 | } 121 | .will-pan { 122 | cursor: grab; 123 | } 124 | 125 | /* Canvas output pane */ 126 | #output.hidden { 127 | transform: translateX(120%); 128 | } 129 | #output { 130 | position: fixed; 131 | height: 92vh; 132 | top: 1rem; 133 | right: 1rem; 134 | bottom: auto; 135 | border-radius: 12px; 136 | color: var(--color-tx-1); 137 | background-color: var(--color-bg-1); 138 | border: 1px solid var(--color-ui-1); 139 | box-shadow: 0 5px 15px rgba(0,0,0,0.2);; 140 | z-index: 0; 141 | width: 24em; 142 | max-width: 40%; 143 | display: flex; 144 | flex-direction: column; 145 | transition: transform 200ms; 146 | } 147 | #output p { 148 | font-size: 90%; 149 | line-height: 1.3; 150 | padding-right: 0.5em; 151 | } 152 | #output-code { 153 | color-scheme: dark; 154 | flex-grow: 1; 155 | width: 100%; 156 | overflow: auto; 157 | -webkit-overflow-scrolling: touch; 158 | padding: 1rem; 159 | border-top: 1px solid var(--color-ui-1); 160 | border-bottom: 1px solid var(--color-ui-1); 161 | } 162 | #output pre { 163 | color-scheme: dark; 164 | width: 100%; 165 | padding: 0.5em; 166 | margin: 0; 167 | } 168 | .code-footer, 169 | .code-header { 170 | font-size: 80%; 171 | font-weight: 500; 172 | padding: 0; 173 | display: flex; 174 | align-items: center; 175 | color: var(--color-tx-2); 176 | gap: 8px; 177 | padding: 1rem; 178 | } 179 | .code-footer { 180 | justify-content: center; 181 | } 182 | .code-header .language { 183 | flex-grow: 1; 184 | } 185 | .close-output { 186 | font-weight: 300; 187 | cursor: pointer; 188 | user-select: none; 189 | -ms-user-select: none; 190 | -webkit-user-select: none; 191 | font-size: 24px; 192 | line-height: 0; 193 | display: flex; 194 | align-items: center; 195 | margin-top: -4px; 196 | } 197 | .close-output:hover { 198 | color: var(--color-tx-1); 199 | } 200 | 201 | /* Pages */ 202 | .page { 203 | padding: 36px 36px 48px; 204 | max-width: 48em; 205 | margin: 0 auto; 206 | } 207 | nav { 208 | padding: 24px 36px; 209 | max-width: 48em; 210 | margin: 0 auto; 211 | display: flex; 212 | align-items: center; 213 | gap: 4px; 214 | } 215 | nav #logo { 216 | flex-grow: 1; 217 | } 218 | nav .link { 219 | color: var(--color-ax-1); 220 | text-decoration: none; 221 | padding: 0.25em 0.5em; 222 | border-radius: 6px; 223 | } 224 | nav .link:hover { 225 | color: var(--color-bg-1); 226 | background-color: var(--color-ax-1); 227 | } 228 | .hidenav #navbar { 229 | display: none; 230 | } 231 | 232 | /* Specific nodes */ 233 | #logo { 234 | border-radius: 8px; 235 | line-height: 0; 236 | z-index: 100; 237 | padding: 4px 12px 4px 4px; 238 | } 239 | #logo .node-name { 240 | top: -1.25em; 241 | padding-left: 4px; 242 | } 243 | #nav { 244 | z-index: 90; 245 | white-space: nowrap; 246 | padding-right: 48px; 247 | } 248 | #readme { 249 | width: 480px; 250 | padding: 36px; 251 | z-index: 80; 252 | } 253 | #spec { 254 | width: 480px; 255 | height: 480px; 256 | z-index: 70; 257 | } 258 | 259 | 260 | /* General node styling */ 261 | .node { 262 | -webkit-tap-highlight-color: rgba(0,0,0,0); 263 | position: absolute; 264 | display: block; 265 | } 266 | .node.is-active { 267 | box-shadow: 268 | 0 0 0 2px var(--color-ui-3); 269 | } 270 | .node.is-dragging { 271 | cursor: grabbing; 272 | box-shadow: 273 | 0 0 0 2px var(--color-ui-3), 274 | 0 5px 15px rgba(0,0,0,0.2); 275 | } 276 | .node.is-dragging iframe { 277 | pointer-events: none; 278 | } 279 | .node:hover .node-name { 280 | opacity: 1; 281 | color: var(--color-tx-1); 282 | border-radius: 8px 8px 0 0; 283 | } 284 | .node-name { 285 | -webkit-tap-highlight-color: rgba(0,0,0,0); 286 | cursor: grab; 287 | opacity: 1; 288 | position: absolute; 289 | height: 2.25em; 290 | padding: 0.25em 0.5em; 291 | width: 100%; 292 | top: -2.25em; 293 | left: 0; 294 | color: var(--color-ui-2); 295 | font-size: calc(var(--font-smaller) * 1/var(--scale)); 296 | -ms-user-select: none; 297 | -webkit-user-select: none; 298 | user-select: none; 299 | } 300 | .node.is-dragging .node-name { 301 | cursor: grabbing; 302 | } 303 | .node-link, 304 | .node-text { 305 | background-color: var(--color-bg-1); 306 | border-radius: 8px; 307 | box-shadow: 0 0 0 2px var(--color-ui-1); 308 | } 309 | .node-file img { 310 | -webkit-user-drag: none; 311 | -moz-user-drag: none; 312 | -o-user-drag: none; 313 | user-drag: none; 314 | } 315 | .node-text-content { 316 | padding: 12px 24px; 317 | } 318 | 319 | /* Canvas controls */ 320 | #controls { 321 | position: fixed; 322 | bottom: 1rem; 323 | right: 1rem; 324 | z-index: 100; 325 | display: flex; 326 | align-items: center; 327 | gap: 6px; 328 | -ms-user-select: none; 329 | -webkit-user-select: none; 330 | user-select: none; 331 | } 332 | 333 | /* Page content */ 334 | h1 { 335 | line-height: 1.1; 336 | margin-top: 0.25em; 337 | } 338 | h2 { 339 | line-height: 1.2; 340 | margin-bottom: 0em; 341 | margin-top: 1.5em; 342 | } 343 | h2 + p { 344 | margin-top: 0.5em; 345 | } 346 | 347 | ul + h2, 348 | ul + h3, 349 | p + h2, 350 | p + h3 { 351 | margin-top: 1.5em; 352 | } 353 | 354 | h2 + h3 { 355 | margin-top: 0.75em; 356 | } 357 | 358 | a { 359 | font-weight: 600; 360 | color: var(--color-tx-1); 361 | text-decoration: underline; 362 | } 363 | small { 364 | color: var(--color-tx-2); 365 | } 366 | small a { 367 | font-weight: 400; 368 | color: var(--color-tx-2); 369 | } 370 | hr { 371 | margin: 0; 372 | border: 0; 373 | height: 1px; 374 | background-color: var(--color-ui-1); 375 | } 376 | iframe { 377 | -webkit-appearance: none; 378 | border: none; 379 | outline: none; 380 | margin: 0; 381 | vertical-align: bottom; 382 | border-radius: 8px; 383 | } 384 | img { 385 | vertical-align: bottom; 386 | } 387 | code { 388 | -webkit-appearance: none; 389 | font-family: var(--font-mono); 390 | cursor: text; 391 | } 392 | pre { 393 | -webkit-appearance: none; 394 | font-family: var(--font-mono); 395 | background-color: transparent; 396 | border-radius: 4px; 397 | padding: 0; 398 | font-size: 85%; 399 | cursor: text; 400 | } 401 | pre:active, 402 | pre:focus { 403 | outline: none; 404 | border: none; 405 | } 406 | pre code { 407 | color: var(--color-tx-2); 408 | background-color: transparent; 409 | border: none; 410 | padding: 0; 411 | font-size: inherit; 412 | } 413 | code { 414 | font-family: var(--font-mono); 415 | background-color: var(--color-bg-2); 416 | border: 1px solid var(--color-ui-1); 417 | border-radius: 4px; 418 | padding: 0 0.2em; 419 | font-size: 85%; 420 | } 421 | ul { 422 | padding-inline-start: 2em; 423 | } 424 | li::marker { 425 | color: var(--color-tx-2); 426 | } 427 | 428 | table { 429 | margin-top: 1.5em; 430 | margin-bottom: 2.5em; 431 | border-collapse:collapse; 432 | border-spacing:0; 433 | } 434 | tr { 435 | border-bottom: 1px solid var(--color-ui-1); 436 | } 437 | td { 438 | padding: 0.5em 1em 0.5em 0; 439 | line-height: 1.3; 440 | } 441 | th:not(:last-child) { 442 | padding-right: 1em; 443 | } 444 | td:last-child { 445 | padding-right: 0; 446 | } 447 | th { 448 | text-align: left; 449 | font-weight: 600; 450 | padding: 0 1em 0.5em 0; 451 | } 452 | 453 | button { 454 | -webkit-tap-highlight-color: rgba(0,0,0,0); 455 | -ms-user-select: none; 456 | -webkit-user-select: none; 457 | user-select: none; 458 | cursor: pointer; 459 | font-family: var(--font-content); 460 | background: var(--color-bg-1); 461 | outline: none; 462 | border: 1px solid var(--color-ui-1); 463 | padding: 4px 8px; 464 | color: var(--color-tx-1); 465 | border-radius: 4px; 466 | font-weight: 500; 467 | } 468 | button:hover { 469 | border-color: var(--color-ui-2); 470 | } 471 | .theme-dark button { 472 | background-color: var(--color-ui-1); 473 | color: var(--color-tx-2); 474 | border: 1px solid var(--color-ui-2); 475 | } 476 | .theme-dark button:hover { 477 | color: var(--color-tx-1); 478 | border: 1px solid var(--color-ui-3); 479 | } 480 | 481 | @media (max-width: 800px) { 482 | body:not(.hidenav) nav { 483 | padding: 24px; 484 | gap: 0; 485 | } 486 | body:not(.hidenav) .page { 487 | padding: 24px 24px 48px 24px; 488 | } 489 | #controls { 490 | bottom: 0; 491 | right: 0; 492 | left: 0; 493 | padding: 1rem; 494 | border-top: 1px solid var(--color-ui-1); 495 | width: 100%; 496 | background-color: var(--color-bg-1); 497 | justify-content: center; 498 | height: 48px; 499 | } 500 | #output { 501 | border-radius: 0; 502 | border: none; 503 | left: 0; 504 | top: 0; 505 | z-index: 200; 506 | width: 100vw; 507 | height: calc(100% - 48px); 508 | transition: none; 509 | max-width: 100vw; 510 | box-shadow: none; 511 | } 512 | #output-code { 513 | padding: 1rem 1rem 6rem; 514 | } 515 | .code-footer { 516 | 517 | } 518 | } 519 | 520 | /* PrismJS 1.29.0 521 | https://prismjs.com/download.html#themes=prism&languages=json */ 522 | /** 523 | * prism.js default theme for JavaScript, CSS and HTML 524 | * Based on dabblet (http://dabblet.com) 525 | * @author Lea Verou 526 | */ 527 | 528 | code[class*="language-"], 529 | pre[class*="language-"] { 530 | text-align: left; 531 | white-space: pre-wrap; 532 | word-spacing: normal; 533 | word-break: normal; 534 | word-wrap: normal; 535 | 536 | -moz-tab-size: 4; 537 | -o-tab-size: 4; 538 | tab-size: 4; 539 | 540 | -webkit-hyphens: none; 541 | -moz-hyphens: none; 542 | -ms-hyphens: none; 543 | hyphens: none; 544 | } 545 | 546 | /* Code blocks */ 547 | pre[class*="language-"] { 548 | overflow: auto; 549 | overflow-x: hidden; 550 | } 551 | 552 | /* Inline code */ 553 | :not(pre) > code[class*="language-"] { 554 | white-space: normal; 555 | } 556 | 557 | .token.comment, 558 | .token.prolog, 559 | .token.doctype, 560 | .token.cdata { 561 | color: slategray; 562 | } 563 | 564 | .token.punctuation { 565 | color: var(--color-tx-2); 566 | } 567 | 568 | .token.namespace { 569 | opacity: .7; 570 | } 571 | 572 | .token.property, 573 | .token.tag, 574 | .token.boolean, 575 | .token.constant, 576 | .token.symbol, 577 | .token.deleted { 578 | color: #f8aa59; 579 | } 580 | 581 | .token.number { 582 | color: #ee529d; 583 | } 584 | 585 | .token.selector, 586 | .token.attr-name, 587 | .token.string, 588 | .token.char, 589 | .token.builtin, 590 | .token.inserted { 591 | color: #fe7568; 592 | } 593 | 594 | .token.operator, 595 | .token.entity, 596 | .token.url, 597 | .language-css .token.string, 598 | .style .token.string { 599 | color: var(--color-tx-2); 600 | } 601 | 602 | .token.atrule, 603 | .token.attr-value, 604 | .token.keyword { 605 | color: #07a; 606 | } 607 | 608 | .token.function, 609 | .token.class-name { 610 | color: #DD4A68; 611 | } 612 | 613 | .token.regex, 614 | .token.important, 615 | .token.variable { 616 | color: #e90; 617 | } 618 | 619 | .token.important, 620 | .token.bold { 621 | font-weight: bold; 622 | } 623 | .token.italic { 624 | font-style: italic; 625 | } 626 | 627 | .token.entity { 628 | cursor: help; 629 | } 630 | 631 | 632 | -------------------------------------------------------------------------------- /assets/canvas.js: -------------------------------------------------------------------------------- 1 | // Initial state of the canvas 2 | let scale, panOffsetX, panOffsetY; 3 | 4 | const ZOOM_SPEED = 0.1; 5 | const minScale = 0.35; 6 | const maxScale = 1.25; 7 | const container = document.getElementById('canvas-nodes'); 8 | 9 | let isDragging = false; 10 | let isSpacePressed = false; 11 | let isPanning = false; 12 | 13 | let startX = 0; 14 | let startY = 0; 15 | let lastTouchX = 0; 16 | let lastTouchY = 0; 17 | let touchStartPanX = 0; 18 | let touchStartPanY = 0; 19 | 20 | function adjustCanvasToViewport() { 21 | const nodes = document.querySelectorAll('.node'); 22 | let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; 23 | 24 | nodes.forEach(node => { 25 | const x = parseInt(node.style.left, 10); 26 | const y = parseInt(node.style.top, 10); 27 | const width = node.offsetWidth; 28 | const height = node.offsetHeight; 29 | 30 | minX = Math.min(minX, x); 31 | maxX = Math.max(maxX, x + width); 32 | minY = Math.min(minY, y); 33 | maxY = Math.max(maxY, y + height); 34 | }); 35 | 36 | const boundingBoxWidth = maxX - minX; 37 | const boundingBoxHeight = maxY - minY; 38 | const viewportWidth = window.innerWidth; 39 | const viewportHeight = window.innerHeight; 40 | 41 | const scaleX = viewportWidth / (boundingBoxWidth + 80); 42 | const scaleY = viewportHeight / (boundingBoxHeight + 80); 43 | scale = Math.min(scaleX, scaleY, 1); // Ensure the scale is not more than 1 44 | 45 | panOffsetX = (viewportWidth - boundingBoxWidth * scale) / 2 - minX * scale; 46 | panOffsetY = (viewportHeight - boundingBoxHeight * scale) / 2 - minY * scale; 47 | 48 | // Apply the calculated scale and pan offsets 49 | applyPanAndZoom(); 50 | 51 | document.getElementById('canvas-nodes').style.opacity = 1; 52 | document.getElementById('canvas-edges').style.opacity = 1; 53 | } 54 | 55 | document.addEventListener('DOMContentLoaded', adjustCanvasToViewport); 56 | 57 | // Zoom 58 | window.addEventListener('wheel', (e) => { 59 | if (e.ctrlKey || e.metaKey) { 60 | if (e.deltaY > 0) { 61 | scale = Math.max(scale - ZOOM_SPEED, minScale); 62 | } else { 63 | scale = Math.min(scale + ZOOM_SPEED, maxScale); 64 | } 65 | 66 | document.body.style.setProperty('--scale', scale); 67 | e.preventDefault(); 68 | } 69 | }, {passive: false}); 70 | 71 | // Buttons 72 | document.getElementById('zoom-in').addEventListener('click', function() { 73 | scale = Math.min(scale + ZOOM_SPEED, maxScale); 74 | document.body.style.setProperty('--scale', scale); 75 | }); 76 | 77 | document.getElementById('zoom-out').addEventListener('click', function() { 78 | scale = Math.max(scale - ZOOM_SPEED, minScale); 79 | document.body.style.setProperty('--scale', scale); 80 | }); 81 | 82 | document.getElementById('zoom-reset').addEventListener('click', function() { 83 | adjustCanvasToViewport(); 84 | }); 85 | 86 | document.getElementById('toggle-output').addEventListener('click', function() { 87 | const output = document.getElementById('output'); 88 | output.classList.toggle('hidden'); 89 | }); 90 | 91 | document.querySelector('.close-output').addEventListener('click', function() { 92 | const output = document.getElementById('output'); 93 | output.classList.toggle('hidden'); 94 | }); 95 | 96 | document.querySelector('.button-copy').addEventListener('click', function() { 97 | const positionsOutput = document.getElementById('positionsOutput').textContent; 98 | navigator.clipboard.writeText(positionsOutput).catch(err => { 99 | console.error('Error copying canvas data: ', err); 100 | }); 101 | }); 102 | 103 | document.querySelector('.button-download').addEventListener('click', function() { 104 | const positionsOutput = document.getElementById('positionsOutput').textContent; 105 | const blob = new Blob([positionsOutput], { type: 'text/plain' }); 106 | const url = URL.createObjectURL(blob); 107 | const a = document.createElement('a'); 108 | a.href = url; 109 | a.download = 'sample.canvas'; 110 | document.body.appendChild(a); 111 | a.click(); 112 | document.body.removeChild(a); 113 | URL.revokeObjectURL(url); 114 | }); 115 | 116 | // Very simplified Markdown conversion 117 | function htmlToMarkdown(html) { 118 | let markdown = html.replace(//gi, "\n"); 119 | markdown = markdown.replace(/([^<]+)<\/a>/gi, "[$2]($1)"); 120 | markdown = markdown.replace(/
    /gi, "\n\n").replace(/<\/ul>/gi, "\n\n").replace(/
  • /gi, "- ").replace(/<\/li>/gi, "\n"); 121 | markdown = markdown.replace(/<[^>]+>/g, ''); 122 | markdown = markdown.replace(/\n\s*-\s+/g, "\n- "); 123 | markdown = markdown.trim().replace(/\n{3,}/g, "\n\n"); 124 | return markdown; 125 | } 126 | 127 | document.addEventListener('DOMContentLoaded', function() { 128 | const links = document.querySelectorAll('a'); 129 | links.forEach(link => { 130 | const url = new URL(link.href); 131 | if (url.hostname !== window.location.hostname) { 132 | link.target = '_blank'; 133 | link.rel = 'noopener noreferrer'; 134 | } 135 | }); 136 | }); 137 | 138 | function prepareForSerialization() { 139 | document.querySelectorAll('a').forEach(link => { 140 | if (link.hasAttribute('target') && link.target === '_blank') { 141 | link.removeAttribute('target'); 142 | link.removeAttribute('rel'); 143 | } 144 | }); 145 | } 146 | 147 | // Serialize canvas data 148 | function updateCanvasData() { 149 | prepareForSerialization(); 150 | const nodes = Array.from(document.querySelectorAll('.node')).map(node => { 151 | const nodeObject = { 152 | id: node.id, 153 | type: node.getAttribute('data-node-type'), 154 | x: parseInt(node.style.left, 10), 155 | y: parseInt(node.style.top, 10), 156 | width: node.offsetWidth, 157 | height: node.offsetHeight, 158 | }; 159 | 160 | const fileAttribute = node.getAttribute('data-node-file'); 161 | if (fileAttribute) { 162 | nodeObject.file = fileAttribute; 163 | } 164 | 165 | if (nodeObject.type === 'text') { 166 | const textContent = node.querySelector('.node-text-content').innerHTML; 167 | nodeObject.text = htmlToMarkdown(textContent); 168 | } 169 | 170 | 171 | return nodeObject; 172 | }); 173 | 174 | const canvasData = { 175 | nodes: nodes, 176 | edges: edges, 177 | }; 178 | 179 | const positionsOutput = document.getElementById('positionsOutput'); 180 | positionsOutput.textContent = JSON.stringify(canvasData, null, 2); 181 | 182 | Prism.highlightElement(positionsOutput); 183 | } 184 | 185 | function getAnchorPoint(node, side) { 186 | const x = parseInt(node.style.left, 10); 187 | const y = parseInt(node.style.top, 10); 188 | const width = node.offsetWidth; 189 | const height = node.offsetHeight; 190 | 191 | switch (side) { 192 | case 'top': 193 | return { x: x + width / 2, y: y }; 194 | case 'right': 195 | return { x: x + width, y: y + height / 2 }; 196 | case 'bottom': 197 | return { x: x + width / 2, y: y + height }; 198 | case 'left': 199 | return { x: x, y: y + height / 2 }; 200 | default: // center or unspecified case 201 | return { x: x + width / 2, y: y + height / 2 }; 202 | } 203 | } 204 | 205 | function drawEdges() { 206 | const svgContainer = document.getElementById('edge-paths'); 207 | svgContainer.innerHTML = ''; // Clear existing edges for redraw 208 | 209 | edges.forEach(edge => { 210 | const fromNode = document.getElementById(edge.fromNode); 211 | const toNode = document.getElementById(edge.toNode); 212 | 213 | if (fromNode && toNode) { 214 | const fromPoint = getAnchorPoint(fromNode, edge.fromSide); 215 | const toPoint = getAnchorPoint(toNode, edge.toSide); 216 | 217 | const curveTightness = 0.75; 218 | const controlPointX1 = fromPoint.x + (toPoint.x - fromPoint.x) * curveTightness; 219 | const controlPointX2 = fromPoint.x + (toPoint.x - fromPoint.x) * (1 - curveTightness); 220 | const controlPointY1 = fromPoint.y; 221 | const controlPointY2 = toPoint.y; 222 | 223 | const d = `M ${fromPoint.x} ${fromPoint.y} C ${controlPointX1} ${controlPointY1}, ${controlPointX2} ${controlPointY2}, ${toPoint.x} ${toPoint.y}`; 224 | 225 | const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 226 | path.setAttribute('d', d); 227 | path.setAttribute('stroke', 'black'); 228 | path.setAttribute('fill', 'none'); 229 | if (edge.toEnd === 'arrow') { 230 | path.setAttribute('marker-end', 'url(#arrowhead)'); 231 | } 232 | 233 | svgContainer.appendChild(path); 234 | } 235 | }); 236 | } 237 | 238 | // Drag nodes 239 | document.querySelectorAll('.node .node-name').forEach(nodeName => { 240 | nodeName.addEventListener('mousedown', function(e) { 241 | if (isSpacePressed) return; 242 | 243 | isDragging = true; 244 | startX = e.clientX; 245 | startY = e.clientY; 246 | selectedElement = this.parentElement; 247 | selectedElement.classList.add('is-dragging'); 248 | }); 249 | }); 250 | 251 | window.addEventListener('mousemove', function(e) { 252 | if (!isDragging || !selectedElement) return; 253 | 254 | const dx = (e.clientX - startX) / scale; 255 | const dy = (e.clientY - startY) / scale; 256 | 257 | selectedElement.style.left = `${parseInt(selectedElement.style.left, 10) + dx}px`; 258 | selectedElement.style.top = `${parseInt(selectedElement.style.top, 10) + dy}px`; 259 | 260 | startX = e.clientX; 261 | startY = e.clientY; 262 | 263 | drawEdges(); 264 | }); 265 | 266 | window.addEventListener('mouseup', function() { 267 | if (isDragging && selectedElement) { 268 | selectedElement.classList.remove('is-dragging'); 269 | isDragging = false; 270 | selectedElement = null; 271 | updateCanvasData(); 272 | drawEdges(); 273 | } 274 | }); 275 | 276 | // Panning 277 | window.addEventListener('keydown', function(e) { 278 | if (e.code === 'Space') { 279 | e.preventDefault(); 280 | isSpacePressed = true; 281 | document.body.classList.add('will-pan'); 282 | } 283 | }); 284 | 285 | window.addEventListener('keyup', function(e) { 286 | if (e.code === 'Space') { 287 | isSpacePressed = false; 288 | document.body.classList.remove('will-pan'); 289 | } 290 | }); 291 | 292 | window.addEventListener('mousedown', function(e) { 293 | if (isSpacePressed && !isDragging) { 294 | isPanning = true; 295 | document.body.style.cursor = 'grabbing'; 296 | panStartX = e.clientX - panOffsetX; 297 | panStartY = e.clientY - panOffsetY; 298 | } 299 | }); 300 | 301 | window.addEventListener('mousemove', function(e) { 302 | if (isPanning) { 303 | panOffsetX = e.clientX - panStartX; 304 | panOffsetY = e.clientY - panStartY; 305 | 306 | document.body.style.setProperty('--pan-x', `${panOffsetX}px`); 307 | document.body.style.setProperty('--pan-y', `${panOffsetY}px`); 308 | } 309 | }); 310 | 311 | window.addEventListener('mouseup', function() { 312 | if (isPanning) { 313 | isPanning = false; 314 | document.body.style.cursor = ''; 315 | } 316 | }); 317 | 318 | // Touch-based devices 319 | let initialDistance = null; 320 | 321 | document.addEventListener('gesturestart', function(e){ e.preventDefault(); }); 322 | 323 | document.getElementById('canvas-container').addEventListener('touchstart', function(e) { 324 | if (e.touches.length === 1) { // Single touch for panning 325 | isPanning = true; 326 | const touch = e.touches[0]; 327 | touchStartPanX = touch.pageX - panOffsetX; 328 | touchStartPanY = touch.pageY - panOffsetY; 329 | lastTouchX = touch.pageX; 330 | lastTouchY = touch.pageY; 331 | } else if (e.touches.length === 2) { // Two-finger touch for zooming 332 | e.preventDefault(); // Prevent page zoom 333 | const touch1 = e.touches[0]; 334 | const touch2 = e.touches[1]; 335 | initialDistance = Math.sqrt((touch2.pageX - touch1.pageX) ** 2 + (touch2.pageY - touch1.pageY) ** 2); 336 | } 337 | }, { passive: false }); 338 | 339 | // Touch move for panning and zooming 340 | document.getElementById('canvas-container').addEventListener('touchmove', function(e) { 341 | if (e.touches.length === 1 && isPanning) { 342 | const touch = e.touches[0]; 343 | const dx = touch.pageX - lastTouchX; 344 | const dy = touch.pageY - lastTouchY; 345 | panOffsetX += dx; 346 | panOffsetY += dy; 347 | lastTouchX = touch.pageX; 348 | lastTouchY = touch.pageY; 349 | applyPanAndZoom(); 350 | drawEdges(); 351 | } else if (e.touches.length === 2) { // Adjust for zooming 352 | e.preventDefault(); 353 | const touch1 = e.touches[0]; 354 | const touch2 = e.touches[1]; 355 | const distance = Math.sqrt((touch2.pageX - touch1.pageX) ** 2 + (touch2.pageY - touch1.pageY) ** 2); 356 | const scaleChange = distance / initialDistance; 357 | scale = Math.min(Math.max(minScale, scale * scaleChange), maxScale); // Apply and limit scale 358 | document.body.style.setProperty('--scale', scale); 359 | initialDistance = distance; 360 | applyPanAndZoom(); 361 | } 362 | }, { passive: false }); 363 | 364 | document.getElementById('canvas-container').addEventListener('touchend', function(e) { 365 | if (isPanning) { 366 | isPanning = false; 367 | } 368 | if (e.touches.length < 2) { 369 | initialDistance = null; // Reset zoom tracking on lifting one finger 370 | } 371 | }); 372 | 373 | // Activate node on touch 374 | document.querySelectorAll('.node .node-name').forEach(nodeName => { 375 | nodeName.addEventListener('touchstart', function(e) { 376 | // Prevent activating multiple nodes simultaneously 377 | deactivateAllNodes(); 378 | const node = this.parentElement; 379 | node.classList.add('is-active'); 380 | // Prepare for potential drag 381 | isDragging = false; 382 | const touch = e.touches[0]; 383 | startX = touch.pageX; 384 | startY = touch.pageY; 385 | selectedElement = node; 386 | e.stopPropagation(); 387 | }, {passive: true}); 388 | }); 389 | 390 | // Deactivate nodes when tapping outside 391 | document.addEventListener('touchstart', function(e) { 392 | if (!e.target.closest('.node')) { 393 | deactivateAllNodes(); 394 | } 395 | }); 396 | 397 | function deactivateAllNodes() { 398 | document.querySelectorAll('.node').forEach(node => { 399 | node.classList.remove('is-active'); 400 | }); 401 | } 402 | 403 | // Handling dragging for an activated node 404 | document.addEventListener('touchmove', function(e) { 405 | if (isDragging && selectedElement && selectedElement.classList.contains('is-active')) { 406 | const touch = e.touches[0]; 407 | const dx = (touch.pageX - startX) / scale; 408 | const dy = (touch.pageY - startY) / scale; 409 | selectedElement.style.left = `${parseInt(selectedElement.style.left, 10) + dx}px`; 410 | selectedElement.style.top = `${parseInt(selectedElement.style.top, 10) + dy}px`; 411 | 412 | // Update startX and startY for the next move event 413 | startX = touch.pageX; 414 | startY = touch.pageY; 415 | 416 | // Call drawEdges to update edge positions based on the new node positions 417 | drawEdges(); 418 | 419 | e.preventDefault(); // Prevent default to avoid scrolling and other touch actions 420 | } 421 | }, { passive: false }); 422 | 423 | // Determine if dragging should start 424 | document.addEventListener('touchmove', function(e) { 425 | if (selectedElement && !isDragging) { 426 | const touch = e.touches[0]; 427 | if (Math.abs(touch.pageX - startX) > 10 || Math.abs(touch.pageY - startY) > 10) { 428 | isDragging = true; // Start dragging if moved beyond threshold 429 | } 430 | } 431 | }, {passive: true}); 432 | 433 | // End dragging 434 | document.addEventListener('touchend', function() { 435 | if (isDragging && selectedElement) { 436 | selectedElement.classList.remove('is-dragging'); 437 | isDragging = false; 438 | selectedElement = null; 439 | } 440 | }); 441 | 442 | function applyPanAndZoom() { 443 | document.body.style.setProperty('--scale', scale); 444 | document.body.style.setProperty('--pan-x', `${panOffsetX}px`); 445 | document.body.style.setProperty('--pan-y', `${panOffsetY}px`); 446 | } 447 | 448 | // Prevent the whole page from zooming on pinch 449 | document.addEventListener('gesturestart', function(e) { 450 | e.preventDefault(); 451 | }); 452 | 453 | document.addEventListener('gesturechange', function(e) { 454 | e.preventDefault(); 455 | }); 456 | 457 | drawEdges(); 458 | updateCanvasData(); --------------------------------------------------------------------------------