İçerikler

JavaScript Ile Mod Hesaplayıcı

1. Gamlar ve Modlar

Major gam formülü iki tam bir yarım üç tam bir yarım kuralından üretilir. Majör gam formülünü bir kere do majör ve bir kere fa majör için kullanırsak, bu gamarı başlangıç kabul ederek diğer gamları bir örüntü üzerinden elde edebiliyoruz.

Bu örüntü diyezli gamlar için, do majörden başlarsak do majörün 5.sesini (sol) bir sonraki gamın kök sesi olarak alıp sol notasından sırasıyla notaları yazarsak en sona kalan fa sesine bir diyez ekliyoruz ve bu şekilde deval edersek diyezli gamlarımızı elde ediyoruz.

/cmajor.gif

Aynı mantık bemollü gamlar içinde geçerli, tek fark bu sefer 5. ses yerine 4. sesi yeni gamın kök sesi olarak alıyoruz ve bu safer bir önceki gamın son sesine bemol ekleyerek yeni gamı üretiyoruz.

/fmajor.gif

2. Temel HTML ve CSS

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0 " />
    <title>Mode Calculator</title>
    <link rel="stylesheet" href="style.css">
    <script src="main.js" defer type="text/javascript"></script>
  </head>
  <body>
    <h1>Major Scale Modes Calculator</h1>
    <div>
      <button>Ab</button>
      <button>A</button>
      <button>Bb</button>
      <button>B</button>
      <button>C</button>
      <button>Db</button>
      <button>D</button>
      <button>Eb</button>
      <button>E</button>
      <button>F</button>
      <button>Gb</button>
      <button>G</button>
    </div>
    <table id="table">
      <thead>
        <th><h2>Modes</h2></th>
        <th><h2>Notes</h2></th>
      </thead>
      <tr id="major">
        <th>Major or Ionian</th>
      </tr>
      <tr id="dorian">
        <th>Dorian</th>
      </tr>
      <tr id="phrygian">
        <th>Phrygian</th>
      </tr>
      <tr id="lydian">
        <th>Lydian</th>
      </tr>
      <tr id="mixolydian">
        <th>Mixolydian</th>
      </tr>
      <tr id="minor">
        <th>Natural Minor or Aeolian</th>
      </tr>
      <tr id="locrian">
        <th>Locrian</th>
      </tr>
    </table>
  </body>
</html>
body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  align-content: center;
  gap: 15px;
  background-color: #121212;
  color: #EEE;
  font-size: 20px;
}

table {
  margin: auto;
  padding: 10px;
  border:2px solid white;
  width: 70%;
  border-collapse: collapse;
}

table, tr, td, th {
  padding 10px;
  margin-top: 40px;
  border-bottom: 1px solid #fff;
}

thead {
  text-align: center;
  background-color: #ff5f00;
}

tbody th {
  height: 35px;
  text-align: left;
  vertical-align: center;
  padding: 15px;
}

tbody td {
  text-align: center;
  vertical-align: center;
  padding: 10px;
}

tr:nth-child(even) {
  background-color: #000;
}

/* Style for buttons */
button {
  padding: 10px 20px;
  font-size: 18px;
  background-color: #ff5f00;
  color: #fff;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  transition: background-color 0.3s ease; /* Smooth hover effect */
}

/* Button hover effect */
button:hover {
  background-color: #ff8533;
}

HTML ve CSS dosyalarımız üstteki gibi olacak. Şimdi javaScirpt dosyamızı yazmaya başlayalım.

3. JavaScript

const isScales = ["B", "E", "A", "D", "G", "C"];

const cMajor = ["C", "D", "E", "F", "G", "A", "B"];
const fMajor = ["F", "G", "A", "Bb", "C", "D", "E"];

const table = document.getElementById("table");
const buttons = document.getElementsByTagName("button");

Sabitlerimizi tanımlamakla başladık. isScales değişkenini kullanıcı bir butona tıkladığı zaman tıkladığı notanın diyezli bir gam olup olmadığını kontrol etmek için kullanacağız.

cMajor ve fMajor değişkenleri bizim gamları hesaplarken kullanacağımız başlangıç gamları olacak. cMajor gamı dizyezli gamlar, fMajor gamı ise bemollü gamları hesaplarken bizim başlangıç dizimiz olacak.

