feat(imagery): show viewable area when zoomed (#5877)

* feat: viewable area

* chore: add test

* fix: get image ref when real-time

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
This commit is contained in:
子瞻 Luci 2022-11-22 05:06:12 +08:00 committed by GitHub
parent a79646a915
commit 1ddf5e5137
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 136 additions and 1 deletions

View File

@ -31,21 +31,32 @@
:title="image.formattedTime" :title="image.formattedTime"
> >
<a <a
class="c-thumb__image-wrapper"
href="" href=""
:download="image.imageDownloadName" :download="image.imageDownloadName"
@click.prevent @click.prevent
> >
<img <img
ref="img"
class="c-thumb__image" class="c-thumb__image"
:src="image.url" :src="image.url"
fetchpriority="low" fetchpriority="low"
@load="imageLoadCompleted"
> >
</a> </a>
<div
v-if="viewableArea"
class="c-thumb__viewable-area"
:style="viewableAreaStyle"
></div>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div> <div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div> </div>
</template> </template>
<script> <script>
const THUMB_PADDING = 4;
const BORDER_WIDTH = 2;
export default { export default {
props: { props: {
image: { image: {
@ -63,6 +74,77 @@ export default {
realTime: { realTime: {
type: Boolean, type: Boolean,
required: true required: true
},
viewableArea: {
type: Object,
default: function () {
return null;
}
}
},
data() {
return {
imgWidth: 0,
imgHeight: 0
};
},
computed: {
viewableAreaStyle() {
if (!this.viewableArea || !this.imgWidth || !this.imgHeight) {
return null;
}
const { widthRatio, heightRatio, xOffsetRatio, yOffsetRatio } = this.viewableArea;
const imgWidth = this.imgWidth;
const imgHeight = this.imgHeight;
let translateX = imgWidth * xOffsetRatio;
let translateY = imgHeight * yOffsetRatio;
let width = imgWidth * widthRatio;
let height = imgHeight * heightRatio;
if (translateX < 0) {
width += translateX;
translateX = 0;
}
if (translateX + width > imgWidth) {
width = imgWidth - translateX;
}
if (translateX + 2 * BORDER_WIDTH > imgWidth) {
translateX = imgWidth - 2 * BORDER_WIDTH;
}
if (translateY < 0) {
height += translateY;
translateY = 0;
}
if (translateY + height > imgHeight) {
height = imgHeight - translateY;
}
if (translateY + 2 * BORDER_WIDTH > imgHeight) {
translateY = imgHeight - 2 * BORDER_WIDTH;
}
return {
'transform': `translate(${translateX + THUMB_PADDING}px, ${translateY + THUMB_PADDING}px)`,
'width': `${width}px`,
'height': `${height}px`
};
}
},
methods: {
imageLoadCompleted() {
if (!this.$refs.img) {
return;
}
const { width: imgWidth, height: imgHeight } = this.$refs.img;
this.imgWidth = imgWidth;
this.imgHeight = imgHeight;
} }
} }
}; };

View File

@ -174,6 +174,7 @@
:active="focusedImageIndex === index" :active="focusedImageIndex === index"
:selected="focusedImageIndex === index && isPaused" :selected="focusedImageIndex === index && isPaused"
:real-time="!isFixed" :real-time="!isFixed"
:viewable-area="focusedImageIndex === index ? viewableArea : null"
@click.native="thumbnailClicked(index)" @click.native="thumbnailClicked(index)"
/> />
</div> </div>
@ -219,6 +220,8 @@ const ZOOM_SCALE_DEFAULT = 1;
const SHOW_THUMBS_THRESHOLD_HEIGHT = 200; const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600; const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
const IMAGE_CONTAINER_BORDER_WIDTH = 1;
export default { export default {
name: 'ImageryView', name: 'ImageryView',
components: { components: {
@ -281,6 +284,8 @@ export default {
}, },
imageTranslateX: 0, imageTranslateX: 0,
imageTranslateY: 0, imageTranslateY: 0,
imageViewportWidth: 0,
imageViewportHeight: 0,
pan: undefined, pan: undefined,
animateZoom: true, animateZoom: true,
imagePanned: false, imagePanned: false,
@ -516,6 +521,23 @@ export default {
} }
return 'Alt drag to pan'; return 'Alt drag to pan';
},
viewableArea() {
if (this.zoomFactor === 1) {
return null;
}
const imageWidth = this.sizedImageWidth * this.zoomFactor;
const imageHeight = this.sizedImageHeight * this.zoomFactor;
const xOffset = (imageWidth - this.imageViewportWidth) / 2;
const yOffset = (imageHeight - this.imageViewportHeight) / 2;
return {
widthRatio: this.imageViewportWidth / imageWidth,
heightRatio: this.imageViewportHeight / imageHeight,
xOffsetRatio: (xOffset - this.imageTranslateX * this.zoomFactor) / imageWidth,
yOffsetRatio: (yOffset - this.imageTranslateY * this.zoomFactor) / imageHeight
};
} }
}, },
watch: { watch: {
@ -1063,12 +1085,12 @@ export default {
} }
this.setSizedImageDimensions(); this.setSizedImageDimensions();
this.setImageViewport();
this.calculateViewHeight(); this.calculateViewHeight();
this.scrollToFocused(); this.scrollToFocused();
}, },
setSizedImageDimensions() { setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight; this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) { if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image // container is wider than image
this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio; this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
@ -1079,6 +1101,17 @@ export default {
this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio; this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
} }
}, },
setImageViewport() {
if (this.imageContainerHeight > this.sizedImageHeight + IMAGE_CONTAINER_BORDER_WIDTH) {
// container is taller than wrapper
this.imageViewportWidth = this.sizedImageWidth;
this.imageViewportHeight = this.sizedImageHeight;
} else {
// container is wider than wrapper
this.imageViewportWidth = this.imageContainerWidth;
this.imageViewportHeight = this.imageContainerHeight;
}
},
handleThumbWindowResizeStart() { handleThumbWindowResizeStart() {
if (!this.autoScroll) { if (!this.autoScroll) {
return; return;

View File

@ -285,6 +285,13 @@
flex: 0 0 auto; flex: 0 0 auto;
padding: 2px 3px; padding: 2px 3px;
} }
&__viewable-area {
position: absolute;
border: 2px yellow solid;
left: 0;
top: 0;
}
} }
.is-small-thumbs { .is-small-thumbs {

View File

@ -529,6 +529,19 @@ describe("The Imagery View Layouts", () => {
done(); done();
}); });
it('should display the viewable area when zoom factor is greater than 1', async () => {
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
parent.querySelector('.t-btn-zoom-in').click();
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(1);
parent.querySelector('.t-btn-zoom-reset').click();
await Vue.nextTick();
expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0);
});
it('should reset the brightness and contrast when clicking the reset button', async () => { it('should reset the brightness and contrast when clicking the reset button', async () => {
const viewInstance = imageryView._getInstance(); const viewInstance = imageryView._getInstance();
await Vue.nextTick(); await Vue.nextTick();