import React , { useContext , useEffect , useRef , useState } from 'react' ; import { AppContext } from '@/contexts/AppContext' ; import { FriendData } from '@/logic/friends' ; import MapIconsCache from '@/logic/mapIconsCache' ; import { store , zoomLevelSettingBounds } from '@/logic/storage' ; import { getTileCache } from '@/logic/tileCache' ; import { getTileMarkerCache } from '@/logic/tileMarkerCache' ; import { getNearestTown } from '@/logic/townLocations' ; import { getAngle , getAngleInterpolator , getNumberInterpolator , getVector2Interpolator , predictVector , squaredDistance , vector2Equal } from '@/logic/util' ; import drawMapFriends from './drawMapFriends' ; import drawMapMarkers from './drawMapMarkers' ; import drawMapNavigation from './drawMapNavigation' ; import drawMapNavigationTarget from './drawMapNavigationTarget' ; import drawMapPlayer from './drawMapPlayer' ; import drawMapTiles from './drawMapTiles' ; import drawNavMesh from './drawNavMesh' ; import drawPlayerCoordinates from './drawPlayerCoordinates' ; import { angleInterpolationTime , locationInterpolationTime , mapFastZoom , mapSlowZoom , tooLargeDistance , townZoomDistance } from './mapConstants' ; import { useInterpolation } from './useInterpolation' ; export type MapRendererParameters = { context : CanvasRenderingContext2D ; center : Vector2 ; playerPosition : Vector2 ; playerAngle : number ; mapCenterPosition : Vector2 ; mapAngle : number ; renderAsCompass : boolean ; zoomLevel : number ; showPlayerCoordinates : boolean ; showNavMesh : boolean ; } ; export type MapIconRendererParameters = { settings : IconSettings ; mapIconsCache : MapIconsCache ; iconScale : number ; showText : boolean ; } ; export type LastDrawParameters = { mapRendererParams : Omit < MapRendererParameters , 'context' > ; iconRendererParams : MapIconRendererParameters ; } ; type MapOverride = Vector2 & { angle : number ; } ; const tileCache = getTileCache ( ) ; const markerCache = getTileMarkerCache ( ) ; export default function useMinimapRenderer ( canvas , hoverCanvas ) { const appContext = useContext ( AppContext ) ; const [ mapIconsCache ] = useState ( ( ) => new MapIconsCache ( ) ) ; const currentPlayerPosition = useRef < Vector2 > ( appContext . settings . lastKnownPosition ) ; const currentPlayerAngle = useRef < number > ( 0 ) ; const lastPlayerPosition = useRef < Vector2 > ( currentPlayerPosition . current ) ; const lastPositionUpdate = useRef < number > ( performance . now ( ) ) ; const lastDrawParameters = useRef < LastDrawParameters > ( ) ; const { get : getInterpolatedZoomLevel , update : updateInterpolatedZoomLevel , getCurrentValue : getCurrentZoomValue , isDone : zoomInterpolationDone , } = useInterpolation ( getNumberInterpolator ( appContext . settings . animationInterpolation ) , appContext . settings . zoomLevel , mapFastZoom ) ; const { get : getInterpolatedAngle , update : updateInterpolatedAngle , isDone : angleInterpolationDone , } = useInterpolation ( getAngleInterpolator ( appContext . settings . animationInterpolation ) , currentPlayerAngle . current , angleInterpolationTime ) ; const { get : getInterpolatedPlayerPosition , update : updateInterpolatedPlayerPosition , isDone : playerPositionInterpolationDone , } = useInterpolation ( getVector2Interpolator ( appContext . settings . animationInterpolation ) , currentPlayerPosition . current , locationInterpolationTime , vector2Equal ) ; const usingTownZoom = useRef ( false ) ; const mapOverride = useRef < MapOverride > ( ) ; const currentFriends = useRef < FriendData [ ] > ( [ ] ) ; function automaticTownZoom ( playerPosition ) { let nextZoomLevel = appContext . settings . zoomLevel ; const wasUsingTownZoom = usingTownZoom . current ; if ( appContext . settings . townZoom && ! mapOverride . current ) { const town = getNearestTown ( playerPosition ) ; if ( town . distance <= townZoomDistance ) { nextZoomLevel = appContext . settings . townZoomLevel ; if ( ! usingTownZoom . current ) { usingTownZoom . current = true ; } } else if ( usingTownZoom . current ) { usingTownZoom . current = false ; } } else if ( usingTownZoom . current ) { usingTownZoom . current = false ; nextZoomLevel = appContext . settings . zoomLevel ; } if ( nextZoomLevel !== getCurrentZoomValue ( ) ) { updateInterpolatedZoomLevel ( nextZoomLevel , wasUsingTownZoom !== usingTownZoom . current ? mapSlowZoom : undefined ) ; } } const draw = ( ) => { const ctx = canvas . current ?. getContext ( '2d' ) ; const hoverCtx = hoverCanvas . current ?. getContext ( '2d' ) ; if ( ! ctx || ! hoverCtx ) { return ; } const playerPos = getInterpolatedPlayerPosition ( ) ; automaticTownZoom ( playerPos ) ; const zoomLevel = getInterpolatedZoomLevel ( ) ; const renderAsCompass = appContext . settings . compassMode && ( appContext . isTransparentSurface ?? false ) ; ctx . canvas . width = ctx . canvas . clientWidth ; ctx . canvas . height = ctx . canvas . clientHeight ; hoverCtx . canvas . width = hoverCtx . canvas . clientWidth ; hoverCtx . canvas . height = hoverCtx . canvas . clientHeight ; const center = { x : ctx . canvas . width / 2 , y : ctx . canvas . height / 2 , } ; const mapCenterPos = mapOverride . current ? { x : mapOverride . current . x , y : mapOverride . current . y } : playerPos ; const playerAngle = getInterpolatedAngle ( ) ; const mapAngle = mapOverride . current ? mapOverride . current ?. angle : playerAngle ; const mapRendererParameters = { center , context : ctx , playerPosition : playerPos , playerAngle , mapCenterPosition : mapCenterPos , mapAngle , renderAsCompass , zoomLevel , showPlayerCoordinates : appContext . settings . showPlayerCoordinates , showNavMesh : appContext . settings . showNavMesh , } ; const toDraw = drawMapTiles ( mapRendererParameters ) ; const iconSettings = appContext . settings . iconSettings ; if ( ! iconSettings ) { return ; } const mapIconRendererParameters = { iconScale : appContext . settings . iconScale , mapIconsCache , settings : iconSettings , showText : appContext . settings . showText , } ; drawNavMesh ( mapRendererParameters , mapIconRendererParameters ) ; const navTarget = drawMapNavigation ( mapRendererParameters ) ; drawMapMarkers ( mapRendererParameters , mapIconRendererParameters , toDraw ) ; drawMapFriends ( mapRendererParameters , mapIconRendererParameters , currentFriends . current ) ; drawMapNavigationTarget ( mapRendererParameters , mapIconRendererParameters , navTarget ) ; drawMapPlayer ( mapRendererParameters , mapIconRendererParameters ) ; drawPlayerCoordinates ( mapRendererParameters , mapIconRendererParameters ) ; lastDrawParameters . current = { mapRendererParams : mapRendererParameters , iconRendererParams : mapIconRendererParameters , } ; } ; function drawWithInterpolation ( force ) { const interpolationsDone = zoomInterpolationDone ( ) && angleInterpolationDone ( ) && playerPositionInterpolationDone ( ) ; if ( interpolationsDone && ! force ) { return ; } draw ( ) ; } const drawWithInterpolationRef = useRef ( drawWithInterpolation ) ; drawWithInterpolationRef . current = drawWithInterpolation ; function redraw ( force ) { drawWithInterpolationRef . current ( force ) ; } function setPlayerPosition ( pos , rotation ) { if ( pos . x === currentPlayerPosition . current . x && pos . y === currentPlayerPosition . current . y ) { return ; } currentPlayerAngle . current = appContext . settings . rotationSource === 'computed' ? getAngle ( lastPlayerPosition . current , currentPlayerPosition . current ) : rotation ; lastPositionUpdate . current = performance . now ( ) ; lastPlayerPosition . current = currentPlayerPosition . current ; currentPlayerPosition . current = pos ; const bigPositionChange = squaredDistance ( lastPlayerPosition . current , currentPlayerPosition . current ) > tooLargeDistance ; const updateTime = bigPositionChange ? 0 : undefined ; if ( ! appContext . settings . extrapolateLocation ) { updateInterpolatedPlayerPosition ( currentPlayerPosition . current , updateTime ) ; } else { const predictedPosition = predictVector ( lastPlayerPosition . current , currentPlayerPosition . current ) ; updateInterpolatedPlayerPosition ( predictedPosition , updateTime ) ; } updateInterpolatedAngle ( currentPlayerAngle . current , updateTime ) ; redraw ( true ) ; } function getZoomLevel ( ) { return usingTownZoom . current ? appContext . settings . townZoomLevel : appContext . settings . zoomLevel ; } function zoomBy ( delta ) { const nextZoomLevel = Math . max ( zoomLevelSettingBounds [ 0 ] , Math . min ( zoomLevelSettingBounds [ 1 ] , getZoomLevel ( ) + delta ) ) ; const keyToUpdate = usingTownZoom . current ? 'townZoomLevel' : 'zoomLevel' ; appContext . update ( { [ keyToUpdate ] : nextZoomLevel } ) ; store ( keyToUpdate , nextZoomLevel ) ; } useEffect ( ( ) => { mapIconsCache . initialize ( appContext . settings . iconScale ) ; } , [ appContext . settings . iconScale ] ) ; useEffect ( ( ) => { function handleTileDownloadingCountChange ( count ) { if ( count === 0 ) { redraw ( true ) ; } } function handleAssetsLoaded ( ) { redraw ( true ) ; } const tileRegistration = tileCache . registerOnTileDownloadingCountChange ( handleTileDownloadingCountChange , window ) ; const markerRegistration = markerCache . registerOnMarkersLoaded ( handleAssetsLoaded , window ) ; const mapIconsCacheRegistration = mapIconsCache . registerMapIconsLoaded ( handleAssetsLoaded , window ) ; return ( ) => { tileRegistration ( ) ; markerRegistration ( ) ; mapIconsCacheRegistration ( ) ; } ; } , [ ] ) ; const updateOn = [ appContext . isTransparentSurface , appContext . settings . animationInterpolation , appContext . settings . compassMode , appContext . settings . extrapolateLocation , appContext . settings . iconScale , appContext . settings . iconSettings , appContext . settings . showNavMesh , appContext . settings . showPlayerCoordinates , appContext . settings . showText , appContext . settings . townZoom , appContext . settings . townZoomLevel , appContext . settings . zoomLevel , ] ; useEffect ( ( ) => { redraw ( true ) ; } , updateOn ) ; return { currentFriends , currentPlayerPosition : currentPlayerPosition as { readonly current : Readonly < Vector2 > ; } , currentPlayerAngle : currentPlayerAngle as { readonly current : Readonly < number > ; } , lastDrawParameters : lastDrawParameters as { readonly current : undefined | Readonly < LastDrawParameters > ; } , getZoomLevel , mapOverride , redraw , setPlayerPosition , zoomBy , } ; }