diff --git a/static/script/cursor.js b/static/script/cursor.js
new file mode 100644
index 0000000..41c7647
--- /dev/null
+++ b/static/script/cursor.js
@@ -0,0 +1,212 @@
+(function() {
+ let possibleColors = [
+ "#D61C59",
+ "#E7D84B",
+ "#1B8798",
+ ];
+ let element = document.body;
+
+ let width = window.innerWidth;
+ let height = window.innerHeight;
+ const cursor = { x: width / 2, y: width / 2 };
+ const lastPos = { x: width / 2, y: width / 2 };
+ const particles = [];
+ const canvImages = [];
+ let canvas, context, animationFrame;
+
+ const char = "*";
+
+ const prefersReducedMotion = window.matchMedia(
+ "(prefers-reduced-motion: reduce)"
+ );
+
+ // Re-initialise or destroy the cursor when the prefers-reduced-motion setting changes
+ prefersReducedMotion.onchange = () => {
+ if (prefersReducedMotion.matches) {
+ destroy();
+ } else {
+ init();
+ }
+ };
+
+ function init() {
+ // Don't show the cursor trail if the user has prefers-reduced-motion enabled
+ if (prefersReducedMotion.matches) {
+ console.log(
+ "This browser has prefers reduced motion turned on, so the cursor did not init"
+ );
+ return false;
+ }
+
+ canvas = document.createElement("canvas");
+ context = canvas.getContext("2d");
+ canvas.style.top = "0px";
+ canvas.style.left = "0px";
+ canvas.style.pointerEvents = "none";
+
+ canvas.style.position = "fixed";
+ element.appendChild(canvas);
+ canvas.width = width;
+ canvas.height = height;
+
+ context.font = "21px serif";
+ context.textBaseline = "middle";
+ context.textAlign = "center";
+
+ possibleColors.forEach((color) => {
+ let measurements = context.measureText(char);
+ let bgCanvas = document.createElement("canvas");
+ let bgContext = bgCanvas.getContext("2d");
+
+ bgCanvas.width = measurements.width;
+ bgCanvas.height =
+ measurements.actualBoundingBoxAscent +
+ measurements.actualBoundingBoxDescent;
+
+ bgContext.fillStyle = color;
+ bgContext.textAlign = "center";
+ bgContext.font = "21px serif";
+ bgContext.textBaseline = "middle";
+ bgContext.fillText(
+ char,
+ bgCanvas.width / 2,
+ measurements.actualBoundingBoxAscent
+ );
+
+ canvImages.push(bgCanvas);
+ });
+
+ bindEvents();
+ loop();
+ }
+
+ // Bind events that are needed
+ function bindEvents() {
+ element.addEventListener("mousemove", onMouseMove);
+ element.addEventListener("touchmove", onTouchMove, { passive: true });
+ element.addEventListener("touchstart", onTouchMove, { passive: true });
+ window.addEventListener("resize", onWindowResize);
+ }
+
+ function onWindowResize(e) {
+ width = window.innerWidth;
+ height = window.innerHeight;
+
+ canvas.width = width;
+ canvas.height = height;
+ }
+
+ function onTouchMove(e) {
+ if (e.touches.length > 0) {
+ for (let i = 0; i < e.touches.length; i++) {
+ addParticle(
+ e.touches[i].clientX,
+ e.touches[i].clientY,
+ canvImages[Math.floor(Math.random() * canvImages.length)]
+ );
+ }
+ }
+ }
+
+ function onMouseMove(e) {
+ window.requestAnimationFrame(() => {
+ cursor.x = e.clientX;
+ cursor.y = e.clientY;
+
+ const distBetweenPoints = Math.hypot(
+ cursor.x - lastPos.x,
+ cursor.y - lastPos.y
+ );
+
+ if (distBetweenPoints > 1.5) {
+ addParticle(
+ cursor.x,
+ cursor.y,
+ canvImages[Math.floor(Math.random() * possibleColors.length)]
+ );
+
+ lastPos.x = cursor.x;
+ lastPos.y = cursor.y;
+ }
+ });
+ }
+
+ function addParticle(x, y, color) {
+ particles.push(new Particle(x, y, color));
+ }
+
+ function updateParticles() {
+ if (particles.length == 0) {
+ return;
+ }
+
+ context.clearRect(0, 0, width, height);
+
+ // Update
+ for (let i = 0; i < particles.length; i++) {
+ particles[i].update(context);
+ }
+
+ // Remove dead particles
+ for (let i = particles.length - 1; i >= 0; i--) {
+ if (particles[i].lifeSpan < 0) {
+ particles.splice(i, 1);
+ }
+ }
+
+ if (particles.length == 0) {
+ context.clearRect(0, 0, width, height);
+ }
+ }
+
+ function loop() {
+ updateParticles();
+ animationFrame = requestAnimationFrame(loop);
+ }
+
+ function destroy() {
+ canvas.remove();
+ cancelAnimationFrame(animationFrame);
+ element.removeEventListener("mousemove", onMouseMove);
+ element.removeEventListener("touchmove", onTouchMove);
+ element.removeEventListener("touchstart", onTouchMove);
+ window.addEventListener("resize", onWindowResize);
+ };
+
+ function Particle(x, y, canvasItem) {
+ const lifeSpan = Math.floor(Math.random() * 30 + 60);
+ this.initialLifeSpan = lifeSpan; //
+ this.lifeSpan = lifeSpan; //ms
+ this.velocity = {
+ x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 2),
+ y: Math.random() * 0.7 + 0.9,
+ };
+ this.position = { x: x, y: y };
+ this.canv = canvasItem;
+
+ this.update = function (context) {
+ this.position.x += this.velocity.x;
+ this.position.y += this.velocity.y;
+ this.lifeSpan--;
+
+ this.velocity.y += 0.02;
+
+ const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0);
+
+ context.drawImage(
+ this.canv,
+ this.position.x - (this.canv.width / 2) * scale,
+ this.position.y - this.canv.height / 2,
+ this.canv.width * scale,
+ this.canv.height * scale
+ );
+ };
+ }
+
+ init();
+
+ return {
+ destroy: destroy
+ }
+ }
+)();
\ No newline at end of file
diff --git a/static/script/cursor.min.js b/static/script/cursor.min.js
new file mode 100644
index 0000000..bd6a859
--- /dev/null
+++ b/static/script/cursor.min.js
@@ -0,0 +1,2 @@
+// https://github.com/tholman/cursor-effects/blob/master/src/fairyDustCursor.js
+!function(){let t=["#D61C59","#E7D84B","#1B8798"],e=document.body,n=window.innerWidth,i=window.innerHeight;const o={x:n/2,y:n/2},s={x:n/2,y:n/2},h=[],a=[];let c,r,d;const l="*",u=window.matchMedia("(prefers-reduced-motion: reduce)");function f(){if(u.matches)return console.log("This browser has prefers reduced motion turned on, so the cursor did not init"),!1;c=document.createElement("canvas"),r=c.getContext("2d"),c.style.top="0px",c.style.left="0px",c.style.pointerEvents="none",c.style.position="fixed",e.appendChild(c),c.width=n,c.height=i,r.font="21px serif",r.textBaseline="middle",r.textAlign="center",t.forEach(t=>{let e=r.measureText(l),n=document.createElement("canvas"),i=n.getContext("2d");n.width=e.width,n.height=e.actualBoundingBoxAscent+e.actualBoundingBoxDescent,i.fillStyle=t,i.textAlign="center",i.font="21px serif",i.textBaseline="middle",i.fillText(l,n.width/2,e.actualBoundingBoxAscent),a.push(n)}),e.addEventListener("mousemove",p),e.addEventListener("touchmove",v,{passive:!0}),e.addEventListener("touchstart",v,{passive:!0}),window.addEventListener("resize",m),w()}function m(t){n=window.innerWidth,i=window.innerHeight,c.width=n,c.height=i}function v(t){if(t.touches.length>0)for(let e=0;e