table ve buttons değişlenleri HTML sayfamızdaki elementlere erişmek için kullanacağız.

class Scales {
  constructor(scale) {
    this.sharped = [];
    this.flatted = [];
  }

Kendimize bir diziler(scales) sınıfı oluşturuyoruz. Diziler sınıfının constructor fonksiyonu bu sınıftan bir nesne oluşturacağımzı zaman çalıştırışan fonksiyondur. Tek bir parametre alır scale bu parametre bizim dizimizi içerecek. İki tane boş dizi oluşturuyoruz bunlar diyezli ve bemollü gamlar ileride oluşturduğumuz gamları bu dizilere ekleyeceğiz.

  // Sharped dizisine bir nota ekleme işlemini gerçekleştirir
  appendToSharped() {
    // bir önceki dizinin 4. elemanını başa alma
    let lastScale = this.sharped[this.sharped.length - 1];
    let temp = lastScale.slice(0, 4);
    let newScale = lastScale.slice(4, 7);
    newScale = newScale.concat(temp);
    newScale[newScale.length - 1] += "#";
    this.sharped.push(newScale);
  }

  // kullanıcının seçtiği notayı objenin içerisinde ara
  searchInSharped(userInputNote) {
    for (let i in this.sharped) {
      if (this.sharped[i][0] == userInputNote) {
        return this.sharped[i];
      }
    }
  }

  // gam oluşturulana kadar ekle
  appendToSharpedTill(scale, note) {
    while(!Array.isArray(scale)) {
      this.appendToSharped();
      scale = this.searchInSharped(note);
    }
    return scale;
  }

appendToSharped(), searchInSharped() ve appendToSharpedTill() metodlarını diyezli gamlarımız için kullanacağız tek tek bu metodların ne yaptıklarına bakalım.

  appendToSharped() {
    // bir önceki dizinin 4. elemanını başa alma
    let lastScale = this.sharped[this.sharped.length - 1];
    let temp = lastScale.slice(0, 4);
    let newScale = lastScale.slice(4, 7);
    newScale = newScale.concat(temp);
    newScale[newScale.length - 1] += "#";
    this.sharped.push(newScale);
  }

lastScale değişkenine mevcut gamlardan en sonuncusunu atıyoruz. Bu gamın 5.sesinden yani dizinin 4.index’inden başlayan bir sonraki gamı oluşturmamız gerekiyor. Bu yüzden geçici bir temp değişkenine mevcut gamın ilk 4 elemanını atıyoruz. Bunu yaparken splice() kullanırsak mevcut gamın ilk 4 elemanı çıkartıyor ama biz mevcut gamın değişmesini istemiyoruz çünki daha sonra bu gamı kullanıcı istediği zaman ona göstermemiz lazım. Bu yüzde slice() metodunu kullanıyoruz. newScale değişkenine mevcut gamın 5. sesinden son sesine kadar olan sesleri atıyoruz ve concat() metoduyla beraber geçici temp dizisiyle birleştiriyoruz böylece elimizde mevcut gamın 5.sesinden başlayan yeni bir gam olmuş oldu. Bu yeni gamın son sesi diyez alacağı için += "# yöntemiyle son sese diyez ekliyoruz. this.sharped.push(newScale); diyerek diyezli gamlar dizemize yeni bir diyez ekliyoruz.

  searchInSharped(userInputNote) {
    for (let i in this.sharped) {
      if (this.sharped[i][0] == userInputNote) {
        return this.sharped[i];
      }
    }
  }

Buradaki metodumuzda ise kullanıcının seçtiği notayı mevcut diyezli gamlar dizisinde arıyor eğer bulursa bulduğu dizeyi return ediyor. Burda for in döndüsü yardımıylar mevcut diyezli gamlar üzerinde bir döngü kuruyoruz ve ilk elemanının kullanıcının girdiği notaya eşit olup olmadığını kontrol ediyoruz eğer eşitse bu gamı return ediyoruz.

  appendToSharpedTill(scale, note) {
    while(!Array.isArray(scale)) {
      this.appendToSharped();
      scale = this.searchInSharped(note);
    }
    return scale;
  }

Bu metodun amacı, bir başlangıç nota ve bu notadan başlayarak bir diyezli (sharp) gam oluşturmaya çalışan bir dizi oluşturmak.

