Final Project:

Drawing Music Tool

Technical Basics

  1. Audio Playback Rate
    1. playbackRate of 1.0 plays the audio at full speed, or 44,100 Hz.
    2. playbackRate of 0.5 plays the audio at half speed, or 22,050 Hz.
    3. playbackRate of 2.0 doubles the audio's playback rate to 88,200 Hz.

Output

Screenshot 2022-12-11 at 8.48.33 PM.png

Code

<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!--Adding Download Button for Image-->
  </head>

  <body>
    <div id="app"></div>
    <canvas id="myCanvas"></canvas>
    <button id="undo">Undo</button>
    <div id="controls">
      <button id="spray">Spray</button>
      <!-- <button id="pen">Pen</button> -->
      <button id="glow">Glow</button>
      <button id="glotchy">Glotchy</button>
      <!-- <button id="chalk">Chalk</button>
      <button id="rainbow">Rainbow</button> -->
      <button id="brush">Brush</button>
      <button id="stamp">Stamp</button>
    </div>

    <div id="colors">
      <div id="rainbowColor"></div>
      <div id="whiteColor" style="background: white;"></div>
      <div id="pinkColor" style="background: pink;"></div>
      <div id="orangeColor" style="background: orange;"></div>
      <div id="blueColor" style="background: rgb(113, 113, 231);"></div>
      <div id="purpleColor" style="background: rgb(228, 110, 228);"></div>
      <!-- <div id="yellowColor" style="background: rgb(241, 241, 27);"></div>
      <div id="brownColor" style="background: rgb(180, 62, 4);"></div> -->
    </div>

    <!-- <div id="design">
      <div id="normal">Normal</div>
      <div id="mirrorVertical">VerticalSym</div>
      <div id="mirrorHorizontal">HorizontalSym</div>
      <div id="mirrorDiagonal">DiagonalSym</div>
      <div id="squareDiagonal">SquareSym</div>
    </div> -->

    <script src="src/index.js"></script>
  </body>
</html>
body {
  font-family: sans-serif;
  margin: 0px;
  overflow: hidden;
}

#myCanvas {
  position: absolute;
}

#controls {
  position: relative;
  display: flex-wrap;
  justify-content: space-around;

  top: 2vw;
  left: 2vw;
  right: 2vw;
}
#controls * {
  margin: 2px;
  padding: 0.5em 1em;
}

/* #design {
  position: relative;
  display: flex;
  justify-content: flex-start;

  top: 14vw;
  left: 2vw;
  right: 2vw;
}

#design * {
  margin: 0.5em;
  color: white;
} */

#undo {
  position: absolute;

  top: 2vw;
  right: 2vw;
  padding: 0.5em 1em;
  background-color: rgb(70, 155, 189);
}

.activeButton {
  background-color: palevioletred;
}

.activeColor {
  border-block-color: black;
  color: palevioletred;
  border: solid;
}

#colors {
  position: absolute;
  display: flex;
  justify-content: flex-start;
  top: 8vw;
  left: 1vw;
  right: 1vw;
}

#colors * {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin: 10px;
}

#rainbowColor {
  background-image: url("assets/rainbowColor.png");
}
import "./styles.css";

//----------------Import from Draggable.js----------------------
import {
  setDragStartCallback,
  setDragMoveCallback,
  setDragEndCallback
} from "./draggable.js";
//----------------Import from Draggable.js ends-----------------

//----------------Web Audio API code for Sound------------------

import beats from "../assets/sounds_long/beats.mp3";
import guitar from "../assets/sounds_long/guitar.mp3";
import organ from "../assets/sounds_long/organ.mp3";
import piano from "../assets/sounds_long/piano.mp3";
import ting from "../assets/sounds_long/ting.mp3";

window.AudioContext = window.AudioContext || window.webkitAudioContext;
let context = new AudioContext();

let buffer_beats = getData(beats);
let buffer_guitar = getData(guitar);
let buffer_organ = getData(organ);
let buffer_piano = getData(piano);
let buffer_ting = getData(ting);

let lastGrain = Date.now();
let lastBuffer = null;
let lastSource = null;
let lastSourceDuration = 0;
let lastGain = 0;

