summaryrefslogtreecommitdiff
path: root/ext/js/display/pronunciation-generator.js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2024-03-04 07:43:31 -0500
committerGitHub <noreply@github.com>2024-03-04 12:43:31 +0000
commit4fe881d68d4c1182bee2e78a559c2064aaf48b0d (patch)
tree00e8b209b90cae66ca289f5ea3e0c466e17d6fd3 /ext/js/display/pronunciation-generator.js
parent81fc2bd6d063db92f90171722e8129875bdb56cd (diff)
Move sandbox files (#731)
* Move sandbox files * Update order
Diffstat (limited to 'ext/js/display/pronunciation-generator.js')
-rw-r--r--ext/js/display/pronunciation-generator.js236
1 files changed, 236 insertions, 0 deletions
diff --git a/ext/js/display/pronunciation-generator.js b/ext/js/display/pronunciation-generator.js
new file mode 100644
index 00000000..2c03eb94
--- /dev/null
+++ b/ext/js/display/pronunciation-generator.js
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023-2024 Yomitan Authors
+ * Copyright (C) 2021-2022 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import {getKanaDiacriticInfo, isMoraPitchHigh} from '../language/ja/japanese.js';
+
+/**
+ * @param {string[]} morae
+ * @param {number} downstepPosition
+ * @param {number[]} nasalPositions
+ * @param {number[]} devoicePositions
+ * @returns {HTMLSpanElement}
+ */
+export function createPronunciationText(morae, downstepPosition, nasalPositions, devoicePositions) {
+ const nasalPositionsSet = nasalPositions.length > 0 ? new Set(nasalPositions) : null;
+ const devoicePositionsSet = devoicePositions.length > 0 ? new Set(devoicePositions) : null;
+ const container = document.createElement('span');
+ container.className = 'pronunciation-text';
+ for (let i = 0, ii = morae.length; i < ii; ++i) {
+ const i1 = i + 1;
+ const mora = morae[i];
+ const highPitch = isMoraPitchHigh(i, downstepPosition);
+ const highPitchNext = isMoraPitchHigh(i1, downstepPosition);
+ const nasal = nasalPositionsSet !== null && nasalPositionsSet.has(i1);
+ const devoice = devoicePositionsSet !== null && devoicePositionsSet.has(i1);
+
+ const n1 = document.createElement('span');
+ n1.className = 'pronunciation-mora';
+ n1.dataset.position = `${i}`;
+ n1.dataset.pitch = highPitch ? 'high' : 'low';
+ n1.dataset.pitchNext = highPitchNext ? 'high' : 'low';
+
+ const characterNodes = [];
+ for (const character of mora) {
+ const n2 = document.createElement('span');
+ n2.className = 'pronunciation-character';
+ n2.textContent = character;
+ n1.appendChild(n2);
+ characterNodes.push(n2);
+ }
+
+ if (devoice) {
+ n1.dataset.devoice = 'true';
+ const n3 = document.createElement('span');
+ n3.className = 'pronunciation-devoice-indicator';
+ n1.appendChild(n3);
+ }
+ if (nasal && characterNodes.length > 0) {
+ n1.dataset.nasal = 'true';
+
+ const group = document.createElement('span');
+ group.className = 'pronunciation-character-group';
+
+ const n2 = characterNodes[0];
+ const character = /** @type {string} */ (n2.textContent);
+
+ const characterInfo = getKanaDiacriticInfo(character);
+ if (characterInfo !== null) {
+ n1.dataset.originalText = mora;
+ n2.dataset.originalText = character;
+ n2.textContent = characterInfo.character;
+ }
+
+ let n3 = document.createElement('span');
+ n3.className = 'pronunciation-nasal-diacritic';
+ n3.textContent = '\u309a'; // Combining handakuten
+ group.appendChild(n3);
+
+ n3 = document.createElement('span');
+ n3.className = 'pronunciation-nasal-indicator';
+ group.appendChild(n3);
+
+ /** @type {ParentNode} */ (n2.parentNode).replaceChild(group, n2);
+ group.insertBefore(n2, group.firstChild);
+ }
+
+ const line = document.createElement('span');
+ line.className = 'pronunciation-mora-line';
+ n1.appendChild(line);
+
+ container.appendChild(n1);
+ }
+ return container;
+}
+
+/**
+ * @param {string[]} morae
+ * @param {number} downstepPosition
+ * @returns {SVGSVGElement}
+ */
+export function createPronunciationGraph(morae, downstepPosition) {
+ const ii = morae.length;
+
+ const svgns = 'http://www.w3.org/2000/svg';
+ const svg = document.createElementNS(svgns, 'svg');
+ svg.setAttribute('xmlns', svgns);
+ svg.setAttribute('class', 'pronunciation-graph');
+ svg.setAttribute('focusable', 'false');
+ svg.setAttribute('viewBox', `0 0 ${50 * (ii + 1)} 100`);
+
+ if (ii <= 0) { return svg; }
+
+ const path1 = document.createElementNS(svgns, 'path');
+ svg.appendChild(path1);
+
+ const path2 = document.createElementNS(svgns, 'path');
+ svg.appendChild(path2);
+
+ const pathPoints = [];
+ for (let i = 0; i < ii; ++i) {
+ const highPitch = isMoraPitchHigh(i, downstepPosition);
+ const highPitchNext = isMoraPitchHigh(i + 1, downstepPosition);
+ const x = i * 50 + 25;
+ const y = highPitch ? 25 : 75;
+ if (highPitch && !highPitchNext) {
+ addGraphDotDownstep(svg, svgns, x, y);
+ } else {
+ addGraphDot(svg, svgns, x, y);
+ }
+ pathPoints.push(`${x} ${y}`);
+ }
+
+ path1.setAttribute('class', 'pronunciation-graph-line');
+ path1.setAttribute('d', `M${pathPoints.join(' L')}`);
+
+ pathPoints.splice(0, ii - 1);
+ {
+ const highPitch = isMoraPitchHigh(ii, downstepPosition);
+ const x = ii * 50 + 25;
+ const y = highPitch ? 25 : 75;
+ addGraphTriangle(svg, svgns, x, y);
+ pathPoints.push(`${x} ${y}`);
+ }
+
+ path2.setAttribute('class', 'pronunciation-graph-line-tail');
+ path2.setAttribute('d', `M${pathPoints.join(' L')}`);
+
+ return svg;
+}
+
+/**
+ * @param {number} downstepPosition
+ * @returns {HTMLSpanElement}
+ */
+export function createPronunciationDownstepPosition(downstepPosition) {
+ const downstepPositionString = `${downstepPosition}`;
+
+ const n1 = document.createElement('span');
+ n1.className = 'pronunciation-downstep-notation';
+ n1.dataset.downstepPosition = downstepPositionString;
+
+ let n2 = document.createElement('span');
+ n2.className = 'pronunciation-downstep-notation-prefix';
+ n2.textContent = '[';
+ n1.appendChild(n2);
+
+ n2 = document.createElement('span');
+ n2.className = 'pronunciation-downstep-notation-number';
+ n2.textContent = downstepPositionString;
+ n1.appendChild(n2);
+
+ n2 = document.createElement('span');
+ n2.className = 'pronunciation-downstep-notation-suffix';
+ n2.textContent = ']';
+ n1.appendChild(n2);
+
+ return n1;
+}
+
+// Private
+
+/**
+ * @param {Element} container
+ * @param {string} svgns
+ * @param {number} x
+ * @param {number} y
+ */
+function addGraphDot(container, svgns, x, y) {
+ container.appendChild(createGraphCircle(svgns, 'pronunciation-graph-dot', x, y, '15'));
+}
+
+/**
+ * @param {Element} container
+ * @param {string} svgns
+ * @param {number} x
+ * @param {number} y
+ */
+function addGraphDotDownstep(container, svgns, x, y) {
+ container.appendChild(createGraphCircle(svgns, 'pronunciation-graph-dot-downstep1', x, y, '15'));
+ container.appendChild(createGraphCircle(svgns, 'pronunciation-graph-dot-downstep2', x, y, '5'));
+}
+
+/**
+ * @param {Element} container
+ * @param {string} svgns
+ * @param {number} x
+ * @param {number} y
+ */
+function addGraphTriangle(container, svgns, x, y) {
+ const node = document.createElementNS(svgns, 'path');
+ node.setAttribute('class', 'pronunciation-graph-triangle');
+ node.setAttribute('d', 'M0 13 L15 -13 L-15 -13 Z');
+ node.setAttribute('transform', `translate(${x},${y})`);
+ container.appendChild(node);
+}
+
+/**
+ * @param {string} svgns
+ * @param {string} className
+ * @param {number} x
+ * @param {number} y
+ * @param {string} radius
+ * @returns {Element}
+ */
+function createGraphCircle(svgns, className, x, y, radius) {
+ const node = document.createElementNS(svgns, 'circle');
+ node.setAttribute('class', className);
+ node.setAttribute('cx', `${x}`);
+ node.setAttribute('cy', `${y}`);
+ node.setAttribute('r', radius);
+ return node;
+}