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:
Michael Kirk 2021-04-01 19:31:02 -07:00 committed by GitHub
parent 20de91bae7
commit 4f81f186af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1299 additions and 921 deletions

View File

@ -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())

View File

@ -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>

View File

@ -1 +0,0 @@
../index.html

View File

@ -1 +0,0 @@
../../data/system/

View File

@ -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

View File

@ -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);
}

View File

@ -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>

View File

@ -1 +0,0 @@
../../data/system/assets/pregame/favicon.ico

View File

@ -1 +0,0 @@
../index.html

View File

@ -1 +0,0 @@
../prefetch.html

View File

@ -1 +0,0 @@
../../data/system/

View File

@ -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

View File

@ -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);
}

View File

@ -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>

View File

@ -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>

View File

@ -1 +0,0 @@
../index.html

View File

@ -1 +0,0 @@
../../data/system/

View File

@ -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

View File

@ -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);
}

View File

@ -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"

View File

@ -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>

View File

@ -1 +0,0 @@
../index.html

View File

@ -1 +0,0 @@
../../data/system/

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,2 @@
node_modules
build

117
web/Makefile Normal file
View 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
View 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
View 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
View 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
View 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
View 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"
}
}

View 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;
}
}

View 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;
}
}

View 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
View 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;
}
}

View 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;
}

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View 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>

View 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>

View 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>

View 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>

View 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
View 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}$/
)
);

View 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;
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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())

View File

@ -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<'_> {

View File

@ -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),

View File

@ -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>

View File

@ -1 +0,0 @@
../index.html

View File

@ -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

View File

@ -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);
}