A tiny WASM animation using Perlin Noise
View Demo
Click the canvas to toggle between 3 different visualization modes.
1. About
2. Dev + Build
3. What I Did
[Step 1] Creating Source Directory
[Step 2] Creating Build Directory
[Step 3] Writing build.sh
[Step 4] Subdirectory Issue
[Step 5] Creating a Symlink
[Step 6] wasm-loader
[Step 7] import.meta
(or file://
)
[Step 8] application/wasm
4. Installed NPM Packages
4-1. All
4-2. Babel
4-3. Webpack
4-4. Other Build Tools
5. References
6. LICENSE
Update: 2023.4.15
If you want the latest WASM app using Perlin noise,
check out perlin-experiment-2.
Instead of using JS for canvas animation, the whole thing
is written in WASM for this new version.
Or, if you want a simpler WASM app for your learning, check out
very-simple-wasm-2023.
While majority of the codes are dedicated for animations, this app intends:
- to demonstrate the use of wasm-pack to build a WASM app,
- to bind the WASM app to specific DOM element(s), and
- to pass configurations from JS to the WASM app.
Here are the features:
- Handles 2 DOM elements:
#wave
and#control
- Generates organic looking waves using Perlin Noise
- Clicking the canvas to toggle between 3 modes: Wave, Equalizer, and Solar
- Displays the current amplitude value in control panel
The key is to make a symlink from JS to the WASM package.
Without the symlink, Webpack is NOT able to find the package.
In our case, for Webpack to locate perlin-wave
, I must run:
yarn link "perlin-wave"
To get a grasp of what I mean, take a look at the app structure:
├── build.sh
├── public
│ ├── assets
│ │ │ # This is the Webpack output
│ │ │ # directory where JS, CSS,
│ │ │ # or any other resources
│ │ │ # will be served.
│ │ │
│ │ ├── app.xxxxxxxxxxxxxxxxxxxx.js
│ │ └── favicon.ico
│ │
│ │ # While `index.html` is usually
│ │ # emitted to Webpack output directory,
│ │ # instead, emitting `index.html`
│ │ # to its UPPER DIRECTORY.
│ │
│ ├── index.html
│ │
│ └── wasm
│ └── perlin-wave
│ │ # After `cargo build`, we run
│ │ # `wasm-pack` in `build.sh`
│ │ # and output the generated
│ │ # set of package here.
│ │
│ ├── package.json
│ ├── perlin-wave_bg.wasm
│ ├── perlin-wave_bg.wasm.d.ts
│ ├── perlin-wave.d.ts
│ └── perlin-wave.js
│
├── src
│ # This is where JS codes reside.
│
└── src_for_wasm
│ # This is where Rust codes reside.
│
└── perlin-wave
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── app.rs
│ ├── constants.rs
│ ├── graphics
│ │ ├── mod.rs
│ │ ├── control.rs
│ │ └── wave.rs
│ ├── lib.rs # This is the module root
│ ├── panels
│ │ ├── mod.rs
│ │ ├── control.rs
│ │ └── wave.rs
│ ├── perlin.rs
│ ├── types.rs
│ └── utils.rs
│
└── target
│ # This is where `cargo build`
│ # will build WASM binaries.
│
└── wasm32-unknown-unknown
### IMPORTANT
After you clone the repo, make sure that you:
1. Build the WASM app
2. Make a symlink
git clone https://github.com/minagawah/perlin-experiment.git
cd perlin-experiment
yarn build:wasm
cd public/wasm/perlin-wave
yarn link
cd ../../../src
yarn link "perlin-wave"
Further details described in:
# 3. What I Did - [Step 5] Creating a Symlink
Once you have the WASM built and the symlink, then you are all set for development.
Run the following:
yarn start
it will first run a debug build for WASM. Once it is done, it will start a devServer at:
This will build for both JS and WASM:
yarn build
Many steps are similar to my previous example, iced-dynamic-import-sample, but using wasm-pack this time instead of using wasm-bindgen.
When imporing WASM from JS, we need special preparations.
I will describe the steps in detail:
First of all, I need a source directory:
mkdir src_for_wasm
cargo new perlin-wave
cd perlin-wave
Secondly, I need a build directory.
When running cargo build
, it will emit binaries under:
src_for_wasm/perlin-wave/target/wasm32-unknown-unknown/release/*.wasm
wasm-pack
will use these files to generate a package under:
public/wasm/perlin-wave
So, I must create one.
mkdir -p public/wasm/perlin-wave
By running yarn build:wasm
, I am runing:
sh ./build.sh perlin-wave release
Let's have a look at build.sh
:
#!/usr/bin/env bash
APP=${1%\/}
PROFILE=${2:=debug}
ROOT_DIR="$PWD"
SRC_DIR="$ROOT_DIR/src_for_wasm/$APP"
OUT_DIR="$ROOT_DIR/public/wasm/$APP"
cd "$SRC_DIR"
wasm-pack build "--$PROFILE" --target web --out-name "$APP" --out-dir "$OUT_DIR"
Running build.sh
will generate the following files:
$ ls -1 public/wasm/perlin-wave/
package.json
perlin-wave_bg.wasm
perlin-wave_bg.wasm.d.ts
perlin-wave.d.ts
perlin-wave.js
public/wasm/perlin-wave/package.json
:
{
"name": "perlin-wave",
"collaborators": [],
"version": "0.1.0",
"files": ["perlin-wave_bg.wasm", "perlin-wave.js", "perlin-wave.d.ts"],
"module": "perlin-wave.js",
"types": "perlin-wave.d.ts",
"sideEffects": false
}
Now, let me talk about serving path...
Let's first take a look at HTML:
<div id="container">
<div id="wrapper">
<div id="wave"></div>
<div id="control"></div>
</div>
</div>
and JS:
import init, * as PerlinWave from 'perlin-wave';
import './styles.css';
const WASM_PATH =
NODE_ENV && NODE_ENV === 'production'
? 'wasm/perlin-wave/perlin-wave_bg.wasm'
: void 0;
const COLOR = '#c0e822';
const COLOR_DARK = '#759203';
const APP_CONFIG = {
bgcolor: '#222',
panels: [
{
id: 'control',
ratio: 15.0 / 1.0,
color: COLOR,
color2: COLOR_DARK,
},
{
id: 'wave',
ratio: 3.0 / 1.0,
color: COLOR,
color2: COLOR_DARK,
},
].reduce(panelsReducer, []),
};
document.addEventListener('DOMContentLoaded', () => {
init(WASM_PATH)
.then(() => {
PerlinWave.run(APP_CONFIG);
})
.catch(err => {
console.error(err);
});
});
if (typeof module.hot !== 'undefined') {
module.hot.accept();
}
function panelsReducer(acc = [], { id, ratio, color, color2 }) {
const key = `#${id}`;
const el = document.querySelector(key);
if (el) {
const width = (el.offsetWidth || 0).toFixed(1);
const height = (width / ratio).toFixed(1);
acc.push({
id,
color,
color2,
width,
height,
});
el.style.height = `${height}px`;
}
return acc;
}
You will notice it mainly does nothing about WASM app,
but just calculates width
and height
.
However, if you take a closer look,
you will notice a weird constant WASM_PATH
being passed as an argument.
What the heck is this all about?
Why is it passing WASM_PATH
as an argument?
Well, this is about the directory you want to serve your assets from.
If I were to serve my assets from site's directory root, there is no need for passing the argument.
Like this:
init()
.then(() => {
//
However, I am serving all the assets from the following subdirectory:
http://tokyo800.jp/mina/perlin-wave/
Let's say we feed nothing for init
, then it would fetch following (which results in 404):
http://tokyo800.jp/perlin-wave/perlin-wave_bg.wasm
Instead, I want this:
http://tokyo800.jp/mina/perlin-experiment/wasm/perlin-wave/perlin-wave_bg.wasm
When the path is not given, it will fetch for: /perlin-wave/perlin-wave_bg.wasm
This is not even close to /mina/perlin-waves
where I have my assets!!!!
So, the point is, when a path is not given, it will fetch for the ABSOLUTE PATH as a default.
To avoid this, I must explicity pass a RELATIVE PATH which is wasm/perlin-wave/perlin-wave_bg.wasm
‐‐
Also, I need to be careful with what to set for publicPath
in Webpack config.
Currently, I have this:
output: {
filename: '[name].[fullhash].js',
path: path.resolve(__dirname, 'public/assets'),
publicPath: 'assets',
},
Meaning, I am serving the JS assets from: http://tokyo800.jp/mina/perlin-experiment/assets
What if, instead of assets
, I had /assets
?
Well... That would be disasterous...
When my HTML page is generated, it will look like this:
<script src="/assets/app.f459b8e437cbd3fa6595.js">
As you can see, it will result in 404...
So, if you had your assets in a subdirectory,
make sure you have a RELATIVE PATH for publicPath
.
‐‐
Notice, also, that I am explicitly specifying
void 0
(which is another way of saying undefined
)
when we are not passing a path to the WASM file.
This does not usually become an issue,
but it may sometimes become a cause of raising
a runtime error especially when we are using
canvas related features.
We need to explicitly pass undefined
because a predicate defined in init
strictly checks against undefined
.
Otherwise, when initializing the WASM app, in:
WebAssembly.instantiate(bytes, imports);
the first argument becomes empty.
‐‐
Also, for those of you don't know, Webpack5 was released on Jan. 12, 2021,
and it no longer supports process.env
,
and you need to manually define envs like NODE_ENV
yourself in Webpack config.
In order for JS to lookup perlin-wave
as a module (not runtime, but build-time),
we are using yarn link
to create a symlink within src
so that it links to public/wasm/perlin-wave
.
Notice that we have "perlin-wave" for the package name
because is defined so in public/wasm/perlin-wave/package.json
(which we generated with build.sh
).
cd public/wasm/perlin-wave
yarn link
--------------------------------------------
yarn link v1.22.5
warning package.json: No license field
warning package.json: No license field
success Registered "perlin-wave".
info You can now run `yarn link "perlin-wave"` in the projects where you want to use this package and it will be used instead.
Done in 0.08s.
--------------------------------------------
Now, visit src
, and link it to the one just registered:
cd src
yarn link "perlin-wave"
We need more setups to handle *.wasm
files.
First of all, we must tell Webpack, instead of file-loader
, but to use wasm-loader
:
webpack.base.js
{
test: /\.wasm$/,
include: path.resolve('public/wasm'),
use: [
{
loader: require.resolve('wasm-loader'),
},
],
},
Now, if you take a look at public/wasm/perlin-wave/perlin-wave.js
,
init
function starts like this:
async function init(input) {
if (typeof input === 'undefined') {
input = import.meta.url.replace(/\.js$/, '_bg.wasm');
}
Notice import.meta
syntax for which Webpack has no idea how to deal with it.
So, we will add babel-plugin-bundled-import-meta
in babel.config.js
to tell Webpack that we are importing *.wasm
files as CJS modules.
babel.config.js
plugins: [
[
'babel-plugin-bundled-import-meta',
{
bundleDir: 'public/wasm',
importStyle: 'cjs',
},
],
],
This is not just about import.meta
,
but this is about explicitly telling Webpack how to dynamically import *.wasm
.
Without it, Webpack attempts to fetch it via file://
, and that is not what we want.
Also, notice bundleDir: 'public/wasm'
is needed
only when the directory you have WASM files are differnt from your Webpack output directory.
Remember that we are using webpack-dev-server
in development.
And, you certainly need to print a MIME header for *.wasm
files.
webpack.dev.js
devServer: {
contentBase: path.resolve(__dirname, './public'),
hot: true,
port: 8080,
// Access to `/assets` should resolve (without 404)
writeToDisk: true,
before: app => {
app.get('*.wasm', (req, res, next) => {
const options = {
root: path.join(__dirname, 'public/wasm'),
dotfiles: 'deny',
headers: {
'Content-Type': 'application/wasm',
},
};
res.sendFile(req.url, options, err => {
if (err) {
console.warn(err);
next(err);
}
});
});
},
},
Make sure also that you have .htaccess
(or whatever for clouds)
in your production server to print application/wasm
for any *.wasm
files served.
Here is what I have:
% cat .htaccess
AddType application/wasm .wasm
Order Deny,Allow
Deny From All
Allow From All
DirectoryIndex index.html index.php
yarn add --dev @babel/core @babel/preset-env @babel/cli core-js@3 @babel/runtime-corejs3 babel-loader babel-plugin-bundled-import-meta webpack webpack-cli webpack-dev-server file-loader css-loader style-loader postcss-loader wasm-loader autoprefixer webpack-merge clean-webpack-plugin html-webpack-plugin copy-webpack-plugin mini-css-extract-plugin license-webpack-plugin prettier pretty-quick
For @babel/polyfill
has been deprecated, we use core-js
.
- @babel/core
- @babel/cli
- @babel/preset-env
useBuiltIns: 'usage'
inbabel.config.js
will automatically insert polyfills.
- core-js@3
- For
@babel/polyfill
has been deprecated.
- For
- @babel/runtime-corejs3
- babel-loader
- We want Babel to read
.babelrc
(orbabel.config.js
).
- We want Babel to read
- babel-plugin-bundled-import-meta
- WASM package uses
import.meta
syntax to dynamically import*.wasm
files, and we want Babel to treat them as CJS modules. Also, we want to avoid the files to be fetched viafile://
.
- WASM package uses
yarn add --dev @babel/core @babel/preset-env @babel/cli core-js@3 @babel/runtime-corejs3 babel-loader babel-plugin-bundled-import-meta
- webpack
- webpack-cli
- webpack-dev-server
- file-loader
- css-loader
- style-loader
- This is for development only. For production, we are using
mini-css-extract-plugin
.
- This is for development only. For production, we are using
- postcss-loader
- wasm-loader
- Instead of
file-loader
.
- Instead of
- autoprefixer
- webpack-merge
- clean-webpack-plugin
- html-webpack-plugin
- Template is in
src/index.html
, and outputspublic/index.html
.
- Template is in
- copy-webpack-plugin
- Just to copy
src/assets
topublic/assets
.
- Just to copy
- mini-css-extract-plugin
- While we are extracting CSS files, and write them to disks, this is for production only.
- license-webpack-plugin
- Extracts license information for production.
yarn add --dev webpack webpack-cli webpack-dev-server file-loader css-loader style-loader postcss-loader wasm-loader autoprefixer webpack-merge clean-webpack-plugin html-webpack-plugin copy-webpack-plugin mini-css-extract-plugin license-webpack-plugin
- prettier
- pretty-quick
yarn add --dev prettier pretty-quick
- The Perlin noise math FAQ
https://mzucker.github.io/html/perlin-noise-math-faq.html - Perlin noise (article) | Noise | Khan Academy
https://www.khanacademy.org/computing/computer-programming/programming-natural-simulations/programming-noise/a/perlin-noise - Understanding Perlin Noise
https://adrianb.io/2014/08/09/perlinnoise.html - Perlin Noise: A Procedural Generation Algorithm
https://rtouti.github.io/graphics/perlin-noise-algorithm
Dual-licensed under either of the followings.
Choose at your option.
- The UNLICENSE (LICENSE.UNLICENSE)
- MIT license (LICENSE.MIT)