-
-
+
- 0.00
- 0.00
- 0.00
- 0.00
-
-
+
+
+
+
+ ISO
+
-
+
+ —
+
-
-
-
-
-
-
+
+ SHUTTER
+
-
+
+ —
+
-
- FOCUS
-
-
-
-
- IRIS
-
- X.X
-
-
- ZOOM
-
- XXmm
+
-
+ ND FILTER
+
-
+
+
+
+ —
+
+ GAIN
+
+
+
+
+ —
+
+
+
+ WB ↕
+
+
+
+ —
+
+
+
+
+ —
+
+
+
+
-
-
CAMERA NAME
-
- CODEC
- RESOLUTION
- FPS
-
-
-
-
-
-
-
-
-
-
-
- TIMECODE
+ +
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
Connection
-| Hostname | -- - - - Use HTTPS - - | -
| Send API Call | -
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+ |
-
|
- Send manual API requests using the above controls. See documentation for details. - |
- |
+
+
+
+
+
+
+ Gamma Curve
+
+
+
+
+
+
+
+
+
+
+ Current:
+ —
+
+
+
+
+ Color Gamut
+
+
+
+
+
+
+
+
+
+
+ Current:
+ —
+
+
+
+ Quick Presets
+
+
+
+
+
+
+
+ Video Format
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lift
+
+
-
-
+ L
+ 0.00
-
-
-
-
+
+
+
+ Presets
-| Preset Select | -- - | -- - | -
+ R
+ 0.00
-
-
-
-
-
+
-
- Exposure
-| ISO | -- |
| AE Mode | -- - | -
| AE Type | -- - | -
+ G
+ 0.00
-
-
-
-
-
-
- Contrast
-| Pivot | -- | - 0 - | -- - | -
| Adjust | -- | - 0 - | -
-
-
+
- Color
-| Hue | -- | - 0 - | -- - | -
| Saturation | -- | - 0 - | -|
| Luma Contribution | -- | - 0 - | -
+ B
+ 0.00
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/style.css b/style.css
index e71a457..123b75d 100644
--- a/style.css
+++ b/style.css
@@ -1,470 +1,770 @@
-/* Sorry the CSS is such a mess. -DS */
+/* Design tokens */
+:root {
+ --bg: oklch(0.11 0.008 250);
+ --surface: oklch(0.17 0.008 250);
+ --elevated: oklch(0.22 0.009 250);
+ --border: oklch(0.28 0.010 250);
+ --border-subtle: oklch(0.22 0.008 250);
-/* ============= WHOLE PAGE STYLES ================== */
+ --text: oklch(0.92 0.005 250);
+ --text-2: oklch(0.62 0.008 250);
+ --text-3: oklch(0.42 0.008 250);
-/* Handle vertical screens */
-@media screen and (max-width: 1400px) and (orientation: portrait) {
- #cameraControlsContainer {
- width: 100vw!important;
- }
+ --accent: oklch(0.78 0.14 76);
+ --accent-dim: oklch(0.78 0.14 76 / 0.14);
+ --accent-border: oklch(0.78 0.14 76 / 0.4);
- #cameraControlsContainerExpanded {
- display: none!important;
- }
+ --rec: oklch(0.58 0.22 22);
+ --rec-dim: oklch(0.58 0.22 22 / 0.18);
+ --rec-border: oklch(0.58 0.22 22 / 0.5);
- body {
- font-size: 125%;
- }
+ --ch-luma: oklch(0.74 0.006 250);
+ --ch-red: oklch(0.66 0.18 22);
+ --ch-green: oklch(0.70 0.16 145);
+ --ch-blue: oklch(0.65 0.15 265);
+
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+
+ --top-bar-h: 52px;
+ --tab-bar-h: 40px;
+ --status-h: 28px;
}
-@media screen and (max-width: 1400px) and (orientation: landscape) {
- #cameraControlsContainer {
- width: 30vw!important;
- }
+/* Reset */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
- #headerContainer {
- height: 5vh!important;
- }
-
- body {
- font-size: 70%;
- }
-}
-
-/* Load NotoSansDisplay Font from resources */
-@font-face {
- font-family: 'NotoSansDisplay';
- src: url('resources/NotoSansDisplay-VariableFont_wdth\,wght.woff') format('woff'), /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
- url('resources/NotoSansDisplay-VariableFont_wdth\,wght.ttf') format('truetype'); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */
-}
-
-.dNone {
- display: none;
-}
-
-body {
- font-family: 'NotoSansDisplay', sans-serif;
- font-weight: 100;
- margin: 0px;
- overflow: hidden;
- background: #181818;
- color: white;
-}
-
-/* Page Body Flexboxes */
-.flexContainerH {
- display: flex;
-}
-
-.flexContainerV {
- display: inline-flex;
-}
-
-/* Inputs */
-
-input[type=text] {
- border-radius: 0.5vh;
- background: rgb(30, 30, 30);
- color: white;
- height: 2em;
- width: 10vw;
- border: 1px solid rgb(20, 20, 20);
- text-align: center;
- font-family: 'NotoSansDisplay', sans-serif;
- outline: none;
- margin: 0px 0.5vw;
-}
-
-input[type=text]:focus {
- border: 1px solid rgb(150, 58, 0);
-}
-
-input[type=number] {
- border-radius: 0.5vh;
- background: rgb(30, 30, 30);
- color: white;
- height: 2em;
- width: 4.666vw;
- border: 1px solid rgb(20, 20, 20);
- text-align: center;
- font-family: 'NotoSansDisplay', sans-serif;
- outline: none;
-}
-
-input[type=number]:focus {
- border: 1px solid rgb(150, 58, 0);
-}
-
-input[type=range][orient=vertical] {
- writing-mode: vertical-lr; direction: rtl;
- width: 2vw;
- height: 80%;
-}
-
-select {
- border-radius: 0.5vh;
- background: rgb(30, 30, 30);
- color: white;
- height: 2em;
- width: 5vw;
- border: 1px solid rgb(20, 20, 20);
- text-align: center;
- font-family: 'NotoSansDisplay', sans-serif;
- outline: none;
-}
-
-select:focus {
- border: 1px solid rgb(150, 58, 0);
-}
-
-button {
- font-family: 'NotoSansDisplay', sans-serif;
- background: #181818;
- background: linear-gradient(0deg, #232323 0%, #404040 100%);
- border: 0.3vh solid black;
- outline: 0.3vh solid #404040;
- color: white;
- height: fit-content;
- width: fit-content;
- border-radius: 0.4em;
- padding: 0.5vh 1vw;
- margin: 1vh;
-}
-
-button:hover {
- background: #313131;
- background: linear-gradient(0deg, #313131 0%, #4d4d4d 100%);
-}
-
-button:active {
- background: #181818;
- box-shadow: inset 0 0 1em #090909;
-}
-
-input[type=file]:focus {
- border: 1px solid rgb(150, 58, 0);
-}
-
-#secureCheckboxLabel {
- margin-right: 1vw;
-}
-
-/* Horizontal Container Styles */
-#headerContainer {
- background: #181818;
- background: linear-gradient(0deg, #181818 0%, #303030 100%);
- border-bottom: 1px solid black;
- width: 100%;
- height: 7.77vh;
- flex-wrap: wrap;
- align-content: center;
-}
-
-#headerContainer h1 {
- color: white;
- margin-left: 1.3vw;
- font-weight: 100;
-}
-
-#cameraSelectContainer {
- background: #222222;
- border: 1px solid black;
- width: 100%;
- height: 3.53vh;
- color: #474747;
- font-size: 1em;
- align-items: center;
- justify-content: center;
-}
-
-#cameraSelectContainer span {
- height: fit-content;
-}
-
-#cameraSelectContainer .camSelectSeparator {
- margin: 0px 0.5em;
-}
-
-.cameraSwitchLabel a {
- text-decoration: none;
- color: #474747;
-}
-
-.cameraSwitchLabel.selectedCam a {
- color: #e66c01;
-}
-
-#allCamerasContainer {
- width: 100%;
- height: 83.1vh;
-
- overflow-x: scroll;
- overflow-y: hidden;
-
- scrollbar-color: #202020 #151515;
-}
-
-#formatDisplay span {
- margin: 0px 1em;
-}
-
-#footerContainer {
- background: #181818;
- background: linear-gradient(0deg, #181818 0%, #303030 100%);
- border: 1px solid black;
- width: 100%;
- height: 5vh;
- position: fixed;
- bottom: 0;
- align-items: center;
- justify-content: space-between;
-}
-
-#footerContainer button {
- padding: 0.4em 0.8em;
- font-size: x-small;
-}
-
-#refreshingText.refreshing {
- color: #606060ff;
-}
-
-#refreshingText {
- color: #60606000;
-}
-
-#footerLinks span {
- margin-right: 1.25vw;
-}
-
-#footerLeft span {
- margin-left: 1.25vw;
-}
-
-#footerLinks a {
- text-decoration: none;
- color: #e66c01;
-}
-
-/* Camera Controls Container */
-#cameraControlsContainer {
- width: 15vw;
+html, body {
height: 100%;
- background: #282828;
- flex-direction: column;
- border: 1px solid black;
+ background: var(--bg);
+ color: var(--text);
+ font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', 'Consolas', monospace;
+ font-size: 13px;
+ line-height: 1.4;
+ -webkit-font-smoothing: antialiased;
+ overflow: hidden;
+}
+
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+/* ============================
+ TOP BAR
+ ============================ */
+
+#topBar {
+ position: fixed;
+ top: 0; left: 0; right: 0;
+ height: var(--top-bar-h);
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 0 12px;
+ z-index: 100;
+}
+
+#camSwitcher {
+ display: flex;
+ gap: 2px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 3px;
flex-shrink: 0;
}
-.liveCam {
- background: rgb(184,3,16);
- background: linear-gradient(90deg, rgba(184,3,16,1) 0%, rgba(255,0,19,1) 15%, rgba(255,0,19,1) 85%, rgba(184,3,16,1) 100%);
+.camBtn {
+ background: none;
+ border: none;
+ color: var(--text-2);
+ font: inherit;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ padding: 4px 8px;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: color 0.12s, background 0.12s;
}
+.camBtn:hover { color: var(--text); background: var(--elevated); }
+.camBtn.selected { color: var(--accent); background: var(--accent-dim); }
-#cameraControlHeadContainer {
- width: 100%;
- height: 5vh;
- border-bottom: 2px solid black;
- align-items: center;
- color: white;
-}
+#topBar.recording { border-bottom-color: var(--rec-border); }
-h2 {
- margin-inline-start: 0.6em;
- margin-inline-end: 0.6em;
- font-weight: 100;
+#camInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ min-width: 140px;
}
#cameraName {
-
-}
-
-/* Color Correction Section */
-#cameraControlColorCorrectionContainer {
- width: 100%;
- height: 33vh;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
-}
-
-#cameraControlLGGTabs {
- margin-top: 2vh;
- border: 1px solid #101010;
- border-radius: 2.5vh;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ color: var(--text);
+ white-space: nowrap;
overflow: hidden;
+ text-overflow: ellipsis;
}
-
-#cameraControlLGGTabs a {
- background: #181818;
- background: linear-gradient(0deg, #181818 0%, #303030 100%);
-
- padding: 0.1vh 1.25vw 0.25vh 1.25vw;
-
- border-left: 1px solid #101010;
-
- text-decoration: none;
- color: #474747;
+.timecode {
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+ color: var(--text-2);
+ letter-spacing: 0.08em;
}
-#cameraControlLGGTabs a.selectedTab {
- color: #e66c01;
-}
-
-.ccExposureSettingValueContainer .expAdjArr {
- text-decoration: none;
- color: #60606000;
-}
-
-.ccExposureSettingValueContainer:hover .expAdjArr {
- color: #606060ff;
-}
-
-.ccExposureSettingValueContainer:hover .expAdjArr:hover {
- color: #474747;
-}
-
-.ccExposureSettingValueContainer:hover .expAdjArr:active {
- color: #e66c01;
-}
-
-#cameraControlColorCorrectionBottomContainer {
- margin-bottom: 1em;
- display: inline-flex;
- align-items: center;
-}
-
-#cameraControlColorCorrectionBottomContainer button {
- margin: 0px 0.5em;
-}
-
-#cameraControlColorCorrectionNumbersContainer span {
- margin: 0px 0.5em;
- text-decoration: underline 3px;
-}
-
-/* Exposure Section */
-#cameraControlExposureContainer {
- width: 100%;
- height: 4.7vh;
- background-color: #171717;
- border-top: 1px solid #2d2d2d;
- border-bottom: 1px solid #2d2d2d;
- display: inline-flex;
- justify-content: space-evenly;
- overflow: hidden;
-}
-
-.ccExposureSettingContainer {
+#formatDisplay {
display: flex;
- color: white;
- font-size: 0.86em;
- flex-direction: column;
- align-content: center;
- justify-content: center;
- align-items: center;
- padding-bottom: 0.5vh;
- padding-top: 0.25vh;
+ gap: 8px;
+ font-size: 11px;
+ color: var(--text-2);
+ letter-spacing: 0.04em;
}
+#formatCodec { color: var(--text); font-weight: 600; }
-.exposureControlLabel {
- color: #6e6e6e;
- font-size: 0.666em;
- display: block;
-}
-
-/* Lens Stuff */
-#cameraControlLensContainer {
- width: 100%;
- height: 41.9vh;
- border-bottom: 1px solid black;
- justify-content: space-evenly;
-}
-
-.lensSliderContainer {
+#transportControls {
+ display: flex;
+ gap: 4px;
+ margin-left: auto;
+}
+
+.iconBtn {
+ width: 32px; height: 32px;
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-2);
+ font-size: 13px;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ transition: color 0.1s, border-color 0.1s, background 0.1s;
+}
+.iconBtn:hover { color: var(--text); border-color: var(--text-3); background: var(--elevated); }
+.iconBtn.activated { color: var(--accent); border-color: var(--accent-border); background: var(--accent-dim); }
+
+#recordButton {
display: flex;
- flex-direction: column;
align-items: center;
+ gap: 6px;
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-2);
+ font: inherit;
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ padding: 6px 12px;
+ cursor: pointer;
+ transition: all 0.1s;
+ flex-shrink: 0;
}
+#recordButton:hover { border-color: var(--rec-border); color: var(--rec); }
+#recordButton.recording { border-color: var(--rec-border); color: var(--rec); background: var(--rec-dim); }
-.lensSliderContainer span {
- margin-top: 1em;
- margin-bottom: 1em;
-}
-
-.circleButton {
- width: 2em;
- height: 2em;
- border-radius: 1em;
- padding: 0;
- margin: 1em 0;
-}
-
-.activated {
- background: #181818;
- box-shadow: inset 0 0 1em #090909;
-}
-
-#transportControls .circleButton {
- margin: 0 0.5em;
-}
-
-/* Right side (expanded) */
-#cameraControlsContainerExpanded {
- width: 84.75vw;
- height: 100%;
- background: #282828;
- flex-direction: column;
- border: 1px solid black;
+#recDot {
+ width: 8px; height: 8px;
+ border-radius: 50%;
+ background: var(--rec);
flex-shrink: 0;
}
-#cameraControlExpandedHeadContainer {
- width: 100%;
- height: 5vh;
- border-bottom: 2px solid black;
+#recordButton.recording #recDot {
+ animation: recPulse 1s ease-in-out infinite;
+}
+
+@keyframes recPulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.3; }
+}
+
+/* ============================
+ TAB BAR
+ ============================ */
+
+#tabBar {
+ position: fixed;
+ top: var(--top-bar-h);
+ left: 0; right: 0;
+ height: var(--tab-bar-h);
+ background: var(--bg);
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: stretch;
+ padding: 0 12px;
+ z-index: 90;
+}
+
+.tab {
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--text-2);
+ font: inherit;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ padding: 0 16px;
+ cursor: pointer;
+ transition: color 0.12s, border-color 0.12s;
+ white-space: nowrap;
+}
+.tab:hover { color: var(--text); }
+.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
+
+/* ============================
+ MAIN CONTENT
+ ============================ */
+
+#mainContent {
+ position: fixed;
+ top: calc(var(--top-bar-h) + var(--tab-bar-h));
+ bottom: var(--status-h);
+ left: 0; right: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.tabPanel { display: none; padding: 20px; }
+.tabPanel.active { display: block; }
+
+/* ============================
+ EXPOSURE TAB
+ ============================ */
+
+.exposureRow {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin-bottom: 20px;
+}
+
+.expPill {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 12px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 140px;
+ flex: 1;
+}
+
+.expPillLabel {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ color: var(--text-3);
+ text-transform: uppercase;
+}
+
+.expPillValue {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.expValue {
+ font-size: 20px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ color: var(--text);
+ flex: 1;
+ text-align: center;
+ outline: none;
+ min-width: 60px;
+}
+.expValue:focus { color: var(--accent); }
+
+.adjBtn {
+ width: 28px; height: 28px;
+ background: none;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ color: var(--text-3);
+ font-size: 10px;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ transition: color 0.1s, border-color 0.1s;
+}
+.adjBtn:hover { color: var(--text); border-color: var(--border); }
+.adjBtn:active { background: var(--elevated); }
+
+.awbBtn {
+ background: none;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ color: var(--text-3);
+ font: inherit;
+ font-size: 10px;
+ font-weight: 700;
+ padding: 4px 8px;
+ cursor: pointer;
+ transition: color 0.1s, border-color 0.1s;
+}
+.awbBtn:hover { color: var(--accent); border-color: var(--accent-border); }
+
+.hidden { display: none !important; }
+
+.settingsRow {
+ display: flex;
+ align-items: flex-end;
+ gap: 16px;
+ flex-wrap: wrap;
+ padding: 12px 0;
+ border-top: 1px solid var(--border-subtle);
+}
+
+.settingGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.settingLabel {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ color: var(--text-3);
+ text-transform: uppercase;
+}
+
+.settingSelect {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text);
+ font: inherit;
+ font-size: 12px;
+ padding: 6px 10px;
+ cursor: pointer;
+ outline: none;
+}
+.settingSelect:focus { border-color: var(--accent); }
+
+.settingInput {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text);
+ font: inherit;
+ font-size: 12px;
+ padding: 6px 10px;
+ outline: none;
+}
+.settingInput:focus { border-color: var(--accent); }
+
+.actionBtn {
+ background: var(--elevated);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text);
+ font: inherit;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 7px 14px;
+ cursor: pointer;
+ transition: background 0.1s, border-color 0.1s;
+ white-space: nowrap;
+ align-self: flex-end;
+}
+.actionBtn:hover { background: var(--border); border-color: var(--text-3); }
+
+.presetBtnRow { display: flex; gap: 4px; }
+
+.presetBtn {
+ background: none;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ color: var(--text-2);
+ font: inherit;
+ font-size: 11px;
+ padding: 5px 10px;
+ cursor: pointer;
+ transition: all 0.1s;
+}
+.presetBtn:hover { color: var(--accent); border-color: var(--accent-border); background: var(--accent-dim); }
+
+/* ============================
+ COLOR SCIENCE TAB
+ ============================ */
+
+.csGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+ margin-bottom: 24px;
+}
+
+.csCard {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.csCard--full { grid-column: 1 / -1; }
+
+.csCardTitle {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ color: var(--text-3);
+ text-transform: uppercase;
+}
+
+.csOptions { display: flex; flex-wrap: wrap; gap: 6px; }
+
+.csOptionBtn {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-2);
+ font: inherit;
+ font-size: 12px;
+ font-weight: 600;
+ padding: 8px 14px;
+ cursor: pointer;
+ transition: all 0.12s;
+}
+.csOptionBtn:hover { color: var(--text); border-color: var(--text-3); background: var(--elevated); }
+.csOptionBtn.selected { color: var(--accent); border-color: var(--accent-border); background: var(--accent-dim); }
+
+.csCurrentRow {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ padding-top: 8px;
+ border-top: 1px solid var(--border-subtle);
+}
+.csCurrentLabel { font-size: 10px; color: var(--text-3); letter-spacing: 0.06em; }
+.csCurrentValue { font-size: 13px; font-weight: 600; color: var(--accent); }
+
+.csQuickPresets {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 8px;
+}
+
+.csQuickBtn {
+ background: var(--elevated);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text);
+ font: inherit;
+ padding: 12px 14px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ text-align: left;
+ transition: all 0.12s;
+}
+.csQuickBtn:hover { border-color: var(--accent-border); background: var(--accent-dim); }
+
+.csQuickBtnName { font-size: 13px; font-weight: 700; }
+.csQuickBtnDetail { font-size: 10px; color: var(--text-2); letter-spacing: 0.04em; }
+
+.formatGrid {
+ display: flex;
+ align-items: flex-end;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.statusNote { font-size: 11px; color: var(--text-2); align-self: center; }
+
+.sectionDivider {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin: 20px 0 12px;
+ color: var(--text-3);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+.sectionDivider::before, .sectionDivider::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ background: var(--border-subtle);
+}
+
+/* ============================
+ COLOR CORRECTION TAB
+ ============================ */
+
+.ccGrid {
+ display: flex;
+ flex-direction: column;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ margin-bottom: 16px;
+}
+
+.ccRow {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-subtle);
+ transition: background 0.1s;
+}
+.ccRow:last-child { border-bottom: none; }
+.ccRow:hover { background: var(--elevated); }
+
+.ccRowLabel {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ color: var(--text-2);
+ width: 52px;
+ flex-shrink: 0;
+}
+
+.ccChannels {
+ display: flex;
+ gap: 24px;
+ flex: 1;
+}
+
+.ccChannel { display: flex; align-items: center; gap: 6px; }
+
+.ccChLabel {
+ font-size: 11px;
+ font-weight: 700;
+ width: 12px;
+ flex-shrink: 0;
+}
+
+.ccVal {
+ font-size: 14px;
+ font-variant-numeric: tabular-nums;
+ color: var(--text);
+ min-width: 44px;
+ outline: none;
+ text-align: right;
+}
+.ccVal:focus { color: var(--accent); }
+
+.ccApplyBtn, .ccResetBtn {
+ width: 30px; height: 30px;
+ background: none;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ color: var(--text-3);
+ font-size: 14px;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ transition: all 0.1s;
+}
+.ccApplyBtn:hover { color: var(--accent); border-color: var(--accent-border); }
+.ccResetBtn:hover { color: var(--text); border-color: var(--border); }
+
+.sliderGroup {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 14px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+.sliderRow { display: flex; align-items: center; gap: 12px; }
+
+.sliderLabel {
+ font-size: 11px;
+ color: var(--text-2);
+ width: 110px;
+ flex-shrink: 0;
+}
+
+.slider {
+ flex: 1;
+ height: 4px;
+ appearance: none;
+ background: var(--elevated);
+ border-radius: 2px;
+ outline: none;
+ cursor: pointer;
+}
+.slider::-webkit-slider-thumb {
+ appearance: none;
+ width: 14px; height: 14px;
+ border-radius: 50%;
+ background: var(--accent);
+ cursor: pointer;
+}
+
+.sliderVal {
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ color: var(--text-2);
+ width: 44px;
+ text-align: right;
+ outline: none;
+}
+.sliderVal:focus { color: var(--accent); }
+
+/* ============================
+ LENS TAB
+ ============================ */
+
+.lensGrid {
+ display: flex;
+ gap: 24px;
+ justify-content: center;
+ padding: 20px;
+ height: calc(100vh - var(--top-bar-h) - var(--tab-bar-h) - var(--status-h) - 40px);
+}
+
+.lensCard {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 20px 40px;
+ min-width: 120px;
+}
+
+.lensCardLabel {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ color: var(--text-3);
+ text-transform: uppercase;
+}
+
+.vertSlider {
+ writing-mode: vertical-lr;
+ direction: rtl;
+ flex: 1;
+ width: 4px;
+ appearance: none;
+ background: var(--elevated);
+ border-radius: 2px;
+ outline: none;
+ cursor: pointer;
+}
+.vertSlider::-webkit-slider-thumb {
+ appearance: none;
+ width: 22px; height: 22px;
+ border-radius: 50%;
+ background: var(--accent);
+ cursor: pointer;
+ box-shadow: 0 0 0 4px var(--accent-dim);
+}
+
+.lensActionBtn {
+ background: var(--elevated);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text);
+ font: inherit;
+ font-size: 12px;
+ font-weight: 700;
+ padding: 8px 20px;
+ cursor: pointer;
+ transition: all 0.1s;
+}
+.lensActionBtn:hover { border-color: var(--accent-border); color: var(--accent); background: var(--accent-dim); }
+
+.lensValueLabel {
+ font-size: 13px;
+ font-variant-numeric: tabular-nums;
+ color: var(--text-2);
+ font-weight: 600;
+}
+
+/* ============================
+ SYSTEM TAB
+ ============================ */
+
+.sysGrid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+}
+
+.sysCard {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.sysCard--full { grid-column: 1 / -1; }
+
+.sysCardTitle {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ color: var(--text-3);
+ text-transform: uppercase;
+}
+
+.sysRow { display: flex; flex-direction: column; gap: 4px; }
+.sysRow--inline { flex-direction: row; align-items: center; gap: 10px; flex-wrap: wrap; }
+
+.hostnameInput { min-width: 220px; }
+
+.apiResponse {
+ font-size: 11px;
+ color: var(--text-2);
+ background: var(--bg);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ padding: 8px 10px;
+ word-break: break-all;
+ min-height: 36px;
+}
+
+.linkRow { display: flex; gap: 16px; flex-wrap: wrap; }
+.sysLink { font-size: 12px; font-weight: 600; color: var(--accent); letter-spacing: 0.04em; }
+
+/* ============================
+ STATUS BAR
+ ============================ */
+
+#statusBar {
+ position: fixed;
+ bottom: 0; left: 0; right: 0;
+ height: var(--status-h);
+ background: var(--bg);
+ border-top: 1px solid var(--border-subtle);
+ display: flex;
align-items: center;
justify-content: space-between;
+ padding: 0 12px;
+ font-size: 10px;
+ color: var(--text-3);
+ letter-spacing: 0.04em;
+ z-index: 100;
}
-#cameraControlExpandedBodyContainer {
- flex-direction: column;
- display: inline-flex;
- height: inherit;
- justify-content: space-around;
-}
+.version { opacity: 0.5; }
-#timecodeLabel {
-
-}
-
-#connectionErrorSpan {
- color: #e64b3d;
-}
-
-#manualRequestResponseP {
- color: #6e6e6e;
-}
-
-/* Table Controls */
-
-.tableControl {
- margin-left: 1.5vw;
-}
-
-.tableControl h3 {
- font-weight: 100;
- margin: 0.5vh 0vw;
-}
-
-table, td {
- margin-left: 0.5vw;
- /* border: 1px solid white; */
- border-collapse: collapse;
- padding: 0.1vw 1vw;
-}
-
-td, tr {
- align-items: center;
-}
\ No newline at end of file
+#mainContent::-webkit-scrollbar { width: 6px; }
+#mainContent::-webkit-scrollbar-track { background: transparent; }
+#mainContent::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
diff --git a/web-ui.js b/web-ui.js
index 83b1df5..995b021 100644
--- a/web-ui.js
+++ b/web-ui.js
@@ -1,655 +1,648 @@
-/* Blackmagic Camera Control WebUI
- WebUI Script functions
- (c) Dylan Speiser 2024
- github.com/DylanSpeiser
+/* Blackmagic Camera Control WebUI
+ (c) Dylan Speiser 2024 — UI redesign 2025
*/
-
-/* Global variables */
-var cameras = []; // Array to store all of the camera objects
-var ci = 0; // Index into this array for the currently selected camera.
-// cameras[ci] is used to reference the currently selected camera object
-
-var WBMode = 0; // 0: balance, 1: tint
-
-var defaultControlsHTML;
-
+var cameras = [];
+var ci = 0;
+var WBMode = 0; // 0: balance, 1: tint
var unsavedChanges = [];
-// Set everything up
function bodyOnLoad() {
- defaultControlsHTML = document.getElementById("allCamerasContainer").innerHTML;
- // prefill camera hostname (or IP address)
- document.getElementById("hostnameInput").value = localStorage.getItem("camerahostname_"+ci.toString());
- if ( localStorage.getItem("camerasecurity_"+ci.toString()) === 'true' ) {
- document.getElementById("secureCheckbox").checked = true
+ document.getElementById('hostnameInput').value = localStorage.getItem('camerahostname_' + ci) || '';
+ if (localStorage.getItem('camerasecurity_' + ci) === 'true') {
+ document.getElementById('secureCheckbox').checked = true;
}
}
+// =====================================================================
+// Tab switching
+// =====================================================================
+
+function switchTab(name) {
+ document.querySelectorAll('.tabPanel').forEach(p => p.classList.remove('active'));
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ document.getElementById('tab-' + name).classList.add('active');
+ document.getElementById('tab-btn-' + name).classList.add('active');
+}
+
+// =====================================================================
+// Camera init / switch
+// =====================================================================
-// Checks the hostname, if it replies successfully then a new BMCamera object
-// is made and gets put in the array at ind
function initCamera() {
- // Get hostname from Hostname text field
- let hostname = document.getElementById("hostnameInput").value;
- let security = document.getElementById("secureCheckbox").checked;
+ const hostname = document.getElementById('hostnameInput').value;
+ const security = document.getElementById('secureCheckbox').checked;
+ const errorSpan = document.getElementById('connectionErrorSpan');
try {
- // Check if the hostname is valid
- let response = sendRequest("GET", (security ? "https://" : "http://")+hostname+"/control/api/v1/system","");
-
+ const response = sendRequest('GET', (security ? 'https://' : 'http://') + hostname + '/control/api/v1/system', '');
if (response.status < 300) {
- // Success, make a new camera, get all relevant info, and populate the UI
cameras[ci] = new BMCamera(hostname, security);
- // Save camera hostname and security status in local storage
- localStorage.setItem("camerahostname_"+ci, hostname)
- localStorage.setItem("camerasecurity_"+ci, security)
+ localStorage.setItem('camerahostname_' + ci, hostname);
+ localStorage.setItem('camerasecurity_' + ci, security);
cameras[ci].updateUI = updateUIAll;
-
cameras[ci].active = true;
-
- document.getElementById("connectionErrorSpan").innerHTML = "Connected.";
- document.getElementById("connectionErrorSpan").setAttribute("style","color: #6e6e6e;");
-
+ errorSpan.textContent = 'Connected.';
+ errorSpan.style.color = 'var(--accent)';
} else {
- // Something has gone wrong, tell the user
- document.getElementById("connectionErrorSpan").innerHTML = response.statusText;
+ errorSpan.textContent = response.statusText;
+ errorSpan.style.color = 'var(--rec)';
}
} catch (error) {
- // Something has gone wrong, tell the user
- document.getElementById("connectionErrorSpan").title = error;
- document.getElementById("connectionErrorSpan").innerHTML = "Error "+error.code+": "+error.name+" (Your hostname is probably incorrect, hover for more details)";
+ errorSpan.title = error;
+ errorSpan.textContent = 'Error ' + error.code + ': ' + error.name;
+ errorSpan.style.color = 'var(--rec)';
}
-
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "Hostname"});
+ unsavedChanges = unsavedChanges.filter(e => e !== 'Hostname');
}
-// =============================== UI Updater ==================================
-// =============================================================================
+function switchCamera(index) {
+ if (cameras[ci]) cameras[ci].active = false;
+ ci = index;
+
+ document.querySelectorAll('.camBtn').forEach((btn, i) => {
+ btn.classList.toggle('selected', i === ci);
+ });
+
+ document.getElementById('cameraName').textContent = 'NOT CONNECTED';
+ document.getElementById('timecodeLabel').textContent = '--:--:--:--';
+ document.getElementById('hostnameInput').value = localStorage.getItem('camerahostname_' + ci) || '';
+ document.getElementById('secureCheckbox').checked = localStorage.getItem('camerasecurity_' + ci) === 'true';
+
+ if (cameras[ci]) cameras[ci].active = true;
+}
+
+// =====================================================================
+// Main UI updater (called by WebSocket)
+// =====================================================================
function updateUIAll() {
- // ========== Camera Name ==========
+ const cam = cameras[ci];
+ if (!cam) return;
- document.getElementById("cameraName").innerHTML = cameras[ci].name;
+ // Camera name
+ document.getElementById('cameraName').textContent = cam.name;
- // ========== Hostname ==========
-
- if (!unsavedChanges.includes("Hostname")) {
- document.getElementById("hostnameInput").value = cameras[ci].hostname;
+ // Hostname
+ if (!unsavedChanges.includes('Hostname')) {
+ document.getElementById('hostnameInput').value = cam.hostname;
}
- // ========== Format ==========
+ // Format display
+ const fmt = cam.propertyData['/system/format'];
+ document.getElementById('formatCodec').textContent =
+ fmt?.codec?.toUpperCase().replace(':', ' ').replace('_', ':') || '—';
+ const res = fmt?.recordResolution;
+ document.getElementById('formatResolution').textContent =
+ res ? res.width + 'x' + res.height : '—';
+ document.getElementById('formatFPS').textContent =
+ fmt?.frameRate ? fmt.frameRate + ' fps' : '—';
- document.getElementById("formatCodec").innerHTML = cameras[ci].propertyData['/system/format']?.codec.toUpperCase().replace(":"," ").replace("_",":");
-
- let resObj = cameras[ci].propertyData['/system/format']?.recordResolution;
- document.getElementById("formatResolution").innerHTML = resObj?.width + "x" + resObj?.height;
- document.getElementById("formatFPS").innerHTML = cameras[ci].propertyData['/system/format']?.frameRate+" fps";
+ // Recording state
+ const isRecording = cam.propertyData['/transports/0/record']?.recording;
+ document.getElementById('topBar').classList.toggle('recording', !!isRecording);
+ document.getElementById('recordButton').classList.toggle('recording', !!isRecording);
+ document.getElementById('recLabel').textContent = isRecording ? 'RECORDING' : 'REC';
- // ========== Recording State ==========
+ // Loop / single clip buttons
+ const loopState = cam.propertyData['/transports/0/playback']?.loop;
+ const singleClipState = cam.propertyData['/transports/0/playback']?.singleClip;
+ document.getElementById('loopButton').classList.toggle('activated', !!loopState);
+ document.getElementById('singleClipButton').classList.toggle('activated', !!singleClipState);
- if (cameras[ci].propertyData['/transports/0/record']?.recording) {
- document.getElementById("cameraControlHeadContainer").classList.add("liveCam");
- document.getElementById("cameraControlExpandedHeadContainer").classList.add("liveCam");
- } else {
- document.getElementById("cameraControlHeadContainer").classList.remove("liveCam");
- document.getElementById("cameraControlExpandedHeadContainer").classList.remove("liveCam");
- }
+ // Timecode
+ document.getElementById('timecodeLabel').textContent =
+ parseTimecode(cam.propertyData['/transports/0/timecode']?.timecode);
- // ========== Playback Loop State ==========
- let loopState = cameras[ci].propertyData['/transports/0/playback']?.loop;
- let singleClipState = cameras[ci].propertyData['/transports/0/playback']?.singleClip;
-
- let loopButton = document.getElementById("loopButton");
- let singleClipButton = document.getElementById("singleClipButton");
-
- if (loopState) {
- loopButton.classList.add("activated");
- } else {
- loopButton.classList.remove("activated");
- }
-
- if (singleClipState) {
- singleClipButton.classList.add("activated");
- } else {
- singleClipButton.classList.remove("activated");
- }
-
- // ========== Timecode ==========
-
- document.getElementById("timecodeLabel").innerHTML = parseTimecode(cameras[ci].propertyData['/transports/0/timecode']?.timecode);
-
- // ========== Presets Dropdown ==========
-
- if (!unsavedChanges.includes("presets")) {
-
- var presetsList = document.getElementById("presetsDropDown");
-
- presetsList.innerHTML = "";
-
- cameras[ci].propertyData['/presets']?.presets.forEach((presetItem) => {
- let presetName = presetItem.split('.', 1);
-
- let textNode = document.createTextNode(presetName);
- let optionNode = document.createElement("option");
- optionNode.setAttribute("name", "presetOption"+presetName);
- optionNode.appendChild(textNode);
- document.getElementById("presetsDropDown").appendChild(optionNode);
+ // Presets dropdown
+ if (!unsavedChanges.includes('presets')) {
+ const dd = document.getElementById('presetsDropDown');
+ dd.innerHTML = '';
+ cam.propertyData['/presets']?.presets?.forEach(item => {
+ const name = item.split('.', 1)[0];
+ const opt = document.createElement('option');
+ opt.textContent = name;
+ dd.appendChild(opt);
});
-
- // ========== Active Preset ==========
-
- var presetsList = document.getElementById("presetsDropDown");
-
- presetsList.childNodes.forEach((child) => {
- if (child.nodeName == 'OPTION' && (child.value+".cset") == cameras[ci].propertyData['/presets/active']?.preset) {
- child.selected=true
- } else {
- child.selected=false
+ dd.childNodes.forEach(child => {
+ if (child.nodeName === 'OPTION') {
+ child.selected = (child.value + '.cset') === cam.propertyData['/presets/active']?.preset;
}
- })
-
+ });
}
- // ========== Iris ==========
+ // Lens
+ document.getElementById('irisRange').value = cam.propertyData['/lens/iris']?.normalised ?? 0;
+ document.getElementById('apertureStopsLabel').textContent =
+ cam.propertyData['/lens/iris']?.apertureStop != null
+ ? 'f/' + cam.propertyData['/lens/iris'].apertureStop.toFixed(1)
+ : '—';
+ document.getElementById('zoomRange').value = cam.propertyData['/lens/zoom']?.normalised ?? 0;
+ document.getElementById('zoomMMLabel').textContent =
+ cam.propertyData['/lens/zoom']?.focalLength != null
+ ? cam.propertyData['/lens/zoom'].focalLength + 'mm'
+ : '—mm';
+ document.getElementById('focusRange').value = cam.propertyData['/lens/focus']?.normalised ?? 0;
- document.getElementById("irisRange").value = cameras[ci].propertyData['/lens/iris']?.normalised;
- document.getElementById("apertureStopsLabel").innerHTML = cameras[ci].propertyData['/lens/iris']?.apertureStop.toFixed(1);
-
- // ========== Zoom ==========
-
- document.getElementById("zoomRange").value = cameras[ci].propertyData['/lens/zoom']?.normalised;
- document.getElementById("zoomMMLabel").innerHTML = cameras[ci].propertyData['/lens/zoom']?.focalLength +"mm";
-
- // ========== Focus ==========
-
- document.getElementById("focusRange").value = cameras[ci].propertyData['/lens/focus']?.normalised;
-
- // ========== ISO ==========
- if (!unsavedChanges.includes("ISO")) {
- if (cameras[ci].propertyData['/video/iso'])
- document.getElementById("ISOInput").value = cameras[ci].propertyData['/video/iso']?.iso;
- }
-
- // ========== GAIN ==========
-
- if (!unsavedChanges.includes("Gain")) {
- let gainString = "";
- let gainInt = cameras[ci].propertyData['/video/gain']?.gain
-
- if (gainInt >= 0) {
- gainString = "+"+gainInt+"db"
- } else {
- gainString = gainInt+"db"
+ // ISO
+ if (!unsavedChanges.includes('ISO')) {
+ const iso = cam.propertyData['/video/iso']?.iso;
+ if (iso != null) {
+ document.getElementById('ISODisplay').textContent = iso;
+ document.getElementById('ISOInput').value = iso;
}
-
- document.getElementById("gainSpan").innerHTML = gainString;
- }
-
- // ========== WHITE BALANCE ===========
-
- if (!unsavedChanges.includes("WB")) {
- document.getElementById("whiteBalanceSpan").innerHTML = cameras[ci].propertyData['/video/whiteBalance']?.whiteBalance+"K";
- }
-
- if (!unsavedChanges.includes("WBT")) {
- document.getElementById("whiteBalanceTintSpan").innerHTML = cameras[ci].propertyData['/video/whiteBalanceTint']?.whiteBalanceTint;
- }
-
- // =========== ND =============
-
- if (!unsavedChanges.includes("ND")) {
- if (cameras[ci].propertyData['/video/ndFilter']) {
- document.getElementById("ndFilterSpan").innerHTML = cameras[ci].propertyData['/video/ndFilter']?.stop;
- } else {
- document.getElementById("ndFilterSpan").innerHTML = 0;
- document.getElementById("ndFilterSpan").disabled = true;
- }
- }
-
- // ============ Shutter =====================
-
- if (!unsavedChanges.includes("Shutter")) {
- let shutterString = "SS"
- let shutterObj = cameras[ci].propertyData['/video/shutter'];
-
- if (shutterObj?.shutterSpeed) {
- shutterString = "1/"+shutterObj.shutterSpeed
- } else if (shutterObj?.shutterAngle) {
- var shangleString = (shutterObj.shutterAngle / 100).toFixed(1).toString()
- if (shangleString.indexOf(".0") > 0) {
- shutterString = parseFloat(shangleString).toFixed(0)+"°";
- } else {
- shutterString = shangleString+"°";
- }
- }
-
- document.getElementById("shutterSpan").innerHTML = shutterString;
- }
-
- // =========== Auto Exposure Mode ===========
-
- if (!unsavedChanges.includes("AutoExposure")) {
- let AEmodeSelect = document.getElementById("AEmodeDropDown");
- let AEtypeSelect = document.getElementById("AEtypeDropDown");
-
- AEmodeSelect.value = cameras[ci].propertyData['/video/autoExposure']?.mode;
- AEtypeSelect.value = cameras[ci].propertyData['/video/autoExposure']?.type;
- }
-
- // =========== COLOR CORRECTION =============
-
- // Lift
- if (!unsavedChanges.includes("CC0")) {
- let liftProps = cameras[ci].propertyData['/colorCorrection/lift'];
- document.getElementsByClassName("CClumaLabel")[0].innerHTML = liftProps?.luma.toFixed(2);
- document.getElementsByClassName("CCredLabel")[0].innerHTML = liftProps?.red.toFixed(2);
- document.getElementsByClassName("CCgreenLabel")[0].innerHTML = liftProps?.green.toFixed(2);
- document.getElementsByClassName("CCblueLabel")[0].innerHTML = liftProps?.blue.toFixed(2);
- }
-
- // Gamma
- if (!unsavedChanges.includes("CC1")) {
- let gammaProps = cameras[ci].propertyData['/colorCorrection/gamma'];
- document.getElementsByClassName("CClumaLabel")[1].innerHTML = gammaProps?.luma.toFixed(2);
- document.getElementsByClassName("CCredLabel")[1].innerHTML = gammaProps?.red.toFixed(2);
- document.getElementsByClassName("CCgreenLabel")[1].innerHTML = gammaProps?.green.toFixed(2);
- document.getElementsByClassName("CCblueLabel")[1].innerHTML = gammaProps?.blue.toFixed(2);
}
// Gain
- if (!unsavedChanges.includes("CC2")) {
- let gainProps = cameras[ci].propertyData['/colorCorrection/gain'];
- document.getElementsByClassName("CClumaLabel")[2].innerHTML = gainProps?.luma.toFixed(2);
- document.getElementsByClassName("CCredLabel")[2].innerHTML = gainProps?.red.toFixed(2);
- document.getElementsByClassName("CCgreenLabel")[2].innerHTML = gainProps?.green.toFixed(2);
- document.getElementsByClassName("CCblueLabel")[2].innerHTML = gainProps?.blue.toFixed(2);
+ if (!unsavedChanges.includes('Gain')) {
+ const gain = cam.propertyData['/video/gain']?.gain;
+ if (gain != null) {
+ document.getElementById('gainDisplay').textContent = (gain >= 0 ? '+' : '') + gain + 'dB';
+ }
+ }
+
+ // White balance
+ if (!unsavedChanges.includes('WB')) {
+ const wb = cam.propertyData['/video/whiteBalance']?.whiteBalance;
+ if (wb != null) document.getElementById('whiteBalanceDisplay').textContent = wb + 'K';
+ }
+ if (!unsavedChanges.includes('WBT')) {
+ const wbt = cam.propertyData['/video/whiteBalanceTint']?.whiteBalanceTint;
+ if (wbt != null) document.getElementById('whiteBalanceTintDisplay').textContent = wbt;
+ }
+
+ // ND filter
+ if (!unsavedChanges.includes('ND')) {
+ const nd = cam.propertyData['/video/ndFilter'];
+ document.getElementById('ndFilterDisplay').textContent = nd ? nd.stop : '0';
+ }
+
+ // Shutter
+ if (!unsavedChanges.includes('Shutter')) {
+ const shutter = cam.propertyData['/video/shutter'];
+ let shutterStr = '—';
+ if (shutter?.shutterSpeed) {
+ shutterStr = '1/' + shutter.shutterSpeed;
+ } else if (shutter?.shutterAngle) {
+ const angle = (shutter.shutterAngle / 100).toFixed(1);
+ shutterStr = (angle.endsWith('.0') ? parseFloat(angle).toFixed(0) : angle) + '°';
+ }
+ document.getElementById('shutterDisplay').textContent = shutterStr;
+ }
+
+ // AE mode/type
+ if (!unsavedChanges.includes('AutoExposure')) {
+ document.getElementById('AEmodeDropDown').value = cam.propertyData['/video/autoExposure']?.mode || 'Off';
+ document.getElementById('AEtypeDropDown').value = cam.propertyData['/video/autoExposure']?.type || '';
+ }
+
+ // Color correction — Lift
+ if (!unsavedChanges.includes('CC0')) {
+ const lift = cam.propertyData['/colorCorrection/lift'];
+ if (lift) {
+ document.getElementsByClassName('CClumaLabel')[0].textContent = lift.luma?.toFixed(2);
+ document.getElementsByClassName('CCredLabel')[0].textContent = lift.red?.toFixed(2);
+ document.getElementsByClassName('CCgreenLabel')[0].textContent = lift.green?.toFixed(2);
+ document.getElementsByClassName('CCblueLabel')[0].textContent = lift.blue?.toFixed(2);
+ }
+ }
+
+ // Gamma
+ if (!unsavedChanges.includes('CC1')) {
+ const gamma = cam.propertyData['/colorCorrection/gamma'];
+ if (gamma) {
+ document.getElementsByClassName('CClumaLabel')[1].textContent = gamma.luma?.toFixed(2);
+ document.getElementsByClassName('CCredLabel')[1].textContent = gamma.red?.toFixed(2);
+ document.getElementsByClassName('CCgreenLabel')[1].textContent = gamma.green?.toFixed(2);
+ document.getElementsByClassName('CCblueLabel')[1].textContent = gamma.blue?.toFixed(2);
+ }
+ }
+
+ // Gain CC
+ if (!unsavedChanges.includes('CC2')) {
+ const gainCC = cam.propertyData['/colorCorrection/gain'];
+ if (gainCC) {
+ document.getElementsByClassName('CClumaLabel')[2].textContent = gainCC.luma?.toFixed(2);
+ document.getElementsByClassName('CCredLabel')[2].textContent = gainCC.red?.toFixed(2);
+ document.getElementsByClassName('CCgreenLabel')[2].textContent = gainCC.green?.toFixed(2);
+ document.getElementsByClassName('CCblueLabel')[2].textContent = gainCC.blue?.toFixed(2);
+ }
}
// Offset
- if (!unsavedChanges.includes("CC3")) {
- let offsetProps = cameras[ci].propertyData['/colorCorrection/offset'];
- document.getElementsByClassName("CClumaLabel")[3].innerHTML = offsetProps?.luma.toFixed(2);
- document.getElementsByClassName("CCredLabel")[3].innerHTML = offsetProps?.red.toFixed(2);
- document.getElementsByClassName("CCgreenLabel")[3].innerHTML = offsetProps?.green.toFixed(2);
- document.getElementsByClassName("CCblueLabel")[3].innerHTML = offsetProps?.blue.toFixed(2);
+ if (!unsavedChanges.includes('CC3')) {
+ const offset = cam.propertyData['/colorCorrection/offset'];
+ if (offset) {
+ document.getElementsByClassName('CClumaLabel')[3].textContent = offset.luma?.toFixed(2);
+ document.getElementsByClassName('CCredLabel')[3].textContent = offset.red?.toFixed(2);
+ document.getElementsByClassName('CCgreenLabel')[3].textContent = offset.green?.toFixed(2);
+ document.getElementsByClassName('CCblueLabel')[3].textContent = offset.blue?.toFixed(2);
+ }
}
// Contrast
- if (!unsavedChanges.includes("CC4")) {
- let constrastProps = cameras[ci].propertyData['/colorCorrection/contrast'];
- document.getElementById("CCcontrastPivotRange").value = constrastProps?.pivot;
- document.getElementById("CCcontrastPivotLabel").innerHTML = constrastProps?.pivot.toFixed(2);
- document.getElementById("CCcontrastAdjustRange").value = constrastProps?.adjust;
- document.getElementById("CCcontrastAdjustLabel").innerHTML = parseInt(constrastProps?.adjust * 50)+"%";
- }
-
- // Color
- if (!unsavedChanges.includes("CC5")) {
- let colorProps = cameras[ci].propertyData['/colorCorrection/color'];
- document.getElementById("CChueRange").value = colorProps?.hue;
- document.getElementById("CCcolorHueLabel").innerHTML = parseInt((colorProps?.hue + 1) * 180)+"°";
-
- document.getElementById("CCsaturationRange").value = colorProps?.saturation;
- document.getElementById("CCcolorSatLabel").innerHTML = parseInt(colorProps?.saturation * 50)+"%";
-
- let lumaContributionProps = cameras[ci].propertyData['/colorCorrection/lumaContribution'];
- document.getElementById("CClumaContributionRange").value = lumaContributionProps?.lumaContribution;
- document.getElementById("CCcolorLCLabel").innerHTML = parseInt(lumaContributionProps?.lumaContribution * 100)+"%";
- }
-
- // ============ Footer Links ===============
- document.getElementById("documentationLink").href = (cameras[ci].useHTTPS ? "https://" : "http://")+cameras[ci].hostname+"/control/documentation.html";
- document.getElementById("mediaManagerLink").href = (cameras[ci].useHTTPS ? "https://" : "http://")+cameras[ci].hostname;
-}
-
-
-// ==============================================================================
-
-// Called when the user changes tabs to a different camera
-function switchCamera(index) {
- if (cameras[ci]) {
- cameras[ci].active = false;
- }
-
- ci = index;
-
- // Reset the Controls
- document.getElementById("allCamerasContainer").innerHTML = defaultControlsHTML;
-
- // Update the UI
-
- for (var i = 0; i < 8; i++) {
- if (i == ci) {
- document.getElementsByClassName("cameraSwitchLabel")[i].classList.add("selectedCam");
- } else {
- document.getElementsByClassName("cameraSwitchLabel")[i].classList.remove("selectedCam");
+ if (!unsavedChanges.includes('CC4')) {
+ const contrast = cam.propertyData['/colorCorrection/contrast'];
+ if (contrast) {
+ document.getElementById('CCcontrastPivotRange').value = contrast.pivot;
+ document.getElementById('CCcontrastPivotLabel').textContent = contrast.pivot?.toFixed(2);
+ document.getElementById('CCcontrastAdjustRange').value = contrast.adjust;
+ document.getElementById('CCcontrastAdjustLabel').textContent = parseInt(contrast.adjust * 50) + '%';
}
}
- document.getElementById("cameraNumberLabel").innerHTML = "CAM"+(ci+1);
- document.getElementById("cameraName").innerHTML = "CAMERA NAME";
- document.getElementById("hostnameInput").value = localStorage.getItem("camerahostname_"+ci.toString());
- if ( localStorage.getItem("camerasecurity_"+ci.toString()) === 'true' ) {
- document.getElementById("secureCheckbox").checked = true
- }
- if (cameras[ci]) {
- cameras[ci].active = true;
- }
-}
-
-// For not-yet-implemented Color Correction UI
-function setCCMode(mode) {
- if (mode == 0) {
- // Lift
-
- } else if (mode == 1) {
- // Gamma
-
- } else {
- // Gain
-
- }
-
- for (var i = 0; i < 3; i++) {
- if (i == mode) {
- document.getElementsByClassName("ccTabLabel")[i].classList.add("selectedTab");
- } else {
- document.getElementsByClassName("ccTabLabel")[i].classList.remove("selectedTab");
+ // Color / hue / sat
+ if (!unsavedChanges.includes('CC5')) {
+ const color = cam.propertyData['/colorCorrection/color'];
+ if (color) {
+ document.getElementById('CChueRange').value = color.hue;
+ document.getElementById('CCcolorHueLabel').textContent = parseInt((color.hue + 1) * 180) + '°';
+ document.getElementById('CCsaturationRange').value = color.saturation;
+ document.getElementById('CCcolorSatLabel').textContent = parseInt(color.saturation * 50) + '%';
+ }
+ const lc = cam.propertyData['/colorCorrection/lumaContribution'];
+ if (lc) {
+ document.getElementById('CClumaContributionRange').value = lc.lumaContribution;
+ document.getElementById('CCcolorLCLabel').textContent = parseInt(lc.lumaContribution * 100) + '%';
}
}
+
+ // Color science (new)
+ updateColorScienceUI();
+
+ // Footer links
+ document.getElementById('documentationLink').href =
+ (cam.useHTTPS ? 'https://' : 'http://') + cam.hostname + '/control/documentation.html';
+ document.getElementById('mediaManagerLink').href =
+ (cam.useHTTPS ? 'https://' : 'http://') + cam.hostname;
}
-// Allows for changing WB/Tint displayed in the UI
-function swapWBMode() {
- if (WBMode == 0) {
- // Balance
- document.getElementById("WBLabel").innerHTML = "TINT";
- document.getElementById("WBValueContainer").classList.add("dNone");
- document.getElementById("WBTintValueContainer").classList.remove("dNone");
-
- WBMode = 1;
+// =====================================================================
+// Color Science (new)
+// =====================================================================
+
+function updateColorScienceUI() {
+ const cam = cameras[ci];
+ if (!cam) return;
+ const cs = cam.propertyData['/video/colorScience'];
+ if (!cs) return;
+
+ const gamma = cs.gamma || '';
+ const gamut = cs.gamut || '';
+
+ document.getElementById('currentGamma').textContent = gamma || '—';
+ document.getElementById('currentGamut').textContent = gamut || '—';
+
+ // Highlight selected gamma button
+ document.querySelectorAll('#gammaOptions .csOptionBtn').forEach(btn => {
+ btn.classList.toggle('selected', btn.dataset.gamma === gamma);
+ });
+
+ // Highlight selected gamut button
+ document.querySelectorAll('#gamutOptions .csOptionBtn').forEach(btn => {
+ btn.classList.toggle('selected', btn.dataset.gamut === gamut);
+ });
+}
+
+function setGamma(btn) {
+ if (!cameras[ci]) return;
+ const gamma = btn.dataset.gamma;
+ cameras[ci].setColorScience(null, gamma);
+ // Optimistic UI update
+ if (!cameras[ci].propertyData['/video/colorScience']) {
+ cameras[ci].propertyData['/video/colorScience'] = {};
+ }
+ cameras[ci].propertyData['/video/colorScience'].gamma = gamma;
+ updateColorScienceUI();
+}
+
+function setGamut(btn) {
+ if (!cameras[ci]) return;
+ const gamut = btn.dataset.gamut;
+ cameras[ci].setColorScience(gamut, null);
+ if (!cameras[ci].propertyData['/video/colorScience']) {
+ cameras[ci].propertyData['/video/colorScience'] = {};
+ }
+ cameras[ci].propertyData['/video/colorScience'].gamut = gamut;
+ updateColorScienceUI();
+}
+
+const COLOR_SCIENCE_PRESETS = {
+ 'braw-film': { gamut: 'Blackmagic Wide Gamut', gamma: 'Blackmagic Design Film' },
+ 'braw-video': { gamut: 'Blackmagic Design', gamma: 'Blackmagic Design Video' },
+ 'braw-ext': { gamut: 'Blackmagic Design', gamma: 'Blackmagic Design Extended Video' },
+ 'rec709': { gamut: 'Rec.709', gamma: 'Rec709' },
+};
+
+function applyColorSciencePreset(presetKey) {
+ if (!cameras[ci]) return;
+ const preset = COLOR_SCIENCE_PRESETS[presetKey];
+ if (!preset) return;
+ cameras[ci].setColorScience(preset.gamut, preset.gamma);
+ if (!cameras[ci].propertyData['/video/colorScience']) {
+ cameras[ci].propertyData['/video/colorScience'] = {};
+ }
+ Object.assign(cameras[ci].propertyData['/video/colorScience'], preset);
+ updateColorScienceUI();
+}
+
+// =====================================================================
+// Video format (new)
+// =====================================================================
+
+function applyVideoFormat() {
+ if (!cameras[ci]) return;
+ const codec = document.getElementById('codecSelect').value;
+ const frameRate = document.getElementById('frameRateSelect').value;
+ const statusEl = document.getElementById('formatSetStatus');
+
+ const data = {};
+ if (codec) data.codec = codec;
+ if (frameRate) data.frameRate = frameRate;
+
+ if (!Object.keys(data).length) {
+ statusEl.textContent = 'Select a codec or frame rate first.';
+ return;
+ }
+
+ cameras[ci].PUTdata('/system/format', data);
+ statusEl.textContent = 'Sent.';
+ setTimeout(() => { statusEl.textContent = ''; }, 2000);
+}
+
+// =====================================================================
+// Exposure controls
+// =====================================================================
+
+function adjustISO(delta) {
+ if (!cameras[ci]) return;
+ const current = cameras[ci].propertyData['/video/iso']?.iso ?? 800;
+ cameras[ci].PUTdata('/video/iso', { iso: current + delta });
+}
+
+function ISOKeyHandler(event) {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ const val = parseInt(document.getElementById('ISODisplay').textContent);
+ if (!isNaN(val)) cameras[ci].PUTdata('/video/iso', { iso: val });
+ unsavedChanges = unsavedChanges.filter(e => e !== 'ISO');
} else {
- //Tint
- document.getElementById("WBLabel").innerHTML = "BALANCE";
- document.getElementById("WBValueContainer").classList.remove("dNone");
- document.getElementById("WBTintValueContainer").classList.add("dNone");
-
- WBMode = 0;
+ unsavedChanges.push('ISO');
}
}
-// Triggered by the button by those text boxes. Reads the info from the inputs and sends it to the camera.
-function manualAPICall() {
- const requestRadioGET = document.getElementById("requestTypeGET");
-
- const requestEndpointText = document.getElementById("manualRequestEndpointLabel").value;
- let requestData = "";
-
- try {
- requestData = JSON.parse(document.getElementById("manualRequestBodyLabel").value);
- } catch (err) {
- document.getElementById("manualRequestResponseP").innerHTML = err;
- }
-
- const requestMethod = (requestRadioGET.checked ? "GET" : "PUT");
- const requestURL = cameras[ci].APIAddress+requestEndpointText;
-
- let response = sendRequest(requestMethod,requestURL,requestData);
-
- document.getElementById("manualRequestResponseP").innerHTML = JSON.stringify(response);
+function ISOBlurHandler() {
+ unsavedChanges = unsavedChanges.filter(e => e !== 'ISO');
}
-/* Control Calling Functions */
-/* Makes the HTML cleaner. */
+function ISOInputHandler() {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ cameras[ci].PUTdata('/video/iso', { iso: parseInt(document.getElementById('ISOInput').value) });
+ unsavedChanges = unsavedChanges.filter(e => e !== 'ISO');
+ } else {
+ unsavedChanges.push('ISO');
+ }
+}
function decreaseND() {
- cameras[ci].PUTdata("/video/ndFilter",{stop: cameras[ci].propertyData['/video/ndFilter'].stop-2});
+ if (!cameras[ci]) return;
+ cameras[ci].PUTdata('/video/ndFilter', { stop: (cameras[ci].propertyData['/video/ndFilter']?.stop ?? 0) - 2 });
}
function increaseND() {
- cameras[ci].PUTdata("/video/ndFilter",{stop: cameras[ci].propertyData['/video/ndFilter'].stop+2});
+ if (!cameras[ci]) return;
+ cameras[ci].PUTdata('/video/ndFilter', { stop: (cameras[ci].propertyData['/video/ndFilter']?.stop ?? 0) + 2 });
+}
+
+function NDFilterInputHandler() {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ cameras[ci].PUTdata('/video/ndFilter', { stop: parseInt(document.getElementById('ndFilterDisplay').textContent) });
+ unsavedChanges = unsavedChanges.filter(e => e !== 'ND');
+ } else {
+ unsavedChanges.push('ND');
+ }
}
function decreaseGain() {
- cameras[ci].PUTdata("/video/gain",{gain: cameras[ci].propertyData['/video/gain'].gain-2});
+ if (!cameras[ci]) return;
+ cameras[ci].PUTdata('/video/gain', { gain: (cameras[ci].propertyData['/video/gain']?.gain ?? 0) - 2 });
}
function increaseGain() {
- cameras[ci].PUTdata("/video/gain",{gain: cameras[ci].propertyData['/video/gain'].gain+2});
+ if (!cameras[ci]) return;
+ cameras[ci].PUTdata('/video/gain', { gain: (cameras[ci].propertyData['/video/gain']?.gain ?? 0) + 2 });
+}
+
+function GainInputHandler() {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ cameras[ci].PUTdata('/video/gain', { gain: parseInt(document.getElementById('gainDisplay').textContent) });
+ unsavedChanges = unsavedChanges.filter(e => e !== 'Gain');
+ } else {
+ unsavedChanges.push('Gain');
+ }
}
function decreaseShutter() {
- let cam = cameras[ci];
-
- if ('shutterSpeed' in cam.propertyData['/video/shutter']) {
- cam.PUTdata("/video/shutter", {"shutterSpeed": cam.propertyData['/video/shutter'].shutterSpeed+10});
+ if (!cameras[ci]) return;
+ const cam = cameras[ci];
+ if ('shutterSpeed' in (cam.propertyData['/video/shutter'] ?? {})) {
+ cam.PUTdata('/video/shutter', { shutterSpeed: cam.propertyData['/video/shutter'].shutterSpeed + 10 });
} else {
- cam.PUTdata("/video/shutter", {"shutterAngle": cam.propertyData['/video/shutter'].shutterAngle-1000});
+ cam.PUTdata('/video/shutter', { shutterAngle: cam.propertyData['/video/shutter'].shutterAngle - 1000 });
}
}
function increaseShutter() {
- let cam = cameras[ci];
-
- if ('shutterSpeed' in cam.propertyData['/video/shutter']) {
- cam.PUTdata("/video/shutter", {"shutterSpeed": cam.propertyData['/video/shutter'].shutterSpeed-10});
+ if (!cameras[ci]) return;
+ const cam = cameras[ci];
+ if ('shutterSpeed' in (cam.propertyData['/video/shutter'] ?? {})) {
+ cam.PUTdata('/video/shutter', { shutterSpeed: cam.propertyData['/video/shutter'].shutterSpeed - 10 });
} else {
- cam.PUTdata("/video/shutter", {"shutterAngle": cam.propertyData['/video/shutter'].shutterAngle+1000});
+ cam.PUTdata('/video/shutter', { shutterAngle: cam.propertyData['/video/shutter'].shutterAngle + 1000 });
}
}
function handleShutterInput() {
- let inputString = document.getElementById("shutterSpan").innerHTML;
-
+ const input = document.getElementById('shutterDisplay').textContent;
if (event.key === 'Enter') {
- let cam = cameras[ci];
-
- if ('shutterSpeed' in cam.propertyData['/video/shutter']) {
- if (inputString.indexOf("1/") >= 0) {
- cam.PUTdata("/video/shutter", {"shutterSpeed" :parseInt(inputString.substring(2))});
- } else {
- cam.PUTdata("/video/shutter", {"shutterSpeed" :parseInt(inputString)});
- }
-
+ event.preventDefault();
+ const cam = cameras[ci];
+ if ('shutterSpeed' in (cam.propertyData['/video/shutter'] ?? {})) {
+ const val = input.includes('1/') ? parseInt(input.substring(2)) : parseInt(input);
+ cam.PUTdata('/video/shutter', { shutterSpeed: val });
} else {
- cam.PUTdata("/video/shutter", {"shutterAngle": parseInt(parseFloat(inputString)*100)});
+ cam.PUTdata('/video/shutter', { shutterAngle: parseInt(parseFloat(input) * 100) });
}
-
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "Shutter"});
+ unsavedChanges = unsavedChanges.filter(e => e !== 'Shutter');
} else {
unsavedChanges.push('Shutter');
}
}
+function swapWBMode() {
+ if (WBMode === 0) {
+ document.getElementById('WBLabel').textContent = 'TINT ↕';
+ document.getElementById('WBValueContainer').classList.add('hidden');
+ document.getElementById('WBTintValueContainer').classList.remove('hidden');
+ WBMode = 1;
+ } else {
+ document.getElementById('WBLabel').textContent = 'WB ↕';
+ document.getElementById('WBValueContainer').classList.remove('hidden');
+ document.getElementById('WBTintValueContainer').classList.add('hidden');
+ WBMode = 0;
+ }
+}
+
function decreaseWhiteBalance() {
- cameras[ci].PUTdata("/video/whiteBalance", {whiteBalance: cameras[ci].propertyData['/video/whiteBalance'].whiteBalance-50});
+ if (!cameras[ci]) return;
+ cameras[ci].PUTdata('/video/whiteBalance', { whiteBalance: (cameras[ci].propertyData['/video/whiteBalance']?.whiteBalance ?? 5600) - 50 });
}
function increaseWhiteBalance() {
- cameras[ci].PUTdata("/video/whiteBalance", {whiteBalance: cameras[ci].propertyData['/video/whiteBalance'].whiteBalance+50});
+ if (!cameras[ci]) return;
+ cameras[ci].PUTdata('/video/whiteBalance', { whiteBalance: (cameras[ci].propertyData['/video/whiteBalance']?.whiteBalance ?? 5600) + 50 });
+}
+
+function WBInputHandler() {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ cameras[ci].PUTdata('/video/whiteBalance', { whiteBalance: parseInt(document.getElementById('whiteBalanceDisplay').textContent) });
+ unsavedChanges = unsavedChanges.filter(e => e !== 'WB');
+ } else {
+ unsavedChanges.push('WB');
+ }
}
function decreaseWhiteBalanceTint() {
- cameras[ci].PUTdata("/video/whiteBalanceTint", {whiteBalanceTint: cameras[ci].propertyData['/video/whiteBalanceTint'].whiteBalanceTint-1});
+ if (!cameras[ci]) return;
+ cameras[ci].PUTdata('/video/whiteBalanceTint', { whiteBalanceTint: (cameras[ci].propertyData['/video/whiteBalanceTint']?.whiteBalanceTint ?? 0) - 1 });
}
function increaseWhiteBalanceTint() {
- cameras[ci].PUTdata("/video/whiteBalanceTint", {whiteBalanceTint: cameras[ci].propertyData['/video/whiteBalanceTint'].whiteBalanceTint+1});
+ if (!cameras[ci]) return;
+ cameras[ci].PUTdata('/video/whiteBalanceTint', { whiteBalanceTint: (cameras[ci].propertyData['/video/whiteBalanceTint']?.whiteBalanceTint ?? 0) + 1 });
}
+function WBTInputHandler() {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ cameras[ci].PUTdata('/video/whiteBalanceTint', { whiteBalanceTint: parseInt(document.getElementById('whiteBalanceTintDisplay').textContent) });
+ unsavedChanges = unsavedChanges.filter(e => e !== 'WBT');
+ } else {
+ unsavedChanges.push('WBT');
+ }
+}
+
+// =====================================================================
+// Color correction
+// =====================================================================
+
+function CCInputHandler(which) {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ setCCFromUI(which);
+ } else {
+ unsavedChanges.push('CC' + which);
+ }
+}
+
+function setCCFromUI(which) {
+ if (which < 4) {
+ const luma = parseFloat(document.getElementsByClassName('CClumaLabel')[which].textContent);
+ const red = parseFloat(document.getElementsByClassName('CCredLabel')[which].textContent);
+ const green = parseFloat(document.getElementsByClassName('CCgreenLabel')[which].textContent);
+ const blue = parseFloat(document.getElementsByClassName('CCblueLabel')[which].textContent);
+ const obj = { red, green, blue, luma };
+
+ const endpoints = ['/colorCorrection/lift', '/colorCorrection/gamma', '/colorCorrection/gain', '/colorCorrection/offset'];
+ cameras[ci].PUTdata(endpoints[which], obj);
+ } else if (which === 4) {
+ const pivot = parseFloat(document.getElementById('CCcontrastPivotLabel').textContent);
+ const adjust = parseInt(document.getElementById('CCcontrastAdjustLabel').textContent) / 50.0;
+ cameras[ci].PUTdata('/colorCorrection/contrast', { pivot, adjust });
+ } else {
+ const hue = (parseInt(document.getElementById('CCcolorHueLabel').textContent) / 180.0) - 1.0;
+ const sat = parseInt(document.getElementById('CCcolorSatLabel').textContent) / 50.0;
+ const lc = parseInt(document.getElementById('CCcolorLCLabel').textContent) / 100.0;
+ cameras[ci].PUTdata('/colorCorrection/color', { hue, saturation: sat });
+ cameras[ci].PUTdata('/colorCorrection/lumaContribution', { lumaContribution: lc });
+ }
+ unsavedChanges = unsavedChanges.filter(e => !e.includes('CC' + which));
+}
+
+function resetCC(which) {
+ const resets = [
+ ['/colorCorrection/lift', { red: 0.0, green: 0.0, blue: 0.0, luma: 0.0 }],
+ ['/colorCorrection/gamma', { red: 0.0, green: 0.0, blue: 0.0, luma: 0.0 }],
+ ['/colorCorrection/gain', { red: 1.0, green: 1.0, blue: 1.0, luma: 1.0 }],
+ ['/colorCorrection/offset', { red: 0.0, green: 0.0, blue: 0.0, luma: 0.0 }],
+ ['/colorCorrection/contrast', { pivot: 0.5, adjust: 1.0 }],
+ ];
+ if (which < 5) {
+ cameras[ci].PUTdata(resets[which][0], resets[which][1]);
+ } else {
+ cameras[ci].PUTdata('/colorCorrection/color', { hue: 0.0, saturation: 1.0 });
+ cameras[ci].PUTdata('/colorCorrection/lumaContribution', { lumaContribution: 1.0 });
+ }
+ unsavedChanges = unsavedChanges.filter(e => !e.includes('CC' + which));
+}
+
+// =====================================================================
+// Presets / AE / Manual API
+// =====================================================================
+
function presetInputHandler() {
- let selectedPreset = document.getElementById("presetsDropDown").value;
+ cameras[ci].PUTdata('/presets/active', { preset: document.getElementById('presetsDropDown').value + '.cset' });
+ unsavedChanges = unsavedChanges.filter(e => e !== 'presets');
+}
- cameras[ci].PUTdata("/presets/active", {preset: selectedPreset+".cset"});
+function AEmodeInputHandler() {
+ cameras[ci].PUTdata('/video/autoExposure', {
+ mode: document.getElementById('AEmodeDropDown').value,
+ type: document.getElementById('AEtypeDropDown').value,
+ });
+ unsavedChanges = unsavedChanges.filter(e => e !== 'AutoExposure');
+}
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "presets"});
+function manualAPICall() {
+ const isGET = document.getElementById('requestTypeGET').checked;
+ const endpoint = document.getElementById('manualRequestEndpointLabel').value;
+ let body = '';
+ try { body = JSON.parse(document.getElementById('manualRequestBodyLabel').value); } catch (_) {}
+ const response = sendRequest(isGET ? 'GET' : 'PUT', cameras[ci].APIAddress + endpoint, body);
+ document.getElementById('manualRequestResponseP').textContent = JSON.stringify(response, null, 2);
}
function hostnameInputHandler() {
- let newHostname = document.getElementById("hostnameInput").value;
-
if (event.key === 'Enter') {
- event.preventDefault;
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "Hostname"});
+ event.preventDefault();
+ unsavedChanges = unsavedChanges.filter(e => e !== 'Hostname');
initCamera();
} else {
unsavedChanges.push('Hostname');
}
}
-function AEmodeInputHandler() {
- let AEmode = document.getElementById("AEmodeDropDown").value;
- let AEtype = document.getElementById("AEtypeDropDown").value;
-
- cameras[ci].PUTdata("/video/autoExposure", {mode: AEmode, type: AEtype});
-
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "AutoExposure"});
-}
-
-function ISOInputHandler() {
- let ISOInput = document.getElementById("ISOInput");
-
- if (event.key === 'Enter') {
- event.preventDefault;
- cameras[ci].PUTdata("/video/iso", {iso: parseInt(ISOInput.value)})
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "ISO"});
- } else {
- unsavedChanges.push('ISO');
- }
-}
-
-// 0: lift, 1: gamma, 2: gain, 3: offset, 4: contrast, 5: color & LC
-function CCInputHandler(which) {
- if (event.key === 'Enter') {
- event.preventDefault;
- setCCFromUI(which);
- } else {
- unsavedChanges.push('CC'+which);
- }
-}
-
-function NDFilterInputHandler() {
- if (event.key === 'Enter') {
- event.preventDefault;
- cameras[ci].PUTdata("/video/ndFilter", {stop: parseInt(document.getElementById("ndFilterSpan").innerHTML)})
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "ND"});
- } else {
- unsavedChanges.push('ND');
- }
-}
-
-function GainInputHandler() {
- if (event.key === 'Enter') {
- event.preventDefault;
- cameras[ci].PUTdata("/video/gain", {gain: parseInt(document.getElementById("gainSpan").innerHTML)})
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "Gain"});
- } else {
- unsavedChanges.push('Gain');
- }
-}
-
-function WBInputHandler() {
- if (event.key === 'Enter') {
- event.preventDefault;
- cameras[ci].PUTdata("/video/whiteBalance", {whiteBalance: parseInt(document.getElementById("whiteBalanceSpan").innerHTML)})
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "WB"});
- } else {
- unsavedChanges.push('WB');
- }
-}
-
-function WBTInputHandler() {
- if (event.key === 'Enter') {
- event.preventDefault;
- cameras[ci].PUTdata("/video/whiteBalanceTint", {whiteBalanceTint: parseInt(document.getElementById("whiteBalanceTintSpan").innerHTML)})
- unsavedChanges = unsavedChanges.filter((e) => {return e !== "WBT"});
- } else {
- unsavedChanges.push('WBT');
- }
-}
-
-// 0: lift, 1: gamma, 2: gain, 3: offset
-function setCCFromUI(which) {
- if (which < 4) {
- var lumaFloat = parseFloat(document.getElementsByClassName("CClumaLabel")[which].innerHTML);
- var redFloat = parseFloat(document.getElementsByClassName("CCredLabel")[which].innerHTML);
- var greenFloat = parseFloat(document.getElementsByClassName("CCgreenLabel")[which].innerHTML);
- var blueFloat = parseFloat(document.getElementsByClassName("CCblueLabel")[which].innerHTML);
-
- var ccobject = {"red": redFloat, "green": greenFloat, "blue": blueFloat, "luma": lumaFloat};
- }
-
- if (which == 0) {
- cameras[ci].PUTdata("/colorCorrection/lift", ccobject);
- } else if (which == 1) {
- cameras[ci].PUTdata("/colorCorrection/gamma", ccobject);
- } else if (which == 2) {
- cameras[ci].PUTdata("/colorCorrection/gain", ccobject);
- } else if (which == 3) {
- cameras[ci].PUTdata("/colorCorrection/offset", ccobject);
- } else if (which == 4) {
- let pivotFloat = parseFloat(document.getElementById("CCcontrastPivotLabel").innerHTML);
- let adjustInt = parseInt(document.getElementById("CCcontrastAdjustLabel").innerHTML);
-
- let adjustFloat = adjustInt/50.0;
-
- cameras[ci].PUTdata("/colorCorrection/contrast", {pivot: pivotFloat, adjust: adjustFloat});
- } else {
- let hueInt = parseInt(document.getElementById("CCcolorHueLabel").innerHTML);
- let satInt = parseInt(document.getElementById("CCcolorSatLabel").innerHTML);
- let lumCoInt = parseInt(document.getElementById("CCcolorLCLabel").innerHTML);
-
- let hueFloat = (hueInt/180.0) - 1.0;
- let satFloat = satInt/50.0;
- let lumCoFloat = lumCoInt/100.0;
-
- cameras[ci].PUTdata("/colorCorrection/color", {hue: hueFloat, saturation: satFloat});
- cameras[ci].PUTdata("/colorCorrection/lumaContribution", {lumaContribution: lumCoFloat});
- }
-
- unsavedChanges = unsavedChanges.filter((e) => {return !e.includes("CC"+which)});
-}
-
-// Reset Color Correction Values
-// 0: lift, 1: gamma, 2: gain, 3: offset, 4: contrast, 5: color & LC
-function resetCC(which) {
- if (which == 0) {
- cameras[ci].PUTdata("/colorCorrection/lift", {"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0});
- } else if (which == 1) {
- cameras[ci].PUTdata("/colorCorrection/gamma", {"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0});
- } else if (which == 2) {
- cameras[ci].PUTdata("/colorCorrection/gain", {"red": 1.0, "green": 1.0, "blue": 1.0, "luma": 1.0});
- } else if (which == 3) {
- cameras[ci].PUTdata("/colorCorrection/offset", {"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0});
- } else if (which == 4) {
- cameras[ci].PUTdata("/colorCorrection/contrast", {"pivot": 0.5, "adjust": 1.0});
- } else if (which == 5) {
- cameras[ci].PUTdata("/colorCorrection/color", {"hue": 0.0, "saturation": 1.0});
- cameras[ci].PUTdata("/colorCorrection/lumaContribution", {"lumaContribution": 1.0});
- }
-
- unsavedChanges = unsavedChanges.filter((e) => {return !e.includes("CC"+which)});
-}
-
-// Triggered by the Loop and Single Clip buttons
function loopHandler(callerString) {
- let playbackState = cameras[ci].propertyData['/transports/0/playback'];
-
- if (callerString === "Loop") {
+ const playbackState = cameras[ci].propertyData['/transports/0/playback'];
+ if (callerString === 'Loop') {
playbackState.loop = !playbackState.loop;
- } else if (callerString === "Single Clip") {
+ } else if (callerString === 'Single Clip') {
playbackState.singleClip = !playbackState.singleClip;
}
-
- cameras[ci].PUTdata("/transports/0/playback", playbackState);
+ cameras[ci].PUTdata('/transports/0/playback', playbackState);
}
-/* Helper Functions */
+// =====================================================================
+// Helpers
+// =====================================================================
+
function parseTimecode(timecodeBCD) {
- let noDropFrame = timecodeBCD & 0b01111111111111111111111111111111; // The first bit of the timecode is 1 if "Drop Frame Timecode" is on. We don't want to include that in the display.
- let decimalTCInt = parseInt(noDropFrame.toString(16), 10); // Convert the BCD number into base ten
- let decimalTCString = decimalTCInt.toString().padStart(8, '0'); // Convert the base ten number to a string eight characters long
- let finalTCString = decimalTCString.match(/.{1,2}/g).join(':'); // Put colons between every two characters
- return finalTCString;
+ if (timecodeBCD == null) return '--:--:--:--';
+ const noDF = timecodeBCD & 0x7fffffff;
+ const str = parseInt(noDF.toString(16), 10).toString().padStart(8, '0');
+ return str.match(/.{1,2}/g).join(':');
}
- (v 1.4.2)
-
+
+
-
-
-
\ No newline at end of file
+
+
+
+ Gamma
+
+
+
+
+
+
+
+
+ L
+ 0.00
+
+
+ R
+ 0.00
+
+
+ G
+ 0.00
+
+
+ B
+ 0.00
+
+
+ Gain
+
+
+
+
+
+
+
+
+ L
+ 1.00
+
+
+ R
+ 1.00
+
+
+ G
+ 1.00
+
+
+ B
+ 1.00
+
+
+ Offset
+
+
+
+
+
+ L
+ 0.00
+
+
+ R
+ 0.00
+
+
+ G
+ 0.00
+
+
+ B
+ 0.00
+
+ Color
+
+
+
+
+
+
+
+
+
+ 180°
+
+
+
+
+ 50%
+
+
+
+
+ 100%
+
+
+
+
+
+
+ FOCUS
+
+
+
+
+
+ IRIS
+
+ f/—
+
+
+
+ ZOOM
+
+ —mm
+
+
+
+
+
+
+
+
+ Connection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Presets
+
+
+
+
+
+
+