完善样式系统与调试链路底座
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
} from '../../utils/orienteeringCourse'
|
||||
|
||||
export interface ProjectedCourseLeg {
|
||||
index: number
|
||||
fromKind: OrienteeringCourseLeg['fromKind']
|
||||
toKind: OrienteeringCourseLeg['toKind']
|
||||
from: ScreenPoint
|
||||
@@ -18,6 +19,7 @@ export interface ProjectedCourseLeg {
|
||||
}
|
||||
|
||||
export interface ProjectedCourseStart {
|
||||
index: number
|
||||
label: string
|
||||
point: ScreenPoint
|
||||
headingDeg: number | null
|
||||
@@ -30,6 +32,7 @@ export interface ProjectedCourseControl {
|
||||
}
|
||||
|
||||
export interface ProjectedCourseFinish {
|
||||
index: number
|
||||
label: string
|
||||
point: ScreenPoint
|
||||
}
|
||||
@@ -59,7 +62,8 @@ export class CourseLayer implements MapLayer {
|
||||
}
|
||||
|
||||
projectStarts(starts: OrienteeringCourseStart[], scene: MapScene, camera: CameraState): ProjectedCourseStart[] {
|
||||
return starts.map((start) => ({
|
||||
return starts.map((start, index) => ({
|
||||
index,
|
||||
label: start.label,
|
||||
point: this.projectPoint(start, scene, camera),
|
||||
headingDeg: start.headingDeg,
|
||||
@@ -75,14 +79,16 @@ export class CourseLayer implements MapLayer {
|
||||
}
|
||||
|
||||
projectFinishes(finishes: OrienteeringCourseFinish[], scene: MapScene, camera: CameraState): ProjectedCourseFinish[] {
|
||||
return finishes.map((finish) => ({
|
||||
return finishes.map((finish, index) => ({
|
||||
index,
|
||||
label: finish.label,
|
||||
point: this.projectPoint(finish, scene, camera),
|
||||
}))
|
||||
}
|
||||
|
||||
projectLegs(legs: OrienteeringCourseLeg[], scene: MapScene, camera: CameraState): ProjectedCourseLeg[] {
|
||||
return legs.map((leg) => ({
|
||||
return legs.map((leg, index) => ({
|
||||
index,
|
||||
fromKind: leg.fromKind,
|
||||
toKind: leg.toKind,
|
||||
from: worldToScreen(camera, lonLatToWorldTile(leg.fromPoint, scene.zoom), false),
|
||||
|
||||
@@ -4,6 +4,109 @@ import { type MapLayer, type LayerRenderContext } from './mapLayer'
|
||||
import { type MapScene } from '../renderer/mapRenderer'
|
||||
import { type ScreenPoint } from './trackLayer'
|
||||
|
||||
function getGpsMarkerMetrics(size: MapScene['gpsMarkerStyleConfig']['size']) {
|
||||
if (size === 'small') {
|
||||
return {
|
||||
coreRadius: 7,
|
||||
ringRadius: 8.35,
|
||||
pulseRadius: 14,
|
||||
indicatorOffset: 1.1,
|
||||
indicatorSize: 7,
|
||||
ringWidth: 2.5,
|
||||
}
|
||||
}
|
||||
if (size === 'large') {
|
||||
return {
|
||||
coreRadius: 11,
|
||||
ringRadius: 12.95,
|
||||
pulseRadius: 22,
|
||||
indicatorOffset: 1.45,
|
||||
indicatorSize: 10,
|
||||
ringWidth: 3.5,
|
||||
}
|
||||
}
|
||||
return {
|
||||
coreRadius: 9,
|
||||
ringRadius: 10.65,
|
||||
pulseRadius: 18,
|
||||
indicatorOffset: 1.25,
|
||||
indicatorSize: 8.5,
|
||||
ringWidth: 3,
|
||||
}
|
||||
}
|
||||
|
||||
function scaleGpsMarkerMetrics(
|
||||
metrics: ReturnType<typeof getGpsMarkerMetrics>,
|
||||
effectScale: number,
|
||||
): ReturnType<typeof getGpsMarkerMetrics> {
|
||||
const safeScale = Math.max(0.88, Math.min(1.28, effectScale || 1))
|
||||
return {
|
||||
coreRadius: metrics.coreRadius * safeScale,
|
||||
ringRadius: metrics.ringRadius * safeScale,
|
||||
pulseRadius: metrics.pulseRadius * safeScale,
|
||||
indicatorOffset: metrics.indicatorOffset * safeScale,
|
||||
indicatorSize: metrics.indicatorSize * safeScale,
|
||||
ringWidth: Math.max(2, metrics.ringWidth * (0.96 + (safeScale - 1) * 0.35)),
|
||||
}
|
||||
}
|
||||
|
||||
function rotatePoint(x: number, y: number, angleRad: number): { x: number; y: number } {
|
||||
const cos = Math.cos(angleRad)
|
||||
const sin = Math.sin(angleRad)
|
||||
return {
|
||||
x: x * cos - y * sin,
|
||||
y: x * sin + y * cos,
|
||||
}
|
||||
}
|
||||
|
||||
function hexToRgbTuple(hex: string): [number, number, number] {
|
||||
const safeHex = /^#[0-9a-fA-F]{6}$/.test(hex) ? hex : '#ffffff'
|
||||
return [
|
||||
parseInt(safeHex.slice(1, 3), 16),
|
||||
parseInt(safeHex.slice(3, 5), 16),
|
||||
parseInt(safeHex.slice(5, 7), 16),
|
||||
]
|
||||
}
|
||||
|
||||
function getGpsPulsePhase(
|
||||
pulseFrame: number,
|
||||
motionState: MapScene['gpsMarkerStyleConfig']['motionState'],
|
||||
): number {
|
||||
const divisor = motionState === 'idle'
|
||||
? 11.5
|
||||
: motionState === 'moving'
|
||||
? 6.2
|
||||
: motionState === 'fast-moving'
|
||||
? 4.3
|
||||
: 4.8
|
||||
return 0.5 + 0.5 * Math.sin(pulseFrame / divisor)
|
||||
}
|
||||
|
||||
function getAnimatedPulseRadius(
|
||||
pulseFrame: number,
|
||||
metrics: ReturnType<typeof getGpsMarkerMetrics>,
|
||||
motionState: MapScene['gpsMarkerStyleConfig']['motionState'],
|
||||
pulseStrength: number,
|
||||
motionIntensity: number,
|
||||
): number {
|
||||
const phase = getGpsPulsePhase(pulseFrame, motionState)
|
||||
const baseRadius = motionState === 'idle'
|
||||
? metrics.pulseRadius * 0.82
|
||||
: motionState === 'moving'
|
||||
? metrics.pulseRadius * 0.94
|
||||
: motionState === 'fast-moving'
|
||||
? metrics.pulseRadius * 1.04
|
||||
: metrics.pulseRadius
|
||||
const amplitude = motionState === 'idle'
|
||||
? metrics.pulseRadius * 0.12
|
||||
: motionState === 'moving'
|
||||
? metrics.pulseRadius * 0.18
|
||||
: motionState === 'fast-moving'
|
||||
? metrics.pulseRadius * 0.24
|
||||
: metrics.pulseRadius * 0.2
|
||||
return baseRadius + amplitude * phase * (0.8 + pulseStrength * 0.18 + motionIntensity * 0.1)
|
||||
}
|
||||
|
||||
function buildVectorCamera(scene: MapScene): CameraState {
|
||||
return {
|
||||
centerWorldX: scene.exactCenterWorldX,
|
||||
@@ -32,35 +135,156 @@ export class GpsLayer implements MapLayer {
|
||||
|
||||
draw(context: LayerRenderContext): void {
|
||||
const { ctx, scene, pulseFrame } = context
|
||||
if (!scene.gpsMarkerStyleConfig.visible) {
|
||||
return
|
||||
}
|
||||
const gpsScreenPoint = this.projectPoint(scene)
|
||||
if (!gpsScreenPoint) {
|
||||
return
|
||||
}
|
||||
|
||||
const pulse = this.getPulseRadius(pulseFrame)
|
||||
const metrics = scaleGpsMarkerMetrics(
|
||||
getGpsMarkerMetrics(scene.gpsMarkerStyleConfig.size),
|
||||
scene.gpsMarkerStyleConfig.effectScale || 1,
|
||||
)
|
||||
const style = scene.gpsMarkerStyleConfig.style
|
||||
const hasBadgeLogo = style === 'badge' && !!scene.gpsMarkerStyleConfig.logoUrl
|
||||
const pulseStrength = Math.max(0.45, Math.min(1.85, scene.gpsMarkerStyleConfig.pulseStrength || 1))
|
||||
const motionState = scene.gpsMarkerStyleConfig.motionState || 'idle'
|
||||
const motionIntensity = Math.max(0, Math.min(1.2, scene.gpsMarkerStyleConfig.motionIntensity || 0))
|
||||
const wakeStrength = Math.max(0, Math.min(1, scene.gpsMarkerStyleConfig.wakeStrength || 0))
|
||||
const warningGlowStrength = Math.max(0, Math.min(1, scene.gpsMarkerStyleConfig.warningGlowStrength || 0))
|
||||
const indicatorScale = Math.max(0.86, Math.min(1.28, scene.gpsMarkerStyleConfig.indicatorScale || 1))
|
||||
const pulse = style === 'dot'
|
||||
? metrics.pulseRadius * 0.82
|
||||
: getAnimatedPulseRadius(pulseFrame, metrics, motionState, pulseStrength, motionIntensity)
|
||||
const [markerR, markerG, markerB] = hexToRgbTuple(scene.gpsMarkerStyleConfig.colorHex)
|
||||
|
||||
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()
|
||||
if (wakeStrength > 0.05 && scene.gpsHeadingDeg !== null) {
|
||||
const headingScreenRad = (scene.gpsHeadingDeg * Math.PI / 180) - scene.rotationRad
|
||||
const wakeHeadingRad = headingScreenRad + Math.PI
|
||||
const wakeCount = motionState === 'fast-moving' ? 3 : 2
|
||||
for (let index = 0; index < wakeCount; index += 1) {
|
||||
const offset = metrics.coreRadius * (0.85 + index * 0.64) * (0.9 + wakeStrength * 0.72)
|
||||
const center = rotatePoint(0, -offset, wakeHeadingRad)
|
||||
const radius = metrics.coreRadius * Math.max(0.22, 0.58 - index * 0.12 + wakeStrength * 0.08)
|
||||
const alpha = Math.max(0.06, (0.14 + wakeStrength * 0.12) * (1 - index * 0.26))
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = `rgba(${markerR}, ${markerG}, ${markerB}, ${alpha})`
|
||||
ctx.arc(gpsScreenPoint.x + center.x, gpsScreenPoint.y + center.y, radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
if (warningGlowStrength > 0.04) {
|
||||
const glowPhase = getGpsPulsePhase(pulseFrame, motionState)
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = `rgba(${markerR}, ${markerG}, ${markerB}, ${0.18 + warningGlowStrength * 0.18})`
|
||||
ctx.lineWidth = Math.max(2, metrics.ringWidth * (1.04 + warningGlowStrength * 0.2))
|
||||
ctx.arc(
|
||||
gpsScreenPoint.x,
|
||||
gpsScreenPoint.y,
|
||||
metrics.ringRadius * (1.18 + warningGlowStrength * 0.12 + glowPhase * 0.04),
|
||||
0,
|
||||
Math.PI * 2,
|
||||
)
|
||||
ctx.stroke()
|
||||
}
|
||||
if (style === 'beacon' || (style === 'badge' && !hasBadgeLogo)) {
|
||||
ctx.beginPath()
|
||||
const pulseAlpha = style === 'badge'
|
||||
? Math.min(0.2, 0.08 + pulseStrength * 0.06)
|
||||
: Math.min(0.26, 0.1 + pulseStrength * 0.08)
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${pulseAlpha})`
|
||||
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()
|
||||
if (style === 'dot') {
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.82, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
|
||||
ctx.lineWidth = metrics.ringWidth
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius + metrics.ringWidth * 0.3, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
} else if (style === 'disc') {
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
|
||||
ctx.lineWidth = metrics.ringWidth * 1.18
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius * 1.05, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 1.02, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.96)'
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.22, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
} else if (style === 'badge') {
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.98)'
|
||||
ctx.lineWidth = metrics.ringWidth * 1.12
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius * 1.06, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.98, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
if (!hasBadgeLogo) {
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.16)'
|
||||
ctx.arc(gpsScreenPoint.x - metrics.coreRadius * 0.16, gpsScreenPoint.y - metrics.coreRadius * 0.22, metrics.coreRadius * 0.18, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
} else {
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
|
||||
ctx.lineWidth = metrics.ringWidth
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.22)'
|
||||
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.18, 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()
|
||||
if (scene.gpsHeadingDeg !== null && scene.gpsMarkerStyleConfig.showHeadingIndicator) {
|
||||
const headingScreenRad = (scene.gpsHeadingDeg * Math.PI / 180) - scene.rotationRad
|
||||
const indicatorBaseDistance = metrics.ringRadius + metrics.indicatorOffset
|
||||
const indicatorSize = metrics.indicatorSize * indicatorScale
|
||||
const indicatorTipDistance = indicatorBaseDistance + indicatorSize * 0.92
|
||||
const tip = rotatePoint(0, -indicatorTipDistance, headingScreenRad)
|
||||
const left = rotatePoint(-indicatorSize * 0.56, -indicatorBaseDistance, headingScreenRad)
|
||||
const right = rotatePoint(indicatorSize * 0.56, -indicatorBaseDistance, headingScreenRad)
|
||||
const alpha = scene.gpsHeadingAlpha
|
||||
|
||||
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.beginPath()
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${Math.max(0.42, alpha)})`
|
||||
ctx.moveTo(gpsScreenPoint.x + tip.x, gpsScreenPoint.y + tip.y)
|
||||
ctx.lineTo(gpsScreenPoint.x + left.x, gpsScreenPoint.y + left.y)
|
||||
ctx.lineTo(gpsScreenPoint.x + right.x, gpsScreenPoint.y + right.y)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
const innerTip = rotatePoint(0, -(indicatorBaseDistance + indicatorSize * 0.72), headingScreenRad)
|
||||
const innerLeft = rotatePoint(-indicatorSize * 0.4, -(indicatorBaseDistance + 0.12), headingScreenRad)
|
||||
const innerRight = rotatePoint(indicatorSize * 0.4, -(indicatorBaseDistance + 0.12), headingScreenRad)
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = `rgba(${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(1, 3), 16)}, ${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(3, 5), 16)}, ${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(5, 7), 16)}, ${alpha})`
|
||||
ctx.moveTo(gpsScreenPoint.x + innerTip.x, gpsScreenPoint.y + innerTip.y)
|
||||
ctx.lineTo(gpsScreenPoint.x + innerLeft.x, gpsScreenPoint.y + innerLeft.y)
|
||||
ctx.lineTo(gpsScreenPoint.x + innerRight.x, gpsScreenPoint.y + innerRight.y)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,25 @@ export interface ScreenPoint {
|
||||
y: number
|
||||
}
|
||||
|
||||
function smoothTrackScreenPoints(points: ScreenPoint[]): ScreenPoint[] {
|
||||
if (points.length < 3) {
|
||||
return points
|
||||
}
|
||||
|
||||
const smoothed: ScreenPoint[] = [points[0]]
|
||||
for (let index = 1; index < points.length - 1; index += 1) {
|
||||
const prev = points[index - 1]
|
||||
const current = points[index]
|
||||
const next = points[index + 1]
|
||||
smoothed.push({
|
||||
x: prev.x * 0.2 + current.x * 0.6 + next.x * 0.2,
|
||||
y: prev.y * 0.2 + current.y * 0.6 + next.y * 0.2,
|
||||
})
|
||||
}
|
||||
smoothed.push(points[points.length - 1])
|
||||
return smoothed
|
||||
}
|
||||
|
||||
function buildVectorCamera(scene: MapScene): CameraState {
|
||||
return {
|
||||
centerWorldX: scene.exactCenterWorldX,
|
||||
@@ -31,7 +50,10 @@ export class TrackLayer implements MapLayer {
|
||||
|
||||
draw(context: LayerRenderContext): void {
|
||||
const { ctx, scene } = context
|
||||
const points = this.projectPoints(scene)
|
||||
if (scene.trackMode === 'none') {
|
||||
return
|
||||
}
|
||||
const points = smoothTrackScreenPoints(this.projectPoints(scene))
|
||||
if (!points.length) {
|
||||
return
|
||||
}
|
||||
@@ -39,34 +61,42 @@ export class TrackLayer implements MapLayer {
|
||||
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) => {
|
||||
if (scene.trackMode === 'tail') {
|
||||
const baseAlpha = 0.12 + scene.trackStyleConfig.glowStrength * 0.08
|
||||
points.forEach((screenPoint, index) => {
|
||||
if (index === 0) {
|
||||
return
|
||||
}
|
||||
const progress = index / Math.max(1, points.length - 1)
|
||||
ctx.strokeStyle = `rgba(84, 243, 216, ${baseAlpha + progress * 0.58})`
|
||||
ctx.lineWidth = 1.4 + progress * 4.2
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(points[index - 1].x, points[index - 1].y)
|
||||
ctx.lineTo(screenPoint.x, screenPoint.y)
|
||||
ctx.stroke()
|
||||
})
|
||||
const head = points[points.length - 1]
|
||||
ctx.fillStyle = 'rgba(84, 243, 216, 0.24)'
|
||||
ctx.beginPath()
|
||||
ctx.arc(screenPoint.x, screenPoint.y, 10, 0, Math.PI * 2)
|
||||
ctx.arc(head.x, head.y, 11, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.fillStyle = '#54f3d8'
|
||||
ctx.beginPath()
|
||||
ctx.arc(head.x, head.y, 5.2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
} else {
|
||||
ctx.strokeStyle = 'rgba(23, 109, 93, 0.96)'
|
||||
ctx.lineWidth = 4.2
|
||||
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 = '#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