diff --git a/.gradebook.db b/.gradebook.db
index d6d2109f19e0816f026e2148fb689d56f33ff532..411a3a6152bf69f0c4cbc8e6d4ab9f26c7931425 100644
Binary files a/.gradebook.db and b/.gradebook.db differ
diff --git a/premier-dessin.md b/premier-dessin.md
index 22ce57311b6ddb9b19eddb40ee514bd66d5d35ce..1f8fbfebaa407f9a93457ebe6f1dc8407df6e130 100644
--- a/premier-dessin.md
+++ b/premier-dessin.md
@@ -14,6 +14,15 @@ kernelspec:
 
 # Premiers graphiques avec Jupyter
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "f43a760bd6125e25df23a9ab2628c3e9", "grade": false, "grade_id": "cell-db040b06c5a92100", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+Dans cette feuille, nous allons reprendre l'exercice 2 du TD et
+effectuer un premier dessin.
+
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "f4cbf5563a6542f5c409f308fdca30a3", "grade": false, "grade_id": "cell-db040b06c5a92101", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+Commençons par créer un canvas vert de taille 640 x 480 :
+
 ```{code-cell}
 ---
 deletable: false
@@ -37,35 +46,44 @@ deletable: false
 editable: false
 nbgrader:
   cell_type: code
-  checksum: bca96fdfc7023d461b707c8ae74b0caa
+  checksum: 9d359d2bf83f09dfcd9eb723473a197f
   grade: false
   grade_id: cell-c2afd88f340f46f4
   locked: true
   schema_version: 3
   solution: false
   task: false
+tags: []
 ---
-RenderWindow window(VideoMode(900, 480), "Ma super fenêtre");
+RenderWindow window(VideoMode(640, 480), "Ma super fenêtre");
+window.canvas
 ```
 
-<canvas id='primitives_canvas'></canvas>
-
 ```{code-cell}
 window.clear(Color::Green);
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "7df12946868397e5fbe4bc994fb8d19c", "grade": false, "grade_id": "cell-db040b06c5a92102", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+puis dessinons un point noir de coordonnées (418, 143). Ce point est
+tout petit, vous aurez peut-être un peu de mal à le distinguer :
+
 ```{code-cell}
-// Dessine un point noir de coordonnées (418, 143)
 draw_point(window, {418, 143}, Color::Black );
 window.display();
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "d8948ac8886182d90df0fb2bce302b52", "grade": false, "grade_id": "cell-db040b06c5a92103", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+1.  Dessinez un segment blanc entre les points de coordonnées
+    respsectives (100,200) et (200,200) :
+
 ```{code-cell}
 ---
 deletable: false
 nbgrader:
   cell_type: code
-  checksum: b956b1c98520d602fab742ceb67096ed
+  checksum: ce1d6a0deeef3221d1ea8d9afbd40bce
   grade: true
   grade_id: cell-e77db2d9ed7265ac
   locked: false
@@ -74,17 +92,31 @@ nbgrader:
   solution: true
   task: false
 ---
-// Dessine un segment blanc entre les points (100,200) et (200,200)
 // REMPLACEZ CETTE LIGNE PAR VOTRE RÉPONSE
 window.display();
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "2be96f003c2555d67d32213924f40a59", "grade": false, "grade_id": "cell-db040b06c5a92104", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+:::{tip} Astuce
+
+Pour éviter de monter et descendre constamment dans cette feuille,
+faites un clic droit sur le canvas et cliquez sur «Create new view for
+output».  Puis disposez la copie obtenue du canvas sur votre espace de
+travail de sorte à voir simultanément votre code et le canvas.
+
+:::
+
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "fea5730c56d940cd6a18b432d9069629", "grade": false, "grade_id": "cell-db040b06c5a92105", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+2.  Dessinez un segment rouge entre les points (200,300) et (200,400) :
+
 ```{code-cell}
 ---
 deletable: false
 nbgrader:
   cell_type: code
-  checksum: 85bdbd533e19a61d9a8a460522c51477
+  checksum: 90954d14f326667b7a51643cd5d83bc0
   grade: true
   grade_id: cell-03b3d3f225c29d1d
   locked: false
@@ -93,17 +125,21 @@ nbgrader:
   solution: true
   task: false
 ---
-// Dessine un segment rouge entre les points (200,300) et (200,400)
 // REMPLACEZ CETTE LIGNE PAR VOTRE RÉPONSE
 window.display();
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "0791571f18f4df2c2bfecb5a3ba9c0ff", "grade": false, "grade_id": "cell-db040b06c5a92106", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+3.  Dessinez un rectangle horizontal vide de sommets diagonaux
+    (200,200) et (400,300) et de contour rouge :
+
 ```{code-cell}
 ---
 deletable: false
 nbgrader:
   cell_type: code
-  checksum: dea1bd15783375ceace4d093b9200ec4
+  checksum: 4c65f8e4cec21af229e5a44dd7063a03
   grade: true
   grade_id: cell-3606e0158c83309d
   locked: false
@@ -112,17 +148,21 @@ nbgrader:
   solution: true
   task: false
 ---
-// Dessine un rectangle horizontal vide de sommets diagonaux (200,200) et (400,300) et de contour rouge
 // REMPLACEZ CETTE LIGNE PAR VOTRE RÉPONSE
 window.display()
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "4eda0a34767a781e0fd3c186999331e2", "grade": false, "grade_id": "cell-db040b06c5a92107", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+4.  Dessinez un rectangle horizontal plein noir de sommets diagonaux
+    (400,150) et (500,200) :
+
 ```{code-cell}
 ---
 deletable: false
 nbgrader:
   cell_type: code
