Partie 1: Application Pictionnary

De $1

Objectifs

Nous allons survoler les bases du développement d'une application HTML5, et développer une application similaire à l'application Draw Something, compatible avec le web mobile.

Cette partie va durer 3 séances de 3h. 

On développe la base de votre premier projet à rendre, l'application Pictionnary Ansynchrone.
Si vous faites proprement tout ce qui est demandé ici, vous aurez au moins 6/20 au projet.

Concepts abordés:

  • Formulaire d'inscription/connexion HTML5,
  • adaptation au web mobile,
  • élément canvas
  • exportation d'un canvas en image
  • format d'échange JSON

Technologies utilisées:

PHP, MySQL, HTML5, CSS3, javascript

Prenez l'habitude d'utiliser les outils de développement de votre navigateur pour étudier le DOM, les échanges réseau, le javascript, le CSS, ...

Mise en route

Discussion:  Qu'est-ce que le HTML 5 ? 

On va aborder un ensemble de concepts nouveau dans le HTML5. 

A faire: Survoler la liste sur le site http://html5test.com/

Dans la partie 1 du cours on va se concentrer sur:

  • Parsing Rules
  • Elements
    • Embedding custom non-visible data
    • Section elements 
    • Text level elements: download, ping, mark, ruby
    • Interactive Elements: Details, Summary
  • Forms
    • nouveaux champs de formulaire, validation, ...
  • 2D graphics
    • canvas
    • Image export format
  • Other
    • JSON encoding and decoding

Discussion:  Quels autres points de la liste connaissez vous ? vous intriguent ?

 

Soyez curieux, demandez, ou cherchez sur le net.

Parsing Rules, headers, balises meta

A faire: Validez chacun de ces documents html dans le validateur du w3c: http://validator.w3.org/#validate_by_input

Corrigez les erreurs.

Prenez l'habitude de valider vos documents HTML.

HTML 4.01

<!doctype html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="fr">
   <head>
      <meta http-equiv="content-type" content="text/html; charset=UTF-8">
      <title>My first HTML document</title>
      <meta name="description" content="Exemple de description">
      <link rel="stylesheet" type="text/css" href="monStyle.css">
      <script type="text/javascript" src="monSript.js"></script>
   </head>
   <body>
      <div>...</div>
   </body>
</html>
 
 
HTML5
<!doctype html>
<html lang="fr">
   <head>
      <meta charset="UTF-8">
      <title>My first HTML document</title>
      <meta name="description" content="Exemple de description">
      <link rel="stylesheet" href="monStyle.css"/>
      <script src="monSript.js"></script>
   </head>
   <body>
      <div>...</div>
   </body>
</html>
 
Discussion: Commentaires ? Des différences importantes ? 
 
Cours: Dans le document à l'url http://www.slow-lab.com/guide-balises-meta.php, lisez les sections suivantes: 
  • Tout jusqu'à: Les balises Meta de partage Facebook
  • Les balises Meta Language et Content-language
  • À Quoi Ressemble Un <Head> Propre?
 
Ne vous attardez pas trop dans les détails, en particulier pour les balises meta de partage Facebook: on y reviendra en détail plus tard...
 
Discussion: Commentaires ? Une idée de ce que peut bien être le web sémantique ? 

Elements

A faire: Téléchargez le projet demoHTML5 ci-joint. Exécutez et étudier les sources de chaque fichier HTML.

1_customNonVisibleData:

Etudiez le document à l'url http://www.alsacreations.com/article/lire/1397-html5-attribut-data-dataset.html 

2_SectionElements:

Etudiez le document à l'url http://www.alsacreations.com/article/lire/1376-html5-section-article-nav-header-footer-aside.html 

3_textLevelElements:

Comprenez l'utilité:

  • de l'élément mark
  • des attributs download et ping
  • des éléments ruby 

4_InteractiveElements:

Etudiez le document à l'url http://www.alsacreations.com/article...s-summary.html  

Formulaire d'inscription

La première étape pour notre projet est une de connexion sur notre site.

Cours: Pour les formulaires HTML5, une introduction peut être lue à la page suivante: http://standardista.com/forms/. Vous vous inspirerez ensuite des documents suivants

