Skip to content

Commit

Permalink
Support for animated GIFs #323
Browse files Browse the repository at this point in the history
  • Loading branch information
m-mohr committed Jul 26, 2024
1 parent 2e0777a commit c9fb8a5
Showing 1 changed file with 114 additions and 32 deletions.
146 changes: 114 additions & 32 deletions src/components/viewer/ImageViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@
<template v-if="error">{{ error }}</template>
<template v-else>
<div class="toolbar">
<span class="value" :title="valueTitle">{{ valueText }}</span>
<span v-if="!play" class="value" :title="valueTitle">{{ valueText }}</span>
<FullscreenButton class="fullscreen-button" :element="() => $refs.imageViewer" @changed="fullscreenToggled" />
<button class="play-button" @click.prevent.stop="togglePlay" title="Play animation (if available)">
<i v-if="play" class="fas fa-stop"></i>
<i v-else class="fas fa-play"></i>
</button>
</div>
<div v-show="!context" class="no-data"><i class="fas fa-spinner fa-spin"></i> Loading image...</div>
<canvas v-show="context" ref="canvas" :class="{'fullsize': fullSize}" :title="title" @click="resize" @mousemove="getPixelValue" @mouseout="resetPixelValue" />
<canvas v-show="context && !play" ref="canvas" :style="style" :title="title" @click.prevent.stop="resize" @mousemove="getPixelValue" @mouseout="resetPixelValue" />
<iframe v-show="context && play" ref="iframe">
<body style="margin: auto; width: 100%; height: 100%; text-align: center;" ref="body">
<img ref="image" :style="style" :title="title" @click.prevent.stop="resize" />
</body>
</iframe>
</template>
</div>
</template>

<script>
import FullscreenButton from '../FullscreenButton.vue';
import Utils from '../../utils';
const unknown = '-';
export default {
name: 'ImageViewer',
Expand All @@ -34,22 +44,35 @@ export default {
img: null,
error: null,
context: null,
value: '-'
value: unknown,
play: false
};
},
async mounted() {
this.$emit('mounted', this);
try {
this.img = await this.data.getData();
this.$refs.canvas.width = this.img.naturalWidth;
this.$refs.canvas.height = this.img.naturalHeight;
this.context = this.$refs.canvas.getContext('2d', {willReadFrequently: true});
this.context.drawImage(this.img, 0, 0);
} catch (error) {
this.error = error;
}
this.img = await this.data.getData();
},
computed: {
style() {
if (this.fullSize || this.fullScreen) {
return {
"max-width": "none",
"max-height": "none",
"object-fit": "none",
"cursor": this.fullScreen ? "auto" : "zoom-out",
"box-sizing": "border-box"
};
}
else {
return {
"max-width": "100%",
"max-height": "100%",
"cursor": "zoom-in",
"object-fit": "contain",
"box-sizing": "border-box"
};
}
},
title() {
if (this.fullScreen) {
return "";
Expand All @@ -68,7 +91,45 @@ export default {
}
}
},
watch: {
img: {
immediate: true,
handler() {
this.updateContent();
}
},
play: {
immediate: true,
handler() {
this.updateContent();
}
}
},
methods: {
updateContent() {
if (!this.img) {
return;
}
try {
if (this.play) {
this.$refs.image.src = this.img.src;
this.$refs.iframe.contentWindow.document.body = this.$refs.body;
this.$refs.iframe.style.width = `${this.img.naturalWidth}px`;
this.$refs.iframe.style.height = `${this.img.naturalHeight}px`;
}
else {
this.$refs.canvas.width = this.img.naturalWidth;
this.$refs.canvas.height = this.img.naturalHeight;
this.context = this.$refs.canvas.getContext('2d', {willReadFrequently: true});
this.context.drawImage(this.img, 0, 0);
}
} catch (error) {
this.error = error;
}
},
togglePlay() {
this.play = !this.play;
},
fullscreenToggled(open) {
this.fullScreen = open;
},
Expand All @@ -78,20 +139,31 @@ export default {
}
},
resetPixelValue() {
this.value = '-';
this.value = unknown;
},
getPixelValue(event) {
try {
let size = this.$refs.canvas.getBoundingClientRect();
let xScale = this.img.naturalWidth / size.width;
let yScale = this.img.naturalHeight / size.height;
let x = event.offsetX * xScale;
let y = event.offsetY * yScale;
let rgba = this.context.getImageData(Math.ceil(x), Math.ceil(y), 1, 1).data;
this.value = Utils.displayRGBA(rgba);
const size = this.$refs.canvas.getBoundingClientRect();
const xScale = this.img.naturalWidth / size.width;
const yScale = this.img.naturalHeight / size.height;
const x = event.offsetX * xScale;
const y = event.offsetY * yScale;
const rgba = Array.from(this.context.getImageData(Math.ceil(x), Math.ceil(y), 1, 1).data);
const alpha = rgba.pop();
// Fully transparent
if (alpha === 0) {
this.value = 'no data';
}
// Grayscale (all values are the same)
else if (rgba.every(v => v === rgba[0])) {
this.value = rgba[0];
}
// RGB and others
else {
this.value = rgba.join(' / ');
}
} catch (error) {
this.value = 'n/a';
console.log(error);
this.value = unknown;
}
}
}
Expand All @@ -109,40 +181,50 @@ export default {
}
.toolbar {
z-index: 1;
position: sticky;
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 3em;
padding: 1em;
box-sizing: border-box;
text-align: left;
}
.fullscreen-button {
.fullscreen-button,
.play-button {
float: right;
}
.play-button {
margin-right: 0.5em;
}
.value {
display: inline-block;
background-color: rgba(255,255,255, 0.5);
border-radius: 5px;
padding: 5px;
margin: -5px;
}
canvas {
iframe, canvas {
border: none;
margin-top: -3em !important;
box-sizing: border-box;
}
canvas,
iframe {
max-width: 100%;
max-height: 100%;
margin: auto;
cursor: zoom-in;
object-fit: contain;
box-sizing: border-box;
background-color: white;
}
canvas.fullsize, .fullscreen canvas {
canvas.fullsize,
iframe.fullsize,
.fullscreen canvas,
.fullscreen iframe {
max-width: none;
max-height: none;
object-fit: none;
cursor: zoom-out;
}
.fullscreen canvas {
cursor: auto;
}
</style>

0 comments on commit c9fb8a5

Please sign in to comment.