async function playGrain(buffer, x = 0.5, speed = 1, angle = 0.5) {
  let now = Date.now();
  //Play new grain every 250msec
  if (now - lastGrain < 250) {
    return;
  }

  let audioPlayedTime = now - lastGrain;

  if (lastBuffer != null) {
    //Playing same sound and not reached end of the sound
    if (lastBuffer === buffer && audioPlayedTime < lastSourceDuration * 1000) {
      lastSource.playbackRate.value = 1 + (angle / Math.PI) * 0.5;
      //lastGain.gain.value = 0.3 + speed * 0.01;
      console.log("playingLast", lastGain.gain.value);
      return;
    }
    console.log("LastDiscont");
    lastSource.disconnect();
    lastBuffer = null;
    lastSource = null;
    lastGain = 0;
    lastSourceDuration = 0;
  }
  lastGrain = now;

  // create a buffer player, play the sound to the destination
  let source = context.createBufferSource();
  source.buffer = await buffer;

  lastBuffer = buffer;
  lastSource = source;
  lastSourceDuration = source.buffer.duration;

  source.playbackRate.value = 1 + angle / Math.PI;

  let grainGain = context.createGain();
  //grainGain.gain.value = 0.5 + speed * 0.1;
  if (buffer === buffer_beats) {
    grainGain.gain.value = 0.6;
  } else {
    grainGain.gain.value = 0.2;
  }

  lastGain = grainGain;

  // let bandPass = context.createBiquadFilter();
  // bandPass.type = "lowpass";
  // bandPass.frequency.value = 100 + x * 2000;

  source.connect(grainGain);
  // bandPass.connect(grainGain);
  grainGain.connect(context.destination);

  source.start();
  console.log("NewPlay");
  // disconnect when it's done playing
  // console.log(source.buffer.duration);
  // setTimeout(() => {
  //   source.disconnect();
  //   console.log(source);
  //   lastBuffer = null;
  //   lastSource = null;
  // }, (source.buffer.duration / source.playbackRate.value) * 1000);

  //setTimeout(() => {
  //  source.disconnect();
  //}, (source.buffer.duration / source.playbackRate.value) * 1000); //Time in ms, so multiplied by 1000
}

// this is just code to download the audio data as a "buffer"
function getData(url) {
  return fetch(url)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`fetch error, status = ${response.status}`);
      }
      return response.arrayBuffer();
    })
    .then((buffer) => context.decodeAudioData(buffer));
}
//----------------Web Audio API code for Sound Ends-------------

//-----------Extra actions to be done during the drag-----------

// If you want to attach extra logic to the drag motions, you can use these callbacks:
// they are not required for the homework!
setDragStartCallback(function (element, x, y, scale, angle) {});
setDragMoveCallback(function (element, x, y, scale, angle) {
  playGrain();
});
setDragEndCallback(function (element, x, y, scale, angle) {});

//-----------Extra actions to be done during the drag ends------

//your code

//-------------Variable definition------------------------------
let myCanvas = document.getElementById("myCanvas");
let ctx = myCanvas.getContext("2d");
let j = 0;

let rainbowColor = document.getElementById("rainbowColor");
let whiteColor = document.getElementById("whiteColor");
let pinkColor = document.getElementById("pinkColor");
let orangeColor = document.getElementById("orangeColor");
let blueColor = document.getElementById("blueColor");
let purpleColor = document.getElementById("purpleColor");

let flagRainbowColor = "false";

let sprayButton = document.getElementById("spray");
let brushButton = document.getElementById("brush");
let stampButton = document.getElementById("stamp");
let glowButton = document.getElementById("glow");
let glotchyButton = document.getElementById("glotchy");

let undoButton = document.getElementById("undo");

let undoStack = [];

let isDrawing = "false";
let lastX, lastY;
let thickness = 5;
let spread = 20;
let fairies = [
  "assets/fairy1.png",
  "assets/fairy2.png",
  "assets/fairy3.png",
  "assets/fairy4.png"
].map(function (url) {
  let image = new Image();
  image.src = url;
  image.crossOrigin = "anonymous";
  return image;
});
let imageHeight = 40;
let imageWidth = 40;

let oilbrushes = [
  "assets/oil1.png",
  "assets/oil2.png",
  "assets/oil3.png",
  "assets/oil4.png"
].map(function (url) {
  let image = new Image();
  image.src = url;
  image.crossOrigin = "anonymous";
  return image;
});

let outlineImage = new Image();
outlineImage.src = "/assets/OutlineImage.png";
outlineImage.crossOrigin = "anonymous";

//-------------Variable definition Ends------------------------

//--------------Start Setup Code-------------------------------

myCanvas.width = window.innerWidth;
myCanvas.height = window.innerHeight;

ctx.fillStyle = "hsl(240,20%,20%)";
ctx.fillRect(0, 0, myCanvas.width, myCanvas.height);

undoStack.push(ctx.getImageData(0, 0, myCanvas.width, myCanvas.height));

sprayButton.classList.add("activeButton");
rainbowColor.classList.add("activeColor");
flagRainbowColor = "true";

//--------------Start Setup Code Ends--------------------------

//-------------Different function definitions------------------
function clearButtons() {
  sprayButton.className = "";
  brushButton.className = "";
  stampButton.className = "";
  glowButton.className = "";
  glotchyButton.className = "";
}

function clearColors() {
  rainbowColor.className = "";
  whiteColor.className = "";
  pinkColor.className = "";
  orangeColor.className = "";
  blueColor.className = "";
  purpleColor.className = "";
}

