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,
|
||||
from_stream, to_stream,
|
||||
};
|
||||
use dialog::parse_map;
|
||||
use dialogi::DialogParameter;
|
||||
use header_config::parse_config;
|
||||
use indexmap::IndexMap;
|
||||
use maud::html;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use pukram2html::{Settings, convert, convert_extended, convert_subheader};
|
||||
use threadpool::ThreadPool;
|
||||
|
||||
mod request;
|
||||
use request::Request;
|
||||
use vn_settings::{
|
||||
ColorSettings, ImageSettings, LayoutSettings, Names, ObjectSettings, Parameter, PlayerSettings,
|
||||
SettingsContext, TimingSettings,
|
||||
};
|
||||
|
||||
mod dialog;
|
||||
mod request;
|
||||
mod vn;
|
||||
|
||||
use request::Request;
|
||||
use vn::render_novel;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let Ok(path) = env::current_dir() else {
|
||||
@ -605,327 +600,6 @@ fn handle_relative_connection(
|
||||
.map(|parent| parse_config(&parent.with_extension("vng")).ok())
|
||||
.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 render_novel(config_map, file_paths.pk, &mut stream, start_level).is_err() {
|
||||
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