mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-23 17:07:12 +03:00
embed mode and reworked web build system (#592)
* root element id from Settings, respect element size * Plumb assets root via settings * adapt crates to new wasm api * more FileLoader cleanup * use tsc bin from node_modules * avoid spurious unlink errors GNU Make considers the src/*/wasm_pkg targets as intermediate build files and attempted to `rm` them. We can stop that my marking them as `.PRECIOUS` https://www.gnu.org/software/make/manual/html_node/Special-Targets.html * `open` doesn't work on Linux We could do something with xdg-open, but meh, not worth having platform dependent logic for this. * fix typo, clarify instructions * make server compatible with older python install on linux * revert change - we dont want to include music on web the leading "-" means exclude a subdir of an included dir. * better wrap of comments * fix misfire in copy/pasted comment * update docs
This commit is contained in:
parent
20de91bae7
commit
4f81f186af
@ -71,6 +71,11 @@ pub fn list_dir(dir: String) -> Vec<String> {
|
||||
|
||||
pub fn slurp_file<I: AsRef<str>>(path: I) -> Result<Vec<u8>> {
|
||||
let path = path.as_ref();
|
||||
debug!(
|
||||
"slurping file: {}, trimmed_path: {}",
|
||||
path,
|
||||
path.trim_start_matches("../data/system/")
|
||||
);
|
||||
|
||||
if let Some(raw) = SYSTEM_DATA.get_file(path.trim_start_matches("../data/system/")) {
|
||||
Ok(raw.contents().to_vec())
|
||||
|
@ -1,142 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script type="module">
|
||||
import { default as init } from './fifteen_min.js';
|
||||
|
||||
function isWebGL1Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function isWebGL2Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl2');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function prettyPrintBytes(bytes) {
|
||||
if (bytes < 1024 ** 2) {
|
||||
return Math.round(bytes / 1024) + " KB";
|
||||
}
|
||||
return Math.round(bytes / 1024 ** 2) + " MB";
|
||||
}
|
||||
|
||||
function main() {
|
||||
let webGL1Supported = isWebGL1Supported();
|
||||
let webGL2Supported = isWebGL2Supported();
|
||||
console.log("supports WebGL 1.0: " + webGL1Supported + ", WebGL 2.0: " + webGL2Supported);
|
||||
if (webGL1Supported || webGL2Supported) {
|
||||
fetchWithProgress();
|
||||
} else {
|
||||
showUnsupported();
|
||||
}
|
||||
}
|
||||
|
||||
function setElementVisibility(elementId, isVisible) {
|
||||
let el = document.getElementById(elementId);
|
||||
if (!el) {
|
||||
console.error("element missing: ", elementId);
|
||||
}
|
||||
if (isVisible) {
|
||||
el.style.display = "block";
|
||||
} else {
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function showUnsupported() {
|
||||
setElementVisibility('progress', false);
|
||||
setElementVisibility('unsupported', true);
|
||||
document.getElementById('unsupported-proceed-btn').onclick = function() {
|
||||
fetchWithProgress();
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWithProgress() {
|
||||
setElementVisibility('progress', true);
|
||||
setElementVisibility('unsupported', false);
|
||||
const t0 = performance.now();
|
||||
console.log("Started loading WASM");
|
||||
let response = await fetch('./fifteen_min_bg.wasm');
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
const reader = response.body.getReader();
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
document.getElementById("progress-text").innerText = prettyPrintBytes(receivedLength) + " / " + prettyPrintBytes(contentLength);
|
||||
document.getElementById("progress-bar").style.width = (100.0 * receivedLength / contentLength) + "%";
|
||||
}
|
||||
document.getElementById("progress-text").innerText = "Loaded " + prettyPrintBytes(contentLength) + ", now initializing WASM module";
|
||||
let blob = new Blob(chunks);
|
||||
let buffer = await blob.arrayBuffer();
|
||||
const t1 = performance.now();
|
||||
console.log(`It took ${t1 - t0} ms to download WASM, now initializing it`);
|
||||
await init(buffer);
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
#loading {
|
||||
background-color: #94C84A;
|
||||
padding: 40px;
|
||||
color: black;
|
||||
font-family: arial;
|
||||
border: solid black 3px;
|
||||
border-radius: 4px;
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
#progress-bar {
|
||||
/* complementary to #loading:background-color */
|
||||
background-color: #FF5733;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#loading h1 {
|
||||
text-align: center;
|
||||
}
|
||||
#unsupported-proceed-btn {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="widgetry-canvas"><div id="loading">
|
||||
<h1>15-minute Neighborhood Explorer</h1>
|
||||
<div id="progress" style="display: none">
|
||||
<h2>Loading...</h2>
|
||||
<div style="width: 100%; background-color: white;">
|
||||
<div style="width: 1%; height: 30px;" id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-text"></div>
|
||||
<p>If you think something has broken, check your browser's developer console (Ctrl+Shift+I or similar)</p>
|
||||
<p>(Your browser must support WebGL and WebAssembly)</p>
|
||||
</div>
|
||||
<div id="unsupported" style="display: none;">
|
||||
<h2>😭 Looks like your browser doesn't support WebGL.</h2>
|
||||
|
||||
<button id="unsupported-proceed-btn" type="button">Load Anyway</button>
|
||||
<p><strong>This will surely fail unless you enable WebGL first.</strong></p>
|
||||
</div>
|
||||
</div></div>
|
||||
</body>
|
||||
<html>
|
@ -1 +0,0 @@
|
||||
../index.html
|
@ -1 +0,0 @@
|
||||
../../data/system/
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
wasm-pack build --dev --target web -- --no-default-features --features wasm
|
||||
cd pkg
|
||||
python3 -m http.server 8000
|
@ -1,3 +1,5 @@
|
||||
use widgetry::Settings;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
@ -8,21 +10,29 @@ mod viewer;
|
||||
type App = map_gui::SimpleApp<()>;
|
||||
|
||||
pub fn main() {
|
||||
widgetry::run(
|
||||
widgetry::Settings::new("15-minute neighborhoods").read_svg(Box::new(abstio::slurp_bytes)),
|
||||
|ctx| {
|
||||
map_gui::SimpleApp::new(ctx, map_gui::options::Options::default(), (), |ctx, app| {
|
||||
vec![viewer::Viewer::random_start(ctx, app)]
|
||||
})
|
||||
},
|
||||
);
|
||||
let settings = Settings::new("15-minute neighborhoods");
|
||||
run(settings);
|
||||
}
|
||||
|
||||
fn run(mut settings: Settings) {
|
||||
settings = settings.read_svg(Box::new(abstio::slurp_bytes));
|
||||
widgetry::run(settings, |ctx| {
|
||||
map_gui::SimpleApp::new(ctx, map_gui::options::Options::default(), (), |ctx, app| {
|
||||
vec![viewer::Viewer::random_start(ctx, app)]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run() {
|
||||
main();
|
||||
#[wasm_bindgen(js_name = "run")]
|
||||
pub fn run_wasm(root_dom_id: String, assets_base_url: String, assets_are_gzipped: bool) {
|
||||
let settings = Settings::new("15-minute neighborhoods")
|
||||
.root_dom_element_id(root_dom_id)
|
||||
.assets_base_url(assets_base_url)
|
||||
.assets_are_gzipped(assets_are_gzipped);
|
||||
|
||||
run(settings);
|
||||
}
|
||||
|
142
game/index.html
142
game/index.html
@ -1,142 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script type="module">
|
||||
import { default as init } from './game.js';
|
||||
|
||||
function isWebGL1Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function isWebGL2Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl2');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function prettyPrintBytes(bytes) {
|
||||
if (bytes < 1024 ** 2) {
|
||||
return Math.round(bytes / 1024) + " KB";
|
||||
}
|
||||
return Math.round(bytes / 1024 ** 2) + " MB";
|
||||
}
|
||||
|
||||
function main() {
|
||||
let webGL1Supported = isWebGL1Supported();
|
||||
let webGL2Supported = isWebGL2Supported();
|
||||
console.log("supports WebGL 1.0: " + webGL1Supported + ", WebGL 2.0: " + webGL2Supported);
|
||||
if (webGL1Supported || webGL2Supported) {
|
||||
fetchWithProgress();
|
||||
} else {
|
||||
showUnsupported();
|
||||
}
|
||||
}
|
||||
|
||||
function setElementVisibility(elementId, isVisible) {
|
||||
let el = document.getElementById(elementId);
|
||||
if (!el) {
|
||||
console.error("element missing: ", elementId);
|
||||
}
|
||||
if (isVisible) {
|
||||
el.style.display = "block";
|
||||
} else {
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function showUnsupported() {
|
||||
setElementVisibility('progress', false);
|
||||
setElementVisibility('unsupported', true);
|
||||
document.getElementById('unsupported-proceed-btn').onclick = function() {
|
||||
fetchWithProgress();
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWithProgress() {
|
||||
setElementVisibility('progress', true);
|
||||
setElementVisibility('unsupported', false);
|
||||
const t0 = performance.now();
|
||||
console.log("Started loading WASM");
|
||||
let response = await fetch('./game_bg.wasm');
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
const reader = response.body.getReader();
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
document.getElementById("progress-text").innerText = prettyPrintBytes(receivedLength) + " / " + prettyPrintBytes(contentLength);
|
||||
document.getElementById("progress-bar").style.width = (100.0 * receivedLength / contentLength) + "%";
|
||||
}
|
||||
document.getElementById("progress-text").innerText = "Loaded " + prettyPrintBytes(contentLength) + ", now initializing WASM module";
|
||||
let blob = new Blob(chunks);
|
||||
let buffer = await blob.arrayBuffer();
|
||||
const t1 = performance.now();
|
||||
console.log(`It took ${t1 - t0} ms to download WASM, now initializing it`);
|
||||
await init(buffer);
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
#loading {
|
||||
background-color: #94C84A;
|
||||
padding: 40px;
|
||||
color: black;
|
||||
font-family: arial;
|
||||
border: solid black 3px;
|
||||
border-radius: 4px;
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
#progress-bar {
|
||||
/* complementary to #loading:background-color */
|
||||
background-color: #FF5733;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#loading h1 {
|
||||
text-align: center;
|
||||
}
|
||||
#unsupported-proceed-btn {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="widgetry-canvas"><div id="loading">
|
||||
<h1>A/B Street</h1>
|
||||
<div id="progress" style="display: none">
|
||||
<h2>Loading...</h2>
|
||||
<div style="width: 100%; background-color: white;">
|
||||
<div style="width: 1%; height: 30px;" id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-text"></div>
|
||||
<p>If you think something has broken, check your browser's developer console (Ctrl+Shift+I or similar)</p>
|
||||
<p>(Your browser must support WebGL and WebAssembly)</p>
|
||||
</div>
|
||||
<div id="unsupported" style="display: none;">
|
||||
<h2>😭 Looks like your browser doesn't support WebGL.</h2>
|
||||
|
||||
<button id="unsupported-proceed-btn" type="button">Load Anyway</button>
|
||||
<p><strong>This will surely fail unless you enable WebGL first.</strong></p>
|
||||
</div>
|
||||
</div></div>
|
||||
</body>
|
||||
<html>
|
@ -1 +0,0 @@
|
||||
../../data/system/assets/pregame/favicon.ico
|
@ -1 +0,0 @@
|
||||
../index.html
|
@ -1 +0,0 @@
|
||||
../prefetch.html
|
@ -1 +0,0 @@
|
||||
../../data/system/
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
wasm-pack build --dev --target web -- --no-default-features --features wasm
|
||||
cd pkg
|
||||
python3 -m http.server 8000
|
@ -10,7 +10,7 @@ use map_gui::options::Options;
|
||||
use map_gui::tools::URLManager;
|
||||
use map_model::Map;
|
||||
use sim::{Sim, SimFlags};
|
||||
use widgetry::{EventCtx, State, Transition};
|
||||
use widgetry::{EventCtx, Settings, State, Transition};
|
||||
|
||||
use crate::app::{App, Flags};
|
||||
use crate::common::jump_to_time_upon_startup;
|
||||
@ -29,6 +29,18 @@ mod pregame;
|
||||
mod sandbox;
|
||||
|
||||
pub fn main() {
|
||||
let settings = Settings::new("A/B Street");
|
||||
run(settings);
|
||||
}
|
||||
|
||||
fn run(mut settings: Settings) {
|
||||
settings = settings
|
||||
.read_svg(Box::new(abstio::slurp_bytes))
|
||||
.window_icon(abstio::path("system/assets/pregame/icon.png"))
|
||||
.loading_tips(map_gui::tools::loading_tips())
|
||||
// This is approximately how much the 3 top panels in sandbox mode require.
|
||||
.require_minimum_width(1500.0);
|
||||
|
||||
let mut args = CmdArgs::new();
|
||||
if args.enabled("--prebake") {
|
||||
challenges::prebake::prebake_all();
|
||||
@ -42,12 +54,7 @@ pub fn main() {
|
||||
let mut opts = Options::default();
|
||||
opts.toggle_day_night_colors = true;
|
||||
opts.update_from_args(&mut args);
|
||||
let mut settings = widgetry::Settings::new("A/B Street")
|
||||
.read_svg(Box::new(abstio::slurp_bytes))
|
||||
.window_icon(abstio::path("system/assets/pregame/icon.png"))
|
||||
.loading_tips(map_gui::tools::loading_tips())
|
||||
// This is approximately how much the 3 top panels in sandbox mode require.
|
||||
.require_minimum_width(1500.0);
|
||||
|
||||
if args.enabled("--dump_raw_events") {
|
||||
settings = settings.dump_raw_events();
|
||||
}
|
||||
@ -329,7 +336,12 @@ fn finish_app_setup(
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run() {
|
||||
main();
|
||||
#[wasm_bindgen(js_name = "run")]
|
||||
pub fn run_wasm(root_dom_id: String, assets_base_url: String, assets_are_gzipped: bool) {
|
||||
let settings = Settings::new("A/B Street")
|
||||
.root_dom_element_id(root_dom_id)
|
||||
.assets_base_url(assets_base_url)
|
||||
.assets_are_gzipped(assets_are_gzipped);
|
||||
|
||||
run(settings);
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ impl<A: AppLike + 'static> State<A> for MapAlreadyLoaded<A> {
|
||||
mod native_loader {
|
||||
use super::*;
|
||||
|
||||
// This loads a JSON or bincoded file, then deserializes it
|
||||
/// Loads a JSON or bincoded file, then deserializes it
|
||||
pub struct FileLoader<A: AppLike, T> {
|
||||
path: String,
|
||||
// Wrapped in an Option just to make calling from event() work. Technically this is unsafe
|
||||
@ -142,7 +142,9 @@ mod native_loader {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Ideally merge with FileLoader
|
||||
/// Loads a file without deserializing it.
|
||||
///
|
||||
/// TODO Ideally merge with FileLoader
|
||||
pub struct RawFileLoader<A: AppLike> {
|
||||
path: String,
|
||||
// Wrapped in an Option just to make calling from event() work. Technically this is unsafe
|
||||
@ -191,9 +193,11 @@ mod wasm_loader {
|
||||
|
||||
use super::*;
|
||||
|
||||
// Instead of blockingly reading a file within ctx.loading_screen, on the web have to
|
||||
// asynchronously make an HTTP request and keep "polling" for completion in a way that's
|
||||
// compatible with winit's event loop.
|
||||
/// Loads a JSON or bincoded file, then deserializes it
|
||||
///
|
||||
/// Instead of blockingly reading a file within ctx.loading_screen, on the web have to
|
||||
/// asynchronously make an HTTP request and keep "polling" for completion in a way that's
|
||||
/// compatible with winit's event loop.
|
||||
pub struct FileLoader<A: AppLike, T> {
|
||||
response: oneshot::Receiver<Result<Vec<u8>>>,
|
||||
on_load:
|
||||
@ -209,22 +213,18 @@ mod wasm_loader {
|
||||
path: String,
|
||||
on_load: Box<dyn FnOnce(&mut EventCtx, &mut A, &mut Timer, Result<T>) -> Transition<A>>,
|
||||
) -> Box<dyn State<A>> {
|
||||
// The current URL is of the index.html page. We can find the data directory relative
|
||||
// to that.
|
||||
let base_url = get_base_url().unwrap();
|
||||
let file_path = path.strip_prefix(&abstio::path("")).unwrap();
|
||||
let base_url = ctx
|
||||
.prerender
|
||||
.assets_base_url()
|
||||
.expect("assets_base_url must be specified for wasm builds via `Settings`");
|
||||
|
||||
// Note that files are gzipped on S3 and other deployments. When running locally, we
|
||||
// just symlink the data/ directory, where files aren't compressed.
|
||||
let url =
|
||||
if base_url.contains("http://0.0.0.0") || base_url.contains("http://localhost") {
|
||||
format!("{}/{}", base_url, file_path)
|
||||
} else if base_url.contains("abstreet.s3-website") {
|
||||
// The directory structure on S3 is a little weird -- the base directory has
|
||||
// data/ alongside game/, fifteen_min/, etc.
|
||||
format!("{}/../data/{}.gz", base_url, file_path)
|
||||
} else {
|
||||
format!("{}/{}.gz", base_url, file_path)
|
||||
};
|
||||
let url = if ctx.prerender.assets_are_gzipped() {
|
||||
format!("{}/{}.gz", base_url, path)
|
||||
} else {
|
||||
format!("{}/{}", base_url, path)
|
||||
};
|
||||
|
||||
// Make the HTTP request nonblockingly. When the response is received, send it through
|
||||
// the channel.
|
||||
@ -312,8 +312,10 @@ mod wasm_loader {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This is a horrible copy of FileLoader. Make the serde FileLoader just build on top of
|
||||
// this one!!!
|
||||
/// This loads a file without deserializing it.
|
||||
///
|
||||
/// TODO This is a horrible copy of FileLoader. Make the serde FileLoader just build on top of
|
||||
/// this one!!!
|
||||
pub struct RawFileLoader<A: AppLike> {
|
||||
response: oneshot::Receiver<Result<Vec<u8>>>,
|
||||
on_load: Option<Box<dyn FnOnce(&mut EventCtx, &mut A, Result<Vec<u8>>) -> Transition<A>>>,
|
||||
@ -328,22 +330,18 @@ mod wasm_loader {
|
||||
path: String,
|
||||
on_load: Box<dyn FnOnce(&mut EventCtx, &mut A, Result<Vec<u8>>) -> Transition<A>>,
|
||||
) -> Box<dyn State<A>> {
|
||||
// The current URL is of the index.html page. We can find the data directory relative
|
||||
// to that.
|
||||
let base_url = get_base_url().unwrap();
|
||||
let file_path = path.strip_prefix(&abstio::path("")).unwrap();
|
||||
let base_url = ctx
|
||||
.prerender
|
||||
.assets_base_url()
|
||||
.expect("assets_base_url must be specified for wasm builds via `Settings`");
|
||||
|
||||
// Note that files are gzipped on S3 and other deployments. When running locally, we
|
||||
// just symlink the data/ directory, where files aren't compressed.
|
||||
let url =
|
||||
if base_url.contains("http://0.0.0.0") || base_url.contains("http://localhost") {
|
||||
format!("{}/{}", base_url, file_path)
|
||||
} else if base_url.contains("abstreet.s3-website") {
|
||||
// The directory structure on S3 is a little weird -- the base directory has
|
||||
// data/ alongside game/, fifteen_min/, etc.
|
||||
format!("{}/../data/{}.gz", base_url, file_path)
|
||||
} else {
|
||||
format!("{}/{}.gz", base_url, file_path)
|
||||
};
|
||||
let url = if ctx.prerender.assets_are_gzipped() {
|
||||
format!("{}/{}.gz", base_url, path)
|
||||
} else {
|
||||
format!("{}/{}", base_url, path)
|
||||
};
|
||||
|
||||
// Make the HTTP request nonblockingly. When the response is received, send it through
|
||||
// the channel.
|
||||
@ -424,26 +422,6 @@ mod wasm_loader {
|
||||
self.panel.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the base URL where the game is running, excluding query parameters and the
|
||||
/// implicit index.html that might be there.
|
||||
fn get_base_url() -> Result<String> {
|
||||
let window = web_sys::window().ok_or(anyhow!("no window?"))?;
|
||||
let url = window.location().href().map_err(|err| {
|
||||
anyhow!(err
|
||||
.as_string()
|
||||
.unwrap_or("window.location.href failed".to_string()))
|
||||
})?;
|
||||
// Consider using a proper url parsing crate. This works fine for now, though.
|
||||
let url = url.split("?").next().ok_or(anyhow!("empty URL?"))?;
|
||||
Ok(url
|
||||
.trim_end_matches("index.html")
|
||||
// TODO This is brittle; we should strip off the trailing filename no matter what it
|
||||
// is.
|
||||
.trim_end_matches("prefetch.html")
|
||||
.trim_end_matches("/")
|
||||
.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FutureLoader<A, T>
|
||||
|
@ -1,142 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script type="module">
|
||||
import { default as init } from './osm_viewer.js';
|
||||
|
||||
function isWebGL1Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function isWebGL2Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl2');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function prettyPrintBytes(bytes) {
|
||||
if (bytes < 1024 ** 2) {
|
||||
return Math.round(bytes / 1024) + " KB";
|
||||
}
|
||||
return Math.round(bytes / 1024 ** 2) + " MB";
|
||||
}
|
||||
|
||||
function main() {
|
||||
let webGL1Supported = isWebGL1Supported();
|
||||
let webGL2Supported = isWebGL2Supported();
|
||||
console.log("supports WebGL 1.0: " + webGL1Supported + ", WebGL 2.0: " + webGL2Supported);
|
||||
if (webGL1Supported || webGL2Supported) {
|
||||
fetchWithProgress();
|
||||
} else {
|
||||
showUnsupported();
|
||||
}
|
||||
}
|
||||
|
||||
function setElementVisibility(elementId, isVisible) {
|
||||
let el = document.getElementById(elementId);
|
||||
if (!el) {
|
||||
console.error("element missing: ", elementId);
|
||||
}
|
||||
if (isVisible) {
|
||||
el.style.display = "block";
|
||||
} else {
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function showUnsupported() {
|
||||
setElementVisibility('progress', false);
|
||||
setElementVisibility('unsupported', true);
|
||||
document.getElementById('unsupported-proceed-btn').onclick = function() {
|
||||
fetchWithProgress();
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWithProgress() {
|
||||
setElementVisibility('progress', true);
|
||||
setElementVisibility('unsupported', false);
|
||||
const t0 = performance.now();
|
||||
console.log("Started loading WASM");
|
||||
let response = await fetch('./osm_viewer_bg.wasm');
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
const reader = response.body.getReader();
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
document.getElementById("progress-text").innerText = prettyPrintBytes(receivedLength) + " / " + prettyPrintBytes(contentLength);
|
||||
document.getElementById("progress-bar").style.width = (100.0 * receivedLength / contentLength) + "%";
|
||||
}
|
||||
document.getElementById("progress-text").innerText = "Loaded " + prettyPrintBytes(contentLength) + ", now initializing WASM module";
|
||||
let blob = new Blob(chunks);
|
||||
let buffer = await blob.arrayBuffer();
|
||||
const t1 = performance.now();
|
||||
console.log(`It took ${t1 - t0} ms to download WASM, now initializing it`);
|
||||
await init(buffer);
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
#loading {
|
||||
background-color: #94C84A;
|
||||
padding: 40px;
|
||||
color: black;
|
||||
font-family: arial;
|
||||
border: solid black 3px;
|
||||
border-radius: 4px;
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
#progress-bar {
|
||||
/* complementary to #loading:background-color */
|
||||
background-color: #FF5733;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#loading h1 {
|
||||
text-align: center;
|
||||
}
|
||||
#unsupported-proceed-btn {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="widgetry-canvas"><div id="loading">
|
||||
<h1>OpenStreetMap Viewer</h1>
|
||||
<div id="progress" style="display: none">
|
||||
<h2>Loading...</h2>
|
||||
<div style="width: 100%; background-color: white;">
|
||||
<div style="width: 1%; height: 30px;" id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-text"></div>
|
||||
<p>If you think something has broken, check your browser's developer console (Ctrl+Shift+I or similar)</p>
|
||||
<p>(Your browser must support WebGL and WebAssembly)</p>
|
||||
</div>
|
||||
<div id="unsupported" style="display: none;">
|
||||
<h2>😭 Looks like your browser doesn't support WebGL.</h2>
|
||||
|
||||
<button id="unsupported-proceed-btn" type="button">Load Anyway</button>
|
||||
<p><strong>This will surely fail unless you enable WebGL first.</strong></p>
|
||||
</div>
|
||||
</div></div>
|
||||
</body>
|
||||
<html>
|
@ -1 +0,0 @@
|
||||
../index.html
|
@ -1 +0,0 @@
|
||||
../../data/system/
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
wasm-pack build --dev --target web -- --no-default-features --features wasm
|
||||
cd pkg
|
||||
python3 -m http.server 8000
|
@ -3,22 +3,33 @@ extern crate log;
|
||||
|
||||
mod viewer;
|
||||
|
||||
use widgetry::Settings;
|
||||
|
||||
pub fn main() {
|
||||
widgetry::run(
|
||||
widgetry::Settings::new("OpenStreetMap viewer").read_svg(Box::new(abstio::slurp_bytes)),
|
||||
|ctx| {
|
||||
map_gui::SimpleApp::new(ctx, map_gui::options::Options::default(), (), |ctx, app| {
|
||||
vec![viewer::Viewer::new(ctx, app)]
|
||||
})
|
||||
},
|
||||
);
|
||||
let settings = Settings::new("OpenStreetMap viewer").read_svg(Box::new(abstio::slurp_bytes));
|
||||
run(settings)
|
||||
}
|
||||
|
||||
pub fn run(mut settings: Settings) {
|
||||
settings = settings.read_svg(Box::new(abstio::slurp_bytes));
|
||||
|
||||
widgetry::run(settings, |ctx| {
|
||||
map_gui::SimpleApp::new(ctx, map_gui::options::Options::default(), (), |ctx, app| {
|
||||
vec![viewer::Viewer::new(ctx, app)]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run() {
|
||||
main();
|
||||
#[wasm_bindgen(js_name = "run")]
|
||||
pub fn run_wasm(root_dom_id: String, assets_base_url: String, assets_are_gzipped: bool) {
|
||||
let settings = Settings::new("OpenStreetMap viewer")
|
||||
.root_dom_element_id(root_dom_id)
|
||||
.assets_base_url(assets_base_url)
|
||||
.assets_are_gzipped(assets_are_gzipped);
|
||||
|
||||
run(settings);
|
||||
}
|
||||
|
@ -1,30 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
VERSION=dev
|
||||
# S3_ROOT=s3://mjk_asdf/abstreet
|
||||
S3_ROOT=s3://abstreet
|
||||
|
||||
set -e
|
||||
|
||||
# The parking mapper doesn't work on WASM yet, so don't include it
|
||||
for tool in game santa fifteen_min osm_viewer; do
|
||||
cd $tool
|
||||
wasm-pack build --release --target web -- --no-default-features --features wasm
|
||||
cd pkg
|
||||
# Temporarily remove the symlink to the data directory; it's uploaded separately by the updater tool
|
||||
rm -f system
|
||||
aws s3 sync . s3://abstreet/$VERSION/$tool
|
||||
# Undo that symlink hiding
|
||||
git checkout system
|
||||
cd ../..
|
||||
done
|
||||
cd web;
|
||||
make release
|
||||
aws s3 sync build/dist/ $S3_ROOT/$VERSION/
|
||||
|
||||
# Set the content type for .wasm files, to speed up how browsers load them
|
||||
aws s3 cp \
|
||||
s3://abstreet/$VERSION \
|
||||
s3://abstreet/$VERSION \
|
||||
$S3_ROOT/$VERSION \
|
||||
$S3_ROOT/$VERSION \
|
||||
--exclude '*' \
|
||||
--include '*.wasm' \
|
||||
--no-guess-mime-type \
|
||||
--content-type="application/wasm" \
|
||||
--metadata-directive="REPLACE" \
|
||||
--recursive
|
||||
|
||||
echo "Have the appropriate amount of fun: http://abstreet.s3-website.us-east-2.amazonaws.com/$VERSION"
|
||||
|
142
santa/index.html
142
santa/index.html
@ -1,142 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script type="module">
|
||||
import { default as init } from './santa.js';
|
||||
|
||||
function isWebGL1Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function isWebGL2Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl2');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function prettyPrintBytes(bytes) {
|
||||
if (bytes < 1024 ** 2) {
|
||||
return Math.round(bytes / 1024) + " KB";
|
||||
}
|
||||
return Math.round(bytes / 1024 ** 2) + " MB";
|
||||
}
|
||||
|
||||
function main() {
|
||||
let webGL1Supported = isWebGL1Supported();
|
||||
let webGL2Supported = isWebGL2Supported();
|
||||
console.log("supports WebGL 1.0: " + webGL1Supported + ", WebGL 2.0: " + webGL2Supported);
|
||||
if (webGL1Supported || webGL2Supported) {
|
||||
fetchWithProgress();
|
||||
} else {
|
||||
showUnsupported();
|
||||
}
|
||||
}
|
||||
|
||||
function setElementVisibility(elementId, isVisible) {
|
||||
let el = document.getElementById(elementId);
|
||||
if (!el) {
|
||||
console.error("element missing: ", elementId);
|
||||
}
|
||||
if (isVisible) {
|
||||
el.style.display = "block";
|
||||
} else {
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function showUnsupported() {
|
||||
setElementVisibility('progress', false);
|
||||
setElementVisibility('unsupported', true);
|
||||
document.getElementById('unsupported-proceed-btn').onclick = function() {
|
||||
fetchWithProgress();
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWithProgress() {
|
||||
setElementVisibility('progress', true);
|
||||
setElementVisibility('unsupported', false);
|
||||
const t0 = performance.now();
|
||||
console.log("Started loading WASM");
|
||||
let response = await fetch('./santa_bg.wasm');
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
const reader = response.body.getReader();
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
document.getElementById("progress-text").innerText = prettyPrintBytes(receivedLength) + " / " + prettyPrintBytes(contentLength);
|
||||
document.getElementById("progress-bar").style.width = (100.0 * receivedLength / contentLength) + "%";
|
||||
}
|
||||
document.getElementById("progress-text").innerText = "Loaded " + prettyPrintBytes(contentLength) + ", now initializing WASM module";
|
||||
let blob = new Blob(chunks);
|
||||
let buffer = await blob.arrayBuffer();
|
||||
const t1 = performance.now();
|
||||
console.log(`It took ${t1 - t0} ms to download WASM, now initializing it`);
|
||||
await init(buffer);
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
#loading {
|
||||
background-color: #94C84A;
|
||||
padding: 40px;
|
||||
color: black;
|
||||
font-family: arial;
|
||||
border: solid black 3px;
|
||||
border-radius: 4px;
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
#progress-bar {
|
||||
/* complementary to #loading:background-color */
|
||||
background-color: #FF5733;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#loading h1 {
|
||||
text-align: center;
|
||||
}
|
||||
#unsupported-proceed-btn {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="widgetry-canvas"><div id="loading">
|
||||
<h1>15-minute Santa</h1>
|
||||
<div id="progress" style="display: none">
|
||||
<h2>Loading...</h2>
|
||||
<div style="width: 100%; background-color: white;">
|
||||
<div style="width: 1%; height: 30px;" id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-text"></div>
|
||||
<p>If you think something has broken, check your browser's developer console (Ctrl+Shift+I or similar)</p>
|
||||
<p>(Your browser must support WebGL and WebAssembly)</p>
|
||||
</div>
|
||||
<div id="unsupported" style="display: none;">
|
||||
<h2>😭 Looks like your browser doesn't support WebGL.</h2>
|
||||
|
||||
<button id="unsupported-proceed-btn" type="button">Load Anyway</button>
|
||||
<p><strong>This will surely fail unless you enable WebGL first.</strong></p>
|
||||
</div>
|
||||
</div></div>
|
||||
</body>
|
||||
<html>
|
@ -1 +0,0 @@
|
||||
../index.html
|
@ -1 +0,0 @@
|
||||
../../data/system/
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
wasm-pack build --dev --target web -- --no-default-features --features wasm
|
||||
cd pkg
|
||||
python3 -m http.server 8000
|
@ -3,6 +3,8 @@ extern crate anyhow;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use widgetry::Settings;
|
||||
|
||||
mod after_level;
|
||||
mod animation;
|
||||
mod before_level;
|
||||
@ -21,33 +23,40 @@ type App = map_gui::SimpleApp<session::Session>;
|
||||
type Transition = widgetry::Transition<App>;
|
||||
|
||||
pub fn main() {
|
||||
widgetry::run(
|
||||
widgetry::Settings::new("15-minute Santa").read_svg(Box::new(abstio::slurp_bytes)),
|
||||
|ctx| {
|
||||
let mut opts = map_gui::options::Options::default();
|
||||
opts.color_scheme = map_gui::colors::ColorSchemeChoice::NightMode;
|
||||
let session = session::Session::load();
|
||||
session.save();
|
||||
let settings = Settings::new("15-minute Santa");
|
||||
run(settings);
|
||||
}
|
||||
|
||||
map_gui::SimpleApp::new(ctx, opts, session, |ctx, app| {
|
||||
if app.opts.dev {
|
||||
app.session.unlock_all();
|
||||
}
|
||||
app.session.music =
|
||||
music::Music::start(ctx, app.session.play_music, "jingle_bells");
|
||||
app.session.music.specify_volume(music::OUT_OF_GAME);
|
||||
fn run(mut settings: Settings) {
|
||||
settings = settings.read_svg(Box::new(abstio::slurp_bytes));
|
||||
widgetry::run(settings, |ctx| {
|
||||
let mut opts = map_gui::options::Options::default();
|
||||
opts.color_scheme = map_gui::colors::ColorSchemeChoice::NightMode;
|
||||
let session = session::Session::load();
|
||||
session.save();
|
||||
|
||||
vec![title::TitleScreen::new(ctx, app)]
|
||||
})
|
||||
},
|
||||
);
|
||||
map_gui::SimpleApp::new(ctx, opts, session, |ctx, app| {
|
||||
if app.opts.dev {
|
||||
app.session.unlock_all();
|
||||
}
|
||||
app.session.music = music::Music::start(ctx, app.session.play_music, "jingle_bells");
|
||||
app.session.music.specify_volume(music::OUT_OF_GAME);
|
||||
|
||||
vec![title::TitleScreen::new(ctx, app)]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run() {
|
||||
main();
|
||||
#[wasm_bindgen(js_name = "run")]
|
||||
pub fn run_wasm(root_dom_id: String, assets_base_url: String, assets_are_gzipped: bool) {
|
||||
let settings = Settings::new("15-minute Santa")
|
||||
.root_dom_element_id(root_dom_id)
|
||||
.assets_base_url(assets_base_url)
|
||||
.assets_are_gzipped(assets_are_gzipped);
|
||||
|
||||
run(settings);
|
||||
}
|
||||
|
2
web/.gitignore
vendored
Normal file
2
web/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
build
|
117
web/Makefile
Normal file
117
web/Makefile
Normal file
@ -0,0 +1,117 @@
|
||||
DEFAULT: dev
|
||||
|
||||
SERVER_PORT=8000
|
||||
REPO_ROOT:=$(shell git rev-parse --show-toplevel)
|
||||
|
||||
##
|
||||
# Section: Tasks (Phony targets)
|
||||
##
|
||||
|
||||
APPS=abstreet fifteen_min osm_viewer santa widgetry_demo
|
||||
|
||||
.PHONY: server clean dev release $(APPS)
|
||||
.PRECIOUS: src/%/wasm_pkg
|
||||
|
||||
TSC=npx tsc --lib dom,es2020 --module es2020 --target es6 --strict --noEmitOnError --outDir build
|
||||
|
||||
server:
|
||||
cd build/dist && python3 -m http.server $(SERVER_PORT)
|
||||
|
||||
clean:
|
||||
rm -fr build
|
||||
rm -fr src/*/wasm_pkg
|
||||
|
||||
# Build all of our apps
|
||||
build: $(APPS) build/dist/index.html
|
||||
|
||||
# Build all of our apps for distribution (optimized)
|
||||
release: export WASM_PACK_FLAGS=--release
|
||||
# note: `clean` is because we don't know if any cached wasm files were optimized
|
||||
# Once you've compiled the optimized wasm this way, you can skip `clean` and just run `make build`, since the remaining output is identical for dev vs release
|
||||
release: clean build
|
||||
|
||||
dev: export WASM_PACK_FLAGS=--dev
|
||||
dev: build build/dist/data
|
||||
|
||||
# Symlink in data for dev builds
|
||||
build/dist/data:
|
||||
ln -sf ../../../data build/dist/data
|
||||
|
||||
##
|
||||
# Section: Shared Deps and Templates
|
||||
##
|
||||
|
||||
build/dist/%.html: src/web_root/*.html
|
||||
# just copy everything... we could do something more nuanced
|
||||
cp -r src/web_root/* build/dist
|
||||
|
||||
# Produce the wasm package via our wasm-pack script
|
||||
# To skip optimization:
|
||||
# WASM_PACK_FLAGS="--dev" make
|
||||
src/%/wasm_pkg:
|
||||
bin/build-wasm $*
|
||||
|
||||
# simply copies over the wasm pkg we
|
||||
# built to be able to compile our typescript
|
||||
build/dist/%/wasm_pkg: src/%/wasm_pkg
|
||||
mkdir -p $(dir $@)
|
||||
cp -r "${<}" "${@}"
|
||||
|
||||
# Concatenate the target crate's js file with the generic widgetry loading
|
||||
# code.
|
||||
#
|
||||
# Alternatives and their downsides would be:
|
||||
# 1. import outside of pkg root
|
||||
# - pkg is less portable
|
||||
# 2. copy widgetry.js file into each package and update import path on build
|
||||
# - not really any less hacky, since we're still transforming at build
|
||||
# - an extra request for the client
|
||||
# 3. use proper bundler like webpack and system.js:
|
||||
# - They're all complex - conceptually and dependency-wise
|
||||
# - We can't use AMD modules because it doesn't support `import.meta`
|
||||
build/dist/%.bundle.js: build/widgetry.js build/%.js
|
||||
mkdir -p $(dir $@)
|
||||
bin/bundle-widgetry-js $^ > $@
|
||||
|
||||
build/%.js: src/%.ts
|
||||
$(TSC) $^
|
||||
|
||||
##
|
||||
# Section: Apps
|
||||
##
|
||||
|
||||
## A/BStreet
|
||||
|
||||
abstreet: build/dist/abstreet/wasm_pkg build/dist/abstreet/abstreet.bundle.js build/dist/abstreet build/dist/abstreet.html
|
||||
|
||||
# Unlike the other crates, we have an explicit rule to give the "game" js a
|
||||
# more meaningful name
|
||||
src/abstreet/wasm_pkg:
|
||||
bin/build-wasm game abstreet
|
||||
|
||||
build/dist/abstreet/abstreet.bundle.js: build/widgetry.js build/abstreet/abstreet.js
|
||||
|
||||
## Fifteen Minute Tool
|
||||
|
||||
fifteen_min: build/dist/fifteen_min/wasm_pkg build/dist/fifteen_min/fifteen_min.bundle.js build/dist/fifteen_min.html
|
||||
|
||||
build/dist/fifteen_min/fifteen_min.bundle.js: build/widgetry.js build/fifteen_min/fifteen_min.js
|
||||
|
||||
## OSM Viewer
|
||||
|
||||
osm_viewer: build/dist/osm_viewer/wasm_pkg build/dist/osm_viewer/osm_viewer.bundle.js build/dist/osm_viewer.html
|
||||
|
||||
build/dist/osm_viewer/osm_viewer.bundle.js: build/widgetry.js build/osm_viewer/osm_viewer.js
|
||||
|
||||
## Widgetry Demo
|
||||
|
||||
widgetry_demo: build/dist/widgetry_demo/wasm_pkg build/dist/widgetry_demo/widgetry_demo.bundle.js build/dist/widgetry_demo.html
|
||||
|
||||
build/dist/widgetry_demo/widgetry_demo.bundle.js: build/widgetry.js build/widgetry_demo/widgetry_demo.js
|
||||
|
||||
## Santa
|
||||
|
||||
santa: build/dist/santa/wasm_pkg build/dist/santa/santa.bundle.js build/dist/santa.html
|
||||
|
||||
build/dist/santa/santa.bundle.js: build/widgetry.js build/santa/santa.js
|
||||
|
42
web/README.md
Normal file
42
web/README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Web Stuff
|
||||
|
||||
This is a collection of API's and build tools for packaging our various
|
||||
Widgetry apps as web applications.
|
||||
|
||||
## Goals
|
||||
|
||||
A web developer, who might not know anything about rust or wasm, should be able
|
||||
to use our packaged javascript libraries on their website with minimal
|
||||
customization or arcanery.
|
||||
|
||||
Users of their website should be able to interact with the widgetry app without
|
||||
it feeling weird or having to jump through hoops.
|
||||
|
||||
## Limitations
|
||||
|
||||
### JS feature: `import.meta`
|
||||
|
||||
To allow the application to live at any URL (rather than presupposing it lives
|
||||
at root, or whatever), we rely on `import.meta` which isn't supported on some
|
||||
browsers before 2018. See: https://caniuse.com/?search=import.meta
|
||||
|
||||
An alternative would be to require configuration, so the loader knows where to
|
||||
download it's "*_wasm.bg file".
|
||||
|
||||
### Browser Feature: WebGL
|
||||
|
||||
We prefer WebGL2, but now gracefully fall back to WebGL1. This should cover
|
||||
all common browsers since late 2014. https://caniuse.com/?search=webgl
|
||||
|
||||
## Examples
|
||||
|
||||
See [`src/web_root/*.js`](examples/) for code examples.
|
||||
|
||||
You can build and see the examples in your webbrowser with:
|
||||
|
||||
```
|
||||
// install typescript build dependency
|
||||
npm install
|
||||
make build
|
||||
make server
|
||||
```
|
36
web/bin/build-wasm
Executable file
36
web/bin/build-wasm
Executable file
@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
set -euf -o pipefail
|
||||
|
||||
PROJECT_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
BIN_NAME=$0
|
||||
function usage {
|
||||
cat <<EOS
|
||||
Usage:
|
||||
$BIN_NAME <crate name> <js pkg name if different from crate name>
|
||||
Example:
|
||||
$BIN_NAME widgetry_demo
|
||||
$BIN_NAME game abstreet
|
||||
EOS
|
||||
}
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
usage
|
||||
exit1
|
||||
fi
|
||||
|
||||
CRATE_NAME="${1}"
|
||||
|
||||
shift
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
JS_NAME="${CRATE_NAME}"
|
||||
else
|
||||
JS_NAME="${1}"
|
||||
fi
|
||||
|
||||
# Default to a dev build
|
||||
WASM_PACK_FLAGS="${WASM_PACK_FLAGS:-"--dev"}"
|
||||
|
||||
cd $PROJECT_ROOT/$CRATE_NAME
|
||||
wasm-pack build $WASM_PACK_FLAGS --target web --out-dir "${PROJECT_ROOT}/web/src/${JS_NAME}/wasm_pkg" -- --no-default-features --features wasm
|
6
web/bin/bundle-widgetry-js
Executable file
6
web/bin/bundle-widgetry-js
Executable file
@ -0,0 +1,6 @@
|
||||
for file in "$@"
|
||||
do
|
||||
# concatenate all input files, removing any reference to importing widgetry
|
||||
# assuming it's one of the input files being concatenated
|
||||
cat $file | grep -v "from '../widgetry.js'" | grep -v 'from "../widgetry.js"'
|
||||
done
|
55
web/package-lock.json
generated
Normal file
55
web/package-lock.json
generated
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "widgetry-apps",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "widgetry-apps",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"prettier": "^2.2.1",
|
||||
"typescript": "^4.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
|
||||
"integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
||||
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"prettier": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
|
||||
"integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
||||
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
12
web/package.json
Normal file
12
web/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"author": "Michael Kirk<michael.code@endoftheworl.de",
|
||||
"name": "widgetry-apps",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"fmt": "prettier --write src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.2.1",
|
||||
"typescript": "^4.2.3"
|
||||
}
|
||||
}
|
20
web/src/abstreet/abstreet.ts
Normal file
20
web/src/abstreet/abstreet.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as wasm_pkg from "./wasm_pkg/game.js";
|
||||
let wasm_bg_path = "wasm_pkg/game_bg.wasm";
|
||||
|
||||
import { InitInput, modRoot, WidgetryApp } from "../widgetry.js";
|
||||
|
||||
export class ABStreet extends WidgetryApp<wasm_pkg.InitOutput> {
|
||||
initializeWasm(
|
||||
module_or_path?: InitInput | Promise<InitInput>
|
||||
): Promise<wasm_pkg.InitOutput> {
|
||||
return wasm_pkg.default(module_or_path);
|
||||
}
|
||||
|
||||
run(rootDomId: string, assetsBaseURL: string, assetsAreGzipped: boolean) {
|
||||
wasm_pkg.run(rootDomId, assetsBaseURL, assetsAreGzipped);
|
||||
}
|
||||
|
||||
wasmURL(): string {
|
||||
return modRoot(import.meta.url) + wasm_bg_path;
|
||||
}
|
||||
}
|
20
web/src/fifteen_min/fifteen_min.ts
Normal file
20
web/src/fifteen_min/fifteen_min.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as wasm_pkg from "./wasm_pkg/fifteen_min.js";
|
||||
let wasm_bg_path = "wasm_pkg/fifteen_min_bg.wasm";
|
||||
|
||||
import { InitInput, modRoot, WidgetryApp } from "../widgetry.js";
|
||||
|
||||
export class FifteenMinute extends WidgetryApp<wasm_pkg.InitOutput> {
|
||||
initializeWasm(
|
||||
module_or_path?: InitInput | Promise<InitInput>
|
||||
): Promise<wasm_pkg.InitOutput> {
|
||||
return wasm_pkg.default(module_or_path);
|
||||
}
|
||||
|
||||
run(rootDomId: string, assetsBaseURL: string, assetsAreGzipped: boolean) {
|
||||
wasm_pkg.run(rootDomId, assetsBaseURL, assetsAreGzipped);
|
||||
}
|
||||
|
||||
wasmURL(): string {
|
||||
return modRoot(import.meta.url) + wasm_bg_path;
|
||||
}
|
||||
}
|
20
web/src/osm_viewer/osm_viewer.ts
Normal file
20
web/src/osm_viewer/osm_viewer.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as wasm_pkg from "./wasm_pkg/osm_viewer.js";
|
||||
let wasm_bg_path = "wasm_pkg/osm_viewer_bg.wasm";
|
||||
|
||||
import { InitInput, modRoot, WidgetryApp } from "../widgetry.js";
|
||||
|
||||
export class OSMViewer extends WidgetryApp<wasm_pkg.InitOutput> {
|
||||
initializeWasm(
|
||||
module_or_path?: InitInput | Promise<InitInput>
|
||||
): Promise<wasm_pkg.InitOutput> {
|
||||
return wasm_pkg.default(module_or_path);
|
||||
}
|
||||
|
||||
run(rootDomId: string, assetsBaseURL: string, assetsAreGzipped: boolean) {
|
||||
wasm_pkg.run(rootDomId, assetsBaseURL, assetsAreGzipped);
|
||||
}
|
||||
|
||||
wasmURL(): string {
|
||||
return modRoot(import.meta.url) + wasm_bg_path;
|
||||
}
|
||||
}
|
20
web/src/santa/santa.ts
Normal file
20
web/src/santa/santa.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as wasm_pkg from "./wasm_pkg/santa.js";
|
||||
let wasm_bg_path = "wasm_pkg/santa_bg.wasm";
|
||||
|
||||
import { InitInput, modRoot, WidgetryApp } from "../widgetry.js";
|
||||
|
||||
export class Santa extends WidgetryApp<wasm_pkg.InitOutput> {
|
||||
initializeWasm(
|
||||
module_or_path?: InitInput | Promise<InitInput>
|
||||
): Promise<wasm_pkg.InitOutput> {
|
||||
return wasm_pkg.default(module_or_path);
|
||||
}
|
||||
|
||||
run(rootDomId: string, assetsBaseURL: string, assetsAreGzipped: boolean) {
|
||||
wasm_pkg.run(rootDomId, assetsBaseURL, assetsAreGzipped);
|
||||
}
|
||||
|
||||
wasmURL(): string {
|
||||
return modRoot(import.meta.url) + wasm_bg_path;
|
||||
}
|
||||
}
|
15
web/src/web_root/abstreet.css
Normal file
15
web/src/web_root/abstreet.css
Normal file
@ -0,0 +1,15 @@
|
||||
body {
|
||||
font-family: Arial;
|
||||
background: white;
|
||||
color: #4a4a4a;
|
||||
}
|
||||
|
||||
.full-screen-widgetry-app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.widgetry-app .preloading {
|
||||
padding: 16px;
|
||||
color: white;
|
||||
}
|
23
web/src/web_root/abstreet.html
Normal file
23
web/src/web_root/abstreet.html
Normal file
@ -0,0 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="abstreet.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="module">
|
||||
import { ABStreet } from "./abstreet/abstreet.bundle.js";
|
||||
let app = new ABStreet("app");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
app.loadAndStart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="widgetry-app full-screen-widgetry-app" id="app">
|
||||
<div class="preloading">
|
||||
<h1>A/B Street</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
167
web/src/web_root/blog_root/2021/03/31/example-post.html
Normal file
167
web/src/web_root/blog_root/2021/03/31/example-post.html
Normal file
@ -0,0 +1,167 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../../../abstreet.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="module">
|
||||
import { ABStreet } from "../../../../abstreet/abstreet.bundle.js";
|
||||
let app = new ABStreet("app");
|
||||
|
||||
// Path is relative to this page's URL.
|
||||
//
|
||||
// To be intuitive, it should be the same kind of relative path you'd use,
|
||||
// for an <img src=...> tag, for example.
|
||||
app.setAssetsBaseURL("../../../../data");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
app.loadAndStart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- custom styles for my example blog -->
|
||||
<style type="text/css">
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.widgetry-app {
|
||||
height: 400px;
|
||||
border:
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 100%;
|
||||
background: #5B2333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
#navigation ul li {
|
||||
display: inline-block;
|
||||
margin-left: 30px;
|
||||
margin-right: 30px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#navigation ul li a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
padding: 16px;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.media-box img {
|
||||
max-width:100%;
|
||||
max-height:100%;
|
||||
}
|
||||
|
||||
.media-box {
|
||||
background: #5B2333;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.media-box-caption {
|
||||
color: white;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="header">
|
||||
<h1>My Blog</h1>
|
||||
<div id="navigation">
|
||||
<ul>
|
||||
<!-- some filler-content links -->
|
||||
<li><a href="https://hypertextbook.com/chaos/">Some Link</a></li>
|
||||
<li><a href="https://hypertextbook.com/chaos/">Another Link</a></li>
|
||||
<li><a href="https://hypertextbook.com/chaos/">A Link to the Past</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main-content">
|
||||
<h2>Another Day, Another Post</h2>
|
||||
<strong>By Jane Jacobs</strong>
|
||||
<p>
|
||||
This page is a bunch of fake content as example of how you might go about
|
||||
embedding a Widgetry app into a web page, like a blog post.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer ornare
|
||||
tempus est sed placerat. In venenatis faucibus finibus. Vivamus vel
|
||||
fringilla velit, id sagittis leo. Fusce at ligula vitae risus rutrum
|
||||
blandit. Aenean non leo ligula. Orci varius natoque penatibus et magnis dis
|
||||
parturient montes, nascetur ridiculus mus. Ut eget pharetra arcu, id congue
|
||||
mi. Donec tempor dignissim hendrerit. Praesent molestie arcu ligula, vel
|
||||
ornare lectus blandit sit amet. In hac habitasse platea dictumst.
|
||||
</p>
|
||||
<div class="media-box">
|
||||
<div class="widgetry-app" id="app">
|
||||
<div class="preloading">
|
||||
<h1>A/B Street</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-box-caption">
|
||||
As the above simulation shows, the results are very true.
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Sed luctus luctus volutpat. Morbi urna turpis, commodo nec ex ac, vulputate
|
||||
dignissim ante. Nunc ac nisi consectetur, sodales sem in, laoreet lorem.
|
||||
Cras id mauris eu leo dictum elementum vitae sed dolor. Sed non ex
|
||||
faucibus, sollicitudin odio sed, sollicitudin risus. Phasellus gravida
|
||||
velit consectetur odio malesuada venenatis a quis turpis. Praesent eros mi,
|
||||
molestie rhoncus sem eget, pellentesque suscipit velit. Sed efficitur
|
||||
rhoncus justo, at malesuada dui hendrerit sed. Aenean ac placerat elit.
|
||||
</p>
|
||||
<p>
|
||||
Etiam ullamcorper leo quis consequat posuere. Proin sem felis, bibendum ac
|
||||
porttitor nec, tristique sit amet nibh. Proin ac nibh a orci porta pharetra
|
||||
gravida in nisl. In mollis hendrerit lacus, a commodo tortor fermentum
|
||||
eget. Vivamus metus felis, sagittis a fermentum sit amet, mollis quis
|
||||
augue. Nullam eros sem, accumsan non convallis non, condimentum id quam.
|
||||
Fusce efficitur in nulla sit amet ornare. Aliquam id est tempor, consequat
|
||||
risus ornare, porttitor urna.
|
||||
</p>
|
||||
<div class="media-box">
|
||||
<img src="../../../static_assets/snow_bike.jpg">
|
||||
<div class="media-box-caption">
|
||||
It was a snowy, bikey day.
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Nullam dignissim eu eros ut pharetra. Phasellus euismod vitae velit non
|
||||
ornare. Nulla turpis tortor, interdum at purus quis, vulputate congue
|
||||
velit. Morbi a elit ac magna sagittis molestie. Maecenas ut ipsum faucibus,
|
||||
aliquet nulla ac, commodo risus. Donec pretium et lorem non laoreet. Morbi
|
||||
eu aliquet ex. Aliquam faucibus nibh quis ligula sollicitudin, quis maximus
|
||||
urna cursus.
|
||||
</p>
|
||||
<p>
|
||||
Suspendisse a nibh diam. Mauris nec ante in felis ullamcorper blandit
|
||||
tincidunt pulvinar enim. Maecenas tempus ante at convallis scelerisque.
|
||||
Vivamus hendrerit dui dui, et mattis libero pulvinar ut. Nullam facilisis
|
||||
nisl eget viverra consectetur. Quisque et elit at urna iaculis venenatis.
|
||||
Vestibulum dapibus dignissim sapien porta tempus. Nam commodo tempus dui in
|
||||
imperdiet. Vestibulum consequat nisl sed leo feugiat, sed convallis sem
|
||||
lobortis. In eget purus mi. Morbi tincidunt, nibh posuere iaculis blandit,
|
||||
erat ex malesuada magna, et posuere lacus eros id risus. Fusce non orci
|
||||
molestie, sagittis sem eu, vulputate tellus.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
BIN
web/src/web_root/blog_root/static_assets/snow_bike.jpg
Normal file
BIN
web/src/web_root/blog_root/static_assets/snow_bike.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 166 KiB |
22
web/src/web_root/fifteen_min.html
Normal file
22
web/src/web_root/fifteen_min.html
Normal file
@ -0,0 +1,22 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="abstreet.css">
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
import { FifteenMinute } from "./fifteen_min/fifteen_min.bundle.js";
|
||||
let app = new FifteenMinute("app");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
app.loadAndStart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="widgetry-app full-screen-widgetry-app" id="app">
|
||||
<div class="preloading">
|
||||
<h1>Fifteen Minute Neighborhood</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
20
web/src/web_root/index.html
Normal file
20
web/src/web_root/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="abstreet.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Apps</h1>
|
||||
<ul>
|
||||
<li><a href="abstreet.html">A/B Street</a></li>
|
||||
<li><a href="fifteen_min.html">Fifteen Minute Neighborhood Tool</a></li>
|
||||
<li><a href="osm_viewer.html">OSM Viewer</a></li>
|
||||
<li><a href="widgetry_demo.html">Widgetry Demo</a></li>
|
||||
<li><a href="santa.html">15-Minute Santa</a></li>
|
||||
</ul>
|
||||
|
||||
<h1>Examples</h1>
|
||||
<ul>
|
||||
<li><a href="blog_root/2021/03/31/example-post.html">An app embedded among other page content.</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
23
web/src/web_root/osm_viewer.html
Normal file
23
web/src/web_root/osm_viewer.html
Normal file
@ -0,0 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="abstreet.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="module">
|
||||
import { OSMViewer } from "./osm_viewer/osm_viewer.bundle.js";
|
||||
let app = new OSMViewer("app");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
app.loadAndStart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="widgetry-app full-screen-widgetry-app" id="app">
|
||||
<div class="preloading">
|
||||
<h1>OSM Viewer</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
24
web/src/web_root/santa.html
Normal file
24
web/src/web_root/santa.html
Normal file
@ -0,0 +1,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="abstreet.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="module">
|
||||
import { Santa } from "./santa/santa.bundle.js";
|
||||
|
||||
let app = new Santa("app");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
app.loadAndStart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="widgetry-app full-screen-widgetry-app" id="app">
|
||||
<div class="preloading">
|
||||
<h1>Fifteen Minute Santa</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
21
web/src/web_root/widgetry_demo.html
Normal file
21
web/src/web_root/widgetry_demo.html
Normal file
@ -0,0 +1,21 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="abstreet.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="module">
|
||||
import { WidgetryDemo } from "./widgetry_demo/widgetry_demo.bundle.js";
|
||||
let widgetryDemo = new WidgetryDemo("app");
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
widgetryDemo.loadAndStart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="widgetry-app full-screen-widgetry-app" id="app">
|
||||
<div class="preloading"><h1>Widgetry Demo</h1></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
363
web/src/widgetry.ts
Normal file
363
web/src/widgetry.ts
Normal file
@ -0,0 +1,363 @@
|
||||
// WidgetryApp is a wrapper for a rust Widgetry app which has been compiled to a wasm package using wasm_bindgen.
|
||||
//
|
||||
// The type signatures of `InitInput` and `initializeWasm` were copy/pasted from the wasm_bindgen
|
||||
// generated ts.d files. They should be stable, unless wasm_bindgen has breaking changes.
|
||||
export type InitInput =
|
||||
| RequestInfo
|
||||
| URL
|
||||
| Response
|
||||
| BufferSource
|
||||
| WebAssembly.Module;
|
||||
|
||||
export abstract class WidgetryApp<InitOutput> {
|
||||
private appLoader: AppLoader<InitOutput>;
|
||||
private _assetsBaseURL: string;
|
||||
private _assetsAreGzipped: boolean;
|
||||
|
||||
public constructor(domId: string) {
|
||||
this.appLoader = new AppLoader(this, domId);
|
||||
|
||||
// Assume a default relative path to where we can find the "system" dir
|
||||
// Overide with `myApp.setAssetsBaseURL('path/to/dir')`
|
||||
this._assetsBaseURL = "./data";
|
||||
|
||||
// Assume files are gzipped unless on localhost.
|
||||
// Overide with `myApp.setAssetsAreGzipped(true)`
|
||||
this._assetsAreGzipped = !isLocalhost;
|
||||
}
|
||||
|
||||
public async loadAndStart() {
|
||||
this.appLoader.loadAndStart();
|
||||
}
|
||||
|
||||
// Assets (the "system" dir) are assumed to be at "./data" relative
|
||||
// to the current URL. Otherwise override with `setAssetsBaseURL`.
|
||||
public assetsBaseURL(): string {
|
||||
return this._assetsBaseURL;
|
||||
}
|
||||
|
||||
public setAssetsBaseURL(newValue: string) {
|
||||
this._assetsBaseURL = newValue;
|
||||
}
|
||||
|
||||
// Assets are assumed to gzipped, unless on localhost
|
||||
public assetsAreGzipped(): boolean {
|
||||
return this._assetsAreGzipped;
|
||||
}
|
||||
|
||||
public setAssetsAreGzipped(newValue: boolean) {
|
||||
this._assetsAreGzipped = newValue;
|
||||
}
|
||||
|
||||
abstract initializeWasm(
|
||||
module_or_path?: InitInput | Promise<InitInput>
|
||||
): Promise<InitOutput>;
|
||||
abstract run(
|
||||
rootDomId: string,
|
||||
assetsBaseURL: string,
|
||||
assetsAreGzipped: boolean
|
||||
): void;
|
||||
abstract wasmURL(): string;
|
||||
}
|
||||
|
||||
enum LoadState {
|
||||
unloaded,
|
||||
loading,
|
||||
loaded,
|
||||
starting,
|
||||
started,
|
||||
error,
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class used by `WidgetryApp` implementations to load their wasm and
|
||||
* render their content.
|
||||
*/
|
||||
export class AppLoader<T> {
|
||||
app: WidgetryApp<T>;
|
||||
el: HTMLElement;
|
||||
loadingEl?: HTMLElement;
|
||||
errorEl?: HTMLElement;
|
||||
domId: string;
|
||||
state: LoadState = LoadState.unloaded;
|
||||
// (receivedLength, totalLength)
|
||||
downloadProgress?: [number, number];
|
||||
errorMessage?: string;
|
||||
|
||||
public constructor(app: WidgetryApp<T>, domId: string) {
|
||||
this.app = app;
|
||||
this.domId = domId;
|
||||
const el = document.getElementById(domId);
|
||||
if (el === null) {
|
||||
throw new Error(`element with domId: ${domId} not found`);
|
||||
}
|
||||
this.el = el;
|
||||
console.log("sim constructor", this);
|
||||
}
|
||||
|
||||
public async loadAndStart() {
|
||||
this.render();
|
||||
try {
|
||||
await this.load();
|
||||
await this.start();
|
||||
} catch (e) {
|
||||
this.reportErrorState(e.toString());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
console.assert(this.state == LoadState.unloaded, "already loaded");
|
||||
this.updateState(LoadState.loading);
|
||||
|
||||
console.log("Started loading WASM");
|
||||
const t0 = performance.now();
|
||||
let response: Response = await fetch(this.app.wasmURL());
|
||||
|
||||
if (response.body == null) {
|
||||
this.reportErrorState("response.body was unexpectedly null");
|
||||
return;
|
||||
}
|
||||
let reader = response.body.getReader();
|
||||
|
||||
let contentLength = response.headers.get("Content-Length");
|
||||
if (contentLength == undefined) {
|
||||
this.reportErrorState(
|
||||
"unable to fetch wasm - contentLength was unexpectedly undefined"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status == 404) {
|
||||
this.reportErrorState(
|
||||
`server misconfiguration, wasm file not found: ${this.app.wasmURL()}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.downloadProgress = [0, parseInt(contentLength)];
|
||||
|
||||
let chunks: Uint8Array[] = [];
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
if (value == undefined) {
|
||||
console.error("reader value was unexpectedly undefined");
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
this.downloadProgress[0] += value.length;
|
||||
this.render();
|
||||
}
|
||||
let blob = new Blob(chunks);
|
||||
let buffer = await blob.arrayBuffer();
|
||||
const t1 = performance.now();
|
||||
console.log(`It took ${t1 - t0} ms to download WASM, now initializing it`);
|
||||
|
||||
// TODO: Prefer streaming instantiation where available (not safari)? Seems like it'd be faster.
|
||||
// const { instance } = await WebAssembly.instantiateStreaming(response, imports);
|
||||
|
||||
//let imports = {};
|
||||
//let instance = await WebAssembly.instantiate(bytes, imports);
|
||||
|
||||
await this.app.initializeWasm(buffer);
|
||||
this.updateState(LoadState.loaded);
|
||||
}
|
||||
|
||||
async start() {
|
||||
console.assert(this.state == LoadState.loaded, "not yet loaded");
|
||||
this.updateState(LoadState.starting);
|
||||
try {
|
||||
console.log(
|
||||
`running app with assetsBaseURL: ${this.app.assetsBaseURL()}, assetsAreGzipped: ${this.app.assetsAreGzipped()}`
|
||||
);
|
||||
this.app.run(
|
||||
this.domId,
|
||||
this.app.assetsBaseURL(),
|
||||
this.app.assetsAreGzipped()
|
||||
);
|
||||
} catch (e) {
|
||||
if (
|
||||
e.toString() ==
|
||||
"Error: Using exceptions for control flow, don't mind me. This isn't actually an error!"
|
||||
) {
|
||||
// This is an expected, albeit unfortunate, control flow mechanism for winit on wasm.
|
||||
this.updateState(LoadState.started);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isWebGL1Supported(): boolean {
|
||||
try {
|
||||
var canvas = document.createElement("canvas");
|
||||
return !!canvas.getContext("webgl");
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isWebGL2Supported(): boolean {
|
||||
try {
|
||||
var canvas = document.createElement("canvas");
|
||||
return !!canvas.getContext("webgl2");
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
updateState(newValue: LoadState) {
|
||||
console.debug(
|
||||
`state change: ${LoadState[this.state]} -> ${LoadState[newValue]}`
|
||||
);
|
||||
this.state = newValue;
|
||||
this.render();
|
||||
}
|
||||
|
||||
reportErrorState(errorMessage: string) {
|
||||
this.errorMessage = errorMessage;
|
||||
this.updateState(LoadState.error);
|
||||
}
|
||||
|
||||
// UI
|
||||
|
||||
render() {
|
||||
this.el.style.backgroundColor = "black";
|
||||
|
||||
switch (this.state) {
|
||||
case LoadState.loading: {
|
||||
if (this.loadingEl == undefined) {
|
||||
this.loadingEl = buildLoadingEl();
|
||||
// insert after rendering initial progress to avoid jitter.
|
||||
this.el.append(this.loadingEl);
|
||||
}
|
||||
|
||||
if (this.downloadProgress != undefined) {
|
||||
let received = this.downloadProgress[0];
|
||||
let total = this.downloadProgress[1];
|
||||
let progressText = `${prettyPrintBytes(
|
||||
received
|
||||
)} / ${prettyPrintBytes(total)}`;
|
||||
let percentText = `${(100.0 * received) / total}%`;
|
||||
this.loadingEl.querySelector<HTMLElement>(
|
||||
".widgetry-app-loader-progress-text"
|
||||
)!.innerText = progressText;
|
||||
this.loadingEl.querySelector<HTMLElement>(
|
||||
".widgetry-app-loader-progress-bar"
|
||||
)!.style.width = percentText;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case LoadState.error: {
|
||||
if (this.loadingEl != undefined) {
|
||||
this.loadingEl.remove();
|
||||
this.loadingEl = undefined;
|
||||
}
|
||||
|
||||
if (this.errorEl == undefined) {
|
||||
if (!this.isWebGL1Supported() && !this.isWebGL2Supported()) {
|
||||
this.errorMessage =
|
||||
this.errorMessage +
|
||||
"😭 Looks like your browser doesn't support WebGL.";
|
||||
}
|
||||
|
||||
if (this.errorMessage == undefined) {
|
||||
this.errorMessage =
|
||||
"An unknown error occurred. Try checking the developer console.";
|
||||
}
|
||||
|
||||
let el = buildErrorEl(this.errorMessage);
|
||||
this.errorEl = el;
|
||||
this.el.append(el);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function modRoot(importMetaURL: string): string {
|
||||
function dirname(path: string): string {
|
||||
return path.match(/.*\//)!.toString();
|
||||
}
|
||||
|
||||
let url = new URL(importMetaURL);
|
||||
url.pathname = dirname(url.pathname).toString();
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function buildLoadingEl(): HTMLElement {
|
||||
let loadingEl = document.createElement("div");
|
||||
loadingEl.innerHTML = `
|
||||
<style type="text/css">
|
||||
.widgetry-app-loader {
|
||||
color: white;
|
||||
padding: 16px;
|
||||
}
|
||||
.widgetry-app-loader-progress-bar-container {
|
||||
background-color: black;
|
||||
border: 1px solid white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.widgetry-app-loader-progress-bar {
|
||||
background-color: white;
|
||||
height: 12px;
|
||||
}
|
||||
.widgetry-app-loader-progress-text {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
<p><strong>Loading...</strong></p>
|
||||
<div class="widgetry-app-loader-progress-bar-container" style="width: 100%;">
|
||||
<div class="widgetry-app-loader-progress-bar" style="width: 1%;"></div>
|
||||
</div>
|
||||
<div class="widgetry-app-loader-progress-text">0 / 0</div>
|
||||
<p>If you think something has broken, check your browser's developer console (Ctrl+Shift+I or similar)</p>
|
||||
<p>(Your browser must support WebGL and WebAssembly)</p>
|
||||
`;
|
||||
loadingEl.setAttribute("class", "widgetry-app-loader");
|
||||
|
||||
return loadingEl;
|
||||
}
|
||||
|
||||
function buildErrorEl(errorMessage: string): HTMLElement {
|
||||
let el = document.createElement("p");
|
||||
el.innerHTML = `
|
||||
<style type="text/css">
|
||||
.widgetry-app-loader-error {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
<h2>Error Loading App</h2>
|
||||
${errorMessage}
|
||||
`;
|
||||
el.setAttribute("class", "widgetry-app-loader-error");
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function prettyPrintBytes(bytes: number): string {
|
||||
if (bytes < 1024 ** 2) {
|
||||
return Math.round(bytes / 1024) + " KB";
|
||||
}
|
||||
return Math.round(bytes / 1024 ** 2) + " MB";
|
||||
}
|
||||
|
||||
// courtesy: https://stackoverflow.com/a/57949518
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "0.0.0.0" ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === "[::1]" ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
20
web/src/widgetry_demo/widgetry_demo.ts
Normal file
20
web/src/widgetry_demo/widgetry_demo.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import * as wasm_pkg from "./wasm_pkg/widgetry_demo.js";
|
||||
let wasm_bg_path = "wasm_pkg/widgetry_demo_bg.wasm";
|
||||
|
||||
import { InitInput, modRoot, WidgetryApp } from "../widgetry.js";
|
||||
|
||||
export class WidgetryDemo extends WidgetryApp<wasm_pkg.InitOutput> {
|
||||
initializeWasm(
|
||||
module_or_path?: InitInput | Promise<InitInput>
|
||||
): Promise<wasm_pkg.InitOutput> {
|
||||
return wasm_pkg.default(module_or_path);
|
||||
}
|
||||
|
||||
run(rootDomId: string, assetsBaseURL: string, assetsAreGzipped: boolean) {
|
||||
wasm_pkg.run(rootDomId, assetsBaseURL, assetsAreGzipped);
|
||||
}
|
||||
|
||||
wasmURL(): string {
|
||||
return modRoot(import.meta.url) + wasm_bg_path;
|
||||
}
|
||||
}
|
@ -22,10 +22,17 @@ pub struct Assets {
|
||||
pub(crate) style: RefCell<Style>,
|
||||
pub text_opts: RefCell<Options>,
|
||||
pub read_svg: Box<dyn Fn(&str) -> Vec<u8>>,
|
||||
base_url: Option<String>,
|
||||
are_gzipped: bool,
|
||||
}
|
||||
|
||||
impl Assets {
|
||||
pub fn new(style: Style, read_svg: Box<dyn Fn(&str) -> Vec<u8>>) -> Assets {
|
||||
pub fn new(
|
||||
style: Style,
|
||||
base_url: Option<String>,
|
||||
are_gzipped: bool,
|
||||
read_svg: Box<dyn Fn(&str) -> Vec<u8>>,
|
||||
) -> Assets {
|
||||
// Many fonts are statically bundled with the library right now, on both native and web.
|
||||
// ctx.is_font_loaded and ctx.load_font can be used to dynamically add more later.
|
||||
let mut fontdb = fontdb::Database::new();
|
||||
@ -44,6 +51,8 @@ impl Assets {
|
||||
extra_fonts: RefCell::new(HashSet::new()),
|
||||
text_opts: RefCell::new(Options::default()),
|
||||
style: RefCell::new(style),
|
||||
base_url,
|
||||
are_gzipped,
|
||||
read_svg,
|
||||
};
|
||||
a.text_opts.borrow_mut().fontdb = fontdb;
|
||||
@ -78,6 +87,14 @@ impl Assets {
|
||||
a
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> Option<&str> {
|
||||
self.base_url.as_ref().map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn are_gzipped(&self) -> bool {
|
||||
self.are_gzipped
|
||||
}
|
||||
|
||||
pub fn is_font_loaded(&self, filename: &str) -> bool {
|
||||
self.extra_fonts.borrow().contains(filename)
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
use abstutil::Timer;
|
||||
|
||||
use crate::backend_glow::{build_program, GfxCtxInnards, PrerenderInnards, SpriteTexture};
|
||||
use crate::ScreenDims;
|
||||
use crate::{ScreenDims, Settings};
|
||||
|
||||
pub fn setup(
|
||||
window_title: &str,
|
||||
settings: &Settings,
|
||||
timer: &mut Timer,
|
||||
) -> (PrerenderInnards, winit::event_loop::EventLoop<()>) {
|
||||
let event_loop = winit::event_loop::EventLoop::new();
|
||||
let window = winit::window::WindowBuilder::new()
|
||||
.with_title(window_title)
|
||||
.with_title(&settings.window_title)
|
||||
.with_maximized(true);
|
||||
// TODO If people are hitting problems with context not matching what their GPU provides, dig up
|
||||
// backend_glium.rs from git and bring the fallback behavior here. (Ideally, there'd be
|
||||
|
@ -5,10 +5,10 @@ use winit::platform::web::WindowExtWebSys;
|
||||
use abstutil::Timer;
|
||||
|
||||
use crate::backend_glow::{build_program, GfxCtxInnards, PrerenderInnards, SpriteTexture};
|
||||
use crate::ScreenDims;
|
||||
use crate::{ScreenDims, Settings};
|
||||
|
||||
pub fn setup(
|
||||
window_title: &str,
|
||||
settings: &Settings,
|
||||
timer: &mut Timer,
|
||||
) -> (PrerenderInnards, winit::event_loop::EventLoop<()>) {
|
||||
info!("Setting up widgetry");
|
||||
@ -18,33 +18,32 @@ pub fn setup(
|
||||
error!("Panicked: {}", info);
|
||||
}));
|
||||
|
||||
let event_loop = winit::event_loop::EventLoop::new();
|
||||
let get_full_size = || {
|
||||
// TODO Not sure how to get scrollbar dims
|
||||
let scrollbars = 30.0;
|
||||
let win = web_sys::window().unwrap();
|
||||
// `inner_width` corresponds to the browser's `self.innerWidth` function, which are in
|
||||
// Logical, not Physical, pixels
|
||||
winit::dpi::LogicalSize::new(
|
||||
win.inner_width().unwrap().as_f64().unwrap() - scrollbars,
|
||||
win.inner_height().unwrap().as_f64().unwrap() - scrollbars,
|
||||
)
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
let root_element = document
|
||||
.get_element_by_id(&settings.root_dom_element_id)
|
||||
.expect("failed to find root widgetry element");
|
||||
|
||||
// Clear out any loading messages
|
||||
root_element.set_inner_html("");
|
||||
|
||||
let root_element_size = {
|
||||
let root_element = root_element.clone();
|
||||
move || {
|
||||
winit::dpi::LogicalSize::new(root_element.client_width(), root_element.client_height())
|
||||
}
|
||||
};
|
||||
|
||||
let event_loop = winit::event_loop::EventLoop::new();
|
||||
let winit_window = winit::window::WindowBuilder::new()
|
||||
.with_title(window_title)
|
||||
.with_inner_size(get_full_size())
|
||||
.with_title(&settings.window_title)
|
||||
.with_inner_size(root_element_size())
|
||||
.build(&event_loop)
|
||||
.unwrap();
|
||||
let canvas = winit_window.canvas();
|
||||
let window = web_sys::window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
let div = document
|
||||
.get_element_by_id("widgetry-canvas")
|
||||
.expect("no widgetry-canvas div");
|
||||
// Clear out any loading messages
|
||||
div.set_inner_html("");
|
||||
div.append_child(&canvas)
|
||||
.expect("can't append canvas to widgetry-canvas div");
|
||||
root_element
|
||||
.append_child(&canvas)
|
||||
.expect("failed to append canvas to widgetry root element");
|
||||
|
||||
let winit_window = Rc::new(winit_window);
|
||||
|
||||
@ -53,8 +52,7 @@ pub fn setup(
|
||||
let winit_window = winit_window.clone();
|
||||
let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |e: web_sys::Event| {
|
||||
debug!("handling resize event: {:?}", e);
|
||||
let size = get_full_size();
|
||||
winit_window.set_inner_size(size)
|
||||
winit_window.set_inner_size(root_element_size());
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
window
|
||||
.add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref())
|
||||
|
@ -284,6 +284,14 @@ impl Prerender {
|
||||
pub(crate) fn window_resized(&self, new_size: ScreenDims) {
|
||||
self.inner.window_resized(new_size, self.get_scale_factor())
|
||||
}
|
||||
|
||||
pub fn assets_base_url(&self) -> Option<&str> {
|
||||
self.assets.base_url()
|
||||
}
|
||||
|
||||
pub fn assets_are_gzipped(&self) -> bool {
|
||||
self.assets.are_gzipped()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::AsRef<Prerender> for GfxCtx<'_> {
|
||||
|
@ -161,7 +161,11 @@ impl<A: SharedAppState> State<A> {
|
||||
|
||||
/// Customize how widgetry works. These settings can't be changed after starting.
|
||||
pub struct Settings {
|
||||
window_title: String,
|
||||
pub(crate) window_title: String,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) root_dom_element_id: String,
|
||||
pub(crate) assets_base_url: Option<String>,
|
||||
pub(crate) assets_are_gzipped: bool,
|
||||
dump_raw_events: bool,
|
||||
scale_factor: Option<f64>,
|
||||
require_minimum_width: Option<f64>,
|
||||
@ -175,6 +179,10 @@ impl Settings {
|
||||
pub fn new(window_title: &str) -> Settings {
|
||||
Settings {
|
||||
window_title: window_title.to_string(),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
root_dom_element_id: "widgetry-canvas".to_string(),
|
||||
assets_base_url: None,
|
||||
assets_are_gzipped: false,
|
||||
dump_raw_events: false,
|
||||
scale_factor: None,
|
||||
require_minimum_width: None,
|
||||
@ -205,6 +213,12 @@ impl Settings {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn root_dom_element_id(mut self, element_id: String) -> Self {
|
||||
self.root_dom_element_id = element_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// If the screen width using the monitor's detected scale factor is below this value (in units
|
||||
/// of logical pixels, not physical), then force the scale factor to be 1. If `scale_factor()`
|
||||
/// has been called, always use that override. This is helpful for users with HiDPI displays at
|
||||
@ -238,6 +252,16 @@ impl Settings {
|
||||
self.read_svg = function;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assets_base_url(mut self, value: String) -> Self {
|
||||
self.assets_base_url = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn assets_are_gzipped(mut self, value: bool) -> Self {
|
||||
self.assets_are_gzipped = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run<
|
||||
@ -248,7 +272,7 @@ pub fn run<
|
||||
make_app: F,
|
||||
) -> ! {
|
||||
let mut timer = Timer::new("setup widgetry");
|
||||
let (prerender_innards, event_loop) = crate::backend::setup(&settings.window_title, &mut timer);
|
||||
let (prerender_innards, event_loop) = crate::backend::setup(&settings, &mut timer);
|
||||
|
||||
if let Some(ref path) = settings.window_icon {
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
@ -268,7 +292,12 @@ pub fn run<
|
||||
|
||||
let monitor_scale_factor = prerender_innards.monitor_scale_factor();
|
||||
let mut prerender = Prerender {
|
||||
assets: Assets::new(style.clone(), settings.read_svg),
|
||||
assets: Assets::new(
|
||||
style.clone(),
|
||||
settings.assets_base_url,
|
||||
settings.assets_are_gzipped,
|
||||
settings.read_svg,
|
||||
),
|
||||
num_uploads: Cell::new(0),
|
||||
inner: prerender_innards,
|
||||
scale_factor: settings.scale_factor.unwrap_or(monitor_scale_factor),
|
||||
|
@ -1,142 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script type="module">
|
||||
import { default as init } from './widgetry_demo.js';
|
||||
|
||||
function isWebGL1Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function isWebGL2Supported() {
|
||||
try {
|
||||
var canvas = document.createElement('canvas');
|
||||
return !!canvas.getContext('webgl2');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function prettyPrintBytes(bytes) {
|
||||
if (bytes < 1024 ** 2) {
|
||||
return Math.round(bytes / 1024) + " KB";
|
||||
}
|
||||
return Math.round(bytes / 1024 ** 2) + " MB";
|
||||
}
|
||||
|
||||
function main() {
|
||||
let webGL1Supported = isWebGL1Supported();
|
||||
let webGL2Supported = isWebGL2Supported();
|
||||
console.log("supports WebGL 1.0: " + webGL1Supported + ", WebGL 2.0: " + webGL2Supported);
|
||||
if (webGL1Supported || webGL2Supported) {
|
||||
fetchWithProgress();
|
||||
} else {
|
||||
showUnsupported();
|
||||
}
|
||||
}
|
||||
|
||||
function setElementVisibility(elementId, isVisible) {
|
||||
let el = document.getElementById(elementId);
|
||||
if (!el) {
|
||||
console.error("element missing: ", elementId);
|
||||
}
|
||||
if (isVisible) {
|
||||
el.style.display = "block";
|
||||
} else {
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function showUnsupported() {
|
||||
setElementVisibility('progress', false);
|
||||
setElementVisibility('unsupported', true);
|
||||
document.getElementById('unsupported-proceed-btn').onclick = function() {
|
||||
fetchWithProgress();
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWithProgress() {
|
||||
setElementVisibility('progress', true);
|
||||
setElementVisibility('unsupported', false);
|
||||
const t0 = performance.now();
|
||||
console.log("Started loading WASM");
|
||||
let response = await fetch('./widgetry_demo_bg.wasm');
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
const reader = response.body.getReader();
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
document.getElementById("progress-text").innerText = prettyPrintBytes(receivedLength) + " / " + prettyPrintBytes(contentLength);
|
||||
document.getElementById("progress-bar").style.width = (100.0 * receivedLength / contentLength) + "%";
|
||||
}
|
||||
document.getElementById("progress-text").innerText = "Loaded " + prettyPrintBytes(contentLength) + ", now initializing WASM module";
|
||||
let blob = new Blob(chunks);
|
||||
let buffer = await blob.arrayBuffer();
|
||||
const t1 = performance.now();
|
||||
console.log(`It took ${t1 - t0} ms to download WASM, now initializing it`);
|
||||
await init(buffer);
|
||||
}
|
||||
|
||||
main();
|
||||
</script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
#loading {
|
||||
background-color: #94C84A;
|
||||
padding: 40px;
|
||||
color: black;
|
||||
font-family: arial;
|
||||
border: solid black 3px;
|
||||
border-radius: 4px;
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
#progress-bar {
|
||||
/* complementary to #loading:background-color */
|
||||
background-color: #FF5733;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#loading h1 {
|
||||
text-align: center;
|
||||
}
|
||||
#unsupported-proceed-btn {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="widgetry-canvas"><div id="loading">
|
||||
<h1>Widgetry Demo</h1>
|
||||
<div id="progress" style="display: none">
|
||||
<h2>Loading...</h2>
|
||||
<div style="width: 100%; background-color: white;">
|
||||
<div style="width: 1%; height: 30px;" id="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-text"></div>
|
||||
<p>If you think something has broken, check your browser's developer console (Ctrl+Shift+I or similar)</p>
|
||||
<p>(Your browser must support WebGL and WebAssembly)</p>
|
||||
</div>
|
||||
<div id="unsupported" style="display: none;">
|
||||
<h2>😭 Looks like your browser doesn't support WebGL.</h2>
|
||||
|
||||
<button id="unsupported-proceed-btn" type="button">Load Anyway</button>
|
||||
<p><strong>This will surely fail unless you enable WebGL first.</strong></p>
|
||||
</div>
|
||||
</div></div>
|
||||
</body>
|
||||
<html>
|
@ -1 +0,0 @@
|
||||
../index.html
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
wasm-pack build --dev --target web -- --no-default-features --features wasm
|
||||
cd pkg
|
||||
python3 -m http.server 8000
|
@ -7,30 +7,33 @@ use geom::{Angle, Duration, Percent, Polygon, Pt2D, Time};
|
||||
use widgetry::{
|
||||
lctrl, Choice, Color, ContentMode, Drawable, EventCtx, Fill, GeomBatch, GfxCtx,
|
||||
HorizontalAlignment, Image, Key, Line, LinePlot, Outcome, Panel, PersistentSplit, PlotOptions,
|
||||
ScreenDims, Series, SharedAppState, State, TabController, Text, TextExt, Texture, Toggle,
|
||||
Transition, UpdateType, VerticalAlignment, Widget,
|
||||
ScreenDims, Series, Settings, SharedAppState, State, TabController, Text, TextExt, Texture,
|
||||
Toggle, Transition, UpdateType, VerticalAlignment, Widget,
|
||||
};
|
||||
|
||||
pub fn main() {
|
||||
// Use this to initialize logging.
|
||||
abstutil::CmdArgs::new().done();
|
||||
|
||||
let settings = Settings::new("widgetry demo");
|
||||
run(settings);
|
||||
}
|
||||
|
||||
fn run(mut settings: Settings) {
|
||||
settings = settings.read_svg(Box::new(abstio::slurp_bytes));
|
||||
// Control flow surrendered here. App implements State, which has an event handler and a draw
|
||||
// callback.
|
||||
//
|
||||
// TODO The demo loads a .svg file, so to make it work on both native and web, for now we use
|
||||
// read_svg. But we should have a more minimal example of how to do that here.
|
||||
widgetry::run(
|
||||
widgetry::Settings::new("widgetry demo").read_svg(Box::new(abstio::slurp_bytes)),
|
||||
|ctx| {
|
||||
// TODO: remove Style::pregame and make light_bg the default.
|
||||
ctx.set_style(widgetry::Style::light_bg());
|
||||
// TODO: Add a toggle to switch theme in demo (and recreate UI in that new theme)
|
||||
// ctx.set_style(widgetry::Style::dark_bg());
|
||||
widgetry::run(settings, |ctx| {
|
||||
// TODO: remove Style::pregame and make light_bg the default.
|
||||
ctx.set_style(widgetry::Style::light_bg());
|
||||
// TODO: Add a toggle to switch theme in demo (and recreate UI in that new theme)
|
||||
// ctx.set_style(widgetry::Style::dark_bg());
|
||||
|
||||
(App {}, vec![Box::new(Demo::new(ctx))])
|
||||
},
|
||||
);
|
||||
(App {}, vec![Box::new(Demo::new(ctx))])
|
||||
});
|
||||
}
|
||||
|
||||
struct App {}
|
||||
@ -608,7 +611,13 @@ fn make_controls(ctx: &mut EventCtx, tabs: &mut TabController) -> Panel {
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run() {
|
||||
main();
|
||||
#[wasm_bindgen(js_name = "run")]
|
||||
pub fn run_wasm(root_dom_id: String, assets_base_url: String, assets_are_gzipped: bool) {
|
||||
// Use this to initialize logging.
|
||||
abstutil::CmdArgs::new().done();
|
||||
let settings = Settings::new("widgetry demo")
|
||||
.root_dom_element_id(root_dom_id)
|
||||
.assets_base_url(assets_base_url)
|
||||
.assets_are_gzipped(assets_are_gzipped);
|
||||
run(settings);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user