|
@@ -0,0 +1,220 @@
|
|
|
+/// <reference path="util.ts" />
|
|
|
+/// <reference path="level.ts" />
|
|
|
+/// <reference path="audio.ts" />
|
|
|
+
|
|
|
+namespace editor {
|
|
|
+ export class Editor {
|
|
|
+ audioManager: audio.AudioManager;
|
|
|
+ audioElement: HTMLInputElement;
|
|
|
+ barElement: HTMLElement;
|
|
|
+ markerListElement: HTMLElement;
|
|
|
+ intervalListElement: HTMLElement;
|
|
|
+ kanaElement: HTMLElement;
|
|
|
+ kanjiElement: HTMLElement;
|
|
|
+ jsonElement: HTMLInputElement;
|
|
|
+ track: audio.Track | null = null;
|
|
|
+ markers: Marker[] = [];
|
|
|
+ rafId: number;
|
|
|
+
|
|
|
+ constructor() {
|
|
|
+ this.audioManager = new audio.AudioManager();
|
|
|
+ this.audioElement = document.querySelector('#audio');
|
|
|
+ this.audioElement.addEventListener('change', event => {
|
|
|
+ this.loadAudio();
|
|
|
+ });
|
|
|
+ this.barElement = document.querySelector('.bar-overlay');
|
|
|
+ this.markerListElement = document.querySelector('.markers');
|
|
|
+ this.intervalListElement = document.querySelector('#intervals');
|
|
|
+ this.kanaElement = document.querySelector('#kana');
|
|
|
+ this.kanjiElement = document.querySelector('#kanji');
|
|
|
+ this.jsonElement = document.querySelector('#json');
|
|
|
+
|
|
|
+ this.markerListElement.addEventListener('click', (event: MouseEvent) => this.markersClick(event));
|
|
|
+ document.querySelector('#play').addEventListener('click', () => this.play());
|
|
|
+ document.querySelector('#pause').addEventListener('click', () => this.pause());
|
|
|
+ document.querySelector('#insert-marker').addEventListener('click', () => this.insertMarker());
|
|
|
+ document.querySelector('.bar').addEventListener('click', (event: MouseEvent) => this.scrubberClick(event));
|
|
|
+ document.querySelector('#import').addEventListener('click', () => this.import());
|
|
|
+ document.querySelector('#export').addEventListener('click', () => this.export());
|
|
|
+ }
|
|
|
+
|
|
|
+ loadAudio(): void {
|
|
|
+ let file = this.audioElement.files[0];
|
|
|
+ if (file != null) {
|
|
|
+ if (this.track != null) {
|
|
|
+ this.track.stop();
|
|
|
+ }
|
|
|
+ this.clearMarkers();
|
|
|
+ this.audioManager.loadTrackFromFile(file).then(t => this.track = t);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ update(): void {
|
|
|
+ let percentage = this.track.getTime() / this.track.getDuration() * 100;
|
|
|
+ this.barElement.style.width = `${percentage}%`;
|
|
|
+ if (this.track.isPlaying()) {
|
|
|
+ this.rafId = requestAnimationFrame(() => this.update());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ scrubberClick(event: MouseEvent): void {
|
|
|
+ let pos = event.clientX - this.markerListElement.offsetLeft;
|
|
|
+ let percentage = pos / this.markerListElement.clientWidth;
|
|
|
+ let targetTime = percentage * this.track.getDuration();
|
|
|
+ this.track.stop();
|
|
|
+ this.track.resumeTime = targetTime;
|
|
|
+ this.play();
|
|
|
+ }
|
|
|
+
|
|
|
+ markersClick(event: MouseEvent): void {
|
|
|
+ let pos = event.clientX - this.markerListElement.offsetLeft;
|
|
|
+ let percentage = pos / this.markerListElement.clientWidth;
|
|
|
+ let targetTime = percentage * this.track.getDuration();
|
|
|
+ this.insertMarker(targetTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ insertMarker(time: number = undefined): void {
|
|
|
+ let marker = new Marker(
|
|
|
+ this.track.getDuration(),
|
|
|
+ (marker: Marker) => this.removeMarker(marker),
|
|
|
+ (marker: Marker) => this.playMarker(marker)
|
|
|
+ );
|
|
|
+ if (time !== undefined) {
|
|
|
+ marker.time = time;
|
|
|
+ } else {
|
|
|
+ marker.time = this.track.getTime();
|
|
|
+ }
|
|
|
+ let insertIndex = this.markers.findIndex(m => m.time > marker.time);
|
|
|
+ if (insertIndex >= 0) {
|
|
|
+ this.markers.splice(insertIndex, 0, marker);
|
|
|
+ } else {
|
|
|
+ this.markers.push(marker);
|
|
|
+ }
|
|
|
+ this.markerListElement.appendChild(marker.markerElement);
|
|
|
+ if (insertIndex >= 0) {
|
|
|
+ this.intervalListElement.insertBefore(marker.liElement, this.markers[insertIndex+1].liElement);
|
|
|
+ } else {
|
|
|
+ this.intervalListElement.appendChild(marker.liElement);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ play(duration: number = undefined): void {
|
|
|
+ window.cancelAnimationFrame(this.rafId);
|
|
|
+ this.track.start(duration);
|
|
|
+ this.update();
|
|
|
+ }
|
|
|
+
|
|
|
+ pause(): void {
|
|
|
+ this.track.pause();
|
|
|
+ }
|
|
|
+
|
|
|
+ playMarker(marker: Marker): void {
|
|
|
+ let start = 0;
|
|
|
+ let index = this.markers.findIndex(m => m == marker);
|
|
|
+ if (index > 0) {
|
|
|
+ start = this.markers[index - 1].time;
|
|
|
+ }
|
|
|
+ let duration = marker.time - start;
|
|
|
+ this.track.stop();
|
|
|
+ this.track.resumeTime = start;
|
|
|
+ this.play(duration);
|
|
|
+ }
|
|
|
+
|
|
|
+ removeMarker(marker: Marker): void {
|
|
|
+ let index = this.markers.findIndex(m => m == marker);
|
|
|
+ this.markers.splice(index, 1);
|
|
|
+ this.markerListElement.removeChild(marker.markerElement);
|
|
|
+ this.intervalListElement.removeChild(marker.liElement);
|
|
|
+ }
|
|
|
+
|
|
|
+ clearMarkers(): void {
|
|
|
+ this.markers.forEach(m => {
|
|
|
+ this.markerListElement.removeChild(m.markerElement);
|
|
|
+ this.intervalListElement.removeChild(m.liElement);
|
|
|
+ });
|
|
|
+ this.markers = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ import(): void {
|
|
|
+ this.clearMarkers();
|
|
|
+ let lines: level.Line[] = JSON.parse(this.jsonElement.value);
|
|
|
+ let kanji = '';
|
|
|
+ let kana = '';
|
|
|
+
|
|
|
+ lines.forEach(line => {
|
|
|
+ kanji += line.kanji + '<br>';
|
|
|
+ kana += line.kana + '<br>';
|
|
|
+ if (line.end != undefined) {
|
|
|
+ this.insertMarker(line.end);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.kanjiElement.innerHTML = kanji;
|
|
|
+ this.kanaElement.innerHTML = kana;
|
|
|
+ }
|
|
|
+
|
|
|
+ export(): void {
|
|
|
+ let kanji = this.kanjiElement.innerHTML.split('<br>');
|
|
|
+ let kana = this.kanaElement.innerHTML.split('<br>');
|
|
|
+ kanji.pop();
|
|
|
+ kana.pop();
|
|
|
+ let length = Math.max(kanji.length, kana.length, this.markers.length);
|
|
|
+
|
|
|
+ let lines = [];
|
|
|
+ let lastStart = 0;
|
|
|
+ for (let i = 0; i < length; ++i) {
|
|
|
+ let data: level.Line = {
|
|
|
+ kanji: kanji[i] || '@',
|
|
|
+ kana: kana[i] || '@',
|
|
|
+ }
|
|
|
+ if (this.markers[i]) {
|
|
|
+ data.start = lastStart;
|
|
|
+ data.end = this.markers[i].time;
|
|
|
+ lastStart = data.end;
|
|
|
+ }
|
|
|
+ lines.push(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.jsonElement.value = JSON.stringify(lines);
|
|
|
+ }
|
|
|
+
|
|
|
+ start(): void {
|
|
|
+ this.loadAudio();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ class Marker {
|
|
|
+ markerElement: HTMLElement;
|
|
|
+ liElement: HTMLElement;
|
|
|
+ inputElement: HTMLInputElement;
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ readonly duration: number,
|
|
|
+ readonly remove: (marker: Marker) => void,
|
|
|
+ readonly play: (marker: Marker) => void
|
|
|
+ ) {
|
|
|
+ this.markerElement = document.createElement('div');
|
|
|
+ this.markerElement.className = 'marker';
|
|
|
+
|
|
|
+ let fragment = util.loadTemplate('interval');
|
|
|
+ this.liElement = fragment.querySelector('*');
|
|
|
+ this.inputElement = fragment.querySelector('.interval');
|
|
|
+ this.inputElement.addEventListener('change', () => {
|
|
|
+ this.time = parseFloat(this.inputElement.value);
|
|
|
+ });
|
|
|
+
|
|
|
+ fragment.querySelector('.play-section').addEventListener('click', () => play(this));
|
|
|
+ fragment.querySelector('.remove-section').addEventListener('click', () => remove(this));
|
|
|
+ }
|
|
|
+
|
|
|
+ get time(): number {
|
|
|
+ return parseFloat(this.inputElement.value);
|
|
|
+ }
|
|
|
+
|
|
|
+ set time(t: number) {
|
|
|
+ this.inputElement.value = t.toFixed(1);
|
|
|
+ let percentage = t * 100 / this.duration;
|
|
|
+ this.markerElement.style.left = `${percentage}%`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|