function rand() {
  //To draw the spray paint aligned to center of brush
  return Math.random() - 0.5;
}

function distance(x1, y1, x2, y2) {
  return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}

function angleRad(x1, y1, x2, y2) {
  return Math.atan2(y1 - y2, x1 - x2);
}

function pointsAlongLine(x1, y1, x2, y2, spacing) {
  let dist = distance(x1, y1, x2, y2);
  let steps = dist / spacing;

  let points = [];
  for (var d = 0; d <= 1; d += 1 / steps) {
    let point = {
      x: x1 * d + x2 * (1 - d),
      y: y1 * d + y2 * (1 - d)
    };
    points.push(point);
  }
  return points;
}

//-------------Different function definitions------------------

//------------Event Listener Callback functions----------------
function drawStart(x, y) {
  isDrawing = "true";

  lastX = x;
  lastY = y;
}

function pushState() {
  //console.log(undoStack.length);
  undoStack.push(ctx.getImageData(0, 0, myCanvas.width, myCanvas.height));
  if (undoStack.length > 10) {
    undoStack.shift();
  }
}
function popState() {
  //console.log(undoStack.length);
  if (undoStack.length === 1) {
    //Only 1 element then nothing to do. By default 1 element will always be there.
    return;
  }
  undoStack.pop();
  let lastItem = undoStack[undoStack.length - 1];
  if (lastItem) {
    ctx.putImageData(lastItem, 0, 0);
  }
}

function drawEnd() {
  if (isDrawing === "true") {
    //Then save state
    pushState();
  }
  isDrawing = "false";
}

function drawMove(x, y) {
  if (isDrawing === "false") {
    return;
  }

  let speed = distance(x, y, lastX, lastY);
  let angle = angleRad(x, y, lastX, lastY);
  //console.log(speed, angle);
  ctx.lineWidth = thickness;

  if (sprayButton.className === "activeButton") {
    playGrain(buffer_beats, x / ctx.canvas.width, speed, angle);
    if (flagRainbowColor === "true") {
      ctx.fillStyle = `hsl(${Math.random() * 360}, 80%, 80%)`;
      ctx.strokeStyle = `hsl(${Math.random() * 360}, 80%, 80%)`;
    }

    for (let i = 0; i < 20; i++) {
      ctx.fillRect(lastX + rand() * spread, lastY + rand() * spread, 1, 1);
    }

    ctx.translate(ctx.canvas.width, 0);
    ctx.scale(-1, 1);
    for (let i = 0; i < 20; i++) {
      ctx.fillRect(lastX + rand() * spread, lastY + rand() * spread, 1, 1);
    }
    ctx.resetTransform();
  } else if (glowButton.className === "activeButton") {
    // console.log("glow");
    playGrain(buffer_guitar, x / ctx.canvas.width, speed, angle);
    if (flagRainbowColor === "true") {
      ctx.fillStyle = `hsl(${Math.random() * 360}, 80%, 80%)`;
      ctx.strokeStyle = `hsl(${Math.random() * 360}, 80%, 80%)`;
    }
    ctx.shadowColor = "hsl(299, 100%, 50%)";
    ctx.shadowBlur = 18;
    ctx.shadowOffsetX = 1;
    ctx.shadowOffsetY = -1;

    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(x, y);
    ctx.stroke();
    ctx.arc(x, y, 2, 0, 2 * Math.PI);
    ctx.fill();

    ctx.translate(ctx.canvas.width, 0);
    ctx.scale(-1, 1);
    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(x, y);
    ctx.stroke();
    ctx.arc(x, y, 2, 0, 2 * Math.PI);
    ctx.fill();
    ctx.resetTransform();

    ctx.shadowBlur = 0;
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = 0;
  } else if (glotchyButton.className === "activeButton") {
    // console.log("glotchy");
    playGrain(buffer_ting, x / ctx.canvas.width, speed, angle);
    if (flagRainbowColor === "true") {
      ctx.fillStyle = `hsl(${Math.random() * 360}, 80%, 80%)`;
      ctx.strokeStyle = `hsl(${Math.random() * 360}, 80%, 80%)`;
    }
    ctx.fillRect(lastX, lastY, x - lastX, y - lastY);

    //Code for Mirror Symmetry (4directions)
    ctx.translate(ctx.canvas.width, 0);
    ctx.scale(-1, 1);
    ctx.fillRect(lastX, lastY, x - lastX, y - lastY);
    ctx.resetTransform();
    ctx.translate(0, ctx.canvas.height);
    ctx.scale(1, -1);
    ctx.fillRect(lastX, lastY, x - lastX, y - lastY);
    ctx.resetTransform();
    ctx.translate(ctx.canvas.width, ctx.canvas.height);
    ctx.scale(-1, -1);
    ctx.fillRect(lastX, lastY, x - lastX, y - lastY);
    ctx.resetTransform();
  } else if (brushButton.className === "activeButton") {
    // console.log("brush");
    playGrain(buffer_organ, x / ctx.canvas.width, speed, angle);
    ctx.drawImage(oilbrushes[0], x, y, imageWidth, imageHeight);
    ctx.translate(0, ctx.canvas.height);
    ctx.scale(1, -1);
    ctx.drawImage(oilbrushes[0], x, y, imageWidth, imageHeight);
    ctx.resetTransform();
  } else if (stampButton.className === "activeButton") {
    // console.log("stamp");
    playGrain(buffer_piano, x / ctx.canvas.width, speed, angle);
    ctx.drawImage(
      fairies[Math.floor(Math.random() * 3)],
      x + rand() * 2,
      y + rand() * 2,
      imageWidth,
      imageHeight
    );

    ctx.translate(ctx.canvas.width, ctx.canvas.height);
    ctx.scale(-1, -1);
    ctx.drawImage(
      fairies[Math.floor(Math.random() * 3)],
      x + rand() * 2,
      y + rand() * 2,
      imageWidth,
      imageHeight
    );
    ctx.resetTransform();
  }

  lastX = x;
  lastY = y;
}

