Skip to content

Commit e5c1aed

Browse files
feat: aspect ratio control
1 parent 8d6637a commit e5c1aed

File tree

4 files changed

+131
-14
lines changed

4 files changed

+131
-14
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gdq-viewport-assign",
3-
"version": "0.10.0",
3+
"version": "0.11.0",
44
"description": "",
55
"main": "dist/index.js",
66
"scripts": {

src/index.html

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@
5454
display: flex;
5555
flex-direction: column;
5656
}
57+
#aspect-ratio {
58+
position: absolute;
59+
width: 100%;
60+
height: 100%;
61+
padding-top: 10%;
62+
display: flex;
63+
flex-direction: column;
64+
background-color: #2b2e38;
65+
}
66+
.ar-legend {
67+
white-space: nowrap;
68+
writing-mode: vertical-lr;
69+
position: absolute;
70+
background-color: #3c404b;
71+
border-radius: 4px;
72+
padding: 5px 0px;
73+
font-size: .8rem;
74+
font-weight: bold;
75+
}
5776
#warning {
5877
position: absolute;
5978
width: 100%;
@@ -380,6 +399,15 @@
380399
</div>
381400
</div>
382401
</div>
402+
<div id="aspect-ratio" class="hide">
403+
<label for="ar" style="text-align: center;">Aspect Ratio Adjustment:</label>
404+
<input type="range" id="ar" name="ar" min=".5" max="1.5" step="any"/>
405+
<div style="width: 100%; line-height: 1rem;">
406+
<div class="ar-legend" id="thinify" style="left: calc((100% - 1rem - 2px) * .25);">16:9 to 4:3</div>
407+
<div class="ar-legend" id="revert-ar" style="left: calc((100% - 1rem - 2px) * .5);">No Change</div>
408+
<div class="ar-legend" id="wideify" style="left: calc((100% - 1rem - 2px) * .8333);">4:3 to 16:9</div>
409+
</div>
410+
</div>
383411
<div id="warning">
384412
<div id="warning-msg">Loading...</div>
385413
</div>

src/main.ts

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ let screenshotBase64 = '';
99
let connectedToOBS = false;
1010
let obsConnectionError = '';
1111
let obsUpdateTimeout: NodeJS.Timeout | null = null;
12-
let cropItem: null | (SceneItemRef & Crop & { width: number; height: number }) =
13-
null;
12+
let cropItem:
13+
| null
14+
| (SceneItemRef &
15+
Crop & {
16+
width: number;
17+
height: number;
18+
scaleX: number;
19+
}) = null;
1420
let cropSide: 'left' | 'right' | 'top' | 'bottom' | null = null;
1521
let targetCrop: Crop | null = null;
1622
let initialCrop: Crop | null = null;
@@ -19,6 +25,8 @@ let cropWidthSide: 'left' | 'right' | null = null;
1925
let cropHeightSide: 'top' | 'bottom' | null = null;
2026
let clickLoc = { clientX: 0, clientY: 0 };
2127
let activelyCropping = false;
28+
let activeArAdjust = false;
29+
let targetAr: number | null = null;
2230
let currentSceneViewports: Viewport[] = [];
2331
let unassignedFeeds: Viewport['assignedFeeds'] = [];
2432
let currentSceneFeedScenes: number[] = [];
@@ -37,6 +45,7 @@ if (localStorage.getItem('obsPassword'))
3745
obsPassword = localStorage.getItem('obsPassword')!;
3846