page inscription.php

Le formulaire d'inscription doit comporter les champs suivants:

adresse email  type email, obligatoire, a le focus par défaut
mot de passe  type password, obligatoire, entre 4 et 8 caractères alphanumériques (validation avec l'attribut pattern)
confirmation du mot de passe  type password, obligatoire, doit être égal au premier champs mot de passe (validation avec l'api validation)
nom  type text, optionnel
prénom  type text, obligatoire
téléphone  type tel, optionnel
site web  type url, optionnel
sexe  type radio button, optionnel
date de naissance  type date, obligatoire
age  type number, disabled, doit être calculé lorsque la date de naissance change
ville  type text, optionnel
taille  type range, de 0m à 2.50m, avec un pas de 0.01m.
couleur préférée  type color, noir par défaut (attribut value)
photo de profil  type file. 

 

Prenez comme base le code suivant. Comme vous pouvez le voir, 

  • Chaque champs est un item d'une liste
  • Chaque champs est précédé d'un élément label associé (lorsqu'on clique sur un label, le champs associé reçoit le focus), et peut être suivi d'une indication sur le format attendu.

Etudiez le code, répondez aux questions, complétez le code comme demandé.

Puis, ajoutez les champs simples tels que nom, prénom, ville, taille, couleur, etc.
Laissez de côté pour le moment les champs un peu plus compliqués que sont:

  • les mots de passes;
  • la date de naissance et le calcul de l'âge
  • la photo de profil

Ces champs font l'objet des trois sections suivantes.

<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8 />
    <title>Pictionnary - Inscription</title>
</head>
<body>

<h2>Inscrivez-vous</h2>
<form class="inscription" action="req_inscription.php" method="post" name="inscription">
    <!-- c'est quoi les attributs action et method ? -->
    <!-- qu'y a-t-il d'autre comme possiblité que post pour l'attribut method ? -->
    <span class="required_notification">Les champs obligatoires sont indiqués par *</span>
    <ul>
        </li>
        <li>
            <label for="email">E-mail :</label>
            <input type="email" name="email" id="email"/>
            <!-- ajouter à input l'attribut qui lui donne le focus automatiquement -->
            <!-- ajouter à input l'attribut qui dit que c'est un champs obligatoire -->
            <!-- quelle est la différence entre les attributs name et id ? -->
            <!-- c'est lequel qui doit être égal à l'attribut for du label ? --> 
            <span class="form_hint">Format attendu "name@something.com"</span>
        </li>
        <li>
            <label for="prenom">Prénom :</label>
            <input type="text" name="prenom" id="prenom"/>
            <!-- ajouter à input l'attribut qui dit que c'est un champs obligatoire -->
            <!-- ajouter à input l'attribut qui donne une indication grisée (placeholder) -->
        </li>
        <li>
            <input type="submit" value="Soumettre Formulaire">
        </li>
    </ul>
</form>
</body>
</html>
     

Validation des mots de passe

Utilisez le code suivant pour les deux champs mot de passe. Etudiez le code, répondez aux questions, complétez le code. En particulier:

  • Il y a un petit bug dans ce code: lorsqu'on clique sur "confirmez mot de passe", c'est le premier champs mot de passe qui reçoit le focus. corrigez celà.  
  • Pour choisir l'expression régulière javascript pour l'attribut pattern, inspirez vous du document: https://developer.mozilla.org/fr/docs/JavaScript/Guide/Expressions_r%C3%A9guli%C3%A8res
  • La vérification de l'égalité des mots de passe est effectuée directement avec l'API validation, lors d'un keyup sur l'un des deux champs. Pourquoi sur les deux champs ?
<li>
            <label for="mdp1">Mot de passe :</label>
            <input type="password" name="password" id="mdp1" pattern="regex" onkeyup="validateMdp2()" title = "Le mot de passe doit contenir de 6 à 8 caractères alphanumériques.">
            <!-- ajouter à input l'attribut qui dit que c'est un champs obligatoire -->
            <!-- ajouter à input l'attribut qui donne une indication grisée (placeholder) -->
            <!-- spécifiez l'expression régulière: le mot de passe doit être composé de 6 à 8 caractères alphanumériques -->
            <!-- quels sont les deux scénarios où l'attribut title sera affiché ? -->
            <!-- encore une fois, quelle est la différence entre name et id pour un input ? -->
            <span class="form_hint">De 6 à 8 caractères alphanumériques.</span>
        </li>
        <li>
            <label for="mdp2">Confirmez mot de passe :</label>
            <input type="password" id="mdp2" required onkeyup="validateMdp2()">
            <!-- ajouter à input l'attribut qui dit que c'est un champs obligatoire -->
            <!-- ajouter à input l'attribut qui donne une indication grisée (placeholder) -->
            <!-- pourquoi est-ce qu'on a pas mis un attribut name ici ? -->
            <!-- quel scénario justifie qu'on ait ajouté l'écouter validateMdp2() à l'évènement onkeyup de l'input mdp1 ? -->
            <span class="form_hint">Les mots de passes doivent être égaux.</span>
            <script>
                validateMdp2 = function(e) {
                    var mdp1 = document.getElementById('mdp1');
                    var mdp2 = document.getElementById('mdp2');
                    if (/* est-ce que mdp1 est valide ? */ && /* est-ce que mdp1 et mdp2 ont la même valeur ? */) {
                        // ici on supprime le message d'erreur personnalisé, et du coup mdp2 devient valide.
                        document.getElementById('mdp2').setCustomValidity('');
                    } else {
                        // ici on ajoute un message d'erreur personnalisé, et du coup mdp2 devient invalide.
                        document.getElementById('mdp2').setCustomValidity('Les mots de passes doivent être égaux.');
                    }
                }
            </script>

 Calcul de l'âge

On veut parser la valeur du champs birthdate en date, comparer avec la date d'aujourd'hui, et récupérer un nombre d'années écoulées.
Utilisez le code suivant pour les champs birthdate et age. Etudiez le code, répondez aux questions, complétez le code. En particulier:

<li>
            <label for="birthdate">Date de naissance:</label>
            <input type="date" name="birthdate" id="birthdate" placeholder="JJ/MM/AAAA" required onchange="computeAge()"/>
            <script>
                computeAge = function(e) {
                    try{
                        // j'affiche dans la console quelques objets javascript, ce qui devrait vous aider.
                        console.log(Date.now());
                        console.log(document.getElementById("birthdate"));
                        console.log(document.getElementById("birthdate").valueAsDate);
                        console.log(Date.parse(document.getElementById("birthdate").valueAsDate));
                        console.log(new Date(0).getYear());
                        console.log(new Date(65572346585).getYear());
                        // modifier ici la valeur de l'élément age
                    } catch(e) {
                        // supprimez ici la valeur de l'élément age
                    }
                }
            </script>
            <span class="form_hint">Format attendu "JJ/MM/AAAA"</span>
        </li>
        <li>
            <label for="age">Age:</label>
            <input type="number" name="age" id="age" disabled/>
            <!-- à quoi sert l'attribut disabled ? -->
        </li>

 

Photo de profil

Pour la photo de profil, on décide de charger l'image, de la redimensionner pour éviter de trop envoyer au serveur, et de stocker l'image dans le formulaire sous la forme d'une data url.

Si vous ne savez pas ce qu'est une data url, survolez le document suivant: http://www.alsacreations.com/article/lire/1439-data-uri-schema.html

Utilisez le code suivant pour le champs file. Etudiez le code, les explications, et écrivez le bout de code demandé.

<li>
            <label for="profilepicfile">Photo de profil:</label>
            <input type="file" id="profilepicfile" onchange="loadProfilePic(this)"/>
            <!-- l'input profilepic va contenir le chemin vers l'image sur l'ordinateur du client -->
            <!-- on ne veut pas envoyer cette info avec le formulaire, donc il n'y a pas d'attribut name -->
            <span class="form_hint">Choisissez une image.</span>
            <input type="hidden" name="profilepic" id="profilepic"/>
            <!-- l'input profilepic va contenir l'image redimensionnée sous forme d'une data url --> 
            <!-- c'est cet input qui sera envoyé avec le formulaire, sous le nom profilepic -->
            <canvas id="preview" width="0" height="0"></canvas>
            <!-- le canvas (nouveauté html5), c'est ici qu'on affichera une visualisation de l'image. -->
            <!-- on pourrait afficher l'image dans un élément img, mais le canvas va nous permettre également 
            de la redimensionner, et de l'enregistrer sous forme d'une data url-->
            <script>
                loadProfilePic = function (e) {
                    // on récupère le canvas où on affichera l'image
                    var canvas = document.getElementById("preview");
                    var ctx = canvas.getContext("2d");
                    // on réinitialise le canvas: on l'efface, et déclare sa largeur et hauteur à 0
                    ctx.setFillColor("white");
                    ctx.fillRect(0,0,canvas.width,canvas.height);
                    canvas.width=0;
                    canvas.height=0;
                    // on récupérer le fichier: le premier (et seul dans ce cas là) de la liste
                    var file = document.getElementById("profilepicfile").files[0];
                    // l'élément img va servir à stocker l'image temporairement
                    var img = document.createElement("img");
                    // l'objet de type FileReader nous permet de lire les données du fichier.
                    var reader = new FileReader();
                    // on prépare la fonction callback qui sera appelée lorsque l'image sera chargée
                    reader.onload = function(e) {
                        //on vérifie qu'on a bien téléchargé une image, grâce au mime type
                        if (!file.type.match(/image.*/)) {
                            // le fichier choisi n'est pas une image: le champs profilepicfile est invalide, et on supprime sa valeur
                            document.getElementById("profilepicfile").setCustomValidity("Il faut télécharger une image.");
                            document.getElementById("profilepicfile").value = "";
                        }
                        else {
                            // le callback sera appelé par la méthode getAsDataURL, donc le paramètre de callback e est une url qui contient 
                            // les données de l'image. On modifie donc la source de l'image pour qu'elle soit égale à cette url
                            // on aurait fait différemment si on appelait une autre méthode que getAsDataURL.
                            img.src = e.target.result;
                            // le champs profilepicfile est valide
                            document.getElementById("profilepicfile").setCustomValidity("");
                            var MAX_WIDTH = 96;
                            var MAX_HEIGHT = 96;
                            var width = img.width;
                            var height = img.height;

                            // A FAIRE: si on garde les deux lignes suivantes, on rétrécit l'image mais elle sera déformée
                            // Vous devez supprimer ces lignes, et modifier width et height pour:
                            //    - garder les proportions, 
                            //    - et que le maximum de width et height soit égal à 96
                            var width = MAX_WIDTH;
                            var height = MAX_HEIGHT;
                            
                            canvas.width = width;
                            canvas.height = height;
                            // on dessine l'image dans le canvas à la position 0,0 (en haut à gauche)
                            // et avec une largeur de width et une hauteur de height
                            ctx.drawImage(img, 0, 0, width, height);
                            // on exporte le contenu du canvas (l'image redimensionnée) sous la forme d'une data url
                            var dataurl = canvas.toDataURL("image/png");
                            // on donne finalement cette dataurl comme valeur au champs profilepic
                            document.getElementById("profilepic").value = dataurl;
                        };
                    }
                    // on charge l'image pour de vrai, lorsque ce sera terminé le callback loadProfilePic sera appelé.
                    reader.readAsDataURL(file);
                }
            </script>
        </li>

Pour plus d'info sur l'API File et l'objet FileReader, ou pour savoir comment on aurait pu faire avec le drag'n drop, référez-vous à ce document (anglais): https://developer.mozilla.org/fr/docs/Using_files_from_web_applications, sections 

 

Style CSS3 et Web mobile

(inspiré de http://webdesign.tutsplus.com/tutorials/site-elements/bring-your-forms-up-to-date-with-css3-and-html5-validation/)

Ajoutez le dossier css fourni dans le fichier css.zip à votre projet, et ajoutez la ligne suivante dans le header: 

<link rel="stylesheet" media="screen" href="css/styles.css" >

La feuille de style styles.css utilise:

Web Mobile

Cette feuille de style ne s'applique que pour les ordinateurs (media="screen"). D'autres valeurs existent, telles que "handheld" (pour les teminaux mobiles), "print" (lors de l'impression) ou encore "braille" (pour les non-voyants)... Lorsque vous rendrez l'application pictionnary, un gros bonus est offert aux applications qui s'adaptent pour les mobiles (en portrait et en paysage). Vous étudierez pour celà le document http://www.alsacreations.com/article/lire/930-css3-media-queries.html

Vous pouvez émuler un terminal mobile à l'aide d'une application ou d'une extension de votre navigateur, ou connecter directement votre serveur et vote smartphone au même réseau wifi et ouvir les ports de connexion. Vous vous débrouillerez. En attendant, passons à la suite.

Gestion de la connexion

On commence à toucher au serveur. Dans PhpMyAdmin ou dans la console MySQL,

  • Créez une base pictionnary avec l'encodage utf8
  • Dans phpmyadmin, verifier les privileges, créez un utilisateur, nom test, client local, mot de passe test, donner tous les privilèges sur la base pictionnary, donner aucun privilège global. C'est avec cet utilisateur qu'on se connectera à la base de données en php. 
  • OU dans la console MySQL, exécutez le sql suivant:
CREATE USER 'test'@'localhost' IDENTIFIED BY  'test';
GRANT USAGE ON * . * TO  'test'@'localhost' IDENTIFIED BY  'test';
GRANT ALL PRIVILEGES ON  `pictionnary` . * TO  'test'@'localhost';

Mainteant dans PHPStorm, connectez-vous à la base de données pictionnary avec l'utilisateur test et le mot de passe test:

view -> tool windows -> database, puis ajouter datasource mysql, jdbc:mysql://localhost:3306/pictionnary, télécharger le driver si besoin.
Si votre pare-feu s'alarme, débloquez les ports.
  • créez une table USERS avec la signature suivante (Soit dans la console mysql, soit dans phpmyadmin, soit maintenant dans PHPStorm)
DROP TABLE IF EXISTS `users`;
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(65) NOT NULL,
`password` varchar(65) NOT NULL,
`nom` varchar(65) DEFAULT NULL,
`prenom` varchar(65) DEFAULT NULL,
`tel` varchar(16) DEFAULT NULL,
`website` varchar(65) DEFAULT NULL,
`sexe` char(1) DEFAULT NULL,
`birthdate` date NOT NULL,
`ville` varchar(65) DEFAULT NULL,
`taille` smallint(6) DEFAULT NULL,
`couleur` char(6) DEFAULT '000000',
`profilepic` blob DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

 

On est maintenant prêt à recevoir une soumission du formulaire "inscription".

Page req_inscription.php

Lorsqu'un utilisateur soumet le formulaire "inscription", le comportement souhaité est le suivant:

  • On vérifie si il existe déjà un utilisateur inscrit avec cet email,
  • Si oui, alors on redirige vers la page inscription.php avec tous les paramètes du formulaire, plus un paramètre nommé erreur qui contient un message d'erreur à afficher en rouge en haut du formulaire. A part l'adresse email, le formulaire devra être pré-rempli.
  • Si non, alors on inscrit l'utilisateur dans la table USERS, on crée une session, et on redirige l'utilisateur vers la page main.php.

Comme on utilise MySQL, on pourrait utiliser mySQL, mySQLi, ou JDO en PHP, c.f., http://www.php.net/manual/fr/mysqlinfo.api.choosing.php
Je propose JDO, puisqu'on s'en servirait pareil si la base était Oracle, Derby ou autre. le manuel en français est disponible ici: http://www.php.net/manual/fr/intro.pdo.php

Si vous ne connaissez pas la gestion des sessions en php, le document de référence est http://www.php.net/manual/fr/book.session.php,
et l'exemple simple est disponible ici: http://www.php.net/manual/fr/session.examples.basic.php 

Etudiez le code, répondez aux questions, complétez le code comme demandé.

<?php

// récupérer les éléments du formulaire
// et se protéger contre l'injection MySQL (plus de détails ici: http://us.php.net/mysql_real_escape_string)
$email=stripslashes($_POST['email']);
$password=stripslashes($_POST['password']);
$nom=stripslashes($_POST['nom']);
$prenom=stripslashes($_POST['prenom']);
$tel=stripslashes($_POST['tel']);
$website=stripslashes($_POST['website']);
$sexe='';
if (array_key_exists('sexe',$_POST)) {
    $sexe=stripslashes($_POST['sexe']);
}
$birthdate=stripslashes($_POST['birthdate']);
$ville=stripslashes($_POST['ville']);
$taille=stripslashes($_POST['taille']);
$couleur=stripslashes($_POST['couleur']);
$profilepic=stripslashes($_POST['profilepic']);

try {
    // Connect to server and select database.
    $dbh = new PDO('mysql:host=localhost;dbname=pictionnary', 'test', 'test');

    // Vérifier si un utilisateur avec cette adresse email existe dans la table.
    // En SQL: sélectionner tous les tuples de la table USERS tels que l'email est égal à $email.
    $sql = $dbh->query("la requète SQL ici");
    if (est-ce que le nombre de réponses est supérieur ou égal à 1 ?) {
        // rediriger l'utilisateur ici, avec tous les paramètres du formulaire plus le message d'erreur
        // utiliser à bon escient la méthode htmlspecialchars http://www.php.net/manual/fr/function.htmlspecialchars.php          // et/ou la méthode urlencode http://php.net/manual/fr/function.urlencode.php
    }
    else {
        // Tenter d'inscrire l'utilisateur dans la base
        $sql = $dbh->prepare("INSERT INTO users (email, password, nom, prenom, tel, website, sexe, birthdate, ville, taille, couleur, profilepic) "
                . "VALUES (:email, :password, :nom, :prenom, :tel, :website, :sexe, :birthdate, :ville, :taille, :couleur, :profilepic)");
        $sql->bindValue(":email", $email);
        // de même, lier la valeur pour le mot de passe
        // lier la valeur pour le nom, attention le nom peut être nul, il faut alors lier avec NULL, ou DEFAULT
        // idem pour le prenom, tel, website, birthdate, ville, taille, profilepic
        // n.b., notez: birthdate est au bon format ici, ce serait pas le cas pour un SGBD Oracle par exemple
        // idem pour la couleur, attention au format ici (7 caractères, 6 caractères attendus seulement)
        // idem pour le prenom, tel, website
        // idem pour le sexe, attention il faut être sûr que c'est bien 'H', 'F', ou ''

        // on tente d'exécuter la requête SQL, si la méthode renvoie faux alors une erreur a été rencontrée.
        if (!$sql->execute()) {
            echo "PDO::errorInfo():<br/>";
            $err = $sql->errorInfo();
            print_r($err);
        } else {

            // ici démarrer une session

            // ensuite on requête à nouveau la base pour l'utilisateur qui vient d'être inscrit, et 
            $sql = $dbh->query("SELECT u.id, u.email, u.nom, u.prenom, u.couleur, u.profilepic FROM USERS u WHERE u.email='".$email."'");
            if ($sql->rowCount()<1) {
                header("Location: main.php?erreur=".urlencode("un problème est survenu"));
            }
            else {
                // on récupère la ligne qui nous intéresse avec $sql->fetch(), 
                // et on enregistre les données dans la session avec $_SESSION["..."]=...
            }

            // ici,  rediriger vers la page main.php
        }
        $dbh = null;
    }
} catch (PDOException $e) {
    print "Erreur !: " . $e->getMessage() . "<br/>";
    $dbh = null;
    die();
}
?>

Modifiez la page incription.php en accord avec le comportement attendu en cas d'échec.

Page header.php

Construisez une page header.php (élément header en html 5), qui contient:

  • si une session existe déjà,
    • un petit message de bienvenue (bonjour xxx),
    • l'image du profile (un élément img avec l'attribut src qui contient le dataurl)
    • un lien vers la page logout.php pour se déconnecter;
  • si aucune session n'existe:
    • un petit formulaire avec un champs login, un champs mot de passe, qui envoie les données en post vers la page req_login.php.
    • un lien vers la page inscription.php pour s'inscrire.

pour savoir si une session existe, vous devez démarrer une session et vérifier si un paramètre de session existe comme suit:

session_start();
if(isset($_SESSION['un paramètre de session']));

La page inscription.php, comme toute autre page de l'application, devra inclure la page header.php.

Page req_login.php

La page req_login.php vérifie si il existe bien une entrée dans la base pour cet email et ce mot de passe:

  • Si un utilisateur est trouvé, on ouvre une session, on enregistre les paramètres importants (email, nom, prenom, profilepic,  et on redirige vers main.php.
  • Sinon, on redirige vers main.php avec un message d'erreur en paramètre d'url.

Page logout.php

Pour se déconnecter, le code php est très simple, il suffit d'ajouter ceci au début de la page:

<?php 
session_start();
session_destroy();
?>

Redirigez ensuite vers la page main.php.

Page main.php

Construisez une page main.php qui inclut la page header.php et, dans un div, un lien vers la page paint.php pour commencer un dessin.

Dessiner et redessiner

Page paint.php

Cours: les tutoriels suivants sont importants:

La page paint.php va contenir les éléments suivants:

  • un canvas avec des écouteurs souris pour le dessin;
  • un formulaire en dessous qui contient:
    • un champs de type selector à quatre niveaux: 0,1,2,3
    • un champs de type color qui a par défaut la valeur de la couleur préférée de l'utilisateur.
    • un bouton pour effacer le dessin (recommencer);
    • un champs caché pour stocker l'ensemble des commandes de dessin juste avant l'envoi du dessin au serveur
    • un champs caché pour stocker le dessin final juste avant l'envoi du dessin au serveur
    • un bouton pour valider le dessin et l'envoyer au serveur.

Etudiez le code, répondez aux questions, complétez le code comme demandé.

<?php
// on démarre la session, si l'utilisateur n'est pas connecté alors on redirige vers la page main.php.
session_start();
if(!isset($_SESSION['prenom'])) {
    header("Location: main.php");
}
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8 />
    <title>Pictionnary</title>
    <link rel="stylesheet" media="screen" href="css/styles.css" >
    <script>

        // les quatre tailles de pinceau possible.
        var sizes=[8,20,44,90];
        // la taille et la couleur du pinceau
        var size, color;
        // la dernière position du stylo
        var x0, y0;
        // le tableau de commandes de dessin à envoyer au serveur lors de la validation du dessin
        var drawingCommands = [];

        var setColor = function() {
            // on récupère la valeur du champs couleur
            color = document.getElementById('color').value;
            console.log("color:" + color);
        }

        var setSize = function() {
            // ici, récupèrez la taille dans le tableau de tailles, en fonction de la valeur choisie dans le champs taille.
            console.log("size:" + size);
        }

        window.onload = function() {
            var canvas = document.getElementById('myCanvas');
            canvas.width = 400;
            canvas.height= 400;
            var context = canvas.getContext('2d');

            setSize();
            setColor();
            document.getElementById('size').onchange = setSize;
            document.getElementById('color').onchange = setColor;

            var isDrawing = false;

            var startDrawing = function(e) {
                console.log("start");
                // crér un nouvel objet qui représente une commande de type "start", avec la position, la couleur
                var command = {};
                command.command="start";
                command.x=e.x;
                ...
                //c'est équivalent à: 
                command = {"command":"start", "x": e.x, ...};

                // Ce genre d'objet Javascript simple est nommé JSON. Pour apprendre ce qu'est le JSON, c.f. http://blog.xebia.fr/2008/05/29/introduction-a-json/

                // on l'ajoute à la liste des commandes
                drawingCommands.push(command);

                // ici, dessinez un cercle de la bonne couleur, de la bonne taille, et au bon endroit. 

                isDrawing = true;
            }

            var stopDrawing = function(e) {
                console.log("stop");
                isDrawing = false;
            }

            var draw = function(e) {
                if(isDrawing) {
                    // ici, créer un nouvel objet qui représente une commande de type "draw", avec la position, et l'ajouter à la liste des commandes.
                    // ici, dessinez un cercle de la bonne couleur, de la bonne taille, et au bon endroit. 
                }
            }

            canvas.onmousedown = startDrawing;
            canvas.onmouseout = stopDrawing;
            canvas.onmouseup = stopDrawing;
            canvas.onmousemove = draw;

            document.getElementById('restart').onclick = function() {
                console.log("clear");
                // ici ajouter à la liste des commandes une nouvelle commande de type "clear"
                // ici, effacer le context, grace à la méthode clearRect.
            };

            document.getElementById('validate').onclick = function() {
                // la prochaine ligne transforme la liste de commandes en une chaîne de caractères, et l'ajoute en valeur au champs "drawingCommands" pour l'envoyer au serveur.
                document.getElementById('drawingCommands').value = JSON.stringify(drawingCommands);

                // ici, exportez le contenu du canvas dans un data url, et ajoutez le en valeur au champs "picture" pour l'envoyer au serveur.
            };
        };
    </script>
</head>
<body>

<canvas id="myCanvas"></canvas>

<form name="tools" action="req_paint.php" method="post">
    <!-- ici, insérez un champs de type range avec id="size", pour choisir un entier entre 0 et 4) -->
    <!-- ici, insérez un champs de type color avec id="color", et comme valeur l'attribut  de session couleur (à l'aide d'une commande php echo).) -->

    <input id="restart" type="button" value="Recommencer"/>
    <input type="hidden" id="drawingCommands" name="drawingCommands"/>
    <!-- à quoi servent ces champs hidden ? -->
    <input type="hidden" id="picture" name="picture"/>
    <input id="validate" type="submit" value="Valider"/>
</form>

</body>
</html>

 

Observez que lorsqu'on dessine trop rapidement, on ne dessine pas un propre trait, mais plutôt un ensemble de cercles. Modifiez le code de votre application pour dessiner non seulement un cercle, mais également un trait de la bonne épaisseur et de la bonne couleur, de la position précédente du stylo à sa position actuelle.

lorsqu'on valide le dessin, on envoie au serveur la structure de données, sous la forme d'un objet JSON.

 

Page req_paint.php

l'utilisateur a posté le formulaire à la page req_paint.php. On veut maintenant enregistrer une nouvelle ligne dans une nouvelle table DRAWINGS dans la base de données, avec les valeurs suivantes:

  • une pour l'identifiant de l'utilisateur qui a dessiné (paramètre de session)
  • une pour les commandes du dessin (paramète du post)
  • une pour le dessin (paramète du post)

créez la table DRAWINGS dans la base de données, et écrivez la page req_paint.php.

Page guess.php

Modifiez d'abord la page main.php: on veut récupérer l'identifiant des dessins créés par l'utilisateur courant, et pour chaque dessin afficher un lien vers la page guess.php, avec le paramètre d'URL id qui vaut l'identifiant du dessin.

Pour la page guess.php, étudiez ce code, répondez aux questions, complétez le code comme demandé.

<?php
session_start();
if(!isset($_SESSION['id'])) {
    header("Location: main.php");
} else {
 // ici, récupérer la liste des commandes dans la table DRAWINGS avec l'identifiant $_GET['id']
 // l'enregistrer dans la variable $commands
}

?>
<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8 />
    <title>Pictionnary</title>
    <link rel="stylesheet" media="screen" href="css/styles.css" >
    <script>
        // la taille et la couleur du pinceau
        var size, color;
        // la dernière position du stylo
        var x0, y0;
        // le tableau de commandes de dessin à envoyer au serveur lors de la validation du dessin
        var drawingCommands = <?php echo $commands;?>;

        window.onload = function() {
            var canvas = document.getElementById('myCanvas');
            canvas.width = 400;
            canvas.height= 400;
            var context = canvas.getContext('2d');

            var start = function(c) {
                // complétez
            }

            var draw = function(c) {
                // complétez
            }

            var clear = function() {
                // complétez
            }

            // étudiez ce bout de code
            var i = 0;
            var iterate = function() {
                if(i>=drawingCommands.length)
                    return;
                var c = drawingCommands[i];
                switch(c.command) {
                    case "start":
                        start(c);
                        break;
                    case "draw":
                        draw(c);
                        break;
                    case "clear":
                        clear();
                        break;
                    default:
                        console.error("cette commande n'existe pas "+ c.command);
                }
                i++;
                setTimeout(iterate,30);
            };

            iterate();

        };

    </script>
</head>
<body>
<canvas id="myCanvas"></canvas>
</body>
</html>

Projet à rendre

Vous avez tous les élements pour faire de ce TP un super pictionnary asynchrone. à vous de jouer !