diff --git a/src/app/(main)/community/events/map/engine.ts b/src/app/(main)/community/events/map/engine.ts index 12260c2d33..abf2ad02f5 100644 --- a/src/app/(main)/community/events/map/engine.ts +++ b/src/app/(main)/community/events/map/engine.ts @@ -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 @@ -109,6 +109,7 @@ 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 @@ -116,7 +117,7 @@ class MapEngine implements MapHandle { private readonly markerIntensityTarget: Float32Array private readonly markerIndexById: Map private activeMarkerIndex = -1 - private hoveredMarkerIndex = -1 + private hoveredMarkerIndices: number[] = [] private markerUniformDirty = true private readonly resizeObserver: ResizeObserver private readonly diagnostics: Diagnostics | null @@ -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) { @@ -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 @@ -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 @@ -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) @@ -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] @@ -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) { @@ -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 } @@ -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 } @@ -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, diff --git a/src/app/(main)/community/events/map/map-colors.tsx b/src/app/(main)/community/events/map/map-colors.tsx index 950c403f5d..c40f395258 100644 --- a/src/app/(main)/community/events/map/map-colors.tsx +++ b/src/app/(main)/community/events/map/map-colors.tsx @@ -4,6 +4,7 @@ export type MapColors = { sea: ColorVec3 land: ColorVec3 marker: ColorVec3 + sharedMarker: ColorVec3 halo: ColorVec3 haloMinOpacity: number } @@ -13,6 +14,7 @@ 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, }, @@ -20,6 +22,7 @@ export const MAP_COLORS = { 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, }, diff --git a/src/app/(main)/community/events/map/map-tooltip.tsx b/src/app/(main)/community/events/map/map-tooltip.tsx index b73623a5fc..9573a90b63 100644 --- a/src/app/(main)/community/events/map/map-tooltip.tsx +++ b/src/app/(main)/community/events/map/map-tooltip.tsx @@ -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 ( 0 ? "visible" : "hidden", }} > - {name} + {activeMeetupIds.map(meetupId => ( + {meetupCityById.get(meetupId)} + ))} ) } diff --git a/src/app/(main)/community/events/map/shaders.ts b/src/app/(main)/community/events/map/shaders.ts index fab651139f..07ab6a63b0 100644 --- a/src/app/(main)/community/events/map/shaders.ts +++ b/src/app/(main)/community/events/map/shaders.ts @@ -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]; @@ -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) { @@ -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) { @@ -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) { diff --git a/src/app/(main)/community/events/meetups-list.tsx b/src/app/(main)/community/events/meetups-list.tsx index 2b0d231112..099d731def 100644 --- a/src/app/(main)/community/events/meetups-list.tsx +++ b/src/app/(main)/community/events/meetups-list.tsx @@ -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[] } @@ -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 @@ -60,7 +60,7 @@ export function MeetupsList({ list.scrollTo({ top: activeAnchor.offsetTop, behavior: "smooth" }) } } - }, [activeMeetupId]) + }, [activeMeetupIds]) if (meetups.length === 0) return null @@ -73,7 +73,7 @@ export function MeetupsList({ )} > {meetups.map((meetup, index) => { - const isActive = meetup.id === activeMeetupId + const isActive = activeMeetupIds.includes(meetup.id) return (
  • diff --git a/src/app/(main)/community/events/meetups-map.tsx b/src/app/(main)/community/events/meetups-map.tsx index 8724d83e6b..6e7687692d 100644 --- a/src/app/(main)/community/events/meetups-map.tsx +++ b/src/app/(main)/community/events/meetups-map.tsx @@ -36,7 +36,8 @@ export function MeetupsMap() { const canvasRef = useRef(null) const handleRef = useRef() const { resolvedTheme } = useTheme() - const [activeMeetupId, setActiveMeetupId] = useState(null) + const [activeMeetupIds, setActiveMeetupIds] = useState([]) + const activeMeetupId = activeMeetupIds[0] ?? null const themeColors = useMemo( () => MAP_COLORS[resolvedTheme as ThemeVariant] || MAP_COLORS.light, [resolvedTheme], @@ -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") } } @@ -102,7 +103,7 @@ export function MeetupsMap() { aspectRatio: ASPECT_RATIO, theme: initialThemeRef.current, signal: abortController.signal, - onActiveMarkerChange: setActiveMeetupId, + onActiveMarkerChange: setActiveMeetupIds, }) if (disposed) { engine.dispose() @@ -141,8 +142,8 @@ export function MeetupsMap() { return (
    { - 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={ @@ -172,7 +173,7 @@ export function MeetupsMap() { }} /> - + @@ -185,8 +186,8 @@ export function MeetupsMap() { )}
    setActiveMeetupIds(id ? [id] : [])} className="shrink-0 md:max-h-full lg:w-[240px]" /> diff --git a/src/app/day/events-map.tsx b/src/app/day/events-map.tsx index 49c5d0f8d4..8f10ef3caf 100644 --- a/src/app/day/events-map.tsx +++ b/src/app/day/events-map.tsx @@ -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() diff --git a/src/components/meetups.ts b/src/components/meetups.ts index c848a69388..e359815f70 100644 --- a/src/components/meetups.ts +++ b/src/components/meetups.ts @@ -78,6 +78,19 @@ export const meetups = [ official: false, }, }, + { + node: { + id: "delhi", + name: "GraphQL Delhi", + latitude: 28.7, + longitude: 77.1, + city: "Delhi", + state: "", + country: "India", + link: "https://guild.host/graphql-delhi", + official: true, + }, + }, { node: { id: "hyderabad", @@ -91,6 +104,19 @@ export const meetups = [ official: true, }, }, + { + node: { + id: "jaipur", + name: "GraphQL Jaipur", + latitude: 26.91, + longitude: 75.79, + city: "Jaipur", + state: "", + country: "India", + link: "https://luma.com/GraphQL-jaipur", + official: false, + }, + }, { node: { id: "london", @@ -104,6 +130,19 @@ export const meetups = [ official: true, }, }, + { + node: { + id: "lucknow", + name: "GraphQL Lucknow", + latitude: 26.84, + longitude: 80.94, + city: "Lucknow", + state: "", + country: "India ", + link: "https://guild.host/graphql-lucknow", + official: true, + }, + }, { node: { id: "montreal", @@ -130,6 +169,19 @@ export const meetups = [ official: false, }, }, + { + node: { + id: "noida", + name: "GraphQL Noida", + latitude: 28.54, + longitude: 77.39, + city: "Noida", + state: "", + country: "India", + link: "https://guild.host/graphql-noida", + official: true, + }, + }, { node: { id: "oslo",