Skip to content

Commit

Permalink
feat(ghostModal): add experimental ghost explorer (#358)
Browse files Browse the repository at this point in the history
* feat(ghostModal): add experimental ghost explorer

* chore: update package.json

* chore: update zeepkist gtr client

---------

Signed-off-by: James Harris <[email protected]>
  • Loading branch information
wopian authored Jun 25, 2023
1 parent 0e5e88a commit 097692d
Show file tree
Hide file tree
Showing 12 changed files with 506 additions and 57 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@
"@unhead/vue": "1.1.27",
"@vueuse/components": "10.2.0",
"@vueuse/schema-org": "2.2.0",
"@zeepkist/gtr-api": "3.7.0",
"@zeepkist/gtr-api": "3.7.2",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",
"ky": "0.33.3",
"pinia": "2.1.4",
"pinia-shared-state": "0.4.5",
"three": "0.153.0",
"vue": "3.3.4",
"vue-router": "4.2.2"
},
Expand All @@ -45,6 +46,7 @@
"@tsconfig/cypress": "1.0.1",
"@tsconfig/node20": "1.0.1",
"@types/node": "20.3.1",
"@types/three": "0.152.1",
"@vitejs/plugin-vue": "4.2.3",
"@vue/eslint-config-prettier": "7.1.0",
"@vue/eslint-config-typescript": "11.0.3",
Expand Down
10 changes: 10 additions & 0 deletions src/components/RecordList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@
v-for="(record, index) in records"
:key="record.screenshotUrl"
:record="record"
:ghosts="
hideTrackInfo
? [
record.ghostUrl,
...records
.map(r => r.ghostUrl)
.filter(url => url !== record.ghostUrl)
]
: [record.ghostUrl]
"
:rank="
typeof rankOffset === 'number' ? rankOffset + index + 1 : undefined
"
Expand Down
106 changes: 57 additions & 49 deletions src/components/RecordRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { LevelRecord } from '@zeepkist/gtr-api'
import { RouterLink } from 'vue-router'
import GhostModal from '~/components/modals/GhostModal.vue'
import UserBadge from '~/components/UserBadge.vue'
import {
calculateRecordPoints,
Expand All @@ -13,13 +14,15 @@
const {
record,
ghosts = [],
rank,
showUser = false,
showBadges = false,
showPoints = false,
hideTrackInfo = false
} = defineProps<{
record: LevelRecord
ghosts?: string[]
rank?: number
showUser?: boolean
showBadges?: boolean
Expand All @@ -35,60 +38,65 @@
</script>

<template>
<div
class="record"
:class="{
'has-no-track': hideTrackInfo,
'has-rank': rank
}">
<div v-if="rank" class="rank">{{ rank }}</div>
<img
v-if="!hideTrackInfo"
:src="record.level.thumbnailUrl"
:alt="`Thumbnail of ${record.level.name}`" />
<div v-if="!hideTrackInfo" class="author">
<router-link :to="{ name: 'level', params: { id: record.level.id } }">
{{ record.level.name }}
</router-link>
<div class="subtext">
By <user-badge :username="record.level.author" />
<ghost-modal :ghost-urls="ghosts">
<div
class="record"
:class="{
'has-no-track': hideTrackInfo,
'has-rank': rank
}">
<div v-if="rank" class="rank">{{ rank }}</div>
<img
v-if="!hideTrackInfo"
:src="record.level.thumbnailUrl"
:alt="`Thumbnail of ${record.level.name}`" />
<div v-if="!hideTrackInfo" class="author">
<router-link
:to="{ name: 'level', params: { id: record.level.id } }"
@click.stop>
{{ record.level.name }}
</router-link>
<div class="subtext">
By <user-badge :username="record.level.author" />
</div>
</div>
</div>
<div class="author">
<router-link
v-if="showUser"
:to="{ name: 'user', params: { steamId: record.user.steamId } }">
<user-badge :username="record.user.steamName" />
</router-link>
<div class="subtext" :title="formatDate(record.dateCreated)">
{{ formatRelativeDate(record.dateCreated) }}
<div class="author">
<router-link
v-if="showUser"
:to="{ name: 'user', params: { steamId: record.user.steamId } }"
@click.stop>
<user-badge :username="record.user.steamName" />
</router-link>
<div class="subtext" :title="formatDate(record.dateCreated)">
{{ formatRelativeDate(record.dateCreated) }}
</div>
</div>
</div>
<div>
<div class="right">{{ formatResultTime(record.time) }}</div>
<div
v-if="
showBadges &&
(record.isBest || record.isWorldRecord || !record.isValid)
"
class="record-badges">
<span v-if="record.isWorldRecord" class="wr" title="World Record"
>WR</span
>
<span v-if="record.isBest" class="pb" title="Personal Best">PB</span>
<span v-if="!record.isValid" class="any" title="Any Percentage"
>any%</span
>
<div>
<div class="right">{{ formatResultTime(record.time) }}</div>
<div
v-if="
showBadges &&
(record.isBest || record.isWorldRecord || !record.isValid)
"
class="record-badges">
<span v-if="record.isWorldRecord" class="wr" title="World Record"
>WR</span
>
<span v-if="record.isBest" class="pb" title="Personal Best">PB</span>
<span v-if="!record.isValid" class="any" title="Any Percentage"
>any%</span
>
</div>
<div v-if="showPoints" class="right subtext">
{{ calculateRecordPoints(rank ?? 1, record.level.points) }} ➤
</div>
</div>
<div v-if="showPoints" class="right subtext">
{{ calculateRecordPoints(rank ?? 1, record.level.points) }} ➤
<div v-if="false" class="actions">
<button disabled>View Ghost</button>
<button disabled>Compare</button>
</div>
</div>
<div v-if="false" class="actions">
<button disabled>View Ghost</button>
<button disabled>Compare</button>
</div>
</div>
</ghost-modal>
</template>