myCanvas.addEventListener("mousedown", function (event) {
  drawStart(event.clientX, event.clientY);
});
myCanvas.addEventListener("mouseup", function (event) {
  drawEnd();
});
myCanvas.addEventListener("mouseout", function (event) {
  drawEnd();
});
myCanvas.addEventListener("mousemove", function (event) {
  drawMove(event.clientX, event.clientY);
});

myCanvas.addEventListener("touchstart", (event) => {
  drawStart(event.touches[0].clientX, event.touches[0].clientY);
});
myCanvas.addEventListener("touchend", (event) => {
  drawEnd();
});
myCanvas.addEventListener("touchmove", (event) => {
  event.preventDefault();
  drawMove(event.touches[0].clientX, event.touches[0].clientY);
});

sprayButton.addEventListener("click", () => {
  clearButtons();
  sprayButton.className = "activeButton";
});

brushButton.addEventListener("click", () => {
  clearButtons();
  brushButton.className = "activeButton";
});

stampButton.addEventListener("click", () => {
  clearButtons();
  stampButton.className = "activeButton";
});

glowButton.addEventListener("click", () => {
  clearButtons();
  glowButton.className = "activeButton";
});

glotchyButton.addEventListener("click", () => {
  clearButtons();
  glotchyButton.className = "activeButton";
});

undoButton.addEventListener("click", () => {
  popState();
});

rainbowColor.addEventListener("click", () => {
  flagRainbowColor = "true";
  ctx.strokeStyle = `hsl(${Math.random() * 360}, 80%, 80%)`;
  ctx.fillStyle = `hsl(${Math.random() * 360}, 80%, 80%)`;
  clearColors();
  rainbowColor.classList.add("activeColor");
});

whiteColor.addEventListener("click", () => {
  ctx.strokeStyle = "white";
  ctx.fillStyle = "white";
  flagRainbowColor = "false";
  clearColors();
  whiteColor.classList.add("activeColor");
});

pinkColor.addEventListener("click", () => {
  ctx.strokeStyle = "pink";
  ctx.fillStyle = "pink";
  flagRainbowColor = "false";
  clearColors();
  pinkColor.classList.add("activeColor");
});

orangeColor.addEventListener("click", () => {
  ctx.strokeStyle = "orange";
  ctx.fillStyle = "orange";
  flagRainbowColor = "false";
  clearColors();
  orangeColor.classList.add("activeColor");
});

blueColor.addEventListener("click", () => {
  ctx.strokeStyle = "rgb(113, 113, 231)";
  ctx.fillStyle = "rgb(113, 113, 231)";
  flagRainbowColor = "false";
  clearColors();
  blueColor.classList.add("activeColor");
});

purpleColor.addEventListener("click", () => {
  ctx.strokeStyle = "rgb(228, 110, 228)";
  ctx.fillStyle = "rgb(228, 110, 228)";
  flagRainbowColor = "false";
  clearColors();
  purpleColor.classList.add("activeColor");
});

//------------Event Listener Callback functions Ends------------
// let lastRender = Date.now();

// function render() {
//   let now = Date.now();
//   let delta = now - lastRender;
//   lastRender = now;
//   ctx.fillStyle = "pink";
//   ctx.font = "30px sans-serif";
//   let x = (now / 10) % ctx.canvas.width;
//   console.log(x);
//   ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//   ctx.fillText(delta, x, 300);
//   ctx.fillRect(x, 0, 10, ctx.canvas.height);
// }
// window.setInterval(render, 16);
//window.requestAnimationFrame