Skip to content

Commit

Permalink
start thinking about layout
Browse files Browse the repository at this point in the history
  • Loading branch information
dankoster committed Oct 18, 2024
1 parent 483425e commit 0cc71e0
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 97 deletions.
154 changes: 97 additions & 57 deletions src/Connections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ import server from "./data"
// ✓ joining/leaving rooms
// ✓ rooms forming
// ✓ draw a continer around rooms
// - ...and label with room id
// - ...and a "tap to join"
// - constrain points to viewport (for now)
// ✓ ...and label with room id
// ✓ ...and a "tap to join"
// ✓ constrain points to viewport (for now)
// ✓ always show avatars (just dots for now)
// - zoom in on the user's room! https://observablehq.com/@d3/scatterplot-tour?collection=@d3/d3-zoom
//
// instead of drag, let each user move around!
// let each user move around?
// - shift the view to follow the user
// - proximity chat!
// - some kind of synthwave terrain for location awareness?

//https://observablehq.com/@d3/modifying-a-force-directed-graph

export default function ConnectionsGraph(props: { connections: Connection[] }) {
export default function ConnectionsGraph(props: { self: Connection, connections: Connection[] }) {

createEffect(() => {
trackStore(server.connections);
Expand Down Expand Up @@ -66,7 +67,7 @@ export default function ConnectionsGraph(props: { connections: Connection[] }) {
nodeIds: string[]
}
type GraphData = {
nodes: { id: string }[],
nodes: Connection[],
links: { source: string, target: string }[],
rooms: Room[]
}
Expand Down Expand Up @@ -114,65 +115,97 @@ export default function ConnectionsGraph(props: { connections: Connection[] }) {


svgObserver = new ResizeObserver(() => {
simulation?.force("x", d3.forceX(svgRef.clientWidth / 2))
.force("y", d3.forceY(svgRef.clientHeight / 2))
.alpha(1).restart();
updateForceLayout(simulation, null, !!props.self.roomId)
})
svgObserver.observe(svgRef)

simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-500))
.force("link", d3.forceLink().id(d => d.id).distance(100))
.force("x", d3.forceX(svgRef.clientWidth / 2))
.force("y", d3.forceY(svgRef.clientHeight / 2))
.on("tick", function ticked() {
nodeCircles?.attr("cx", d => d.x)
.attr("cy", d => d.y)

linkLines?.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y)

const enclosingCircleByRoomId = {}
roomCircles?.each(function (d) {
const circles = nodeCircles?._groups[0]
.filter(c => c.__data__.roomId === d.id) //find other nodes in the same room
.map(c => ({
x: c.cx.animVal.value,
y: c.cy.animVal.value,
r: c.r.animVal.value
}))
const circle = d3.packEnclose(circles)
enclosingCircleByRoomId[d.id] = circle
})
.attr("cx", d => enclosingCircleByRoomId[d?.id]?.x)
.attr("cy", d => enclosingCircleByRoomId[d?.id]?.y)
.attr('r', d => enclosingCircleByRoomId[d?.id]?.r + 20)

roomLabels?.each(function (d) {
const circle = enclosingCircleByRoomId[d.id]
const startAngle = -180 * Math.PI / 180
const endAngle = -180 * Math.PI / 180 + 2 * Math.PI
const anticlockwise = false
const path = d3.path()
path.arc(circle?.x, circle?.y, circle?.r + 30, startAngle, endAngle, anticlockwise)
circle.path = path?.toString()
})
updateForceLayout(simulation, [], !!props.self.roomId)

simulation.on("tick", function ticked() {
nodeCircles?.attr("cx", d => d.x)
.attr("cy", d => d.y)

linkLines?.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y)

const enclosingCircleByRoomId = {}
roomCircles?.each(function (d) {
const circles = nodeCircles?._groups[0]
.filter(c => c.__data__.roomId === d.id) //find other nodes in the same room
.map(c => ({
x: c.cx.animVal.value,
y: c.cy.animVal.value,
r: c.r.animVal.value
}))
const circle = d3.packEnclose(circles)
enclosingCircleByRoomId[d.id] = circle
})
.attr("cx", d => enclosingCircleByRoomId[d?.id]?.x)
.attr("cy", d => enclosingCircleByRoomId[d?.id]?.y)
.attr('r', d => enclosingCircleByRoomId[d?.id]?.r + 20)

roomLabels?.each(function (d) {
const circle = enclosingCircleByRoomId[d.id]
const startAngle = -180 * Math.PI / 180
const endAngle = -180 * Math.PI / 180 + 2 * Math.PI
const anticlockwise = false
const path = d3.path()
path.arc(circle?.x, circle?.y, circle?.r + 30, startAngle, endAngle, anticlockwise)
circle.path = path?.toString()
})

roomLabels?.selectAll("path")
.attr("d", d => enclosingCircleByRoomId[d?.id]?.path)
roomLabels?.selectAll("path")
.attr("d", d => enclosingCircleByRoomId[d?.id]?.path)

//HACK: the textPath doesn't re-draw when it's linked path changes
// so we reset the link to the path on every tick
roomLabels?.selectAll("textPath")
.attr("xlink:href", function (d) { return `#${this.previousElementSibling?.id}` })
});
//HACK: the textPath doesn't re-draw when it's linked path changes
// so we reset the link to the path on every tick
roomLabels?.selectAll("textPath")
.attr("xlink:href", function (d) { return `#${this.previousElementSibling?.id}` })
});

if (graph)
update(graph)
})

