feat(frontend): implements stiched 3d previews
This commit is contained in:
@@ -3,22 +3,35 @@
|
||||
:style="`position: relative; height: ${height}px;`"
|
||||
:class="`${$vuetify.theme.dark ? 'grey darken-4' : 'grey lighten-4'}`"
|
||||
@mouseenter="hovered = true"
|
||||
@touchmove="parseTouch"
|
||||
@mouseleave="
|
||||
hovered = false
|
||||
imageIndex = 0
|
||||
"
|
||||
@mouseleave="hovered = false"
|
||||
@mousemove="setIndex"
|
||||
@touchmove="
|
||||
(e) =>
|
||||
setIndex({
|
||||
target: e.target,
|
||||
clientX: e.touches[0].clientX,
|
||||
clientY: e.touches[0].clientY
|
||||
})
|
||||
"
|
||||
>
|
||||
<!--
|
||||
Note: alternate style, sketchfab inspired! We are controlling image display via manipulating
|
||||
bg image props position. Results in less dom elements, and no flickering!
|
||||
-->
|
||||
<div :style="bgStyle"></div>
|
||||
<v-fade-transition>
|
||||
<div
|
||||
v-show="!(fullPreviewImage && hovered) || legacyMode"
|
||||
ref="image_360"
|
||||
:style="background"
|
||||
></div>
|
||||
</v-fade-transition>
|
||||
|
||||
<div
|
||||
ref="image_360"
|
||||
xxxv-show="!legacyMode && hovered && fullPreviewImage"
|
||||
:style="background360"
|
||||
></div>
|
||||
|
||||
<v-progress-linear
|
||||
v-show="loading"
|
||||
indeterminate
|
||||
height="2"
|
||||
height="4"
|
||||
style="position: absolute; bottom: 0"
|
||||
/>
|
||||
</div>
|
||||
@@ -36,7 +49,7 @@ export default {
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 180
|
||||
default: 200
|
||||
},
|
||||
rotate: {
|
||||
type: Boolean,
|
||||
@@ -45,68 +58,97 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMounted: false,
|
||||
loading: false,
|
||||
hovered: false,
|
||||
hasStartedLoadingImages: false,
|
||||
currentPreviewImg: '',
|
||||
previewImages: [],
|
||||
previewImage: null,
|
||||
fullPreviewImage: null,
|
||||
imageIndex: 0,
|
||||
legacyMode: false,
|
||||
angles: [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
|
||||
22, 23, 0
|
||||
]
|
||||
controller: new AbortController()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
revImg() {
|
||||
return this.legacyMode ? this.previewImages : [...this.previewImages].reverse()
|
||||
background() {
|
||||
return `
|
||||
position: absolute;
|
||||
top:0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-image:url(${this.previewImage});
|
||||
background-position: center center;
|
||||
`
|
||||
},
|
||||
bgStyle() {
|
||||
let bgStyle = `
|
||||
background360() {
|
||||
let background = `
|
||||
position: absolute;
|
||||
opacity: ${!this.legacyMode && this.hovered && this.fullPreviewImage ? 1 : 0};
|
||||
transition: opacity 0.5s;
|
||||
top:0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-image:`
|
||||
if (this.fullPreviewImage && (this.hovered || true)) {
|
||||
background += `url(${this.fullPreviewImage});`
|
||||
} else {
|
||||
background += `url(${this.previewImage});`
|
||||
}
|
||||
if (!this.isMounted) return background
|
||||
const scaleFactor = this.$refs.image_360.getBoundingClientRect().height / 400
|
||||
const actualWidth = scaleFactor * 700
|
||||
const widthDiff =
|
||||
(this.$refs.image_360.getBoundingClientRect().width - actualWidth) * 0.5
|
||||
|
||||
for (let i = 0; i < this.revImg.length; i++) {
|
||||
bgStyle += `url(${this.revImg[i]})${i !== this.revImg.length - 1 ? ',' : ';'}`
|
||||
if (this.fullPreviewImage && (this.hovered || true)) {
|
||||
background += `background-position: ${-(
|
||||
actualWidth * (2 * this.imageIndex + 1) -
|
||||
widthDiff
|
||||
)}px 0;`
|
||||
} else {
|
||||
background += `background-position: center center;`
|
||||
}
|
||||
bgStyle += `
|
||||
background-position:`
|
||||
for (let i = 0; i < this.revImg.length; i++) {
|
||||
bgStyle += `${i === this.imageIndex ? 'center' : '10000px'}${
|
||||
i !== this.revImg.length - 1 ? ',' : ';'
|
||||
}`
|
||||
}
|
||||
return bgStyle
|
||||
return background
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
hovered(val) {
|
||||
if (!this.rotate) return
|
||||
if (!val || this.hasStartedLoadingImages) return
|
||||
this.getOtherAngles()
|
||||
this.hasStartedLoadingImages = true
|
||||
if (val && !this.fullPreviewImage) {
|
||||
setTimeout(async () => {
|
||||
if (!this.hovered) return
|
||||
if (this.legacyMode) return
|
||||
if (this.fullPreviewImage) return
|
||||
this.loading = true
|
||||
try {
|
||||
this.fullPreviewImage = await this.getPreviewImage('all')
|
||||
} catch (err) {
|
||||
if (err.toString() === 'Failed getting preview') {
|
||||
this.legacyMode = true
|
||||
}
|
||||
// else: we've aborted the request due to mouse moving out
|
||||
}
|
||||
this.loading = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
if (!val && this.loading) {
|
||||
this.controller.abort()
|
||||
this.controller = new AbortController()
|
||||
this.loading = false
|
||||
}
|
||||
if (!val) {
|
||||
this.imageIndex = 0
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.previewImages.push(await this.getPreviewImage())
|
||||
this.previewImage = await this.getPreviewImage()
|
||||
this.isMounted = true
|
||||
},
|
||||
methods: {
|
||||
parseTouch(e) {
|
||||
if (this.loading || !this.rotate) {
|
||||
this.imageIndex = 0
|
||||
return
|
||||
}
|
||||
this.hovered = true
|
||||
this.setIndex({
|
||||
target: e.target,
|
||||
clientX: e.touches[0].clientX,
|
||||
clientY: e.touches[0].clientY
|
||||
})
|
||||
},
|
||||
setIndex(e) {
|
||||
if (this.loading || !this.rotate) {
|
||||
this.imageIndex = 0
|
||||
@@ -114,13 +156,14 @@ export default {
|
||||
}
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const step = rect.width / this.previewImages.length
|
||||
const step = rect.width / 24
|
||||
let index = Math.round(x / step)
|
||||
if (index >= this.previewImages.length) index = this.previewImages.length - 1
|
||||
if (index >= 24) index = 24 - 1
|
||||
this.imageIndex = index
|
||||
},
|
||||
async getPreviewImage(angle = 0) {
|
||||
const res = await fetch(this.url + `/${angle}`, {
|
||||
signal: this.controller.signal,
|
||||
headers: localStorage.getItem('AuthToken')
|
||||
? { Authorization: `Bearer ${localStorage.getItem('AuthToken')}` }
|
||||
: {}
|
||||
@@ -131,44 +174,6 @@ export default {
|
||||
}
|
||||
const blob = await res.blob()
|
||||
return URL.createObjectURL(blob)
|
||||
},
|
||||
async getOtherAngles() {
|
||||
// Note: previously, previews were generated for only 5 angles (-30deg, -15, 0, 15, 30deg), corresponding to
|
||||
// labelled angles (-2, -1, 0, 1, 2). We have now switched to generating full 360deg previews in increments
|
||||
// of 15 deg, going clockwise straight. Eg., 0 = 0deg, 1 = 15deg, 2 = 30 deg, etc.
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const img = await this.getPreviewImage(this.angles[this.angles.length - 1]) // note: this throws if requesting an incorrect angle.
|
||||
this.$set(this.previewImages, this.angles.length - 1, img)
|
||||
const promises = []
|
||||
|
||||
for (let i = 1; i < this.angles.length; i++) {
|
||||
promises.push(this.getPreviewImage(this.angles[i]))
|
||||
}
|
||||
|
||||
const otherImgs = await Promise.all(promises)
|
||||
for (let i = 0; i < otherImgs.length; i++) {
|
||||
this.$set(this.previewImages, i + 1, otherImgs[i])
|
||||
}
|
||||
} catch (e) {
|
||||
// legacy track
|
||||
this.legacyMode = true
|
||||
const otherImgs = await Promise.all([
|
||||
this.getPreviewImage(-1),
|
||||
this.getPreviewImage(-2),
|
||||
this.getPreviewImage(1),
|
||||
this.getPreviewImage(2)
|
||||
])
|
||||
this.previewImages.unshift(otherImgs[0])
|
||||
this.previewImages.unshift(otherImgs[1])
|
||||
this.previewImages.push(otherImgs[2])
|
||||
this.previewImages.push(otherImgs[3])
|
||||
// TODO: Weird. getPreviewImage throws, but the try block keeps going until the for loop,
|
||||
// resulting in an incorrect array; we need to filter out nulls/undefineds here...
|
||||
this.previewImages = this.previewImages.filter((i) => !!i)
|
||||
}
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,25 +183,4 @@ export default {
|
||||
transition: all 0.3s;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
opacity: 0.8;
|
||||
object-fit: cover;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-img:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stream-link a {
|
||||
/* color: inherit; */
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stream-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user