Comment refaire l'interface de Yahoo Pipes
Depuis que j'ai découvert Yahoo Pipes, je me demandais comment ils avaient pu faire une interface aussi chouette en html. Ce qui me posait le plus de pb c'était bien sur les jolis câbles de liaisons entre les boites, d'autant que je n'avait aucune idée de comment on pouvait faire sa en html. J'ai donc décidé de m'y mettre, de comprendre et de le refaire.
Pour commencer, j'ai pris mon firebug et j'ai exploré (inspecté !) les objets affichés pour trouver de quoi était fait le fameux lien.En fait tout est là : ce câble est un <canvas>
Mais qu'est-ce donc qu'un canvas ?
Un canvas est un nouveau élément HTML qui permet de manipuler des graphiques en bitmap. Le canvas est intégré au draft du HTML 5 et a été originellement proposé par apple et son safari. Il est supporté nativement par firefox depuis la 1.5 et n'est pas encore supporté par IE (mais il y a moyen de le faire marcher sous IE, on en parlera plus bas)Je ne vais pas refaire le tuto de canvas, vous pouvez suivre ce tuto sur le site de mozilla, il est très bien fait.
Les 'wires' de yahoo
Mon but ici est de refaire le système de câbles entre deux boites comme dans yahoo pipes.J'ai choisit d'utiliser mootools comme librairie javascript, parce que j'aime bien mootools. Cela permet de ne pas perdre de temps sur la gestion des box et des drag&drop.
Donc j'ai simplement commencé par définir un bout d'html avec un container, deux boites et le fameux canvas :
<body onload = "start();">
<div id="containment">
<canvas id="lien"></canvas>
<div id="boite1">
1
</div>
<div id="boite2">
2
</div>
</div>
</body>
<div id="containment">
<canvas id="lien"></canvas>
<div id="boite1">
1
</div>
<div id="boite2">
2
</div>
</div>
</body>
le css qui va avec (rien de compliqué):
<style type="text/css">
#containment {
height: 500px;
width: 600px;
background: #ddd;
position: relative;
}
#boite1 {
height: 60px;
width: 40px;
border: 2px solid black;
background: #39f;
color: white;
position: absolute;
top: 50;
left: 50;
}
#boite2 {
height: 55px;
width: 40px;
border: 2px solid black;
background: blue;
color: white;
position: absolute;
top: 250;
left: 250;
}
canvas {
/*border: 1px solid red;*/
position: absolute;
top: 0;
left: 0;
}
</style>
#containment {
height: 500px;
width: 600px;
background: #ddd;
position: relative;
}
#boite1 {
height: 60px;
width: 40px;
border: 2px solid black;
background: #39f;
color: white;
position: absolute;
top: 50;
left: 50;
}
#boite2 {
height: 55px;
width: 40px;
border: 2px solid black;
background: blue;
color: white;
position: absolute;
top: 250;
left: 250;
}
canvas {
/*border: 1px solid red;*/
position: absolute;
top: 0;
left: 0;
}
</style>
Ensuite un peu de code mootools pour rendre les boites draggables :
function start() {
var container = $('containment');
new Drag.Move('boite1', {'container': container});
new Drag.Move('boite2', {'container': container});
$('boite1').addEvent('mousemove', function() {
replaceLink();
});
$('boite2').addEvent('mousemove', function() {
replaceLink();
});
replaceLink();
}
var container = $('containment');
new Drag.Move('boite1', {'container': container});
new Drag.Move('boite2', {'container': container});
$('boite1').addEvent('mousemove', function() {
replaceLink();
});
$('boite2').addEvent('mousemove', function() {
replaceLink();
});
replaceLink();
}
Donc tout sera fait dans la fonction 'replaceLink'
Cette fonction est en fait relativement simple :
Elle fait (dans l'ordre) :
- calculer les coordonnée du point de départ et du point d'arrivée du trait : dans cet exemple le point de départ est collé au bas de la boite 1 et le point d'arrivée en haut de la boite 2.
pointA_x = $('boite1').getStyle('left').toInt()+($('boite1').getStyle('width').toInt())/2;
pointA_y = $('boite1').getStyle('top').toInt()+($('boite1').getStyle('height').toInt());
pointB_x = $('boite2').getStyle('left').toInt()+($('boite2').getStyle('width').toInt())/2;
pointB_y = $('boite2').getStyle('top').toInt(); //+($('boite2').getStyle('height').toInt())/2;
pointA_y = $('boite1').getStyle('top').toInt()+($('boite1').getStyle('height').toInt());
pointB_x = $('boite2').getStyle('left').toInt()+($('boite2').getStyle('width').toInt())/2;
pointB_y = $('boite2').getStyle('top').toInt(); //+($('boite2').getStyle('height').toInt())/2;
- Traiter correctement le point de départ et d'arrivée en fonction de la position relative des boites l'une par rapport à l'autre (notamment quand la boite 2 se retrouve au dessus de la 1, etc..)
lien_width = Math.abs(pointB_x - pointA_x);
lien_x = Math.min(pointA_x, pointB_x);
lien_height = Math.abs(pointB_y - pointA_y);
lien_y = Math.min(pointA_y,pointB_y);
lien_x = Math.min(pointA_x, pointB_x);
lien_height = Math.abs(pointB_y - pointA_y);
lien_y = Math.min(pointA_y,pointB_y);
- Préparer le contexte
var canvas = $('lien');
if (canvas.getContext){
var ctx = canvas.getContext('2d');
// drawing code here
ctx.lineCap = 'round';
if (canvas.getContext){
var ctx = canvas.getContext('2d');
// drawing code here
ctx.lineCap = 'round';
- Effacer l'image précédente
ctx.clearRect(0,0,600,500);
- Dessiner un trait qui part bien du point de départ et qui arrive bien au point d'arrivée : ceci est fait en utilisant les fonctions 'basiques' du canvas : MoveTo et soit LineTo soit bezierCurveTo si on veut un trait tout arrondi.
Donc en fait c'est très simple.
Là où on passe un peu de temps c'est vraiment le rendre joli.
Au début, j'ai voulu adapter la taille du <canvas> au rectangle dont les coins sont formés par le point de départ du trait et le point d'arrivée (c'est ce qui me paraissait le plus logique). Cela fonctionne bien, me permet de travailler avec toujours le même système de coordonnées, mais le pb est que mon trait était alors rescallé automatiquement à la nouvelle taille du canvas (ce qui semble être un comportement standard). [cliquez ici pour voir ce que cela donnait en live : éloignez les boites pour le remarquer. (attention, cette version ne marche pas bien sous IE)]
Yahoo semble pourtant utiliser cette technique, mais je n'ai pas réussit à la faire fonctionner, j'ai donc changé d'angle d'approche : j'ai arrêté de resizer mon canvas, je l'ai directement initalisé à la taille du conteneur et ensuite j'ai joué avec la translation :
ctx.save();
ctx.translate(lien_x, lien_y)
ctx.translate(lien_x, lien_y)
Cela me permet d'avoir toujours le point (0,0) en haut à gauche du rectangle formé par le point de départ et le point d'arrivée.
Voici deux cas de figure en schéma :


- Ensuite il ne reste plus qu'à dessiner :
la suite de la fonction est simplement là pour gérer les 4 cas de figure de la position relative de la boîte 1 par rapport à la boite 2. Cela ne sert finalement qu'à donner des bons paramètres à la courbe de bézier pour que cela soit joli.
Il y a un autre élément où j'ai eu un peu de mal c'est pour faire en sorte que le trait ait un bord (plus foncé que l'intérieur dans l'exemple). J'ai essayé de jouer avec les propriétés border, mais c'est en fait beaucoup plus simple :
il faut faire un trait foncé plus gros, puis un trait plus clair et plus fin qui va se superposer, tout simplement.
Cette partie du code est optimisable (on peut la rendre plus compacte), mais je l'ai volontairement laissé tel quel pour gagner en lisibilité.
ctx.beginPath();
m_a = 100; // offset pour les beziers
if (pointA_x < pointB_x) {
if (pointA_y < pointB_y) {
// coin en haut à gauche
ctx.lineWidth = 12;
ctx.strokeStyle = '#07c';
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0,m_a, lien_width, lien_height-m_a, lien_width, lien_height);
ctx.stroke();
ctx.lineWidth = 8;
ctx.strokeStyle = '#09f';
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0,m_a, lien_width, lien_height-m_a, lien_width, lien_height);
ctx.stroke();
} else {
// coin en bas à gauche
ctx.lineWidth = 12;
ctx.strokeStyle = '#07c';
ctx.moveTo(lien_width, 0);
ctx.bezierCurveTo(lien_width, -m_a, 0, lien_height+m_a, 0, lien_height);
ctx.stroke();
ctx.lineWidth = 8;
ctx.strokeStyle = '#09f';
ctx.moveTo(lien_width, 0);
ctx.bezierCurveTo(lien_width, -m_a, 0, lien_height+m_a, 0, lien_height);
ctx.stroke();
}
} else {
if (pointA_y < pointB_y) {
// coin en haut à droite
ctx.lineWidth = 12;
ctx.strokeStyle = '#07c';
ctx.moveTo(lien_width, 0);
ctx.bezierCurveTo(lien_width, m_a, 0, lien_height-m_a, 0, lien_height);
ctx.stroke();
ctx.lineWidth = 8;
ctx.strokeStyle = '#09f';
ctx.moveTo(lien_width, 0);
ctx.bezierCurveTo(lien_width, m_a, 0, lien_height-m_a, 0, lien_height);
ctx.stroke();
} else {
// coin en bas à droite
ctx.lineWidth = 12;
ctx.strokeStyle = '#07c';
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0,-m_a, lien_width, lien_height+m_a, lien_width, lien_height);
ctx.stroke();
ctx.lineWidth = 8;
ctx.strokeStyle = '#09f';
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0,-m_a, lien_width, lien_height+m_a, lien_width, lien_height);
ctx.stroke();
}
}
ctx.restore();
m_a = 100; // offset pour les beziers
if (pointA_x < pointB_x) {
if (pointA_y < pointB_y) {
// coin en haut à gauche
ctx.lineWidth = 12;
ctx.strokeStyle = '#07c';
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0,m_a, lien_width, lien_height-m_a, lien_width, lien_height);
ctx.stroke();
ctx.lineWidth = 8;
ctx.strokeStyle = '#09f';
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0,m_a, lien_width, lien_height-m_a, lien_width, lien_height);
ctx.stroke();
} else {
// coin en bas à gauche
ctx.lineWidth = 12;
ctx.strokeStyle = '#07c';
ctx.moveTo(lien_width, 0);
ctx.bezierCurveTo(lien_width, -m_a, 0, lien_height+m_a, 0, lien_height);
ctx.stroke();
ctx.lineWidth = 8;
ctx.strokeStyle = '#09f';
ctx.moveTo(lien_width, 0);
ctx.bezierCurveTo(lien_width, -m_a, 0, lien_height+m_a, 0, lien_height);
ctx.stroke();
}
} else {
if (pointA_y < pointB_y) {
// coin en haut à droite
ctx.lineWidth = 12;
ctx.strokeStyle = '#07c';
ctx.moveTo(lien_width, 0);
ctx.bezierCurveTo(lien_width, m_a, 0, lien_height-m_a, 0, lien_height);
ctx.stroke();
ctx.lineWidth = 8;
ctx.strokeStyle = '#09f';
ctx.moveTo(lien_width, 0);
ctx.bezierCurveTo(lien_width, m_a, 0, lien_height-m_a, 0, lien_height);
ctx.stroke();
} else {
// coin en bas à droite
ctx.lineWidth = 12;
ctx.strokeStyle = '#07c';
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0,-m_a, lien_width, lien_height+m_a, lien_width, lien_height);
ctx.stroke();
ctx.lineWidth = 8;
ctx.strokeStyle = '#09f';
ctx.moveTo(0, 0);
ctx.bezierCurveTo(0,-m_a, lien_width, lien_height+m_a, lien_width, lien_height);
ctx.stroke();
}
}
ctx.restore();
Fonctionnement avec IE
IE ne supporte pas nativement les canvas (même pas IE7). Pour contourner ce pb, des gars de chez google ont eu l'excellente idée et le talent d'utiliser le support natif de VML dans IE pour émuler les canvas. Il suffit d'inclure cette librairie pour faire fonctionner canvas sous IE, pourquoi s'en priver..Améliorations et Optimisations
Je ne suis pas complètement satisfait des perfomances de cette démo, notamment quand on bouge la souris très vite. Yahoo n'a pas ce pb.A première vue, je supposerait que c'est du au clearRect que je fait pour effacer les dessins précédent, mais je n'ai pas trouvé d'autre solution pour l'instant. Si vous avez des idées, n'hésitez pas à les proposer en commentant cet article.
Démo et téléchargement
Ci-dessous pour télécharger le sources (incluant mootools 1.11 et Exanvas 0002) :
| Filename | Filesize | Date | |
|---|---|---|---|
| . testcanvas.zip | 55.95 kB | 2008-08-11 | |
Ecrivez un commentaire
- Les champs obligatoires sont marques d'une *.
jm
Posts: 2
Posts: 2
Reply #1 on : Mon June 25, 2007, 22:11:36
![[-]](assets/templates/perso/images/fleche_haut.gif)
![[+]](assets/templates/perso/images/fleche_bas.gif)
![[RSS]](assets/templates/perso/images/rss.gif)
![[RSS]](assets/templates/perso/images/rss_small.gif)
Posts: 2
Reply #2 on : Fri August 03, 2007, 15:04:07