La problématique

Comme je l’indiquais dans mon précédent article, les sous-titres de Storyline sont beaucoup plus pratiques que de devoir les refaire à la main pour plusieurs raisons comme la rapidité d’intégration, la gestion des calques, de la chronologies, etc.

Je précisais que j’avais identifié deux manières de procéder pour les personnaliser. Dans la première version, je vous proposais de stocker les sous-titres dans une variable et de les afficher dans un bloc de texte dans le contenu. Voici donc la seconde possibilité, beaucoup plus simple à mettre en place mais aussi un peu plus limitée.

Le concept

Dans cette seconde version, je vous propose de modifier l’affichage de l’objet sous-titre en copiant les propriétés d’un objet modèle du module.

Il y a toutefois certaines limitations :

  • l’affichage ne pourra pas être animé comme dans la précédente version
  • le bloc du sous-titre disparaîtra automatiquement à la fin de la voix-off
  • le fond et le contour ne pourront pas être rempli avec un dégradé de couleur
  • la forme sera obligatoirement un rectangle (éventuellement avec les coins arrondis)

Le résultat

Ci-dessus un aperçu du résultat final :

Les sous-titres reprennent les informations de dimensions, coordonnées, couleur de fond, taille du taille, couleur du texte, couleur de bordure et taille de bordure de l’objet d’exemple dans le module.

Il faut en revanche indiquer, en option, le rayon des bords arrondis si on le souhaite (il n’est pas possible de les récupérer automatiquement), si on préfère supprimer l’ombre sous le texte et la dimension de la marge intérieure (10 px par défaut).

Les fichiers sources (archive 7zip, 260 ko) :

Le paramétrage du module

Les variables

Plutôt simple, aucune variable nécessaire.

Le masque

Les objets

Nous n’aurons besoin que deux objets au minimum : un objet web pour le script et le modèle à mimer pour le cadre du sous-titre.

Le script

Si vous avez lu les articles précédents sur les sous-titres et le menu, vous savez donc que pour optimiser le code, j’ai pris l’habitude de l’intégrer dans un objet web (un fichier index.html + un fichier js). Nous intégrons donc les fichiers du dossier présent dans l’archive de cette façon.

Le modèle

À présent, nous avons besoin d’un objet qui serve de modèle. Comme le cadre de sous-titre est un élément div de la page, nous sommes limité au niveau des formes, c’est pourquoi il faudra choisir un rectangle. Appliquons lui les dimensions que nous souhaitons, plaçons le correctement et paramétrons sa couleur de fond et de contour (couleurs pleines), l’épaisseur du contour, ainsi que la taille et la couleur du texte.

Le texte de notre objet nous servant uniquement d’aperçu pour le rendu final, nous choisissons donc dès maintenant la police de caractères que nous souhaitons utiliser. Toutefois, elle ne sera pas récupérée automatiquement par le script à ce niveau là, il faudra la paramétrer ensuite dans les options des sous-titres du lecteur.

Dans les options d’accessibilité, nous renseignons un nom que nous reporterons plus loin dans le déclencheur JavaScript, ici « closed caption ».

Enfin, nous le définissons son état initial à « masqué » afin qu’il ne soit pas accessible aux lecteurs d’écran ou à la tabulation.

Le bouton d’affichage des sous-titres

Si vous le souhaitez, vous pouvez ajouter un bouton personnalisé pour l’affichage des sous-titres. Il s’agit d’un bouton classique avec un déclencheur qui basculera la variable projet « Player.DisplayCaptions » entre vrai et faux.

Le déclencheur

Nous ajoutons enfin un déclencheur pour exécuter un petit code JavaScript pour les options :

window.options = {
	id : "closed captions" //the alt text of your sample
	//padding : "10px", //the space around your text, 10px by default, css property
	//textShadow : false, //If you want to delete the shadow behind the text
	//cornerRadius : 10px //the size of the rounded corners, css property
}

Seule la première option est obligatoire puisqu’il s’agit du nom de notre modèle afin que le script le retrouve dans la page.

