Merge branch 'main' of gordon.zumepro.cz:zumepro/pythagoras
This commit is contained in:
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 65 KiB |
1
assets/tul_logo.svg
Normal file
1
assets/tul_logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.1 KiB |
1
assets/zmp_logo.svg
Normal file
1
assets/zmp_logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="50mm" viewBox="0 0 50 50.000001" width="50mm" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd" transform="matrix(-.63629813 0 0 -.63629813 66.59034128483 91.64520371211)"><path d="m-11.759578-96.806604v-3.098032q-2.223016-.189194-3.500067-1.418944-1.253402-1.2534-1.3007-3.28723h2.128418q0 1.15881.685824 1.89193.709473.73312 1.986525.89867v-6.19607l-.638526-.18919q-1.773682-.54393-2.743295-1.82098-.945964-1.27705-.945964-3.00344 0-1.89192 1.158805-3.12168 1.182455-1.22975 3.16898-1.41894v-3.12168h1.418946v3.09803q1.9628749.18919 3.1453299 1.41894 1.182455 1.22976 1.2061041 3.16898h-2.128419q0-1.1115-.5912275-1.79733-.5675783-.70947-1.6317875-.87501v5.86497l.9459637.30744q1.7263842.54393 2.6723482 1.84463.945964 1.3007.945964 3.05073 0 1.93923-1.2534023 3.21628-1.2297532 1.27705-3.3108736 1.489894v3.098032zm-2.199367-16.412476q0 1.06421.567579 1.84463.567578.78042 1.631788 1.11151v-5.41564q-1.040561.16554-1.631788.80406-.567579.63853-.567579 1.65544zm3.618313 11.39887q1.1588056-.1892 1.7973312-.89867.6385257-.70947.6385257-1.84463 0-1.04056-.5675784-1.79733-.5439293-.78042-1.5844895-1.11151l-.283789-.0946z" fill="#fff" transform="matrix(-1.0225486 0 0 -.97794862 24.554623 -1.612364)"/><path d="m-59.312695-104.31899q-.273111.60084-.710088 1.11065-.436977.4916-1.001405.87395-.546222.36415-1.219894.58264-.673673.20028-1.438383.20028-.782917 0-1.584041-.21849-.801125-.2367-1.547627-.54622-.910369-.36415-1.747908-.69188-.819332-.32774-1.602249-.32774-.910368 0-1.511212.45519-.600843.43698-1.074235 1.20169l-1.857152-1.32914q.273111-.60085.710088-1.09244.436977-.50981.983198-.87396.564429-.38235 1.238101-.58263.673673-.21849 1.438383-.21849.892161 0 1.80253.29131.910368.29132 1.747908.63726.782917.32774 1.529419.60085.746502.2549 1.401968.2549.892161 0 1.511212-.43698.61905-.45518 1.074235-1.21989z" fill="#9aff00" transform="scale(-1)"/><path d="m100.26473 100.06705v-3.596477l-7.441251 7.192957 7.441251 7.19295v-3.59647l-3.720626-3.59648z" fill="#fff"/></g></svg>
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,7 +1,7 @@
|
|||||||
CLIENT_PAGES := index.html
|
CLIENT_PAGES := index.html
|
||||||
CLIENT_STYLES := style.css
|
CLIENT_STYLES := style.css
|
||||||
CLIENT_SCRIPTS := script.js
|
CLIENT_SCRIPTS := script.js
|
||||||
CLIENT_ASSETS := fm_logo.svg qr_ask.svg
|
CLIENT_ASSETS := fm_logo.svg zmp_logo.svg tul_logo.svg qr_ask.svg
|
||||||
CLIENT_TARGETS := $(CLIENT_PAGES:%=static/%) \
|
CLIENT_TARGETS := $(CLIENT_PAGES:%=static/%) \
|
||||||
$(CLIENT_STYLES:%=static/%) \
|
$(CLIENT_STYLES:%=static/%) \
|
||||||
$(CLIENT_SCRIPTS:%=static/%) \
|
$(CLIENT_SCRIPTS:%=static/%) \
|
||||||
|
@@ -3,7 +3,7 @@ import {
|
|||||||
} from "./pythagoras_client";
|
} from "./pythagoras_client";
|
||||||
import { ScrollingTextBox } from "./scrolling_textbox";
|
import { ScrollingTextBox } from "./scrolling_textbox";
|
||||||
import { dict, IDLE_LOGOS, QUESTION_LINK, QUESTION_QR } from "./settings";
|
import { dict, IDLE_LOGOS, QUESTION_LINK, QUESTION_QR } from "./settings";
|
||||||
import { el, sleep } from "./tools";
|
import { AsyncRunner, el, sleep, wait_for_dom_refresh } from "./tools";
|
||||||
|
|
||||||
interface PresentationScreen {
|
interface PresentationScreen {
|
||||||
prepare(): Promise<void>;
|
prepare(): Promise<void>;
|
||||||
@@ -27,7 +27,7 @@ export class PresentationManager {
|
|||||||
await this.screen.prepare();
|
await this.screen.prepare();
|
||||||
this.dom_root.innerHTML = "";
|
this.dom_root.innerHTML = "";
|
||||||
this.dom_root.appendChild(this.screen.dom);
|
this.dom_root.appendChild(this.screen.dom);
|
||||||
await sleep(10);
|
await wait_for_dom_refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async serve(ws_client: PythagorasClient): Promise<void> {
|
public async serve(ws_client: PythagorasClient): Promise<void> {
|
||||||
@@ -67,24 +67,29 @@ export class PresentationManager {
|
|||||||
|
|
||||||
class MainScreen implements PresentationScreen {
|
class MainScreen implements PresentationScreen {
|
||||||
private dom_root: HTMLDivElement;
|
private dom_root: HTMLDivElement;
|
||||||
private subs_english: ScrollingTextBox;
|
private subs_english: AsyncRunner<ScrollingTextBox>;
|
||||||
private subs_czech: ScrollingTextBox;
|
private subs_czech: AsyncRunner<ScrollingTextBox>;
|
||||||
private dom_question: HTMLDivElement;
|
private dom_question: HTMLDivElement;
|
||||||
private dom_question_link: HTMLDivElement;
|
private dom_question_link: HTMLDivElement;
|
||||||
private question_insert: HTMLParagraphElement;
|
private question_insert: HTMLParagraphElement;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.subs_english = new ScrollingTextBox();
|
const subs_en = new ScrollingTextBox();
|
||||||
this.subs_czech = new ScrollingTextBox();
|
const subs_cz = new ScrollingTextBox();
|
||||||
|
this.subs_english = new AsyncRunner(subs_en);
|
||||||
|
this.subs_czech = new AsyncRunner(subs_cz);
|
||||||
this.question_insert = el.p("");
|
this.question_insert = el.p("");
|
||||||
this.dom_question_link = el.div([el.h1(QUESTION_LINK)], ["link"]);
|
this.dom_question_link = el.div([el.h1(QUESTION_LINK)], ["link"]);
|
||||||
this.dom_question = el.div([this.dom_question_link, this.question_insert], ["question"]);
|
this.dom_question = el.div([this.dom_question_link, this.question_insert], ["question"]);
|
||||||
this.dom_root = el.div([
|
this.dom_root = el.div([
|
||||||
this.dom_question,
|
this.dom_question,
|
||||||
el.div([
|
el.div([
|
||||||
el.div([this.subs_english.dom], ["lang"]),
|
el.div([subs_en.dom], ["lang"]),
|
||||||
el.div([this.subs_czech.dom], ["lang"]),
|
el.div([subs_cz.dom], ["lang"]),
|
||||||
], ["subtitles"]),
|
], ["subtitles"]),
|
||||||
|
el.div([
|
||||||
|
el.p("Jedná se o automaticky generovaný přepis. Omluvte, prosíme, případné chyby.", ["disclaimer"]),
|
||||||
|
]),
|
||||||
], ["main"]);
|
], ["main"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +98,7 @@ class MainScreen implements PresentationScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
await sleep(10);
|
await wait_for_dom_refresh();
|
||||||
this.dom_root.style.opacity = "1";
|
this.dom_root.style.opacity = "1";
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
}
|
}
|
||||||
@@ -117,13 +122,19 @@ class MainScreen implements PresentationScreen {
|
|||||||
public async serve(trigger: PythagorasIncomingMessage): Promise<void> {
|
public async serve(trigger: PythagorasIncomingMessage): Promise<void> {
|
||||||
switch (trigger.type) {
|
switch (trigger.type) {
|
||||||
case PythagorasIncomingMessageType.SubEnUpdateCur:
|
case PythagorasIncomingMessageType.SubEnUpdateCur:
|
||||||
await this.subs_english.update_current(trigger.text);
|
this.subs_english.run((target: ScrollingTextBox) => {
|
||||||
|
return target.update_current(trigger.text)
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case PythagorasIncomingMessageType.SubEnSubmit:
|
case PythagorasIncomingMessageType.SubEnSubmit:
|
||||||
await this.subs_english.finish_line(trigger.text);
|
this.subs_english.run((target: ScrollingTextBox) => {
|
||||||
|
return target.finish_current(trigger.text);
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case PythagorasIncomingMessageType.SubCzSubmit:
|
case PythagorasIncomingMessageType.SubCzSubmit:
|
||||||
await this.subs_czech.add_line(trigger.text);
|
this.subs_czech.run((target: ScrollingTextBox) => {
|
||||||
|
return target.add_line(trigger.text);
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case PythagorasIncomingMessageType.SelectedMessage:
|
case PythagorasIncomingMessageType.SelectedMessage:
|
||||||
if (trigger.message === null) { this.hide_question(); }
|
if (trigger.message === null) { this.hide_question(); }
|
||||||
@@ -198,7 +209,7 @@ class VideoScreen implements PresentationScreen {
|
|||||||
this.dom_video.currentTime = seconds_from_start;
|
this.dom_video.currentTime = seconds_from_start;
|
||||||
this.dom_root.innerHTML = "";
|
this.dom_root.innerHTML = "";
|
||||||
this.dom_root.appendChild(this.dom_video);
|
this.dom_root.appendChild(this.dom_video);
|
||||||
await sleep(10);
|
await wait_for_dom_refresh();
|
||||||
await this.start();
|
await this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +226,7 @@ class IdleScreen implements PresentationScreen {
|
|||||||
private dom_root: HTMLDivElement;
|
private dom_root: HTMLDivElement;
|
||||||
private dom_title: HTMLHeadingElement;
|
private dom_title: HTMLHeadingElement;
|
||||||
private dom_subtitle: HTMLHeadingElement;
|
private dom_subtitle: HTMLHeadingElement;
|
||||||
private dom_logos: HTMLImageElement[];
|
private dom_logos: HTMLDivElement[];
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.dom_title = el.h1(dict.IDLE_TITLE)
|
this.dom_title = el.h1(dict.IDLE_TITLE)
|
||||||
@@ -226,14 +237,14 @@ class IdleScreen implements PresentationScreen {
|
|||||||
|
|
||||||
public async prepare(): Promise<void> {
|
public async prepare(): Promise<void> {
|
||||||
for (const logo of IDLE_LOGOS) {
|
for (const logo of IDLE_LOGOS) {
|
||||||
this.dom_logos.push(await el.img(logo, ["logo"]));
|
this.dom_logos.push(el.div([await el.img(logo)], ["logo"]));
|
||||||
}
|
}
|
||||||
const logos = el.div([...this.dom_logos], ["logos"]);
|
const logos = el.div([...this.dom_logos], ["logos"]);
|
||||||
this.dom_root.appendChild(logos);
|
this.dom_root.appendChild(logos);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
await sleep(10);
|
await wait_for_dom_refresh();
|
||||||
this.dom_title.style.transform = "translateY(0)";
|
this.dom_title.style.transform = "translateY(0)";
|
||||||
this.dom_title.style.opacity = "1";
|
this.dom_title.style.opacity = "1";
|
||||||
await sleep(250);
|
await sleep(250);
|
||||||
|
@@ -119,7 +119,6 @@ export class PythagorasClient {
|
|||||||
const payload = await this.recv_inner();
|
const payload = await this.recv_inner();
|
||||||
const text = payload.bytes === undefined ?
|
const text = payload.bytes === undefined ?
|
||||||
(await payload.text()).slice(4) : (utf8_decode((await payload.bytes()).slice(4)));
|
(await payload.text()).slice(4) : (utf8_decode((await payload.bytes()).slice(4)));
|
||||||
console.log("payload:", text);
|
|
||||||
const parsed = JSON.parse(text);
|
const parsed = JSON.parse(text);
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
@@ -1,138 +1,281 @@
|
|||||||
import { DELAY_WORDS } from "./settings";
|
import { el, sleep, wait_for_dom_refresh } from "./tools";
|
||||||
import { el, sleep } from "./tools";
|
|
||||||
|
|
||||||
export class ScrollingTextBox {
|
class Word {
|
||||||
private dom_root: HTMLDivElement;
|
private dom_root: HTMLParagraphElement;
|
||||||
|
|
||||||
private prev_line_words: HTMLDivElement[];
|
private inner: string;
|
||||||
private cur_line_words: HTMLDivElement[];
|
private is_visible: boolean;
|
||||||
|
|
||||||
private prev_line: HTMLParagraphElement;
|
private static ANIMATION_DURATION: number = 20;
|
||||||
private current_line: HTMLParagraphElement;
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor(inner: string, initially_visible: boolean = false) {
|
||||||
this.dom_root = el.div([], ["scrolling-textbox"]);
|
this.inner = inner;
|
||||||
this.cur_line_words = [];
|
this.is_visible = initially_visible;
|
||||||
this.prev_line_words = [];
|
this.dom_root = el.p(this.inner, ["word"]);
|
||||||
|
|
||||||
this.current_line = el.div(this.cur_line_words, ["paragraph", "active"]);
|
if (!this.is_visible) {
|
||||||
this.prev_line = el.div(this.prev_line_words, ["paragraph", "previous"]);
|
this.dom_root.style.opacity = "0";
|
||||||
|
this.dom_root.style.transform = "scale(0)";
|
||||||
this.dom_root.appendChild(this.prev_line);
|
|
||||||
this.dom_root.appendChild(this.current_line);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update_current(text: string): Promise<void> {
|
|
||||||
this.update_words(this.current_line, this.cur_line_words, text);
|
|
||||||
await sleep(10);
|
|
||||||
this.show_new_words(this.cur_line_words);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async finish_line(text: string): Promise<void> {
|
|
||||||
this.update_words(this.current_line, this.cur_line_words, text, true);
|
|
||||||
const current_height = this.current_line.getBoundingClientRect().height;
|
|
||||||
this.prev_line.style.transform = `translateY(calc(-100% - ${current_height}px))`;
|
|
||||||
this.current_line.style.transform = "translateY(-100%)";
|
|
||||||
this.current_line.style.color = "grey";
|
|
||||||
await sleep(500);
|
|
||||||
this.dom_root.removeChild(this.prev_line);
|
|
||||||
this.prev_line = this.current_line;
|
|
||||||
this.prev_line_words = this.cur_line_words;
|
|
||||||
this.prev_line.className = "paragraph previous";
|
|
||||||
this.current_line = el.div([], ["paragraph", "active"]);
|
|
||||||
this.cur_line_words = [];
|
|
||||||
this.dom_root.appendChild(this.current_line);
|
|
||||||
await sleep(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async add_line(text: string): Promise<void> {
|
|
||||||
const current_height = this.current_line.getBoundingClientRect().height;
|
|
||||||
const next_words = this.make_words(text);
|
|
||||||
const next_line = el.div(next_words, ["paragraph", "previous"]);
|
|
||||||
next_line.style.transform = `translateY(${current_height}px)`;
|
|
||||||
next_line.style.opacity = "0";
|
|
||||||
this.dom_root.appendChild(next_line);
|
|
||||||
await sleep(50);
|
|
||||||
next_line.style.transform = "translateY(0px)";
|
|
||||||
next_line.style.opacity = "1";
|
|
||||||
this.current_line.style.transform = "translateY(-100%)";
|
|
||||||
this.current_line.style.color = "grey";
|
|
||||||
this.prev_line.style.transform = `translateY(calc(-100% - ${current_height}px))`;
|
|
||||||
await sleep(500);
|
|
||||||
this.dom_root.removeChild(this.prev_line);
|
|
||||||
this.prev_line = this.current_line;
|
|
||||||
this.prev_line_words = this.cur_line_words;
|
|
||||||
this.prev_line.className = "paragraph previous";
|
|
||||||
this.current_line = next_line;
|
|
||||||
this.current_line.className = "paragraph active";
|
|
||||||
this.cur_line_words = next_words;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async show_new_words(words: HTMLParagraphElement[]): Promise<void> {
|
|
||||||
for (const word of words) {
|
|
||||||
if (word.style.opacity != "0") { continue; }
|
|
||||||
word.style.opacity = "1";
|
|
||||||
word.style.transform = "scale(100%)";
|
|
||||||
await sleep(50);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private update_words(
|
public set text(new_value: string) {
|
||||||
parent: HTMLDivElement,
|
this.inner = new_value;
|
||||||
words: HTMLParagraphElement[],
|
this.dom_root.innerText = this.inner;
|
||||||
new_text: string,
|
|
||||||
override: boolean = false,
|
|
||||||
): void {
|
|
||||||
const new_words = new_text.split(" ");
|
|
||||||
if (!override) {
|
|
||||||
for (let i = 0; i < DELAY_WORDS; ++i) { new_words.pop(); }
|
|
||||||
}
|
|
||||||
if (words.length > new_words.length) {
|
|
||||||
this.trim_words(parent, words, new_words.length);
|
|
||||||
for (let i = 0; i < new_words.length; ++i) {
|
|
||||||
if (words[i].innerText == new_words[i]) { continue; }
|
|
||||||
words[i].innerText = new_words[i];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const to_push: HTMLParagraphElement[] = [];
|
|
||||||
for (let i = 0; i < new_words.length; ++i) {
|
|
||||||
if (i < words.length) {
|
|
||||||
if (new_words[i] == words[i].innerText) { continue; }
|
|
||||||
words[i].innerText = new_words[i];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const word = el.p(new_words[i], ["word"]);
|
|
||||||
if (!override) {
|
|
||||||
word.style.opacity = "0";
|
|
||||||
word.style.transform = "scale(0)";
|
|
||||||
}
|
|
||||||
words.push(word);
|
|
||||||
to_push.push(word);
|
|
||||||
}
|
|
||||||
parent.append(...to_push);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private trim_words(
|
public get text(): string {
|
||||||
parent: HTMLDivElement,
|
return this.inner;
|
||||||
words: HTMLParagraphElement[],
|
|
||||||
target_length: number
|
|
||||||
): void {
|
|
||||||
if (target_length >= words.length || words.length == 0) { return; }
|
|
||||||
for (let i = words.length - 1; i >= target_length; --i) {
|
|
||||||
const word = words.pop();
|
|
||||||
if (word === undefined) { continue; }
|
|
||||||
parent.removeChild(word);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private make_words(text: string): HTMLDivElement[] {
|
public async show(): Promise<void> {
|
||||||
const words = text.split(" ");
|
if (this.is_visible) { return; }
|
||||||
let res = [];
|
this.dom_root.style.opacity = "1";
|
||||||
for (const word of words) {
|
this.dom_root.style.transform = "scale(100%)";
|
||||||
res.push(el.p(word, ["word"]));
|
this.is_visible = true;
|
||||||
}
|
await sleep(Word.ANIMATION_DURATION);
|
||||||
return res;
|
}
|
||||||
|
|
||||||
|
public get visible(): boolean {
|
||||||
|
return this.is_visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get dom(): HTMLDivElement {
|
||||||
|
return this.dom_root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Paragraph {
|
||||||
|
private dom_root: HTMLDivElement;
|
||||||
|
private words: Word[];
|
||||||
|
|
||||||
|
private height: number;
|
||||||
|
private height_current: boolean;
|
||||||
|
private visible: boolean;
|
||||||
|
private vertical_position: number;
|
||||||
|
|
||||||
|
private static SUBTLE_COLOR: string = "gray";
|
||||||
|
private static ACTIVE_COLOR: string = "white";
|
||||||
|
private static WORD_ANIM_POST_WAIT: number = 180;
|
||||||
|
|
||||||
|
public constructor(initial_text: string | null) {
|
||||||
|
this.dom_root = el.div([], ["paragraph"]);
|
||||||
|
this.set_color(Paragraph.ACTIVE_COLOR);
|
||||||
|
this.words = [];
|
||||||
|
this.visible = true;
|
||||||
|
this.height = 0;
|
||||||
|
this.height_current = true;
|
||||||
|
this.vertical_position = 0;
|
||||||
|
this.set_initial_text(initial_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private set_initial_text(text: string | null): void {
|
||||||
|
if (text === null) { return; }
|
||||||
|
this.visible = false;
|
||||||
|
this.height_current = false;
|
||||||
|
this.dom_root.style.opacity = "0";
|
||||||
|
const initial_words = text.split(" ");
|
||||||
|
for (const word of initial_words) {
|
||||||
|
const cur_word = new Word(word, true);
|
||||||
|
this.words.push(cur_word);
|
||||||
|
this.dom_root.appendChild(cur_word.dom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private set_color(new_color: string): void {
|
||||||
|
this.dom_root.style.color = new_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
private trim_words(target_length: number): void {
|
||||||
|
if (this.words.length == 0 || target_length < 0) { return; }
|
||||||
|
if (this.words.length != target_length) { this.invalidate_height(); }
|
||||||
|
for (let i = this.words.length - 1; i >= target_length; --i) {
|
||||||
|
const current_pop = this.words.pop();
|
||||||
|
if (current_pop === undefined) { continue; }
|
||||||
|
this.dom_root.removeChild(current_pop.dom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private invalidate_height(): void {
|
||||||
|
this.height_current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set_text(new_text: string): void {
|
||||||
|
const new_words = new_text.split(" ");
|
||||||
|
if (new_words.length < this.words.length) { this.trim_words(new_words.length); }
|
||||||
|
for (let i = 0; i < new_words.length; ++i) {
|
||||||
|
const new_word = new_words[i];
|
||||||
|
if (this.words.length <= i) {
|
||||||
|
const cur_word = new Word(new_word);
|
||||||
|
this.words.push(cur_word);
|
||||||
|
this.dom_root.appendChild(cur_word.dom);
|
||||||
|
this.invalidate_height();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.words[i].text != new_word) {
|
||||||
|
this.words[i].text = new_word;
|
||||||
|
this.invalidate_height();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async animate(): Promise<void> {
|
||||||
|
for (const word of this.words) {
|
||||||
|
await word.show();
|
||||||
|
}
|
||||||
|
await sleep(Paragraph.WORD_ANIM_POST_WAIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculate_height(): void {
|
||||||
|
this.height = this.dom_root.getBoundingClientRect().height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get real_height(): number {
|
||||||
|
if (!this.height_current) {
|
||||||
|
this.calculate_height();
|
||||||
|
this.height_current = true;
|
||||||
|
}
|
||||||
|
return this.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public subtleize(): Promise<void> {
|
||||||
|
this.set_color(Paragraph.SUBTLE_COLOR);
|
||||||
|
return sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async show(): Promise<void> {
|
||||||
|
if (this.visible) { return; }
|
||||||
|
this.dom_root.style.opacity = "1";
|
||||||
|
return sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
public set translate_y(value: number) {
|
||||||
|
this.vertical_position = value;
|
||||||
|
this.dom_root.style.transform = `translateY(${value}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get translate_y(): number {
|
||||||
|
return this.vertical_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get dom(): HTMLDivElement {
|
||||||
|
return this.dom_root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScrollingTextBox {
|
||||||
|
private dom_root: HTMLDivElement;
|
||||||
|
|
||||||
|
private paragraphs: Paragraph[];
|
||||||
|
|
||||||
|
private static MAX_HEIGHT: number = 1000;
|
||||||
|
private static CENTER_HEIGHT: number = 600;
|
||||||
|
private static VERTICAL_MARGIN: number = 10;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.dom_root = el.div([], ["scrolling-textbox"]);
|
||||||
|
this.paragraphs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private add_paragraph(initial_text: string | null): void {
|
||||||
|
const new_paragraph = new Paragraph(initial_text);
|
||||||
|
new_paragraph.translate_y = ScrollingTextBox.CENTER_HEIGHT;
|
||||||
|
this.paragraphs.push(new_paragraph);
|
||||||
|
this.dom_root.appendChild(new_paragraph.dom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private add_paragraph_bottom(initial_text: string): void {
|
||||||
|
if (this.paragraphs.length == 0) {
|
||||||
|
this.add_paragraph(initial_text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const most_bottom = this.paragraphs[this.paragraphs.length - 1];
|
||||||
|
const offset = most_bottom.translate_y + most_bottom.real_height;
|
||||||
|
const new_paragraph = new Paragraph(initial_text);
|
||||||
|
new_paragraph.translate_y = offset;
|
||||||
|
this.paragraphs.push(new_paragraph);
|
||||||
|
this.dom_root.appendChild(new_paragraph.dom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all paragraphs up to (not including) a certain index.
|
||||||
|
*/
|
||||||
|
private trim_to(exclusive_end: number): void {
|
||||||
|
const spliced = this.paragraphs.splice(0, exclusive_end);
|
||||||
|
for (const item of spliced) {
|
||||||
|
this.dom_root.removeChild(item.dom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run scroll animation on the first element and then pop it off of the DOM.
|
||||||
|
*/
|
||||||
|
private async scroll_trim(offset: number): Promise<void> {
|
||||||
|
const to_trim = this.paragraphs.shift();
|
||||||
|
if (to_trim === undefined) { return; }
|
||||||
|
to_trim.translate_y = offset;
|
||||||
|
await sleep(500);
|
||||||
|
this.dom_root.removeChild(to_trim.dom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update elements DOM scrolls and wait for animations to complete.
|
||||||
|
*
|
||||||
|
* Note that this will remove any paragraphs that aren't visible anymore.
|
||||||
|
*/
|
||||||
|
private async update_scroll(): Promise<void> {
|
||||||
|
if (this.paragraphs.length == 0) { return; }
|
||||||
|
const last = this.paragraphs[this.paragraphs.length - 1];
|
||||||
|
let offset = Math.min(
|
||||||
|
ScrollingTextBox.MAX_HEIGHT - last.real_height, ScrollingTextBox.CENTER_HEIGHT
|
||||||
|
);
|
||||||
|
last.translate_y = offset;
|
||||||
|
if (this.paragraphs.length == 1) { return; }
|
||||||
|
for (let i = this.paragraphs.length - 2; i >= 0; --i) {
|
||||||
|
offset -= ScrollingTextBox.VERTICAL_MARGIN;
|
||||||
|
const cur = this.paragraphs[i];
|
||||||
|
if (offset <= 0) {
|
||||||
|
this.trim_to(i);
|
||||||
|
this.scroll_trim(offset - cur.real_height);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset -= cur.real_height;
|
||||||
|
cur.translate_y = offset;
|
||||||
|
}
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update_current(text: string): Promise<void> {
|
||||||
|
if (this.paragraphs.length == 0) {
|
||||||
|
this.add_paragraph(null);
|
||||||
|
this.update_scroll();
|
||||||
|
}
|
||||||
|
const current = this.paragraphs[this.paragraphs.length - 1];
|
||||||
|
current.set_text(text);
|
||||||
|
await wait_for_dom_refresh();
|
||||||
|
await Promise.all([this.update_scroll(), current.animate()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async finish_current(text: string): Promise<void> {
|
||||||
|
if (this.paragraphs.length == 0) {
|
||||||
|
this.add_paragraph(text);
|
||||||
|
}
|
||||||
|
const current = this.paragraphs[this.paragraphs.length - 1];
|
||||||
|
current.set_text(text);
|
||||||
|
this.add_paragraph(null);
|
||||||
|
await Promise.all([this.update_scroll(), current.subtleize(), current.animate()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async add_line(text: string): Promise<void> {
|
||||||
|
if (this.paragraphs.length != 0) {
|
||||||
|
this.paragraphs[this.paragraphs.length - 1].subtleize();
|
||||||
|
}
|
||||||
|
this.add_paragraph_bottom(text);
|
||||||
|
await wait_for_dom_refresh();
|
||||||
|
await Promise.all([
|
||||||
|
this.update_scroll(),
|
||||||
|
this.paragraphs[this.paragraphs.length - 1].show(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get dom(): HTMLDivElement {
|
public get dom(): HTMLDivElement {
|
||||||
|
@@ -4,7 +4,9 @@ export namespace dict {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const IDLE_LOGOS: string[] = [
|
export const IDLE_LOGOS: string[] = [
|
||||||
|
"files/tul_logo.svg",
|
||||||
"files/fm_logo.svg",
|
"files/fm_logo.svg",
|
||||||
|
"files/zmp_logo.svg",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const QUESTION_QR: string = "files/qr_ask.svg";
|
export const QUESTION_QR: string = "files/qr_ask.svg";
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
|
|
||||||
|
#run {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial;
|
font-family: Arial;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
@@ -19,14 +26,14 @@ body {
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
transition: .5s ease transform, .5s ease opacity;
|
transition: .5s ease transform, .5s ease opacity;
|
||||||
|
font-size: 3em;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
transition: .5s ease opacity;
|
transition: .5s ease opacity;
|
||||||
color: grey;
|
color: color.scale(white, $lightness: -25%);
|
||||||
font-size: 1em;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -34,16 +41,23 @@ body {
|
|||||||
|
|
||||||
.logos {
|
.logos {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 10em;
|
margin-top: 15em;
|
||||||
width: 40rem;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
transition: .5s ease-out transform, .5s ease-out opacity;
|
transition: .5s ease-out transform, .5s ease-out opacity;
|
||||||
width: 15em;
|
height: 7em;
|
||||||
transform: translateY(100%);
|
transform: translateY(100%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
margin: 0 1em;
|
margin: 0 2em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 25em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +77,15 @@ body {
|
|||||||
.main {
|
.main {
|
||||||
transition: .5s ease opacity;
|
transition: .5s ease opacity;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
|
.disclaimer {
|
||||||
|
color: color.adjust(white, $lightness: -40%);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.question {
|
.question {
|
||||||
@@ -120,14 +143,18 @@ body {
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 1000px;
|
||||||
|
|
||||||
.paragraph {
|
.paragraph {
|
||||||
transition: .5s ease color, .5s ease transform, .5s ease opacity;
|
transition: .5s ease color, .5s ease transform, .5s ease opacity;
|
||||||
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: $line-height;
|
font-size: $line-height;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
top: 0;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
height: fit-content;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
|
@@ -31,6 +31,43 @@ export async function sleep(millis: number): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function wait_for_dom_refresh(): Promise<void> {
|
||||||
|
return new Promise<void>((resolver) => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
resolver();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a locking abstraction around any ref-passed object to provide inner mutability that can
|
||||||
|
* automatically release on promise resolution. This provides "atomic transactions" on the inner
|
||||||
|
* type.
|
||||||
|
*
|
||||||
|
* This allows you to not await the transaction result, but offload the waiting to the next call
|
||||||
|
* to run. So other tasks can normally run when the transaction is executing. And if the inner
|
||||||
|
* type is needed again... the run call will wait for the (possibly) running transaction to
|
||||||
|
* finish first.
|
||||||
|
*/
|
||||||
|
export class AsyncRunner<T> {
|
||||||
|
private inner: T;
|
||||||
|
private lock: Promise<void>;
|
||||||
|
|
||||||
|
public constructor(inner: T) {
|
||||||
|
this.inner = inner;
|
||||||
|
this.lock = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run(transaction: (target: T) => Promise<void>): Promise<void> {
|
||||||
|
await this.lock;
|
||||||
|
this.lock = new Promise((resolver) => {
|
||||||
|
transaction(this.inner).then(() => { resolver() });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export namespace el {
|
export namespace el {
|
||||||
function add_classes(target: HTMLElement, classes?: string[]): void {
|
function add_classes(target: HTMLElement, classes?: string[]): void {
|
||||||
if (classes === undefined) { return; }
|
if (classes === undefined) { return; }
|
||||||
|
Reference in New Issue
Block a user