3947
//setup cropping controls
48+
const arControl = document.getElementById('ar') as HTMLInputElement;
4049
const cropDiv = document.getElementById('primary-crop') as HTMLDivElement;
4150
const cropFrame = document.getElementById('crop-frame') as HTMLDivElement;
4251
const cropImg = document.getElementById('crop-image') as HTMLImageElement;
@@ -343,9 +352,71 @@ function cropItemToTarget(check?: 'check') {
343352
cropItemToTarget('check');
344353
}, 30);
345354
})
346-
.catch(obsError);
355+
.catch((err) => {
356+
activelyCropping = false;
357+
obsError(err);
358+
});
347359
} else activelyCropping = false;
348360
}
361+
document.getElementById('thinify')!.onclick = () => {
362+
arControl.value='0.75';
363+
targetAr = 0.75;
364+
adjustAr();
365+
};
366+
document.getElementById('revert-ar')!.onclick = () => {
367+
arControl.value='1';
368+
targetAr = 1;
369+
adjustAr();
370+
};
371+
document.getElementById('wideify')!.onclick = () => {
372+
arControl.value=(4/3).toString();
373+
targetAr = 4/3;
374+
adjustAr();
375+
};
376+
arControl.oninput = () => {
377+
const val = parseFloat(arControl.value);
378+
if (!isNaN(val)) {
379+
targetAr = parseFloat(arControl.value);
380+
adjustAr();
381+
}
382+
};
383+
function adjustAr(check?: 'check') {
384+
if (!cropItem || targetAr == null) {
385+
obsError("Can't adjust aspect ratio without cropitem and targetAr");
386+
activeArAdjust = false;
387+
targetAr = null;
388+
return;
389+
}
390+
if (activeArAdjust && !check) return;
391+
activeArAdjust = true;
392+
if (Math.round(targetAr * 300) != Math.round(cropItem.scaleX * 300)) {
393+
const newTransform: Partial<ObsSceneItemTransform> = {
394+
scaleX: targetAr,
395+
scaleY: 1,
396+
};
397+
obs
398+
.call('SetSceneItemTransform', {
399+
sceneName: cropItem.sceneName,
400+
sceneItemId: cropItem.sceneItemId,
401+
sceneItemTransform: newTransform,
402+
})
403+
.then(() => {
404+
if (cropItem && targetAr !== null) {
405+
cropItem.scaleX = targetAr;
406+
}
407+
setTimeout(() => {
408+
adjustAr('check');
409+
}, 30);
410+
})
411+
.catch((err) => {
412+
activeArAdjust = false;
413+
obsError(err);
414+
});
415+
} else {
416+
targetAr = null;
417+
activeArAdjust = false;
418+
}
419+
}
349420
document.getElementById('camera-crop')!.onclick = () => {
350421
cropViewportFeed('camera');
351422
};
@@ -450,8 +521,7 @@ function initOBS() {
450521
obsError("Can't init, not connected");
451522
return;
452523
}
453-
if (cropItem) return;
454-
if (inInit) {
524+
if (inInit || cropItem) {
455525
initBuffered = true;
456526
return;
457527
}
@@ -490,16 +560,16 @@ function initOBS() {
490560
.catch(obsError)
491561
.then(() => {
492562
inInit = false;
493-
if (initBuffered) {
494-
initBuffered = false;
495-
if (obsUpdateTimeout) {
563+
if (initBuffered) {
564+
initBuffered = false;
565+
if (obsUpdateTimeout) {
496566
clearTimeout(obsUpdateTimeout);
497567
}
498568
obsUpdateTimeout = setTimeout(() => {
499569
initOBS();
500570
obsUpdateTimeout = null;
501571
}, 200);
502-
}
572+
}
503573
});
504574
}
505575

@@ -581,6 +651,7 @@ function populateViewportsFromActiveFeed() {
581651
right: sceneItemList[i].sceneItemTransform.cropRight,
582652
top: sceneItemList[i].sceneItemTransform.cropTop,
583653
bottom: sceneItemList[i].sceneItemTransform.cropBottom,
654+
scaleX: sceneItemList[i].sceneItemTransform.scaleX,
584655
width: sceneItemList[i].sceneItemTransform.sourceWidth,
585656
height: sceneItemList[i].sceneItemTransform.sourceHeight,
586657
x: sceneItemList[i].sceneItemTransform.positionX,
@@ -679,6 +750,7 @@ function populateViewportsFromActiveFeed() {
679750
async function refreshViewportsDiv() {
680751
document.getElementById('viewports')!.classList.remove('hide');
681752
document.getElementById('crop')!.classList.add('hide');
753+
document.getElementById('aspect-ratio')!.classList.add('hide');
682754
cropImg.src = '';
683755
const listDiv = document.getElementById('viewports-list')!;
684756
listDiv.innerHTML = '';
@@ -1169,6 +1241,22 @@ function refreshFooter() {
11691241
if (cropItem) {
11701242
let button = document.createElement('div');
11711243
button.classList.add('icon', 'footer');
1244+
button.style.width = 'auto';
1245+
button.style.padding = '0px 5px';
1246+
button.innerHTML = 'Aspect Ratio';
1247+
button.onclick = () => {
1248+
cropSide = null;
1249+
if (
1250+
document.getElementById('aspect-ratio')!.classList.contains('hide') &&
1251+
cropItem
1252+
) {
1253+
document.getElementById('aspect-ratio')!.classList.remove('hide');
1254+
arControl.value = cropItem.scaleX.toString();
1255+
} else document.getElementById('aspect-ratio')!.classList.add('hide');
1256+
};
1257+
footer.appendChild(button);
1258+
button = document.createElement('div');
1259+
button.classList.add('icon', 'footer');
11721260
let icon = document.createElement('img');
11731261
icon.width = 16;
11741262
icon.classList.add('icon', 'footer');
@@ -1188,6 +1276,8 @@ function refreshFooter() {
11881276
cropRight: 0,
11891277
cropTop: 0,
11901278
cropBottom: 0,
1279+
scaleX: 1,
1280+
scaleY: 1,
11911281
},
11921282
})
11931283
.then(() => {
@@ -1196,6 +1286,7 @@ function refreshFooter() {
11961286
cropItem.right = 0;
11971287
cropItem.top = 0;
11981288
cropItem.bottom = 0;
1289+
arControl.value = '1';
11991290
} else obsError('No cropItem');
12001291
refreshCropImage();
12011292
})
@@ -1311,6 +1402,7 @@ async function addSourceToViewport(source: ObsSceneItem, viewport: Viewport) {
13111402
right: 0,
13121403
top: 0,
13131404
bottom: 0,
1405+
scaleX: source.sceneItemTransform.scaleX,
13141406
width: source.sceneItemTransform.sourceWidth,
13151407
height: source.sceneItemTransform.sourceHeight,
13161408
x: viewport.x,
@@ -1386,7 +1478,6 @@ function cropViewportFeed(cropType: 'camera' | 'game1' | 'game2') {
13861478
const game1 = { left: -1, right: -1, top: -1, bottom: -1 };
13871479
const game2 = { left: -1, right: -1, top: -1, bottom: -1 };
13881480
temp = findGreenBlock(ctx, 'y', y1, y2, x1, true);
1389-
console.log(temp);
13901481
if (temp[1] < y2) game2.top = temp[1];
13911482
camera.bottom = temp[0];
13921483
temp = findGreenBlock(ctx, 'y', y1, 0, x1, true);
@@ -1408,9 +1499,6 @@ function cropViewportFeed(cropType: 'camera' | 'game1' | 'game2') {
14081499
game2.left = temp[1] == -1 ? 0 : temp[1];
14091500
temp = findGreenBlock(ctx, 'y', y2, height - 1, x1, true);
14101501
game2.bottom = temp[0] == -1 ? height - 1 : temp[0];
1411-
console.log(camera);
1412-
console.log(game1);
1413-
console.log(game2);
14141502
switch (cropType) {
14151503
case 'game1':
14161504
newItemRec = game1;

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Viewport = {
3737
height: number;
3838
x: number;
3939
y: number;
40+
scaleX: number;
4041
boundsWidth: number;
4142
boundsHeight: number;
4243
})[];

0 commit comments

Comments
 (0)