From fc7b961010eba88c4d729be3b36a75d5f307cf32 Mon Sep 17 00:00:00 2001 From: csantosm <4a.santos@gmail.com> Date: Fri, 1 Jul 2022 12:50:59 +0200 Subject: [PATCH] openvidu-components: Updated layout --- .../src/lib/models/layout.model.ts | 1191 ++++++++++++----- 1 file changed, 872 insertions(+), 319 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-angular/src/lib/models/layout.model.ts b/openvidu-components-angular/projects/openvidu-angular/src/lib/models/layout.model.ts index b2fb3b1e..8fad4222 100644 --- a/openvidu-components-angular/projects/openvidu-angular/src/lib/models/layout.model.ts +++ b/openvidu-components-angular/projects/openvidu-angular/src/lib/models/layout.model.ts @@ -5,8 +5,10 @@ export enum LayoutClass { ROOT_ELEMENT = 'OT_root', BIG_ELEMENT = 'OV_big', SMALL_ELEMENT = 'OV_small', + IGNORED_ELEMENT = 'OV_ignored', SIDENAV_CONTAINER = 'sidenav-container', - NO_SIZE_ELEMENT = 'no-size' + NO_SIZE_ELEMENT = 'no-size', + CLASS_NAME = 'layout' } /** @@ -14,7 +16,13 @@ export enum LayoutClass { */ export enum SidenavMode { OVER = 'over', - SIDE = 'side' + SIDE = 'side' +} + +export enum LayoutAlignment { + START = 'start', + CENTER = 'center', + END = 'end' } /** @@ -49,11 +57,21 @@ export interface OpenViduLayoutOptions { */ smallClass: string; + /** + * The class to add to elements that should be ignored + */ + ignoredClass: string; + /** * The maximum percentage of space the big ones should take up */ bigPercentage: any; + /** + * If this is set then it will scale down the big space if there is left over whitespace down to this minimum size + */ + minBigPercentage: number; + /** * FixedRatio for the big ones */ @@ -72,7 +90,41 @@ export interface OpenViduLayoutOptions { /** * Whether to place the big one in the top left `true` or bottom right */ - bigFirst: any; + bigFirst: boolean | 'column' | 'row'; + + /** + * + */ + alignItems: LayoutAlignment; + /** + * + */ + bigAlignItems: LayoutAlignment; + /** + * + */ + smallAlignItems: LayoutAlignment; + /** + * The maximum width of the elements + */ + maxWidth: number; + /** + * The maximum height of the elements + */ + maxHeight: number; + smallMaxWidth: number; + smallMaxHeight: number; + bigMaxWidth: number; + bigMaxHeight: number; + + /** + * If there are less elements on the last row then we can scale them up to take up more space + */ + scaleLastRow?: boolean; + /** + * Scale last row for the big elements + */ + bigScaleLastRow?: boolean; } /** @@ -91,10 +143,14 @@ export class OpenViduLayout { /** * Update the layout container + * module export layout */ - updateLayout() { + updateLayout(container: HTMLElement, opts: any) { setTimeout(() => { - if (this.layoutContainer?.style?.display === 'none') { + this.layoutContainer = container; + this.opts = opts; + + if (this.css(this.layoutContainer, 'display') === 'none') { return; } let id = this.layoutContainer.id; @@ -102,19 +158,54 @@ export class OpenViduLayout { id = 'OT_' + this.cheapUUID(); this.layoutContainer.id = id; } - const smallOnes: HTMLVideoElement[] = Array.prototype.filter.call( - this.layoutContainer.querySelectorAll('#' + id + '>.' + this.opts.smallClass), - this.filterDisplayNone + + opts.containerHeight = + this.getHeight(this.layoutContainer) - + this.getCSSNumber(this.layoutContainer, 'border-top') - + this.getCSSNumber(this.layoutContainer, 'border-bottom'); + opts.containerWidth = + this.getWidth(this.layoutContainer) - + this.getCSSNumber(this.layoutContainer, 'border-left') - + this.getCSSNumber(this.layoutContainer, 'border-right'); + + const children = Array.prototype.filter.call( + this.layoutContainer.querySelectorAll(`#${id}>*:not(.${LayoutClass.IGNORED_ELEMENT})`), + () => this.filterDisplayNone ); - const bigOnes: HTMLVideoElement[] = Array.prototype.filter - .call(this.layoutContainer.querySelectorAll('#' + id + '>.' + this.opts.bigClass), this.filterDisplayNone) - .filter((x) => !smallOnes.includes(x)); + const elements = children.map((element) => { + const res = this.getChildDims(element); + res.big = element.classList.contains(this.opts.bigClass); + return res; + }); - const normalOnes: HTMLVideoElement[] = Array.prototype.filter - .call(this.layoutContainer.querySelectorAll('#' + id + '>*:not(.' + this.opts.bigClass + ')'), this.filterDisplayNone) - .filter((x) => !smallOnes.includes(x)); + const layout = this.getLayout(opts, elements); + layout.boxes.forEach((box, idx) => { + const elem = children[idx]; + this.css(elem, 'position', 'absolute'); + const actualWidth = + box.width - + -this.getCSSNumber(elem, 'margin-left') - + this.getCSSNumber(elem, 'margin-right') - + (this.css(elem, 'box-sizing') !== 'border-box' + ? this.getCSSNumber(elem, 'padding-left') + + this.getCSSNumber(elem, 'padding-right') + + this.getCSSNumber(elem, 'border-left') + + this.getCSSNumber(elem, 'border-right') + : 0); - this.attachElements(bigOnes, normalOnes, smallOnes); + const actualHeight = + box.height - + -this.getCSSNumber(elem, 'margin-top') - + this.getCSSNumber(elem, 'margin-bottom') - + (this.css(elem, 'box-sizing') !== 'border-box' + ? this.getCSSNumber(elem, 'padding-top') + + this.getCSSNumber(elem, 'padding-bottom') + + this.getCSSNumber(elem, 'border-top') + + this.getCSSNumber(elem, 'border-bottom') + : 0); + + this.positionElement(elem, box.left, box.top, actualWidth, actualHeight, this.opts.animate); + }); }, 50); } @@ -124,45 +215,95 @@ export class OpenViduLayout { * @param opts */ initLayoutContainer(container: HTMLElement, opts: OpenViduLayoutOptions) { - this.opts = { - maxRatio: opts.maxRatio != null ? opts.maxRatio : 3 / 2, - minRatio: opts.minRatio != null ? opts.minRatio : 9 / 16, - fixedRatio: opts.fixedRatio != null ? opts.fixedRatio : false, - animate: opts.animate != null ? opts.animate : false, - bigClass: opts.bigClass != null ? opts.bigClass : 'OT_big', - smallClass: opts.smallClass != null ? opts.smallClass : 'OT_small', - bigPercentage: opts.bigPercentage != null ? opts.bigPercentage : 0.8, - bigFixedRatio: opts.bigFixedRatio != null ? opts.bigFixedRatio : false, - bigMaxRatio: opts.bigMaxRatio != null ? opts.bigMaxRatio : 3 / 2, - bigMinRatio: opts.bigMinRatio != null ? opts.bigMinRatio : 9 / 16, - bigFirst: opts.bigFirst != null ? opts.bigFirst : true - }; + // this.opts = this.defaults(opts, { + // maxRatio: 3 / 2, + // minRatio: 9 / 16, + // fixedRatio: false, + // animate: false, + // bigClass: LayoutClass.BIG_ELEMENT, + // smallClass: LayoutClass.SMALL_ELEMENT, + // bigPercentage: 0.8, + // bigFixedRatio: false, + // bigMaxRatio: 3 / 2, + // bigMinRatio: 9 / 16, + // bigFirst: true, + // alignItems: 'center', + // bigAlignItems: 'center', + // smallAlignItems: 'center' + // }); + this.opts = opts; this.layoutContainer = container; } - /** - * Set the layout configuration - * @param options - */ - setLayoutOptions(options: OpenViduLayoutOptions) { - this.opts = options; - } - getLayoutContainer(): HTMLElement { return this.layoutContainer; } + /** + * Set the layout configuration + * @param options + */ + private setLayoutOptions(options: OpenViduLayoutOptions) { + this.opts = options; + } + + private css(el: HTMLVideoElement | HTMLElement, propertyName: any, value?: string) { + if (!!value) { + // We are setting one css property + el.style[propertyName] = value; + return NaN; + } else if (typeof propertyName === 'object') { + // We are setting several CSS properties at once + Object.keys(propertyName).forEach((key) => { + this.css(el, key, propertyName[key]); + }); + return NaN; + } else { + // We are getting the css property + var computedStyle = /*(this.opts && this.opts.window) ||*/ window.getComputedStyle(el); + var currentValue = computedStyle.getPropertyValue(propertyName); + + if (currentValue === '') { + currentValue = el.style[propertyName]; + } + + return currentValue; + } + } + + private height(el) { + if (el.offsetHeight > 0) { + return `${el.offsetHeight}px`; + } + return this.css(el, 'height'); + } + private width(el) { + if (el.offsetWidth > 0) { + return `${el.offsetWidth}px`; + } + return this.css(el, 'width'); + } + private defaults(custom: OpenViduLayoutOptions, defaults: OpenViduLayoutOptions): OpenViduLayoutOptions { + var res = defaults; + Object.keys(defaults).forEach((key) => { + if (custom.hasOwnProperty(key)) { + res[key] = custom[key]; + } + }); + return res; + } + /** * @hidden */ private fixAspectRatio(elem: HTMLVideoElement, width: number) { - const sub: HTMLVideoElement = elem.querySelector('.OT_root'); + const sub: HTMLVideoElement = elem.querySelector(`.${LayoutClass.ROOT_ELEMENT}`); if (sub) { // If this is the parent of a subscriber or publisher then we need // to force the mutation observer on the publisher or subscriber to // trigger to get it to fix it's layout const oldWidth = sub.style.width; - sub.style.width = width + 'px'; + sub.style.width = `${width}px`; // sub.style.height = height + 'px'; sub.style.width = oldWidth || ''; } @@ -173,47 +314,68 @@ export class OpenViduLayout { */ private positionElement(elem: HTMLVideoElement, x: number, y: number, width: number, height: number, animate: any) { const targetPosition = { - left: x + 'px', - top: y + 'px', - width: width + 'px', - height: height + 'px' + left: `${x}px`, + top: `${y}px`, + width: `${width}px`, + height: `${height}px` }; this.fixAspectRatio(elem, width); - setTimeout(() => { - // animation added in css transition: all .1s linear; - elem.style.left = targetPosition.left; - elem.style.top = targetPosition.top; - elem.style.width = targetPosition.width; - elem.style.height = targetPosition.height; - this.fixAspectRatio(elem, width); - }, 10); + if (animate) { + setTimeout(() => { + // animation added in css transition: all .1s linear; + elem.style.left = targetPosition.left; + elem.style.top = targetPosition.top; + elem.style.width = targetPosition.width; + elem.style.height = targetPosition.height; + this.fixAspectRatio(elem, width); + }, 10); + } else { + this.css(elem, targetPosition); + if (!elem.classList.contains(LayoutClass.CLASS_NAME)) { + elem.classList.add(LayoutClass.CLASS_NAME); + } + } + this.fixAspectRatio(elem, width); } /** * @hidden */ - private getVideoRatio(elem: HTMLVideoElement) { - if (!elem) { - return 3 / 4; + private getChildDims(child: HTMLVideoElement): { + height: number; + width: number; + big?: boolean; + } { + if (child) { + if (child.videoHeight && child.videoWidth) { + return { + height: child.videoHeight, + width: child.videoWidth + }; + } + const video: HTMLVideoElement = child.querySelector('video'); + if (video && video.videoHeight && video.videoWidth) { + return { + height: video.videoHeight, + width: video.videoWidth + }; + } } - const video: HTMLVideoElement = elem.querySelector('video'); - if (video && video.videoHeight && video.videoWidth) { - return video.videoHeight / video.videoWidth; - } else if (elem.videoHeight && elem.videoWidth) { - return elem.videoHeight / elem.videoWidth; - } - return 3 / 4; + return { + height: 480, + width: 640 + }; } /** * @hidden */ private getCSSNumber(elem: HTMLElement, prop: string) { - const cssStr = window.getComputedStyle(elem)[prop]; + const cssStr = this.css(elem, prop); - return cssStr ? parseInt(cssStr, 10) : 0; + return cssStr ? parseInt(cssStr.toString(), 10) : 0; } /** @@ -228,118 +390,666 @@ export class OpenViduLayout { * @hidden */ private getHeight(elem: HTMLElement) { - const heightStr = window.getComputedStyle(elem)['height']; - return heightStr ? parseInt(heightStr, 10) : 0; + const heightStr = this.height(elem); + return heightStr ? parseInt(heightStr.toString(), 10) : 0; } /** * @hidden */ private getWidth(elem: HTMLElement) { - const widthStr = window.getComputedStyle(elem)['width']; - return widthStr ? parseInt(widthStr, 10) : 0; + const widthStr = this.width(elem); + return widthStr ? parseInt(widthStr.toString(), 10) : 0; } /** * @hidden */ - private getBestDimensions(minR: number, maxR: number, count: number, WIDTH: number, HEIGHT: number, targetHeight: number) { - let maxArea, targetCols, targetRows, targetWidth, tWidth, tHeight, tRatio; + // private arrange( + // children: HTMLVideoElement[], + // containerWidth: number, + // containerHeight: number, + // offsetLeft: number, + // offsetTop: number, + // fixedRatio: boolean, + // minRatio: number, + // maxRatio: number, + // animate: any + // ) { + // const boxes = this.getLayout( + // { + // containerWidth, + // containerHeight, + // minRatio, + // maxRatio, + // fixedRatio, + // }, + // children.map((child) => this.getVideoRatio(child)) + // ); + + // boxes.forEach((box, idx) => { + // const elem = children[idx]; + // this.css(elem, 'position', 'absolute'); + // const actualWidth = + // box.width - + // this.getCSSNumber(elem, 'paddingLeft') - + // this.getCSSNumber(elem, 'paddingRight') - + // this.getCSSNumber(elem, 'marginLeft') - + // this.getCSSNumber(elem, 'marginRight') - + // this.getCSSNumber(elem, 'borderLeft') - + // this.getCSSNumber(elem, 'borderRight'); + + // const actualHeight = + // box.height - + // this.getCSSNumber(elem, 'paddingTop') - + // this.getCSSNumber(elem, 'paddingBottom') - + // this.getCSSNumber(elem, 'marginTop') - + // this.getCSSNumber(elem, 'marginBottom') - + // this.getCSSNumber(elem, 'borderTop') - + // this.getCSSNumber(elem, 'borderBottom'); + + // this.positionElement( + // elem, + // box.left + offsetLeft, + // box.top + offsetTop, + // actualWidth, + // actualHeight, + // animate + // ); + // }); + // } + + /** + * @hidden + */ + // private attachElements( + // bigOnes: HTMLVideoElement[], + // normalOnes: HTMLVideoElement[], + // smallOnes: HTMLVideoElement[] + // ) { + // const containerHeight = + // this.getHeight(this.layoutContainer) - + // this.getCSSNumber(this.layoutContainer, 'borderTop') - + // this.getCSSNumber(this.layoutContainer, 'borderBottom'); + // const containerWidth = + // this.getWidth(this.layoutContainer) - + // this.getCSSNumber(this.layoutContainer, 'borderLeft') - + // this.getCSSNumber(this.layoutContainer, 'borderRight'); + // const offsetLeft = 0; + // const offsetTop = 0; + // if (this.existBigAndNormalOnes(bigOnes, normalOnes, smallOnes)) { + // const smallOnesAux = smallOnes.length > 0 ? smallOnes : normalOnes; + // const bigOnesAux = bigOnes.length > 0 ? bigOnes : normalOnes; + // this.arrangeBigAndSmallOnes(bigOnesAux, smallOnesAux, { + // containerHeight, + // containerWidth, + // }); + // } else if (this.onlyExistBigOnes(bigOnes, normalOnes, smallOnes)) { + // // We only have one bigOne just center it + // this.arrange( + // bigOnes, + // containerWidth, + // containerHeight, + // 0, + // 0, + // this.opts.bigFixedRatio, + // this.opts.bigMinRatio, + // this.opts.bigMaxRatio, + // this.opts.animate + // ); + // } else if ( + // this.existBigAndNormalAndSmallOnes(bigOnes, normalOnes, smallOnes) + // ) { + // this.arrangeBigAndSmallOnes(bigOnes, normalOnes.concat(smallOnes), { + // containerHeight, + // containerWidth, + // }); + // } else { + // const normalOnesAux = normalOnes.concat(smallOnes); + // this.arrange( + // normalOnesAux, + // containerWidth - offsetLeft, + // containerHeight - offsetTop, + // offsetLeft, + // offsetTop, + // this.opts.fixedRatio, + // this.opts.minRatio, + // this.opts.maxRatio, + // this.opts.animate + // ); + // } + // } + + /** + * @hidden + */ + // private arrangeBigAndSmallOnes( + // bigOnesAux: HTMLVideoElement[], + // smallOnesAux: HTMLVideoElement[], + // data: { containerHeight: number; containerWidth: number } + // ) { + // const { containerWidth, containerHeight } = data; + // let offsetLeft = 0; + // let offsetTop = 0; + // const availableRatio = containerHeight / containerWidth; + // let bigOffsetTop = 0; + // let bigOffsetLeft = 0; + // let bigWidth, bigHeight; + // if (availableRatio > this.getVideoRatio(bigOnesAux[0])) { + // // We are tall, going to take up the whole width and arrange small + // // guys at the bottom + // bigWidth = containerWidth; + // bigHeight = Math.floor(containerHeight * this.opts.bigPercentage); + // offsetTop = bigHeight; + // bigOffsetTop = containerHeight - offsetTop; + // } else { + // // We are wide, going to take up the whole height and arrange the small + // // guys on the right + // bigHeight = containerHeight; + // bigWidth = Math.floor(containerWidth * this.opts.bigPercentage); + // offsetLeft = bigWidth; + // bigOffsetLeft = containerWidth - offsetLeft; + // } + // if (this.opts.bigFirst) { + // this.arrange( + // bigOnesAux, + // bigWidth, + // bigHeight, + // 0, + // 0, + // this.opts.bigFixedRatio, + // this.opts.bigMinRatio, + // this.opts.bigMaxRatio, + // this.opts.animate + // ); + // this.arrange( + // smallOnesAux, + // containerWidth - offsetLeft, + // containerHeight - offsetTop, + // offsetLeft, + // offsetTop, + // this.opts.fixedRatio, + // this.opts.minRatio, + // this.opts.maxRatio, + // this.opts.animate + // ); + // } else { + // this.arrange( + // smallOnesAux, + // containerWidth - offsetLeft, + // containerHeight - offsetTop, + // 0, + // 0, + // this.opts.fixedRatio, + // this.opts.minRatio, + // this.opts.maxRatio, + // this.opts.animate + // ); + // this.arrange( + // bigOnesAux, + // bigWidth, + // bigHeight, + // bigOffsetLeft, + // bigOffsetTop, + // this.opts.bigFixedRatio, + // this.opts.bigMinRatio, + // this.opts.bigMaxRatio, + // this.opts.animate + // ); + // } + // } + + /** + * @hidden + */ + // private existBigAndNormalOnes( + // bigOnes: HTMLVideoElement[], + // normalOnes: HTMLVideoElement[], + // smallOnes: HTMLVideoElement[] + // ) { + // return ( + // (bigOnes.length > 0 && normalOnes.length > 0 && smallOnes.length === 0) || + // (bigOnes.length > 0 && normalOnes.length === 0 && smallOnes.length > 0) || + // (bigOnes.length === 0 && normalOnes.length > 0 && smallOnes.length > 0) + // ); + // } + + /** + * @hidden + */ + // private onlyExistBigOnes( + // bigOnes: HTMLVideoElement[], + // normalOnes: HTMLVideoElement[], + // smallOnes: HTMLVideoElement[] + // ): boolean { + // return ( + // bigOnes.length > 0 && normalOnes.length === 0 && smallOnes.length === 0 + // ); + // } + + /** + * @hidden + */ + // private existBigAndNormalAndSmallOnes( + // bigOnes: HTMLVideoElement[], + // normalOnes: HTMLVideoElement[], + // smallOnes: HTMLVideoElement[] + // ): boolean { + // return bigOnes.length > 0 && normalOnes.length > 0 && smallOnes.length > 0; + // } + + /** + * @hidden + */ + private filterDisplayNone(element: HTMLElement) { + return this.css(element, 'display') !== 'none'; + } + + /** + * + * -------------------------------------------------------------------------------- + * + * GET LAYOUT + * + * + */ + + /** + * @hidden + */ + private getBestDimensions( + minRatio: number, + maxRatio: number, + width: number, + height: number, + count: number, + maxWidth: number, + maxHeight: number + ) { + let maxArea: number; + let targetCols: number; + let targetRows: number; + let targetHeight: number; + let targetWidth: number; + let tWidth: number; + let tHeight: number; + let tRatio: number; // Iterate through every possible combination of rows and columns // and see which one has the least amount of whitespace for (let i = 1; i <= count; i++) { - const colsAux = i; - const rowsAux = Math.ceil(count / colsAux); + const cols = i; + const rows = Math.ceil(count / cols); // Try taking up the whole height and width - tHeight = Math.floor(HEIGHT / rowsAux); - tWidth = Math.floor(WIDTH / colsAux); + tHeight = Math.floor(height / rows); + tWidth = Math.floor(width / cols); tRatio = tHeight / tWidth; - if (tRatio > maxR) { + if (tRatio > maxRatio) { // We went over decrease the height - tRatio = maxR; + tRatio = maxRatio; tHeight = tWidth * tRatio; - } else if (tRatio < minR) { + } else if (tRatio < minRatio) { // We went under decrease the width - tRatio = minR; + tRatio = minRatio; tWidth = tHeight / tRatio; } + tWidth = Math.min(maxWidth, tWidth); + tHeight = Math.min(maxHeight, tHeight); const area = tWidth * tHeight * count; // If this width and height takes up the most space then we're going with that - if (maxArea === undefined || area > maxArea) { - maxArea = area; - targetHeight = tHeight; - targetWidth = tWidth; - targetCols = colsAux; - targetRows = rowsAux; + if (maxArea === undefined || area >= maxArea) { + if (!(area === maxArea && count % (cols * rows) > count % (targetRows * targetCols))) { + // Favour even numbers of participants in each row, eg. 2 on each row + // instead of 3 in one row and then 1 on the next + maxArea = area; + targetHeight = tHeight; + targetWidth = tWidth; + targetCols = cols; + targetRows = rows; + } } } return { - maxArea: maxArea, - targetCols: targetCols, - targetRows: targetRows, - targetHeight: targetHeight, - targetWidth: targetWidth, + maxArea, + targetCols, + targetRows, + targetHeight, + targetWidth, ratio: targetHeight / targetWidth }; } - /** - * @hidden - */ - private arrange( - children: HTMLVideoElement[], - WIDTH: number, - HEIGHT: number, - offsetLeft: number, - offsetTop: number, - fixedRatio: boolean, - minRatio: number, - maxRatio: number, - animate: any - ) { - let targetHeight; + private getVideoRatio(element: { height: number; width: number; big?: boolean }) { + return element.height / element.width; + } + private getLayout(opts: any, elements: { height: number; width: number; big?: boolean }[]) { + const { + maxRatio = 3 / 2, + minRatio = 9 / 16, + fixedRatio = false, + bigPercentage = 0.8, + minBigPercentage = 0, + bigFixedRatio = false, + bigMaxRatio = 3 / 2, + bigMinRatio = 9 / 16, + bigFirst = true, + containerWidth = 640, + containerHeight = 480, + alignItems = 'center', + bigAlignItems = 'center', + smallAlignItems = 'center', + maxWidth = Infinity, + maxHeight = Infinity, + smallMaxWidth = Infinity, + smallMaxHeight = Infinity, + bigMaxWidth = Infinity, + bigMaxHeight = Infinity, + scaleLastRow = true, + bigScaleLastRow = true + } = opts; + const availableRatio = containerHeight / containerWidth; + let offsetLeft = 0; + let offsetTop = 0; + let bigOffsetTop = 0; + let bigOffsetLeft = 0; + const bigIndices = []; + let bigBoxes = []; + let smallBoxes = []; + let areas: { big: any; small: any } = { big: null, small: null }; + + // Move to Get Layout + const smallOnes = elements.filter((element) => !element.big); + const bigOnes = elements.filter((element, idx) => { + if (element.big) { + bigIndices.push(idx); + return true; + } + return false; + }); + //TODO: Habia un codigo personalizado que servía para + //TODO: tener videos grandes, pequeños y normales + //.filter((x) => !smallOnes.includes(x)); + + // const normalOnes: HTMLVideoElement[] = Array.prototype.filter + // .call( + // this.layoutContainer.querySelectorAll( + // `#${id}>*:not(.${this.opts.bigClass})` + // ), + // () => this.filterDisplayNone + // ) + // .filter((x) => !smallOnes.includes(x)); + // this.attachElements(bigOnes, normalOnes, smallOnes); + if (bigOnes.length > 0 && smallOnes.length > 0) { + let bigWidth; + let bigHeight; + let showBigFirst = bigFirst; + + if (availableRatio > this.getVideoRatio(bigOnes[0])) { + // We are tall, going to take up the whole width and arrange small + // guys at the bottom + bigWidth = containerWidth; + bigHeight = Math.floor(containerHeight * bigPercentage); + if (minBigPercentage > 0) { + // Find the best size for the big area + let bigDimensions; + if (!bigFixedRatio) { + bigDimensions = this.getBestDimensions( + bigMinRatio, + bigMaxRatio, + bigWidth, + bigHeight, + bigOnes.length, + bigMaxWidth, + bigMaxHeight + ); + } else { + // Use the ratio of the first video element we find to approximate + const ratio = bigOnes[0].height / bigOnes[0].width; + bigDimensions = this.getBestDimensions( + ratio, + ratio, + bigWidth, + bigHeight, + bigOnes.length, + bigMaxWidth, + bigMaxHeight + ); + } + bigHeight = Math.max( + containerHeight * minBigPercentage, + Math.min(bigHeight, bigDimensions.targetHeight * bigDimensions.targetRows) + ); + // Don't awkwardly scale the small area bigger than we need to and end up with floating + // videos in the middle + const smallDimensions = this.getBestDimensions( + minRatio, + maxRatio, + containerWidth, + containerHeight - bigHeight, + smallOnes.length, + smallMaxWidth, + smallMaxHeight + ); + bigHeight = Math.max(bigHeight, containerHeight - smallDimensions.targetRows * smallDimensions.targetHeight); + } + offsetTop = bigHeight; + bigOffsetTop = containerHeight - offsetTop; + if (bigFirst === 'column') { + showBigFirst = false; + } else if (bigFirst === 'row') { + showBigFirst = true; + } + } else { + // We are wide, going to take up the whole height and arrange the small + // guys on the right + bigHeight = containerHeight; + bigWidth = Math.floor(containerWidth * bigPercentage); + if (minBigPercentage > 0) { + // Find the best size for the big area + let bigDimensions; + if (!bigFixedRatio) { + bigDimensions = this.getBestDimensions( + bigMinRatio, + bigMaxRatio, + bigWidth, + bigHeight, + bigOnes.length, + bigMaxWidth, + bigMaxHeight + ); + } else { + // Use the ratio of the first video element we find to approximate + const ratio = bigOnes[0].height / bigOnes[0].width; + bigDimensions = this.getBestDimensions( + ratio, + ratio, + bigWidth, + bigHeight, + bigOnes.length, + bigMaxWidth, + bigMaxHeight + ); + } + bigWidth = Math.max( + containerWidth * minBigPercentage, + Math.min(bigWidth, bigDimensions.targetWidth * bigDimensions.targetCols) + ); + // Don't awkwardly scale the small area bigger than we need to and end up with floating + // videos in the middle + const smallDimensions = this.getBestDimensions( + minRatio, + maxRatio, + containerWidth - bigWidth, + containerHeight, + smallOnes.length, + smallMaxWidth, + smallMaxHeight + ); + bigWidth = Math.max(bigWidth, containerWidth - smallDimensions.targetCols * smallDimensions.targetWidth); + } + offsetLeft = bigWidth; + bigOffsetLeft = containerWidth - offsetLeft; + if (bigFirst === 'column') { + showBigFirst = true; + } else if (bigFirst === 'row') { + showBigFirst = false; + } + } + if (showBigFirst) { + areas.big = { + top: 0, + left: 0, + width: bigWidth, + height: bigHeight + }; + areas.small = { + top: offsetTop, + left: offsetLeft, + width: containerWidth - offsetLeft, + height: containerHeight - offsetTop + }; + } else { + areas.big = { + left: bigOffsetLeft, + top: bigOffsetTop, + width: bigWidth, + height: bigHeight + }; + areas.small = { + top: 0, + left: 0, + width: containerWidth - offsetLeft, + height: containerHeight - offsetTop + }; + } + } else if (bigOnes.length > 0 && smallOnes.length === 0) { + // We only have one bigOne just center it + areas.big = { + top: 0, + left: 0, + width: containerWidth, + height: containerHeight + }; + } else { + areas.small = { + top: offsetTop, + left: offsetLeft, + width: containerWidth - offsetLeft, + height: containerHeight - offsetTop + }; + } + + if (areas.big) { + bigBoxes = this.getLayoutAux( + { + containerWidth: areas.big.width, + containerHeight: areas.big.height, + offsetLeft: areas.big.left, + offsetTop: areas.big.top, + fixedRatio: bigFixedRatio, + minRatio: bigMinRatio, + maxRatio: bigMaxRatio, + alignItems: bigAlignItems, + maxWidth: bigMaxWidth, + maxHeight: bigMaxHeight, + scaleLastRow: bigScaleLastRow + }, + bigOnes + ); + } + if (areas.small) { + smallBoxes = this.getLayoutAux( + { + containerWidth: areas.small.width, + containerHeight: areas.small.height, + offsetLeft: areas.small.left, + offsetTop: areas.small.top, + fixedRatio, + minRatio, + maxRatio, + alignItems: areas.big ? smallAlignItems : alignItems, + maxWidth: areas.big ? smallMaxWidth : maxWidth, + maxHeight: areas.big ? smallMaxHeight : maxHeight, + scaleLastRow + }, + smallOnes + ); + } + + const boxes = []; + let bigBoxesIdx = 0; + let smallBoxesIdx = 0; + // Rebuild the array in the right order based on where the bigIndices should be + elements.forEach((element, idx) => { + if (bigIndices.indexOf(idx) > -1) { + boxes[idx] = bigBoxes[bigBoxesIdx]; + bigBoxesIdx += 1; + } else { + boxes[idx] = smallBoxes[smallBoxesIdx]; + smallBoxesIdx += 1; + } + }); + return { boxes, areas }; + } + + private getLayoutAux(opts: any, elements: { height: number; width: number; big?: boolean }[]) { + const { + maxRatio = 3 / 2, + minRatio = 9 / 16, + fixedRatio = false, + containerWidth = 640, + containerHeight = 480, + offsetLeft = 0, + offsetTop = 0, + alignItems = 'center', + maxWidth = Infinity, + maxHeight = Infinity, + scaleLastRow = true + } = opts; + const ratios = elements.map((element) => element.height / element.width); + const count = ratios.length; - const count = children.length; let dimensions; if (!fixedRatio) { - dimensions = this.getBestDimensions(minRatio, maxRatio, count, WIDTH, HEIGHT, targetHeight); + dimensions = this.getBestDimensions(minRatio, maxRatio, containerWidth, containerHeight, count, maxWidth, maxHeight); } else { // Use the ratio of the first video element we find to approximate - const ratio = this.getVideoRatio(children.length > 0 ? children[0] : null); - dimensions = this.getBestDimensions(ratio, ratio, count, WIDTH, HEIGHT, targetHeight); + const ratio = ratios.length > 0 ? ratios[0] : null; + dimensions = this.getBestDimensions(ratio, ratio, containerWidth, containerHeight, count, maxWidth, maxHeight); } // Loop through each stream in the container and place it inside - let x = 0, - y = 0; + let x = 0; + let y = 0; const rows = []; let row; + const boxes = []; + // Iterate through the children and create an array with a new item for each row // and calculate the width of each row so that we know if we go over the size and need // to adjust - for (let i = 0; i < children.length; i++) { + for (let i = 0; i < ratios.length; i++) { if (i % dimensions.targetCols === 0) { // This is a new row row = { - children: [], + ratios: [], width: 0, height: 0 }; rows.push(row); } - const elem: HTMLVideoElement = children[i]; - row.children.push(elem); + const ratio = ratios[i]; + row.ratios.push(ratio); let targetWidth = dimensions.targetWidth; - targetHeight = dimensions.targetHeight; + const targetHeight = dimensions.targetHeight; // If we're using a fixedRatio then we need to set the correct ratio for this element if (fixedRatio) { - targetWidth = targetHeight / this.getVideoRatio(elem); + targetWidth = targetHeight / ratio; } row.width += targetWidth; row.height = targetHeight; @@ -349,27 +1059,27 @@ export class OpenViduLayout { let remainingShortRows = 0; for (let i = 0; i < rows.length; i++) { row = rows[i]; - if (row.width > WIDTH) { + if (row.width > containerWidth) { // Went over on the width, need to adjust the height proportionally - row.height = Math.floor(row.height * (WIDTH / row.width)); - row.width = WIDTH; - } else if (row.width < WIDTH) { + row.height = Math.floor(row.height * (containerWidth / row.width)); + row.width = containerWidth; + } else if (row.width < containerWidth && row.height < maxHeight) { remainingShortRows += 1; } totalRowHeight += row.height; } - if (totalRowHeight < HEIGHT && remainingShortRows > 0) { + if (scaleLastRow && totalRowHeight < containerHeight && remainingShortRows > 0) { // We can grow some of the rows, we're not taking up the whole height - let remainingHeightDiff = HEIGHT - totalRowHeight; + let remainingHeightDiff = containerHeight - totalRowHeight; totalRowHeight = 0; for (let i = 0; i < rows.length; i++) { row = rows[i]; - if (row.width < WIDTH) { + if (row.width < containerWidth) { // Evenly distribute the extra height between the short rows let extraHeight = remainingHeightDiff / remainingShortRows; - if (extraHeight / row.height > (WIDTH - row.width) / row.width) { + if (extraHeight / row.height > (containerWidth - row.width) / row.width) { // We can't go that big or we'll go too wide - extraHeight = Math.floor(((WIDTH - row.width) / row.width) * row.height); + extraHeight = Math.floor(((containerWidth - row.width) / row.width) * row.height); } row.width += Math.floor((extraHeight / row.height) * row.width); row.height += extraHeight; @@ -380,216 +1090,59 @@ export class OpenViduLayout { } } // vertical centering - y = (HEIGHT - totalRowHeight) / 2; + switch (alignItems) { + case 'start': + y = 0; + break; + case 'end': + y = containerHeight - totalRowHeight; + break; + case 'center': + default: + y = (containerHeight - totalRowHeight) / 2; + break; + } // Iterate through each row and place each child for (let i = 0; i < rows.length; i++) { row = rows[i]; - // center the row - const rowMarginLeft = (WIDTH - row.width) / 2; + let rowMarginLeft; + switch (alignItems) { + case 'start': + rowMarginLeft = 0; + break; + case 'end': + rowMarginLeft = containerWidth - row.width; + break; + case 'center': + default: + rowMarginLeft = (containerWidth - row.width) / 2; + break; + } x = rowMarginLeft; - for (let j = 0; j < row.children.length; j++) { - const elem: HTMLVideoElement = row.children[j]; + let targetHeight; + for (let j = 0; j < row.ratios.length; j++) { + const ratio = row.ratios[j]; let targetWidth = dimensions.targetWidth; targetHeight = row.height; // If we're using a fixedRatio then we need to set the correct ratio for this element if (fixedRatio) { - targetWidth = Math.floor(targetHeight / this.getVideoRatio(elem)); + targetWidth = Math.floor(targetHeight / ratio); + } else if (targetHeight / targetWidth !== dimensions.targetHeight / dimensions.targetWidth) { + // We grew this row, we need to adjust the width to account for the increase in height + targetWidth = Math.floor((dimensions.targetWidth / dimensions.targetHeight) * targetHeight); } - elem.style.position = 'absolute'; - // $(elem).css('position', 'absolute'); - const actualWidth = - targetWidth - - this.getCSSNumber(elem, 'paddingLeft') - - this.getCSSNumber(elem, 'paddingRight') - - this.getCSSNumber(elem, 'marginLeft') - - this.getCSSNumber(elem, 'marginRight') - - this.getCSSNumber(elem, 'borderLeft') - - this.getCSSNumber(elem, 'borderRight'); - const actualHeight = - targetHeight - - this.getCSSNumber(elem, 'paddingTop') - - this.getCSSNumber(elem, 'paddingBottom') - - this.getCSSNumber(elem, 'marginTop') - - this.getCSSNumber(elem, 'marginBottom') - - this.getCSSNumber(elem, 'borderTop') - - this.getCSSNumber(elem, 'borderBottom'); - - this.positionElement(elem, x + offsetLeft, y + offsetTop, actualWidth, actualHeight, animate); + boxes.push({ + left: x + offsetLeft, + top: y + offsetTop, + width: targetWidth, + height: targetHeight + }); x += targetWidth; } y += targetHeight; } - } - - /** - * @hidden - */ - private attachElements(bigOnes: HTMLVideoElement[], normalOnes: HTMLVideoElement[], smallOnes: HTMLVideoElement[]) { - const HEIGHT = - this.getHeight(this.layoutContainer) - - this.getCSSNumber(this.layoutContainer, 'borderTop') - - this.getCSSNumber(this.layoutContainer, 'borderBottom'); - const WIDTH = - this.getWidth(this.layoutContainer) - - this.getCSSNumber(this.layoutContainer, 'borderLeft') - - this.getCSSNumber(this.layoutContainer, 'borderRight'); - const offsetLeft = 0; - const offsetTop = 0; - - if (this.existBigAndNormalOnes(bigOnes, normalOnes, smallOnes)) { - const smallOnesAux = smallOnes.length > 0 ? smallOnes : normalOnes; - const bigOnesAux = bigOnes.length > 0 ? bigOnes : normalOnes; - this.arrangeBigAndSmallOnes(bigOnesAux, smallOnesAux); - } else if (this.onlyExistBigOnes(bigOnes, normalOnes, smallOnes)) { - // We only have one bigOne just center it - this.arrange( - bigOnes, - WIDTH, - HEIGHT, - 0, - 0, - this.opts.bigFixedRatio, - this.opts.bigMinRatio, - this.opts.bigMaxRatio, - this.opts.animate - ); - } else if (this.existBigAndNormalAndSmallOnes(bigOnes, normalOnes, smallOnes)) { - this.arrangeBigAndSmallOnes(bigOnes, normalOnes.concat(smallOnes)); - } else { - const normalOnesAux = normalOnes.concat(smallOnes); - this.arrange( - normalOnesAux, - WIDTH - offsetLeft, - HEIGHT - offsetTop, - offsetLeft, - offsetTop, - this.opts.fixedRatio, - this.opts.minRatio, - this.opts.maxRatio, - this.opts.animate - ); - } - } - - /** - * @hidden - */ - private arrangeBigAndSmallOnes(bigOnesAux: HTMLVideoElement[], smallOnesAux: HTMLVideoElement[]) { - const HEIGHT = - this.getHeight(this.layoutContainer) - - this.getCSSNumber(this.layoutContainer, 'borderTop') - - this.getCSSNumber(this.layoutContainer, 'borderBottom'); - const WIDTH = - this.getWidth(this.layoutContainer) - - this.getCSSNumber(this.layoutContainer, 'borderLeft') - - this.getCSSNumber(this.layoutContainer, 'borderRight'); - const availableRatio = HEIGHT / WIDTH; - - let offsetLeft = 0; - let offsetTop = 0; - let bigOffsetTop = 0; - let bigOffsetLeft = 0; - let bigWidth, bigHeight; - - if (availableRatio > this.getVideoRatio(bigOnesAux[0])) { - // We are tall, going to take up the whole width and arrange small - // guys at the bottom - bigWidth = WIDTH; - bigHeight = Math.floor(HEIGHT * this.opts.bigPercentage); - offsetTop = bigHeight; - bigOffsetTop = HEIGHT - offsetTop; - } else { - // We are wide, going to take up the whole height and arrange the small - // guys on the right - bigHeight = HEIGHT; - bigWidth = Math.floor(WIDTH * this.opts.bigPercentage); - offsetLeft = bigWidth; - bigOffsetLeft = WIDTH - offsetLeft; - } - if (this.opts.bigFirst) { - this.arrange( - bigOnesAux, - bigWidth, - bigHeight, - 0, - 0, - this.opts.bigFixedRatio, - this.opts.bigMinRatio, - this.opts.bigMaxRatio, - this.opts.animate - ); - this.arrange( - smallOnesAux, - WIDTH - offsetLeft, - HEIGHT - offsetTop, - offsetLeft, - offsetTop, - this.opts.fixedRatio, - this.opts.minRatio, - this.opts.maxRatio, - this.opts.animate - ); - } else { - this.arrange( - smallOnesAux, - WIDTH - offsetLeft, - HEIGHT - offsetTop, - 0, - 0, - this.opts.fixedRatio, - this.opts.minRatio, - this.opts.maxRatio, - this.opts.animate - ); - this.arrange( - bigOnesAux, - bigWidth, - bigHeight, - bigOffsetLeft, - bigOffsetTop, - this.opts.bigFixedRatio, - this.opts.bigMinRatio, - this.opts.bigMaxRatio, - this.opts.animate - ); - } - } - - /** - * @hidden - */ - private existBigAndNormalOnes(bigOnes: HTMLVideoElement[], normalOnes: HTMLVideoElement[], smallOnes: HTMLVideoElement[]) { - return ( - (bigOnes.length > 0 && normalOnes.length > 0 && smallOnes.length === 0) || - (bigOnes.length > 0 && normalOnes.length === 0 && smallOnes.length > 0) || - (bigOnes.length === 0 && normalOnes.length > 0 && smallOnes.length > 0) - ); - } - - /** - * @hidden - */ - private onlyExistBigOnes(bigOnes: HTMLVideoElement[], normalOnes: HTMLVideoElement[], smallOnes: HTMLVideoElement[]): boolean { - return bigOnes.length > 0 && normalOnes.length === 0 && smallOnes.length === 0; - } - - /** - * @hidden - */ - private existBigAndNormalAndSmallOnes( - bigOnes: HTMLVideoElement[], - normalOnes: HTMLVideoElement[], - smallOnes: HTMLVideoElement[] - ): boolean { - return bigOnes.length > 0 && normalOnes.length > 0 && smallOnes.length > 0; - } - - /** - * @hidden - */ - private filterDisplayNone(element: HTMLElement) { - return element.style.display !== 'none'; + return boxes; } }