-  checksum: 8d1e034e1cda25ee20a1c154f6f26185
+  checksum: f66e8172c61aca3cb946861a95326520
   grade: true
   grade_id: cell-25abe8a7c6c8c617
   locked: false
@@ -131,17 +171,21 @@ nbgrader:
   solution: true
   task: false
 ---
-// Dessine un rectangle horizontal plein noir de sommets diagonaux (400,150) et (500,200)
 // REMPLACEZ CETTE LIGNE PAR VOTRE RÉPONSE
 window.display()
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "2f11cf73501e6fcc67b66d7ced218656", "grade": false, "grade_id": "cell-db040b06c5a92108", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+5.  Dessinez un segment rouge entre les points (400,300) et
+    (500,400) :
+
 ```{code-cell}
 ---
 deletable: false
 nbgrader:
   cell_type: code
-  checksum: 196d46fbc53bc7393b523ace887864ab
+  checksum: 49650230393c3fc8d743c211525de50c
   grade: true
   grade_id: cell-c6d75131df2bea6d
   locked: false
@@ -150,17 +194,42 @@ nbgrader:
   solution: true
   task: false
 ---
-// Dessine un segment rouge entre les points (400,300) et (500,400)
 // REMPLACEZ CETTE LIGNE PAR VOTRE RÉPONSE
 window.display()
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "34955bd551a86d7f66e34edca2617cab", "grade": false, "grade_id": "cell-db040b06c5a92109", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+6.  ♣ Dessinez un triangle bleu entre les (0,0), (640,0) et (0,160) :
+
+```{code-cell}
+---
+deletable: false
+nbgrader:
+  cell_type: code
+  checksum: de3439e6f2af1c2ebadec5e92840b900
+  grade: true
+  grade_id: cell-c6d75131df2bea6e
+  locked: false
+  points: 1
+  schema_version: 3
+  solution: true
+  task: false
+---
+// REMPLACEZ CETTE LIGNE PAR VOTRE RÉPONSE
+window.display()
+```
+
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "16aaa6f47064b571ba75a417797361e4", "grade": false, "grade_id": "cell-db040b06c5a92110", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+7.  Dessinez un cercle noir de centre (415,145) et de rayon 10 :
+
 ```{code-cell}
 ---
 deletable: false
 nbgrader:
   cell_type: code
-  checksum: 783c0f2dd02e54533dd5243b34fe2ccd
+  checksum: e56a4a8d478945ff8d1ef1c4497dcf05
   grade: true
   grade_id: cell-63406c09d76b868b
   locked: false
@@ -169,17 +238,20 @@ nbgrader:
   solution: true
   task: false
 ---
-// Dessine un cercle noir de centre (415,145) et de rayon 10
 // REMPLACEZ CETTE LIGNE PAR VOTRE RÉPONSE
 window.display()
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "92e964b26520043007e0fe1972b38638", "grade": false, "grade_id": "cell-db040b06c5a92111", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+8.  Dessinez un disque jaune de centre (550, 75) et de rayon 50 :
+
 ```{code-cell}
 ---
 deletable: false
 nbgrader:
   cell_type: code
-  checksum: cfc9ac790998bab53ad2de41b30f2f6d
+  checksum: 6419729d5f34a928308cdd11942a3f1a
   grade: true
   grade_id: cell-4f7332edb4646cbf
   locked: false
@@ -188,8 +260,25 @@ nbgrader:
   solution: true
   task: false
 ---
-// Dessine un disque jaune de centre (700, 100) et de rayon 50
 // REMPLACEZ CETTE LIGNE PAR VOTRE RÉPONSE
 window.display()
 ```
 