Les autres options :

  • padding : il s’agit de la marge intérieure pour le texte, elle est de 10 px défaut si nous la renseignons pas ; sa syntaxe est identique à la propriété CSS (https://developer.mozilla.org/fr/docs/Web/CSS/padding). Comme les objets sont créés en SVG, il n’est pas possible de récupérer cette valeur automatiquement sur notre modèle.
  • textShadow : par défaut, Storyline ajoute une ombre sous le texte des sous-titres, si vous souhaitez la supprimer, il faut passer cette option à « true » ;
  • cornerRadius : pour la même raison que le padding, il n’est pas possible de récupérer automatiquement l’arrondi des coins du rectangle, il faudra donc la renseigner ici, la syntaxe correspond à celle de règle CSS border-radius.

Le lecteur

Comme indiqué plus haut, il ne nous reste plus qu’à choisir la police de caractères dans les paramètres du lecteur et nous avons ainsi terminé le paramétrage de nos sous-titres.

Explication du script

var customCC = function(p){
	//Set the options
	var options = p.options;
	if(options.padding == null){
		options.padding = "10px";
	}

Comme il n’y a aucune variable utilisée, le script n’a pas besoin de récupérer l’API de Storyline, il commence donc par la création de la fonction et la récupération des options, si la marge intérieur n’a pas été définie par l’utilisateur, elle est initialisée à 10 px.

//Get the example element
	var example = p.document.querySelector('[data-acc-text = "'+options.id+'"]');
	var exampleStyle = example.querySelector('text');
	var backgroundPath = example.querySelector('path');

	//Get the closed caption container
	var ccContainer = p.document.querySelector(".caption");
	
	//Get the slide element and the closed caption parent container to calculate the coords and dimensions
	var slide = p.document.querySelector("#slide");
	var ccContainerParent = p.document.querySelector(".caption-container");

Puis le script récupère notre élément d’exemple, la partie correspondant au texte et celle du fond, et le cadre des sous-titres. Il récupère également l’élément de la diapositive et le parent du sous-titre afin de déterminer les coordonnées de nos sous-titres.

	//Initialize the options
	options.fontSize = exampleStyle.getAttribute('font-size');
	options.fontFamily = ccContainer.style.fontFamily;
	options.color = exampleStyle.getAttribute('fill');
	options.borderColor = backgroundPath.getAttribute('stroke');
	options.borderOpacity = backgroundPath.getAttribute('stroke-opacity');
	options.borderWidth = backgroundPath.getAttribute('stroke-width');

Il initialise à présent les options dont nous auront besoin plus loin (leurs noms sont à priori assez explicites).

//Create the stylesheet
	var sheet = (function() {
		if(!p.document.querySelector('#cc-style')){
			// Create the <style> tag
			var style = p.document.createElement("style");
			style.type = 'text/css';
			style.id = 'cc-style';

			// WebKit hack :(
			style.appendChild(p.document.createTextNode(""));

			// Add the <style> element to the page
			p.document.head.appendChild(style);

			
		}
		else{
			var style = p.document.querySelector('#cc-style');
		}
		return style.sheet;
	})();

La fonction pour l’ajout d’une feuille de style est créée. Il faut une condition au début afin qu’elle ne soit pas créée de multiples fois.

var zoom = function(){
		//Get zoom value
		var slideOriginalWidth = p.document.querySelector('.slide > svg').getAttribute('viewBox').split(' ')[2];
		var slideActualWidth = p.document.querySelector('.slide > svg').getAttribute('width');
		return slideActualWidth/slideOriginalWidth;
	}

La fonction pour déterminer la mise à l »échelle du module afin d’ajuster les dimensions de notre sous-titre.

//Function to resize new elements relative to the content
	var resizeFrame = function(){
		//Display the example box to retrieve its dimensions
		example.classList.remove('hidden');

Le script crée à présent la fonction principale qui va placer et redimensionner nos sous-titres. Il commence par supprimer la class « hidden » afin de faire apparaître temporairement notre élément d’exemple sans quoi il n’aurait pas accès à ses coordonnées.

//Test the alignments, use of round and -1 because the dimensions are not accurate depending the browser
		var exampleMarginLeft = Math.round(exampleStyle.getBBox().x);
		var exampleMarginTop = Math.round(exampleStyle.getBBox().y);
		var exampleMarginRight = Math.round(example.querySelector('svg').getBBox().width-exampleStyle.getBBox().x-exampleStyle.getBBox().width);
		var exampleMarginBottom = Math.round(example.querySelector('svg').getBBox().height-exampleStyle.getBBox().y-exampleStyle.getBBox().height);
		
		//Set text align
		if(-1>exampleMarginLeft-exampleMarginRight){
			options.xAlign = 'left';
		}
		else if(-1<=exampleMarginLeft-exampleMarginRight || 1<=exampleMarginLeft-exampleMarginRight){
			options.xAlign = 'center';
		}
		else{
			options.xAlign = 'right';
		}
		
		//Set text vertical align
		if(-1>exampleMarginTop-exampleMarginBottom){
			options.yAlign = 'start';
		}
		else if(-1<=exampleMarginTop-exampleMarginBottom || 1<=exampleMarginTop-exampleMarginBottom){
			options.yAlign = 'center';
		}
		else{
			options.yAlign = 'end';
		}

Le script va déterminer de façon automatique quels sont les alignements horizontaux et verticaux du texte. Comme les dimensions sont un peu variables selon le niveau de mise à l’échelle et les navigateurs, il est préférable de comparer les dimensions avec une légère marge (plus ou moins 1 px).

ccContainer.style.fontSize = (parseInt(options.fontSize)*zoom()) +'px';

La taille du texte est ajustée.

var slideCoords = [parseInt(p.getComputedStyle(slide).transform.match(/matrix.*\((.+)\)/)[1].split(', ')[4]),parseInt(p.getComputedStyle(slide).transform.match(/matrix.*\((.+)\)/)[1].split(', ')[5])];
				
		var ccContainerCoords = [parseInt(p.getComputedStyle(ccContainerParent).transform.match(/matrix.*\((.+)\)/)[1].split(', ')[4]),parseInt(p.getComputedStyle(ccContainerParent).transform.match(/matrix.*\((.+)\)/)[1].split(', ')[5])];
		
		var exampleCoords = [parseInt(p.getComputedStyle(example).transform.match(/matrix.*\((.+)\)/)[1].split(', ')[4]),parseInt(p.getComputedStyle(example).transform.match(/matrix.*\((.+)\)/)[1].split(', ')[5])];
		
		ccContainer.style.left = (exampleCoords[0]+(slideCoords[0]-ccContainerCoords[0]))+'px';
		ccContainer.style.top = (exampleCoords[1]+(slideCoords[1]-ccContainerCoords[1]))+'px';
		ccContainer.style.width = example.style.width;
		ccContainer.style.height = example.style.height;

Le script va à présent déterminer les coordonnées et les dimensions de notre bloc de sous-titres. Le référentiel n’étant pas le même pour nos sous-titres que pour notre modèle, il faut déterminer la différence entre les abscisses et les ordonnées de chacun.

if(ccContainer.querySelector('div')){
			if(options.borderColor){
				ccContainer.querySelector('div').style.borderWidth = (options.borderWidth*zoom())+'px';
			}
			if(options.cornerRadius){
				ccContainer.querySelector('div').style.borderRadius = (options.cornerRadius*zoom())+'px';
			}
		}

Le script va également déterminer l’épaisseur du bord et la valeur de l’arrondi selon la mise à l’échelle.

//Hide the example box
		example.classList.add('hidden');
	}

Enfin, le modèle est masqué.

resizeFrame();
	window.onresize = resizeFrame;
	
	var mutationObserver = new MutationObserver(function(mutations) {
		resizeFrame();
	});
	mutationObserver.observe(ccContainerParent, {attributes: true, attributeFilter: ['style'], subtree:false});

La fonction est exécutée une première fois, puis le sera à nouveau lors d’un redimensionnement de la fenêtre ou d’un changement de taille du conteneur de sous-titres, par exemple à l’ouverture/fermeture du menu à gauche.

function hexToRgb(hex) {
	  // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
	  var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
	  hex = hex.replace(shorthandRegex, function(m, r, g, b) {
		return r + r + g + g + b + b;
	  });

	  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
	  return result ? {
		r: parseInt(result[1], 16),
		g: parseInt(result[2], 16),
		b: parseInt(result[3], 16)
	  } : null;
	}

Ci-dessus une fonction pour convertir un code hexdécimal en valeur RGB.

//CSS
	sheet.insertRule('.caption{transform: translateX(0) translateZ(0) !important;',0);

	sheet.insertRule('.caption>div{width:100%; height:100% !important; margin:0;display:flex;align-items:'+options.yAlign+';padding:'+options.padding+';background-color: rgba('+hexToRgb(backgroundPath.getAttribute('fill')).r+', '+hexToRgb(backgroundPath.getAttribute('fill')).g+', '+hexToRgb(backgroundPath.getAttribute('fill')).b+', '+backgroundPath.getAttribute('fill-opacity')+') !important;',0);
	
	if(options.borderColor){
		sheet.insertRule('.caption>div{border-color: rgba('+hexToRgb(options.borderColor).r+', '+hexToRgb(options.borderColor).g+', '+hexToRgb(options.borderColor).b+', '+options.borderOpacity+') !important; border-style:solid;border-width:'+(options.borderWidth*zoom())+'px',0);
	}
	if(options.cornerRadius){
		sheet.insertRule('.caption>div{border-radius:'+(options.cornerRadius*zoom())+'px',0);
	}
	
	sheet.insertRule('.caption>div{-ms-flex-align:'+options.yAlign+';',0);
	
	sheet.insertRule('.caption p{width:100%; margin:0 !important;text-align : '+options.xAlign+';padding:0 !important; background-color: rgba(0,0,0,0) !important;',0);
	
	if(options.textShadow === false){
		sheet.insertRule('.caption p{text-shadow:none !important;',0);
	}

À présent les règles de style CSS.

}
customCC(window.parent);

Enfin, la fonction est fermée et exécutée avec window.parent comme argument pour indiquer le parent de notre script.

Conclusion

Ce script est donc plus simple dans sa mise en place : un rectangle avec un texte pour le modèle, un objet web et un déclencheur JavaScript avec une seule option obligatoire, au minimum.

Les limitations inhérentes à cette simplification semblent toutefois acceptables car elles reprennent finalement les contraintes déjà existantes des sous-titres de Storyline.

Le code a été testé avec IE11, Edge, Chrome et Firefox, mais n’hésitez pas à me remonter tout dysfonctionnement que vous pourriez rencontrer ou si certains points ne sont pas assez explicites en me contactant sur linkedin : http://www.linkedin.com/in/gregoryfauchille

Merci à Stackoverflow sans qui tout cela n’aurait pas été possible.