<style scoped lang="less">
Expand Down
149 changes: 149 additions & 0 deletions src/components/canvases/GhostCanvas.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import {
createGhosts,
createGhostScene,
createSphere,
formatResultTime
} from '~/utils'
export interface Position {
x: number
y: number
z: number
}
export interface Quaternion {
x: number
y: number
z: number
w: number
}
const { ghostUrls = [] } = defineProps<{
ghostUrls: string[]
}>()
const containerRef = ref()
const { camera, clock, controls, renderer, scene } = createGhostScene()
const { ghosts, totalDuration } = await createGhosts(scene, ghostUrls)
let longestGhost = ghosts[0]
for (const ghost of ghosts) {
if (ghost.ghost.frameCount > longestGhost.ghost.frameCount) {
longestGhost = ghost
}
}
const allPoints = ghosts.flatMap(({ points }) => points)
const { center } = createSphere(scene, allPoints)
const cameraOffsets = {
x: 1.1,
y: 1.5,
z: 1.5
}
let currentFrame = 0
let currentTime = ref(0)
onMounted(() => {
containerRef.value.append(renderer.domElement)
renderer.setSize(
containerRef.value.clientWidth,
containerRef.value.clientHeight
)
// Calculate the aspect ratio of the renderer
const aspectRatio =
containerRef.value.clientWidth / containerRef.value.clientHeight
camera.aspect = aspectRatio
animate()
})
onUnmounted(() => {
renderer.domElement.remove()
})
const animateDrawing = () => {
const elapsedTime = clock.getElapsedTime()
currentTime.value =
longestGhost.ghost.frames[0].time +
(elapsedTime / totalDuration) * totalDuration
while (
currentFrame < longestGhost.ghost.frames.length - 1 &&
longestGhost.ghost.frames[currentFrame + 1].time < currentTime.value
) {
currentFrame++
}
const leadingPosition = ghosts[0].points[currentFrame]
if (leadingPosition) {
camera.position.set(
leadingPosition.x * cameraOffsets.x,
leadingPosition.y * cameraOffsets.y,
leadingPosition.z * cameraOffsets.z
)
//camera.lookAt(leadingPosition)
} else {
const position = ghosts[0].points.at(-1) ?? center
camera.position.set(
position.x * cameraOffsets.x,
position.y * cameraOffsets.y,
position.z * cameraOffsets.z
)
//camera.lookAt(position)
}
for (const { geometry, material, ghost } of ghosts) {
const visiblePoints = Math.floor(
(ghost.frameCount / totalDuration) * totalDuration
)
const drawRange = Math.min(visiblePoints, currentFrame + 1)
material.visible = true
geometry.setDrawRange(0, drawRange)
}
}
function animate() {
const hasNextFrame = currentFrame < longestGhost.ghost.frames.length - 1
if (hasNextFrame) animateDrawing()
controls.enableZoom = true
controls.enableRotate = true
controls.enablePan = true
camera.updateProjectionMatrix()
controls.update()
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
</script>

<template>
<div>{{ formatResultTime(currentTime) }}</div>
<div ref="containerRef" :class="$style.container"></div>
</template>

<style module lang="less">
.container {
z-index: -1;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
Loading

0 comments on commit 097692d

Please sign in to comment.