734 lines
18 KiB
HTML
734 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>DICOM Viewer - Fullscreen Mobile Friendly</title>
|
||
<style>
|
||
html, body {
|
||
margin: 0; padding: 0;
|
||
width: 100%; height: 100%;
|
||
background: #111; color: white; font-family: sans-serif;
|
||
overflow: hidden;
|
||
}
|
||
#dicomContainer {
|
||
position: relative;
|
||
width: 100%; height: 100%;
|
||
top: 0;
|
||
left: 0;
|
||
border-radius: 0;
|
||
overflow: visible;
|
||
}
|
||
#dicomImage {
|
||
width: 100%;
|
||
height: calc(100% - 100px);
|
||
background: black;
|
||
touch-action: none;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
border-radius: 0;
|
||
overflow: visible;
|
||
}
|
||
|
||
#dicomImage canvas {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
position: relative;
|
||
top: 0;
|
||
border-radius: 0 !important;
|
||
}
|
||
|
||
/* Ensure cornerstone canvas shows original rectangular view */
|
||
.cornerstone-element {
|
||
position: relative !important;
|
||
top: 0 !important;
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
border-radius: 0 !important;
|
||
overflow: visible !important;
|
||
}
|
||
|
||
.cornerstone-canvas {
|
||
position: relative !important;
|
||
top: 0 !important;
|
||
border-radius: 0 !important;
|
||
max-width: 100% !important;
|
||
max-height: 100% !important;
|
||
clip-path: none !important;
|
||
mask: none !important;
|
||
}
|
||
|
||
/* Remove any circular clipping */
|
||
.cornerstone-element canvas,
|
||
#dicomImage canvas,
|
||
canvas {
|
||
border-radius: 0 !important;
|
||
clip-path: none !important;
|
||
mask: none !important;
|
||
shape-outside: none !important;
|
||
}
|
||
|
||
/* Ensure full rectangular display */
|
||
div[class*="cornerstone"] {
|
||
border-radius: 0 !important;
|
||
clip-path: none !important;
|
||
overflow: visible !important;
|
||
}
|
||
#controls {
|
||
position: absolute;
|
||
bottom: 15px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0,0,0,0.8);
|
||
padding: 12px 16px;
|
||
border-radius: 12px;
|
||
z-index: 9999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
#controls {
|
||
bottom: 20px;
|
||
padding: 14px 18px;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
#dicomImage {
|
||
height: calc(100% - 110px);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
#controls {
|
||
bottom: 25px;
|
||
padding: 16px 20px;
|
||
border-radius: 20px;
|
||
left: 10px;
|
||
right: 10px;
|
||
transform: none;
|
||
max-width: none;
|
||
}
|
||
|
||
#dicomImage {
|
||
height: calc(100% - 130px);
|
||
}
|
||
}
|
||
#frameControls, #actionControls {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
width: 90%;
|
||
}
|
||
|
||
#frameControls {
|
||
flex-wrap: nowrap;
|
||
width: 100%;
|
||
max-width: 240px;
|
||
gap: 5px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
#frameControls, #actionControls {
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
width: 90%;
|
||
}
|
||
|
||
#frameControls {
|
||
flex-wrap: nowrap;
|
||
width: 100%;
|
||
max-width: 255px;
|
||
gap: 6px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
#frameControls, #actionControls {
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
width: 90%;
|
||
}
|
||
|
||
#frameControls {
|
||
flex-wrap: nowrap;
|
||
width: 100%;
|
||
max-width: 270px;
|
||
gap: 8px;
|
||
}
|
||
}
|
||
button {
|
||
background: #222;
|
||
color: white;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
padding: 6px 10px;
|
||
font-size: 14px;
|
||
min-width: 34px;
|
||
min-height: 34px;
|
||
cursor: pointer;
|
||
touch-action: manipulation;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
button {
|
||
padding: 8px 12px;
|
||
font-size: 16px;
|
||
min-width: 35px;
|
||
min-height: 35px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
button {
|
||
padding: 10px 14px;
|
||
font-size: 18px;
|
||
min-width: 37px;
|
||
min-height: 37px;
|
||
}
|
||
}
|
||
input[type=range] {
|
||
width: 135px;
|
||
height: 6px;
|
||
cursor: pointer;
|
||
flex: 1;
|
||
min-width: 100px;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
background: rgba(255,255,255,0.3);
|
||
border-radius: 3px;
|
||
outline: none;
|
||
}
|
||
|
||
input[type=range]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 16px;
|
||
height: 16px;
|
||
background: #2196F3;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
input[type=range]::-moz-range-thumb {
|
||
width: 16px;
|
||
height: 16px;
|
||
background: #2196F3;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
border: none;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
input[type=range]:disabled {
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
input[type=range]:disabled::-webkit-slider-thumb {
|
||
background: #666;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
input[type=range]:disabled::-moz-range-thumb {
|
||
background: #666;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
input[type=range] {
|
||
width: 155px;
|
||
height: 6px;
|
||
min-width: 120px;
|
||
}
|
||
|
||
input[type=range]::-webkit-slider-thumb {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
|
||
input[type=range]::-moz-range-thumb {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
input[type=range] {
|
||
width: 170px;
|
||
height: 6px;
|
||
min-width: 140px;
|
||
}
|
||
|
||
input[type=range]::-webkit-slider-thumb {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
input[type=range]::-moz-range-thumb {
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
}
|
||
input[type=file] {
|
||
display: none;
|
||
}
|
||
|
||
#frameInfo {
|
||
margin-top: 6px;
|
||
color: rgba(255,255,255,0.9);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
#frameInfo {
|
||
font-size: 14px;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
#frameInfo {
|
||
font-size: 16px;
|
||
margin-top: 10px;
|
||
}
|
||
}
|
||
|
||
/* Loading Overlay Styles */
|
||
#loadingOverlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 10000;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
#loadingOverlay.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.loader {
|
||
width: 50px;
|
||
height: 50px;
|
||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||
border-top: 4px solid #2196F3;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.loading-text {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.loading-subtext {
|
||
color: rgba(255, 255, 255, 0.7);
|
||
font-size: 14px;
|
||
text-align: center;
|
||
max-width: 300px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.loader {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-width: 5px;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.loading-subtext {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.loader {
|
||
width: 70px;
|
||
height: 70px;
|
||
border-width: 6px;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.loading-subtext {
|
||
font-size: 18px;
|
||
max-width: 280px;
|
||
}
|
||
}
|
||
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="dicomContainer">
|
||
<div id="dicomImage"></div>
|
||
|
||
<!-- Loading Overlay -->
|
||
<div id="loadingOverlay" class="hidden">
|
||
<div class="loader"></div>
|
||
<div class="loading-text">Loading DICOM Image...</div>
|
||
<div class="loading-subtext">Please wait while we process your medical image</div>
|
||
</div>
|
||
|
||
<div id="controls">
|
||
<div id="actionControls">
|
||
<button onclick="resetView()">⟳</button>
|
||
<button onclick="zoomIn()">+</button>
|
||
<button onclick="zoomOut()">-</button>
|
||
<button onclick="document.getElementById('fileInput').click()">📂</button>
|
||
</div>
|
||
<div id="frameControls">
|
||
<button onclick="prevFrame()">◀</button>
|
||
<input type="range" id="frameSlider" min="1" max="2" value="1" disabled>
|
||
<button onclick="nextFrame()">▶</button>
|
||
</div>
|
||
<div id="frameInfo"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hidden file input -->
|
||
<input type="file" id="fileInput" accept=".dcm" multiple>
|
||
|
||
<!-- Load from URL (Hidden for WebView usage) -->
|
||
<input type="text" id="dicomUrl" placeholder="Enter DICOM URL" size="50" style="display: none;">
|
||
<button id="loadUrlBtn" style="display: none;">Load URL</button>
|
||
|
||
<!-- Libraries -->
|
||
<script src="https://cdn.jsdelivr.net/npm/cornerstone-core@2.6.1/dist/cornerstone.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/dicom-parser/dist/dicomParser.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/cornerstone-wado-image-loader@4.13.2/dist/cornerstoneWADOImageLoader.bundle.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/cornerstone-tools@4.22.1/dist/cornerstoneTools.min.js"></script>
|
||
|
||
<script>
|
||
const element = document.getElementById('dicomImage');
|
||
const frameInfo = document.getElementById('frameInfo');
|
||
const frameSlider = document.getElementById('frameSlider');
|
||
const loadingOverlay = document.getElementById('loadingOverlay');
|
||
|
||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||
|
||
cornerstone.enable(element);
|
||
cornerstoneTools.init({ showSVGCursors: true });
|
||
|
||
// Touch tools
|
||
cornerstoneTools.addTool(cornerstoneTools.ZoomTouchPinchTool);
|
||
cornerstoneTools.addTool(cornerstoneTools.StackScrollMultiTouchTool);
|
||
cornerstoneTools.addTool(cornerstoneTools.WwwcRegionTool);
|
||
|
||
cornerstoneTools.setToolActive('ZoomTouchPinch', {});
|
||
cornerstoneTools.setToolActive('StackScrollMultiTouch', {});
|
||
cornerstoneTools.setToolActive('WwwcRegion', {});
|
||
|
||
// 🔑 Listen for messages from React Native WebView
|
||
window.addEventListener('message', (event) => {
|
||
try {
|
||
const message = event.data;
|
||
|
||
// Handle direct URL string
|
||
if (typeof message === 'string' && message.startsWith('http')) {
|
||
loadDicom("wadouri:" + message);
|
||
document.getElementById('dicomUrl').value = message;
|
||
|
||
// Send success message back to React Native
|
||
if (window.ReactNativeWebView) {
|
||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||
type: 'success',
|
||
message: 'DICOM URL received and loading started'
|
||
}));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Handle structured JSON message
|
||
try {
|
||
const parsedMessage = JSON.parse(message);
|
||
if (parsedMessage.type === 'loadDicom' && parsedMessage.data) {
|
||
loadDicom("wadouri:" + parsedMessage.data);
|
||
document.getElementById('dicomUrl').value = parsedMessage.data;
|
||
|
||
// Send success message back to React Native
|
||
if (window.ReactNativeWebView) {
|
||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||
type: 'success',
|
||
message: 'Structured DICOM message received and loading started'
|
||
}));
|
||
}
|
||
}
|
||
} catch (parseError) {
|
||
// Message is not JSON, treating as string
|
||
}
|
||
} catch (error) {
|
||
|
||
// Send error message back to React Native
|
||
if (window.ReactNativeWebView) {
|
||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||
type: 'error',
|
||
message: 'Failed to process message: ' + error.message
|
||
}));
|
||
}
|
||
}
|
||
});
|
||
|
||
// For Android WebView compatibility
|
||
document.addEventListener('message', (event) => {
|
||
window.dispatchEvent(new MessageEvent('message', { data: event.data }));
|
||
});
|
||
|
||
// Notify React Native that WebView is ready
|
||
window.addEventListener('load', () => {
|
||
// Initialize slider appearance
|
||
frameSlider.style.opacity = '0.6';
|
||
|
||
if (window.ReactNativeWebView) {
|
||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||
type: 'ready',
|
||
message: 'DICOM Viewer is ready to receive URLs'
|
||
}));
|
||
}
|
||
});
|
||
|
||
// 🔑 Resize observer for responsiveness
|
||
window.addEventListener('resize', () => {
|
||
cornerstone.resize(element, true);
|
||
});
|
||
|
||
// Loading overlay control functions
|
||
function showLoading(message = 'Loading DICOM Image...', subtext = 'Please wait while we process your medical image') {
|
||
if (loadingOverlay) {
|
||
loadingOverlay.querySelector('.loading-text').textContent = message;
|
||
loadingOverlay.querySelector('.loading-subtext').textContent = subtext;
|
||
loadingOverlay.classList.remove('hidden');
|
||
}
|
||
}
|
||
|
||
function hideLoading() {
|
||
if (loadingOverlay) {
|
||
loadingOverlay.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
let stack = null;
|
||
|
||
// File input
|
||
document.getElementById('fileInput').addEventListener('change', e => {
|
||
const files = Array.from(e.target.files);
|
||
if (!files.length) return;
|
||
|
||
// Show loading while processing files
|
||
if (files.length === 1) {
|
||
showLoading('Processing DICOM File...', 'Loading selected medical image...');
|
||
setTimeout(() => {
|
||
loadDicom(cornerstoneWADOImageLoader.wadouri.fileManager.add(files[0]));
|
||
}, 100);
|
||
} else {
|
||
showLoading('Processing DICOM Files...', `Loading ${files.length} selected medical images...`);
|
||
setTimeout(() => {
|
||
loadSeries(files.map(file => cornerstoneWADOImageLoader.wadouri.fileManager.add(file)));
|
||
}, 100);
|
||
}
|
||
});
|
||
|
||
// URL load
|
||
document.getElementById('loadUrlBtn').addEventListener('click', () => {
|
||
const url = document.getElementById('dicomUrl').value.trim();
|
||
if (!url) return;
|
||
showLoading('Loading from URL...', 'Fetching DICOM image from provided URL...');
|
||
setTimeout(() => {
|
||
loadDicom("wadouri:" + url);
|
||
}, 100);
|
||
});
|
||
|
||
// Load dicom
|
||
function loadDicom(imageId) {
|
||
// Show loading overlay
|
||
showLoading('Loading DICOM Image...', 'Processing medical image data, please wait...');
|
||
|
||
cornerstone.loadImage(imageId).then(image => {
|
||
// Hide loading overlay on successful load
|
||
hideLoading();
|
||
|
||
cornerstone.displayImage(element, image);
|
||
cornerstone.resize(element, true); // ensure fit on load
|
||
|
||
const numFrames = parseInt(image.data.string('x00280008') || '1', 10);
|
||
|
||
// Always setup frame controls, even for single frames
|
||
if (numFrames > 1) {
|
||
stack = { currentImageIdIndex: 0, imageIds: [] };
|
||
for (let i = 0; i < numFrames; i++) {
|
||
stack.imageIds.push(imageId + `&frame=${i}`);
|
||
}
|
||
setupStack(stack);
|
||
} else {
|
||
// Single frame - setup basic stack for consistency
|
||
stack = { currentImageIdIndex: 0, imageIds: [imageId] };
|
||
setupStack(stack, true); // Pass true to indicate single frame
|
||
}
|
||
|
||
|
||
|
||
// Notify React Native of successful load
|
||
if (window.ReactNativeWebView) {
|
||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||
type: 'success',
|
||
message: 'DICOM image loaded and displayed successfully',
|
||
data: {
|
||
frames: numFrames,
|
||
imageId: imageId
|
||
}
|
||
}));
|
||
}
|
||
|
||
}).catch(err => {
|
||
// Hide loading overlay on error
|
||
hideLoading();
|
||
|
||
// Notify React Native of error
|
||
if (window.ReactNativeWebView) {
|
||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||
type: 'error',
|
||
message: 'Failed to load DICOM image: ' + err.message || err,
|
||
data: {
|
||
imageId: imageId,
|
||
error: err.message || err.toString()
|
||
}
|
||
}));
|
||
}
|
||
});
|
||
}
|
||
|
||
function loadSeries(imageIds) {
|
||
// Show loading overlay for series
|
||
showLoading('Loading DICOM Series...', `Processing ${imageIds.length} images, please wait...`);
|
||
|
||
stack = { currentImageIdIndex: 0, imageIds };
|
||
cornerstone.loadImage(imageIds[0]).then(image => {
|
||
// Hide loading overlay on successful load
|
||
hideLoading();
|
||
|
||
cornerstone.displayImage(element, image);
|
||
cornerstone.resize(element, true);
|
||
setupStack(stack, imageIds.length === 1);
|
||
}).catch(err => {
|
||
// Hide loading overlay on error
|
||
hideLoading();
|
||
|
||
// Handle error silently or send to React Native
|
||
if (window.ReactNativeWebView) {
|
||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||
type: 'error',
|
||
message: 'Failed to load DICOM series: ' + err.message || err
|
||
}));
|
||
}
|
||
});
|
||
}
|
||
|
||
function setupStack(s, isSingleFrame = false) {
|
||
cornerstoneTools.addStackStateManager(element, ['stack']);
|
||
cornerstoneTools.addToolState(element, 'stack', s);
|
||
|
||
// Handle single frame slider display
|
||
if (isSingleFrame || s.imageIds.length === 1) {
|
||
frameSlider.min = 1;
|
||
frameSlider.max = 2; // Set max to 2 to show proper indicator
|
||
frameSlider.value = 1;
|
||
frameSlider.disabled = true; // Disable interaction for single frame
|
||
frameSlider.style.opacity = '0.6'; // Visual indicator it's disabled
|
||
} else {
|
||
frameSlider.min = 1;
|
||
frameSlider.max = s.imageIds.length;
|
||
frameSlider.value = 1;
|
||
frameSlider.disabled = false; // Enable interaction for multiple frames
|
||
frameSlider.style.opacity = '1'; // Full opacity for enabled state
|
||
}
|
||
|
||
updateFrameInfo(s);
|
||
|
||
// Remove existing event listeners to avoid duplicates
|
||
element.removeEventListener('cornerstonetoolsstackscroll', updateFrameInfo);
|
||
frameSlider.removeEventListener('input', handleSliderInput);
|
||
|
||
// Add event listeners only if not single frame
|
||
if (!isSingleFrame && s.imageIds.length > 1) {
|
||
element.addEventListener('cornerstonetoolsstackscroll', () => updateFrameInfo(s));
|
||
frameSlider.addEventListener('input', handleSliderInput);
|
||
}
|
||
}
|
||
|
||
function handleSliderInput() {
|
||
const index = parseInt(frameSlider.value) - 1;
|
||
changeFrame(index);
|
||
}
|
||
|
||
function updateFrameInfo(s) {
|
||
frameInfo.textContent = `Image ${s.currentImageIdIndex + 1} of ${s.imageIds.length}`;
|
||
frameSlider.value = s.currentImageIdIndex + 1;
|
||
}
|
||
|
||
|
||
|
||
function changeFrame(index) {
|
||
if (!stack) return;
|
||
if (index < 0 || index >= stack.imageIds.length) return;
|
||
stack.currentImageIdIndex = index;
|
||
cornerstone.loadImage(stack.imageIds[index]).then(img => {
|
||
cornerstone.displayImage(element, img);
|
||
cornerstone.resize(element, true);
|
||
updateFrameInfo(stack);
|
||
}).catch(err => {
|
||
// Handle frame change error silently
|
||
});
|
||
}
|
||
|
||
function prevFrame() { if (stack) changeFrame(stack.currentImageIdIndex - 1); }
|
||
function nextFrame() { if (stack) changeFrame(stack.currentImageIdIndex + 1); }
|
||
|
||
function resetView() { cornerstone.reset(element); }
|
||
function zoomIn() {
|
||
const viewport = cornerstone.getViewport(element);
|
||
viewport.scale *= 1.2;
|
||
cornerstone.setViewport(element, viewport);
|
||
}
|
||
function zoomOut() {
|
||
const viewport = cornerstone.getViewport(element);
|
||
viewport.scale /= 1.2;
|
||
cornerstone.setViewport(element, viewport);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|