mirror of
https://github.com/swingmx/webclient.git
synced 2025-12-24 19:30:20 +00:00
blurhash draft 2
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"@vueuse/integrations": "^9.2.0",
|
||||
"@vueuse/motion": "^2.0.0",
|
||||
"axios": "^0.26.1",
|
||||
"blurhash": "^2.0.5",
|
||||
"fuse.js": "^6.6.2",
|
||||
"motion": "^10.15.5",
|
||||
"node-vibrant": "3.1.6",
|
||||
|
||||
@@ -17,138 +17,75 @@
|
||||
<img
|
||||
v-for="(img, index) in images"
|
||||
:key="img.key"
|
||||
:ref="el => setImageRef(img.key, el)"
|
||||
:src="img.src"
|
||||
:class="`${index === activeIndex ? 'active' : ''} il-image ${imgClass || ''}`"
|
||||
:class="`${index === activeIndex && readyToShowKeys.has(img.key) ? 'active' : ''} il-image ${
|
||||
imgClass || ''
|
||||
}`"
|
||||
:style="{
|
||||
transitionDuration: `${duration}ms`,
|
||||
}"
|
||||
@load="onImageLoad(img.key, $event)"
|
||||
@load="onDomImageLoad(img.key, $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { decode } from 'blurhash'
|
||||
import { computed, ref, watch, nextTick, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
image: string
|
||||
duration: number
|
||||
blurhash?: string
|
||||
imgClass?: string
|
||||
duration: number
|
||||
preloadImage?: string
|
||||
}>()
|
||||
|
||||
const imageKey = ref(0)
|
||||
const activeIndex = ref(0)
|
||||
const imageLoaded = ref(false)
|
||||
const readyToShowKeys = ref<Set<number>>(new Set())
|
||||
const imageHeights = ref<Record<number, number>>({})
|
||||
const imageLoader = ref<HTMLDivElement | null>(null)
|
||||
const blurhashCanvas = ref<HTMLCanvasElement | null>(null)
|
||||
const images = ref<Array<{ src: string; key: number }>>([])
|
||||
const activeIndex = ref(0)
|
||||
const imageKey = ref(0)
|
||||
const preloadedImageUrl = ref<string | null>(null)
|
||||
const preloadImageElement = ref<HTMLImageElement | null>(null)
|
||||
const imageHeights = ref<Record<number, number>>({})
|
||||
const imageNaturalHeights = ref<Record<number, number>>({})
|
||||
const containerWidth = ref(0)
|
||||
const imageLoaded = ref(false)
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
function updateContainerWidth() {
|
||||
if (imageLoader.value) {
|
||||
containerWidth.value = imageLoader.value.clientWidth
|
||||
if (containerWidth.value) {
|
||||
recalculateHeights(containerWidth.value)
|
||||
if (props.blurhash) {
|
||||
renderBlurhash()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recalculateHeights(width: number) {
|
||||
const updated: Record<number, number> = {}
|
||||
Object.entries(imageNaturalHeights.value).forEach(([key, naturalHeight]) => {
|
||||
updated[Number(key)] = Math.min(width, naturalHeight)
|
||||
})
|
||||
if (Object.keys(updated).length) {
|
||||
imageHeights.value = { ...imageHeights.value, ...updated }
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateContainerWidth()
|
||||
if (typeof ResizeObserver !== 'undefined' && imageLoader.value) {
|
||||
resizeObserver = new ResizeObserver(updateContainerWidth)
|
||||
resizeObserver.observe(imageLoader.value)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.preloadImage,
|
||||
newPreloadImage => {
|
||||
if (!newPreloadImage) {
|
||||
preloadedImageUrl.value = null
|
||||
preloadImageElement.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (preloadedImageUrl.value === newPreloadImage) {
|
||||
return
|
||||
}
|
||||
|
||||
preloadedImageUrl.value = newPreloadImage
|
||||
const img = new Image()
|
||||
img.src = newPreloadImage
|
||||
preloadImageElement.value = img
|
||||
}
|
||||
)
|
||||
const imageRefs = ref<Record<number, HTMLImageElement | null>>({})
|
||||
const containerWidth = computed(() => imageLoader.value?.clientWidth)
|
||||
|
||||
watch(
|
||||
() => props.image,
|
||||
newImage => {
|
||||
async newImage => {
|
||||
if (!newImage) return
|
||||
renderBlurhash()
|
||||
|
||||
imageLoaded.value = false
|
||||
readyToShowKeys.value.clear()
|
||||
|
||||
if (preloadedImageUrl.value === newImage && preloadImageElement.value?.complete) {
|
||||
preloadedImageUrl.value = null
|
||||
preloadImageElement.value = null
|
||||
} else if (preloadedImageUrl.value !== newImage) {
|
||||
preloadedImageUrl.value = null
|
||||
preloadImageElement.value = null
|
||||
}
|
||||
|
||||
const imageKeyValue = imageKey.value++
|
||||
const newImageObj = {
|
||||
src: newImage,
|
||||
key: imageKey.value++,
|
||||
key: imageKeyValue,
|
||||
}
|
||||
|
||||
images.value.push(newImageObj)
|
||||
|
||||
if (images.value.length > 2) {
|
||||
images.value.shift()
|
||||
const removedImage = images.value.shift()
|
||||
if (removedImage) {
|
||||
delete imageRefs.value[removedImage.key]
|
||||
readyToShowKeys.value.delete(removedImage.key)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
activeIndex.value = images.value.length - 1
|
||||
}, 10)
|
||||
|
||||
await loadImageManually(newImage, imageKeyValue)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.blurhash, containerWidth.value, imageHeights.value],
|
||||
() => {
|
||||
if (props.blurhash && containerWidth.value && blurhashCanvas.value) {
|
||||
renderBlurhash()
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
function renderBlurhash() {
|
||||
if (!props.blurhash || !blurhashCanvas.value || !containerWidth.value) return
|
||||
|
||||
@@ -172,27 +109,87 @@ function renderBlurhash() {
|
||||
}
|
||||
}
|
||||
|
||||
function onImageLoad(imageKeyValue: number, event: Event) {
|
||||
const target = event.target as HTMLImageElement | null
|
||||
if (!target || !imageLoader.value) return
|
||||
|
||||
imageLoaded.value = true
|
||||
|
||||
imageNaturalHeights.value = {
|
||||
...imageNaturalHeights.value,
|
||||
[imageKeyValue]: target.naturalHeight,
|
||||
}
|
||||
|
||||
const minHeight = Math.min(imageLoader.value.clientWidth || 0, target.naturalHeight)
|
||||
imageHeights.value = { ...imageHeights.value, [imageKeyValue]: minHeight }
|
||||
|
||||
if (images.value.length > 1 && activeIndex.value === images.value.length - 1) {
|
||||
setTimeout(() => {
|
||||
images.value = images.value.slice(-1)
|
||||
activeIndex.value = 0
|
||||
}, props.duration)
|
||||
function setImageRef(key: number, el: unknown) {
|
||||
if (el && el instanceof HTMLImageElement) {
|
||||
imageRefs.value[key] = el
|
||||
} else {
|
||||
imageRefs.value[key] = null
|
||||
}
|
||||
}
|
||||
|
||||
function onDomImageLoad(imageKeyValue: number, eventOrElement: Event | HTMLImageElement) {
|
||||
if (readyToShowKeys.value.has(imageKeyValue)) return
|
||||
|
||||
const imgElement = eventOrElement instanceof Event ? (eventOrElement.target as HTMLImageElement) : eventOrElement
|
||||
|
||||
if (!imgElement || !imageLoader.value) return
|
||||
|
||||
if (imgElement.complete && imgElement.naturalHeight > 0) {
|
||||
const minHeight = Math.min(imageLoader.value?.clientWidth || 0, imgElement.naturalHeight)
|
||||
imageHeights.value = { ...imageHeights.value, [imageKeyValue]: minHeight }
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
readyToShowKeys.value.add(imageKeyValue)
|
||||
imageLoaded.value = true
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function loadImageManually(imageSrc: string, imageKeyValue: number) {
|
||||
try {
|
||||
const response = await fetch(imageSrc)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load image: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const imageUrl = URL.createObjectURL(blob)
|
||||
|
||||
await nextTick()
|
||||
|
||||
const imageIndex = images.value.findIndex(img => img.key === imageKeyValue)
|
||||
if (imageIndex !== -1) {
|
||||
const imgElement = imageRefs.value[imageKeyValue]
|
||||
if (imgElement) {
|
||||
if (imgElement.complete && imgElement.naturalHeight > 0) {
|
||||
onDomImageLoad(imageKeyValue, imgElement)
|
||||
}
|
||||
}
|
||||
images.value[imageIndex].src = imageUrl
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading image:', error)
|
||||
await nextTick()
|
||||
const imageIndex = images.value.findIndex(img => img.key === imageKeyValue)
|
||||
if (imageIndex !== -1) {
|
||||
const imgElement = imageRefs.value[imageKeyValue]
|
||||
if (imgElement) {
|
||||
if (imgElement.complete && imgElement.naturalHeight > 0) {
|
||||
onDomImageLoad(imageKeyValue, imgElement)
|
||||
}
|
||||
}
|
||||
images.value[imageIndex].src = imageSrc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => activeIndex.value,
|
||||
() => {
|
||||
if (images.value.length > 1 && activeIndex.value === images.value.length - 1) {
|
||||
setTimeout(() => {
|
||||
images.value = images.value.slice(-1)
|
||||
activeIndex.value = 0
|
||||
}, props.duration)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
renderBlurhash()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -2102,6 +2102,11 @@ bl@^4.0.3:
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
blurhash@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.5.tgz#efde729fc14a2f03571a6aa91b49cba80d1abe4b"
|
||||
integrity sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==
|
||||
|
||||
bmp-js@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233"
|
||||
|
||||
Reference in New Issue
Block a user