[libvirt PATCH] docs: introduce a more interesting 404 error page

Daniel P. Berrangé posted 1 patch 1 year, 2 months ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/libvirt tags/patchew/20230216145313.1912358-1-berrange@redhat.com
docs/404.html.in       |    7 +-
docs/css/libvirt.css   |   12 +
docs/js/app.js         |  108 +++
docs/js/meson.build    |    2 +
docs/js/particles.js   | 1541 ++++++++++++++++++++++++++++++++++++++++
docs/logos/meson.build |    2 +
docs/logos/penguin.png |  Bin 0 -> 4236 bytes
docs/logos/penguin.svg |  358 ++++++++++
docs/page.xsl          |    8 +
9 files changed, 2035 insertions(+), 3 deletions(-)
create mode 100644 docs/js/app.js
create mode 100644 docs/js/particles.js
create mode 100644 docs/logos/penguin.png
create mode 100644 docs/logos/penguin.svg
[libvirt PATCH] docs: introduce a more interesting 404 error page
Posted by Daniel P. Berrangé 1 year, 2 months ago
Our 404 error page is a little bit too boring. The solution is to
add more penguins !

This relies on MIT licensed javascript code imported from

  https://github.com/VincentGarreau/particles.js

The image is extracted from the existing libvirt logo SVG file.

Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
---
 docs/404.html.in       |    7 +-
 docs/css/libvirt.css   |   12 +
 docs/js/app.js         |  108 +++
 docs/js/meson.build    |    2 +
 docs/js/particles.js   | 1541 ++++++++++++++++++++++++++++++++++++++++
 docs/logos/meson.build |    2 +
 docs/logos/penguin.png |  Bin 0 -> 4236 bytes
 docs/logos/penguin.svg |  358 ++++++++++
 docs/page.xsl          |    8 +
 9 files changed, 2035 insertions(+), 3 deletions(-)
 create mode 100644 docs/js/app.js
 create mode 100644 docs/js/particles.js
 create mode 100644 docs/logos/penguin.png
 create mode 100644 docs/logos/penguin.svg

Yes, this could easily be considered an April Fools' joke, but at
the same time it is common to have a bit of fun with 404 error
pages, so it is also a real suggestion :-)

Live example rendering is at:

  https://berrange.gitlab.io/-/libvirt/-/jobs/3749944370/artifacts/website/404.html

The main "cost" of the fun is a few 10's of KB more in the dist
from the JS/SVG/PNG

diff --git a/docs/404.html.in b/docs/404.html.in
index 0ac76f125a..39cded58f6 100644
--- a/docs/404.html.in
+++ b/docs/404.html.in
@@ -5,15 +5,16 @@
     <h1>404 page not found</h1>
 
     <p>
-      Someone appears to have eaten the <del>penguin</del>
-      page you were looking for. You might want to try
+      We can't figure out which <del>penguin</del> page you were
+      looking for. You might want to try
     </p>
     <ul>
       <li>going back to the <a href="https://libvirt.org/">home page</a> to find
         a collection of links to interesting pages on this site</li>
       <li>using the search box at the top right corner of the screen to
         locate the content on this site or mailing list archives</li>
+      <li>selecting the right penguin swimming in the pool below</li>
     </ul>
-
+    <div id="particles-js"><!-- preserve --></div>
   </body>
 </html>
diff --git a/docs/css/libvirt.css b/docs/css/libvirt.css
index 097dfcbe8e..fbd193da74 100644
--- a/docs/css/libvirt.css
+++ b/docs/css/libvirt.css
@@ -636,3 +636,15 @@ th p, td p {
 #contents p.topic-title {
     display: none;
 }
+
+canvas {
+    display: block;
+    vertical-align: bottom;
+}
+
+#particles-js {
+    background-color: rgb(230,250,255);
+    border: 2px solid rgb(150,180,255);
+    width: 100%;
+    height: 100%;
+}
diff --git a/docs/js/app.js b/docs/js/app.js
new file mode 100644
index 0000000000..79fae27c2e
--- /dev/null
+++ b/docs/js/app.js
@@ -0,0 +1,108 @@
+particlesJS('particles-js',
+  {
+    "particles": {
+      "number": {
+        "value": 20,
+        "density": {
+          "enable": true,
+          "value_area": 800
+        }
+      },
+      "color": {
+        "value": "#ff0000"
+      },
+      "shape": {
+        "type": "image",
+        "image": {
+          "src": "logos/penguin.png",
+          "width": 73,
+          "height": 138
+        }
+      },
+      "opacity": {
+        "value": 0.5,
+        "random": true,
+        "anim": {
+          "enable": true,
+          "speed": 1,
+          "opacity_min": 0.1,
+          "sync": false
+        }
+      },
+      "size": {
+        "value": 50,
+        "random": true,
+        "anim": {
+          "enable": false,
+          "speed": 40,
+          "size_min": 0.1,
+          "sync": false
+        }
+      },
+      "line_linked": {
+        "enable": false,
+      },
+      "move": {
+        "enable": true,
+        "speed": 6,
+        "direction": "none",
+        "random": false,
+        "straight": false,
+        "out_mode": "bounce",
+        "attract": {
+          "enable": true,
+          "rotateX": 600,
+          "rotateY": 1200
+        }
+      }
+    },
+    "interactivity": {
+      "detect_on": "canvas",
+      "events": {
+        "onhover": {
+          "enable": true,
+          "mode": "repulse"
+        },
+        "onclick": {
+          "enable": true,
+          "mode": "push"
+        },
+        "resize": true
+      },
+      "modes": {
+        "grab": {
+          "distance": 400,
+          "line_linked": {
+            "opacity": 1
+          }
+        },
+        "bubble": {
+          "distance": 400,
+          "size": 40,
+          "duration": 2,
+          "opacity": 8,
+          "speed": 3
+        },
+        "repulse": {
+          "distance": 200
+        },
+        "push": {
+          "particles_nb": 4
+        },
+        "remove": {
+          "particles_nb": 2
+        }
+      }
+    },
+    "retina_detect": true,
+    "config_demo": {
+      "hide_card": false,
+      "background_color": "#b61924",
+      "background_image": "",
+      "background_position": "50% 50%",
+      "background_repeat": "no-repeat",
+      "background_size": "cover"
+    }
+  }
+
+);
diff --git a/docs/js/meson.build b/docs/js/meson.build
index cbf2dc2633..4e3e05bf89 100644
--- a/docs/js/meson.build
+++ b/docs/js/meson.build
@@ -1,5 +1,7 @@
 docs_js_files = [
   'main.js',
+  'particles.js',
+  'app.js'
 ]
 
 install_data(docs_js_files, install_dir: docs_html_dir / 'js')