function updateForceLayout(sim, links, inRoom: boolean) {
try {
sim?.force("boundingBox", () => {
nodeCircles?.each(node => {
if (node.x < 30) node.x = 30
if (node.y < 30) node.y = 30
if (node.x > svgRef.clientWidth - 30) node.x = svgRef.clientWidth - 30
if (node.y > svgRef.clientHeight - 30) node.y = svgRef.clientHeight - 30
})
})

if (inRoom) {
sim?.force("x", d3.forceX(svgRef.clientWidth / 2))
.force("y", d3.forceY(svgRef.clientHeight / 2))
.force("charge", d3.forceManyBody().strength(500))
.force("link", null)
.force("collide", d3.forceCollide((d) => d.r || 50))
} else {
sim?.force("x", d3.forceX(svgRef.clientWidth / 2))
.force("y", d3.forceY(svgRef.clientHeight / 2))
.force("charge", d3.forceManyBody().strength(-500))
.force("link", d3.forceLink().id(d => d.id).distance(50))
.force("collide", d3.forceCollide((d) => d.r))
}

const linkForce = simulation?.force("link")
if (linkForce && Array.isArray(links) && links.length)
linkForce.links(links).distance(50); //https://d3js.org/d3-force/link

sim?.alpha(0.6).restart();
} catch (error) {
console.error(error)
}
}



