Compare commits

...

13 Commits

3 changed files with 577 additions and 0 deletions

View File

@ -4,6 +4,7 @@ members = [
"apps/music-player",
"apps/sandbox",
"crates/script",
"scripts/force-directed-graph",
"scripts/music-player",
"scripts/sao-ui",
]

View File

@ -0,0 +1,10 @@
[package]
name = "canary-force-directed-graph-script"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
canary-script = { path = "../../crates/script" }

View File

@ -0,0 +1,566 @@
use canary_script::api::{BindPanel, DrawContext, Font, Message, Panel, PanelImpl, TextLayout};
use canary_script::{export_abi, Color, CursorEventKind, MeshVertex, Vec2};
static LABEL_FONT: &str = "Liberation Sans";
pub struct Body {
pub position: Vec2,
pub velocity: Vec2,
pub acceleration: Vec2,
pub mass: f32,
pub friction: f32,
pub fixed: bool,
}
impl Default for Body {
fn default() -> Self {
Self {
position: Vec2::ZERO,
velocity: Vec2::ZERO,
acceleration: Vec2::ZERO,
friction: 0.001,
mass: 1.0,
fixed: false,
}
}
}
impl Body {
pub fn apply_force(&mut self, force: Vec2) {
self.acceleration += force / self.mass; // F = ma
}
pub fn apply_velocity(&mut self, vel: Vec2) {
self.velocity += vel;
}
pub fn update(&mut self, dt: f32) {
if self.fixed {
self.velocity = Vec2::ZERO;
return;
}
let pos = self.position;
let vel = self.velocity;
let acc = self.acceleration;
let new_pos = pos + vel * dt + acc * (dt * dt * 0.5);
let new_vel = vel + acc * (dt * 0.5);
self.position = new_pos;
self.velocity = new_vel * self.friction.powf(dt);
self.acceleration = Vec2::ZERO;
}
}
pub struct NodeInfo {
pub label: TextLayout,
pub label_offset: Vec2,
pub text_size: f32,
pub hidden: bool,
pub parent: Option<usize>,
pub unhide: Vec<usize>,
}
impl NodeInfo {
pub fn new(label: &str) -> Self {
let text_size = 5.0;
let label = TextLayout::new(&Font::new(LABEL_FONT), &label);
let bounds = label.get_bounds();
let label_offset = Vec2::new(-bounds.width() / 2.0, text_size / 2.0);
Self {
label,
label_offset,
text_size,
hidden: false,
parent: None,
unhide: Vec::new(),
}
}
}
pub struct Node {
pub body: Body,
pub info: NodeInfo,
}
#[derive(Debug, Clone)]
pub struct MenuNode {
pub label: String,
pub children: Vec<MenuNode>,
}
impl MenuNode {
pub fn make_test() -> Self {
Self {
label: "Menu".to_string(),
children: vec![
Self {
label: "File".to_string(),
children: vec![
Self {
label: "Open...".to_string(),
children: vec![],
},
Self {
label: "Save as...".to_string(),
children: vec![],
},
Self {
label: "Save".to_string(),
children: vec![],
},
Self {
label: "Quit".to_string(),
children: vec![],
},
],
},
Self {
label: "Edit".to_string(),
children: vec![
Self {
label: "Undo".to_string(),
children: vec![],
},
Self {
label: "Redo".to_string(),
children: vec![],
},
],
},
Self {
label: "View".to_string(),
children: vec![
Self {
label: "Zoom in".to_string(),
children: vec![],
},
Self {
label: "Zoom out".to_string(),
children: vec![],
},
Self {
label: "Reset zoom".to_string(),
children: vec![],
},
Self {
label: "Fullscreen".to_string(),
children: vec![],
},
],
},
Self {
label: "Help".to_string(),
children: vec![Self {
label: "About".to_string(),
children: vec![],
}],
},
],
}
}
pub fn make_force_graph(&self) -> ForceGraph {
let mut graph = ForceGraph::default();
let root = Node {
body: Body {
fixed: true,
..Default::default()
},
info: NodeInfo::new(&self.label),
};
let parent = graph.nodes.len();
graph.nodes.push(root);
self.build_force_graph(&mut graph, parent);
graph.unhide(parent);
graph
}
pub fn build_force_graph(&self, graph: &mut ForceGraph, parent: usize) {
let mut siblings = Vec::new();
let mut first_sibling = None;
let mut last_sibling = None;
const SIBLING_LENGTH: f32 = 60.0;
const SIBLING_STRENGTH: f32 = 0.1;
for child in self.children.iter() {
let id = graph.nodes.len();
let _ = first_sibling.get_or_insert(id);
graph.nodes.push(Node {
body: Body {
position: Vec2::from_angle(id as f32),
..Default::default()
},
info: NodeInfo {
hidden: true,
parent: Some(parent),
..NodeInfo::new(&child.label)
},
});
graph.springs.push(Constraint {
from: parent,
to: id,
length: 30.0,
strength: 1.0,
});
if let Some(last) = last_sibling {
graph.springs.push(Constraint {
from: last,
to: id,
length: SIBLING_LENGTH,
strength: SIBLING_STRENGTH,
});
}
siblings.push(id);
last_sibling = Some(id);
graph.connections.push(Connection {
from: parent,
to: id,
width: 1.0,
color: Color::WHITE,
});
child.build_force_graph(graph, id);
}
if let (Some(first), Some(last)) = (first_sibling, last_sibling) {
graph.springs.push(Constraint {
from: first,
to: last,
length: SIBLING_LENGTH,
strength: SIBLING_STRENGTH,
});
}
graph.nodes[parent].info.unhide.extend_from_slice(&siblings);
}
}
#[derive(Clone)]
pub struct Constraint {
pub from: usize,
pub to: usize,
pub length: f32,
pub strength: f32,
}
impl Constraint {
pub fn apply(&self, mut delta: Vec2) -> Vec2 {
const ALPHA: f32 = 0.99;
let distance = delta.length();
let displacement = distance - self.length;
delta *= displacement / distance * self.strength * ALPHA;
delta / 2.0
}
}
#[derive(Clone)]
pub struct Connection {
pub from: usize,
pub to: usize,
pub width: f32,
pub color: Color,
}
#[derive(Default)]
pub struct ForceGraph {
pub nodes: Vec<Node>,
pub springs: Vec<Constraint>,
pub connections: Vec<Connection>,
}
impl ForceGraph {
pub fn update(&mut self, dt: f32) {
self.apply_springs(dt);
self.apply_repulsion();
for node in self.nodes.iter_mut() {
if !node.info.hidden {
node.body.update(dt);
}
}
}
pub fn reaction(&self, from: usize, to: usize, delta: Vec2) -> (Vec2, Vec2) {
let from_fixed = self.nodes[from].body.fixed;
let to_fixed = self.nodes[to].body.fixed;
if from_fixed {
let from = Vec2::ZERO;
let to = if to_fixed { Vec2::ZERO } else { -delta };
(from, to)
} else if to_fixed {
let to = Vec2::ZERO;
let from = if from_fixed { Vec2::ZERO } else { delta };
(from, to)
} else {
let half_delta = delta / 2.0;
(half_delta, -half_delta)
}
}
pub fn with_unhidden<R>(
&self,
from: usize,
to: usize,
f: impl FnOnce(&Node, &Node) -> R,
) -> Option<R> {
let from = &self.nodes[from];
let to = &self.nodes[to];
if from.info.hidden || to.info.hidden {
None
} else {
Some(f(from, to))
}
}
pub fn apply_springs(&mut self, dt: f32) {
for spring in self.springs.iter() {
let from = spring.from;
let to = spring.to;
let vel = self.with_unhidden(from, to, |from, to| {
let from = from.body.position + from.body.velocity * dt;
let to = to.body.position + to.body.velocity * dt;
let delta = to - from;
spring.apply(delta)
});
if let Some(vel) = vel {
let (from_vel, to_vel) = self.reaction(from, to, vel);
self.nodes[from].body.apply_velocity(from_vel);
self.nodes[to].body.apply_velocity(to_vel);
}
}
}
pub fn apply_repulsion(&mut self) {
for i in 1..self.nodes.len() {
for j in i..self.nodes.len() {
let from = i - 1;
let to = j;
let force = self.with_unhidden(from, to, |from, to| {
const REPULSION_CONSTANT: f32 = 1000.0;
let delta = to.body.position - from.body.position;
let proximity = delta.length().max(0.1);
let force = -(REPULSION_CONSTANT / (proximity * proximity));
delta * force
});
if let Some(force) = force {
let (from_force, to_force) = self.reaction(from, to, force);
self.nodes[from].body.apply_force(from_force);
self.nodes[to].body.apply_force(to_force);
}
}
}
}
pub fn unhide(&mut self, node: usize) {
let node = &mut self.nodes[node];
node.info.hidden = false;
let position = node.body.position;
let children = node.info.unhide.to_owned();
// Avoid divide-by-zero
if children.is_empty() {
return;
}
let mut neighbor_num = children.len();
let mut rotate_by = Vec2::new(1.0, 0.0);
if let Some(parent) = node.info.parent {
neighbor_num += 1;
rotate_by = self.nodes[parent].body.position - position;
rotate_by = rotate_by.normalize();
}
let theta = std::f32::consts::TAU / neighbor_num as f32;
for (index, child) in children.into_iter().enumerate() {
let angle = Vec2::from_angle((index + 1) as f32 * theta).rotate(rotate_by);
let child = &mut self.nodes[child];
child.info.hidden = false;
child.body.fixed = false;
child.body.position = position + angle;
child.body.velocity = Vec2::ZERO;
}
}
pub fn hide(&mut self, node: usize) {
let node = &mut self.nodes[node];
node.info.hidden = true;
let children = node.info.unhide.to_owned();
for child in children {
self.hide(child);
}
}
pub fn on_select(&mut self, id: usize) {
let node = &mut self.nodes[id];
if !node.body.fixed {
node.body.fixed = true;
self.unhide(id);
} else {
node.body.fixed = false;
let children = node.info.unhide.to_owned();
for child in children {
self.hide(child);
}
}
}
pub fn draw(&self, ctx: &DrawContext) {
let radius = 5.0;
let thickness = 1.0;
let color = Color::WHITE;
let label_color = Color::WHITE;
for node in self.nodes.iter() {
if node.info.hidden {
continue;
}
ctx.draw_ring(node.body.position, radius, thickness, color);
let offset = node.body.position + node.info.label_offset;
ctx.draw_text_layout(&node.info.label, offset, node.info.text_size, label_color);
}
for connection in self.connections.iter() {
self.with_unhidden(connection.from, connection.to, |from, to| {
let from = from.body.position;
let to = to.body.position;
let delta = to - from;
if delta.length() < radius * 2.0 {
return;
}
let delta_norm = delta.normalize();
let from = from + delta_norm * radius;
let to = to - delta_norm * radius;
let delta_cross = Vec2::new(delta_norm.y, -delta_norm.x);
let delta_side = delta_cross * connection.width / 2.0;
let vertices = [
MeshVertex {
position: from + delta_side,
color,
},
MeshVertex {
position: from - delta_side,
color,
},
MeshVertex {
position: to + delta_side,
color,
},
MeshVertex {
position: to - delta_side,
color,
},
];
let indices = [0, 1, 2, 1, 2, 3];
ctx.draw_indexed(&vertices, &indices);
});
}
}
}
export_abi!(ForceDirectedGraphPanel);
pub struct ForceDirectedGraphPanel {
panel: Panel,
graph: ForceGraph,
center: Vec2,
last_focus: Vec2,
focus_anim: f32,
focus_target: usize,
size: Vec2,
}
impl BindPanel for ForceDirectedGraphPanel {
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
let menu = MenuNode::make_test();
let graph = menu.make_force_graph();
let panel = Self {
panel,
graph,
last_focus: Vec2::ZERO,
center: Vec2::ZERO,
focus_anim: 1.0,
focus_target: 0,
size: Vec2::splat(100.0),
};
Box::new(panel)
}
}
impl PanelImpl for ForceDirectedGraphPanel {
fn update(&mut self, dt: f32) {
self.graph.update(dt);
self.focus_anim += dt;
let target_pos = self.graph.nodes[self.focus_target].body.position;
let anim = self.focus_anim / 0.5;
if anim < 0.0 {
self.center = self.last_focus;
} else if anim < 1.0 {
let anim = -(anim * (anim - 2.0));
self.center = target_pos * anim + self.last_focus * (1.0 - anim);
} else {
self.center = target_pos;
}
}
fn draw(&mut self) {
let offset = self.size / 2.0 - self.center;
let ctx = DrawContext::new(self.panel).with_offset(offset);
self.graph.draw(&ctx);
}
fn on_resize(&mut self, new_size: Vec2) {
self.size = new_size;
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
let offset = self.size / 2.0 - self.center;
let at = at - offset;
if let CursorEventKind::Deselect = kind {
let node = self
.graph
.nodes
.iter()
.position(|node| !node.info.hidden && node.body.position.distance(at) < 6.0);
if let Some(node) = node {
self.graph.on_select(node);
self.last_focus = self.center;
self.focus_target = node;
self.focus_anim = 0.0;
}
}
}
fn on_message(&mut self, msg: Message) {}
}