Files
NiceTransposer/niceTransposer.html
2025-08-07 08:15:25 +00:00

351 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>不囉唆的文字移調器 by NiceChord 好和弦</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 10px;
line-height: 1.2;
}
.controls {
margin: 5px 0;
}
.button-row {
margin: 2px 0;
}
.button-row h3 {
margin: 0 0 10px 0;
font-size: 1em;
}
button {
margin: 2px;
font-size: 12px;
cursor: pointer;
}
.content {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.text-area {
flex: 1;
min-width: 300px;
}
h4 {
margin: 5px;
}
textarea {
width: 100%;
height: 200px;
font-family: monospace;
font-size: 14px;
box-sizing: border-box;
}
@media (max-width: 768px) {
.content {
flex-direction: column;
}
button {
font-size: 12px;
}
}
</style>
</head>
<body>
<h3>不囉唆的文字移調器 by <a href="https://nicechord.com">NiceChord 好和弦</a></h3>
<div class="controls">
<div class="button-row">
<h4>移高:</h4>
<button onclick="transpose(1, '增一度')">增一度</button>
<button onclick="transpose(0, '減二度')">減二度</button>
<button onclick="transpose(1, '小二度')">小二度</button>
<button onclick="transpose(2, '大二度')">大二度</button>
<button onclick="transpose(3, '增二度')">增二度</button>
<button onclick="transpose(2, '減三度')">減三度</button>
<button onclick="transpose(3, '小三度')">小三度</button>
<button onclick="transpose(4, '大三度')">大三度</button>
<button onclick="transpose(5, '增三度')">增三度</button>
<button onclick="transpose(4, '減四度')">減四度</button>
<button onclick="transpose(5, '完全四度')">完全四度</button>
<button onclick="transpose(6, '增四度')">增四度</button>
<button onclick="transpose(6, '減五度')">減五度</button>
<button onclick="transpose(7, '完全五度')">完全五度</button>
<button onclick="transpose(8, '增五度')">增五度</button>
<button onclick="transpose(7, '減六度')">減六度</button>
<button onclick="transpose(8, '小六度')">小六度</button>
<button onclick="transpose(9, '大六度')">大六度</button>
<button onclick="transpose(10, '增六度')">增六度</button>
<button onclick="transpose(9, '減七度')">減七度</button>
<button onclick="transpose(10, '小七度')">小七度</button>
<button onclick="transpose(11, '大七度')">大七度</button>
<button onclick="transpose(12, '增七度')">增七度</button>
<button onclick="transpose(11, '減八度')">減八度</button>
</div>
<div class="button-row">
<h4>移低:</h4>
<button onclick="transpose(-1, '增一度')">增一度</button>
<button onclick="transpose(0, '減二度')">減二度</button>
<button onclick="transpose(-1, '小二度')">小二度</button>
<button onclick="transpose(-2, '大二度')">大二度</button>
<button onclick="transpose(-3, '增二度')">增二度</button>
<button onclick="transpose(-2, '減三度')">減三度</button>
<button onclick="transpose(-3, '小三度')">小三度</button>
<button onclick="transpose(-4, '大三度')">大三度</button>
<button onclick="transpose(-5, '增三度')">增三度</button>
<button onclick="transpose(-4, '減四度')">減四度</button>
<button onclick="transpose(-5, '完全四度')">完全四度</button>
<button onclick="transpose(-6, '增四度')">增四度</button>
<button onclick="transpose(-6, '減五度')">減五度</button>
<button onclick="transpose(-7, '完全五度')">完全五度</button>
<button onclick="transpose(-8, '增五度')">增五度</button>
<button onclick="transpose(-7, '減六度')">減六度</button>
<button onclick="transpose(-8, '小六度')">小六度</button>
<button onclick="transpose(-9, '大六度')">大六度</button>
<button onclick="transpose(-10, '增六度')">增六度</button>
<button onclick="transpose(-9, '減七度')">減七度</button>
<button onclick="transpose(-10, '小七度')">小七度</button>
<button onclick="transpose(-11, '大七度')">大七度</button>
<button onclick="transpose(-12, '增七度')">增七度</button>
<button onclick="transpose(-11, '減八度')">減八度</button>
</div>
</div>
<div class="content">
<div class="text-area">
<h4>原始文字:</h4>
<textarea id="input" placeholder="請輸入包含音名或和弦代號的文字。例如:&#10;&#10;C C G G A A G&#10;&#10;或&#10;&#10;Cmaj7 | Am7 | Fmaj7 | G9sus | C(add2) ||"></textarea>
</div>
<div class="text-area">
<h4>移調結果:</h4>
<textarea id="output" readonly></textarea>
</div>
</div>
<script>
// ===== 音名基礎資料定義 =====
// 七個基本音名字母,按照音階順序排列
const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
// 將基本音名字母對應到半音數值(以 C 為 0 基準)
// C=0, D=2, E=4, F=5, G=7, A=9, B=11
const noteToSemitone = {
'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11
};
// ===== 音名解析函數 =====
/**
* 解析音名字串,將其拆解為字母、修飾符和半音數值
* @param {string} noteStr - 音名字串,例如 "C#", "Bb", "F##" 等
* @returns {object|null} - 包含 letter字母、modifier修飾符、semitone半音數值的物件若無效則回傳 null
*/
function parseNote(noteStr) {
// 將輸入的 x重升記號轉換為 ##(雙升記號),統一使用 ## 格式
noteStr = noteStr.replace(/x/g, '##');
// 提取第一個字元作為音名字母
const letter = noteStr[0];
// 提取第一個字元之後的所有字元作為修飾符(升降記號)
const modifier = noteStr.slice(1);
// 檢查字母是否為有效的音名字母A-G
if (!noteLetters.includes(letter)) {
return null; // 無效音名,回傳 null
}
// 計算修飾符對半音數的影響
// 每個 # 代表升半音(+1每個 b 代表降半音(-1
let modifierValue = 0;
for (let char of modifier) {
if (char === '#') modifierValue += 1; // 升記號:+1 半音
else if (char === 'b') modifierValue -= 1; // 降記號:-1 半音
}
// 回傳解析結果
return {
letter: letter, // 音名字母
modifier: modifier, // 修飾符字串
semitone: (noteToSemitone[letter] + modifierValue + 12) % 12 // 最終半音數值0-11
};
}
// ===== 目標字母計算函數 =====
/**
* 根據起始字母、音程類型和方向,計算目標音名字母
* @param {string} fromLetter - 起始音名字母A-G
* @param {string} intervalType - 音程類型,例如 "大三度"、"完全五度" 等
* @param {number} direction - 方向1 為向上,-1 為向下)
* @returns {string} - 目標音名字母
*/
function getTargetLetter(fromLetter, intervalType, direction) {
// 找到起始字母在 noteLetters 陣列中的索引位置
const letterIndex = noteLetters.indexOf(fromLetter);
let steps; // 字母距離(度數-1
// 根據音程類型決定字母之間的距離
// 注意:度數是音名字母之間的距離,例如 C 到 E 是三度相距2個字母位置
switch(intervalType) {
case '增一度': steps = 0; break; // 同一個字母
case '減二度': case '小二度': case '大二度': case '增二度': steps = 1; break; // 相鄰字母
case '減三度': case '小三度': case '大三度': case '增三度': steps = 2; break; // 相距2個字母
case '減四度': case '完全四度': case '增四度': steps = 3; break; // 相距3個字母
case '減五度': case '完全五度': case '增五度': steps = 4; break; // 相距4個字母
case '減六度': case '小六度': case '大六度': case '增六度': steps = 5; break; // 相距5個字母
case '減七度': case '小七度': case '大七度': case '增七度': steps = 6; break; // 相距6個字母
case '減八度': steps = 7; break; // 八度(回到同一字母但在不同八度)
default: steps = 0;
}
// 根據移調方向調整字母距離
if (direction < 0) {
if (intervalType === '減八度') {
steps = 7; // 減八度向下就是7個字母距離一個完整八度
} else {
// 向下移調時,不需要特別調整 steps
// 因為我們會在下面的計算中使用 direction 來處理方向
}
}
// 計算目標字母的索引位置
// 使用 (index + offset + 7) % 7 來處理負數的模運算
// +7 是為了確保結果不會是負數
const targetIndex = (letterIndex + steps * direction + 7) % 7;
// 回傳目標字母
return noteLetters[targetIndex];
}
// ===== 修飾符計算函數 =====
/**
* 根據目標字母和目標半音數,計算需要的修飾符(升降記號)
* @param {string} targetLetter - 目標音名字母
* @param {number} targetSemitone - 目標半音數值0-11
* @returns {string} - 修飾符字串,例如 "#", "bb", "###" 等
*/
function calculateModifier(targetLetter, targetSemitone) {
// 取得目標字母的基本半音數值(不含修飾符)
const baseSemitone = noteToSemitone[targetLetter];
// 計算需要調整的半音差距
// 使用 (target - base + 12) % 12 來處理跨八度的情況
const diff = (targetSemitone - baseSemitone + 12) % 12;
// 根據半音差距生成相應的修飾符
if (diff === 0) {
return ''; // 不需要修飾符
} else if (diff <= 6) {
// 如果差距在6個半音以內使用升記號
// 例如diff=2 會產生 "##"
return '#'.repeat(diff);
} else {
// 如果差距超過6個半音使用降記號會比較簡潔
// 例如diff=10 相當於降2個半音會產生 "bb"
return 'b'.repeat(12 - diff);
}
}
// ===== 單一音名移調函數 =====
/**
* 對單一音名進行移調
* @param {string} noteStr - 原始音名字串
* @param {number} semitones - 移調的半音數
* @param {string} intervalType - 音程類型
* @param {number} direction - 移調方向1=向上,-1=向下)
* @returns {string} - 移調後的音名字串
*/
function transposeNote(noteStr, semitones, intervalType, direction) {
// 解析原始音名
const parsed = parseNote(noteStr);
if (!parsed) return noteStr; // 如果解析失敗,回傳原字串
// 根據音程類型和方向計算目標字母
const targetLetter = getTargetLetter(parsed.letter, intervalType, direction);
// 計算目標半音數值
// 原始半音數 + 移調半音數,並確保結果在 0-11 範圍內
const targetSemitone = (parsed.semitone + semitones + 12) % 12;
// 計算目標字母需要的修飾符
const modifier = calculateModifier(targetLetter, targetSemitone);
// 組合目標字母和修飾符,回傳移調結果
return targetLetter + modifier;
}
// ===== 文字移調函數 =====
/**
* 在文字中尋找音名並進行移調,其他文字保持不變
* @param {string} text - 包含音名的原始文字
* @param {number} semitones - 移調的半音數
* @param {string} intervalType - 音程類型
* @param {number} direction - 移調方向
* @returns {string} - 移調後的文字
*/
function transposeText(text, semitones, intervalType, direction) {
// 正規表達式:匹配音名模式
// ([A-G]) - 捕獲一個大寫字母 A-G 作為音名字母
// (x|[#b]*)? - 可選的修飾符,可以是 x 或任意數量的 # 和 b
const noteRegex = /([A-G])(x|[#b]*)?/g;
// 使用 replace 方法對所有匹配的音名進行移調
return text.replace(noteRegex, (match, letter, modifier) => {
// 重建完整的音名字串
const noteStr = letter + (modifier || '');
// 對這個音名進行移調
return transposeNote(noteStr, semitones, intervalType, direction);
});
}
// ===== 主要移調執行函數 =====
/**
* 執行移調操作的主函數,從輸入框讀取文字並輸出移調結果
* @param {number} semitones - 移調的半音數
* @param {string} intervalType - 音程類型字串
*/
function transpose(semitones, intervalType) {
// 從輸入文字框取得原始文字
const input = document.getElementById('input').value;
// 判斷移調方向:正數為向上,負數為向下
const direction = semitones > 0 ? 1 : -1;
// 執行文字移調
const result = transposeText(input, semitones, intervalType, direction);
// 將結果顯示在輸出文字框
document.getElementById('output').value = result;
}
// ===== 事件監聽器設定 =====
// 當輸入文字框內容改變時,清空輸出文字框
// 這樣使用者就知道需要重新點擊按鈕來進行移調
document.getElementById('input').addEventListener('input', () => {
document.getElementById('output').value = '';
});
</script>
</body>
</html>