Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ new GraphGPU({
|--------|-------------|
| `setPalette(name)` | Switch color palette (recolors nodes AND edges) |
| `setBackground(rgba)` | Change background color |
| `setNodeSize(size)` | Change node size at runtime |
| `setEdgeOpacity(opacity)` | Change edge opacity at runtime |
| `setLabelsVisible(bool)` | Show or hide all labels (node + edge) |
| `getTagColors()` | Get current tag→color assignments |

#### Interaction
Expand Down Expand Up @@ -194,7 +197,7 @@ npm install
npm run dev
```

Features light/dark theme toggle, palette switching, node editing/deletion, animated physics mode, and a status bar with live node properties.
Features light/dark theme toggle, palette switching, node editing/deletion, animated physics mode, a live settings panel (node size, edge opacity, labels, all physics parameters), and a status bar with live node properties.

### Architecture

Expand Down Expand Up @@ -222,7 +225,8 @@ src/

### Contributing

You are 100% welcome! Just make a PR. 🚀
Anything you notice or want to suggest, just [open an issue](https://github.com/drkameleon/GraphGPU/issues).
Want to contribute code? You are 100% welcome too! Just make a PR. 🚀

<hr/>

Expand Down
118 changes: 117 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
{{ darkMode ? 'light' : 'dark' }}
</button>
<div class="spacer"></div>
<button class="btn" @click="settingsModal.open = true">
<i class="ph-sliders-horizontal-bold"></i> settings
</button>
</div>

<!-- CANVAS CONTAINER (label overlay attaches here) -->
Expand Down Expand Up @@ -137,6 +140,119 @@
</div>
</div>
</div>

<!-- SETTINGS MODAL -->
<div class="modal-overlay" v-if="settingsModal.open" @click.self="settingsModal.open = false">
<div class="modal-card settings-card" :class="{ light: !darkMode }">
<div class="modal-head">
<span><i class="ph-sliders-horizontal-bold"></i> Settings</span>
<button class="close" @click="settingsModal.open = false">&times;</button>
</div>
<div class="modal-body">
<div class="settings-section">
<div class="settings-section-title">Appearance</div>

<div class="setting-row">
<label>Node size</label>
<div class="slider-group">
<input type="range" min="2" max="20" step="0.5"
:value="settingsModal.nodeSize"
@input="onSettingChange('nodeSize', $event)" />
<span class="slider-val">{{ settingsModal.nodeSize }}</span>
</div>
</div>

<div class="setting-row">
<label>Edge opacity</label>
<div class="slider-group">
<input type="range" min="0.01" max="1" step="0.01"
:value="settingsModal.edgeOpacity"
@input="onSettingChange('edgeOpacity', $event)" />
<span class="slider-val">{{ settingsModal.edgeOpacity }}</span>
</div>
</div>

<div class="setting-row">
<label>Show labels</label>
<div class="toggle-group">
<button class="toggle-btn" :class="{ active: settingsModal.showLabels }"
@click="onToggleSetting('showLabels')">
{{ settingsModal.showLabels ? 'On' : 'Off' }}
</button>
</div>
</div>
</div>

<div class="settings-section">
<div class="settings-section-title">Physics</div>

<div class="setting-row">
<label>Repulsion</label>
<div class="slider-group">
<input type="range" min="-2" max="0" step="0.01"
:value="settingsModal.gravitationalConstant"
@input="onSettingChange('gravitationalConstant', $event)" />
<span class="slider-val">{{ settingsModal.gravitationalConstant }}</span>
</div>
</div>

<div class="setting-row">
<label>Spring length</label>
<div class="slider-group">
<input type="range" min="0.05" max="1" step="0.01"
:value="settingsModal.springLength"
@input="onSettingChange('springLength', $event)" />
<span class="slider-val">{{ settingsModal.springLength }}</span>
</div>
</div>

<div class="setting-row">
<label>Spring stiffness</label>
<div class="slider-group">
<input type="range" min="0.01" max="0.3" step="0.005"
:value="settingsModal.springConstant"
@input="onSettingChange('springConstant', $event)" />
<span class="slider-val">{{ settingsModal.springConstant }}</span>
</div>
</div>

<div class="setting-row">
<label>Central gravity</label>
<div class="slider-group">
<input type="range" min="0" max="0.1" step="0.001"
:value="settingsModal.centralGravity"
@input="onSettingChange('centralGravity', $event)" />
<span class="slider-val">{{ settingsModal.centralGravity }}</span>
</div>
</div>

<div class="setting-row">
<label>Damping</label>
<div class="slider-group">
<input type="range" min="0.01" max="0.5" step="0.01"
:value="settingsModal.damping"
@input="onSettingChange('damping', $event)" />
<span class="slider-val">{{ settingsModal.damping }}</span>
</div>
</div>

<div class="setting-row">
<label>Barnes-Hut θ</label>
<div class="slider-group">
<input type="range" min="0" max="1.5" step="0.05"
:value="settingsModal.barnesHutTheta"
@input="onSettingChange('barnesHutTheta', $event)" />
<span class="slider-val">{{ settingsModal.barnesHutTheta }}</span>
</div>
</div>
</div>
</div>
<div class="modal-foot">
<button class="btn-cancel" @click="resetSettings">Reset defaults</button>
<button class="btn-save" @click="settingsModal.open = false">Done</button>
</div>
</div>
</div>
</div>

<div class="fallback" id="fallback">
Expand All @@ -147,4 +263,4 @@ <h2>WebGPU Not Available</h2>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
</html>
88 changes: 87 additions & 1 deletion demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ interface DeleteModalState {
label: string;
}

interface SettingsModalState {
open: boolean;
nodeSize: number;
edgeOpacity: number;
showLabels: boolean;
gravitationalConstant: number;
springLength: number;
springConstant: number;
centralGravity: number;
damping: number;
barnesHutTheta: number;
}

// ── Constants ──

const NODE_TYPE_TAGS = ['person', 'movie', 'country', 'book'] as const;
Expand All @@ -35,6 +48,18 @@ const LAYOUT_OPTS = {
maxIterations: 1000,
} as const;

const DEFAULT_SETTINGS: Omit<SettingsModalState, 'open'> = {
nodeSize: 8,
edgeOpacity: 0.8,
showLabels: true,
gravitationalConstant: -0.25,
springLength: 0.2,
springConstant: 0.06,
centralGravity: 0.012,
damping: 0.18,
barnesHutTheta: 0.3,
};

const LIGHT_BG: [number, number, number, number] = [0.96, 0.96, 0.965, 1];
const DARK_BG: [number, number, number, number] = [0.118, 0.122, 0.149, 1];

Expand Down Expand Up @@ -80,6 +105,10 @@ createApp({
// Modals
const editModal = reactive<EditModalState>({ open: false, nodeId: null, properties: {} });
const deleteModal = reactive<DeleteModalState>({ open: false, nodeId: null, label: '' });
const settingsModal = reactive<SettingsModalState>({
open: false,
...DEFAULT_SETTINGS,
});

// Legend
const legendItems = ref<LegendItem[]>([]);
Expand Down Expand Up @@ -209,6 +238,62 @@ createApp({
updateCounts();
}

// ── Settings ──

function getPhysicsOpts() {
return {
gravitationalConstant: settingsModal.gravitationalConstant,
springLength: settingsModal.springLength,
springConstant: settingsModal.springConstant,
centralGravity: settingsModal.centralGravity,
damping: settingsModal.damping,
barnesHutTheta: settingsModal.barnesHutTheta,
maxIterations: 1000,
};
}

function onSettingChange(key: string, event: Event): void {
const val = parseFloat((event.target as HTMLInputElement).value);
(settingsModal as any)[key] = val;

if (!g) return;

// Appearance — apply immediately
if (key === 'nodeSize') {
g.setNodeSize(val);
} else if (key === 'edgeOpacity') {
g.setEdgeOpacity(val);
}

// Physics — restart layout with new params
if (['gravitationalConstant', 'springLength', 'springConstant',
'centralGravity', 'damping', 'barnesHutTheta'].includes(key)) {
if (layoutRunning.value) {
g.stopLayout();
g.startLayout(getPhysicsOpts());
}
}
}

function onToggleSetting(key: string): void {
if (key === 'showLabels') {
settingsModal.showLabels = !settingsModal.showLabels;
g?.setLabelsVisible(settingsModal.showLabels);
}
}

function resetSettings(): void {
Object.assign(settingsModal, DEFAULT_SETTINGS);
if (!g) return;
g.setNodeSize(DEFAULT_SETTINGS.nodeSize);
g.setEdgeOpacity(DEFAULT_SETTINGS.edgeOpacity);
g.setLabelsVisible(DEFAULT_SETTINGS.showLabels);
if (layoutRunning.value) {
g.stopLayout();
g.startLayout(getPhysicsOpts());
}
}

// ── Init ──

onMounted(async () => {
Expand Down Expand Up @@ -310,10 +395,11 @@ createApp({
hasSelection,
tooltipVisible, tooltipStyle, tooltipTag, tooltipName, tooltipProps, tooltipColor,
legendItems, paletteNames,
editModal, deleteModal,
editModal, deleteModal, settingsModal,
toggleLayout, fitView, resetGraph, toggleAnimated, toggleDarkMode,
switchPalette, getPalettePreview,
showEditModal, saveEdit, deleteSelected, confirmDelete,
onSettingChange, onToggleSetting, resetSettings,
};
},
}).mount('#app');
Expand Down
Loading