function update(graphData: GraphData) {

if (!graphData) return
Expand All @@ -186,9 +219,16 @@ export default function ConnectionsGraph(props: { connections: Connection[] }) {
links = links?.map(d => Object.assign({}, d));
rooms = [...rooms]

//if we're in a call, only show ourself and the others in the call
if (props.self.roomId) {
nodes = nodes.filter(n => n.roomId === props.self.roomId)
links = []
rooms = []
}

updateForceLayout(simulation, links, !!props.self.roomId)

simulation?.nodes(nodes);
simulation?.force("link").links(links).distance(50); //https://d3js.org/d3-force/link
simulation?.alpha(1).restart();

nodeCircles = nodeCircles?.data(nodes, d => d.id)
.join(
Expand Down
2 changes: 0 additions & 2 deletions src/VideoCall.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@

.local-video-container {
position: relative;
padding: 1rem;
}
.remote-video-container {
position: relative;
padding: 1rem;
}

video {
Expand Down
13 changes: 7 additions & 6 deletions src/VideoCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,13 @@ export default function VideoCall(props: { room: Room, user: Connection, connect
})

return <div class="video-call">
<div class="local-video-container">
<video class="local" ref={localVideo} autoplay playsinline></video>
</div>
<div id="remote-videos" class="remote-video-container">
</div>
<Show when={props.connections?.length === 0}>
<Show when={props.connections?.length > 0}>
<div class="local-video-container">
<video class="local" ref={localVideo} autoplay playsinline></video>
</div>
<div id="remote-videos" class="remote-video-container" />
</Show>
<Show when={props.room && props.connections?.length === 0}>
waiting for connections...
</Show>
{/* <div class="connections">
Expand Down
28 changes: 22 additions & 6 deletions src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ div#root {
padding-left: 2rem;
text-wrap: nowrap;
touch-action: none;
position: relative;
}

.logo::after {
content: "chat with people, not ai";
position: absolute;
font-size: x-small;
bottom: -0.5rem;
right: 0;
color: #ffffff99;
}

.stats {
Expand All @@ -62,11 +72,17 @@ div#root {
grid-template-rows: 1fr min-content;
grid-auto-flow: row;

.them {
.middle {
display: grid;
grid-template-rows: 1fr;
grid-template-rows: 1fr 0fr;
transition: 500ms;

&.room {
grid-template-rows: 60px 1fr;
}
}


.toolbar {
padding: 1rem;
border-top: 1px solid;
Expand Down Expand Up @@ -100,10 +116,10 @@ div#root {
}

.public-info {
flex-grow: 1;
display: flex;
align-items: center;
gap: 1rem;
flex-grow: 1;
display: flex;
align-items: center;
gap: 1rem;
}

.buttons {
Expand Down
39 changes: 13 additions & 26 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "./index.css"
import "./main.css"

import { For, Match, onMount, Show, Switch } from "solid-js";
import { createMemo, For, Match, onMount, Show, Switch } from "solid-js";
import { render } from "solid-js/web";
import server from "./data";
import type { Connection, Room } from "../server/api";
Expand Down Expand Up @@ -41,36 +41,22 @@ function User(props: { con: Connection }) {
console.log('...exit room', response.ok)
}

const room = createMemo(() => server.rooms.find(r => r.id === props.con.roomId))

return <div class="user-view">
<div class="them">
<Show when={props.con.roomId}>
<VideoCall
user={props.con}
room={server.rooms.find(r => r.id === props.con.roomId)}
connections={server.connections.filter(sc => sc.id != props.con.id && sc.roomId === props.con.roomId)} />
</Show>
<div class="middle" classList={{ "room" : !!room() }}>
<ConnectionsGraph self={props.con} connections={server.connections} />
<VideoCall
user={props.con}
room={room()}
connections={server.connections.filter(sc => props.con.roomId && sc.id != props.con.id && sc.roomId === props.con.roomId)} />
{/* <Show when={props.con.roomId}>
</Show> */}

<Switch>
{/* NOT IN A ROOM */}
<Match when={!props.con.roomId}>
<ConnectionsGraph connections={server.connections} />
{/* <Rooms rooms={server.rooms.filter(room => room.ownerId !== props.con.id)} /> */}
{/* <Connections connections={server.connections.filter(con => con.id !== server.id() && !con.roomId)} /> */}
</Match>
{/* CREATED A ROOM */}
{/* <Match when={server.rooms.some(room => room.ownerId === props.con.id)}>
<Connections connections={server.connections.filter(sc => sc.id != props.con.id && sc.roomId === props.con.roomId)} />
</Match> */}
{/* JOINED A ROOM */}
{/* <Match when={server.rooms.some(room => room.ownerId !== props.con.id)}>
<Connections connections={server.connections.filter(sc => sc.id != props.con.id && sc.roomId === props.con.roomId)} />
</Match> */}
</Switch>
</div>
{/* style={{ "background-color": props.con.color }} */}
{/* <RoomLabel con={props.con} /> */}
<div class="toolbar">
{/* <div class="public-info">
{/* <div class="public-info">
<input
type="text"
maxlength="123"
Expand All @@ -79,6 +65,7 @@ function User(props: { con: Connection }) {
onfocus={(e) => e.target.setSelectionRange(0, e.target.value.length)}
value={props.con.text ?? ''} />
</div> */}
<div class="toolbar">
<div class="buttons">
<div class="color-button">
<span>color</span>
Expand Down

0 comments on commit 0cc71e0

Please sign in to comment.