Skip to content

Commit

Permalink
Compass: face-lift widget using canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaellehmkuhl committed Sep 28, 2023
1 parent e0ff2af commit 23d46f2
Showing 1 changed file with 127 additions and 35 deletions.
162 changes: 127 additions & 35 deletions src/components/widgets/Compass.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
<template>
<div class="compass">
<img
:class="{ rotated: widget.options.headingStyle === HeadingStyle.HEAD_UP }"
class="compass-ticks"
src="@/assets/nautic-compass.svg"
draggable="false"
/>
<img
:class="{ rotated: widget.options.headingStyle === HeadingStyle.NORTH_UP }"
class="boat"
src="@/assets/boat.svg"
draggable="false"
/>
<div ref="compassRoot" class="compass">
<canvas
ref="canvasRef"
:height="smallestDimension"
:width="smallestDimension"
class="rounded-3xl bg-slate-950/70"
></canvas>
</div>
<Dialog v-model:show="widget.managerVars.configMenuOpen" class="w-72">
<div class="w-full h-full">
Expand All @@ -26,16 +20,37 @@
</template>

<script setup lang="ts">
import { computed, onBeforeMount, toRefs } from 'vue'
import { useElementSize } from '@vueuse/core'
import gsap from 'gsap'
import { computed, onBeforeMount, ref, toRefs } from 'vue'
import Dialog from '@/components/Dialog.vue'
import Dropdown from '@/components/Dropdown.vue'
import { degrees, radians, resetCanvas } from '@/libs/utils'
import { useMainVehicleStore } from '@/stores/mainVehicle'
import type { Widget } from '@/types/widgets'
const store = useMainVehicleStore()
const compassRoot = ref()
const canvasRef = ref<HTMLCanvasElement | undefined>()
const canvasContext = ref()
const angleStyle = computed(() => `${store.attitude.yaw ?? 0}rad`)
// Object used to store current render state
const renderVariables = {
yawAngleDegrees: 0,
}
// Angles used for the main marks
const mainAngles = {
[0]: 'N',
[45]: 'NE',
[90]: 'E',
[135]: 'SE',
[180]: 'S',
[225]: 'SW',
[270]: 'W',
[315]: 'NW',
}
/**
* Possible compass configurations.
Expand Down Expand Up @@ -64,30 +79,107 @@ onBeforeMount(() => {
}
}
})
// Calculates the smallest between the widget dimensions, so we can keep the inner content always inside it, without overlays
const { width, height } = useElementSize(compassRoot)
const smallestDimension = computed(() => (width.value < height.value ? width.value : height.value))
// Renders the updated canvas state
const renderCanvas = (): void => {
if (canvasRef.value === undefined) return
if (canvasContext.value === undefined) canvasContext.value = canvasRef.value.getContext('2d')
const ctx = canvasContext.value
resetCanvas(ctx)
const halfCanvasSize = 0.5 * smallestDimension.value
// Set canvas general properties
ctx.textAlign = 'center'
ctx.strokeStyle = 'white'
ctx.font = `bold 22px Arial`
ctx.fillStyle = 'white'
ctx.lineWidth = 6
ctx.textBaseline = 'middle'
const outerCircleRadius = halfCanvasSize - 60
const innerIndicatorRadius = halfCanvasSize - 100
const outerIndicatorRadius = halfCanvasSize - 80
// Start drawing from the center
ctx.translate(halfCanvasSize, halfCanvasSize)
// Draw central angle text
ctx.fillText(`${renderVariables.yawAngleDegrees.toFixed(0)}°`, 4, 0)
// Set 0 degrees on the top position
ctx.rotate(radians(-90))
// Draw line and identification for each angle
for (const [angleDegrees, angleName] of Object.entries(mainAngles)) {
ctx.save()
ctx.rotate(radians(Number(angleDegrees)))
ctx.beginPath()
ctx.moveTo(outerIndicatorRadius, 0)
ctx.lineTo(outerCircleRadius, 0)
// Draw angle text
ctx.translate(outerCircleRadius + 16.5, 0)
ctx.rotate(radians(90))
ctx.fillText(angleName, 0, 0)
ctx.stroke()
ctx.restore()
}
// Draw outer circle
ctx.beginPath()
ctx.arc(0, 0, outerCircleRadius, 0, radians(360))
ctx.stroke()
// Draw central indicator
ctx.rotate(radians(renderVariables.yawAngleDegrees))
ctx.beginPath()
ctx.lineWidth = 1
ctx.strokeStyle = 'red'
ctx.fillStyle = 'red'
const triangleBaseSize = 5
ctx.moveTo(innerIndicatorRadius, triangleBaseSize)
ctx.lineTo(outerIndicatorRadius - triangleBaseSize, 0)
ctx.lineTo(innerIndicatorRadius, -triangleBaseSize)
ctx.lineTo(innerIndicatorRadius, triangleBaseSize)
ctx.closePath()
ctx.fill()
ctx.stroke()
}
// Update canvas at 60fps
setInterval(() => {
const angleDegrees = degrees(store.attitude.yaw ?? 0)
const fullRangeAngleDegrees = angleDegrees < 0 ? angleDegrees + 360 : angleDegrees
const fromWestToEast = renderVariables.yawAngleDegrees > 270 && fullRangeAngleDegrees < 90
const fromEastToWest = renderVariables.yawAngleDegrees < 90 && fullRangeAngleDegrees > 270
// If cruzing 0 degrees, use a chained animation, so the pointer does not turn 360 degrees to the other side (visual artifact)
if (fromWestToEast) {
gsap.to(renderVariables, 0.05, { yawAngleDegrees: 0 })
gsap.fromTo(renderVariables, 0.05, { yawAngleDegrees: 0 }, { yawAngleDegrees: fullRangeAngleDegrees })
} else if (fromEastToWest) {
gsap.to(renderVariables, 0.05, { yawAngleDegrees: 360 })
gsap.fromTo(renderVariables, 0.05, { yawAngleDegrees: 360 }, { yawAngleDegrees: fullRangeAngleDegrees })
} else {
gsap.to(renderVariables, 0.1, { yawAngleDegrees: fullRangeAngleDegrees })
}
renderCanvas()
}, 1000 / 60)
</script>

<style scoped>
.compass {
position: relative;
}
.compass-ticks {
transition: -webkit-transform 0.2s;
user-select: none;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
opacity: 0.9;
}
.boat {
position: absolute;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
user-select: none;
height: 55%;
}
.rotated {
transform: rotate(v-bind('angleStyle'));
height: 100%;
}
</style>

0 comments on commit 23d46f2

Please sign in to comment.