,

Portal : du jeu à la réalité, comment créer votre portail grâce à des technos web ? - partie 2

Etape 5 : ajout du mur de flammes

Maintenant que nous nous sommes occupés de la partie WebRTC, nous allons ajouter un peu de graphisme à tout cela. Pour le moment, notre flux WebRTC arrive directement dans une balise vidéo, mais il se trouve que les canvas et les vidéos fonctionnent très bien ensemble !  En effet, nous allons faire des snapshots de notre balise vidéo que nous allons injecter dans un canvas et ainsi pouvoir commencer à jouer plus sérieusement avec des effets graphiques.

Nous allons donc avoir :

  • 1 balise vidéo en “display:none”
  • 1 canvas restituant la vidéo mais avec un masque
  • 1 canvas affichant le mur de flamme.

display:none

Pour ce faire, il suffit simplement de faire en sorte que dans notre html, nous ayons le code suivant :

<video id=’remoteVideo’ autoplay muted style= »display:none; »></video>

canvas avec la vidéo

Nous allons maintenant ajouter à notre application (app.js) l’affichage du canvas qui recevra la vidéo :

 

var canvasRemoteElement = document.querySelector(‘#canvasRemoteVideo’);

var ctxRemote = canvasRemoteElement.getContext(‘2d’);

 

function snapshot(){

var canvasToUse = canvasRemoteElement;

var contextToUse = ctxRemote;

var videoToUse = remoteVideo;

 

canvasRemoteElement.width = remoteVideo.videoWidth;

canvasRemoteElement.height = remoteVideo.videoHeight;

if (remoteStream){

ctxRemote.drawImage(remoteVideo, 0,0);

}

 

window.requestAnimationFrame(snapshot);

}

snapshot();

Nous utilisons simplement la possibilité de dessiner dans un canvas une image d’une vidéo.

Mur de flamme

Vous devez copier le contenu du fichier canvas.js du projet html5-canvas-demo dans notre fichier ./js/canvasFire.js.

Nous allons maintenant afficher le mur dans un canvas. Nous devons donc éditer notre fichier app.js.

var canvasFireElement = document.querySelector(‘#canvasFireLocalVideo’);

var ctxFire = canvasFireElement.getContext(‘2d’);

Nous allons aussi modifier la méthode snapshot afin d’y intégrer toute la partie flammes.

var init = false;

function snapshot(){

var canvasToUse = canvasRemoteElement;

var contextToUse = ctxRemote;

var videoToUse = remoteVideo;

canvasRemoteElement.width = remoteVideo.videoWidth;

canvasRemoteElement.height = remoteVideo.videoHeight;

if (remoteStream){

ctxRemote.drawImage(remoteVideo, 0,0);

}

var idealWidth = Math.min(canvasToUse.parentElement.clientWidth, videoToUse.videoWidth + 100);

var minVideoWidth = Math.min(canvasToUse.parentElement.clientWidth – 50, videoToUse.videoWidth);

var ratio = videoToUse.videoWidth / videoToUse.videoHeight;

var idealHeight = Math.min(idealWidth / ratio, videoToUse.videoHeight);

var useVideoWidth = idealWidth === videoToUse.videoWidth + 100;

 

canvasToUse.width = idealWidth; //landscapeMode ? idealHeight : idealWidth;

canvasToUse.height = canvasToUse.width;

canvasToUse.style.top = ((canvasToUse.parentElement.clientHeight – canvasToUse.height) / 2)+ »px »;

canvasToUse.style.left = ((canvasToUse.parentElement.clientWidth – canvasToUse.width) / 2)+ »px »;

 

canvasFireElement.width = idealWidth;// landscapeMode ? idealHeight : idealWidth;

canvasFireElement.height = canvasFireElement.width;

canvasFireElement.style.top = ((canvasToUse.parentElement.clientHeight – canvasFireElement.height) / 2)+ »px »;

canvasFireElement.style.left = ((canvasToUse.parentElement.clientWidth – canvasFireElement.width) / 2)+ »px »;

var refValue = idealWidth;

if (localStream){

if (!init

&& canvasToUse.width == Math.round(refValue)

&& canvasToUse.height == Math.round(refValue)

&& canvasFireElement.width == Math.round(refValue)

&& canvasFireElement.height == Math.round(refValue)){

if (canvasFireElement.width != 100){

init = true;

canvasDemo.canvas = document.getElementById(‘canvasFireLocalVideo’);

canvasDemo.init();

}

}

if (init){

canvasDemo.refresh();

}

}

window.requestAnimationFrame(snapshot);

}

Le code ajouté nous permet d’initialiser graphiquement le canvas et de demander de piloter les rafraîchissements du mur de flammes à partir de notre application. Nous allons donc devoir faire une modification dans le fichier canvasFire.js : ajouter une méthode de rafraîchissement et supprimer l’appel au requestAnimationFrame :

this.refresh = function(){

update();

}

// main render loop

var update = function()

{

smooth();

draw();

frames++;

//requestAnimFrame(function() { update(); });

};

Etape 6 : ajouter un cercle de feux

Le principe : créer un cercle à partir d’un carré

Le principe pour le mur de flammes est simple : le projet html5-canvas-demo nous fournit un seul mur de flammes, hors nous, nous voulons 1 cercle. Nous allons nous y prendre en plusieurs étapes.

  1. création d’un mur de flammes sur chacun des axes cardinaux. De cette façon nous aurons des flammes partout autour de notre image
  2. Mise en place un masque ovale afin de restreindre la zone affichant les flammes
  3. Rotation de ces flammes pour leur donner plus de mouvement et se rapprocher du rendu du jeu Portal.
  4. Enfin, autorisation d’une deuxième couleur car chaque portail possède sa propre couleur (bleu et orange)

Pour rappel, le projet initial fonctionne de la façon suivante : à chaque fois qu’il peut dessiner (window.requestAnimationFrame), on dessine une image de particules de flammes grâce à la méthode drawImage du context du canvas.

Des flammes selon les axes cardinaux

Afin d’afficher les flammes selon les axes nous allons créer une fonction qui nous permet d’afficher pour un angle donné une image de flamme dans le fichier canvasFire.js.

var drawAngle = function(angleDegree){

var rad = angleDegree * Math.PI / 180;

var fullWidth = width * scale;

var fullHeight = height * scale;

context.translate(fullWidth / 2, fullHeight / 2);

context.rotate(rad);

context.translate(-fullWidth / 2, -fullHeight / 2);

var trY = Math.abs(Math.cos(rad))*((fullHeight – dims.height) / 2);

context.translate(0,-trY);

context.drawImage(buffer, 0, 0, width * scale, height * scale);

// On doit revenir en arriere sur le mouvement pour traiter un nouvel angle

context.translate(0,trY);

context.translate(fullWidth / 2, fullHeight / 2);

context.rotate(-rad);

context.translate(-fullWidth / 2, -fullHeight / 2);

}

Cette méthode applique une rotation du context du canvas avant de dessiner la flamme. Une des choses importante dans cette méthode est le retour à la position initiale !  C’est très important pour pouvoir traiter un nouvel angle !

Nous devons maintenant appeler cette méthode là où le drawImage d’origine était effectué, à savoir dans la méthode “draw”. Le contenu de cette fonction sera désormais le suivant :

// draw colormap->palette values to screen

var draw = function()

{

// render the image data to the offscreen buffer…

bufferContext.putImageData(imageData, 0, 0);

// …then draw it to scale to the onscreen canvas

// Image de base en bas !

drawAngle(0);

drawAngle(90);

drawAngle(180);

drawAngle(270);

};

Il faut également ajouter les dimensions de l’image de destination afin de calculer les bons angles.

var dims = {};

this.canvas = undefined;

this.init = function(dim)

{

context = this.canvas.getContext(‘2d’);

width = Math.round(this.canvas.width / scale);

height = Math.round(this.canvas.height / scale);

 

if (dim){

dims = dim;

}else{

dims = {width: width, height : height};

}

colorMap = Array(width * height);

 

for(var i = 0; i < colorMap.length; i++)

colorMap[i] = 0;

initPalette();

initBuffer();

update();

};

Il faut enfin modifier la façon d’afficher les pixels afin de forcer un affichage de pixels transparents ! En effet, actuellement seul le dernier canvas est affiché. Il faut donc modifier la fonction drawPixel comme suit :

// set pixels in imageData

var drawPixel = function(x, y, color)

{

var offset = (x + y * imageData.width) * 4;

imageData.data[offset] = color[0];

imageData.data[offset + 1] = color[1];

imageData.data[offset + 2] = color[2];

if (color[0] <= 100 && color[1] === 0 && color[2] <= 100){

imageData.data[offset + 3] = 0;

}else{

imageData.data[offset + 3] = 255;

}

};

 

Comme nous avons changé la méthode init de canvasFire.js. Nous devons donc changer l’appel à cette méthode dans app.js. Ainsi dans la méthode snapshot, il faut remplacer :

canvasDemo.init();

par

canvasDemo.init({width :minVideoWidth, height : idealHeight + 100});

Nous constatons que nous allons bien afficher 4 murs de flammes autour de notre vidéo.

Mise en place des masques

Dans l’application finale, la vidéo et les flammes s’affichent dans un ovale. Nous allons utiliser pour ce faire une fonction des canvas : clip. Cette dernière nous permettra de définir une zone ovale dans le canvas et nous y dessinerons notre vidéo et nos flammes. Plus précisément, nous allons dessiner un ovale de flammes et par-dessus, nous dessinerons un ovale contenant l’image de la vidéo. Voici donc la version presque finale de la méthode snapshot dans le fichier app.js.

function snapshot(){

var canvasToUse = canvasRemoteElement;

var contextToUse = ctxRemote;

var videoToUse = remoteVideo;

canvasRemoteElement.width = remoteVideo.videoWidth;

canvasRemoteElement.height = remoteVideo.videoHeight;

if (remoteStream){

ctxRemote.drawImage(remoteVideo, 0,0);

}

var delta = 50;

var idealWidth = Math.min(canvasToUse.parentElement.clientWidth, videoToUse.videoWidth + 100);

var minVideoWidth = Math.min(canvasToUse.parentElement.clientWidth – 50, videoToUse.videoWidth);

var ratio = videoToUse.videoWidth / videoToUse.videoHeight;

var idealHeight = Math.min(idealWidth / ratio, videoToUse.videoHeight);

var useVideoWidth = idealWidth === videoToUse.videoWidth + 100;

canvasToUse.width = idealWidth;

canvasToUse.height = canvasToUse.width;

canvasToUse.style.top = ((canvasToUse.parentElement.clientHeight – canvasToUse.height) / 2)+ »px »;

canvasToUse.style.left = ((canvasToUse.parentElement.clientWidth – canvasToUse.width) / 2)+ »px »;

canvasFireElement.width = idealWidth;

canvasFireElement.height = canvasFireElement.width;

canvasFireElement.style.top = ((canvasToUse.parentElement.clientHeight – canvasFireElement.height) / 2)+ »px »;

canvasFireElement.style.left = ((canvasToUse.parentElement.clientWidth – canvasFireElement.width) / 2)+ »px »;

 

var refValue = idealWidth;

if (localStream){

if (!init

&& canvasToUse.width == Math.round(refValue)

&& canvasToUse.height == Math.round(refValue)

&& canvasFireElement.width == Math.round(refValue)

&& canvasFireElement.height == Math.round(refValue)){

if (canvasFireElement.width != 100){

init = true;

canvasDemo.canvas = document.getElementById(‘canvasFireLocalVideo’);

canvasDemo.init({width :minVideoWidth, height : idealHeight + 100});

}

}

var deltaX = 0, deltaY = 0;

ctxFire.save();

ctxFire.beginPath();

deltaX =  (canvasFireElement.width – minVideoWidth) / 2;

deltaY =  (canvasFireElement.height- idealHeight) / 2;

ctxFire.fillStyle = « rgba(0, 0, 0, 0) »;

drawEllipse(ctxFire, deltaX, deltaY, minVideoWidth, idealHeight);

// Clip to the current path

ctxFire.clip();

// Undo the clipping

if (init){

canvasDemo.refresh();

}

ctxFire.restore();

// Save the state, so we can undo the clipping

contextToUse.save();

contextToUse.beginPath();

deltaX =  (canvasToUse.width – minVideoWidth +delta) / 2;

deltaY =  (canvasToUse.height – idealHeight+delta) / 2;

contextToUse.fillStyle = « rgba(0, 0, 0, 0) »;

drawEllipse(contextToUse, deltaX , deltaY, minVideoWidth-delta , idealHeight-delta);

// Clip to the current path

contextToUse.clip();

contextToUse.drawImage(videoToUse,0,0, videoToUse.videoWidth, videoToUse.videoHeight, deltaX, deltaY, minVideoWidth, idealHeight);

// Undo the clipping

contextToUse.restore();

 

}

window.requestAnimationFrame(snapshot);

}

Il faut donc ajouter la fonction :

function drawEllipse(ctx, x, y, w, h) {

var kappa = .5522848,

ox = (w / 2) * kappa, // control point offset horizontal

oy = (h / 2) * kappa, // control point offset vertical

xe = x + w,           // x-end

ye = y + h,           // y-end

xm = x + w / 2,       // x-middle

ym = y + h / 2;       // y-middle

 

ctx.beginPath();

ctx.moveTo(x, ym);

ctx.bezierCurveTo(x, ym – oy, xm – ox, y, xm, y);

ctx.bezierCurveTo(xm + ox, y, xe, ym – oy, xe, ym);

ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);

ctx.bezierCurveTo(xm – ox, ye, x, ym + oy, x, ym);

ctx.closePath();

ctx.stroke();

}

Nous pouvons donc voir que l’on dessine des Ellipses dans lesquelles nous faisons nos affichages de flammes suivis de l’affichage de la vidéo.

Rotation des flammes

La gestion de la rotation des flammes se fait dans le fichier canvasFire.js. Précédemment nous avions défini l’affichage du mur de flammes selon 4 axes cardinaux. Nous allons donc simplement faire évoluer ces angles au fil du temps pour mettre en place la rotation.

Commençons par ajouter de nouvelles variables globales :

var timeOut = 7;

var angleInc = 360 / (timeOut * 100);

var angle = 0;

var lastTime = 0;

Nous allons simplement mettre à jour le contenu de la fonction draw pour faire évoluer les angles :

// draw colormap->palette values to screen

var draw = function()

{

// render the image data to the offscreen buffer…

bufferContext.putImageData(imageData, 0, 0);

// …then draw it to scale to the onscreen canvas

// Image de base en bas !

 

var timeStamp = new Date().getTime();

angle += angleInc * ((lastTime – timeStamp) / 100);

angle = angle % 360;

lastTime = timeStamp;

 

drawAngle(angle);

drawAngle(angle+90);

drawAngle(angle+180);

drawAngle(angle+270);

 

};

Maintenant notre cercle de flammes évolue en tournant.

Ajout d’une autre couleur

Afin de compléter comme il se doit notre portal, nous devons donc ajouter une deuxième couleur ! Dans le fichier canvasFire.js, la définition de la couleur utilisée se fait dans la méthode initPalette();.

// init palette from warm to white hot colors

var initPalette = function()

{

palette = Array(256);

 

for(var i = 0; i < 64; i++)

{

if (color === ‘red’){

palette[i] = [(i << 2), 0, 0];

palette[i + 64] = [255, (i << 2), 0];

palette[i + 128] = [255, 255, (i << 2)];

palette[i + 192] = [255, 255, 255];

}else if (color === ‘blue’) {

 

palette[i] = [0, 0, (i << 2)];

palette[i + 64] = [0, (i << 2), 255];

palette[i + 128] = [0, 128, 100+(i << 2)];

palette[i + 192] = [0, 128, 255];

}

}

};

Nous allons donc modifier son appel dans la méthode init et ajouter la variable globale color.

var color = ‘red’;

 

this.init = function(colorToApply, dim)

{

if (colorToApply){

color = colorToApply;

}

context = this.canvas.getContext(‘2d’);

 

width = Math.round(this.canvas.width / scale);

height = Math.round(this.canvas.height / scale);

 

if (dim){

dims = dim;

}else{

dims = {width: width, height : height};

}

 

colorMap = Array(width * height);

 

for(var i = 0; i < colorMap.length; i++)

colorMap[i] = 0;

 

initPalette();

initBuffer();

 

update();

};

Comme vous le constatez, nous avons fait évoluer la signature de la méthode init. Ceci veut dire que nous allons faire notre ultime modification dans le fichier app.js dans la méthode snapshot(). Nous allons remplacer :

canvasDemo.init({width :minVideoWidth, height : idealHeight + 100});

par :

canvasDemo.init(isInitiator ? ‘red’ : ‘blue’, {width :minVideoWidth, height : idealHeight + 100});

Nous en avons fini avec le code !

Etape 7 : Fin

 

Il ne vous reste plus qu’à projeter le résultat de l’application sur 2 murs différents avec une webcam de chaque côté pour filmer le tout. Le rêve devient réalité, et vous êtes fin prêt à concurrencer Homer Simpson, confortablement installé dans votre salon. A vous de jouer et de créer votre propre Portal !

Crédits :

Tout le code source est disponible ici : https://github.com/GDG-Nantes/portal-devfest-2013

Vous pouvez retrouver cet article sur le blog de l’auteur : http://jef.binomed.fr

Jean François Garreau

Consultant Expert Innovation SQLI Nantes

0 commentaires

votre commentaire

Se joindre à la discussion ?
Vous êtes libre de contribuer !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Inscription newsletter

Ne manquez plus nos derniers articles !