diff --git a/docs/js/particles.js b/docs/js/particles.js
new file mode 100644
index 0000000000..ef3917fab7
--- /dev/null
+++ b/docs/js/particles.js
@@ -0,0 +1,1541 @@
+/* -----------------------------------------------
+/* Author : Vincent Garreau  - vincentgarreau.com
+/* MIT license: https://opensource.org/licenses/MIT
+/* Demo / Generator : vincentgarreau.com/particles.js
+/* GitHub : github.com/VincentGarreau/particles.js
+/* How to use? : Check the GitHub README
+/* v2.0.0
+/* ----------------------------------------------- */
+
+var pJS = function(tag_id, params){
+
+  var canvas_el = document.querySelector('#'+tag_id+' > .particles-js-canvas-el');
+
+  /* particles.js variables with default values */
+  this.pJS = {
+    canvas: {
+      el: canvas_el,
+      w: canvas_el.offsetWidth,
+      h: canvas_el.offsetHeight
+    },
+    particles: {
+      number: {
+        value: 400,
+        density: {
+          enable: true,
+          value_area: 800
+        }
+      },
+      color: {
+        value: '#fff'
+      },
+      shape: {
+        type: 'circle',
+        stroke: {
+          width: 0,
+          color: '#ff0000'
+        },
+        polygon: {
+          nb_sides: 5
+        },
+        image: {
+          src: '',
+          width: 100,
+          height: 100
+        }
+      },
+      opacity: {
+        value: 1,
+        random: false,
+        anim: {
+          enable: false,
+          speed: 2,
+          opacity_min: 0,
+          sync: false
+        }
+      },
+      size: {
+        value: 20,
+        random: false,
+        anim: {
+          enable: false,
+          speed: 20,
+          size_min: 0,
+          sync: false
+        }
+      },
+      line_linked: {
+        enable: true,
+        distance: 100,
+        color: '#fff',
+        opacity: 1,
+        width: 1
+      },
+      move: {
+        enable: true,
+        speed: 2,
+        direction: 'none',
+        random: false,
+        straight: false,
+        out_mode: 'out',
+        bounce: false,
+        attract: {
+          enable: false,
+          rotateX: 3000,
+          rotateY: 3000
+        }
+      },
+      array: []
+    },
+    interactivity: {
+      detect_on: 'canvas',
+      events: {
+        onhover: {
+          enable: true,
+          mode: 'grab'
+        },
+        onclick: {
+          enable: true,
+          mode: 'push'
+        },
+        resize: true
+      },
+      modes: {
+        grab:{
+          distance: 100,
+          line_linked:{
+            opacity: 1
+          }
+        },
+        bubble:{
+          distance: 200,
+          size: 80,
+          duration: 0.4
+        },
+        repulse:{
+          distance: 200,
+          duration: 0.4
+        },
+        push:{
+          particles_nb: 4
+        },
+        remove:{
+          particles_nb: 2
+        }
+      },
+      mouse:{}
+    },
+    retina_detect: false,
+    fn: {
+      interact: {},
+      modes: {},
+      vendors:{}
+    },
+    tmp: {}
+  };
+
+  var pJS = this.pJS;
+
+  /* params settings */
+  if(params){
+    Object.deepExtend(pJS, params);
+  }
+
+  pJS.tmp.obj = {
+    size_value: pJS.particles.size.value,
+    size_anim_speed: pJS.particles.size.anim.speed,
+    move_speed: pJS.particles.move.speed,
+    line_linked_distance: pJS.particles.line_linked.distance,
+    line_linked_width: pJS.particles.line_linked.width,
+    mode_grab_distance: pJS.interactivity.modes.grab.distance,
+    mode_bubble_distance: pJS.interactivity.modes.bubble.distance,
+    mode_bubble_size: pJS.interactivity.modes.bubble.size,
+    mode_repulse_distance: pJS.interactivity.modes.repulse.distance
+  };
+
+
+  pJS.fn.retinaInit = function(){
+
+    if(pJS.retina_detect && window.devicePixelRatio > 1){
+      pJS.canvas.pxratio = window.devicePixelRatio;
+      pJS.tmp.retina = true;
+    }
+    else{
+      pJS.canvas.pxratio = 1;
+      pJS.tmp.retina = false;
+    }
+
+    pJS.canvas.w = pJS.canvas.el.offsetWidth * pJS.canvas.pxratio;
+    pJS.canvas.h = pJS.canvas.el.offsetHeight * pJS.canvas.pxratio;
+
+    pJS.particles.size.value = pJS.tmp.obj.size_value * pJS.canvas.pxratio;
+    pJS.particles.size.anim.speed = pJS.tmp.obj.size_anim_speed * pJS.canvas.pxratio;
+    pJS.particles.move.speed = pJS.tmp.obj.move_speed * pJS.canvas.pxratio;
+    pJS.particles.line_linked.distance = pJS.tmp.obj.line_linked_distance * pJS.canvas.pxratio;
+    pJS.interactivity.modes.grab.distance = pJS.tmp.obj.mode_grab_distance * pJS.canvas.pxratio;
+    pJS.interactivity.modes.bubble.distance = pJS.tmp.obj.mode_bubble_distance * pJS.canvas.pxratio;
+    pJS.particles.line_linked.width = pJS.tmp.obj.line_linked_width * pJS.canvas.pxratio;
+    pJS.interactivity.modes.bubble.size = pJS.tmp.obj.mode_bubble_size * pJS.canvas.pxratio;
+    pJS.interactivity.modes.repulse.distance = pJS.tmp.obj.mode_repulse_distance * pJS.canvas.pxratio;
+
+  };
+
+
+
+  /* ---------- pJS functions - canvas ------------ */
+
+  pJS.fn.canvasInit = function(){
+    pJS.canvas.ctx = pJS.canvas.el.getContext('2d');
+  };
+
+  pJS.fn.canvasSize = function(){
+
+    pJS.canvas.el.width = pJS.canvas.w;
+    pJS.canvas.el.height = pJS.canvas.h;
+
+    if(pJS && pJS.interactivity.events.resize){
+
+      window.addEventListener('resize', function(){
+
+          pJS.canvas.w = pJS.canvas.el.offsetWidth;
+          pJS.canvas.h = pJS.canvas.el.offsetHeight;
+
+          /* resize canvas */
+          if(pJS.tmp.retina){
+            pJS.canvas.w *= pJS.canvas.pxratio;
+            pJS.canvas.h *= pJS.canvas.pxratio;
+          }
+
+          pJS.canvas.el.width = pJS.canvas.w;
+          pJS.canvas.el.height = pJS.canvas.h;
+
+          /* repaint canvas on anim disabled */
+          if(!pJS.particles.move.enable){
+            pJS.fn.particlesEmpty();
+            pJS.fn.particlesCreate();
+            pJS.fn.particlesDraw();
+            pJS.fn.vendors.densityAutoParticles();
+          }
+
+        /* density particles enabled */
+        pJS.fn.vendors.densityAutoParticles();
+
+      });
+
+    }
+
+  };
+
+
+  pJS.fn.canvasPaint = function(){
+    pJS.canvas.ctx.fillRect(0, 0, pJS.canvas.w, pJS.canvas.h);
+  };
+
+  pJS.fn.canvasClear = function(){
+    pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h);
+  };
+
+
+  /* --------- pJS functions - particles ----------- */
+
+  pJS.fn.particle = function(color, opacity, position){
+
+    /* size */
+    this.radius = (pJS.particles.size.random ? Math.random() : 1) * pJS.particles.size.value;
+    if(pJS.particles.size.anim.enable){
+      this.size_status = false;
+      this.vs = pJS.particles.size.anim.speed / 100;
+      if(!pJS.particles.size.anim.sync){
+        this.vs = this.vs * Math.random();
+      }
+    }
+
+    /* position */
+    this.x = position ? position.x : Math.random() * pJS.canvas.w;
+    this.y = position ? position.y : Math.random() * pJS.canvas.h;
+
+    /* check position  - into the canvas */
+    if(this.x > pJS.canvas.w - this.radius*2) this.x = this.x - this.radius;
+    else if(this.x < this.radius*2) this.x = this.x + this.radius;
+    if(this.y > pJS.canvas.h - this.radius*2) this.y = this.y - this.radius;
+    else if(this.y < this.radius*2) this.y = this.y + this.radius;
+
+    /* check position - avoid overlap */
+    if(pJS.particles.move.bounce){
+      pJS.fn.vendors.checkOverlap(this, position);
+    }
+
+    /* color */
+    this.color = {};
+    if(typeof(color.value) == 'object'){
+
+      if(color.value instanceof Array){
+        var color_selected = color.value[Math.floor(Math.random() * pJS.particles.color.value.length)];
+        this.color.rgb = hexToRgb(color_selected);
+      }else{
+        if(color.value.r != undefined && color.value.g != undefined && color.value.b != undefined){
+          this.color.rgb = {
+            r: color.value.r,
+            g: color.value.g,
+            b: color.value.b
+          }
+        }
+        if(color.value.h != undefined && color.value.s != undefined && color.value.l != undefined){
+          this.color.hsl = {
+            h: color.value.h,
+            s: color.value.s,
+            l: color.value.l
+          }
+        }
+      }
+
+    }
+    else if(color.value == 'random'){
+      this.color.rgb = {
+        r: (Math.floor(Math.random() * (255 - 0 + 1)) + 0),
+        g: (Math.floor(Math.random() * (255 - 0 + 1)) + 0),
+        b: (Math.floor(Math.random() * (255 - 0 + 1)) + 0)
+      }
+    }
+    else if(typeof(color.value) == 'string'){
+      this.color = color;
+      this.color.rgb = hexToRgb(this.color.value);
+    }
+
+    /* opacity */
+    this.opacity = (pJS.particles.opacity.random ? Math.random() : 1) * pJS.particles.opacity.value;
+    if(pJS.particles.opacity.anim.enable){
+      this.opacity_status = false;
+      this.vo = pJS.particles.opacity.anim.speed / 100;
+      if(!pJS.particles.opacity.anim.sync){
+        this.vo = this.vo * Math.random();
+      }
+    }
+
+    /* animation - velocity for speed */
+    var velbase = {}
+    switch(pJS.particles.move.direction){
+      case 'top':
+        velbase = { x:0, y:-1 };
+      break;
+      case 'top-right':
+        velbase = { x:0.5, y:-0.5 };
+      break;
+      case 'right':
+        velbase = { x:1, y:-0 };
+      break;
+      case 'bottom-right':
+        velbase = { x:0.5, y:0.5 };
+      break;
+      case 'bottom':
+        velbase = { x:0, y:1 };
+      break;
+      case 'bottom-left':
+        velbase = { x:-0.5, y:1 };
+      break;
+      case 'left':
+        velbase = { x:-1, y:0 };
+      break;
+      case 'top-left':
+        velbase = { x:-0.5, y:-0.5 };
+      break;
+      default:
+        velbase = { x:0, y:0 };
+      break;
+    }
+
+    if(pJS.particles.move.straight){
+      this.vx = velbase.x;
+      this.vy = velbase.y;
+      if(pJS.particles.move.random){
+        this.vx = this.vx * (Math.random());
+        this.vy = this.vy * (Math.random());
+      }
+    }else{
+      this.vx = velbase.x + Math.random()-0.5;
+      this.vy = velbase.y + Math.random()-0.5;
+    }
+
+    // var theta = 2.0 * Math.PI * Math.random();
+    // this.vx = Math.cos(theta);
+    // this.vy = Math.sin(theta);
+
+    this.vx_i = this.vx;
+    this.vy_i = this.vy;
+
+
+
+    /* if shape is image */
+
+    var shape_type = pJS.particles.shape.type;
+    if(typeof(shape_type) == 'object'){
+      if(shape_type instanceof Array){
+        var shape_selected = shape_type[Math.floor(Math.random() * shape_type.length)];
+        this.shape = shape_selected;
+      }
+    }else{
+      this.shape = shape_type;
+    }
+
+    if(this.shape == 'image'){
+      var sh = pJS.particles.shape;
+      this.img = {
+        src: sh.image.src,
+        ratio: sh.image.width / sh.image.height
+      }
+      if(!this.img.ratio) this.img.ratio = 1;
+      if(pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg != undefined){
+        pJS.fn.vendors.createSvgImg(this);
+        if(pJS.tmp.pushing){
+          this.img.loaded = false;
+        }
+      }
+    }
+
+
+
+  };
+
+
+  pJS.fn.particle.prototype.draw = function() {
+
+    var p = this;
+
+    if(p.radius_bubble != undefined){
+      var radius = p.radius_bubble;
+    }else{
+      var radius = p.radius;
+    }
+
+    if(p.opacity_bubble != undefined){
+      var opacity = p.opacity_bubble;
+    }else{
+      var opacity = p.opacity;
+    }
+
+    if(p.color.rgb){
+      var color_value = 'rgba('+p.color.rgb.r+','+p.color.rgb.g+','+p.color.rgb.b+','+opacity+')';
+    }else{
+      var color_value = 'hsla('+p.color.hsl.h+','+p.color.hsl.s+'%,'+p.color.hsl.l+'%,'+opacity+')';
+    }
+
+    pJS.canvas.ctx.fillStyle = color_value;
+    pJS.canvas.ctx.beginPath();
+
+    switch(p.shape){
+
+      case 'circle':
+        pJS.canvas.ctx.arc(p.x, p.y, radius, 0, Math.PI * 2, false);
+      break;
+
+      case 'edge':
+        pJS.canvas.ctx.rect(p.x-radius, p.y-radius, radius*2, radius*2);
+      break;
+
+      case 'triangle':
+        pJS.fn.vendors.drawShape(pJS.canvas.ctx, p.x-radius, p.y+radius / 1.66, radius*2, 3, 2);
+      break;
+
+      case 'polygon':
+        pJS.fn.vendors.drawShape(
+          pJS.canvas.ctx,
+          p.x - radius / (pJS.particles.shape.polygon.nb_sides/3.5), // startX
+          p.y - radius / (2.66/3.5), // startY
+          radius*2.66 / (pJS.particles.shape.polygon.nb_sides/3), // sideLength
+          pJS.particles.shape.polygon.nb_sides, // sideCountNumerator
+          1 // sideCountDenominator
+        );
+      break;
+
+      case 'star':
+        pJS.fn.vendors.drawShape(
+          pJS.canvas.ctx,
+          p.x - radius*2 / (pJS.particles.shape.polygon.nb_sides/4), // startX
+          p.y - radius / (2*2.66/3.5), // startY
+          radius*2*2.66 / (pJS.particles.shape.polygon.nb_sides/3), // sideLength
+          pJS.particles.shape.polygon.nb_sides, // sideCountNumerator
+          2 // sideCountDenominator
+        );
+      break;
+
+      case 'image':
+
+        function draw(){
+          pJS.canvas.ctx.drawImage(
+            img_obj,
+            p.x-radius,
+            p.y-radius,
+            radius*2,
+            radius*2 / p.img.ratio
+          );
+        }
+
+        if(pJS.tmp.img_type == 'svg'){
+          var img_obj = p.img.obj;
+        }else{
+          var img_obj = pJS.tmp.img_obj;
+        }
+
+        if(img_obj){
+          draw();
+        }
+
+      break;
+
+    }
+
+    pJS.canvas.ctx.closePath();
+
+    if(pJS.particles.shape.stroke.width > 0){
+      pJS.canvas.ctx.strokeStyle = pJS.particles.shape.stroke.color;
+      pJS.canvas.ctx.lineWidth = pJS.particles.shape.stroke.width;
+      pJS.canvas.ctx.stroke();
+    }
+
+    pJS.canvas.ctx.fill();
+
+  };
+
+
+  pJS.fn.particlesCreate = function(){
+    for(var i = 0; i < pJS.particles.number.value; i++) {
+      pJS.particles.array.push(new pJS.fn.particle(pJS.particles.color, pJS.particles.opacity.value));
+    }
+  };
+
+  pJS.fn.particlesUpdate = function(){
+
+    for(var i = 0; i < pJS.particles.array.length; i++){
+
+      /* the particle */
+      var p = pJS.particles.array[i];
+
+      // var d = ( dx = pJS.interactivity.mouse.click_pos_x - p.x ) * dx + ( dy = pJS.interactivity.mouse.click_pos_y - p.y ) * dy;
+      // var f = -BANG_SIZE / d;
+      // if ( d < BANG_SIZE ) {
+      //     var t = Math.atan2( dy, dx );
+      //     p.vx = f * Math.cos(t);
+      //     p.vy = f * Math.sin(t);
+      // }
+
+      /* move the particle */
+      if(pJS.particles.move.enable){
+        var ms = pJS.particles.move.speed/2;
+        p.x += p.vx * ms;
+        p.y += p.vy * ms;
+      }
+
+      /* change opacity status */
+      if(pJS.particles.opacity.anim.enable) {
+        if(p.opacity_status == true) {
+          if(p.opacity >= pJS.particles.opacity.value) p.opacity_status = false;
+          p.opacity += p.vo;
+        }else {
+          if(p.opacity <= pJS.particles.opacity.anim.opacity_min) p.opacity_status = true;
+          p.opacity -= p.vo;
+        }
+        if(p.opacity < 0) p.opacity = 0;
+      }
+
+      /* change size */
+      if(pJS.particles.size.anim.enable){
+        if(p.size_status == true){
+          if(p.radius >= pJS.particles.size.value) p.size_status = false;
+          p.radius += p.vs;
+        }else{
+          if(p.radius <= pJS.particles.size.anim.size_min) p.size_status = true;
+          p.radius -= p.vs;
+        }
+        if(p.radius < 0) p.radius = 0;
+      }
+
+      /* change particle position if it is out of canvas */
+      if(pJS.particles.move.out_mode == 'bounce'){
+        var new_pos = {
+          x_left: p.radius,
+          x_right:  pJS.canvas.w,
+          y_top: p.radius,
+          y_bottom: pJS.canvas.h
+        }
+      }else{
+        var new_pos = {
+          x_left: -p.radius,
+          x_right: pJS.canvas.w + p.radius,
+          y_top: -p.radius,
+          y_bottom: pJS.canvas.h + p.radius
+        }
+      }
+
+      if(p.x - p.radius > pJS.canvas.w){
+        p.x = new_pos.x_left;
+        p.y = Math.random() * pJS.canvas.h;
+      }
+      else if(p.x + p.radius < 0){
+        p.x = new_pos.x_right;
+        p.y = Math.random() * pJS.canvas.h;
+      }
+      if(p.y - p.radius > pJS.canvas.h){
+        p.y = new_pos.y_top;
+        p.x = Math.random() * pJS.canvas.w;
+      }
+      else if(p.y + p.radius < 0){
+        p.y = new_pos.y_bottom;
+        p.x = Math.random() * pJS.canvas.w;
+      }
+
+      /* out of canvas modes */
+      switch(pJS.particles.move.out_mode){
+        case 'bounce':
+          if (p.x + p.radius > pJS.canvas.w) p.vx = -p.vx;
+          else if (p.x - p.radius < 0) p.vx = -p.vx;
+          if (p.y + p.radius > pJS.canvas.h) p.vy = -p.vy;
+          else if (p.y - p.radius < 0) p.vy = -p.vy;
+        break;
+      }
+
+      /* events */
+      if(isInArray('grab', pJS.interactivity.events.onhover.mode)){
+        pJS.fn.modes.grabParticle(p);
+      }
+
+      if(isInArray('bubble', pJS.interactivity.events.onhover.mode) || isInArray('bubble', pJS.interactivity.events.onclick.mode)){
+        pJS.fn.modes.bubbleParticle(p);
+      }
+
+      if(isInArray('repulse', pJS.interactivity.events.onhover.mode) || isInArray('repulse', pJS.interactivity.events.onclick.mode)){
+        pJS.fn.modes.repulseParticle(p);
+      }
+
+      /* interaction auto between particles */
+      if(pJS.particles.line_linked.enable || pJS.particles.move.attract.enable){
+        for(var j = i + 1; j < pJS.particles.array.length; j++){
+          var p2 = pJS.particles.array[j];
+
+          /* link particles */
+          if(pJS.particles.line_linked.enable){
+            pJS.fn.interact.linkParticles(p,p2);
+          }
+
+          /* attract particles */
+          if(pJS.particles.move.attract.enable){
+            pJS.fn.interact.attractParticles(p,p2);
+          }
+
+          /* bounce particles */
+          if(pJS.particles.move.bounce){
+            pJS.fn.interact.bounceParticles(p,p2);
+          }
+
+        }
+      }
+
+
+    }
+
+  };
+
+  pJS.fn.particlesDraw = function(){
+
+    /* clear canvas */
+    pJS.canvas.ctx.clearRect(0, 0, pJS.canvas.w, pJS.canvas.h);
+
+    /* update each particles param */
+    pJS.fn.particlesUpdate();
+
+    /* draw each particle */
+    for(var i = 0; i < pJS.particles.array.length; i++){
+      var p = pJS.particles.array[i];
+      p.draw();
+    }
+
+  };
+
+  pJS.fn.particlesEmpty = function(){
+    pJS.particles.array = [];
+  };
+
+  pJS.fn.particlesRefresh = function(){
+
+    /* init all */
+    cancelRequestAnimFrame(pJS.fn.checkAnimFrame);
+    cancelRequestAnimFrame(pJS.fn.drawAnimFrame);
+    pJS.tmp.source_svg = undefined;
+    pJS.tmp.img_obj = undefined;
+    pJS.tmp.count_svg = 0;
+    pJS.fn.particlesEmpty();
+    pJS.fn.canvasClear();
+
+    /* restart */
+    pJS.fn.vendors.start();
+
+  };
+
+
+  /* ---------- pJS functions - particles interaction ------------ */
+
+  pJS.fn.interact.linkParticles = function(p1, p2){
+
+    var dx = p1.x - p2.x,
+        dy = p1.y - p2.y,
+        dist = Math.sqrt(dx*dx + dy*dy);
+
+    /* draw a line between p1 and p2 if the distance between them is under the config distance */
+    if(dist <= pJS.particles.line_linked.distance){
+
+      var opacity_line = pJS.particles.line_linked.opacity - (dist / (1/pJS.particles.line_linked.opacity)) / pJS.particles.line_linked.distance;
+
+      if(opacity_line > 0){
+
+        /* style */
+        var color_line = pJS.particles.line_linked.color_rgb_line;
+        pJS.canvas.ctx.strokeStyle = 'rgba('+color_line.r+','+color_line.g+','+color_line.b+','+opacity_line+')';
+        pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width;
+        //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */
+
+        /* path */
+        pJS.canvas.ctx.beginPath();
+        pJS.canvas.ctx.moveTo(p1.x, p1.y);
+        pJS.canvas.ctx.lineTo(p2.x, p2.y);
+        pJS.canvas.ctx.stroke();
+        pJS.canvas.ctx.closePath();
+
+      }
+
+    }
+
+  };
+
+
+  pJS.fn.interact.attractParticles  = function(p1, p2){
+
+    /* condensed particles */
+    var dx = p1.x - p2.x,
+        dy = p1.y - p2.y,
+        dist = Math.sqrt(dx*dx + dy*dy);
+
+    if(dist <= pJS.particles.line_linked.distance){
+
+      var ax = dx/(pJS.particles.move.attract.rotateX*1000),
+          ay = dy/(pJS.particles.move.attract.rotateY*1000);
+
+      p1.vx -= ax;
+      p1.vy -= ay;
+
+      p2.vx += ax;
+      p2.vy += ay;
+
+    }
+
+
+  }
+
+
+  pJS.fn.interact.bounceParticles = function(p1, p2){
+
+    var dx = p1.x - p2.x,
+        dy = p1.y - p2.y,
+        dist = Math.sqrt(dx*dx + dy*dy),
+        dist_p = p1.radius+p2.radius;
+
+    if(dist <= dist_p){
+      p1.vx = -p1.vx;
+      p1.vy = -p1.vy;
+
+      p2.vx = -p2.vx;
+      p2.vy = -p2.vy;
+    }
+
+  }
+
+
+  /* ---------- pJS functions - modes events ------------ */
+
+  pJS.fn.modes.pushParticles = function(nb, pos){
+
+    pJS.tmp.pushing = true;
+
+    for(var i = 0; i < nb; i++){
+      pJS.particles.array.push(
+        new pJS.fn.particle(
+          pJS.particles.color,
+          pJS.particles.opacity.value,
+          {
+            'x': pos ? pos.pos_x : Math.random() * pJS.canvas.w,
+            'y': pos ? pos.pos_y : Math.random() * pJS.canvas.h
+          }
+        )
+      )
+      if(i == nb-1){
+        if(!pJS.particles.move.enable){
+          pJS.fn.particlesDraw();
+        }
+        pJS.tmp.pushing = false;
+      }
+    }
+
+  };
+
+
+  pJS.fn.modes.removeParticles = function(nb){
+
+    pJS.particles.array.splice(0, nb);
+    if(!pJS.particles.move.enable){
+      pJS.fn.particlesDraw();
+    }
+
+  };
+
+
+  pJS.fn.modes.bubbleParticle = function(p){
+
+    /* on hover event */
+    if(pJS.interactivity.events.onhover.enable && isInArray('bubble', pJS.interactivity.events.onhover.mode)){
+
+      var dx_mouse = p.x - pJS.interactivity.mouse.pos_x,
+          dy_mouse = p.y - pJS.interactivity.mouse.pos_y,
+          dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse),
+          ratio = 1 - dist_mouse / pJS.interactivity.modes.bubble.distance;
+
+      function init(){
+        p.opacity_bubble = p.opacity;
+        p.radius_bubble = p.radius;
+      }
+
+      /* mousemove - check ratio */
+      if(dist_mouse <= pJS.interactivity.modes.bubble.distance){
+
+        if(ratio >= 0 && pJS.interactivity.status == 'mousemove'){
+
+          /* size */
+          if(pJS.interactivity.modes.bubble.size != pJS.particles.size.value){
+
+            if(pJS.interactivity.modes.bubble.size > pJS.particles.size.value){
+              var size = p.radius + (pJS.interactivity.modes.bubble.size*ratio);
+              if(size >= 0){
+                p.radius_bubble = size;
+              }
+            }else{
+              var dif = p.radius - pJS.interactivity.modes.bubble.size,
+                  size = p.radius - (dif*ratio);
+              if(size > 0){
+                p.radius_bubble = size;
+              }else{
+                p.radius_bubble = 0;
+              }
+            }
+
+          }
+
+          /* opacity */
+          if(pJS.interactivity.modes.bubble.opacity != pJS.particles.opacity.value){
+
+            if(pJS.interactivity.modes.bubble.opacity > pJS.particles.opacity.value){
+              var opacity = pJS.interactivity.modes.bubble.opacity*ratio;
+              if(opacity > p.opacity && opacity <= pJS.interactivity.modes.bubble.opacity){
+                p.opacity_bubble = opacity;
+              }
+            }else{
+              var opacity = p.opacity - (pJS.particles.opacity.value-pJS.interactivity.modes.bubble.opacity)*ratio;
+              if(opacity < p.opacity && opacity >= pJS.interactivity.modes.bubble.opacity){
+                p.opacity_bubble = opacity;
+              }
+            }
+
+          }
+
+        }
+
+      }else{
+        init();
+      }
+
+
+      /* mouseleave */
+      if(pJS.interactivity.status == 'mouseleave'){
+        init();
+      }
+
+    }
+
+    /* on click event */
+    else if(pJS.interactivity.events.onclick.enable && isInArray('bubble', pJS.interactivity.events.onclick.mode)){
+
+
+      if(pJS.tmp.bubble_clicking){
+        var dx_mouse = p.x - pJS.interactivity.mouse.click_pos_x,
+            dy_mouse = p.y - pJS.interactivity.mouse.click_pos_y,
+            dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse),
+            time_spent = (new Date().getTime() - pJS.interactivity.mouse.click_time)/1000;
+
+        if(time_spent > pJS.interactivity.modes.bubble.duration){
+          pJS.tmp.bubble_duration_end = true;
+        }
+
+        if(time_spent > pJS.interactivity.modes.bubble.duration*2){
+          pJS.tmp.bubble_clicking = false;
+          pJS.tmp.bubble_duration_end = false;
+        }
+      }
+
+
+      function process(bubble_param, particles_param, p_obj_bubble, p_obj, id){
+
+        if(bubble_param != particles_param){
+
+          if(!pJS.tmp.bubble_duration_end){
+            if(dist_mouse <= pJS.interactivity.modes.bubble.distance){
+              if(p_obj_bubble != undefined) var obj = p_obj_bubble;
+              else var obj = p_obj;
+              if(obj != bubble_param){
+                var value = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration);
+                if(id == 'size') p.radius_bubble = value;
+                if(id == 'opacity') p.opacity_bubble = value;
+              }
+            }else{
+              if(id == 'size') p.radius_bubble = undefined;
+              if(id == 'opacity') p.opacity_bubble = undefined;
+            }
+          }else{
+            if(p_obj_bubble != undefined){
+              var value_tmp = p_obj - (time_spent * (p_obj - bubble_param) / pJS.interactivity.modes.bubble.duration),
+                  dif = bubble_param - value_tmp;
+                  value = bubble_param + dif;
+              if(id == 'size') p.radius_bubble = value;
+              if(id == 'opacity') p.opacity_bubble = value;
+            }
+          }
+
+        }
+
+      }
+
+      if(pJS.tmp.bubble_clicking){
+        /* size */
+        process(pJS.interactivity.modes.bubble.size, pJS.particles.size.value, p.radius_bubble, p.radius, 'size');
+        /* opacity */
+        process(pJS.interactivity.modes.bubble.opacity, pJS.particles.opacity.value, p.opacity_bubble, p.opacity, 'opacity');
+      }
+
+    }
+
+  };
+
+
+  pJS.fn.modes.repulseParticle = function(p){
+
+    if(pJS.interactivity.events.onhover.enable && isInArray('repulse', pJS.interactivity.events.onhover.mode) && pJS.interactivity.status == 'mousemove') {
+
+      var dx_mouse = p.x - pJS.interactivity.mouse.pos_x,
+          dy_mouse = p.y - pJS.interactivity.mouse.pos_y,
+          dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse);
+
+      var normVec = {x: dx_mouse/dist_mouse, y: dy_mouse/dist_mouse},
+          repulseRadius = pJS.interactivity.modes.repulse.distance,
+          velocity = 100,
+          repulseFactor = clamp((1/repulseRadius)*(-1*Math.pow(dist_mouse/repulseRadius,2)+1)*repulseRadius*velocity, 0, 50);
+
+      var pos = {
+        x: p.x + normVec.x * repulseFactor,
+        y: p.y + normVec.y * repulseFactor
+      }
+
+      if(pJS.particles.move.out_mode == 'bounce'){
+        if(pos.x - p.radius > 0 && pos.x + p.radius < pJS.canvas.w) p.x = pos.x;
+        if(pos.y - p.radius > 0 && pos.y + p.radius < pJS.canvas.h) p.y = pos.y;
+      }else{
+        p.x = pos.x;
+        p.y = pos.y;
+      }
+
+    }
+
+
+    else if(pJS.interactivity.events.onclick.enable && isInArray('repulse', pJS.interactivity.events.onclick.mode)) {
+
+      if(!pJS.tmp.repulse_finish){
+        pJS.tmp.repulse_count++;
+        if(pJS.tmp.repulse_count == pJS.particles.array.length){
+          pJS.tmp.repulse_finish = true;
+        }
+      }
+
+      if(pJS.tmp.repulse_clicking){
+
+        var repulseRadius = Math.pow(pJS.interactivity.modes.repulse.distance/6, 3);
+
+        var dx = pJS.interactivity.mouse.click_pos_x - p.x,
+            dy = pJS.interactivity.mouse.click_pos_y - p.y,
+            d = dx*dx + dy*dy;
+
+        var force = -repulseRadius / d * 1;
+
+        function process(){
+
+          var f = Math.atan2(dy,dx);
+          p.vx = force * Math.cos(f);
+          p.vy = force * Math.sin(f);
+
+          if(pJS.particles.move.out_mode == 'bounce'){
+            var pos = {
+              x: p.x + p.vx,
+              y: p.y + p.vy
+            }
+            if (pos.x + p.radius > pJS.canvas.w) p.vx = -p.vx;
+            else if (pos.x - p.radius < 0) p.vx = -p.vx;
+            if (pos.y + p.radius > pJS.canvas.h) p.vy = -p.vy;
+            else if (pos.y - p.radius < 0) p.vy = -p.vy;
+          }
+
+        }
+
+        // default
+        if(d <= repulseRadius){
+          process();
+        }
+
+        // bang - slow motion mode
+        // if(!pJS.tmp.repulse_finish){
+        //   if(d <= repulseRadius){
+        //     process();
+        //   }
+        // }else{
+        //   process();
+        // }
+
+
+      }else{
+
+        if(pJS.tmp.repulse_clicking == false){
+
+          p.vx = p.vx_i;
+          p.vy = p.vy_i;
+
+        }
+
+      }
+
+    }
+
+  }
+
+
+  pJS.fn.modes.grabParticle = function(p){
+
+    if(pJS.interactivity.events.onhover.enable && pJS.interactivity.status == 'mousemove'){
+
+      var dx_mouse = p.x - pJS.interactivity.mouse.pos_x,
+          dy_mouse = p.y - pJS.interactivity.mouse.pos_y,
+          dist_mouse = Math.sqrt(dx_mouse*dx_mouse + dy_mouse*dy_mouse);
+
+      /* draw a line between the cursor and the particle if the distance between them is under the config distance */
+      if(dist_mouse <= pJS.interactivity.modes.grab.distance){
+
+        var opacity_line = pJS.interactivity.modes.grab.line_linked.opacity - (dist_mouse / (1/pJS.interactivity.modes.grab.line_linked.opacity)) / pJS.interactivity.modes.grab.distance;
+
+        if(opacity_line > 0){
+
+          /* style */
+          var color_line = pJS.particles.line_linked.color_rgb_line;
+          pJS.canvas.ctx.strokeStyle = 'rgba('+color_line.r+','+color_line.g+','+color_line.b+','+opacity_line+')';
+          pJS.canvas.ctx.lineWidth = pJS.particles.line_linked.width;
+          //pJS.canvas.ctx.lineCap = 'round'; /* performance issue */
+
+          /* path */
+          pJS.canvas.ctx.beginPath();
+          pJS.canvas.ctx.moveTo(p.x, p.y);
+          pJS.canvas.ctx.lineTo(pJS.interactivity.mouse.pos_x, pJS.interactivity.mouse.pos_y);
+          pJS.canvas.ctx.stroke();
+          pJS.canvas.ctx.closePath();
+
+        }
+
+      }
+
+    }
+
+  };
+
+
+
+  /* ---------- pJS functions - vendors ------------ */
+
+  pJS.fn.vendors.eventsListeners = function(){
+
+    /* events target element */
+    if(pJS.interactivity.detect_on == 'window'){
+      pJS.interactivity.el = window;
+    }else{
+      pJS.interactivity.el = pJS.canvas.el;
+    }
+
+
+    /* detect mouse pos - on hover / click event */
+    if(pJS.interactivity.events.onhover.enable || pJS.interactivity.events.onclick.enable){
+
+      /* el on mousemove */
+      pJS.interactivity.el.addEventListener('mousemove', function(e){
+
+        if(pJS.interactivity.el == window){
+          var pos_x = e.clientX,
+              pos_y = e.clientY;
+        }
+        else{
+          var pos_x = e.offsetX || e.clientX,
+              pos_y = e.offsetY || e.clientY;
+        }
+
+        pJS.interactivity.mouse.pos_x = pos_x;
+        pJS.interactivity.mouse.pos_y = pos_y;
+
+        if(pJS.tmp.retina){
+          pJS.interactivity.mouse.pos_x *= pJS.canvas.pxratio;
+          pJS.interactivity.mouse.pos_y *= pJS.canvas.pxratio;
+        }
+
+        pJS.interactivity.status = 'mousemove';
+
+      });
+
+      /* el on onmouseleave */
+      pJS.interactivity.el.addEventListener('mouseleave', function(e){
+
+        pJS.interactivity.mouse.pos_x = null;
+        pJS.interactivity.mouse.pos_y = null;
+        pJS.interactivity.status = 'mouseleave';
+
+      });
+
+    }
+
+    /* on click event */
+    if(pJS.interactivity.events.onclick.enable){
+
+      pJS.interactivity.el.addEventListener('click', function(){
+
+        pJS.interactivity.mouse.click_pos_x = pJS.interactivity.mouse.pos_x;
+        pJS.interactivity.mouse.click_pos_y = pJS.interactivity.mouse.pos_y;
+        pJS.interactivity.mouse.click_time = new Date().getTime();
+
+        if(pJS.interactivity.events.onclick.enable){
+
+          switch(pJS.interactivity.events.onclick.mode){
+
+            case 'push':
+              if(pJS.particles.move.enable){
+                pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse);
+              }else{
+                if(pJS.interactivity.modes.push.particles_nb == 1){
+                  pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb, pJS.interactivity.mouse);
+                }
+                else if(pJS.interactivity.modes.push.particles_nb > 1){
+                  pJS.fn.modes.pushParticles(pJS.interactivity.modes.push.particles_nb);
+                }
+              }
+            break;
+
+            case 'remove':
+              pJS.fn.modes.removeParticles(pJS.interactivity.modes.remove.particles_nb);
+            break;
+
+            case 'bubble':
+              pJS.tmp.bubble_clicking = true;
+            break;
+
+            case 'repulse':
+              pJS.tmp.repulse_clicking = true;
+              pJS.tmp.repulse_count = 0;
+              pJS.tmp.repulse_finish = false;
+              setTimeout(function(){
+                pJS.tmp.repulse_clicking = false;
+              }, pJS.interactivity.modes.repulse.duration*1000)
+            break;
+
+          }
+
+        }
+
+      });
+
+    }
+
+
+  };
+
+  pJS.fn.vendors.densityAutoParticles = function(){
+
+    if(pJS.particles.number.density.enable){
+
+      /* calc area */
+      var area = pJS.canvas.el.width * pJS.canvas.el.height / 1000;
+      if(pJS.tmp.retina){
+        area = area/(pJS.canvas.pxratio*2);
+      }
+
+      /* calc number of particles based on density area */
+      var nb_particles = area * pJS.particles.number.value / pJS.particles.number.density.value_area;
+
+      /* add or remove X particles */
+      var missing_particles = pJS.particles.array.length - nb_particles;
+      if(missing_particles < 0) pJS.fn.modes.pushParticles(Math.abs(missing_particles));
+      else pJS.fn.modes.removeParticles(missing_particles);
+
+    }
+
+  };
+
+
+  pJS.fn.vendors.checkOverlap = function(p1, position){
+    for(var i = 0; i < pJS.particles.array.length; i++){
+      var p2 = pJS.particles.array[i];
+
+      var dx = p1.x - p2.x,
+          dy = p1.y - p2.y,
+          dist = Math.sqrt(dx*dx + dy*dy);
+
+      if(dist <= p1.radius + p2.radius){
+        p1.x = position ? position.x : Math.random() * pJS.canvas.w;
+        p1.y = position ? position.y : Math.random() * pJS.canvas.h;
+        pJS.fn.vendors.checkOverlap(p1);
+      }
+    }
+  };
+
+
+  pJS.fn.vendors.createSvgImg = function(p){
+
+    /* set color to svg element */
+    var svgXml = pJS.tmp.source_svg,
+        rgbHex = /#([0-9A-F]{3,6})/gi,
+        coloredSvgXml = svgXml.replace(rgbHex, function (m, r, g, b) {
+          if(p.color.rgb){
+            var color_value = 'rgba('+p.color.rgb.r+','+p.color.rgb.g+','+p.color.rgb.b+','+p.opacity+')';
+          }else{
+            var color_value = 'hsla('+p.color.hsl.h+','+p.color.hsl.s+'%,'+p.color.hsl.l+'%,'+p.opacity+')';
+          }
+          return color_value;
+        });
+
+    /* prepare to create img with colored svg */
+    var svg = new Blob([coloredSvgXml], {type: 'image/svg+xml;charset=utf-8'}),
+        DOMURL = window.URL || window.webkitURL || window,
+        url = DOMURL.createObjectURL(svg);
+
+    /* create particle img obj */
+    var img = new Image();
+    img.addEventListener('load', function(){
+      p.img.obj = img;
+      p.img.loaded = true;
+      DOMURL.revokeObjectURL(url);
+      pJS.tmp.count_svg++;
+    });
+    img.src = url;
+
+  };
+
+
+  pJS.fn.vendors.destroypJS = function(){
+    cancelAnimationFrame(pJS.fn.drawAnimFrame);
+    canvas_el.remove();
+    pJSDom = null;
+  };
+
+
+  pJS.fn.vendors.drawShape = function(c, startX, startY, sideLength, sideCountNumerator, sideCountDenominator){
+
+    // By Programming Thomas - https://programmingthomas.wordpress.com/2013/04/03/n-sided-shapes/
+    var sideCount = sideCountNumerator * sideCountDenominator;
+    var decimalSides = sideCountNumerator / sideCountDenominator;
+    var interiorAngleDegrees = (180 * (decimalSides - 2)) / decimalSides;
+    var interiorAngle = Math.PI - Math.PI * interiorAngleDegrees / 180; // convert to radians
+    c.save();
+    c.beginPath();
+    c.translate(startX, startY);
+    c.moveTo(0,0);
+    for (var i = 0; i < sideCount; i++) {
+      c.lineTo(sideLength,0);
+      c.translate(sideLength,0);
+      c.rotate(interiorAngle);
+    }
+    //c.stroke();
+    c.fill();
+    c.restore();
+
+  };
+
+  pJS.fn.vendors.exportImg = function(){
+    window.open(pJS.canvas.el.toDataURL('image/png'), '_blank');
+  };
+
+
+  pJS.fn.vendors.loadImg = function(type){
+
+    pJS.tmp.img_error = undefined;
+
+    if(pJS.particles.shape.image.src != ''){
+
+      if(type == 'svg'){
+
+        var xhr = new XMLHttpRequest();
+        xhr.open('GET', pJS.particles.shape.image.src);
+        xhr.onreadystatechange = function (data) {
+          if(xhr.readyState == 4){
+            if(xhr.status == 200){
+              pJS.tmp.source_svg = data.currentTarget.response;
+              pJS.fn.vendors.checkBeforeDraw();
+            }else{
+              console.log('Error pJS - Image not found');
+              pJS.tmp.img_error = true;
+            }
+          }
+        }
+        xhr.send();
+
+      }else{
+
+        var img = new Image();
+        img.addEventListener('load', function(){
+          pJS.tmp.img_obj = img;
+          pJS.fn.vendors.checkBeforeDraw();
+        });
+        img.src = pJS.particles.shape.image.src;
+
+      }
+
+    }else{
+      console.log('Error pJS - No image.src');
+      pJS.tmp.img_error = true;
+    }
+
+  };
+
+
+  pJS.fn.vendors.draw = function(){
+
+    if(pJS.particles.shape.type == 'image'){
+
+      if(pJS.tmp.img_type == 'svg'){
+
+        if(pJS.tmp.count_svg >= pJS.particles.number.value){
+          pJS.fn.particlesDraw();
+          if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame);
+          else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
+        }else{
+          //console.log('still loading...');
+          if(!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
+        }
+
+      }else{
+
+        if(pJS.tmp.img_obj != undefined){
+          pJS.fn.particlesDraw();
+          if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame);
+          else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
+        }else{
+          if(!pJS.tmp.img_error) pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
+        }
+
+      }
+
+    }else{
+      pJS.fn.particlesDraw();
+      if(!pJS.particles.move.enable) cancelRequestAnimFrame(pJS.fn.drawAnimFrame);
+      else pJS.fn.drawAnimFrame = requestAnimFrame(pJS.fn.vendors.draw);
+    }
+
+  };
+
+
+  pJS.fn.vendors.checkBeforeDraw = function(){
+
+    // if shape is image
+    if(pJS.particles.shape.type == 'image'){
+
+      if(pJS.tmp.img_type == 'svg' && pJS.tmp.source_svg == undefined){
+        pJS.tmp.checkAnimFrame = requestAnimFrame(check);
+      }else{
+        //console.log('images loaded! cancel check');
+        cancelRequestAnimFrame(pJS.tmp.checkAnimFrame);
+        if(!pJS.tmp.img_error){
+          pJS.fn.vendors.init();
+          pJS.fn.vendors.draw();
+        }
+
+      }
+
+    }else{
+      pJS.fn.vendors.init();
+      pJS.fn.vendors.draw();
+    }
+
+  };
+
+
+  pJS.fn.vendors.init = function(){
+
+    /* init canvas + particles */
+    pJS.fn.retinaInit();
+    pJS.fn.canvasInit();
+    pJS.fn.canvasSize();
+    pJS.fn.canvasPaint();
+    pJS.fn.particlesCreate();
+    pJS.fn.vendors.densityAutoParticles();
+
+    /* particles.line_linked - convert hex colors to rgb */
+    pJS.particles.line_linked.color_rgb_line = hexToRgb(pJS.particles.line_linked.color);
+
+  };
+
+
+  pJS.fn.vendors.start = function(){
+
+    if(isInArray('image', pJS.particles.shape.type)){
+      pJS.tmp.img_type = pJS.particles.shape.image.src.substr(pJS.particles.shape.image.src.length - 3);
+      pJS.fn.vendors.loadImg(pJS.tmp.img_type);
+    }else{
+      pJS.fn.vendors.checkBeforeDraw();
+    }
+
+  };
+
+
+
+
+  /* ---------- pJS - start ------------ */
+
+
+  pJS.fn.vendors.eventsListeners();
+
+  pJS.fn.vendors.start();
+
+
+
+};
+
+/* ---------- global functions - vendors ------------ */
+
+Object.deepExtend = function(destination, source) {
+  for (var property in source) {
+    if (source[property] && source[property].constructor &&
+     source[property].constructor === Object) {
+      destination[property] = destination[property] || {};
+      arguments.callee(destination[property], source[property]);
+    } else {
+      destination[property] = source[property];
+    }
+  }
+  return destination;
+};
+
+window.requestAnimFrame = (function(){
+  return  window.requestAnimationFrame ||
+    window.webkitRequestAnimationFrame ||
+    window.mozRequestAnimationFrame    ||
+    window.oRequestAnimationFrame      ||
+    window.msRequestAnimationFrame     ||
+    function(callback){
+      window.setTimeout(callback, 1000 / 60);
+    };
+})();
+
+window.cancelRequestAnimFrame = ( function() {
+  return window.cancelAnimationFrame         ||
+    window.webkitCancelRequestAnimationFrame ||
+    window.mozCancelRequestAnimationFrame    ||
+    window.oCancelRequestAnimationFrame      ||
+    window.msCancelRequestAnimationFrame     ||
+    clearTimeout
+} )();
+
+function hexToRgb(hex){
+  // By Tim Down - https://stackoverflow.com/a/5624139/3493650
+  // 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;
+};
+
+function clamp(number, min, max) {
+  return Math.min(Math.max(number, min), max);
+};
+
+function isInArray(value, array) {
+  return array.indexOf(value) > -1;
+}
+
+
+/* ---------- particles.js functions - start ------------ */
+
+window.pJSDom = [];
+
+window.particlesJS = function(tag_id, params){
+
+  //console.log(params);
+
+  /* no string id? so it's object params, and set the id with default id */
+  if(typeof(tag_id) != 'string'){
+    params = tag_id;
+    tag_id = 'particles-js';
+  }
+
+  /* no id? set the id to default id */
+  if(!tag_id){
+    tag_id = 'particles-js';
+  }
+
+  /* pJS elements */
+  var pJS_tag = document.getElementById(tag_id),
+      pJS_canvas_class = 'particles-js-canvas-el',
+      exist_canvas = pJS_tag.getElementsByClassName(pJS_canvas_class);
+
+  /* remove canvas if exists into the pJS target tag */
+  if(exist_canvas.length){
+    while(exist_canvas.length > 0){
+      pJS_tag.removeChild(exist_canvas[0]);
+    }
+  }
+
+  /* create canvas element */
+  var canvas_el = document.createElement('canvas');
+  canvas_el.className = pJS_canvas_class;
+
+  /* set size canvas */
+  canvas_el.style.width = "100%";
+  canvas_el.style.height = "100%";
+
+  /* append canvas */
+  var canvas = document.getElementById(tag_id).appendChild(canvas_el);
+
+  /* launch particle.js */
+  if(canvas != null){
+    pJSDom.push(new pJS(tag_id, params));
+  }
+
+};
+
+window.particlesJS.load = function(tag_id, path_config_json, callback){
+
+  /* load json config */
+  var xhr = new XMLHttpRequest();
+  xhr.open('GET', path_config_json);
+  xhr.onreadystatechange = function (data) {
+    if(xhr.readyState == 4){
+      if(xhr.status == 200){
+        var params = JSON.parse(data.currentTarget.response);
+        window.particlesJS(tag_id, params);
+        if(callback) callback();
+      }else{
+        console.log('Error pJS - XMLHttpRequest status: '+xhr.status);
+        console.log('Error pJS - File config not found');
+      }
+    }
+  };
+  xhr.send();
+
+};
diff --git a/docs/logos/meson.build b/docs/logos/meson.build
index 70ba67d71c..0038d4fa42 100644
--- a/docs/logos/meson.build
+++ b/docs/logos/meson.build
@@ -18,6 +18,8 @@ docs_logo_files = [
   'logo-square.svg',
   'logo-sticker-hexagon.svg',
   'logo-sticker-square.svg',
+  'penguin.svg',
+  'penguin.png',
 ]
 
 install_data(docs_logo_files, install_dir: docs_html_dir / 'logos')
