Commit 9f5e2051 authored by Simon Goller's avatar Simon Goller
Browse files

Using TypeScript to load a level from SVG

parent 951651d3
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "anyhow"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4"
[[package]]
name = "approx"
version = "0.4.0"
......@@ -446,12 +452,14 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
name = "rc3-muccc"
version = "0.1.0"
dependencies = [
"anyhow",
"console_error_panic_hook",
"js-sys",
"legion",
"nalgebra",
"ncollide2d",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
......@@ -586,6 +594,18 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fe9756085a84584ee9457a002b7cdfe0bfff169f45d2591d8be1345a6780e35"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.69"
......
......@@ -13,9 +13,11 @@ path = "src/main.rs"
[dependencies]
js-sys = "0.3.46"
wasm-bindgen = "0.2.69"
wasm-bindgen-futures = "0.4.19"
nalgebra = "0.23.1"
ncollide2d = "0.26.1"
console_error_panic_hook = "0.1.6"
anyhow = { version = "1.0", default-features = false }
[dependencies.legion]
version = "0.3.1"
......@@ -37,4 +39,8 @@ features = [
"Performance",
"MouseEvent",
"DomRect",
"HtmlObjectElement",
"Response",
"ReadableStream",
"HtmlImageElement"
]
# What is this?
This will at some point be a game, written in Rust, using WebAssembly.
# Hacking
- [Project Structure](#project-structure)
- [Build Dependencies](#build-dependencies)
......@@ -10,7 +9,8 @@ This will at some point be a game, written in Rust, using WebAssembly.
## Project Structure
The project is mainly split into two parts: The game logic, written in Rust,
living in `src/`, and the web-frontend in `www/src/` which is mainly HTML and
SCSS + a bit of JS glue code.
SCSS + a bit of JS glue code. Additionally, some helper custom javascript
functions which are called in rust are defined in `rust-web-modules/`.
#### `src/` - Rust Game Logic
- `src/main.rs`: Entry-point, world setup, and game loop
......@@ -25,7 +25,6 @@ SCSS + a bit of JS glue code.
- `www/src/styles.scss`: Stylesheet using Sass
- `www/src/_rc3-*`: Styles and Fonts for RC3
## Build Dependencies
Not quite sure what the dependencies really are, please add
as you find out:
......@@ -57,6 +56,7 @@ wasm-pack build
# For the webpack dev server:
cd www/
npm run rust-web-modules
npm run start
```
......
let loadingSequence = 0;
export function load_svg(url: string): Promise<string> {
return new Promise((success) =>
fetch(url).then((response) =>
response.text().then((svgText) => {
const loadingDiv = document.createElement("div");
const loadingDivId = `loading-div-${loadingSequence}`
loadingDiv.setAttribute("id", loadingDivId);
loadingSequence += 1;
loadingDiv.innerHTML = svgText;
document.body.appendChild(loadingDiv);
success(loadingDivId);
})
)
);
}
export function clean_svg(loadingId: string) {
document.getElementById(loadingId).remove();
}
export function extract_svg_code(loadingId: string): string | null {
const loadingDiv = document.getElementById(loadingId);
if (!loadingDiv) {
return null;
}
return loadingDiv.innerHTML;
}
export function hide_canvas_node_by_query(loadingId: string, query: string) {
document.getElementById(loadingId).querySelectorAll(query)
.forEach((elment: HTMLElement) => elment.style.display = "none");
}
export function show_canvas_node_by_query(loadingId: string, query: string) {
document.getElementById(loadingId).querySelectorAll(query)
.forEach((elment: HTMLElement) => elment.style.display = "block");
}
export function convert_svg_to_html_image(loadingId: string): Promise<HTMLImageElement> {
return new Promise((success, fail) => {
const image = new Image();
const base64Canvas = "data:image/svg+xml;base64," + btoa(extract_svg_code(loadingId));
if (base64Canvas === null) {
return fail();
}
image.onload = () => {
success(image);
};
image.onerror = event => {
fail(event);
}
image.src = base64Canvas;
});
}
export function convert_svg_to_image(loadingId: string, width: number, height: number): Promise<HTMLImageElement> {
return new Promise((success, fail) => {
convert_svg_to_html_image(loadingId)
.then(svgImage => {
const canvas = document.createElement("canvas") as HTMLCanvasElement;
canvas.width = width;
canvas.height = height;
const image = new Image();
const ctx = canvas.getContext("2d");
ctx.drawImage(svgImage, 0, 0);
image.onload = () => {
success(image);
};
image.onerror = event => {
fail(event);
};
image.src = canvas.toDataURL("image/png");
}).catch(e => fail(e));
});
}
\ No newline at end of file
......@@ -5,11 +5,13 @@ pub mod colliders;
pub mod components;
pub mod resources;
pub mod utils;
pub mod svg_loader;
#[wasm_bindgen(start)]
pub fn start() {
pub async fn start() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
let level = svg_loader::SvgLevel::load_from_svg_file("testlevel.svg", 1000, 1000).await;
let canvas = utils::document().get_element_by_id("game-canvas").unwrap();
let canvas: web_sys::HtmlCanvasElement = canvas
.dyn_into::<web_sys::HtmlCanvasElement>()
......@@ -90,6 +92,7 @@ pub fn start() {
let world_rc = std::rc::Rc::new(std::cell::RefCell::new(world));
let resources_rc = std::rc::Rc::new(std::cell::RefCell::new(resources));
utils::event_handler(
|fun| {
......@@ -140,9 +143,16 @@ pub fn start() {
.unwrap();
ctx.set_fill_style(&"#100e23".into());
ctx.fill_rect(0.0, 0.0, 1920.0, 1080.0);
}
ctx.draw_image_with_html_image_element(&level.background_image, 0.0, 0.0).unwrap();
}
schedule.execute(&mut world_rc.borrow_mut(), &mut *resources);
{
let ctx = &mut *resources
.get_mut::<web_sys::CanvasRenderingContext2d>()
.unwrap();
ctx.draw_image_with_html_image_element(&level.foreground_image, 0.0, 0.0).unwrap();
}
}
});
}
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys;
use js_sys;
use wasm_bindgen_futures;
#[wasm_bindgen(raw_module = "./svg-level-loader")]
extern "C" {
async fn load_svg(url: &str) -> JsValue;
fn clean_svg(loading_id: &str);
fn hide_canvas_node_by_query(loading_id: &str, query: &str);
fn show_canvas_node_by_query(loading_id: &str, query: &str);
async fn convert_svg_to_image(loading_id: &str, width: u32, height: u32) -> JsValue;
}
pub struct SvgLoader {
loading_id: String
}
impl SvgLoader {
pub async fn load_svg(url: &str) -> Self {
let loading_id = load_svg(url).await.as_string().unwrap();
SvgLoader { loading_id }
}
pub fn hide_canvas_node_by_query(&self, query: &str) {
hide_canvas_node_by_query(&self.loading_id, query)
}
pub fn show_canvas_node_by_query(&self, query: &str) {
show_canvas_node_by_query(&self.loading_id, query)
}
pub async fn convert_svg_to_image(&self, width: u32, height: u32) -> web_sys::HtmlImageElement {
convert_svg_to_image(&self.loading_id, width, height).await.dyn_into().unwrap()
}
}
impl Drop for SvgLoader {
fn drop(&mut self) {
//clean_svg(&self.loading_id)
}
}
pub struct SvgLevel {
pub foreground_image: web_sys::HtmlImageElement,
pub background_image: web_sys::HtmlImageElement,
}
impl SvgLevel {
pub async fn load_from_svg_file(url: &str, width: u32, height: u32) -> Self{
let svg_loader = SvgLoader::load_svg(url).await;
svg_loader.hide_canvas_node_by_query("g[inkscape\\:groupmode=\"layer\"]");
svg_loader.show_canvas_node_by_query("g[inkscape\\:groupmode=\"layer\"][inkscape\\:label=\"background\"]");
let background_image = svg_loader.convert_svg_to_image(width, height).await;
svg_loader.hide_canvas_node_by_query("g[inkscape\\:groupmode=\"layer\"][inkscape\\:label=\"background\"]");
svg_loader.show_canvas_node_by_query("g[inkscape\\:groupmode=\"layer\"][inkscape\\:label=\"foreground\"]");
let foreground_image = svg_loader.convert_svg_to_image(width, height).await;
svg_loader.hide_canvas_node_by_query("g[inkscape\\:groupmode=\"layer\"][inkscape\\:label=\"foreground\"]");
let svg_level = SvgLevel{foreground_image, background_image};
svg_level
}
}
......@@ -4418,8 +4418,7 @@
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true,
"optional": true
"dev": true
},
"pify": {
"version": "3.0.0",
......@@ -5894,6 +5893,70 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true
},
"ts-loader": {
"version": "8.0.11",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.11.tgz",
"integrity": "sha512-06X+mWA2JXoXJHYAesUUL4mHFYhnmyoCdQVMXofXF552Lzd4wNwSGg7unJpttqUP7ziaruM8d7u8LUB6I1sgzA==",
"dev": true,
"requires": {
"chalk": "^2.3.0",
"enhanced-resolve": "^4.0.0",
"loader-utils": "^1.0.2",
"micromatch": "^4.0.0",
"semver": "^6.0.0"
},
"dependencies": {
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"micromatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
"integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.0.5"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
}
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
......@@ -5922,6 +5985,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"typescript": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz",
"integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
"dev": true
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
......
......@@ -5,7 +5,9 @@
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server"
"start": "webpack-dev-server",
"all": "wasm-pack build && webpack-dev-server",
"rust-web-modules": "tsc ../rust-web-modules/svg-level-loader.ts --outFile ../pkg/svg-level-loader.js --module amd"
},
"author": "Rahix <rahix@rahix.de>",
"license": "GPL-3.0-or-later",
......@@ -20,6 +22,8 @@
"sass": "^1.30.0",
"sass-loader": "^10.1.0",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.11",
"typescript": "^4.1.2",
"webpack": "^4.44.2",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5"
......
// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import("./index.js")
.catch(e => console.error("Error importing `index.js`:", e));
import("./index.ts").catch((e) =>
console.error("Error importing `index.js`:", e)
);
import * as wasm from "rc3-muccc";
import "./styles.scss";
// wasm.greet("rahix");
wasm.start();
"use strict";
exports.__esModule = true;
exports.convert_svg_to_image = exports.convert_svg_to_html_image = exports.show_canvas_node_by_query = exports.hide_canvas_node_by_query = exports.extract_svg_code = exports.clean_svg = exports.load_svg = void 0;
var loadingSequence = 0;
function load_svg(url) {
return new Promise(function (success) {
return fetch(url).then(function (response) {
return response.text().then(function (svgText) {
var loadingDiv = document.createElement("div");
var loadingDivId = "loading-div-" + loadingSequence;
loadingDiv.setAttribute("id", loadingDivId);
loadingSequence += 1;
loadingDiv.innerHTML = svgText;
document.body.appendChild(loadingDiv);
success(loadingDivId);
});
});
});
}
exports.load_svg = load_svg;
function clean_svg(loadingId) {
document.getElementById(loadingId).remove();
}
exports.clean_svg = clean_svg;
function extract_svg_code(loadingId) {
var loadingDiv = document.getElementById(loadingId);
if (!loadingDiv) {
return null;
}
return loadingDiv.innerHTML;
}
exports.extract_svg_code = extract_svg_code;
function hide_canvas_node_by_query(loadingId, query) {
document.getElementById(loadingId).querySelectorAll(query)
.forEach(function (elment) { return elment.style.display = "none"; });
}
exports.hide_canvas_node_by_query = hide_canvas_node_by_query;
function show_canvas_node_by_query(loadingId, query) {
document.getElementById(loadingId).querySelectorAll(query)
.forEach(function (elment) { return elment.style.display = "block"; });
}
exports.show_canvas_node_by_query = show_canvas_node_by_query;
function convert_svg_to_html_image(loadingId) {
return new Promise(function (success, fail) {
var image = new Image();
var base64Canvas = "data:image/svg+xml;base64," + btoa(extract_svg_code(loadingId));
if (base64Canvas === null) {
return fail();
}
image.onload = function () {
success(image);
};
image.onerror = function (event) {
fail(event);
};
image.src = base64Canvas;
});
}
exports.convert_svg_to_html_image = convert_svg_to_html_image;
function convert_svg_to_image(loadingId, width, height) {
return new Promise(function (success, fail) {
convert_svg_to_html_image(loadingId)
.then(function (svgImage) {
var canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
var image = new Image();
var ctx = canvas.getContext("2d");
ctx.drawImage(svgImage, 0, 0);
image.onload = function () {
success(image);
};
image.onerror = function (event) {
fail(event);
};
image.src = canvas.toDataURL("image/png");
})["catch"](function (e) { return fail(e); });
});
}
exports.convert_svg_to_image = convert_svg_to_image;
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="testlevel.svg"
inkscape:version="1.0beta1 (32d4812, 2019-09-19)"
id="svg8"
version="1.1"
viewBox="0 0 210 297"
height="297mm"
width="210mm">
<defs
id="defs2" />
<sodipodi:namedview
inkscape:window-maximized="0"
inkscape:window-y="26"
inkscape:window-x="0"
inkscape:window-height="1325"
inkscape:window-width="2560"
showgrid="false"
inkscape:document-rotation="0"
inkscape:current-layer="layer1"
inkscape:document-units="mm"
inkscape:cy="520.39441"
inkscape:cx="-861.23074"
inkscape:zoom="0.57155355"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="display:inline"
inkscape:label="background"
id="layer3"
inkscape:groupmode="layer">
<rect
y="165.2623"
x="120.82201"
height="64.345818"
width="71.289619"
id="rect856"
style="fill:#00ff25;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-opacity:1" />
</g>
<g
id="layer1"
inkscape:groupmode="layer"
inkscape:label="colliders">
<ellipse
ry="27.080797"
rx="28.469553"
cy="141.88484"
cx="95.129974"
id="path858"
style="fill:#ff00ce;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-opacity:1" />
</g>
<g
style="display:inline"
inkscape:label="foreground"
id="layer2"
inkscape:groupmode="layer">
<rect
y="58.790787"
x="28.238092"
height="62.031223"
width="76.381729"
id="rect854"
style="fill:#ff0000;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-opacity:1" />
</g>
</svg>
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es2020",
"target": "es5",
"allowJs": true
}
}
\ No newline at end of file
......@@ -9,23 +9,22 @@ module.exports = {
},
mode: "development",
plugins: [
new CopyWebpackPlugin(["src/index.html"]),
],
plugins: [new CopyWebpackPlugin(["src/index.html"])],
module: {
rules: [
{
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
]
use: ["style-loader", "css-loader", "sass-loader"],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
use: ["file-loader"],
},