Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 657a3eb

Browse files
feat: Hamiltonian Path & Circuit Finder — interactive backtracking visualizer
Add interactive HTML tool for finding Hamiltonian paths and circuits: - Canvas-based graph editor (click to add nodes, drag to add edges) - Backtracking solver with Warnsdorff heuristic for pruning - Real-time animation with pause/resume/stop controls - 8 preset graphs (Petersen, Cube, Dodecahedron, K5, C8, K3,3, Herschel, Wheel6) - Path and Circuit mode toggle
1 parent 8b29197 commit 657a3eb

2 files changed

Lines changed: 374 additions & 0 deletions

File tree

docs/hamiltonian.html

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Hamiltonian Path & Circuit Finder - GraphVisual</title>
7+
<link rel="stylesheet" href="styles.css">
8+
<style>
9+
.ham-container { display: flex; gap: 20px; flex-wrap: wrap; }
10+
.ham-canvas-wrap { flex: 1; min-width: 500px; }
11+
.ham-canvas-wrap canvas { width: 100%; border: 2px solid #334155; border-radius: 8px; background: #0f172a; cursor: crosshair; }
12+
.ham-controls { width: 300px; }
13+
.ham-panel { background: #1e293b; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
14+
.ham-panel h3 { margin: 0 0 12px; color: #94a3b8; font-size: 13px; text-transform: uppercase; letter-spacing: 1px; }
15+
.ham-btn { display: inline-block; padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600; margin: 3px; transition: all .15s; }
16+
.ham-btn-primary { background: #3b82f6; color: #fff; }
17+
.ham-btn-primary:hover { background: #2563eb; }
18+
.ham-btn-success { background: #10b981; color: #fff; }
19+
.ham-btn-success:hover { background: #059669; }
20+
.ham-btn-danger { background: #ef4444; color: #fff; }
21+
.ham-btn-danger:hover { background: #dc2626; }
22+
.ham-btn-warn { background: #f59e0b; color: #fff; }
23+
.ham-btn-warn:hover { background: #d97706; }
24+
.ham-btn-sm { padding: 5px 10px; font-size: 12px; }
25+
.ham-btn:disabled { opacity: .4; cursor: not-allowed; }
26+
.ham-preset { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
27+
.ham-log { background: #0f172a; border-radius: 6px; padding: 10px; font-family: 'Consolas','Courier New',monospace; font-size: 12px; color: #94a3b8; max-height: 200px; overflow-y: auto; line-height: 1.5; }
28+
.ham-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
29+
.ham-stat { background: #0f172a; border-radius: 6px; padding: 10px; text-align: center; }
30+
.ham-stat .val { font-size: 22px; font-weight: 700; color: #e2e8f0; }
31+
.ham-stat .lbl { font-size: 11px; color: #64748b; margin-top: 2px; }
32+
.ham-speed { width: 100%; margin: 8px 0; }
33+
.ham-result { padding: 10px; border-radius: 6px; margin-top: 10px; font-weight: 600; text-align: center; }
34+
.ham-result.found { background: #064e3b; color: #34d399; }
35+
.ham-result.not-found { background: #7f1d1d; color: #fca5a5; }
36+
.ham-result.running { background: #1e3a5f; color: #93c5fd; }
37+
.ham-legend { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; }
38+
.ham-legend-item { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #94a3b8; }
39+
.ham-legend-dot { width: 12px; height: 12px; border-radius: 50%; }
40+
select, input[type=number] { background: #0f172a; color: #e2e8f0; border: 1px solid #334155; border-radius: 4px; padding: 6px 8px; font-size: 13px; }
41+
.ham-mode-toggle { display: flex; border-radius: 6px; overflow: hidden; border: 1px solid #334155; }
42+
.ham-mode-toggle button { flex: 1; padding: 8px; border: none; background: #0f172a; color: #94a3b8; cursor: pointer; font-size: 13px; font-weight: 600; }
43+
.ham-mode-toggle button.active { background: #3b82f6; color: #fff; }
44+
.ham-instructions { font-size: 12px; color: #64748b; margin-top: 8px; line-height: 1.6; }
45+
</style>
46+
</head>
47+
<body>
48+
<nav class="sidebar">
49+
<a href="index.html" class="sidebar-logo"><span>📊</span><div><h2>GraphVisual</h2><small>Documentation</small></div></a>
50+
<a href="index.html" class="sidebar-link"><span class="icon">🏠</span>Home</a>
51+
<a href="hamiltonian.html" class="sidebar-link active"><span class="icon">🔁</span>Hamiltonian</a>
52+
<a href="euler.html" class="sidebar-link"><span class="icon">🛤️</span>Euler Path</a>
53+
<a href="pathfinder.html" class="sidebar-link"><span class="icon">🧭</span>Path Finder</a>
54+
<a href="coloring.html" class="sidebar-link"><span class="icon">🎨</span>Coloring</a>
55+
<a href="planarity.html" class="sidebar-link"><span class="icon">📐</span>Planarity</a>
56+
</nav>
57+
<main class="content">
58+
<h1>🔁 Hamiltonian Path & Circuit Finder</h1>
59+
<p>Find Hamiltonian paths and circuits using backtracking with pruning. Watch the algorithm explore and backtrack in real-time.</p>
60+
61+
<div class="ham-container">
62+
<div class="ham-canvas-wrap">
63+
<canvas id="hamCanvas" width="700" height="500"></canvas>
64+
<div class="ham-legend">
65+
<div class="ham-legend-item"><div class="ham-legend-dot" style="background:#64748b"></div>Unvisited</div>
66+
<div class="ham-legend-item"><div class="ham-legend-dot" style="background:#3b82f6"></div>Current</div>
67+
<div class="ham-legend-item"><div class="ham-legend-dot" style="background:#10b981"></div>In Path</div>
68+
<div class="ham-legend-item"><div class="ham-legend-dot" style="background:#ef4444"></div>Backtracked</div>
69+
<div class="ham-legend-item"><div class="ham-legend-dot" style="background:#fbbf24"></div>Solution</div>
70+
</div>
71+
<div class="ham-instructions">
72+
<strong>Click</strong> canvas to add nodes. <strong>Drag</strong> between nodes to add edges. <strong>Right-click</strong> node to delete. <strong>Shift+click</strong> node to set start.
73+
</div>
74+
</div>
75+
<div class="ham-controls">
76+
<div class="ham-panel">
77+
<h3>Mode</h3>
78+
<div class="ham-mode-toggle">
79+
<button id="modePath" class="active" onclick="setMode('path')">Path</button>
80+
<button id="modeCircuit" onclick="setMode('circuit')">Circuit</button>
81+
</div>
82+
</div>
83+
<div class="ham-panel">
84+
<h3>Presets</h3>
85+
<div class="ham-preset">
86+
<button class="ham-btn ham-btn-primary ham-btn-sm" onclick="loadPreset('petersen')">Petersen</button>
87+
<button class="ham-btn ham-btn-primary ham-btn-sm" onclick="loadPreset('cube')">Cube</button>
88+
<button class="ham-btn ham-btn-primary ham-btn-sm" onclick="loadPreset('dodecahedron')">Dodecahedron</button>
89+
<button class="ham-btn ham-btn-primary ham-btn-sm" onclick="loadPreset('complete5')">K₅</button>
90+
<button class="ham-btn ham-btn-primary ham-btn-sm" onclick="loadPreset('cycle8')">C₈</button>
91+
<button class="ham-btn ham-btn-primary ham-btn-sm" onclick="loadPreset('bipartite')">K₃,₃</button>
92+
<button class="ham-btn ham-btn-primary ham-btn-sm" onclick="loadPreset('herschel')">Herschel</button>
93+
<button class="ham-btn ham-btn-primary ham-btn-sm" onclick="loadPreset('wheel6')">Wheel₆</button>
94+
</div>
95+
</div>
96+
<div class="ham-panel">
97+
<h3>Animation Speed</h3>
98+
<input type="range" id="speedSlider" class="ham-speed" min="1" max="100" value="50">
99+
<div style="display:flex;justify-content:space-between;font-size:11px;color:#64748b"><span>Slow</span><span>Fast</span></div>
100+
</div>
101+
<div class="ham-panel">
102+
<h3>Actions</h3>
103+
<button class="ham-btn ham-btn-success" id="btnRun" onclick="runSolver()">▶ Find</button>
104+
<button class="ham-btn ham-btn-warn" id="btnPause" onclick="togglePause()" disabled>⏸ Pause</button>
105+
<button class="ham-btn ham-btn-danger" id="btnStop" onclick="stopSolver()" disabled>⏹ Stop</button>
106+
<button class="ham-btn ham-btn-primary" onclick="clearGraph()">🗑 Clear</button>
107+
<div id="resultBox"></div>
108+
</div>
109+
<div class="ham-panel">
110+
<h3>Statistics</h3>
111+
<div class="ham-stats">
112+
<div class="ham-stat"><div class="val" id="statNodes">0</div><div class="lbl">Nodes</div></div>
113+
<div class="ham-stat"><div class="val" id="statEdges">0</div><div class="lbl">Edges</div></div>
114+
<div class="ham-stat"><div class="val" id="statSteps">0</div><div class="lbl">Steps</div></div>
115+
<div class="ham-stat"><div class="val" id="statBacktracks">0</div><div class="lbl">Backtracks</div></div>
116+
</div>
117+
</div>
118+
<div class="ham-panel">
119+
<h3>Algorithm Log</h3>
120+
<div class="ham-log" id="logBox">Ready. Add nodes or load a preset.</div>
121+
</div>
122+
</div>
123+
</div>
124+
125+
<script>
126+
const canvas = document.getElementById('hamCanvas');
127+
const ctx = canvas.getContext('2d');
128+
let nodes = [], edges = [], mode = 'path', startNode = 0;
129+
let solving = false, paused = false, stopRequested = false;
130+
let steps = 0, backtracks = 0;
131+
let visitState = [], pathEdges = [], currentPath = [];
132+
let dragFrom = null, dragPos = null, hoverNode = -1;
133+
134+
const COLORS = { unvisited: '#64748b', current: '#3b82f6', inPath: '#10b981', backtracked: '#ef4444', solution: '#fbbf24' };
135+
136+
function delay() { const v = document.getElementById('speedSlider').value; return Math.max(5, 500 - v * 4.9); }
137+
138+
function draw() {
139+
ctx.clearRect(0, 0, canvas.width, canvas.height);
140+
// edges
141+
edges.forEach(([a, b], i) => {
142+
const na = nodes[a], nb = nodes[b];
143+
const isPath = pathEdges.includes(i);
144+
const isSolution = visitState.solution;
145+
ctx.beginPath(); ctx.moveTo(na.x, na.y); ctx.lineTo(nb.x, nb.y);
146+
ctx.strokeStyle = isPath ? (isSolution ? COLORS.solution : COLORS.inPath) : '#334155';
147+
ctx.lineWidth = isPath ? 3 : 1.5;
148+
ctx.stroke();
149+
});
150+
// drag line
151+
if (dragFrom !== null && dragPos) {
152+
ctx.beginPath(); ctx.moveTo(nodes[dragFrom].x, nodes[dragFrom].y);
153+
ctx.lineTo(dragPos.x, dragPos.y);
154+
ctx.strokeStyle = '#3b82f6'; ctx.lineWidth = 2; ctx.setLineDash([5,5]); ctx.stroke(); ctx.setLineDash([]);
155+
}
156+
// nodes
157+
nodes.forEach((n, i) => {
158+
let color = COLORS.unvisited;
159+
if (visitState.solution && currentPath.includes(i)) color = COLORS.solution;
160+
else if (visitState[i] === 'current') color = COLORS.current;
161+
else if (visitState[i] === 'inPath') color = COLORS.inPath;
162+
else if (visitState[i] === 'backtracked') color = COLORS.backtracked;
163+
ctx.beginPath(); ctx.arc(n.x, n.y, i === startNode ? 18 : 15, 0, Math.PI * 2);
164+
ctx.fillStyle = color; ctx.fill();
165+
ctx.strokeStyle = i === hoverNode ? '#fff' : '#1e293b'; ctx.lineWidth = 2; ctx.stroke();
166+
ctx.fillStyle = '#fff'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
167+
ctx.fillText(i.toString(), n.x, n.y);
168+
if (i === startNode) { ctx.fillStyle = '#fbbf24'; ctx.font = '10px sans-serif'; ctx.fillText('★', n.x, n.y - 22); }
169+
});
170+
updateStats();
171+
}
172+
173+
function updateStats() {
174+
document.getElementById('statNodes').textContent = nodes.length;
175+
document.getElementById('statEdges').textContent = edges.length;
176+
document.getElementById('statSteps').textContent = steps;
177+
document.getElementById('statBacktracks').textContent = backtracks;
178+
}
179+
180+
function log(msg) {
181+
const box = document.getElementById('logBox');
182+
box.innerHTML += '\n' + msg;
183+
box.scrollTop = box.scrollHeight;
184+
}
185+
186+
function setMode(m) {
187+
mode = m;
188+
document.getElementById('modePath').classList.toggle('active', m === 'path');
189+
document.getElementById('modeCircuit').classList.toggle('active', m === 'circuit');
190+
}
191+
192+
function nodeAt(x, y) { return nodes.findIndex(n => Math.hypot(n.x - x, n.y - y) < 18); }
193+
function edgeExists(a, b) { return edges.some(([u, v]) => (u === a && v === b) || (u === b && v === a)); }
194+
function neighbors(n) { return edges.reduce((acc, [a, b], i) => { if (a === n) acc.push({node: b, edge: i}); if (b === n) acc.push({node: a, edge: i}); return acc; }, []); }
195+
196+
canvas.addEventListener('mousedown', e => {
197+
const r = canvas.getBoundingClientRect();
198+
const x = (e.offsetX / r.width) * canvas.width, y = (e.offsetY / r.height) * canvas.height;
199+
const idx = nodeAt(x, y);
200+
if (e.button === 2) { e.preventDefault(); if (idx >= 0) { deleteNode(idx); } return; }
201+
if (e.shiftKey && idx >= 0) { startNode = idx; log(`Start node → ${idx}`); draw(); return; }
202+
if (idx >= 0) { dragFrom = idx; } else { nodes.push({x, y}); log(`Added node ${nodes.length-1}`); draw(); }
203+
});
204+
canvas.addEventListener('mousemove', e => {
205+
const r = canvas.getBoundingClientRect();
206+
const x = (e.offsetX / r.width) * canvas.width, y = (e.offsetY / r.height) * canvas.height;
207+
hoverNode = nodeAt(x, y);
208+
if (dragFrom !== null) { dragPos = {x, y}; draw(); }
209+
});
210+
canvas.addEventListener('mouseup', e => {
211+
if (dragFrom !== null) {
212+
const r = canvas.getBoundingClientRect();
213+
const x = (e.offsetX / r.width) * canvas.width, y = (e.offsetY / r.height) * canvas.height;
214+
const idx = nodeAt(x, y);
215+
if (idx >= 0 && idx !== dragFrom && !edgeExists(dragFrom, idx)) {
216+
edges.push([dragFrom, idx]); log(`Edge ${dragFrom}${idx}`);
217+
}
218+
}
219+
dragFrom = null; dragPos = null; draw();
220+
});
221+
canvas.addEventListener('contextmenu', e => e.preventDefault());
222+
223+
function deleteNode(idx) {
224+
nodes.splice(idx, 1);
225+
edges = edges.filter(([a, b]) => a !== idx && b !== idx).map(([a, b]) => [a > idx ? a-1 : a, b > idx ? b-1 : b]);
226+
if (startNode >= nodes.length) startNode = 0;
227+
if (startNode === idx) startNode = 0;
228+
else if (startNode > idx) startNode--;
229+
log(`Deleted node ${idx}`); draw();
230+
}
231+
232+
function clearGraph() { nodes = []; edges = []; visitState = []; pathEdges = []; currentPath = []; steps = 0; backtracks = 0; startNode = 0; document.getElementById('logBox').innerHTML = 'Cleared.'; document.getElementById('resultBox').innerHTML = ''; draw(); }
233+
234+
function showResult(found, msg) {
235+
const box = document.getElementById('resultBox');
236+
box.className = 'ham-result ' + (found ? 'found' : 'not-found');
237+
box.textContent = msg;
238+
}
239+
240+
async function runSolver() {
241+
if (solving) return;
242+
if (nodes.length < 2) { log('Need at least 2 nodes.'); return; }
243+
solving = true; paused = false; stopRequested = false; steps = 0; backtracks = 0;
244+
visitState = new Array(nodes.length).fill('unvisited'); pathEdges = []; currentPath = [];
245+
document.getElementById('btnRun').disabled = true;
246+
document.getElementById('btnPause').disabled = false;
247+
document.getElementById('btnStop').disabled = false;
248+
document.getElementById('resultBox').innerHTML = '<div class="ham-result running">Searching...</div>';
249+
log(`\n--- Finding Hamiltonian ${mode} from node ${startNode} ---`);
250+
251+
const found = await hamiltonian(startNode);
252+
if (stopRequested) { log('Stopped by user.'); }
253+
else if (found) {
254+
visitState.solution = true;
255+
showResult(true, `✅ Hamiltonian ${mode} found! (${steps} steps, ${backtracks} backtracks)`);
256+
log(`✅ Found: ${currentPath.join(' → ')}${mode === 'circuit' ? ' → ' + currentPath[0] : ''}`);
257+
} else {
258+
showResult(false, `❌ No Hamiltonian ${mode} exists (${steps} steps, ${backtracks} backtracks)`);
259+
log('❌ No solution exists.');
260+
}
261+
draw();
262+
solving = false;
263+
document.getElementById('btnRun').disabled = false;
264+
document.getElementById('btnPause').disabled = true;
265+
document.getElementById('btnStop').disabled = true;
266+
}
267+
268+
async function hamiltonian(node) {
269+
if (stopRequested) return false;
270+
while (paused) await new Promise(r => setTimeout(r, 100));
271+
272+
currentPath.push(node);
273+
visitState[node] = 'current';
274+
steps++;
275+
draw();
276+
await new Promise(r => setTimeout(r, delay()));
277+
278+
if (currentPath.length === nodes.length) {
279+
if (mode === 'path') { visitState[node] = 'inPath'; return true; }
280+
// circuit: check edge back to start
281+
if (edgeExists(node, startNode)) {
282+
const ei = edges.findIndex(([a,b]) => (a===node&&b===startNode)||(a===startNode&&b===node));
283+
if (ei >= 0) pathEdges.push(ei);
284+
visitState[node] = 'inPath';
285+
return true;
286+
}
287+
}
288+
289+
if (currentPath.length < nodes.length) {
290+
// pruning: check if unvisited nodes are still reachable
291+
const nbrs = neighbors(node).filter(n => !currentPath.includes(n.node));
292+
// sort by degree (Warnsdorff-like heuristic)
293+
nbrs.sort((a, b) => {
294+
const da = neighbors(a.node).filter(n => !currentPath.includes(n.node)).length;
295+
const db = neighbors(b.node).filter(n => !currentPath.includes(n.node)).length;
296+
return da - db;
297+
});
298+
for (const {node: next, edge: ei} of nbrs) {
299+
visitState[node] = 'inPath';
300+
pathEdges.push(ei);
301+
draw();
302+
if (await hamiltonian(next)) return true;
303+
pathEdges.pop();
304+
}
305+
}
306+
307+
// backtrack
308+
currentPath.pop();
309+
visitState[node] = 'backtracked';
310+
backtracks++;
311+
steps++;
312+
log(`↩ Backtrack from ${node}`);
313+
draw();
314+
await new Promise(r => setTimeout(r, delay() / 2));
315+
return false;
316+
}
317+
318+
function togglePause() {
319+
paused = !paused;
320+
document.getElementById('btnPause').textContent = paused ? '▶ Resume' : '⏸ Pause';
321+
if (!paused) log('Resumed.');
322+
else log('Paused.');
323+
}
324+
function stopSolver() { stopRequested = true; }
325+
326+
// Presets
327+
function loadPreset(name) {
328+
clearGraph();
329+
const cx = canvas.width / 2, cy = canvas.height / 2;
330+
const ring = (n, r, ox=0, oy=0) => Array.from({length: n}, (_, i) => ({x: cx + ox + r * Math.cos(2*Math.PI*i/n - Math.PI/2), y: cy + oy + r * Math.sin(2*Math.PI*i/n - Math.PI/2)}));
331+
332+
if (name === 'petersen') {
333+
nodes = [...ring(5, 180), ...ring(5, 80)];
334+
edges = [[0,1],[1,2],[2,3],[3,4],[4,0],[5,7],[7,9],[9,6],[6,8],[8,5],[0,5],[1,6],[2,7],[3,8],[4,9]];
335+
log('Loaded Petersen graph (no Hamiltonian circuit!)');
336+
} else if (name === 'cube') {
337+
nodes = [{x:cx-100,y:cy-100},{x:cx+100,y:cy-100},{x:cx+100,y:cy+100},{x:cx-100,y:cy+100},{x:cx-50,y:cy-50},{x:cx+150,y:cy-50},{x:cx+150,y:cy+150},{x:cx-50,y:cy+150}];
338+
edges = [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]];
339+
log('Loaded Cube graph (Q₃)');
340+
} else if (name === 'dodecahedron') {
341+
nodes = [...ring(10, 200), ...ring(10, 110)];
342+
edges = [[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9],[9,0],[0,10],[1,11],[2,12],[3,13],[4,14],[5,15],[6,16],[7,17],[8,18],[9,19],[10,11],[12,13],[14,15],[16,17],[18,19],[11,12],[13,14],[15,16],[17,18],[19,10]];
343+
log('Loaded Dodecahedron');
344+
} else if (name === 'complete5') {
345+
nodes = ring(5, 150);
346+
edges = [[0,1],[0,2],[0,3],[0,4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]];
347+
log('Loaded K₅ (complete graph)');
348+
} else if (name === 'cycle8') {
349+
nodes = ring(8, 170);
350+
edges = [[0,1],[1,2],[2,3],[3,4],[4,5],[5,6],[6,7],[7,0]];
351+
log('Loaded C₈ (cycle)');
352+
} else if (name === 'bipartite') {
353+
nodes = [{x:cx-120,y:cy-120},{x:cx-120,y:cy},{x:cx-120,y:cy+120},{x:cx+120,y:cy-120},{x:cx+120,y:cy},{x:cx+120,y:cy+120}];
354+
edges = [[0,3],[0,4],[0,5],[1,3],[1,4],[1,5],[2,3],[2,4],[2,5]];
355+
log('Loaded K₃,₃ (complete bipartite)');
356+
} else if (name === 'herschel') {
357+
nodes = [{x:cx,y:cy-180},{x:cx-150,y:cy-80},{x:cx+150,y:cy-80},{x:cx-180,y:cy+40},{x:cx+180,y:cy+40},{x:cx-100,y:cy+40},{x:cx+100,y:cy+40},{x:cx-60,y:cy+140},{x:cx+60,y:cy+140},{x:cx,y:cy+100},{x:cx,y:cy+200}];
358+
edges = [[0,1],[0,2],[1,3],[1,5],[2,4],[2,6],[3,5],[3,7],[4,6],[4,8],[5,9],[6,9],[7,10],[8,10],[9,7],[9,8]];
359+
log('Loaded Herschel graph (no Hamiltonian circuit!)');
360+
} else if (name === 'wheel6') {
361+
const outer = ring(6, 170);
362+
nodes = [{x:cx,y:cy}, ...outer];
363+
edges = [[1,2],[2,3],[3,4],[4,5],[5,6],[6,1],[0,1],[0,2],[0,3],[0,4],[0,5],[0,6]];
364+
log('Loaded Wheel₆');
365+
}
366+
draw();
367+
}
368+
369+
loadPreset('cube');
370+
</script>
371+
</main>
372+
</body>
373+
</html>

0 commit comments

Comments
 (0)