feat: initialize mini program map engine
This commit is contained in:
51
miniprogram/engine/layer/gpsLayer.ts
Normal file
51
miniprogram/engine/layer/gpsLayer.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { type CameraState } from '../camera/camera'
|
||||
import { lonLatToWorldTile } from '../../utils/projection'
|
||||
import { worldToScreen } from '../camera/camera'
|
||||
import { type MapLayer, type LayerRenderContext } from './mapLayer'
|
||||
import { type MapScene } from '../renderer/mapRenderer'
|
||||
import { type ScreenPoint } from './trackLayer'
|
||||
|
||||
export class GpsLayer implements MapLayer {
|
||||
projectPoint(scene: MapScene, camera: CameraState): ScreenPoint {
|
||||
const worldPoint = lonLatToWorldTile(scene.gpsPoint, scene.zoom)
|
||||
const screenPoint = worldToScreen(camera, worldPoint, false)
|
||||
return {
|
||||
x: screenPoint.x + scene.translateX,
|
||||
y: screenPoint.y + scene.translateY,
|
||||
}
|
||||
}
|
||||
|
||||
getPulseRadius(pulseFrame: number): number {
|
||||
return 18 + 6 * (0.5 + 0.5 * Math.sin(pulseFrame / 6))
|
||||
}
|
||||
|
||||
draw(context: LayerRenderContext): void {
|
||||
const { ctx, camera, scene, pulseFrame } = context
|
||||
const gpsScreenPoint = this.projectPoint(scene, camera)
|
||||
const pulse = this.getPulseRadius(pulseFrame)
|
||||
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = 'rgba(33, 158, 188, 0.22)'
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, pulse, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = '#21a1bc'
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, 9, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = '#ffffff'
|
||||
ctx.lineWidth = 3
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, 13, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.fillStyle = '#0b3d4a'
|
||||
ctx.font = 'bold 16px sans-serif'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.textBaseline = 'bottom'
|
||||
ctx.fillText('GPS', gpsScreenPoint.x + 14, gpsScreenPoint.y - 12)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
15
miniprogram/engine/layer/mapLayer.ts
Normal file
15
miniprogram/engine/layer/mapLayer.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type CameraState } from '../camera/camera'
|
||||
import { type MapScene } from '../renderer/mapRenderer'
|
||||
import { type TileStore } from '../tile/tileStore'
|
||||
|
||||
export interface LayerRenderContext {
|
||||
ctx: any
|
||||
camera: CameraState
|
||||
scene: MapScene
|
||||
pulseFrame: number
|
||||
tileStore: TileStore
|
||||
}
|
||||
|
||||
export interface MapLayer {
|
||||
draw(context: LayerRenderContext): void
|
||||
}
|
||||
124
miniprogram/engine/layer/tileLayer.ts
Normal file
124
miniprogram/engine/layer/tileLayer.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { createTileGrid, type TileItem } from '../../utils/tile'
|
||||
import { getTileSizePx, type CameraState } from '../camera/camera'
|
||||
import { type MapScene } from '../renderer/mapRenderer'
|
||||
import { type TileStore } from '../tile/tileStore'
|
||||
import { type MapLayer, type LayerRenderContext } from './mapLayer'
|
||||
|
||||
function buildGridKey(scene: MapScene, tileSize: number): string {
|
||||
return [
|
||||
scene.tileSource,
|
||||
scene.zoom,
|
||||
scene.centerTileX,
|
||||
scene.centerTileY,
|
||||
scene.viewportWidth,
|
||||
scene.viewportHeight,
|
||||
tileSize,
|
||||
scene.overdraw,
|
||||
].join('|')
|
||||
}
|
||||
|
||||
export class TileLayer implements MapLayer {
|
||||
lastVisibleTileCount: number
|
||||
lastReadyTileCount: number
|
||||
cachedGridKey: string
|
||||
cachedTiles: TileItem[]
|
||||
|
||||
constructor() {
|
||||
this.lastVisibleTileCount = 0
|
||||
this.lastReadyTileCount = 0
|
||||
this.cachedGridKey = ''
|
||||
this.cachedTiles = []
|
||||
}
|
||||
|
||||
prepareTiles(scene: MapScene, camera: CameraState, tileStore: TileStore): TileItem[] {
|
||||
const tileSize = getTileSizePx(camera)
|
||||
if (!tileSize) {
|
||||
this.lastVisibleTileCount = 0
|
||||
this.lastReadyTileCount = 0
|
||||
this.cachedGridKey = ''
|
||||
this.cachedTiles = []
|
||||
return []
|
||||
}
|
||||
|
||||
const gridKey = buildGridKey(scene, tileSize)
|
||||
if (gridKey !== this.cachedGridKey) {
|
||||
this.cachedGridKey = gridKey
|
||||
this.cachedTiles = createTileGrid({
|
||||
urlTemplate: scene.tileSource,
|
||||
zoom: scene.zoom,
|
||||
centerTileX: scene.centerTileX,
|
||||
centerTileY: scene.centerTileY,
|
||||
viewportWidth: scene.viewportWidth,
|
||||
viewportHeight: scene.viewportHeight,
|
||||
tileSize,
|
||||
overdraw: scene.overdraw,
|
||||
})
|
||||
}
|
||||
|
||||
tileStore.queueVisibleTiles(this.cachedTiles, scene, gridKey)
|
||||
this.lastVisibleTileCount = this.cachedTiles.length
|
||||
this.lastReadyTileCount = 0
|
||||
return this.cachedTiles
|
||||
}
|
||||
|
||||
draw(context: LayerRenderContext): void {
|
||||
const { ctx, scene, camera, tileStore } = context
|
||||
const tiles = this.prepareTiles(scene, camera, tileStore)
|
||||
|
||||
for (const tile of tiles) {
|
||||
const entry = tileStore.getEntry(tile.url)
|
||||
const drawLeft = tile.leftPx + scene.translateX
|
||||
const drawTop = tile.topPx + scene.translateY
|
||||
|
||||
if (entry && entry.status === 'ready' && entry.image) {
|
||||
ctx.drawImage(entry.image, drawLeft, drawTop, tile.sizePx, tile.sizePx)
|
||||
this.lastReadyTileCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const parentFallback = tileStore.getParentFallbackSlice(tile, scene)
|
||||
let drewFallback = false
|
||||
if (parentFallback) {
|
||||
ctx.drawImage(
|
||||
parentFallback.entry.image,
|
||||
parentFallback.sourceX,
|
||||
parentFallback.sourceY,
|
||||
parentFallback.sourceWidth,
|
||||
parentFallback.sourceHeight,
|
||||
drawLeft,
|
||||
drawTop,
|
||||
tile.sizePx,
|
||||
tile.sizePx,
|
||||
)
|
||||
drewFallback = true
|
||||
}
|
||||
|
||||
const childFallback = tileStore.getChildFallback(tile, scene)
|
||||
if (childFallback) {
|
||||
const cellWidth = tile.sizePx / childFallback.division
|
||||
const cellHeight = tile.sizePx / childFallback.division
|
||||
for (const child of childFallback.children) {
|
||||
const childImageWidth = child.entry.image.width || 256
|
||||
const childImageHeight = child.entry.image.height || 256
|
||||
ctx.drawImage(
|
||||
child.entry.image,
|
||||
0,
|
||||
0,
|
||||
childImageWidth,
|
||||
childImageHeight,
|
||||
drawLeft + child.offsetX * cellWidth,
|
||||
drawTop + child.offsetY * cellHeight,
|
||||
cellWidth,
|
||||
cellHeight,
|
||||
)
|
||||
}
|
||||
drewFallback = true
|
||||
}
|
||||
|
||||
if (!drewFallback) {
|
||||
ctx.fillStyle = entry && entry.status === 'error' ? '#d9b2b2' : '#dbeed4'
|
||||
ctx.fillRect(drawLeft, drawTop, tile.sizePx, tile.sizePx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
miniprogram/engine/layer/trackLayer.ts
Normal file
61
miniprogram/engine/layer/trackLayer.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { type CameraState } from '../camera/camera'
|
||||
import { lonLatToWorldTile } from '../../utils/projection'
|
||||
import { worldToScreen } from '../camera/camera'
|
||||
import { type MapLayer, type LayerRenderContext } from './mapLayer'
|
||||
import { type MapScene } from '../renderer/mapRenderer'
|
||||
|
||||
export interface ScreenPoint {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export class TrackLayer implements MapLayer {
|
||||
projectPoints(scene: MapScene, camera: CameraState): ScreenPoint[] {
|
||||
return scene.track.map((point) => {
|
||||
const worldPoint = lonLatToWorldTile(point, scene.zoom)
|
||||
const screenPoint = worldToScreen(camera, worldPoint, false)
|
||||
return {
|
||||
x: screenPoint.x + scene.translateX,
|
||||
y: screenPoint.y + scene.translateY,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
draw(context: LayerRenderContext): void {
|
||||
const { ctx, camera, scene } = context
|
||||
const points = this.projectPoints(scene, camera)
|
||||
|
||||
ctx.save()
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.strokeStyle = 'rgba(23, 109, 93, 0.96)'
|
||||
ctx.lineWidth = 6
|
||||
ctx.beginPath()
|
||||
|
||||
points.forEach((screenPoint, index) => {
|
||||
if (index === 0) {
|
||||
ctx.moveTo(screenPoint.x, screenPoint.y)
|
||||
return
|
||||
}
|
||||
ctx.lineTo(screenPoint.x, screenPoint.y)
|
||||
})
|
||||
ctx.stroke()
|
||||
|
||||
ctx.fillStyle = '#f7fbf2'
|
||||
ctx.strokeStyle = '#176d5d'
|
||||
ctx.lineWidth = 4
|
||||
points.forEach((screenPoint, index) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(screenPoint.x, screenPoint.y, 10, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
ctx.fillStyle = '#176d5d'
|
||||
ctx.font = 'bold 14px sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(String(index + 1), screenPoint.x, screenPoint.y)
|
||||
ctx.fillStyle = '#f7fbf2'
|
||||
})
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user