-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Renamed tool template to specify that it is MCR tool specific -- at least given the tools we've added so far. Added a dependency for EditImage.vue, a component of ImageUpload.vue that is itself a component of MCRToolTemplate.vue. Removed the old MCR homepage. Added a pageContent getter for tools as well.
- Loading branch information
1 parent
b66d498
commit 377fbac
Showing
6 changed files
with
1,388 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,344 @@ | ||
<!-- | ||
################################################################################ | ||
# | ||
# File Name: EditImage.vue | ||
# Application: templates | ||
# Description: a modal that allows the user to edit the selected image. Current functionality includes cropping the image and setting the phase. | ||
# | ||
# Created by: Atul Jalan 6/20/20 | ||
# Customized for NanoMine | ||
# | ||
################################################################################ | ||
--> | ||
|
||
<template> | ||
<div class='modal' v-if='value'> | ||
<div class='image-cropper-container'> | ||
|
||
<h1>{{ computedTitle }}</h1> | ||
|
||
<!-- displayed when user opens image cropper --> | ||
<div class='imageWrapper' v-if='type === "crop"'> | ||
<cropper :src='file.url' :stencil-props='stencil_props' @change='onCropChange'></cropper> | ||
</div> | ||
|
||
<!-- instructions (varies based on use case) --> | ||
<p v-if='type === "phase"'><strong>Instructions:</strong> click on the phase within the image that you would like to be analyzed.</p> | ||
<p v-if='type === "calibrate"'><strong>Instruction:</strong> click and drag over the scale bar within the image to calibrate image size to scale bar.<p> | ||
|
||
<!-- displayed when user opens phase select --> | ||
<div class='relative imageWrapper' v-if='type === "phase"' ref='imageWrapperDiv'> | ||
<img class='image' :src='file.url' @click='onPhaseChange($event)' ref='phaseImage'> | ||
<div class='phaseDot' v-bind:style="{ top: computedTop, left: computedLeft, backgroundColor: computedBackground, border: computedBorder}"></div> | ||
</div> | ||
|
||
<!-- displayed when user opens calibration tool --> | ||
<div class='relative imageWrapper' v-if='type === "calibrate"' ref='calibrationContainer'> | ||
<img class='image' :src='file.url' draggable='false' ref='calibrationImage' @mousedown='mouseDown($event)' @mousemove='mouseMove($event)' @mouseup='mouseUp()'> | ||
<div class='calibrationLine' ref='calibrationLine' v-bind:style="{width: calibrationLine.width + 'px', top: calibrationLine.top + 'px', left: calibrationLine.left + 'px'}" @mouseup='mouseUp()'></div> | ||
</div> | ||
|
||
<!-- displayed when user opens calibration tool --> | ||
<div class='scale-bar-inputs' v-if='type === "calibrate"'> | ||
|
||
<div> | ||
<md-field> | ||
<label>Scale bar width</label> | ||
<md-input v-model="scaleBar.width" @change="calculateScale()"></md-input> | ||
</md-field> | ||
</div> | ||
|
||
<div> | ||
<md-field> | ||
<label>Scale bar units</label> | ||
<md-select v-model="scaleBar.units"> | ||
<md-option value="nanometers">Nanometers (nm)</md-option> | ||
<md-option value="micrometers">Micrometers (µM)</md-option> | ||
<md-option value="millimeters">Millimeters (mm)</md-option> | ||
</md-select> | ||
</md-field> | ||
</div> | ||
|
||
</div> | ||
|
||
<div class='image-cropper-container-buttons'> | ||
<p v-if='type === "phase"'>x-offset: {{ phase.x_offset }}</p> <!-- only displayed when user opens phase select --> | ||
<p v-if='type === "phase"'>y-offset: {{ phase.y_offset }}</p> <!-- only displayed when user opens phase select --> | ||
<p v-if='type === "calibrate"'>width: {{ calibratedDimensions.width }}</p> <!-- only displayed when user opens phase select --> | ||
<p v-if='type === "calibrate"'>height: {{ calibratedDimensions.height }}</p> <!-- only displayed when user opens phase select --> | ||
<md-button class="md-primary" @click='closeModal()'>Cancel</md-button> | ||
<md-button class="md-primary" @click='saveImage()'>Save</md-button> | ||
</div> | ||
|
||
</div> | ||
</div> | ||
</template> | ||
|
||
<script> | ||
import { Cropper } from 'vue-advanced-cropper' | ||
export default { | ||
name: 'EditImage', | ||
components: { | ||
Cropper | ||
}, | ||
props: { | ||
value: { | ||
required: true | ||
}, | ||
file: Object, | ||
type: String, | ||
aspectRatio: String | ||
}, | ||
watch: { | ||
// update phase dot information when new image is opened in modal | ||
file: { | ||
deep: true, | ||
handler (newValue, oldValue) { | ||
if (newValue.name !== oldValue.name) { | ||
this.phaseDotVisibility = false | ||
} | ||
this.phase = newValue.phase | ||
} | ||
} | ||
}, | ||
mounted () { | ||
// locks the aspect ratio at which the user can crop an image | ||
if (this.aspectRatio === 'square') { | ||
this.stencil_props.aspectRatio = 1 | ||
} else if (this.aspectRatio === 'free') { | ||
if ('aspectRatio' in this.stencil_props) { | ||
delete this.stencil_props.aspectRatio | ||
} | ||
} | ||
}, | ||
data () { | ||
return { | ||
title: '', | ||
cropped_url: null, | ||
coordinates: null, | ||
stencil_props: {}, | ||
phase: { x_offset: 0, y_offset: 0 }, | ||
phaseDotVisibility: false, | ||
calibrationLine: { | ||
width: 0, | ||
left: 0, | ||
top: 0, | ||
drawLine: false | ||
}, | ||
calibratedDimensions: { | ||
width: 0, | ||
height: 0 | ||
}, | ||
scaleBar: { | ||
width: 0, | ||
units: null | ||
} | ||
} | ||
}, | ||
methods: { | ||
onPhaseChange (e) { | ||
// takes the click offset from top left of image and multiplies that by how much the image is scaled up/down to fit the modal | ||
this.phase.x_offset = parseInt(e.offsetX * (this.file.pixelSize.width / e.target.clientWidth)) | ||
this.phase.y_offset = parseInt(e.offsetY * (this.file.pixelSize.height / e.target.clientHeight)) | ||
this.phaseDotVisibility = true | ||
}, | ||
mouseDown (e) { | ||
this.calibrationLine.top = e.offsetY | ||
this.calibrationLine.left = ((this.$refs.calibrationContainer.clientWidth - e.target.clientWidth) / 2) + e.offsetX | ||
this.calibrationLine.width = 0 | ||
this.calibrationLine.drawLine = true | ||
}, | ||
mouseMove (e) { | ||
if (this.calibrationLine.drawLine === true) { | ||
this.calibrationLine.width = (((this.$refs.calibrationContainer.clientWidth - e.target.clientWidth) / 2) + e.offsetX) - this.calibrationLine.left | ||
} | ||
if (e.offsetX > e.target.clientWidth - 10) { | ||
this.drawLine = false | ||
} | ||
}, | ||
mouseUp () { | ||
this.calibrationLine.drawLine = false | ||
this.calculateScale() | ||
}, | ||
calculateScale () { | ||
this.calibratedDimensions.width = parseInt(this.scaleBar.width * (this.$refs.calibrationImage.clientWidth / this.calibrationLine.width)) | ||
this.calibratedDimensions.height = parseInt(this.scaleBar.width * (this.$refs.calibrationImage.clientHeight / this.calibrationLine.width)) | ||
}, | ||
onCropChange ({ coordinates, canvas }) { | ||
this.cropped_url = canvas.toDataURL() | ||
this.coordinates = coordinates | ||
}, | ||
closeModal () { | ||
this.$emit('input', !this.value) | ||
}, | ||
saveImage () { | ||
if (this.type === 'crop') { | ||
this.$emit('setCroppedImage', this.cropped_url, this.file.name, this.coordinates) | ||
} else if (this.type === 'phase') { | ||
this.$emit('setPhase', this.file.name, this.phase) | ||
} else if (this.type === 'calibrate') { | ||
this.$emit('setCalibration', this.calibratedDimensions, this.scaleBar) | ||
} | ||
this.closeModal() | ||
} | ||
}, | ||
// computed variables are for the phase dot (to determine position and toggle visibility), and modal title | ||
computed: { | ||
// phase dot position is calculated from the offset from the top left corner of its parent div | ||
// gives the y offset of the phase dot | ||
computedTop: function () { | ||
if (this.$refs.phaseImage === undefined) { return this.phase.y_offset * 0 } // refs are not yet rendered on first run | ||
var scaleFactor = this.$refs.phaseImage.clientHeight / this.file.pixelSize.height // image might be scaled up/down to fit the modal. | ||
return ((this.phase.y_offset * scaleFactor) - 3) + 'px' // -3 pixels to center dot on where they click | ||
}, | ||
// gives the x offset of the phase dot | ||
computedLeft: function () { | ||
if (this.$refs.phaseImage === undefined) { return this.phase.x_offset * 0 } // refs are not yet rendered on first run | ||
var scaleFactor = this.$refs.phaseImage.clientWidth / this.file.pixelSize.width // image might be scaled up/down to fit the modal. | ||
var extraOffset = (this.$refs.imageWrapperDiv.clientWidth - this.$refs.phaseImage.clientWidth) / 2 // phase dot is anchored to the div that contains img. Div width may be larger than img width. | ||
return ((this.phase.x_offset * scaleFactor) + extraOffset - 3) + 'px' // -3 pixels to center dot on where they click | ||
}, | ||
// computed background and computed border determine whether phase dot is displayed | ||
computedBackground: function () { | ||
if (this.phaseDotVisibility === true) { | ||
return 'white' | ||
} else { | ||
return 'transparent' | ||
} | ||
}, | ||
computedBorder: function () { | ||
if (this.phaseDotVisibility === true) { | ||
return '1px solid black' | ||
} else { | ||
return '1px solid transparent' | ||
} | ||
}, | ||
// determines the title of the modal | ||
computedTitle: function () { | ||
if (this.type === 'crop') { | ||
return 'Crop image' | ||
} else if (this.type === 'phase') { | ||
return 'Set phase' | ||
} else if (this.type === 'calibrate') { | ||
return 'Scale bar calibration' | ||
} else { | ||
return '' | ||
} | ||
} | ||
} | ||
} | ||
</script> | ||
|
||
<style scoped> | ||
h1 { | ||
margin-top: 0px; | ||
background-color: black; | ||
color: white; | ||
width: 100%; | ||
border-top-left-radius: 4px; | ||
border-top-right-radius: 4px; | ||
} | ||
.modal { | ||
position: fixed; | ||
top: 0; | ||
left: 0; | ||
width: 100vw; | ||
height: 100vh; | ||
background-color: rgba(0, 0, 0, 0.3); /* dims the entire screen to make the modal stand out */ | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: center; | ||
align-items: center; | ||
z-index: 1; /* ensures that the modal appears on top of other elements */ | ||
} | ||
.image-cropper-container { | ||
width: 700px; | ||
margin-top: 48px; /* deal with extra heigh from the navigation pane */ | ||
max-width: 90%; | ||
max-height: calc(90% - 48px); | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: flex-start; | ||
align-items: center; | ||
background-color: white; | ||
border: 2px solid black; | ||
border-radius: 8px; | ||
overflow-y: auto; | ||
} | ||
.imageWrapper { | ||
width: 90%; | ||
height: 75%; | ||
margin-bottom: 20px; | ||
} | ||
.image { | ||
max-width: 100%; | ||
max-height: 100%; | ||
} | ||
.relative { | ||
position: relative; | ||
} | ||
.phaseDot { | ||
position: absolute; | ||
width: 6px; | ||
height: 6px; | ||
border-radius: 50%; | ||
} | ||
.scale-bar-inputs { | ||
display: flex; | ||
flex-direction: row; | ||
justify-content: space-between; | ||
width: 90%; | ||
} | ||
.scale-bar-inputs div { | ||
width: 98%; | ||
} | ||
.image-cropper-container-buttons { | ||
display: flex; | ||
flex-direction: row; | ||
justify-content: center; | ||
align-items: center; | ||
margin-bottom: 20px; | ||
} | ||
.image-cropper-container-buttons p { | ||
margin-bottom: 0px; | ||
font-weight: 700; | ||
margin-left: 8px; | ||
margin-right: 8px; | ||
background-color: rgba(192, 192, 192, 0.5); | ||
padding: 8px 12px; | ||
border-radius: 2px; | ||
} | ||
.calibrationLine { | ||
position: absolute; | ||
height: 5px; | ||
border: 1px solid white; | ||
background-color: black; | ||
} | ||
</style> | ||
|
||
<style> | ||
.image-cropper-container img { | ||
margin-top: 0px; | ||
} | ||
</style> |
Oops, something went wrong.