  1. appendToSharpedTill metodu iki parametre alır: scale: Bu parametre, bir dizi veya gamı temsil eden bir dize olmalıdır. İlk başta genellikle başlangıç bir dizidir. note: Bu parametre, başlangıç notayı temsil eden bir dizedir. Örneğin, “C#” gibi bir nota olabilir.

  2. while(!Array.isArray(scale)) şartıyla başlayan bir while döngüsü bulunur. Bu döngü, scale bir diziye dönüşene kadar çalışır. Yani, başlangıçta scale bir dizi değilse, bu döngü devam eder.

Döngünün içinde this.appendToSharped(); ifadesi çağrılır. Bu ifade, appendToSharped metodunu çağırarak mevcut scale dizisinin sonuna bir nota ekler. Bu nota diyez işareti (#) ile işaretlenir.

Sonra scale = this.searchInSharped(note); ifadesiyle scale yeniden atanır. Bu ifade, kullanıcının seçtiği nota (note) göre sharped dizisinde bu notanın gamını bulur ve scale değişkenine atar.

Döngü, scale bir diziye dönüşene kadar bu adımları tekrarlar. Yani, gam tamamlandığında döngü sona erer.

Son olarak, oluşturulan gamı (scale) geri döndürür.

  appendToFlatted() {
    // bir önceki dizinin 3. elemanını başa alma
    let lastScale = this.flatted[this.flatted.length - 1];
    let temp = lastScale.slice(0, 3);
    let newScale = lastScale.slice(3, 7);
    newScale[newScale.length - 1] += "b";
    newScale = newScale.concat(temp);
    this.flatted.push(newScale);
  }

  searchInFlatted(userInputNote) {
    for (let i in this.flatted) {
      if (this.flatted[i][0] == userInputNote) {
        return this.flatted[i];
      }
    }
  }

  appendToFlattedTill(scale, note) {
    while (!Array.isArray(scale)) {
      this.appendToFlatted();
      scale = this.searchInFlatted(note);
    }
    return scale;
  }

Aynı metodların bir de bemollü gamlar için olanlarını yazdık burada ki tek fark appendToFlatted() metodu diyezli gamlarda 5.sesi (4.index) yeni gamın ilk sesi yaparken burada 4.sesi (3.index) yeni gamın ilk sesi haline getiriyoruz.

const scales = new Scales();

scales.sharped.push(cMajor);
scales.flatted.push(fMajor);

Burada Scales() sınıfından scales nesnesini oluşturduk. Sonra başlangıç gamlarımızı dizilerimizde ekledik.

function makeModes(scale) {
  let temp = scale.shift(0);
  scale.push(temp);
}

Bu fonksiyon ana gamdan modları oluşturmamıza yarıyor. Modlar basitçe bir gamı başka kendi içerisindeki başka bir sesi kök ses kabul edip çalmaktır. Örnek vermek gerekirse do majör gamını ele alırsak Do notasından do ya kadar sırasıyla do, re, mi, fa, sol, la, si şeklinde çalarsak do major iken mi, fa, sol, la, si, do, re şeklinde çalarsak mi miksolidyen modunu elde ederiz. Bu fonksiyonda sırasıyla ilk elementi sona taşıyor. Bu fonksiyonu ilerde kodumuzun içerisinde bir döngü içerisinde çağırarak modlarımızı yaratacağız.

function drawRow(scale) {
  let tr = document.getElementsByTagName("tr");
  for (let i=1; i < tr.length; i++) {
    let td = tr[i].querySelector('td');

    // Eğer mevcut bir td varsa, onun içeriğini değiştir
    if (td) {
      td.textContent = scale;
    } else {
      // Yoksa yeni bir td ekleme
      let td = document.createElement("td");
      td.textContent = scale;
      tr[i].appendChild(td);
    }

    // Başına scale[0] eklemek için <th> içeriğini güncelleyin
    let th = tr[i].querySelector('th');
    // bu regex yalnızca eklenen scale[0] karakterlerini yakalar.
    let regex = /^[A-G](?:[b#])? /;
    if(regex.test(th.textContent) == true) {
      th.textContent = th.textContent.replace(regex, scale[0] + " ");
    } else {
      th.textContent = scale[0] + ' ' + th.textContent;
    }

    makeModes(scale);
  }
}

Bu fonksiyon, HTML tablosunda satırların (row) oluşturulması ve güncellenmesi için kullanılır.

  • let tr = document.getElementsByTagName("tr"); ifadesinde HTML’deki tüm “tr” (satır) elementlerini bir NodeList’e atar. Bu NodeList, tablodaki satırları temsil ediyor.

  • for (let i=1; i < tr.length; i++) döngüsünü satırlar üzerinde gezinmek için kurduk. i değişkenini 1’den başlattık çünkü ilk satırda başlıklarımız bulunuyor, başlıklarla bir işimiz olmadığı için onları döngüye katmıyoruz.

  • let td = tr[i].querySelector('td'); burada mevcut satır elementini (td), eğer böyle bir element varsa td değişkenine atar.

  • if (td) ifadesi, mevcut satırın bir hücresi (td) olduğunu kontrol eder. Eğer hücre varsa, hücrenin içeriğini scale ile günceller. Yani, gamın notalarını bu hücreye yazar.

  • Eğer satırın içinde bir hücre yoksa (yeni bir satır ekleniyor), else bloğu çalışır. Bu blok, yeni bir “td” elementi oluşturur ve bu hücreyi scale ile doldurur. Ardından, bu yeni hücreyi satıra ekler.

  • let th = tr[i].querySelector('th'); burada th (table header) elementlerinizi seçtik.

  • 
      let regex = /^[A-G](?:[b#])? /;
      if(regex.test(th.textContent) == true) {
        th.textContent = th.textContent.replace(regex, scale[0] + " ");
      } else {
        th.textContent = scale[0] + ' ' + th.textContent;
      }
    

burada bir regex oluşturduk. Bu regexin amacı tablonun solundaki Dorian, Phrygian gibi mod isimlerinin başına hangi kök sesin olduğunu yazmak, yani D Dorian, E Phrygian gibi. Eğer daha önceden girilmiş bir kök ses varsa (D Dorian) buradaki kök sesi (D) bu regex yakalar ve yeni kök ses ile değiştirir. Eğer yoksa kök sesi direk ekler bu yalnızca başlangıçta karşılaşacağımız bir durum bu sorunu çözmek için daha optimum yöntemler olsada ben string’ler ve regex’ler ile biraz pratik yapmak için bu yöntemi seçtim.

  • Son olarak makeModes(scale); fonksiyonunu çağırarak bir sonraki mod üretilir.

Button Click Event

// kullanıcının seçtiği nota
let note = "";

// butonlara click eventi ata
for (button of buttons) {
  button.addEventListener("click", (event) => {
    note = event.target.textContent; // user input
    // tıklanan nota diyezli gam mı ?
    if (isScales.indexOf(note) !== -1) {
      let scale = scales.searchInSharped(note);
      if (Array.isArray(scale)) {
        drawRow(scale);
      } else {
        scale = scales.appendToSharpedTill(scale, note);
        drawRow(scale);
      }
    } else {
      let scale = scales.searchInFlatted(note);
      if (Array.isArray(scale)) {
        drawRow(scale);
      } else {
        scale = scales.appendToFlattedTill(scale, note);
        drawRow(scale);
      }
    }
  });
}

Artık tüm fonksiyonlarımız, sınıflarımız ve metodlarımız hazır. Şimdi butonlarımıza bir click eventListener’ı atayacağız. Burada note değişkenine kullanıcının tıkadığı butonun içerisinde yer alan notayı atıyoruz.

if (isScales.indexOf(note) !== -1) burada kullanıcının seçtiği notanın diyezli bir gam olup olmadığının kontrolünü yapıyoruz. Eğer diyezli gamsa let scale = scales.searchInSharped(note); diyezli gamlar içerisinde arıyoruz eğer varsa bize bir dizi değişkeni döndürücek bunun sağlamasını Array.isArray() metodu ile yapıyoruz. Eğer döndürmezse o zaman scales.appendToSharpedTill() metodumuzu çağırarak istenilen gamı elde edene kadar scales.sharped‘a gam ekliyoruz. Son olarakta drawRow(scale); ile gamımızı tabloya ekliyoruz bemollü gamlar içinde mantık tamamen aynı.