Refactor Dockerignore and enhance Leaflet styles for improved map functionality
- Updated .dockerignore to streamline build context by ensuring unnecessary files are excluded. - Refined CSS styles in leaflet-overrides.css to enhance visual consistency and user experience for map tooltips and popups. - Improved map initialization and update handling in useMapApi and useMapUpdates composables for better performance and reliability.
This commit is contained in:
@@ -1,192 +1,192 @@
|
||||
import type L from 'leaflet'
|
||||
import { getColorForCharacterId, type CharacterColors } from '~/lib/characterColors'
|
||||
import { HnHMaxZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
||||
|
||||
export type LeafletApi = L
|
||||
|
||||
function buildCharacterIconUrl(colors: CharacterColors): string {
|
||||
const svg =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 32" width="24" height="32">' +
|
||||
`<path fill="${colors.fill}" stroke="${colors.stroke}" stroke-width="1" d="M12 2a6 6 0 0 1 6 6c0 4-6 10-6 10s-6-6-6-10a6 6 0 0 1 6-6z"/>` +
|
||||
'<circle cx="12" cy="8" r="2.5" fill="white"/>' +
|
||||
'</svg>'
|
||||
return 'data:image/svg+xml,' + encodeURIComponent(svg)
|
||||
}
|
||||
|
||||
export function createCharacterIcon(L: LeafletApi, colors: CharacterColors): L.Icon {
|
||||
return new L.Icon({
|
||||
iconUrl: buildCharacterIconUrl(colors),
|
||||
iconSize: [25, 32],
|
||||
iconAnchor: [12, 17],
|
||||
popupAnchor: [0, -32],
|
||||
tooltipAnchor: [12, 0],
|
||||
})
|
||||
}
|
||||
|
||||
export interface CharacterData {
|
||||
name: string
|
||||
position: { x: number; y: number }
|
||||
type: string
|
||||
id: number
|
||||
map: number
|
||||
/** True when this character was last updated by one of the current user's tokens. */
|
||||
ownedByMe?: boolean
|
||||
}
|
||||
|
||||
export interface CharacterMapViewRef {
|
||||
map: L.Map
|
||||
mapid: number
|
||||
markerLayer?: L.LayerGroup
|
||||
}
|
||||
|
||||
export interface MapCharacter {
|
||||
id: number
|
||||
name: string
|
||||
position: { x: number; y: number }
|
||||
type: string
|
||||
map: number
|
||||
text: string
|
||||
value: number
|
||||
ownedByMe?: boolean
|
||||
leafletMarker: L.Marker | null
|
||||
remove: (mapview: CharacterMapViewRef) => void
|
||||
add: (mapview: CharacterMapViewRef) => void
|
||||
update: (mapview: CharacterMapViewRef, updated: CharacterData | MapCharacter) => void
|
||||
setClickCallback: (callback: (e: L.LeafletMouseEvent) => void) => void
|
||||
}
|
||||
|
||||
const CHARACTER_MOVE_DURATION_MS = 280
|
||||
|
||||
function easeOutQuad(t: number): number {
|
||||
return t * (2 - t)
|
||||
}
|
||||
|
||||
export function createCharacter(data: CharacterData, L: LeafletApi): MapCharacter {
|
||||
let leafletMarker: L.Marker | null = null
|
||||
let onClick: ((e: L.LeafletMouseEvent) => void) | null = null
|
||||
let ownedByMe = data.ownedByMe ?? false
|
||||
let animationFrameId: number | null = null
|
||||
const colors = getColorForCharacterId(data.id, { ownedByMe })
|
||||
let characterIcon = createCharacterIcon(L, colors)
|
||||
|
||||
const character: MapCharacter = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
position: { ...data.position },
|
||||
type: data.type,
|
||||
map: data.map,
|
||||
text: data.name,
|
||||
value: data.id,
|
||||
get ownedByMe() {
|
||||
return ownedByMe
|
||||
},
|
||||
set ownedByMe(v: boolean | undefined) {
|
||||
ownedByMe = v ?? false
|
||||
},
|
||||
|
||||
get leafletMarker() {
|
||||
return leafletMarker
|
||||
},
|
||||
|
||||
remove(mapview: CharacterMapViewRef): void {
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animationFrameId = null
|
||||
}
|
||||
if (leafletMarker) {
|
||||
const layer = mapview.markerLayer ?? mapview.map
|
||||
layer.removeLayer(leafletMarker)
|
||||
leafletMarker = null
|
||||
}
|
||||
},
|
||||
|
||||
add(mapview: CharacterMapViewRef): void {
|
||||
if (character.map === mapview.mapid) {
|
||||
const position = mapview.map.unproject([character.position.x, character.position.y], HnHMaxZoom)
|
||||
leafletMarker = L.marker(position, { icon: characterIcon })
|
||||
const gridX = Math.floor(character.position.x / TileSize)
|
||||
const gridY = Math.floor(character.position.y / TileSize)
|
||||
const tooltipContent = `${character.name} · ${gridX}, ${gridY}`
|
||||
leafletMarker.bindTooltip(tooltipContent, {
|
||||
direction: 'top',
|
||||
permanent: false,
|
||||
offset: L.point(-10.5, -18),
|
||||
})
|
||||
leafletMarker.on('click', (e: L.LeafletMouseEvent) => {
|
||||
if (onClick) onClick(e)
|
||||
})
|
||||
const targetLayer = mapview.markerLayer ?? mapview.map
|
||||
leafletMarker.addTo(targetLayer)
|
||||
const markerEl = (leafletMarker as unknown as { getElement?: () => HTMLElement }).getElement?.()
|
||||
if (markerEl) markerEl.setAttribute('aria-label', character.name)
|
||||
}
|
||||
},
|
||||
|
||||
update(mapview: CharacterMapViewRef, updated: CharacterData | MapCharacter): void {
|
||||
const updatedOwnedByMe = (updated as { ownedByMe?: boolean }).ownedByMe ?? false
|
||||
if (ownedByMe !== updatedOwnedByMe) {
|
||||
ownedByMe = updatedOwnedByMe
|
||||
characterIcon = createCharacterIcon(L, getColorForCharacterId(character.id, { ownedByMe }))
|
||||
if (leafletMarker) leafletMarker.setIcon(characterIcon)
|
||||
}
|
||||
if (character.map !== updated.map) {
|
||||
character.remove(mapview)
|
||||
}
|
||||
character.map = updated.map
|
||||
character.position = { ...updated.position }
|
||||
if (!leafletMarker && character.map === mapview.mapid) {
|
||||
character.add(mapview)
|
||||
return
|
||||
}
|
||||
if (!leafletMarker) return
|
||||
|
||||
const newLatLng = mapview.map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
|
||||
|
||||
const updateTooltip = (): void => {
|
||||
const gridX = Math.floor(character.position.x / TileSize)
|
||||
const gridY = Math.floor(character.position.y / TileSize)
|
||||
leafletMarker?.setTooltipContent(`${character.name} · ${gridX}, ${gridY}`)
|
||||
}
|
||||
|
||||
const from = leafletMarker.getLatLng()
|
||||
const latDelta = newLatLng.lat - from.lat
|
||||
const lngDelta = newLatLng.lng - from.lng
|
||||
const distSq = latDelta * latDelta + lngDelta * lngDelta
|
||||
if (distSq < 1e-12) {
|
||||
updateTooltip()
|
||||
return
|
||||
}
|
||||
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animationFrameId = null
|
||||
}
|
||||
const start = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
||||
const duration = CHARACTER_MOVE_DURATION_MS
|
||||
|
||||
const tick = (): void => {
|
||||
const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - start
|
||||
const t = Math.min(1, elapsed / duration)
|
||||
const eased = easeOutQuad(t)
|
||||
leafletMarker?.setLatLng({
|
||||
lat: from.lat + latDelta * eased,
|
||||
lng: from.lng + lngDelta * eased,
|
||||
})
|
||||
if (t >= 1) {
|
||||
animationFrameId = null
|
||||
leafletMarker?.setLatLng(newLatLng)
|
||||
updateTooltip()
|
||||
return
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(tick)
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(tick)
|
||||
},
|
||||
|
||||
setClickCallback(callback: (e: L.LeafletMouseEvent) => void): void {
|
||||
onClick = callback
|
||||
},
|
||||
}
|
||||
|
||||
return character
|
||||
}
|
||||
import type L from 'leaflet'
|
||||
import { getColorForCharacterId, type CharacterColors } from '~/lib/characterColors'
|
||||
import { HnHMaxZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
||||
|
||||
export type LeafletApi = L
|
||||
|
||||
function buildCharacterIconUrl(colors: CharacterColors): string {
|
||||
const svg =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 32" width="24" height="32">' +
|
||||
`<path fill="${colors.fill}" stroke="${colors.stroke}" stroke-width="1" d="M12 2a6 6 0 0 1 6 6c0 4-6 10-6 10s-6-6-6-10a6 6 0 0 1 6-6z"/>` +
|
||||
'<circle cx="12" cy="8" r="2.5" fill="white"/>' +
|
||||
'</svg>'
|
||||
return 'data:image/svg+xml,' + encodeURIComponent(svg)
|
||||
}
|
||||
|
||||
export function createCharacterIcon(L: LeafletApi, colors: CharacterColors): L.Icon {
|
||||
return new L.Icon({
|
||||
iconUrl: buildCharacterIconUrl(colors),
|
||||
iconSize: [25, 32],
|
||||
iconAnchor: [12, 17],
|
||||
popupAnchor: [0, -32],
|
||||
tooltipAnchor: [12, 0],
|
||||
})
|
||||
}
|
||||
|
||||
export interface CharacterData {
|
||||
name: string
|
||||
position: { x: number; y: number }
|
||||
type: string
|
||||
id: number
|
||||
map: number
|
||||
/** True when this character was last updated by one of the current user's tokens. */
|
||||
ownedByMe?: boolean
|
||||
}
|
||||
|
||||
export interface CharacterMapViewRef {
|
||||
map: L.Map
|
||||
mapid: number
|
||||
markerLayer?: L.LayerGroup
|
||||
}
|
||||
|
||||
export interface MapCharacter {
|
||||
id: number
|
||||
name: string
|
||||
position: { x: number; y: number }
|
||||
type: string
|
||||
map: number
|
||||
text: string
|
||||
value: number
|
||||
ownedByMe?: boolean
|
||||
leafletMarker: L.Marker | null
|
||||
remove: (mapview: CharacterMapViewRef) => void
|
||||
add: (mapview: CharacterMapViewRef) => void
|
||||
update: (mapview: CharacterMapViewRef, updated: CharacterData | MapCharacter) => void
|
||||
setClickCallback: (callback: (e: L.LeafletMouseEvent) => void) => void
|
||||
}
|
||||
|
||||
const CHARACTER_MOVE_DURATION_MS = 280
|
||||
|
||||
function easeOutQuad(t: number): number {
|
||||
return t * (2 - t)
|
||||
}
|
||||
|
||||
export function createCharacter(data: CharacterData, L: LeafletApi): MapCharacter {
|
||||
let leafletMarker: L.Marker | null = null
|
||||
let onClick: ((e: L.LeafletMouseEvent) => void) | null = null
|
||||
let ownedByMe = data.ownedByMe ?? false
|
||||
let animationFrameId: number | null = null
|
||||
const colors = getColorForCharacterId(data.id, { ownedByMe })
|
||||
let characterIcon = createCharacterIcon(L, colors)
|
||||
|
||||
const character: MapCharacter = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
position: { ...data.position },
|
||||
type: data.type,
|
||||
map: data.map,
|
||||
text: data.name,
|
||||
value: data.id,
|
||||
get ownedByMe() {
|
||||
return ownedByMe
|
||||
},
|
||||
set ownedByMe(v: boolean | undefined) {
|
||||
ownedByMe = v ?? false
|
||||
},
|
||||
|
||||
get leafletMarker() {
|
||||
return leafletMarker
|
||||
},
|
||||
|
||||
remove(mapview: CharacterMapViewRef): void {
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animationFrameId = null
|
||||
}
|
||||
if (leafletMarker) {
|
||||
const layer = mapview.markerLayer ?? mapview.map
|
||||
layer.removeLayer(leafletMarker)
|
||||
leafletMarker = null
|
||||
}
|
||||
},
|
||||
|
||||
add(mapview: CharacterMapViewRef): void {
|
||||
if (character.map === mapview.mapid) {
|
||||
const position = mapview.map.unproject([character.position.x, character.position.y], HnHMaxZoom)
|
||||
leafletMarker = L.marker(position, { icon: characterIcon })
|
||||
const gridX = Math.floor(character.position.x / TileSize)
|
||||
const gridY = Math.floor(character.position.y / TileSize)
|
||||
const tooltipContent = `${character.name} · ${gridX}, ${gridY}`
|
||||
leafletMarker.bindTooltip(tooltipContent, {
|
||||
direction: 'top',
|
||||
permanent: false,
|
||||
offset: L.point(-10.5, -18),
|
||||
})
|
||||
leafletMarker.on('click', (e: L.LeafletMouseEvent) => {
|
||||
if (onClick) onClick(e)
|
||||
})
|
||||
const targetLayer = mapview.markerLayer ?? mapview.map
|
||||
leafletMarker.addTo(targetLayer)
|
||||
const markerEl = (leafletMarker as unknown as { getElement?: () => HTMLElement }).getElement?.()
|
||||
if (markerEl) markerEl.setAttribute('aria-label', character.name)
|
||||
}
|
||||
},
|
||||
|
||||
update(mapview: CharacterMapViewRef, updated: CharacterData | MapCharacter): void {
|
||||
const updatedOwnedByMe = (updated as { ownedByMe?: boolean }).ownedByMe ?? false
|
||||
if (ownedByMe !== updatedOwnedByMe) {
|
||||
ownedByMe = updatedOwnedByMe
|
||||
characterIcon = createCharacterIcon(L, getColorForCharacterId(character.id, { ownedByMe }))
|
||||
if (leafletMarker) leafletMarker.setIcon(characterIcon)
|
||||
}
|
||||
if (character.map !== updated.map) {
|
||||
character.remove(mapview)
|
||||
}
|
||||
character.map = updated.map
|
||||
character.position = { ...updated.position }
|
||||
if (!leafletMarker && character.map === mapview.mapid) {
|
||||
character.add(mapview)
|
||||
return
|
||||
}
|
||||
if (!leafletMarker) return
|
||||
|
||||
const newLatLng = mapview.map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
|
||||
|
||||
const updateTooltip = (): void => {
|
||||
const gridX = Math.floor(character.position.x / TileSize)
|
||||
const gridY = Math.floor(character.position.y / TileSize)
|
||||
leafletMarker?.setTooltipContent(`${character.name} · ${gridX}, ${gridY}`)
|
||||
}
|
||||
|
||||
const from = leafletMarker.getLatLng()
|
||||
const latDelta = newLatLng.lat - from.lat
|
||||
const lngDelta = newLatLng.lng - from.lng
|
||||
const distSq = latDelta * latDelta + lngDelta * lngDelta
|
||||
if (distSq < 1e-12) {
|
||||
updateTooltip()
|
||||
return
|
||||
}
|
||||
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animationFrameId = null
|
||||
}
|
||||
const start = typeof performance !== 'undefined' ? performance.now() : Date.now()
|
||||
const duration = CHARACTER_MOVE_DURATION_MS
|
||||
|
||||
const tick = (): void => {
|
||||
const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - start
|
||||
const t = Math.min(1, elapsed / duration)
|
||||
const eased = easeOutQuad(t)
|
||||
leafletMarker?.setLatLng({
|
||||
lat: from.lat + latDelta * eased,
|
||||
lng: from.lng + lngDelta * eased,
|
||||
})
|
||||
if (t >= 1) {
|
||||
animationFrameId = null
|
||||
leafletMarker?.setLatLng(newLatLng)
|
||||
updateTooltip()
|
||||
return
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(tick)
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(tick)
|
||||
},
|
||||
|
||||
setClickCallback(callback: (e: L.LeafletMouseEvent) => void): void {
|
||||
onClick = callback
|
||||
},
|
||||
}
|
||||
|
||||
return character
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user