1363 lines
47 KiB
HTML
1363 lines
47 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Mechanical Counter - Controller</title>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
|
|
<style>
|
|
@font-face {
|
|
font-family: 'OpenGostTypeB';
|
|
src: url('/static/fonts/OpenGost Type B/OpenGostTypeB.ttf') format('truetype');
|
|
font-weight: normal;
|
|
font-style: normal;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'OpenGostTypeB', monospace;
|
|
background: linear-gradient(135deg, #1e3c72, #2a5298);
|
|
color: #fff;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 15px;
|
|
padding: 30px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
h1 {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
color: #00ff00;
|
|
text-shadow: 0 0 10px #00ff00;
|
|
font-size: 2.5em;
|
|
}
|
|
|
|
.status {
|
|
text-align: center;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
margin-bottom: 30px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.status.connected {
|
|
background-color: rgba(0, 255, 0, 0.2);
|
|
color: #00ff00;
|
|
border: 1px solid #00ff00;
|
|
}
|
|
|
|
.status.disconnected {
|
|
background-color: rgba(255, 0, 0, 0.2);
|
|
color: #ff0000;
|
|
border: 1px solid #ff0000;
|
|
}
|
|
|
|
.counter-inputs {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 15px;
|
|
margin-bottom: 30px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.digit-input-group {
|
|
text-align: center;
|
|
}
|
|
|
|
.digit-label {
|
|
display: block;
|
|
margin-bottom: 10px;
|
|
font-size: 1.2em;
|
|
color: #00ff00;
|
|
text-shadow: 0 0 5px #00ff00;
|
|
}
|
|
|
|
.digit-input-group {
|
|
text-align: center;
|
|
position: relative;
|
|
}
|
|
|
|
.digit-input {
|
|
width: 80px;
|
|
height: 80px;
|
|
font-size: 2.5em;
|
|
text-align: center;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border: 2px solid #00ff00;
|
|
border-radius: 10px;
|
|
color: #00ff00;
|
|
font-family: 'OpenGostTypeB', monospace;
|
|
text-shadow: 0 0 10px #00ff00;
|
|
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.digit-buttons {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.digit-btn {
|
|
width: 40px;
|
|
height: 30px;
|
|
font-size: 1.2em;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-family: 'OpenGostTypeB', monospace;
|
|
font-weight: bold;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.digit-btn-up {
|
|
background: linear-gradient(45deg, #00ff00, #00cc00);
|
|
color: #000;
|
|
box-shadow: 0 2px 8px rgba(0, 255, 0, 0.3);
|
|
}
|
|
|
|
.digit-btn-up:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 255, 0, 0.5);
|
|
}
|
|
|
|
.digit-btn-down {
|
|
background: linear-gradient(45deg, #ff6b6b, #ee5a52);
|
|
color: #fff;
|
|
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3);
|
|
}
|
|
|
|
.digit-btn-down:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.5);
|
|
}
|
|
|
|
.digit-btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.digit-input:focus {
|
|
outline: none;
|
|
border-color: #00ffff;
|
|
box-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.digit-input::-webkit-outer-spin-button,
|
|
.digit-input::-webkit-inner-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
|
|
.digit-input[type=number] {
|
|
-moz-appearance: textfield;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
margin-bottom: 30px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
padding: 15px 30px;
|
|
font-size: 1.1em;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-family: 'OpenGostTypeB', monospace;
|
|
font-weight: bold;
|
|
transition: all 0.3s ease;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(45deg, #00ff00, #00cc00);
|
|
color: #000;
|
|
box-shadow: 0 4px 15px rgba(0, 255, 0, 0.3);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(0, 255, 0, 0.5);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: linear-gradient(45deg, #ff6b6b, #ee5a52);
|
|
color: #fff;
|
|
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.5);
|
|
}
|
|
|
|
.btn-warning {
|
|
background: linear-gradient(45deg, #ffa726, #ff9800);
|
|
color: #000;
|
|
box-shadow: 0 4px 15px rgba(255, 167, 38, 0.3);
|
|
}
|
|
|
|
.btn-warning:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(255, 167, 38, 0.5);
|
|
}
|
|
|
|
.quick-actions {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.quick-actions h3 {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
color: #00ff00;
|
|
text-shadow: 0 0 5px #00ff00;
|
|
}
|
|
|
|
.quick-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.quick-btn {
|
|
padding: 10px 20px;
|
|
font-size: 0.9em;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-family: 'OpenGostTypeB', monospace;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.quick-btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.sent-events {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.sent-events h3 {
|
|
text-align: center;
|
|
margin-bottom: 15px;
|
|
color: #00ff00;
|
|
}
|
|
|
|
.sent-event {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
border-radius: 5px;
|
|
border-left: 4px solid #00ff00;
|
|
}
|
|
|
|
.sent-event-time {
|
|
color: #ccc;
|
|
font-size: 0.8em;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.sent-event-data {
|
|
font-family: monospace;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
padding: 5px;
|
|
border-radius: 3px;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.nav-link {
|
|
display: inline-block;
|
|
margin-top: 20px;
|
|
padding: 10px 20px;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
color: #fff;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.nav-link:hover {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.position-controls {
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid #00ff00;
|
|
}
|
|
|
|
.position-controls h3 {
|
|
color: #00ff00;
|
|
text-align: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.digit-position-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
margin-bottom: 15px;
|
|
padding: 10px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.digit-position-label {
|
|
min-width: 80px;
|
|
font-weight: bold;
|
|
color: #00ff00;
|
|
}
|
|
|
|
.position-slider-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex: 1;
|
|
}
|
|
|
|
.position-slider {
|
|
flex: 1;
|
|
height: 6px;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 3px;
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
.position-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #00ff00;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.position-slider::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #00ff00;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
.position-value {
|
|
min-width: 50px;
|
|
text-align: center;
|
|
font-family: monospace;
|
|
color: #00ff00;
|
|
}
|
|
|
|
.font-size-controls {
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid #00ff00;
|
|
}
|
|
|
|
.font-size-controls h3 {
|
|
color: #00ff00;
|
|
text-align: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.font-size-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.font-size-label {
|
|
font-weight: bold;
|
|
color: #00ff00;
|
|
min-width: 100px;
|
|
}
|
|
|
|
.font-size-slider {
|
|
flex: 1;
|
|
max-width: 300px;
|
|
height: 6px;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 3px;
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
.font-size-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #00ff00;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.font-size-slider::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #00ff00;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
.font-size-value {
|
|
min-width: 60px;
|
|
text-align: center;
|
|
font-family: monospace;
|
|
color: #00ff00;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.font-size-preview {
|
|
display: inline-block;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
border: 1px solid #00ff00;
|
|
border-radius: 5px;
|
|
margin-left: 15px;
|
|
}
|
|
|
|
.wheel-size-controls {
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid #00ff00;
|
|
}
|
|
|
|
.wheel-size-controls h3 {
|
|
color: #00ff00;
|
|
text-align: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.wheel-size-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.wheel-size-label {
|
|
font-weight: bold;
|
|
color: #00ff00;
|
|
min-width: 80px;
|
|
}
|
|
|
|
.wheel-size-slider {
|
|
flex: 1;
|
|
max-width: 200px;
|
|
height: 6px;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 3px;
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
.wheel-size-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #00ff00;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wheel-size-slider::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #00ff00;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
.wheel-size-value {
|
|
min-width: 60px;
|
|
text-align: center;
|
|
font-family: monospace;
|
|
color: #00ff00;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.wheel-size-preview {
|
|
display: inline-block;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
border: 1px solid #00ff00;
|
|
border-radius: 5px;
|
|
margin-left: 15px;
|
|
text-align: center;
|
|
}
|
|
|
|
.wheel-size-preview-box {
|
|
background: linear-gradient(145deg, #2a2a2a, #1a1a1a);
|
|
border: 1px solid #444;
|
|
border-radius: 5px;
|
|
display: inline-block;
|
|
}
|
|
|
|
.animation-speed-controls {
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid #00ff00;
|
|
}
|
|
|
|
.animation-speed-controls h3 {
|
|
color: #00ff00;
|
|
text-align: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.animation-speed-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.animation-speed-label {
|
|
font-weight: bold;
|
|
color: #00ff00;
|
|
min-width: 120px;
|
|
}
|
|
|
|
.animation-speed-slider {
|
|
flex: 1;
|
|
max-width: 300px;
|
|
height: 6px;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 3px;
|
|
outline: none;
|
|
-webkit-appearance: none;
|
|
}
|
|
|
|
.animation-speed-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #00ff00;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.animation-speed-slider::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
background: #00ff00;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
.animation-speed-value {
|
|
min-width: 80px;
|
|
text-align: center;
|
|
font-family: monospace;
|
|
color: #00ff00;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.animation-speed-preview {
|
|
display: inline-block;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
border: 1px solid #00ff00;
|
|
border-radius: 5px;
|
|
margin-left: 15px;
|
|
text-align: center;
|
|
}
|
|
|
|
.animation-speed-preview-box {
|
|
width: 40px;
|
|
height: 60px;
|
|
background: linear-gradient(145deg, #2a2a2a, #1a1a1a);
|
|
border: 1px solid #444;
|
|
border-radius: 5px;
|
|
display: inline-block;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.animation-speed-preview-strip {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 600px;
|
|
transition: transform 0.5s ease;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.animation-speed-preview-digit {
|
|
width: 100%;
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #ffffff;
|
|
font-size: 20px;
|
|
border-bottom: 1px solid #333;
|
|
}
|
|
|
|
.counter-preview {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
padding: 20px;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border-radius: 10px;
|
|
border: 1px solid #00ff00;
|
|
}
|
|
|
|
.counter-preview h3 {
|
|
color: #00ff00;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.preview-digits {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
font-family: 'OpenGostTypeB', monospace;
|
|
font-size: 2em;
|
|
color: #00ff00;
|
|
text-shadow: 0 0 10px #00ff00;
|
|
}
|
|
|
|
.preview-digit {
|
|
width: 40px;
|
|
height: 60px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
border: 1px solid #00ff00;
|
|
border-radius: 5px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Mechanical Counter Controller</h1>
|
|
<div id="status" class="status disconnected">Disconnected</div>
|
|
|
|
<div class="font-size-controls">
|
|
<h3>Global Font Size</h3>
|
|
<div class="font-size-group">
|
|
<div class="font-size-label">Font Size:</div>
|
|
<input type="range" class="font-size-slider" id="fontSizeSlider" min="12" max="120" value="48">
|
|
<span class="font-size-value" id="fontSizeValue">48px</span>
|
|
<div class="font-size-preview">
|
|
<span id="fontSizePreview" style="font-size: 48px;">123</span>
|
|
</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<button class="btn btn-primary" onclick="updateFontSize()">Update Font Size</button>
|
|
<button class="btn btn-secondary" onclick="resetFontSize()">Reset Font Size</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="animation-speed-controls">
|
|
<h3>Global Animation Speed Control</h3>
|
|
<div class="animation-speed-group">
|
|
<div class="animation-speed-label">Speed:</div>
|
|
<input type="range" class="animation-speed-slider" id="globalAnimationSpeed" min="0.1" max="2.0" value="0.5" step="0.1">
|
|
<span class="animation-speed-value" id="globalAnimationSpeedValue">0.5s</span>
|
|
<div class="animation-speed-preview">
|
|
<div class="animation-speed-preview-box">
|
|
<div class="animation-speed-preview-strip" id="speedPreviewStrip">
|
|
<div class="animation-speed-preview-digit">0</div>
|
|
<div class="animation-speed-preview-digit">1</div>
|
|
<div class="animation-speed-preview-digit">2</div>
|
|
<div class="animation-speed-preview-digit">3</div>
|
|
<div class="animation-speed-preview-digit">4</div>
|
|
<div class="animation-speed-preview-digit">5</div>
|
|
<div class="animation-speed-preview-digit">6</div>
|
|
<div class="animation-speed-preview-digit">7</div>
|
|
<div class="animation-speed-preview-digit">8</div>
|
|
<div class="animation-speed-preview-digit">9</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<button class="btn btn-primary" onclick="updateGlobalAnimationSpeed()">Update All Wheels</button>
|
|
<button class="btn btn-secondary" onclick="resetGlobalAnimationSpeed()">Reset to Default</button>
|
|
<button class="btn btn-warning" onclick="testAnimationSpeed()">Test Animation</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wheel-size-controls">
|
|
<h3>Digit Wheel Size</h3>
|
|
<div class="wheel-size-group">
|
|
<div class="wheel-size-label">Width:</div>
|
|
<input type="range" class="wheel-size-slider" id="wheelWidthSlider" min="40" max="200" value="80">
|
|
<span class="wheel-size-value" id="wheelWidthValue">80px</span>
|
|
<div class="wheel-size-label">Height:</div>
|
|
<input type="range" class="wheel-size-slider" id="wheelHeightSlider" min="60" max="300" value="120">
|
|
<span class="wheel-size-value" id="wheelHeightValue">120px</span>
|
|
</div>
|
|
<div class="wheel-size-group">
|
|
<div class="wheel-size-preview">
|
|
<div class="wheel-size-preview-box" id="wheelSizePreview" style="width: 80px; height: 120px;">
|
|
<div style="color: #ffffff; font-size: 24px; line-height: 120px; text-align: center;">8</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<button class="btn btn-primary" onclick="updateWheelSize()">Update Wheel Size</button>
|
|
<button class="btn btn-secondary" onclick="resetWheelSize()">Reset Wheel Size</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="position-controls">
|
|
<h3>Digit Positions</h3>
|
|
<div class="digit-position-group">
|
|
<div class="digit-position-label">Digit 1:</div>
|
|
<div class="position-slider-group">
|
|
<span>X:</span>
|
|
<input type="range" class="position-slider" id="pos1x" min="0" max="800" value="50">
|
|
<span class="position-value" id="pos1xval">50</span>
|
|
<span>Y:</span>
|
|
<input type="range" class="position-slider" id="pos1y" min="0" max="600" value="200">
|
|
<span class="position-value" id="pos1yval">200</span>
|
|
</div>
|
|
</div>
|
|
<div class="digit-position-group">
|
|
<div class="digit-position-label">Digit 2:</div>
|
|
<div class="position-slider-group">
|
|
<span>X:</span>
|
|
<input type="range" class="position-slider" id="pos2x" min="0" max="800" value="150">
|
|
<span class="position-value" id="pos2xval">150</span>
|
|
<span>Y:</span>
|
|
<input type="range" class="position-slider" id="pos2y" min="0" max="600" value="200">
|
|
<span class="position-value" id="pos2yval">200</span>
|
|
</div>
|
|
</div>
|
|
<div class="digit-position-group">
|
|
<div class="digit-position-label">Digit 3:</div>
|
|
<div class="position-slider-group">
|
|
<span>X:</span>
|
|
<input type="range" class="position-slider" id="pos3x" min="0" max="800" value="250">
|
|
<span class="position-value" id="pos3xval">250</span>
|
|
<span>Y:</span>
|
|
<input type="range" class="position-slider" id="pos3y" min="0" max="600" value="200">
|
|
<span class="position-value" id="pos3yval">200</span>
|
|
</div>
|
|
</div>
|
|
<div class="digit-position-group">
|
|
<div class="digit-position-label">Digit 4:</div>
|
|
<div class="position-slider-group">
|
|
<span>X:</span>
|
|
<input type="range" class="position-slider" id="pos4x" min="0" max="800" value="350">
|
|
<span class="position-value" id="pos4xval">350</span>
|
|
<span>Y:</span>
|
|
<input type="range" class="position-slider" id="pos4y" min="0" max="600" value="200">
|
|
<span class="position-value" id="pos4yval">200</span>
|
|
</div>
|
|
</div>
|
|
<div class="digit-position-group">
|
|
<div class="digit-position-label">Digit 5:</div>
|
|
<div class="position-slider-group">
|
|
<span>X:</span>
|
|
<input type="range" class="position-slider" id="pos5x" min="0" max="800" value="450">
|
|
<span class="position-value" id="pos5xval">450</span>
|
|
<span>Y:</span>
|
|
<input type="range" class="position-slider" id="pos5y" min="0" max="600" value="200">
|
|
<span class="position-value" id="pos5yval">200</span>
|
|
</div>
|
|
</div>
|
|
<div class="digit-position-group">
|
|
<div class="digit-position-label">Digit 6:</div>
|
|
<div class="position-slider-group">
|
|
<span>X:</span>
|
|
<input type="range" class="position-slider" id="pos6x" min="0" max="800" value="550">
|
|
<span class="position-value" id="pos6xval">550</span>
|
|
<span>Y:</span>
|
|
<input type="range" class="position-slider" id="pos6y" min="0" max="600" value="200">
|
|
<span class="position-value" id="pos6yval">200</span>
|
|
</div>
|
|
</div>
|
|
<div style="text-align: center; margin-top: 15px;">
|
|
<div style="color: #00ff00; margin-bottom: 10px;">
|
|
Viewport: <span id="viewportInfo">800 x 600</span>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="updatePositions()">Update Positions</button>
|
|
<button class="btn btn-secondary" onclick="resetPositions()">Reset Positions</button>
|
|
<button class="btn btn-warning" onclick="requestViewportUpdate()" style="margin-left: 10px;">Request Viewport Update</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="counter-preview">
|
|
<h3>Current Counter Value</h3>
|
|
<div class="preview-digits" id="previewDigits">
|
|
<div class="preview-digit">0</div>
|
|
<div class="preview-digit">0</div>
|
|
<div class="preview-digit">0</div>
|
|
<div class="preview-digit">0</div>
|
|
<div class="preview-digit">0</div>
|
|
<div class="preview-digit">0</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="counter-inputs">
|
|
<div class="digit-input-group">
|
|
<label class="digit-label">Digit 1</label>
|
|
<input type="number" class="digit-input" id="digit1" min="0" max="9" value="0">
|
|
<div class="digit-buttons">
|
|
<button class="digit-btn digit-btn-up" onclick="incrementDigit(0)">▲</button>
|
|
<button class="digit-btn digit-btn-down" onclick="decrementDigit(0)">▼</button>
|
|
</div>
|
|
</div>
|
|
<div class="digit-input-group">
|
|
<label class="digit-label">Digit 2</label>
|
|
<input type="number" class="digit-input" id="digit2" min="0" max="9" value="0">
|
|
<div class="digit-buttons">
|
|
<button class="digit-btn digit-btn-up" onclick="incrementDigit(1)">▲</button>
|
|
<button class="digit-btn digit-btn-down" onclick="decrementDigit(1)">▼</button>
|
|
</div>
|
|
</div>
|
|
<div class="digit-input-group">
|
|
<label class="digit-label">Digit 3</label>
|
|
<input type="number" class="digit-input" id="digit3" min="0" max="9" value="0">
|
|
<div class="digit-buttons">
|
|
<button class="digit-btn digit-btn-up" onclick="incrementDigit(2)">▲</button>
|
|
<button class="digit-btn digit-btn-down" onclick="decrementDigit(2)">▼</button>
|
|
</div>
|
|
</div>
|
|
<div class="digit-input-group">
|
|
<label class="digit-label">Digit 4</label>
|
|
<input type="number" class="digit-input" id="digit4" min="0" max="9" value="0">
|
|
<div class="digit-buttons">
|
|
<button class="digit-btn digit-btn-up" onclick="incrementDigit(3)">▲</button>
|
|
<button class="digit-btn digit-btn-down" onclick="decrementDigit(3)">▼</button>
|
|
</div>
|
|
</div>
|
|
<div class="digit-input-group">
|
|
<label class="digit-label">Digit 5</label>
|
|
<input type="number" class="digit-input" id="digit5" min="0" max="9" value="0">
|
|
<div class="digit-buttons">
|
|
<button class="digit-btn digit-btn-up" onclick="incrementDigit(4)">▲</button>
|
|
<button class="digit-btn digit-btn-down" onclick="decrementDigit(4)">▼</button>
|
|
</div>
|
|
</div>
|
|
<div class="digit-input-group">
|
|
<label class="digit-label">Digit 6</label>
|
|
<input type="number" class="digit-input" id="digit6" min="0" max="9" value="0">
|
|
<div class="digit-buttons">
|
|
<button class="digit-btn digit-btn-up" onclick="incrementDigit(5)">▲</button>
|
|
<button class="digit-btn digit-btn-down" onclick="decrementDigit(5)">▼</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button class="btn btn-primary" onclick="updateCounter()">Update Counter</button>
|
|
<button class="btn btn-secondary" onclick="resetCounter()">Reset to Zero</button>
|
|
<button class="btn btn-warning" onclick="randomCounter()">Random Value</button>
|
|
</div>
|
|
|
|
<div class="quick-actions">
|
|
<h3>Quick Actions</h3>
|
|
<div class="quick-buttons">
|
|
<button class="quick-btn" onclick="setCounter([1,2,3,4,5,6])">123456</button>
|
|
<button class="quick-btn" onclick="setCounter([9,9,9,9,9,9])">999999</button>
|
|
<button class="quick-btn" onclick="setCounter([0,0,0,0,0,0])">000000</button>
|
|
<button class="quick-btn" onclick="setCounter([1,0,0,0,0,0])">100000</button>
|
|
<button class="quick-btn" onclick="setCounter([0,0,0,0,0,1])">000001</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sent-events">
|
|
<h3>Sent Updates</h3>
|
|
<div id="sentEvents"></div>
|
|
</div>
|
|
|
|
<a href="/" class="nav-link">← Back to Home</a>
|
|
</div>
|
|
|
|
<script>
|
|
// Connect to WebSocket server
|
|
const socket = io();
|
|
const statusDiv = document.getElementById('status');
|
|
const sentEventsDiv = document.getElementById('sentEvents');
|
|
const digitInputs = [
|
|
document.getElementById('digit1'),
|
|
document.getElementById('digit2'),
|
|
document.getElementById('digit3'),
|
|
document.getElementById('digit4'),
|
|
document.getElementById('digit5'),
|
|
document.getElementById('digit6')
|
|
];
|
|
const previewDigits = document.querySelectorAll('.preview-digit');
|
|
|
|
// Current counter values
|
|
let currentValues = [0, 0, 0, 0, 0, 0];
|
|
|
|
// Current position values
|
|
let currentPositions = [
|
|
{x: 50, y: 200},
|
|
{x: 150, y: 200},
|
|
{x: 250, y: 200},
|
|
{x: 350, y: 200},
|
|
{x: 450, y: 200},
|
|
{x: 550, y: 200}
|
|
];
|
|
|
|
// Debounce timer for position updates
|
|
let positionUpdateTimer = null;
|
|
|
|
// Viewport dimensions (default values)
|
|
let viewportWidth = 800;
|
|
let viewportHeight = 600;
|
|
|
|
// Font size (default value)
|
|
let currentFontSize = 48;
|
|
|
|
// Wheel size (default values)
|
|
let currentWheelWidth = 80;
|
|
let currentWheelHeight = 120;
|
|
|
|
// Animation speed (default value)
|
|
let currentAnimationSpeed = 0.5;
|
|
|
|
// Connection status
|
|
socket.on('connect', function() {
|
|
statusDiv.textContent = 'Connected';
|
|
statusDiv.className = 'status connected';
|
|
console.log('Connected to server');
|
|
});
|
|
|
|
socket.on('disconnect', function() {
|
|
statusDiv.textContent = 'Disconnected';
|
|
statusDiv.className = 'status disconnected';
|
|
console.log('Disconnected from server');
|
|
});
|
|
|
|
// Handle viewport updates from clients
|
|
socket.on('viewport_update', function(data) {
|
|
console.log('Received viewport update:', data);
|
|
console.log('Current viewport before update:', viewportWidth, 'x', viewportHeight);
|
|
updateViewportRanges(data.width, data.height);
|
|
console.log('Updated viewport to:', viewportWidth, 'x', viewportHeight);
|
|
});
|
|
|
|
// Add event listeners to digit inputs
|
|
digitInputs.forEach((input, index) => {
|
|
input.addEventListener('input', function() {
|
|
const value = parseInt(this.value) || 0;
|
|
currentValues[index] = value;
|
|
updatePreview();
|
|
});
|
|
|
|
input.addEventListener('change', function() {
|
|
// Ensure value is between 0-9
|
|
let value = parseInt(this.value) || 0;
|
|
value = Math.max(0, Math.min(9, value));
|
|
this.value = value;
|
|
currentValues[index] = value;
|
|
updatePreview();
|
|
});
|
|
});
|
|
|
|
// Add event listeners to position sliders
|
|
for (let i = 1; i <= 6; i++) {
|
|
const xSlider = document.getElementById(`pos${i}x`);
|
|
const ySlider = document.getElementById(`pos${i}y`);
|
|
const xValue = document.getElementById(`pos${i}xval`);
|
|
const yValue = document.getElementById(`pos${i}yval`);
|
|
|
|
xSlider.addEventListener('input', function() {
|
|
xValue.textContent = this.value;
|
|
currentPositions[i-1].x = parseInt(this.value);
|
|
// Send live update with debounce
|
|
debouncedPositionUpdate();
|
|
});
|
|
|
|
ySlider.addEventListener('input', function() {
|
|
yValue.textContent = this.value;
|
|
currentPositions[i-1].y = parseInt(this.value);
|
|
// Send live update with debounce
|
|
debouncedPositionUpdate();
|
|
});
|
|
}
|
|
|
|
// Add event listener to font size slider
|
|
const fontSizeSlider = document.getElementById('fontSizeSlider');
|
|
const fontSizeValue = document.getElementById('fontSizeValue');
|
|
const fontSizePreview = document.getElementById('fontSizePreview');
|
|
|
|
fontSizeSlider.addEventListener('input', function() {
|
|
const newSize = parseInt(this.value);
|
|
currentFontSize = newSize;
|
|
fontSizeValue.textContent = newSize + 'px';
|
|
fontSizePreview.style.fontSize = newSize + 'px';
|
|
});
|
|
|
|
// Add event listeners to wheel size sliders
|
|
const wheelWidthSlider = document.getElementById('wheelWidthSlider');
|
|
const wheelHeightSlider = document.getElementById('wheelHeightSlider');
|
|
const wheelWidthValue = document.getElementById('wheelWidthValue');
|
|
const wheelHeightValue = document.getElementById('wheelHeightValue');
|
|
const wheelSizePreview = document.getElementById('wheelSizePreview');
|
|
|
|
wheelWidthSlider.addEventListener('input', function() {
|
|
const newWidth = parseInt(this.value);
|
|
currentWheelWidth = newWidth;
|
|
wheelWidthValue.textContent = newWidth + 'px';
|
|
wheelSizePreview.style.width = newWidth + 'px';
|
|
});
|
|
|
|
wheelHeightSlider.addEventListener('input', function() {
|
|
const newHeight = parseInt(this.value);
|
|
currentWheelHeight = newHeight;
|
|
wheelHeightValue.textContent = newHeight + 'px';
|
|
wheelSizePreview.style.height = newHeight + 'px';
|
|
wheelSizePreview.querySelector('div').style.lineHeight = newHeight + 'px';
|
|
});
|
|
|
|
// Add event listener to animation speed slider
|
|
const animationSpeedSlider = document.getElementById('globalAnimationSpeed');
|
|
const animationSpeedValue = document.getElementById('globalAnimationSpeedValue');
|
|
const speedPreviewStrip = document.getElementById('speedPreviewStrip');
|
|
|
|
animationSpeedSlider.addEventListener('input', function() {
|
|
const newSpeed = parseFloat(this.value);
|
|
currentAnimationSpeed = newSpeed;
|
|
animationSpeedValue.textContent = newSpeed + 's';
|
|
speedPreviewStrip.style.transition = `transform ${newSpeed}s ease`;
|
|
});
|
|
|
|
function updateCounter() {
|
|
const counterData = {
|
|
type: 'counter_update',
|
|
values: [...currentValues],
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
socket.emit('counter_update', counterData);
|
|
addSentEvent(counterData);
|
|
}
|
|
|
|
function resetCounter() {
|
|
setCounter([0, 0, 0, 0, 0, 0]);
|
|
}
|
|
|
|
function randomCounter() {
|
|
const randomValues = Array.from({length: 6}, () => Math.floor(Math.random() * 10));
|
|
setCounter(randomValues);
|
|
}
|
|
|
|
function setCounter(values) {
|
|
values.forEach((value, index) => {
|
|
digitInputs[index].value = value;
|
|
currentValues[index] = value;
|
|
});
|
|
updatePreview();
|
|
updateCounter();
|
|
}
|
|
|
|
function updatePreview() {
|
|
currentValues.forEach((value, index) => {
|
|
previewDigits[index].textContent = value;
|
|
});
|
|
}
|
|
|
|
function updatePositions() {
|
|
const positionData = {
|
|
type: 'position_update',
|
|
positions: [...currentPositions],
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
socket.emit('position_update', positionData);
|
|
addSentEvent(positionData);
|
|
}
|
|
|
|
function resetPositions() {
|
|
const defaultPositions = [
|
|
{x: 50, y: 200},
|
|
{x: 150, y: 200},
|
|
{x: 250, y: 200},
|
|
{x: 350, y: 200},
|
|
{x: 450, y: 200},
|
|
{x: 550, y: 200}
|
|
];
|
|
|
|
// Update sliders and values
|
|
for (let i = 1; i <= 6; i++) {
|
|
const xSlider = document.getElementById(`pos${i}x`);
|
|
const ySlider = document.getElementById(`pos${i}y`);
|
|
const xValue = document.getElementById(`pos${i}xval`);
|
|
const yValue = document.getElementById(`pos${i}yval`);
|
|
|
|
xSlider.value = defaultPositions[i-1].x;
|
|
ySlider.value = defaultPositions[i-1].y;
|
|
xValue.textContent = defaultPositions[i-1].x;
|
|
yValue.textContent = defaultPositions[i-1].y;
|
|
}
|
|
|
|
currentPositions = [...defaultPositions];
|
|
updatePositions();
|
|
}
|
|
|
|
function debouncedPositionUpdate() {
|
|
if (positionUpdateTimer) {
|
|
clearTimeout(positionUpdateTimer);
|
|
}
|
|
positionUpdateTimer = setTimeout(() => {
|
|
sendLivePositionUpdate();
|
|
}, 50); // 50ms debounce
|
|
}
|
|
|
|
function sendLivePositionUpdate() {
|
|
const positionData = {
|
|
type: 'position_update',
|
|
positions: [...currentPositions],
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
console.log('Sending position update:', positionData);
|
|
socket.emit('position_update', positionData);
|
|
}
|
|
|
|
function updateViewportRanges(width, height) {
|
|
console.log('updateViewportRanges called with:', width, 'x', height);
|
|
viewportWidth = width;
|
|
viewportHeight = height;
|
|
|
|
// Update viewport info display
|
|
const viewportInfoElement = document.getElementById('viewportInfo');
|
|
if (viewportInfoElement) {
|
|
viewportInfoElement.textContent = `${width} x ${height}`;
|
|
console.log('Updated viewport info display to:', width, 'x', height);
|
|
} else {
|
|
console.error('Viewport info element not found!');
|
|
}
|
|
|
|
// Update all slider ranges
|
|
for (let i = 1; i <= 6; i++) {
|
|
const xSlider = document.getElementById(`pos${i}x`);
|
|
const ySlider = document.getElementById(`pos${i}y`);
|
|
|
|
if (xSlider && ySlider) {
|
|
xSlider.max = width;
|
|
ySlider.max = height;
|
|
|
|
// Ensure current values don't exceed new ranges
|
|
const currentX = parseInt(xSlider.value);
|
|
const currentY = parseInt(ySlider.value);
|
|
|
|
if (currentX > width) {
|
|
xSlider.value = width;
|
|
currentPositions[i-1].x = width;
|
|
document.getElementById(`pos${i}xval`).textContent = width;
|
|
}
|
|
|
|
if (currentY > height) {
|
|
ySlider.value = height;
|
|
currentPositions[i-1].y = height;
|
|
document.getElementById(`pos${i}yval`).textContent = height;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Updated viewport ranges to ${width}x${height}`);
|
|
}
|
|
|
|
function updateFontSize() {
|
|
const fontSizeData = {
|
|
type: 'font_size_update',
|
|
fontSize: currentFontSize,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
console.log('Sending font size update:', fontSizeData);
|
|
socket.emit('font_size_update', fontSizeData);
|
|
addSentEvent(fontSizeData);
|
|
}
|
|
|
|
function updateWheelSize() {
|
|
const wheelSizeData = {
|
|
type: 'wheel_size_update',
|
|
width: currentWheelWidth,
|
|
height: currentWheelHeight,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
console.log('Sending wheel size update:', wheelSizeData);
|
|
socket.emit('wheel_size_update', wheelSizeData);
|
|
addSentEvent(wheelSizeData);
|
|
}
|
|
|
|
function resetWheelSize() {
|
|
const defaultWidth = 80;
|
|
const defaultHeight = 120;
|
|
|
|
wheelWidthSlider.value = defaultWidth;
|
|
wheelHeightSlider.value = defaultHeight;
|
|
currentWheelWidth = defaultWidth;
|
|
currentWheelHeight = defaultHeight;
|
|
|
|
wheelWidthValue.textContent = defaultWidth + 'px';
|
|
wheelHeightValue.textContent = defaultHeight + 'px';
|
|
wheelSizePreview.style.width = defaultWidth + 'px';
|
|
wheelSizePreview.style.height = defaultHeight + 'px';
|
|
wheelSizePreview.querySelector('div').style.lineHeight = defaultHeight + 'px';
|
|
|
|
updateWheelSize();
|
|
}
|
|
|
|
function resetFontSize() {
|
|
const defaultSize = 48;
|
|
fontSizeSlider.value = defaultSize;
|
|
currentFontSize = defaultSize;
|
|
fontSizeValue.textContent = defaultSize + 'px';
|
|
fontSizePreview.style.fontSize = defaultSize + 'px';
|
|
updateFontSize();
|
|
}
|
|
|
|
function updateGlobalAnimationSpeed() {
|
|
const animationSpeedData = {
|
|
type: 'animation_speed_update',
|
|
speed: currentAnimationSpeed,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
console.log('Sending animation speed update:', animationSpeedData);
|
|
socket.emit('animation_speed_update', animationSpeedData);
|
|
addSentEvent(animationSpeedData);
|
|
}
|
|
|
|
function resetGlobalAnimationSpeed() {
|
|
const defaultSpeed = 0.5;
|
|
animationSpeedSlider.value = defaultSpeed;
|
|
currentAnimationSpeed = defaultSpeed;
|
|
animationSpeedValue.textContent = defaultSpeed + 's';
|
|
speedPreviewStrip.style.transition = `transform ${defaultSpeed}s ease`;
|
|
updateGlobalAnimationSpeed();
|
|
}
|
|
|
|
function testAnimationSpeed() {
|
|
// Animate the preview strip to show the current speed
|
|
const strip = document.getElementById('speedPreviewStrip');
|
|
const currentTransform = strip.style.transform;
|
|
|
|
// Reset to top
|
|
strip.style.transform = 'translateY(0px)';
|
|
|
|
// Animate to show digit 5
|
|
setTimeout(() => {
|
|
strip.style.transform = 'translateY(-300px)';
|
|
}, 100);
|
|
|
|
// Reset back to top after animation
|
|
setTimeout(() => {
|
|
strip.style.transform = currentTransform;
|
|
}, (currentAnimationSpeed * 1000) + 200);
|
|
}
|
|
|
|
function incrementDigit(index) {
|
|
let currentValue = currentValues[index];
|
|
currentValue = (currentValue + 1) % 10; // Wrap around from 9 to 0
|
|
currentValues[index] = currentValue;
|
|
|
|
// Update input field
|
|
digitInputs[index].value = currentValue;
|
|
|
|
// Update preview
|
|
updatePreview();
|
|
|
|
// Send instant update
|
|
updateCounter();
|
|
}
|
|
|
|
function decrementDigit(index) {
|
|
let currentValue = currentValues[index];
|
|
currentValue = (currentValue - 1 + 10) % 10; // Wrap around from 0 to 9
|
|
currentValues[index] = currentValue;
|
|
|
|
// Update input field
|
|
digitInputs[index].value = currentValue;
|
|
|
|
// Update preview
|
|
updatePreview();
|
|
|
|
// Send instant update
|
|
updateCounter();
|
|
}
|
|
|
|
function requestViewportUpdate() {
|
|
console.log('Requesting viewport update from clients...');
|
|
socket.emit('request_viewport', { timestamp: new Date().toISOString() });
|
|
}
|
|
|
|
function addSentEvent(data) {
|
|
const eventDiv = document.createElement('div');
|
|
eventDiv.className = 'sent-event';
|
|
|
|
const timeDiv = document.createElement('div');
|
|
timeDiv.className = 'sent-event-time';
|
|
timeDiv.textContent = new Date().toLocaleTimeString();
|
|
|
|
const dataDiv = document.createElement('div');
|
|
dataDiv.className = 'sent-event-data';
|
|
dataDiv.textContent = `Counter: ${data.values.join('')}`;
|
|
|
|
eventDiv.appendChild(timeDiv);
|
|
eventDiv.appendChild(dataDiv);
|
|
|
|
sentEventsDiv.appendChild(eventDiv);
|
|
|
|
// Auto-scroll to bottom
|
|
sentEventsDiv.scrollTop = sentEventsDiv.scrollHeight;
|
|
}
|
|
|
|
// Initialize preview
|
|
updatePreview();
|
|
</script>
|
|
</body>
|
|
</html> |