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
59 changes: 34 additions & 25 deletions src/app/(main)/community/events/map/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type BootOptions = {
aspectRatio: number
theme: MapColors
signal?: AbortSignal
onActiveMarkerChange?: (id: string | null) => void
onActiveMarkerChange?: (ids: string[]) => void
}

const MIN_ZOOM = 1
Expand Down Expand Up @@ -109,14 +109,15 @@ class MapEngine implements MapHandle {
private readonly markerData: Float32Array
private markerCount: number
private markerCapacityWarned = false
private readonly sharedMarkerColor: Float32Array
private readonly markerColor: Float32Array
private haloColor: Float32Array
private haloMinOpacity: number
private readonly markerIntensity: Float32Array
private readonly markerIntensityTarget: Float32Array
private readonly markerIndexById: Map<string, number>
private activeMarkerIndex = -1
private hoveredMarkerIndex = -1
private hoveredMarkerIndices: number[] = []
private markerUniformDirty = true
private readonly resizeObserver: ResizeObserver
private readonly diagnostics: Diagnostics | null
Expand Down Expand Up @@ -144,7 +145,7 @@ class MapEngine implements MapHandle {
}
private readonly pointerTrail = new Float32Array(192)
private trailCount = 0
private readonly onActiveMarkerChange?: (id: string | null) => void
private readonly onActiveMarkerChange?: (ids: string[]) => void
private destroyed = false

constructor(options: InternalOptions) {
Expand All @@ -161,6 +162,7 @@ class MapEngine implements MapHandle {
this.markerPoints = options.markers
this.markerData = new Float32Array(MARKER_CAPACITY * 4)
this.markerCount = this.packMarkers(this.markerPoints, this.markerData)
this.sharedMarkerColor = new Float32Array(options.theme.sharedMarker)
this.markerColor = new Float32Array(options.theme.marker)
this.haloColor = new Float32Array(options.theme.halo)
this.haloMinOpacity = options.theme.haloMinOpacity
Expand Down Expand Up @@ -204,6 +206,7 @@ class MapEngine implements MapHandle {
setThemeColors(colors: MapColors) {
this.seaColor.set(colors.sea)
this.landColor.set(colors.land)
this.sharedMarkerColor.set(colors.sharedMarker)
this.markerColor.set(colors.marker)
this.haloColor.set(colors.halo)
this.haloMinOpacity = colors.haloMinOpacity
Expand Down Expand Up @@ -308,14 +311,14 @@ class MapEngine implements MapHandle {
if (!this.onActiveMarkerChange) return
const device = this.pointerToDevice(clientX, clientY)
if (!device) {
this.notifyHoverChange(-1)
this.notifyHoverChange([])
return
}
const [px, py] = device
const dims = this.getWorldDimensions()
const deviceCell = this.cellSize * this.pixelRatio
if (!(deviceCell > 0)) {
this.notifyHoverChange(-1)
this.notifyHoverChange([])
return
}
const cellX = Math.floor(px / deviceCell)
Expand All @@ -324,21 +327,21 @@ class MapEngine implements MapHandle {
const centerY = (cellY + 0.5) * deviceCell
const zoomedHeight = dims.worldHeight * this.zoom
if (!(zoomedHeight > 0)) {
this.notifyHoverChange(-1)
this.notifyHoverChange([])
return
}
const normalizedY = (centerY - this.pan[1]) / zoomedHeight
if (normalizedY < 0 || normalizedY > 1) {
this.notifyHoverChange(-1)
this.notifyHoverChange([])
return
}
const periodX = dims.worldWidth * this.zoom
if (!(periodX > 0 && Number.isFinite(periodX))) {
this.notifyHoverChange(-1)
this.notifyHoverChange([])
return
}
const halfPeriod = 0.5 * periodX
let foundIndex = -1
const foundIndices: number[] = []
for (let i = 0; i < this.markerCount; i++) {
const base = i * 4
const markerX = this.markerData[base]
Expand All @@ -351,30 +354,28 @@ class MapEngine implements MapHandle {
const markerCellX = Math.floor(nearestX / deviceCell)
const markerCellY = Math.floor(screenY / deviceCell)
if (markerCellX === cellX && markerCellY === cellY) {
foundIndex = i
break
foundIndices.push(i)
}
}
this.notifyHoverChange(foundIndex)
this.notifyHoverChange(foundIndices)
}

private notifyHoverChange(index: number) {
if (index === this.hoveredMarkerIndex) return
this.hoveredMarkerIndex = index
private notifyHoverChange(indices: number[]) {
if (
indices.length === this.hoveredMarkerIndices.length &&
indices.every((index, i) => index === this.hoveredMarkerIndices[i])
)
return
this.hoveredMarkerIndices = indices
this.updateCursor()
if (!this.onActiveMarkerChange) return
if (this.destroyed) return
const id =
index >= 0 && index < this.markerPoints.length
? this.markerPoints[index].id
: null
this.onActiveMarkerChange(id)
if (!this.onActiveMarkerChange || this.destroyed) return
this.onActiveMarkerChange(indices.map(index => this.markerPoints[index].id))
}

private updateCursor() {
if (this.pointer.active) return
this.canvas.style.cursor =
this.hoveredMarkerIndex >= 0 ? "pointer" : "default"
this.hoveredMarkerIndices.length > 0 ? "pointer" : "default"
}

private updatePointerTrail(px: number, py: number) {
Expand Down Expand Up @@ -479,7 +480,7 @@ class MapEngine implements MapHandle {
this.velocity[1] = 0
this.canvas.setPointerCapture(event.pointerId)
this.canvas.style.cursor = "move"
this.notifyHoverChange(-1)
this.notifyHoverChange([])
this.pointerTrail.fill(-1)
this.trailCount = 0
}
Expand Down Expand Up @@ -529,7 +530,7 @@ class MapEngine implements MapHandle {
private handlePointerUp = (event: PointerEvent) => {
if (event.type === "pointerleave" || event.type === "pointercancel") {
this.hoverPointer.hasValue = false
this.notifyHoverChange(-1)
this.notifyHoverChange([])
this.pointerTrail.fill(-1)
this.trailCount = 0
}
Expand Down Expand Up @@ -754,6 +755,14 @@ class MapEngine implements MapHandle {
this.markerColor[1],
this.markerColor[2],
)
setUniform3f(
gl,
this.dotsProgram,
"uSharedMarkerColor",
this.sharedMarkerColor[0],
this.sharedMarkerColor[1],
this.sharedMarkerColor[2],
)
setUniform3f(
gl,
this.dotsProgram,
Expand Down
3 changes: 3 additions & 0 deletions src/app/(main)/community/events/map/map-colors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type MapColors = {
sea: ColorVec3
land: ColorVec3
marker: ColorVec3
sharedMarker: ColorVec3
halo: ColorVec3
haloMinOpacity: number
}
Expand All @@ -13,13 +14,15 @@ export const MAP_COLORS = {
sea: [0.9804, 0.9882, 0.9569], // neu-50
land: [0.8627, 0.8706, 0.8275], // neu-300
marker: [0.8824, 0.0039, 0.5961], // #E10198 = pri-base
sharedMarker: [0.6, 0, 0.41], // pri-dark
halo: [1.0, 0.6, 0.8784], // hsl(318, 100%, 80%) = pri-light
haloMinOpacity: 0.75,
},
dark: {
sea: [0.0549, 0.0588, 0.0431], // neu-50
land: [0.1647, 0.1804, 0.1373], // a shade darker than neu-800
marker: [1, 0.6, 0.8745], // #FF99DF = pri-light
sharedMarker: [1, 0.8, 0.9367], // pri-lighter
halo: [1.0, 0.8, 0.9373], // hsl(319, 100%, 90%) = pri-lighter
haloMinOpacity: 0.5,
},
Expand Down
17 changes: 9 additions & 8 deletions src/app/(main)/community/events/map/map-tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@

import { meetups } from "@/components/meetups"

const meetupNameById = new Map(meetups.map(({ node }) => [node.id, node.name]))
const meetupCityById = new Map(meetups.map(({ node }) => [node.id, node.city]))

type MapTooltipProps = {
id: string
activeMeetupId: string | null
activeMeetupIds: string[]
}

export function MapTooltip({ id, activeMeetupId }: MapTooltipProps) {
const name = activeMeetupId && meetupNameById.get(activeMeetupId)
export function MapTooltip({ id, activeMeetupIds }: MapTooltipProps) {
return (
<span
id={id}
role="tooltip"
className="pointer-events-none absolute left-0 top-0 z-10 hidden min-w-0 whitespace-nowrap border border-neu-200/40 bg-neu-0/40 px-2 py-[3px] text-xs text-neu-900 shadow-sm backdrop-blur-sm group-hover/map:flex"
className="pointer-events-none absolute left-0 top-0 z-10 hidden min-w-0 flex-col whitespace-nowrap border border-neu-200/40 bg-neu-0/40 px-2 py-[3px] text-xs text-neu-900 shadow-sm backdrop-blur-sm group-hover/map:flex"
style={{
transform: `translate3d(calc(var(--x) - 50%), calc(var(--y) - 50% - 22px), 0)`,
visibility: activeMeetupId ? "visible" : "hidden",
transform: `translate3d(calc(var(--x) - 50%), calc(var(--y) - 100% - 12px), 0)`,
visibility: activeMeetupIds.length > 0 ? "visible" : "hidden",
}}
>
{name}
{activeMeetupIds.map(meetupId => (
<span key={meetupId}>{meetupCityById.get(meetupId)}</span>
))}
</span>
)
}
9 changes: 6 additions & 3 deletions src/app/(main)/community/events/map/shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ uniform vec3 uSeaColor;
uniform vec4 uMarkers[${MARKER_CAPACITY}];
uniform int uMarkerCount;
uniform vec3 uMarkerColor;
uniform vec3 uSharedMarkerColor;
uniform vec3 uHaloColor;
uniform float uHaloMinOpacity;
uniform vec3 uPointerTrail[64];
Expand All @@ -50,6 +51,7 @@ vec2 markerCellCenter(vec4 marker, vec2 referencePx) {
}

vec2 markerStateAtCellCenterPx(vec2 cellCenterPx) {
vec2 state = vec2(0.0);
ivec2 cellIndex = ivec2(floor(cellCenterPx / uCell));
for (int i = 0; i < ${MARKER_CAPACITY}; i++) {
if (i >= uMarkerCount) {
Expand All @@ -59,10 +61,11 @@ vec2 markerStateAtCellCenterPx(vec2 cellCenterPx) {
vec2 markerCenter = markerCellCenter(marker, cellCenterPx);
ivec2 markerIndex = ivec2(floor(markerCenter / uCell));
if (markerIndex.x == cellIndex.x && markerIndex.y == cellIndex.y) {
return vec2(marker.z, marker.w);
state.x = max(state.x, marker.z);
state.y += 1.0;
}
}
return vec2(0.0);
return state;
}

float sampleCoverage(vec2 uv) {
Expand Down Expand Up @@ -121,7 +124,7 @@ void main() {
vec3 terrainColor = mix(uSeaColor, uLandColor, landMask);
vec3 color = terrainColor;
if (markerType > 0.5) {
color = uMarkerColor;
color = markerState.y > 1.0 ? uSharedMarkerColor : uMarkerColor;
}
float squareHalf = baseHalfSquare;
if (markerType <= 0.5) {
Expand Down
10 changes: 5 additions & 5 deletions src/app/(main)/community/events/meetups-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const DEFAULT_MEETUPS: MeetupListItem[] = meetupNodes

export interface MeetupsListProps {
className?: string
activeMeetupId?: string | null
activeMeetupIds?: string[]
onActiveMeetupChange?: (meetupId: string | null) => void
meetups?: MeetupListItem[]
}
Expand All @@ -42,12 +42,12 @@ export interface MeetupsListProps {
*/
export function MeetupsList({
className,
activeMeetupId,
activeMeetupIds = [],
onActiveMeetupChange,
meetups = DEFAULT_MEETUPS,
}: MeetupsListProps) {
useEffect(() => {
if (activeMeetupId) {
if (activeMeetupIds.length > 0) {
const list = document.querySelector("#meetups-list")
if (!list) return

Expand All @@ -60,7 +60,7 @@ export function MeetupsList({
list.scrollTo({ top: activeAnchor.offsetTop, behavior: "smooth" })
}
}
}, [activeMeetupId])
}, [activeMeetupIds])

if (meetups.length === 0) return null

Expand All @@ -73,7 +73,7 @@ export function MeetupsList({
)}
>
{meetups.map((meetup, index) => {
const isActive = meetup.id === activeMeetupId
const isActive = activeMeetupIds.includes(meetup.id)

return (
<li key={meetup.id}>
Expand Down
17 changes: 9 additions & 8 deletions src/app/(main)/community/events/meetups-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export function MeetupsMap() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const handleRef = useRef<MapHandle>()
const { resolvedTheme } = useTheme()
const [activeMeetupId, setActiveMeetupId] = useState<string | null>(null)
const [activeMeetupIds, setActiveMeetupIds] = useState<string[]>([])
const activeMeetupId = activeMeetupIds[0] ?? null
const themeColors = useMemo(
() => MAP_COLORS[resolvedTheme as ThemeVariant] || MAP_COLORS.light,
[resolvedTheme],
Expand Down Expand Up @@ -69,7 +70,7 @@ export function MeetupsMap() {
)

const handleMapClick = () => {
if (activeMeetup?.node.link) {
if (activeMeetupIds.length === 1 && activeMeetup?.node.link) {
window.open(activeMeetup.node.link, "_blank", "noopener,noreferrer")
}
}
Expand Down Expand Up @@ -102,7 +103,7 @@ export function MeetupsMap() {
aspectRatio: ASPECT_RATIO,
theme: initialThemeRef.current,
signal: abortController.signal,
onActiveMarkerChange: setActiveMeetupId,
onActiveMarkerChange: setActiveMeetupIds,
})
if (disposed) {
engine.dispose()
Expand Down Expand Up @@ -141,8 +142,8 @@ export function MeetupsMap() {
return (
<div
id="meetups-map"
onMouseOut={() => {
setActiveMeetupId(null)
onMouseLeave={() => {
setActiveMeetupIds([])
}}
className="my-6 flex flex-row-reverse divide-neu-200 border border-neu-200 bg-[--sea] [--sea:--sea-light] dark:divide-neu-50 dark:border-neu-50 dark:[--sea:--sea-dark] max-md:flex-col max-md:divide-y md:h-[592px]"
style={
Expand Down Expand Up @@ -172,7 +173,7 @@ export function MeetupsMap() {
}}
/>

<MapTooltip id="map-tooltip" activeMeetupId={activeMeetupId} />
<MapTooltip id="map-tooltip" activeMeetupIds={activeMeetupIds} />

<InfoTip />

Expand All @@ -185,8 +186,8 @@ export function MeetupsMap() {
)}
</div>
<MeetupsList
activeMeetupId={activeMeetupId}
onActiveMeetupChange={setActiveMeetupId}
activeMeetupIds={activeMeetupIds}
onActiveMeetupChange={id => setActiveMeetupIds(id ? [id] : [])}
className="shrink-0 md:max-h-full lg:w-[240px]"
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/day/events-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function EventsMap({ children }: { children: React.ReactNode }) {
aspectRatio: ASPECT_RATIO,
theme: initialThemeRef.current,
signal: abortController.signal,
onActiveMarkerChange: setActiveEventId,
onActiveMarkerChange: ids => setActiveEventId(ids[0] ?? null),
})
if (disposed) {
engine.dispose()
Expand Down
Loading
Loading