++++ {"deletable": false, "editable": false, "nbgrader": {"cell_type": "markdown", "checksum": "ef878f93df33be50d5262273cefbc00a", "grade": false, "grade_id": "cell-db040b06c5a92112", "locked": true, "schema_version": 3, "solution": false, "task": false}}
+
+:::{attention}
+
+Comme vous l'avez constaté, les performances sont lamentables. C'est
+parce que nous avons abusé des canvas de HTML : alors que ceux-ci sont
+conçus pour du dessin vectoriel, nous approximons les figures
+vectorielles (lignes, triangles, ...) par des accumulations d'un très
+grand nombre de pixels (combien pour le triangle bleu?), sachant que
+chacun de ces pixels est en fait représenté en interne par un petit
+rectangle.
+
+En dehors de ce premier dessin -- dont l'objectif pédagogique était de
+faire un peu d'algorithmique -- il faut au contraire utiliser les
+primitives vectorielles fournies.
+
+:::
+
diff --git a/primitives_jupyter.hpp b/primitives_jupyter.hpp
index 99679abfc266fde9bd3123eae6e1e7f4d19c6cb2..a097185092e1dc8b197d7c5d65ea5346f03d1493 100644
--- a/primitives_jupyter.hpp
+++ b/primitives_jupyter.hpp
@@ -1,3 +1,6 @@
+#ifndef PRIMITIVES_JUPYTER_H
+#define PRIMITIVES_JUPYTER_H
+
 /**
    Variante de primitives.h pour dessiner dans Jupyter, sans la SFML
 
@@ -11,48 +14,16 @@
    NE PRENNEZ PAS EXEMPLE DESSUS!
  **/
 
-#include <iostream>
-#include <sstream>
-#include <xwidgets/xhtml.hpp>
-
-/** Execute javascript code in the Jupyter window
- **/
-void execute_javascript(std::string js) {
-    nl::json mime_bundle;
-    mime_bundle["application/javascript"] = js;
-    ::xeus::get_interpreter().display_data(mime_bundle,
-                                           nl::json::object(),
-                                           nl::json::object());
-}
-
-void load_javascript_library() {
-    execute_javascript(R"js(
-document.primitives_get_context = function() {
-    return document.primitives_canvas.getContext('2d');
-}
-document.primitives_draw_rectangle = function(x, y, width, height, color) {
-    var ctx = document.primitives_get_context();
-    ctx.strokeStyle = color;
-    ctx.strokeRect(x, y, width, height);
-}
-document.primitives_draw_filled_rectangle = function(x, y, width, height, color) {
-    var ctx = document.primitives_get_context();
-    ctx.fillStyle = color;
-    ctx.fillRect(x, y, width, height);
-}
-document.primitives_draw_point = function(x, y, color) {
-    document.primitives_draw_rectangle(x, y, 1, 1, color);
-}
-)js");
-}
+#include <xcanvas/xcanvas.hpp>
 
 namespace Color {
     using Color = std::string;
-    Color White = "white";
-    Color Red = "red";
-    Color Yellow = "yellow";
-    Color Green = "#00FF00";
-    Color Black = "black";
+    Color White = "White";
+    Color Blue = "Blue";
+    Color Red = "Red";
+    Color Yellow = "Yellow";
+    Color Green = "#00CC00";
+    Color Black = "Black";
 }
 
 class Point {
@@ -70,49 +41,43 @@ class VideoMode {
     VideoMode(int w, int h): width(w), height(h) {}
 };
 
+using Canvas = xw::xmaterialize<xc::xcanvas>;
+
 class RenderWindow {
-    xw::html html;
     VideoMode vm;
 
     public:
-    // A buffer of javascript commands to execute
-    std::ostringstream buffer;
+    Canvas canvas;
+
     RenderWindow(VideoMode _vm, std::string s) : vm(_vm) {
-        std::ostringstream canvas;
-        // canvas << "<canvas id='primitives_canvas'"
-        //        << " width=" << vm.width << " height=" << vm.height
-        //        << " style='border:1px solid #000000;'></canvas>";
-        load_javascript_library();
-	buffer << "document.primitives_canvas = document.getElementById('primitives_canvas');";
-	buffer << "document.primitives_canvas.height = " << vm.height << ";";
-	buffer << "document.primitives_canvas.width = " << vm.width << ";";
-	buffer << "document.primitives_canvas.style = 'border:1px solid #000000';";
-	display();
-	// Disabled until xwidgets is functional again
-	// For now, the canvas needs to be created in a markdown cell of the notebook
-        // html.value = canvas.str();
-        // html.display();
+	vm = _vm;
+	canvas = xc::canvas().initialize()
+	    .width(vm.width)
+	    .height(vm.height)
+	    .finalize();
+	canvas.cache();
     }
 
-    // Update the canvas by executing all javascript commands in the buffer
-    void display() {
-        execute_javascript(buffer.str());
-        buffer = std::ostringstream();
+    Canvas& get_canvas() {
+	return canvas;
     }
 
     // Clear the canvas with the given color
     void clear(Color::Color color) {
-        buffer << "document.primitives_draw_filled_rectangle(0, 0, "
-               << vm.width << ", "
-               << vm.height <<  ", '"
-               << color << "');";
-        display();
+	canvas.fill_style = color;
+	canvas.fill_rect(0, 0, vm.width, vm.height);
+	display();
+    }
+
+    void display() {
+	canvas.flush();
+	canvas.cache();
     }
 };
 
 void draw_point(RenderWindow &window, Point p, Color::Color color) {
-    window.buffer << "document.primitives_draw_point("
-                  << p.x << ", "
-                  << p.y << ", '"
-                  << color << "');";
+    window.canvas.fill_style = color;
+    window.canvas.fill_rect(p.x, p.y, 1, 1);
 }
+
+#endif