diff --git a/docs/logos/penguin.png b/docs/logos/penguin.png
new file mode 100644
index 0000000000000000000000000000000000000000..48a481c54b6a71f2d43f5ebdf1d4455202c27d8a
GIT binary patch
literal 4236
zcmV;75OeQ|P)<h;3K|Lk000e1NJLTq002n<004>z1^@s6<_~Aa00009a7bBm000*e
z000*e0m>%xKL7v#8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H15E)5C
zK~#90-JN@I6;+<cKj-E_ZmvWi1QH-@<PktwP%;c6%YwK%NP!NHKp|Bdq1<Jc$6AFY
z1I*S+>F%ntjKU}bItCVNgaIcEGosE8h`3Caw=#uHz)?hA3E?4<L_!|-w}14#m%e>p
z-F>_7P3Bv5D(UXi=XdV6f9Lc$zw`T@Rw6<yD5W|9vw=y#cYqwg4;%;f1HTrLKg2BH
zF9jkJH33tAGk_#W$ACFNMy#?rdWuERzW}W!2-2q`z`g1IK`9{#co?loSs^Hx{J?8K
zZn}S%lSu`L0YE9$5BLPgOR^RJ5qMNYK2Nfob^(v!_&-UWAb{b(ex=mkI#5>H5z-{3
z)Lp<~Ksi``6HqQ9TO2Gi<<M)=Je(y6v?0SrrBu1Y<)#!$L{Lh3f%8BYm*n_?IU=&b
zrSe@xP?IL(9zj7l6-udTF3NBXL6ZJ11+p@LUnr&SOMxs$5hUr}6v@j5DwR^hQY6n2
zD4+*$A;ogP1V)O;<rK@c1rKmXG6nd2J~nRL$ijsSQOfko@it(yQYs@E*{Q^Q>`g%g
zkf~FrN)XRJ`>fUbUr6_q!_?dj_^Fv6l$)Cy_58ef^Fk!GwYABJ5hJY5^}e5O5)vg3
z&?}Lu&7M7*hK2@Se)(lQp75E>rAwDW|9d<hmM&eIs5m_ouudt}*UC-lz#bAj@yg1|
zkhUydyg2My@7}#dkCv7e$;->L+U!1}9owS-P9*vf+qP{Bk#PO`btx<?47+~t;K4AW
zCQqJhr+-_TZZhIEw_6jb+Ue7$L;vUG<gj4Dg0Sl^z4TJpaY;!@q9UVVxl-z{?cC@#
zJZO{9xcK0M55kTge)!?A<IS5l^WlddhK_?OY7yDM2BlP%otxc86A7Av*XxxF7cPXF
zli%-`E?v5W<tZ#Il)ZcRN>fvl3>h-S_K*VjVY-`Fv_8p{sdmYdC1L%}z<~oJ@_0O+
z2=Z*C>p*F`iP0wov=M2cj6haa7JK&WVbrKm`2BwJ^YgiS^(p`b`Ec(T7(W`icY`js
z!lf&)eFtpX1~yvX4iS04@?9?CIwqfTmo8mo{rdIt{PWKXAS3TYwpJi*XAwCcJ+1gR
z+Zy#~x;03zN#9`6uHE?-`1uN$@lfKtr_Oa{;OKf<TU)J&05}T_5s?O~H#!XuTE}e~
zCB?A+U2_BhlnnYOs;jHH?>@_Bq&~optls1nKF4Ge$jCsB?2O&CBGScCBL2qE8*%mO
z)zYa`Co4_v2GBp<%?;py*-B6N8w&(|T`uDPR*cxYcdzvB-P;OLZ>5_g4`ABx_02UC
znX<kMU4IK*{u`7x;_}^h-_3yo2e{)7(`KxhN~zIiGrNUfVBxJ(wPUu(h5kCN@rYa;
zA@a>rA~$wLXih9#ym(Q{%F4{j{V3hd?PaqB<>ux}Lqmi1EB*CRo0An>y?Rydy6Y~p
zL`_O3NsG*q^vENRBt=Rz)Ya8VX=$k$`G*Ls6Q(+OfSXhN1eiE+qS>sbQBY98d+)tR
zj~+b|Wi3PVnMw;rVp5Zi9692+CIwMdRVCTk*@^1#F=@~yfSzUu@_N0}+}!LMNg^Vz
zzWS<}hcz|jBn6ndny7L^hYod>pdiZ2%M+1wFx4alXaAoiYLk8Yrp8vkdi81s4H^_L
z%Xd<}R30Shc%qEO#l<dHMT5`h<E^*eB0D=fUbY{mq%r{nNji}zV}5?V%T>9Fva&Lk
zFJB%n%l%5JvJ_P(MADxUW%T)cE>|TSPe1)MWo2=BSeB}~1W7uQC}VDJuFF*kM`mUw
z6%`d^WyM?<ctj~RH5GLUl5`5V5-($?PMut@j1j|!4`<P$Me(w%R!VhsyB;0@5fL=k
z<e0EwS#$)?Jo8Mvb-W_rCvMk4zX0%Yyo@a^EiP9jI=o&lFTVI<yezYoQV+UW2R%uL
z<7I4aZg#UCaWQ@RbjFPv7ca{?rBs1i^7SOejW(*UuXn2srm$+&stAu39SYG_f1P%*
z26QBe$T{Lz1oSR8>A>*e!+Gqn$Kqvq5cmf-WE&q%Ol`V+IsNX9&pr2CqLKJZN~ySW
z8aptOR23`Z#ful6mTeg&B_%xZ#1rwd_<*;SQqGQi^|M`I7%^tw{rdHD+W=q+=g*%P
zuh*N%Qm{#!HkHUtk`z!IO>kCLmbA9Erjn1Cm_L8M8N1UWaf2k$h}slO@K;}bl}avR
z;`Hg$W~`X4zyso-JxPxpv6|+oQ>V<fVpmaIT+GawGZSSChG`5)R$erccE-wFU0t25
zJjbwP$&z^ErXUJ{|4~X6CL=3Ciin&>^ACu?v17-Qk>w}`4<5{e51N{d^aFmE%!oYJ
zqljv$JbLt~T{*5`;lhPxvknJ7Qc5NCS&xcn9To61qI8D&`S~gAo)g3G_sihHgH0`m
z1=0ziNF3<6LEAJT0+%me4s%ydH<VJ$pFiKqEv3Nkl~T7@xyceq0Q>gsvr@k6C@(LM
zW?F6pR-%QB88H-?Zbe2clFr4t;lP0dR?2rBojZ4C+O&wMRUc3xBK^=7Unc@{fhB>z
zZ(9-)tqr2Zz(sA>W5$e0b=Q*+d-m*!R@Wus)`(j)l6u4<$?Nq>Q&Uq4DM$>z-!Fp(
z4Kmhsk2t)7Ys6(VC-?|lzkZ#)d-qz>R=1&)V$Pg7#?zyfQr(>>KPpKg(ng%s6MY;>
zd%?_^GmYV`8EAb`(ug;%k7<hBwQH9XvK>WnaWVJYbC2<~BT)tMNNS9AVgLU9G&H2$
z2R#BaX3Q|2a>TJB9!WQ1U1(`(Vdu`6^Wd~$+O%n8WP}BX_fkp?PPSZIBmunh&O6D<
zb{vI;g^U?9#&|kuZHh-y+;EER+qdKQM|}aN6%RcW5zjFp*)nZwQe9mg2M-=hR=(pf
zMs)(?lU<LCM^eJY|83j0B_rDv3>Ywg($Z4>sct}0{Zc%VB4Zv5*s^6yaD?8jFl9=F
zCv#F^7fF$H^5n^O+2roO|9;~s*GP)y|M!jY#OBSLlacQV#*Q6p^ruQLfG5_Z4sFD(
zpD_B-HlwQ6pYPMBkF>S5rD`r~8e_+f)z`fZxK*5-;3}oE(K<XmR*T3^;8p#}vuDq;
zd-v|79+VrHFd^*C??JP3+d{17mQVOjf&YsZr~|M8%{e}Vii!%m@?FFD@#BrB?zCN4
zH1z^*Cp?~`tKs@jbk`*(Cr7Saxst-hlo$;S4U(Cesc&)>;;=pGG@7ZT5||(&UmLEk
zMr-E)G&MD`Y11a#56TVrd_Hc!{dWB+XPcX7VdmXM<WUj18s+*);Me+dZ@lq_-8#62
zQKLrb5ARS)W!fnx<`6(6;VAJx(9{@!Pd@pCs;ZdoO=*FFq#WR{?39ymF+CPU<Ol}m
zidSBF#cpw~VA!yTof-$)D#&hU7ErYD=sNJs%uG3R=8UTyvK2HmG>FIJ(Rbbp#KDf+
zEWx{h{|g?swzjf%?b>9DbQC_H&*&&`%MB+fk^=a5{lVI`YulA+cj(Ze`a@gIEoqWO
z<bAZkCZPEXy!P5_$rkGv1`G&O9$P_sPG}Oqzv>TGtXM%)lcjxPT!W#x71=4n5t2k?
z4-ne;t+uw7H{X2IiK3InZMWT~Kg?H3CEUX<0(N@)XbBjF?oC%xQqnG#!$1A>Q{(b?
ziKO!FW*~b%@T=hQsZ*zT{q@(KD&95<3nSbmt@}?}Wi?(x+Z{Qy+h9RKfn2+G&3$!=
zgSNId$;imiYm#NRWydrrK=_wv8x8|nsKu&PtDF>J86J;^u3f`|E3JQW#44+a63`i_
zA@Jn#^75pnrp9%Ri3fvQ&g^LQj+Y$OqyXvl!1CbnwQJW{wk&0V*HMv^6ZS&0tVxcO
z6u{4ckQ38}4I9$xMra8CiW>_S4X%(RB7QXA#1;U*-_MgzKAE;<@dPn)3E`F|AtFbC
zknLpOzJ07&v&JoPabY67BL4jj)gu<3K^L;Rbm>ygoQdiE>>3&yW4c<If~zEnW(IzO
zfCaO@zMi>r=ej8}DhwpWbp(k9w>1e7*#U&w+57Lm&$@N%+!h-KgXt#1W}gsN#|(B7
zkc+lQlK|rL`Q-TV<8BR$!qD2<Dp^@s`f<_}tMzwXlL8OoTePpBwt=Spvu4do$%YIy
zH8sZFF(UjK5+Nld0f<Nyx<TgQ!-rYCc(LnpqXZ0{w_Hdv&B$s(dnms{XS%4UsBo>R
zi9~Fs*9v4?u6HWP2@|M=e4Y(f-0Stqp+kpUBkA$SAJ>!grL8)rlAw@KxCfm{rB|<B
za`NO!S4c8UZg+GTk^)3cNB0qj{{8z)ZEdZi1f4i>!f2Iz&Q4uZ)^Qsl+80e<2p)g&
z#TQJPG|ACDQ7S7dji>BI4y8+z0@~yOeu?gB4;eB<&Ye4#v=%itH%m!LiP76F-&Q@-
zNm78QEZ`kIQ3D4Kl+Ql<EGd%KuU~I$YP)UMG2J8uh|0z=G|0=#ldW5~+9If~u1<=I
zii{-9@6aR#h`I%BcZ<*u^LRY+)KgDMV`HOff|{F~WzwWcMuKhtg&m$Gxv5bR8_JZF
zlt^V|WkN6ChK7b{1PSo7r0SJ!Vxkb`0aiu%p;B5}Dl1p6l<Mm0FmmeZ>SWEDHL<>8
zaRcc47e$hetXV*PG_nBc)~%cL>(@{E^yw4zx-KnFwl1NsythXvrG@|-vF~2I1DGh9
z&E0-I+A~Q2N+}PTb@RWlwj8t*_<;xwoMQkP^hfB9Hh1if1WYE1CKP|Bl|R#mQy>w$
zj<$P}f>28J1Qr1Qh_-J*EPOj+7{C3)HZFN{7>Ty+hS`nq>QTIl@GFY=4M;0=Ad-Rz
zxXyeJ7zcb8?VFi7J-TyY+GK87_9Xp#^~BPKEirH$m_?C%+~G)yK#lqV+D`8yT`2t;
ziqRdHq#9L*_Vu2z#w*zLXp4cj(`E&*jkC&2t$GFRYhp%%0OkR|aZuufjz!Wrbtf4d
zLRdg-`}`;rKSCG#k|ewxhol-c3lICyBB<hFG=*{uou5>aINCA#q#Csh&0pM1==w#9
zqz4z258()6&%r$e#f<i^N$1s%(Sm)<#`YI*T6LwTG}6YSXkTZwi+3rME$y468np<;
z)0XecK-*KYxBL+g(+Ri$Sws~cW<UxTCoSW=dI)VQVjW61!tN>C@h}mnvrxk8G;$w3
zr4iaINnfj5@vsKF_45$Ee%@1d(Z&ei_X+O&6T$-&$y^3V$os@rB%$kQ;#t7n?xFym
zC>vz;k#oTJs8u5o<^p5TzP0N|i!=UdV7?Y%F=&5pw@a!sE!3iUqS%4<c5@KzuemjI
zH}(mFIJ-dGMf)R3*>FY=zkj7eH3^~pk>ufHVr<`N0$V8*$9<vOA4xssOSBaet2jU=
z<xW=z+9OE-S^P8VX8c?NTm@d>2ID9YSK?aR-`gz&<>5S2Q2agG?ht)}Z_!pzJ_q*U
iXD2OGQ6jE}Y5qTwpyTPgK1_}P0000<MNUMnLSTZ-C<_w+

literal 0
HcmV?d00001

diff --git a/docs/logos/penguin.svg b/docs/logos/penguin.svg
new file mode 100644
index 0000000000..9938dc35ab
--- /dev/null
+++ b/docs/logos/penguin.svg
@@ -0,0 +1,358 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="46.727402"
+   height="88.205261"
+   viewBox="0 0 12.363292 23.337642"
+   version="1.1"
+   id="svg2754"
+   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+   sodipodi:docname="penguin.svg"
+   inkscape:export-filename="../logo-square.png"
+   inkscape:export-xdpi="150"
+   inkscape:export-ydpi="150"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs
+     id="defs2748">
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Drop Shadow"
+       id="filter5799-3"
+       x="-0.042427354"
+       y="-0.049351394"
+       width="1.1025327"
+       height="1.1192659">
+      <feFlood
+         flood-opacity="1"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood5801-7" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite5803-4" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="6"
+         result="blur"
+         id="feGaussianBlur5805-3" />
+      <feOffset
+         dx="6"
+         dy="6"
+         result="offset"
+         id="feOffset5807-7" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="composite2"
+         id="feComposite5809-1" />
+    </filter>
+    <mask
+       maskUnits="userSpaceOnUse"
+       id="mask3913-3">
+      <path
+         style="fill:#2f6962"
+         id="path3915-9"
+         d="M 95.108,61.776 244.59,10.309 c 15.666,-5.394 32.738,2.933 38.132,18.599 l 39.799,115.58 c 5.394,15.666 -2.932,32.738 -18.598,38.131 l -103.98,35.801 z"
+         inkscape:connector-curvature="0" />
+    </mask>
+    <mask
+       maskUnits="userSpaceOnUse"
+       id="mask4168-6">
+      <path
+         style="fill:#7c858c"
+         id="path4170-4"
+         d="m 113.124,36.661 c 17.233,36.62 75.371,131.675 105.366,171.054 -4.069,13.505 -18.428,20.812 -30.299,17.44 L 72.849,58.555 C 72.779,46.208 95.748,31.956 113.124,36.661 Z"
+         sodipodi:nodetypes="ccccc"
+         inkscape:connector-curvature="0" />
+    </mask>
+    <filter
+       inkscape:label="Ridged Border"
+       inkscape:menu="Bevels"
+       inkscape:menu-tooltip="Ridged border with inner bevel"
+       style="color-interpolation-filters:sRGB"
+       id="filter5576-4"
+       x="0"
+       y="0"
+       width="1"
+       height="1">
+      <feMorphology
+         radius="4.3"
+         in="SourceAlpha"
+         result="result91"
+         id="feMorphology5578-9" />
+      <feComposite
+         operator="out"
+         in="SourceGraphic"
+         in2="result91"
+         id="feComposite5580-1" />
+      <feGaussianBlur
+         result="result0"
+         stdDeviation="1.2"
+         id="feGaussianBlur5582-2" />
+      <feDiffuseLighting
+         diffuseConstant="1"
+         id="feDiffuseLighting5584-3">
+        <feDistantLight
+           elevation="66"
+           azimuth="225"
+           id="feDistantLight5586-1" />
+      </feDiffuseLighting>
+      <feBlend
+         mode="multiply"
+         in2="SourceGraphic"
+         id="feBlend5588-8" />
+      <feComposite
+         operator="in"
+         in2="SourceAlpha"
+         id="feComposite5590-1" />
+    </filter>
+    <linearGradient
+       id="linearGradient3301">
+      <stop
+         id="stop3303"
+         offset="0"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         id="stop3305"
+         offset="1"
+         style="stop-color:#868686;stop-opacity:1;" />
+    </linearGradient>
+    <mask
+       maskUnits="userSpaceOnUse"
+       id="mask4481-9">
+      <path
+         style="clip-rule:evenodd;fill:#aaaaaa;fill-rule:evenodd"
+         id="path4483-2"
+         d="m 213.96734,236.83626 c -1.97572,1.50086 -5.71734,14.75814 -3.57441,17.84065 3.18643,4.58465 22.42412,-12.69851 20.66112,-17.24646 -1.81294,-4.67518 -15.35694,-1.41227 -17.08671,-0.59419 z m -9.6286,-7.34722 2.48938,3.78195 c 0,0 0.69017,1.04372 0.29628,3.40302 -0.6156,3.68976 -3.17495,13.25876 -3.32578,21.12255 -0.0183,0.94976 -0.91914,9.5039 7.21943,7.07006 8.1393,-2.43309 24.96824,-17.85854 28.33195,-24.69252 3.36372,-6.83399 -0.96483,-10.63865 -6.21591,-10.98709 -5.2511,-0.34921 -13.3456,0.21962 -17.06874,0.84795 -3.72313,0.62832 -3.49361,-0.75818 -6.67723,-4.3506 -0.23896,-0.26961 -5.04938,3.80468 -5.04938,3.80468 z"
+         inkscape:connector-curvature="0" />
+    </mask>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       inkscape:label="Drop Shadow"
+       id="filter6162"
+       x="-0.23894344"
+       y="-0.15139444"
+       width="1.4778869"
+       height="1.3658699">
+      <feFlood
+         flood-opacity="1"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         id="feFlood6164" />
+      <feComposite
+         in="flood"
+         in2="SourceGraphic"
+         operator="in"
+         result="composite1"
+         id="feComposite6166" />
+      <feGaussianBlur
+         in="composite1"
+         stdDeviation="6"
+         result="blur"
+         id="feGaussianBlur6168" />
+      <feOffset
+         dx="6"
+         dy="6"
+         result="offset"
+         id="feOffset6170" />
+      <feComposite
+         in="SourceGraphic"
+         in2="offset"
+         operator="over"
+         result="fbSourceGraphic"
+         id="feComposite6172" />
+      <feColorMatrix
+         result="fbSourceGraphicAlpha"
+         in="fbSourceGraphic"
+         values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
+         id="feColorMatrix6174" />
+      <feFlood
+         id="feFlood6176"
+         flood-opacity="1"
+         flood-color="rgb(0,0,0)"
+         result="flood"
+         in="fbSourceGraphic" />
+      <feComposite
+         id="feComposite6178"
+         in2="fbSourceGraphic"
+         in="flood"
+         operator="in"
+         result="composite1" />
+      <feGaussianBlur
+         id="feGaussianBlur6180"
+         in="composite1"
+         stdDeviation="6"
+         result="blur" />
+      <feOffset
+         id="feOffset6182"
+         dx="-6"
+         dy="6"
+         result="offset" />
+      <feComposite
+         id="feComposite6184"
+         in2="offset"
+         in="fbSourceGraphic"
+         operator="over"
+         result="composite2" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.5027778"
+     inkscape:cx="103.28524"
+     inkscape:cy="68.324084"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g2753-0"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1920"
+     inkscape:window-height="1131"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     units="px"
+     inkscape:showpageshadow="2"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1" />
+  <metadata
+     id="metadata2751">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(15.314622,-233.97731)">
+    <g
+       id="g4944"
+       transform="matrix(0.24862701,-0.09049283,0.09049283,0.24862701,-92.040544,272.60195)">
+      <g
+         transform="rotate(19,545.20406,769.86173)"
+         id="g4988-0">
+        <g
+           id="g5736-0">
+          <text
+             xml:space="preserve"
+             style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+             x="137.92564"
+             y="176.00142"
+             id="text5724-0"
+             transform="rotate(1.0000002)"><tspan
+               sodipodi:role="line"
+               id="tspan5726-6"
+               x="137.92564"
+               y="176.00142"
+               style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:180px;font-family:Overpass;-inkscape-font-specification:'Overpass Bold';letter-spacing:-10px;fill:#e6e6e6;fill-opacity:1" /></text>
+          <g
+             id="g2753-0"
+             transform="translate(-18.749067,-72.815807)">
+            <path
+               d="m 122.88954,229.92364 c 3.341,7.154 7.81,5.77 7.88,5.746 1.371,-0.472 5.829,-2.582 4.696,-5.873 -1.025,-2.977 -8.385,-3.373 -13.437,-1.633 -5.015,1.727 -10.665,6.602 -9.64,9.579 1.133,3.291 5.946,2.208 7.316,1.737 0.069,-0.024 4.799,-0.91 2.672,-9.379 -0.078,-0.312 0.404,-0.412 0.513,-0.177 z"
+               id="path2755-8"
+               style="clip-rule:evenodd;fill:#ffe600;fill-rule:evenodd"
+               inkscape:connector-curvature="0"
+               inkscape:export-filename="../penguin.png"
+               inkscape:export-xdpi="150"
+               inkscape:export-ydpi="150" />
+            <path
+               d="m 93.013543,187.02664 c -4.764,-17.913 -3.791,-32.973 2.812,-35.247 6.601997,-2.273 16.640997,8.995 23.914997,26.044 1.745,2.829 5.157,8.08 7.865,10.513 1.976,1.775 1.993,2.732 -1.079,2.212 -0.927,-0.157 -1.726,-0.358 -2.438,-0.662 5.736,19.254 5.048,35.957 -1.934,38.36 -6.981,2.404 -17.806,-10.334 -25.140997,-29.038 -0.374,0.678 -0.879,1.328 -1.513,2.022 -2.1,2.301 -2.676,1.537 -2.212,-1.079 0.636,-3.584 0.092,-9.822 -0.275,-13.125 z"
+               id="path2757-1"
+               style="fill:#000000;fill-opacity:1"
+               inkscape:connector-curvature="0" />
+            <path
+               d="m 100.36354,200.08264 c -5.497997,-15.966 -5.113997,-30.576 0.857,-32.632 5.971,-2.056 15.268,9.22 20.766,25.187 5.498,15.966 5.114,30.576 -0.857,32.632 -5.971,2.056 -15.268,-9.22 -20.766,-25.187 z m 0.699,-36.784 c -0.536,-1.558 -0.428,-3.008 0.243,-3.239 0.671,-0.231 1.649,0.845 2.186,2.402 0.536,1.558 0.427,3.008 -0.243,3.239 -0.671,0.231 -1.65,-0.845 -2.186,-2.402 z m -4.700997,1.618 c -0.536,-1.558 -0.427,-3.008 0.243,-3.239 0.67,-0.231 1.649,0.845 2.185,2.402 0.536,1.557 0.428,3.008 -0.243,3.239 -0.671,0.231 -1.649,-0.844 -2.185,-2.402 z"
+               id="path2759-9"
+               style="clip-rule:evenodd;fill:#ffffff;fill-opacity:1;fill-rule:evenodd"
+               inkscape:connector-curvature="0" />
+            <g
+               id="g2761-8"
+               transform="translate(-7.9684573,2.7456446)">
+              <ellipse
+                 transform="matrix(-0.9455,0.3255,-0.3255,-0.9455,267.4128,287.6192)"
+                 cx="109.643"
+                 cy="166.18201"
+                 rx="4.428"
+                 ry="2.142"
+                 id="ellipse2763-1"
+                 style="fill:#ffe600" />
+              <path
+                 d=""
+                 id="path2765-6"
+                 style="fill:#ffe600"
+                 inkscape:connector-curvature="0" />
+            </g>
+          </g>
+        </g>
+        <text
+           transform="rotate(1.0000002)"
+           id="text5053-7"
+           y="53.587437"
+           x="-408.52026"
+           style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           xml:space="preserve"><tspan
+             y="53.587437"
+             x="-408.52026"
+             id="tspan5055-2"
+             sodipodi:role="line" /></text>
+        <text
+           transform="rotate(1.0000002)"
+           id="text5057-8"
+           y="-232.88652"
+           x="-525.88629"
+           style="font-style:normal;font-weight:normal;font-size:90px;line-height:125%;font-family:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           xml:space="preserve"><tspan
+             y="-232.88652"
+             x="-525.88629"
+             id="tspan5059-1"
+             sodipodi:role="line" /></text>
+        <text
+           transform="rotate(1.0000002)"
+           id="text5069-7"
+           y="-200.07454"
+           x="543.02759"
+           style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           xml:space="preserve"><tspan
+             y="-200.07454"
+             x="543.02759"
+             id="tspan5071-7"
+             sodipodi:role="line" /></text>
+      </g>
+      <text
+         xml:space="preserve"
+         style="font-style:normal;font-weight:normal;font-size:40px;line-height:25px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         x="582.63574"
+         y="-37.070248"
+         id="text4950"><tspan
+           sodipodi:role="line"
+           id="tspan4948"
+           x="582.63574"
+           y="-37.070248" /></text>
+    </g>
+  </g>
+</svg>
diff --git a/docs/page.xsl b/docs/page.xsl
index 548d5a11c4..7c4f295053 100644
--- a/docs/page.xsl
+++ b/docs/page.xsl
@@ -116,6 +116,9 @@
         <script type="text/javascript" src="{$href_base}js/main.js">
           <xsl:comment>// forces non-empty element</xsl:comment>
         </script>
+        <script type="text/javascript" src="{$href_base}js/particles.js">
+          <xsl:comment>// forces non-empty element</xsl:comment>
+        </script>
       </head>
       <body onload="pageload()">
         <div id="body">
@@ -197,6 +200,11 @@
           </div>
           <br class="clear"/>
         </div>
+        <xsl:if test="$pagesrc = 'docs/404.html.in'">
+          <script src="{$href_base}js/app.js">
+            <xsl:comment>// forces non-empty element</xsl:comment>
+          </script>
+        </xsl:if>
       </body>
     </html>
   </xsl:template>
-- 
2.39.1

Re: [libvirt PATCH] docs: introduce a more interesting 404 error page
Posted by Andrea Bolognani 1 year, 2 months ago
On Thu, Feb 16, 2023 at 02:53:13PM +0000, Daniel P. Berrangé wrote:
> Our 404 error page is a little bit too boring. The solution is to
> add more penguins !
>
> This relies on MIT licensed javascript code imported from
>
>   https://github.com/VincentGarreau/particles.js
>
> The image is extracted from the existing libvirt logo SVG file.
>
> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
> ---
>  docs/404.html.in       |    7 +-
>  docs/css/libvirt.css   |   12 +
>  docs/js/app.js         |  108 +++
>  docs/js/meson.build    |    2 +
>  docs/js/particles.js   | 1541 ++++++++++++++++++++++++++++++++++++++++
>  docs/logos/meson.build |    2 +
>  docs/logos/penguin.png |  Bin 0 -> 4236 bytes
>  docs/logos/penguin.svg |  358 ++++++++++
>  docs/page.xsl          |    8 +
>  9 files changed, 2035 insertions(+), 3 deletions(-)
>  create mode 100644 docs/js/app.js
>  create mode 100644 docs/js/particles.js
>  create mode 100644 docs/logos/penguin.png
>  create mode 100644 docs/logos/penguin.svg
>
> Yes, this could easily be considered an April Fools' joke, but at
> the same time it is common to have a bit of fun with 404 error
> pages, so it is also a real suggestion :-)

+1 to the April Fools' joke, -1 to actually merging this. Sorry!

-- 
Andrea Bolognani / Red Hat / Virtualization
Re: [libvirt PATCH] docs: introduce a more interesting 404 error page
Posted by Jiri Denemark 1 year, 2 months ago
On Thu, Feb 16, 2023 at 14:53:13 +0000, Daniel P. Berrangé wrote:
> Our 404 error page is a little bit too boring. The solution is to
> add more penguins !
> 
> This relies on MIT licensed javascript code imported from
> 
>   https://github.com/VincentGarreau/particles.js
> 
> The image is extracted from the existing libvirt logo SVG file.
> 
> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>

Looks like April 1st material which was just sent too early :-)

Just drop this patch, please.

Jirka
Re: [libvirt PATCH] docs: introduce a more interesting 404 error page
Posted by Peter Krempa 1 year, 2 months ago
On Thu, Feb 16, 2023 at 14:53:13 +0000, Daniel P. Berrangé wrote:
> Our 404 error page is a little bit too boring. The solution is to
> add more penguins !
> 
> This relies on MIT licensed javascript code imported from
> 
>   https://github.com/VincentGarreau/particles.js
> 
> The image is extracted from the existing libvirt logo SVG file.
> 
> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
> ---
>  docs/404.html.in       |    7 +-
>  docs/css/libvirt.css   |   12 +
>  docs/js/app.js         |  108 +++
>  docs/js/meson.build    |    2 +
>  docs/js/particles.js   | 1541 ++++++++++++++++++++++++++++++++++++++++
>  docs/logos/meson.build |    2 +
>  docs/logos/penguin.png |  Bin 0 -> 4236 bytes
>  docs/logos/penguin.svg |  358 ++++++++++
>  docs/page.xsl          |    8 +
>  9 files changed, 2035 insertions(+), 3 deletions(-)
>  create mode 100644 docs/js/app.js
>  create mode 100644 docs/js/particles.js
>  create mode 100644 docs/logos/penguin.png
>  create mode 100644 docs/logos/penguin.svg
> 
> Yes, this could easily be considered an April Fools' joke, but at
> the same time it is common to have a bit of fun with 404 error
> pages, so it is also a real suggestion :-)
> 
> Live example rendering is at:
> 
>   https://berrange.gitlab.io/-/libvirt/-/jobs/3749944370/artifacts/website/404.html
> 
> The main "cost" of the fun is a few 10's of KB more in the dist
> from the JS/SVG/PNG

TBH ...

> diff --git a/docs/js/app.js b/docs/js/app.js
> new file mode 100644
> index 0000000000..79fae27c2e
> --- /dev/null
> +++ b/docs/js/app.js
> @@ -0,0 +1,108 @@
> +particlesJS('particles-js',
> +  {
> +    "particles": {
> +      "number": {
> +        "value": 20,
> +        "density": {
> +          "enable": true,
> +          "value_area": 800
> +        }
> +      },
> +      "color": {
> +        "value": "#ff0000"
> +      },
> +      "shape": {
> +        "type": "image",

> +          "src": "logos/penguin.png",
> +          "width": 73,
> +          "height": 138
> +        }
> +      },
> +      "opacity": {
> +        "value": 0.5,
> +        "random": true,
> +        "anim": {
> +          "enable": true,
> +          "speed": 1,
> +          "opacity_min": 0.1,
> +          "sync": false
> +        }
> +      },
> +      "size": {
> +        "value": 50,
> +        "random": true,
> +        "anim": {
> +          "enable": false,
> +          "speed": 40,
> +          "size_min": 0.1,
> +          "sync": false
> +        }
> +      },
> +      "line_linked": {
> +        "enable": false,
> +      },
> +      "move": {
> +        "enable": true,
> +        "speed": 6,
> +        "direction": "none",
> +        "random": false,
> +        "straight": false,
> +        "out_mode": "bounce",
> +        "attract": {
> +          "enable": true,
> +          "rotateX": 600,
> +          "rotateY": 1200
> +        }
> +      }
> +    },
> +    "interactivity": {
> +      "detect_on": "canvas",
> +      "events": {
> +        "onhover": {
> +          "enable": true,
> +          "mode": "repulse"
> +        },
> +        "onclick": {
> +          "enable": true,
> +          "mode": "push"
> +        },
> +        "resize": true
> +      },

... distributing all of this and more to put on a page which you should
not even see in the first place is not something I'd like to see.

Additionally I've planned to convert also the 404 page to rST
eventually to go with the rest of the web. The only reason I didn't do
so yet is the use of <del> which rST doesn't support and I didn't bother
to come up with a different text yet.
Re: [libvirt PATCH] docs: introduce a more interesting 404 error page
Posted by Daniel P. Berrangé 1 year, 2 months ago
On Thu, Feb 16, 2023 at 04:21:33PM +0100, Peter Krempa wrote:
> Additionally I've planned to convert also the 404 page to rST
> eventually to go with the rest of the web. The only reason I didn't do
> so yet is the use of <del> which rST doesn't support and I didn't bother
> to come up with a different text yet.

Absolutely don't let that <del> stop you converting the page to RST.
Just purge the <del> text and convert the rest.

With regards,
Daniel
-- 
|: https://berrange.com      -o-    https://www.flickr.com/photos/dberrange :|
|: https://libvirt.org         -o-            https://fstop138.berrange.com :|
|: https://entangle-photo.org    -o-    https://www.instagram.com/dberrange :|