diff --git a/src/views/dash/circlepacker.ts b/src/views/dash/circlepacker.ts index ba992c3..f4ab504 100644 --- a/src/views/dash/circlepacker.ts +++ b/src/views/dash/circlepacker.ts @@ -1,96 +1,120 @@ import { busSigns, getArrivals } from './trimet' + const membersDiv = document.getElementById('members')! +const DELTA_AVG = 0.25 + export class Vector2D { x: number y: number + constructor(x = 0, y = 0) { this.x = x this.y = y } + getDistanceFrom(vector: Vector2D) { return Math.sqrt((this.x - vector.x) ** 2 + (this.y - vector.y) ** 2) } + normalized() { const divisor = new Vector2D().getDistanceFrom(this) + return new Vector2D(this.x / divisor, this.y / divisor) } + scaled(scalar: number) { return new Vector2D(this.x * scalar, this.y * scalar) } + added(vector: Vector2D) { return new Vector2D(this.x + vector.x, this.y + vector.y) } } + export abstract class Circle { position: Vector2D velocity: Vector2D acceleration: Vector2D + r: number + readonly element: HTMLElement + constructor(r: number) { this.element = membersDiv.appendChild(document.createElement('name')) + this.r = r this.position = new Vector2D() this.acceleration = new Vector2D() this.velocity = new Vector2D() } + get mass() { return Math.PI * this.r ** 2 // return 200; } + get charge() { return this.r ** 2 / 5 } + destroy() { this.element.remove() } + abstract updateSize(): void } + export class MemberCircle extends Circle { loginTime: number name: string email: string + constructor(loginTime: number, email: string, name: string, imgurl: string) { super(Math.sqrt(0.2) * 10) + this.loginTime = loginTime this.email = email this.element.id = email this.element.style.backgroundImage = `url(${imgurl})` this.element.className = 'memberCircle' + const nameBubble = this.element.appendChild(document.createElement('name')) this.name = nameBubble.innerHTML = name nameBubble.className = 'bubblename' nameBubble.style.backgroundColor = BUBBLE_COLORS[Math.floor(Math.random() * BUBBLE_COLORS.length)] // name.style.fontSize = `${Math.min(30*multiplier, 20)}px`; - nameBubble.style.fontSize = '25px' + nameBubble.style.fontSize = '2.5vh' } + updateSize() { this.r = Math.sqrt((Date.now() - this.loginTime) / 360000 + 0.2) * 20 } } + export class ClockCircle extends Circle { constructor() { super(Math.sqrt(0.2) * 10) @@ -98,13 +122,13 @@ export class ClockCircle extends Circle { this.element.innerHTML = `
- +
11:35
- +
@@ -116,12 +140,14 @@ export class ClockCircle extends Circle {
-- min
-- min
+
Beaverton
-- min
-- min
+
` @@ -148,6 +174,7 @@ export class ClockCircle extends Circle { timetext.classList.remove('timeSmallerText') } + await updateBusElements([...document.querySelectorAll('.bustime.east')], busSigns.east) await updateBusElements([...document.querySelectorAll('.bustime.west')], busSigns.west) } @@ -155,19 +182,24 @@ export class ClockCircle extends Circle { setInterval(updateBusTimes, 15000) } + updateSize() { this.r = 20 - this.r = Math.max(...placedCircles.map((circle) => circle.r * 1.2)) + this.r = Math.max(...placedCircles.map((circle) => circle.r * 1.5)) } } + export let placedCircles: Circle[] = [] + const BUBBLE_COLORS = ['rgba(35,132,198,.5)', 'rgba(255,214,0,.5)', 'rgba(241,93,34,.5)', 'rgba(108,157,204,.5)'] const FORCE_MULTIPLIER = 0.1 const FRICTION = 0.8 const TIME_SCALE = 1 -const MARGIN = 1 +const MARGIN = 0.5 +const SNAP_DISTANCE = 20 + export function getBounds() { if (placedCircles.length == 0) { @@ -179,6 +211,7 @@ export function getBounds() { } } + const bounds = { minX: Infinity, maxX: -Infinity, @@ -186,6 +219,7 @@ export function getBounds() { maxY: -Infinity } + for (const circle of placedCircles) { bounds.maxX = Math.max(circle.position.x + circle.r, bounds.maxX) bounds.minX = Math.min(circle.position.x - circle.r, bounds.minX) @@ -193,25 +227,32 @@ export function getBounds() { bounds.minY = Math.min(circle.position.y - circle.r, bounds.minY) } + return bounds } + let aspectRatio = 1 function updateAspectRatio() { aspectRatio = membersDiv.clientWidth / membersDiv.clientHeight } + const BOUNDARY_FIELD = 0.005 + export function applyBoundaryForce(circle: Circle) { const acceleration = circle.position.scaled((-circle.charge / circle.mass) * BOUNDARY_FIELD * FORCE_MULTIPLIER) + acceleration.y *= aspectRatio + circle.acceleration = acceleration.scaled(1 / (3 + 2 ** acceleration.getDistanceFrom(new Vector2D()))) // .scaled(1/Math.sqrt(acceleration.getDistanceFrom(new Vector2D()))); } + export function updateCircleList(loggedIn: Record): string[] { const filled: Record = {} placedCircles = placedCircles.filter((circle) => { @@ -225,38 +266,60 @@ export function updateCircleList(loggedIn: Record): string[] { return true }) + return Object.keys(loggedIn).filter((email) => !filled[email]) } + export function placeCircles(circles: Circle[]) { circles = circles.sort((circleA, circleB) => circleB.r - circleA.r) + const bounds = getBounds() + const newPlacedCircles = [] let maxNewBoundSpace = Math.max(...circles.map((circle) => circle.r)) + for (const circle of circles) { const offsetX = Math.random() * maxNewBoundSpace + circle.r const offsetY = Math.random() * maxNewBoundSpace + circle.r + circle.position.x = Math.random() > 0.5 ? offsetX + bounds.maxX : bounds.minX - offsetX circle.position.y = Math.random() > 0.5 ? offsetY + bounds.maxY : bounds.minY - offsetY + newPlacedCircles.push(circle) maxNewBoundSpace += circle.r } + placedCircles.push(...newPlacedCircles) } + export function updateCircles(time: number) { - if (time > 100) return + if (time > 1000) return + placedCircles.forEach((circle) => circle.updateSize()) + + const sortedCircles = placedCircles.sort((a, b) => a.r - b.r) + + + let sizeSum = 0 + + + for(const circle of sortedCircles) + circle.r -= (circle.r - (sizeSum += circle.r)/(sortedCircles.length+1)) * DELTA_AVG + + time *= TIME_SCALE + const center = new Vector2D( placedCircles.map((circle) => circle.position.x).reduce((sum, r) => sum + r, 0) / placedCircles.length, placedCircles.map((circle) => circle.position.y).reduce((sum, r) => sum + r, 0) / placedCircles.length @@ -270,23 +333,31 @@ export function updateCircles(time: number) { .added(circle.acceleration.scaled((FRICTION * time ** 2) / 2)) .added(center) + circle.velocity = circle.velocity.added(circle.acceleration.scaled(time)).scaled(FRICTION) + applyBoundaryForce(circle) }) + for (let circleIndex = 0; circleIndex < placedCircles.length; circleIndex++) { const circle = placedCircles[circleIndex] + for (let secondaryIndex = circleIndex + 1; secondaryIndex < placedCircles.length; secondaryIndex++) { const otherCircle = placedCircles[secondaryIndex] + const distance = circle.position.getDistanceFrom(otherCircle.position) + const force = ((circle.charge * otherCircle.charge) / distance ** 2) * FORCE_MULTIPLIER + const forceDirection = new Vector2D(otherCircle.position.x - circle.position.x, otherCircle.position.y - circle.position.y).normalized() + circle.acceleration.x -= (force * forceDirection.x) / circle.mass circle.acceleration.y -= (force * forceDirection.y) / circle.mass otherCircle.acceleration.x -= (force * -forceDirection.x) / otherCircle.mass @@ -295,28 +366,92 @@ export function updateCircles(time: number) { } } -export function sizeCircles() { - const { maxX, maxY, minX, minY } = getBounds() + +let renderedCircles: { circle: Circle; top: any; left: any; dia: any }[] = []; + + +export async function sizeCircles() { + const sizedCircles = []; + for(const circle of placedCircles) { + let minScale = Infinity; + + + placedCircles.forEach(otherCircle => { + if(otherCircle == circle) + return Infinity; + minScale = Math.min(circle.position.getDistanceFrom(otherCircle.position) / (circle.r + otherCircle.r), minScale); + }); + + + sizedCircles.push({ + circle, + radius : circle.r * (minScale == Infinity ? 1 : minScale) + }); + } + + + let maxX = 0, maxY = 0, minX = 0, minY = 0; + + + sizedCircles.forEach(circle => { + maxX = Math.max(circle.circle.position.x + circle.radius, maxX); + minX = Math.min(circle.circle.position.x - circle.radius, minX); + maxY = Math.max(circle.circle.position.y + circle.radius, maxY); + minY = Math.min(circle.circle.position.y - circle.radius, minY); + }); + + const widthMult = membersDiv.clientWidth / membersDiv.clientHeight + const lengthYX = (maxY - minY) * widthMult const lengthX = maxX - minX + const vwWidth = (membersDiv.clientWidth / window.innerWidth) * 100 + const multiplier = vwWidth / (lengthYX > lengthX ? lengthYX : lengthX) + const offsetX = (vwWidth - lengthX * multiplier) / 2 const offsetY = (vwWidth - lengthYX * multiplier) / 2 - for (const circle of placedCircles) { - const elem = circle.element - const radius = circle.r * 2 * multiplier - MARGIN - elem.style.width = elem.style.height = `${radius}vw` - elem.style.left = `${(circle.position.x - minX) * multiplier + offsetX - radius / 2}vw` - elem.style.top = `${(circle.position.y - minY) * multiplier + offsetY - radius / 2}vw` + let snapCircles = false; + for(const circle of sizedCircles) { + const elem = circle.circle.element; - if (circle instanceof ClockCircle) circle.element.style.fontSize = Math.min(radius) + 'vw' + + let diameter = circle.radius * multiplier * 2 - MARGIN; + + + elem.style.width = elem.style.height = `${diameter}vw`; + + + elem.style.left = `${(circle.circle.position.x - minX) * multiplier + offsetX - diameter/2}vw`; + elem.style.top = `${(circle.circle.position.y - minY) * multiplier + offsetY - diameter/2}vw`; + + let rendered = renderedCircles.find(renderedCircle => circle.circle == renderedCircle.circle); + const computedStyle = elem.computedStyleMap(); + if(rendered == undefined) { + renderedCircles.push({ + circle : circle.circle, + // @ts-ignore + top : computedStyle.get("top").value, + // @ts-ignore + left : computedStyle.get("left").value, + // @ts-ignore + dia : computedStyle.get("width").value + }) + } else { + // @ts-ignore + if(!(Math.abs(computedStyle.get("top").value - rendered.top) < SNAP_DISTANCE && Math.abs(computedStyle.get("left").value - rendered.left) < SNAP_DISTANCE)) { + snapCircles = true; + } + } } } + + + diff --git a/src/views/dash/index.ts b/src/views/dash/index.ts index 289ee43..70d5dc5 100644 --- a/src/views/dash/index.ts +++ b/src/views/dash/index.ts @@ -5,22 +5,28 @@ import { getLoggedIn, getMemberList } from '~views/grid/clockapi' import { APIMember, WSCluckChange } from '~types' import socket_io from 'socket.io-client' + let members: Record let loggedInCache: Record = {} + window['openFullscreen'] = openFullscreen + setTimeout(cyclePanel) setInterval(cyclePanel, 1000 * 60) // chnage panel every 1 minutes setInterval(populateCircles, 50) // refresh circles at 20Hz + let prevTime = Date.now() + function populateCircles() { const membersToAdd = updateCircleList(loggedInCache) const circlesToAdd = membersToAdd.map((entry) => { const member = members[entry] + return new MemberCircle( loggedInCache[entry].getTime(), // 1000 / 60 / 60 member.email, @@ -30,12 +36,14 @@ function populateCircles() { }) placeCircles(circlesToAdd) + const now = Date.now() updateCircles(now - prevTime) sizeCircles() prevTime = now } + async function update() { try { const loggedIn = await getLoggedIn() @@ -49,6 +57,7 @@ async function update() { } } + async function start() { members = {} const memberlist = await getMemberList() @@ -65,6 +74,7 @@ async function start() { }) } + const socket = socket_io({ path: '/ws' }) socket.on('cluck_change', (data: WSCluckChange) => { if (data.logging_in) { @@ -74,6 +84,7 @@ socket.on('cluck_change', (data: WSCluckChange) => { } }) + socket.on('disconnect', () => { document.getElementById('logo')!.style.display = 'none' document.body.style.backgroundColor = 'red' @@ -83,4 +94,8 @@ socket.on('connect', () => { document.body.style.backgroundColor = 'black' }) + setTimeout(start) + + +