Put vn rendering into a new vn module
This commit is contained in:
parent
d35d4824a0
commit
995b946dcb
336
src/main.rs
336
src/main.rs
@ -18,23 +18,18 @@ use data_stream::{
|
|||||||
FromStream, ToStream, collections::SizeSettings, default_settings::PortableSettings,
|
FromStream, ToStream, collections::SizeSettings, default_settings::PortableSettings,
|
||||||
from_stream, to_stream,
|
from_stream, to_stream,
|
||||||
};
|
};
|
||||||
use dialog::parse_map;
|
|
||||||
use dialogi::DialogParameter;
|
|
||||||
use header_config::parse_config;
|
use header_config::parse_config;
|
||||||
use indexmap::IndexMap;
|
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use percent_encoding::percent_decode_str;
|
use percent_encoding::percent_decode_str;
|
||||||
use pukram2html::{Settings, convert, convert_extended, convert_subheader};
|
use pukram2html::{Settings, convert, convert_extended, convert_subheader};
|
||||||
use threadpool::ThreadPool;
|
use threadpool::ThreadPool;
|
||||||
|
|
||||||
mod request;
|
|
||||||
use request::Request;
|
|
||||||
use vn_settings::{
|
|
||||||
ColorSettings, ImageSettings, LayoutSettings, Names, ObjectSettings, Parameter, PlayerSettings,
|
|
||||||
SettingsContext, TimingSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod dialog;
|
mod dialog;
|
||||||
|
mod request;
|
||||||
|
mod vn;
|
||||||
|
|
||||||
|
use request::Request;
|
||||||
|
use vn::render_novel;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let Ok(path) = env::current_dir() else {
|
let Ok(path) = env::current_dir() else {
|
||||||
@ -605,327 +600,6 @@ fn handle_relative_connection(
|
|||||||
.map(|parent| parse_config(&parent.with_extension("vng")).ok())
|
.map(|parent| parse_config(&parent.with_extension("vng")).ok())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
fn render_scene(settings: &PlayerSettings, name: &str, output: &mut Vec<u8>) {
|
|
||||||
for object in &settings.objects.objects {
|
|
||||||
let Some(image_set) = object.image.get(name) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
for image in &settings.images.images[image_set] {
|
|
||||||
let Some(image) = image.get_ref(name) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = writeln!(
|
|
||||||
output,
|
|
||||||
"<img src={image:?} style='max-height: 100%; max-width: 100%; object-fit: cover'/>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_novel(
|
|
||||||
mut config_map: IndexMap<Box<str>, Box<str>>,
|
|
||||||
pk_path: &Path,
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
start_level: usize,
|
|
||||||
) -> std::result::Result<(), dialogi::ParsingError> {
|
|
||||||
let mut player_settings = PlayerSettings {
|
|
||||||
colors: ColorSettings::common(),
|
|
||||||
timing: TimingSettings::common(),
|
|
||||||
images: ImageSettings::common(),
|
|
||||||
objects: ObjectSettings::common(),
|
|
||||||
layout: LayoutSettings::common(),
|
|
||||||
names: Names::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut layers = HashMap::new();
|
|
||||||
|
|
||||||
let keys: Vec<_> = config_map.keys().cloned().collect();
|
|
||||||
for key in keys {
|
|
||||||
let Some(("Layer", name)) = key.split_once(':') else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let value = config_map.shift_remove(&key).expect("Invalid layer");
|
|
||||||
if !value.is_empty() {
|
|
||||||
eprintln!("Layers don't accept arguments!");
|
|
||||||
}
|
|
||||||
layers.insert(name.into(), layers.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
if layers.is_empty() {
|
|
||||||
layers.insert("Background".into(), 0);
|
|
||||||
layers.insert("Character".into(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut settings_context = SettingsContext {
|
|
||||||
object_cache: HashMap::new(),
|
|
||||||
layers,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (key, value) in config_map {
|
|
||||||
if let Some(parameter) = Parameter::create(&key, &mut settings_context) {
|
|
||||||
let setter = parameter.value_setter(&value, &settings_context);
|
|
||||||
player_settings.set_character_default(setter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let dialogs = parse_map(pk_path, &mut settings_context)?;
|
|
||||||
|
|
||||||
let mut scenes = Vec::new();
|
|
||||||
let mut sections = Vec::new();
|
|
||||||
|
|
||||||
for dialog_sequence in dialogs {
|
|
||||||
let changes = &dialog_sequence.changes;
|
|
||||||
let mut states = HashMap::new();
|
|
||||||
for parameter in changes.keys() {
|
|
||||||
states.insert(parameter.clone(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
for block in &dialog_sequence.blocks {
|
|
||||||
let mut block_content = Vec::new();
|
|
||||||
|
|
||||||
for parameter in block
|
|
||||||
.lines
|
|
||||||
.iter()
|
|
||||||
.flat_map(|l| &l.actions)
|
|
||||||
.chain(&block.final_actions)
|
|
||||||
{
|
|
||||||
if let Some(state) = states.get_mut(parameter) {
|
|
||||||
let change = &dialog_sequence.changes[parameter][*state];
|
|
||||||
player_settings.change(change);
|
|
||||||
*state += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut scene_data = Vec::new();
|
|
||||||
render_scene(&player_settings, &block.name, &mut scene_data);
|
|
||||||
scenes.push(unsafe { String::from_utf8_unchecked(scene_data) });
|
|
||||||
|
|
||||||
let section_html = if block.lines.is_empty() {
|
|
||||||
html! {}
|
|
||||||
} else {
|
|
||||||
convert_subheader(
|
|
||||||
block.lines.iter().map(|l| l.text.as_ref()),
|
|
||||||
&mut block_content,
|
|
||||||
start_level,
|
|
||||||
);
|
|
||||||
|
|
||||||
html! {
|
|
||||||
fieldset class="visual-novel-box" {
|
|
||||||
@if !block.name.is_empty() {
|
|
||||||
legend class="character-name" { (block.name) }
|
|
||||||
}
|
|
||||||
div class="dialog-content" {
|
|
||||||
@if let Ok(content) = String::from_utf8(block_content) {
|
|
||||||
(maud::PreEscaped(content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sections.push(section_html);
|
|
||||||
}
|
|
||||||
|
|
||||||
player_settings.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = html! {
|
|
||||||
div id="story-container" {
|
|
||||||
div class="textbox-container" {
|
|
||||||
@for (index, scene) in scenes.iter().enumerate() {
|
|
||||||
div class="scene-section" data-section-index=(index)
|
|
||||||
style=(format!("display: {};", if index == 0 { "block" } else { "none" })) {
|
|
||||||
(maud::PreEscaped(scene))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div class="textbox-content" {
|
|
||||||
@for (index, section) in sections.iter().enumerate() {
|
|
||||||
div class="story-section" data-section-index=(index)
|
|
||||||
style=(format!("display: {};", if index == 0 { "block" } else { "none" })) {
|
|
||||||
(section)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
div class="nav-controls" {
|
|
||||||
button class="nav-button" onclick="prev()" { "←" }
|
|
||||||
span id="section-counter" { "1/" (sections.len()) }
|
|
||||||
button class="nav-button" onclick="next()" { "→" }
|
|
||||||
}
|
|
||||||
|
|
||||||
style {
|
|
||||||
(maud::PreEscaped(r"
|
|
||||||
.nav-controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin: 2rem 0;
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button {
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
background: #f0f0f0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#section-counter {
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #666;
|
|
||||||
margin: 0 1rem;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button:hover {
|
|
||||||
background: #e0e0e0;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.textbox-container {
|
|
||||||
width: 75%;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
padding-top: 42.1875%; /* 16:9 Aspect Ratio */
|
|
||||||
background: #f8f8f8;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textbox-content {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1rem;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visual-novel-box {
|
|
||||||
border: 0.2vw solid #3a3a3a;
|
|
||||||
border-radius: 1vw;
|
|
||||||
padding: 2vw;
|
|
||||||
margin: 1vh 0;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
box-shadow: 0 0.4vw 0.6vw rgba(0, 0, 0, 0.1);
|
|
||||||
width: 90%;
|
|
||||||
min-width: 85%;
|
|
||||||
font-size: 1.6vw;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 3vh;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-name {
|
|
||||||
font-family: 'Georgia', serif;
|
|
||||||
font-size: 1.8vw;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0 1.5vw;
|
|
||||||
background: #4a6b8a;
|
|
||||||
color: white;
|
|
||||||
border-radius: 0.5vw;
|
|
||||||
margin: 0;
|
|
||||||
transform: translateY(-60%);
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 1vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue-content {
|
|
||||||
font-size: 1.6vw;
|
|
||||||
line-height: 1.4;
|
|
||||||
max-height: 20vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 1vh 0.5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visual-novel-box::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
"))
|
|
||||||
}
|
|
||||||
|
|
||||||
script {
|
|
||||||
(maud::PreEscaped(r"
|
|
||||||
let currentSection = 0;
|
|
||||||
const totalSections = ".to_owned() + §ions.len().to_string() + r";
|
|
||||||
|
|
||||||
function updateSection() {
|
|
||||||
document.querySelectorAll('.story-section').forEach((el, index) => {
|
|
||||||
el.style.display = index === currentSection ? 'block' : 'none';
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.scene-section').forEach((el, index) => {
|
|
||||||
el.style.display = index === currentSection ? 'block' : 'none';
|
|
||||||
});
|
|
||||||
document.getElementById('section-counter').textContent =
|
|
||||||
`${currentSection + 1}/${totalSections}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function prev() {
|
|
||||||
if (currentSection > 0) {
|
|
||||||
--currentSection;
|
|
||||||
updateSection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function next() {
|
|
||||||
if (currentSection < totalSections - 1) {
|
|
||||||
++currentSection;
|
|
||||||
updateSection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
switch(e.keyCode) {
|
|
||||||
case 37:
|
|
||||||
prev();
|
|
||||||
break;
|
|
||||||
case 39:
|
|
||||||
next();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = write!(stream, "{}", html.into_string());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(config_map) = config_map {
|
if let Some(config_map) = config_map {
|
||||||
if render_novel(config_map, file_paths.pk, &mut stream, start_level).is_err() {
|
if render_novel(config_map, file_paths.pk, &mut stream, start_level).is_err() {
|
||||||
fail(stream);
|
fail(stream);
|
||||||
|
|||||||
333
src/vn.rs
Normal file
333
src/vn.rs
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
use std::{collections::HashMap, io::prelude::*, net::TcpStream, path::Path};
|
||||||
|
|
||||||
|
use dialogi::DialogParameter;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use maud::html;
|
||||||
|
use pukram2html::convert_subheader;
|
||||||
|
use vn_settings::{
|
||||||
|
ColorSettings, ImageSettings, LayoutSettings, Names, ObjectSettings, Parameter, PlayerSettings,
|
||||||
|
SettingsContext, TimingSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::dialog::parse_map;
|
||||||
|
|
||||||
|
fn render_scene(settings: &PlayerSettings, name: &str, output: &mut Vec<u8>) {
|
||||||
|
for object in &settings.objects.objects {
|
||||||
|
let Some(image_set) = object.image.get(name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for image in &settings.images.images[image_set] {
|
||||||
|
let Some(image) = image.get_ref(name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
output,
|
||||||
|
"<img src={image:?} style='max-height: 100%; max-width: 100%; object-fit: cover'/>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_novel(
|
||||||
|
mut config_map: IndexMap<Box<str>, Box<str>>,
|
||||||
|
pk_path: &Path,
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
start_level: usize,
|
||||||
|
) -> std::result::Result<(), dialogi::ParsingError> {
|
||||||
|
let mut player_settings = PlayerSettings {
|
||||||
|
colors: ColorSettings::common(),
|
||||||
|
timing: TimingSettings::common(),
|
||||||
|
images: ImageSettings::common(),
|
||||||
|
objects: ObjectSettings::common(),
|
||||||
|
layout: LayoutSettings::common(),
|
||||||
|
names: Names::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut layers = HashMap::new();
|
||||||
|
|
||||||
|
let keys: Vec<_> = config_map.keys().cloned().collect();
|
||||||
|
for key in keys {
|
||||||
|
let Some(("Layer", name)) = key.split_once(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = config_map.shift_remove(&key).expect("Invalid layer");
|
||||||
|
if !value.is_empty() {
|
||||||
|
eprintln!("Layers don't accept arguments!");
|
||||||
|
}
|
||||||
|
layers.insert(name.into(), layers.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
if layers.is_empty() {
|
||||||
|
layers.insert("Background".into(), 0);
|
||||||
|
layers.insert("Character".into(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut settings_context = SettingsContext {
|
||||||
|
object_cache: HashMap::new(),
|
||||||
|
layers,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (key, value) in config_map {
|
||||||
|
if let Some(parameter) = Parameter::create(&key, &mut settings_context) {
|
||||||
|
let setter = parameter.value_setter(&value, &settings_context);
|
||||||
|
player_settings.set_character_default(setter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dialogs = parse_map(pk_path, &mut settings_context)?;
|
||||||
|
|
||||||
|
let mut scenes = Vec::new();
|
||||||
|
let mut sections = Vec::new();
|
||||||
|
|
||||||
|
for dialog_sequence in dialogs {
|
||||||
|
let changes = &dialog_sequence.changes;
|
||||||
|
let mut states = HashMap::new();
|
||||||
|
for parameter in changes.keys() {
|
||||||
|
states.insert(parameter.clone(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for block in &dialog_sequence.blocks {
|
||||||
|
let mut block_content = Vec::new();
|
||||||
|
|
||||||
|
for parameter in block
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.flat_map(|l| &l.actions)
|
||||||
|
.chain(&block.final_actions)
|
||||||
|
{
|
||||||
|
if let Some(state) = states.get_mut(parameter) {
|
||||||
|
let change = &dialog_sequence.changes[parameter][*state];
|
||||||
|
player_settings.change(change);
|
||||||
|
*state += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut scene_data = Vec::new();
|
||||||
|
render_scene(&player_settings, &block.name, &mut scene_data);
|
||||||
|
scenes.push(unsafe { String::from_utf8_unchecked(scene_data) });
|
||||||
|
|
||||||
|
let section_html = if block.lines.is_empty() {
|
||||||
|
html! {}
|
||||||
|
} else {
|
||||||
|
convert_subheader(
|
||||||
|
block.lines.iter().map(|l| l.text.as_ref()),
|
||||||
|
&mut block_content,
|
||||||
|
start_level,
|
||||||
|
);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
fieldset class="visual-novel-box" {
|
||||||
|
@if !block.name.is_empty() {
|
||||||
|
legend class="character-name" { (block.name) }
|
||||||
|
}
|
||||||
|
div class="dialog-content" {
|
||||||
|
@if let Ok(content) = String::from_utf8(block_content) {
|
||||||
|
(maud::PreEscaped(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sections.push(section_html);
|
||||||
|
}
|
||||||
|
|
||||||
|
player_settings.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = html! {
|
||||||
|
div id="story-container" {
|
||||||
|
div class="textbox-container" {
|
||||||
|
@for (index, scene) in scenes.iter().enumerate() {
|
||||||
|
div class="scene-section" data-section-index=(index)
|
||||||
|
style=(format!("display: {};", if index == 0 { "block" } else { "none" })) {
|
||||||
|
(maud::PreEscaped(scene))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="textbox-content" {
|
||||||
|
@for (index, section) in sections.iter().enumerate() {
|
||||||
|
div class="story-section" data-section-index=(index)
|
||||||
|
style=(format!("display: {};", if index == 0 { "block" } else { "none" })) {
|
||||||
|
(section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="nav-controls" {
|
||||||
|
button class="nav-button" onclick="prev()" { "←" }
|
||||||
|
span id="section-counter" { "1/" (sections.len()) }
|
||||||
|
button class="nav-button" onclick="next()" { "→" }
|
||||||
|
}
|
||||||
|
|
||||||
|
style {
|
||||||
|
(maud::PreEscaped(r"
|
||||||
|
.nav-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#section-counter {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 1rem;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textbox-container {
|
||||||
|
width: 75%;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
padding-top: 42.1875%; /* 16:9 Aspect Ratio */
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textbox-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-novel-box {
|
||||||
|
border: 0.2vw solid #3a3a3a;
|
||||||
|
border-radius: 1vw;
|
||||||
|
padding: 2vw;
|
||||||
|
margin: 1vh 0;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
box-shadow: 0 0.4vw 0.6vw rgba(0, 0, 0, 0.1);
|
||||||
|
width: 90%;
|
||||||
|
min-width: 85%;
|
||||||
|
font-size: 1.6vw;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3vh;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-name {
|
||||||
|
font-family: 'Georgia', serif;
|
||||||
|
font-size: 1.8vw;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0 1.5vw;
|
||||||
|
background: #4a6b8a;
|
||||||
|
color: white;
|
||||||
|
border-radius: 0.5vw;
|
||||||
|
margin: 0;
|
||||||
|
transform: translateY(-60%);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 1vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogue-content {
|
||||||
|
font-size: 1.6vw;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-height: 20vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1vh 0.5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-novel-box::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
"))
|
||||||
|
}
|
||||||
|
|
||||||
|
script {
|
||||||
|
(maud::PreEscaped(r"
|
||||||
|
let currentSection = 0;
|
||||||
|
const totalSections = ".to_owned() + §ions.len().to_string() + r";
|
||||||
|
|
||||||
|
function updateSection() {
|
||||||
|
document.querySelectorAll('.story-section').forEach((el, index) => {
|
||||||
|
el.style.display = index === currentSection ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.scene-section').forEach((el, index) => {
|
||||||
|
el.style.display = index === currentSection ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
document.getElementById('section-counter').textContent =
|
||||||
|
`${currentSection + 1}/${totalSections}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev() {
|
||||||
|
if (currentSection > 0) {
|
||||||
|
--currentSection;
|
||||||
|
updateSection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (currentSection < totalSections - 1) {
|
||||||
|
++currentSection;
|
||||||
|
updateSection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
switch(e.keyCode) {
|
||||||
|
case 37:
|
||||||
|
prev();
|
||||||
|
break;
|
||||||
|
case 39:
|
||||||
|
next();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = write!(stream, "{}", html.into_string());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user