Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions assets/src/blocks/godam-player/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,11 @@
.easydam-layer.hotspot-layer {
pointer-events: none;
background: transparent;
z-index: 5;
}

.video-js .vjs-control-bar {
z-index: 8;
}


Expand Down
5 changes: 5 additions & 0 deletions assets/src/css/godam-player.scss
Original file line number Diff line number Diff line change
Expand Up @@ -423,12 +423,17 @@
.easydam-layer.hotspot-layer {
pointer-events: none;
background: transparent;
z-index: 5;

&.overlapped {
z-index: 0;
}
}

.video-js .vjs-control-bar {
z-index: 8;
}

.godam-menu-item-container {
align-items: center;
width: 100%;
Expand Down
5 changes: 5 additions & 0 deletions assets/src/js/godam-player/managers/eventsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default class EventsManager {
this.onPlayerConfigurationSetup = callbacks.onPlayerConfigurationSetup;
this.onTimeUpdate = callbacks.onTimeUpdate;
this.onFullscreenChange = callbacks.onFullscreenChange;
this.onVideoResize = callbacks.onVideoResize;
this.onPlay = callbacks.onPlay;
this.onControlsMove = callbacks.onControlsMove;
}
Expand Down Expand Up @@ -109,6 +110,10 @@ export default class EventsManager {
* Handle video resize events
*/
handleVideoResize() {
if ( this.onVideoResize ) {
this.onVideoResize();
}

// Skip if video is fullscreen or classic skin
if ( ! this.player ||
typeof this.player.isFullscreen !== 'function' ||
Expand Down
138 changes: 123 additions & 15 deletions assets/src/js/godam-player/managers/layers/hotspotLayerManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
*/
import { __, sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { HOTSPOT_CONSTANTS } from '../../utils/constants';

/**
* Hotspot Layer Manager
* Handles hotspot layer functionality including creation, positioning, and interaction
*/
export default class HotspotLayerManager {
static BASE_WIDTH = 800;
static BASE_HEIGHT = 600;
static BASE_WIDTH = HOTSPOT_CONSTANTS.BASE_WIDTH;
static BASE_HEIGHT = HOTSPOT_CONSTANTS.BASE_HEIGHT;

constructor( player, isDisplayingLayers, currentPlayerVideoInstanceId ) {
this.player = player;
Expand Down Expand Up @@ -89,7 +94,17 @@ export default class HotspotLayerManager {
}
} );

this.updateHotspotPositions();
// Use requestAnimationFrame to wait for layout to stabilize after fullscreen resize
let framesToWait = 2;
const waitForResize = () => {
if ( framesToWait > 0 ) {
framesToWait--;
window.requestAnimationFrame( waitForResize );
} else {
this.updateHotspotPositions();
}
};
window.requestAnimationFrame( waitForResize );
}

/**
Expand Down Expand Up @@ -123,6 +138,64 @@ export default class HotspotLayerManager {
} );
}

/**
* Compute content rectangle
*
* @return {Object|null} Content rectangle {left, top, width, height} or null
*/
computeContentRect() {
const videoEl = this.player.tech( true )?.el() || this.player.el().querySelector( 'video' );
const containerEl = this.player.el();

if ( ! videoEl || ! containerEl ) {
return null;
}

const nativeW = videoEl.videoWidth || this.player.videoWidth() || 0;
const nativeH = videoEl.videoHeight || this.player.videoHeight() || 0;

const elW = containerEl.offsetWidth;
const elH = containerEl.offsetHeight;

// If video dimensions aren't loaded yet, use full container
if ( ! nativeW || ! nativeH ) {
return {
left: 0,
top: 0,
width: elW,
height: elH,
};
}

const videoAspectRatio = nativeW / nativeH;
const containerAspectRatio = elW / elH;

let contentW, contentH, offsetX, offsetY;

if ( containerAspectRatio > videoAspectRatio ) {
// Pillarboxed (black bars on left/right)
contentH = elH;
contentW = elH * videoAspectRatio;
offsetX = ( elW - contentW ) / 2;
offsetY = 0;
} else {
// Letterboxed (black bars on top/bottom)
contentW = elW;
contentH = elW / videoAspectRatio;
offsetX = 0;
offsetY = ( elH - contentH ) / 2;
}

const result = {
left: Math.round( offsetX ),
top: Math.round( offsetY ),
width: Math.round( contentW ),
height: Math.round( contentH ),
};

return result;
}

/**
* Create hotspot element
*
Expand All @@ -139,18 +212,39 @@ export default class HotspotLayerManager {
hotspotDiv.classList.add( 'hotspot', 'circle' );
hotspotDiv.style.position = 'absolute';

const contentRect = this.computeContentRect();

// Positioning
const fallbackPosX = hotspot.oPosition?.x ?? hotspot.position.x;
const fallbackPosY = hotspot.oPosition?.y ?? hotspot.position.y;
const pixelX = ( fallbackPosX / baseWidth ) * containerWidth;
const pixelY = ( fallbackPosY / baseHeight ) * containerHeight;

let fallbackDiameter = hotspot.oSize?.diameter ?? hotspot.size?.diameter;
if ( ! fallbackDiameter ) {
if ( hotspot.unit === 'percent' && contentRect ) {
fallbackDiameter = ( HOTSPOT_CONSTANTS.DEFAULT_DIAMETER_PX / contentRect.width ) * 100;
} else {
fallbackDiameter = hotspot.unit === 'percent' ? HOTSPOT_CONSTANTS.DEFAULT_DIAMETER_PERCENT : HOTSPOT_CONSTANTS.DEFAULT_DIAMETER_PX;
}
}

let pixelX, pixelY, pixelDiameter;

if ( hotspot.unit === 'percent' && contentRect ) {
// New percentage-based positioning
pixelX = contentRect.left + ( ( fallbackPosX / 100 ) * contentRect.width );
pixelY = contentRect.top + ( ( fallbackPosY / 100 ) * contentRect.height );
pixelDiameter = ( fallbackDiameter / 100 ) * contentRect.width;
} else {
// Legacy pixel-based positioning (relative to 800x600)
// We now map these to the contentRect instead of the full container to avoid black bars
const effectiveRect = contentRect || { left: 0, top: 0, width: containerWidth, height: containerHeight };
pixelX = effectiveRect.left + ( ( fallbackPosX / baseWidth ) * effectiveRect.width );
pixelY = effectiveRect.top + ( ( fallbackPosY / baseHeight ) * effectiveRect.height );
pixelDiameter = ( fallbackDiameter / baseWidth ) * effectiveRect.width;
}

hotspotDiv.style.left = `${ pixelX }px`;
hotspotDiv.style.top = `${ pixelY }px`;

// Sizing
const fallbackDiameter = hotspot.oSize?.diameter ?? hotspot.size?.diameter ?? 48;
const pixelDiameter = ( fallbackDiameter / baseWidth ) * containerWidth;
hotspotDiv.style.width = `${ pixelDiameter }px`;
hotspotDiv.style.height = `${ pixelDiameter }px`;

Expand Down Expand Up @@ -322,6 +416,8 @@ export default class HotspotLayerManager {
const baseWidth = HotspotLayerManager.BASE_WIDTH;
const baseHeight = HotspotLayerManager.BASE_HEIGHT;

const contentRect = this.computeContentRect();

this.hotspotLayers.forEach( ( layerObj ) => {
const hotspotDivs = layerObj.layerElement.querySelectorAll( '.hotspot' );
hotspotDivs.forEach( ( hotspotDiv, index ) => {
Expand All @@ -330,14 +426,26 @@ export default class HotspotLayerManager {
// Recalc position
const fallbackPosX = hotspot.oPosition?.x ?? hotspot.position.x;
const fallbackPosY = hotspot.oPosition?.y ?? hotspot.position.y;
const pixelX = ( fallbackPosX / baseWidth ) * containerWidth;
const pixelY = ( fallbackPosY / baseHeight ) * containerHeight;
const fallbackDiameter = hotspot.oSize?.diameter ?? hotspot.size?.diameter ?? 48;

let pixelX, pixelY, pixelDiameter;

if ( hotspot.unit === 'percent' && contentRect ) {
// New percentage-based positioning
pixelX = contentRect.left + ( ( fallbackPosX / 100 ) * contentRect.width );
pixelY = contentRect.top + ( ( fallbackPosY / 100 ) * contentRect.height );
pixelDiameter = ( fallbackDiameter / 100 ) * contentRect.width;
} else {
// Legacy pixel-based positioning
// We now map these to the contentRect instead of the full container to avoid black bars
const effectiveRect = contentRect || { left: 0, top: 0, width: containerWidth, height: containerHeight };
pixelX = effectiveRect.left + ( ( fallbackPosX / baseWidth ) * effectiveRect.width );
pixelY = effectiveRect.top + ( ( fallbackPosY / baseHeight ) * effectiveRect.height );
pixelDiameter = ( fallbackDiameter / baseWidth ) * effectiveRect.width;
}

hotspotDiv.style.left = `${ pixelX }px`;
hotspotDiv.style.top = `${ pixelY }px`;

// Recalc size
const fallbackDiameter = hotspot.oSize?.diameter ?? hotspot.size?.diameter ?? 48;
const pixelDiameter = ( fallbackDiameter / baseWidth ) * containerWidth;
hotspotDiv.style.width = `${ pixelDiameter }px`;
hotspotDiv.style.height = `${ pixelDiameter }px`;

Expand Down
7 changes: 7 additions & 0 deletions assets/src/js/godam-player/managers/layersManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ export default class LayersManager {
this.hotspotLayerManager.handleHotspotLayersTimeUpdate( currentTime );
}

/**
* Handle video resize events
*/
handleVideoResize() {
this.hotspotLayerManager.updateHotspotPositions();
}

/**
* Handle fullscreen changes for layers
*/
Expand Down
12 changes: 12 additions & 0 deletions assets/src/js/godam-player/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ export const FORM_TYPES = {
METFORM: 'metform',
};

/**
* Hotspot constants.
*/
export const HOTSPOT_CONSTANTS = {
DEFAULT_DIAMETER_PERCENT: 15,
DEFAULT_DIAMETER_PX: 48,
MIN_PX: 10,
MIN_PERCENT_FALLBACK: 5,
BASE_WIDTH: 800,
BASE_HEIGHT: 600,
};

/**
* Layer types.
*/
Expand Down
1 change: 1 addition & 0 deletions assets/src/js/godam-player/videoPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export default class GodamVideoPlayer {
onPlayerConfigurationSetup: () => this.controlsManager.setupPlayerConfiguration(),
onTimeUpdate: ( currentTime ) => this.handleTimeUpdate( currentTime ),
onFullscreenChange: () => this.layersManager.handleFullscreenChange(),
onVideoResize: () => this.layersManager.handleVideoResize(),
onPlay: () => this.layersManager.handlePlay(),
onControlsMove: () => this.controlsManager.moveVideoControls(),
} );
Expand Down
13 changes: 9 additions & 4 deletions pages/video-editor/components/SidebarLayers.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import MetformIcon from '../assets/layers/MetFormIcon.png';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { Button, Icon, Tooltip } from '@wordpress/components';
import { plus, preformatted, customLink, arrowRight, video, customPostType, thumbsUp, error } from '@wordpress/icons';
import { useState } from '@wordpress/element';
Expand Down Expand Up @@ -225,14 +225,15 @@ const SidebarLayers = ( { currentTime, onSelectLayer, onPauseVideo, duration } )
id: uuidv4(),
tooltipText: __( 'Click me!', 'godam' ),
position: { x: 50, y: 50 },
size: { diameter: 48 },
oSize: { diameter: 48 },
size: { diameter: 15 },
oSize: { diameter: 15 },
oPosition: { x: 50, y: 50 },
link: '',
backgroundColor: '#0c80dfa6',
showStyle: false,
showIcon: false,
icon: '',
unit: 'percent',
},
],
} ),
Expand Down Expand Up @@ -372,7 +373,11 @@ const SidebarLayers = ( { currentTime, onSelectLayer, onPauseVideo, duration } )
iconPosition="left"
onClick={ openModal }
disabled={ ! currentTime || layers.find( ( l ) => ( l.displayTime ) === ( currentTime ) ) }
>{ __( 'Add layer at ', 'godam' ) } { currentTime }s
>
{
// translators: %s is the current time in seconds.
sprintf( __( 'Add layer at %ss', 'godam' ), currentTime )
}
</Button>
{ layers.find( ( l ) => l.displayTime === currentTime ) && (
<p className="text-slate-500 text-center">
Expand Down
Loading
Loading