Final Project:

Drawing Music Tool

Technical Basics

  1. Audio Playback Rate
    1. A playbackRate of 1.0 plays the audio at full speed, or 44,100 Hz.
    2. A playbackRate of 0.5 plays the audio at half speed, or 22,050 Hz.
    3. A 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