Skip to content

Commit b4e7beb

Browse files
committed
second canvas
1 parent 71a2f88 commit b4e7beb

8 files changed

Lines changed: 366 additions & 6 deletions

File tree

src/lib/canvases.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// List of available canvas files
2+
// Update this when you add new canvas files to static/
3+
export const canvases = [
4+
{ name: 'main', title: 'Main Canvas', file: 'main.canvas' },
5+
{ name: 'second', title: 'Second Canvas', file: 'second.canvas' }
6+
];

src/routes/+page.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { redirect } from '@sveltejs/kit';
2+
import { base } from '$app/paths';
3+
4+
export const prerender = true;
5+
6+
export function load() {
7+
// Redirect root to main canvas
8+
throw redirect(307, `${base}/main`);
9+
}

src/routes/+page.svelte

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,20 @@
3030
document.body.style.setProperty('--pan-y', '0px');
3131
3232
try {
33+
// Get canvas name from URL hash or query parameter, default to 'main.canvas'
34+
// Supports both /#canvas-name and ?canvas=canvas-name
35+
let canvasFile = window.location.hash.slice(1); // Remove the #
36+
if (!canvasFile) {
37+
const urlParams = new URLSearchParams(window.location.search);
38+
canvasFile = urlParams.get('canvas') || 'main.canvas';
39+
}
40+
// Add .canvas extension if not present
41+
if (!canvasFile.endsWith('.canvas')) {
42+
canvasFile += '.canvas';
43+
}
44+
3345
// Files in static/ are served at root
34-
const response = await fetch(`${base}/main.canvas`);
46+
const response = await fetch(`${base}/${canvasFile}`);
3547
if (!response.ok) throw new Error('Canvas file not found');
3648
canvasData = await response.json();
3749

src/routes/[canvas]/+page.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { canvases } from '$lib/canvases.js';
2+
import { error } from '@sveltejs/kit';
3+
4+
export const prerender = true;
5+
6+
export function entries() {
7+
// Generate a route for each canvas
8+
return canvases.map(canvas => ({ canvas: canvas.name }));
9+
}
10+
11+
export function load({ params }) {
12+
const canvas = canvases.find(c => c.name === params.canvas);
13+
14+
if (!canvas) {
15+
throw error(404, 'Canvas not found');
16+
}
17+
18+
return {
19+
canvasName: canvas.name,
20+
canvasTitle: canvas.title,
21+
canvasFile: canvas.file
22+
};
23+
}

src/routes/[canvas]/+page.svelte

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
<script>
2+
import { onMount, tick } from 'svelte';
3+
import { base } from '$app/paths';
4+
import { page } from '$app/stores';
5+
import Markdown from 'svelte-exmarkdown';
6+
import { gfmPlugin } from 'svelte-exmarkdown/gfm';
7+
import { canvases } from '$lib/canvases.js';
8+
import '$lib/canvas.css';
9+
10+
const plugins = [gfmPlugin()];
11+
12+
let canvasData = $state(null);
13+
let error = $state(null);
14+
let fileContents = $state({});
15+
16+
// Get canvas info from page data
17+
const { data } = $derived($page);
18+
19+
// Helper to load scripts dynamically
20+
function loadScript(src) {
21+
return new Promise((resolve, reject) => {
22+
const script = document.createElement('script');
23+
script.src = src;
24+
script.onload = resolve;
25+
script.onerror = reject;
26+
document.head.appendChild(script);
27+
});
28+
}
29+
30+
// Track if scripts are loaded
31+
let scriptsLoaded = false;
32+
33+
async function loadCanvas(canvasFile) {
34+
try {
35+
// Reset state
36+
canvasData = null;
37+
fileContents = {};
38+
error = null;
39+
40+
// Files in static/ are served at root
41+
const response = await fetch(`${base}/${canvasFile}`);
42+
if (!response.ok) throw new Error('Canvas file not found');
43+
canvasData = await response.json();
44+
45+
// Load markdown file contents
46+
if (canvasData.nodes) {
47+
for (const node of canvasData.nodes) {
48+
if (node.type === 'file' && node.file.endsWith('.md')) {
49+
try {
50+
// Files in static/ are served at root, not /static/
51+
const mdResponse = await fetch(`${base}/${node.file}`);
52+
if (mdResponse.ok) {
53+
let content = await mdResponse.text();
54+
// Strip YAML frontmatter
55+
content = content.replace(/^---\n[\s\S]*?\n---\n/, '');
56+
// Strip leading h1 header to avoid duplication with card title
57+
content = content.replace(/^#\s+.*\n+/, '');
58+
fileContents[node.id] = content;
59+
}
60+
} catch (e) {
61+
console.warn(`Could not load ${node.file}:`, e);
62+
}
63+
}
64+
}
65+
}
66+
67+
// Wait for all nodes to be rendered in DOM
68+
await tick();
69+
await new Promise(resolve => setTimeout(resolve, 100));
70+
71+
// Store edges globally BEFORE loading canvas.js
72+
window.edges = canvasData.edges || [];
73+
74+
// Load scripts only once
75+
if (!scriptsLoaded) {
76+
await loadScript(`${base}/prism.js`);
77+
await loadScript(`${base}/canvas.js`);
78+
scriptsLoaded = true;
79+
}
80+
81+
// Wait a bit for canvas.js to initialize
82+
await new Promise(resolve => setTimeout(resolve, 100));
83+
84+
// Initialize canvas.js functions
85+
if (typeof adjustCanvasToViewport === 'function') {
86+
adjustCanvasToViewport();
87+
}
88+
if (typeof drawEdges === 'function') {
89+
drawEdges();
90+
}
91+
if (typeof updateCanvasData === 'function') {
92+
updateCanvasData();
93+
}
94+
95+
// Manually set up drag handlers for dynamically created nodes
96+
document.querySelectorAll('.node .node-name').forEach(nodeName => {
97+
nodeName.addEventListener('mousedown', function(e) {
98+
if (window.isSpacePressed) return;
99+
window.isDragging = true;
100+
window.startX = e.clientX;
101+
window.startY = e.clientY;
102+
window.selectedElement = this.parentElement;
103+
window.selectedElement.classList.add('is-dragging');
104+
});
105+
});
106+
} catch (e) {
107+
error = e.message;
108+
console.error('Error loading canvas:', e);
109+
}
110+
}
111+
112+
onMount(() => {
113+
// Set body ID and styles for canvas.js
114+
document.body.id = 'home';
115+
document.body.style.setProperty('--scale', '1');
116+
document.body.style.setProperty('--pan-x', '0px');
117+
document.body.style.setProperty('--pan-y', '0px');
118+
});
119+
120+
// Load canvas whenever the data changes
121+
$effect(() => {
122+
if (data.canvasFile) {
123+
loadCanvas(data.canvasFile);
124+
}
125+
});
126+
</script>
127+
128+
<svelte:head>
129+
<title>{data.canvasTitle} - JSON Canvas Viewer</title>
130+
</svelte:head>
131+
132+
<nav id="canvas-nav">
133+
{#each canvases as canvas}
134+
<a
135+
href="{base}/{canvas.name}"
136+
class="canvas-link"
137+
class:active={data.canvasName === canvas.name}
138+
>
139+
{canvas.title}
140+
</a>
141+
{/each}
142+
</nav>
143+
144+
<div id="container">
145+
<div id="canvas-container">
146+
<svg id="canvas-edges" preserveAspectRatio="none">
147+
<defs>
148+
<marker id="arrowhead" viewBox="0 0 10 10" refX="0" refY="5" markerWidth="6" markerHeight="6" orient="auto">
149+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor"/>
150+
</marker>
151+
</defs>
152+
<g id="edge-paths"></g>
153+
</svg>
154+
155+
<div id="canvas-nodes">
156+
{#if error}
157+
<div style="padding: 20px; color: red;">
158+
Error: {error}
159+
</div>
160+
{:else if canvasData}
161+
{#each canvasData.nodes as node}
162+
<node
163+
id={node.id}
164+
class="node {node.type === 'file' ? 'node-link' : 'node-' + node.type}"
165+
data-node-type={node.type}
166+
style="left: {node.x}px; top: {node.y}px; width: {node.width}px; height: {node.height}px;"
167+
>
168+
<div class="node-name">
169+
{#if node.type === 'file'}
170+
{node.file.replace(/\.md$/, '')}
171+
{/if}
172+
</div>
173+
174+
{#if node.type === 'text'}
175+
<div class="node-text-content">
176+
{@html node.text || ''}
177+
</div>
178+
{:else if node.type === 'file'}
179+
{#if fileContents[node.id]}
180+
<div class="node-text-content">
181+
<h1>{node.file.replace(/\.md$/, '')}</h1>
182+
<Markdown md={fileContents[node.id]} {plugins} />
183+
</div>
184+
{:else}
185+
<div class="node-text-content">
186+
<p style="color: gray;">Loading {node.file}...</p>
187+
</div>
188+
{/if}
189+
{:else if node.type === 'link'}
190+
<a href={node.url} target="_blank" rel="noopener noreferrer">
191+
{node.url}
192+
</a>
193+
{/if}
194+
</node>
195+
{/each}
196+
{:else}
197+
<div style="padding: 20px;">Loading canvas...</div>
198+
{/if}
199+
</div>
200+
201+
<div id="output" class="theme-dark hidden">
202+
<div class="code-header">
203+
<span class="language">JSON Canvas</span>
204+
<span class="close-output">×</span>
205+
</div>
206+
<div id="output-code">
207+
<pre><code class="language-json" id="positionsOutput"></code></pre>
208+
</div>
209+
<div class="code-footer">
210+
<button class="button-copy">Copy code</button>
211+
<button class="button-download">Download file</button>
212+
</div>
213+
</div>
214+
215+
<div id="controls">
216+
<div id="zoom-controls">
217+
<button id="toggle-output">Toggle output</button>
218+
<button id="zoom-out">Zoom out</button>
219+
<button id="zoom-in">Zoom in</button>
220+
<button id="zoom-reset">Reset</button>
221+
</div>
222+
</div>
223+
</div>
224+
</div>
225+
226+
<style>
227+
:global(body) {
228+
margin: 0;
229+
padding: 0;
230+
}
231+
232+
:global(.node-text-content) {
233+
overflow-y: auto;
234+
overflow-x: hidden;
235+
padding: 0 0.5rem 0 1rem;
236+
height: 100%;
237+
box-sizing: border-box;
238+
position: relative;
239+
}
240+
241+
:global(.node-text-content > *:first-child) {
242+
margin-top: 1rem;
243+
}
244+
245+
:global(.node-text-content > *:last-child) {
246+
margin-bottom: 1rem;
247+
}
248+
249+
:global(.node) {
250+
overflow: hidden;
251+
display: flex;
252+
flex-direction: column;
253+
}
254+
255+
:global(.node-text) {
256+
overflow: visible;
257+
}
258+
259+
:global(#canvas-edges path) {
260+
stroke: var(--color-ui-3, #5E0641);
261+
stroke-width: 2px;
262+
fill: none;
263+
}
264+
265+
:global(#arrowhead polygon) {
266+
fill: var(--color-ui-3, #5E0641);
267+
}
268+
269+
#canvas-nav {
270+
position: fixed;
271+
top: 1rem;
272+
left: 1rem;
273+
z-index: 200;
274+
display: flex;
275+
gap: 0.5rem;
276+
background: var(--color-bg-1);
277+
border: 1px solid var(--color-ui-1);
278+
border-radius: 8px;
279+
padding: 0.5rem;
280+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
281+
}
282+
283+
.canvas-link {
284+
padding: 0.5rem 1rem;
285+
text-decoration: none;
286+
color: var(--color-tx-1);
287+
border-radius: 6px;
288+
font-weight: 500;
289+
transition: all 150ms;
290+
}
291+
292+
.canvas-link:hover {
293+
background: var(--color-bg-2);
294+
}
295+
296+
.canvas-link.active {
297+
background: var(--color-ui-3);
298+
color: var(--color-bg-1);
299+
}
300+
</style>

static/.obsidian/workspace.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
"state": {
1414
"type": "canvas",
1515
"state": {
16-
"file": "main.canvas",
16+
"file": "second.canvas",
1717
"viewState": {
18-
"x": 90,
19-
"y": 71.05925903320312,
18+
"x": 0,
19+
"y": 0,
2020
"zoom": 0
2121
}
2222
},
2323
"icon": "lucide-layout-dashboard",
24-
"title": "main"
24+
"title": "second"
2525
}
2626
}
2727
]
@@ -174,10 +174,11 @@
174174
},
175175
"active": "0f6e29742202f5b9",
176176
"lastOpenFiles": [
177+
"advanced.md",
178+
"main.canvas",
177179
"canvas.js.tmp.34235.1762896218093",
178180
"welcome.md",
179181
"small-but-qualitative-data.md",
180-
"main.canvas",
181182
"big-observational-data.md",
182183
"survey-data.md",
183184
"team-data-annotations.md",

0 commit comments

Comments
 (0)